Skip to content

Commit bdd34c7

Browse files
committed
Create git tags and GitHub releases after publishing
When changesets/action published packages itself, it also pushed a name@version tag and created a GitHub release with the changelog entry for each package. Publishing through ESRP dropped both. Recreate them from the publish manifest once publishing finishes, so a release looks the same as before. This reuses the GitHub App token template from tslib's release pipeline, since release jobs have no checkout to run the token CLI from.
1 parent b590177 commit bdd34c7

4 files changed

Lines changed: 752 additions & 0 deletions

File tree

‎build/azure-pipelines/pack-changed-packages.mjs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ function versionChanged(pkg) {
100100
return previous?.version !== pkg.packageJson.version;
101101
}
102102

103+
// Match changesets/action, which uses the package's CHANGELOG.md section for
104+
// the version as the GitHub release body.
105+
function readReleaseNotes(pkg) {
106+
const changelogPath = path.join(root, pkg.dir, "CHANGELOG.md");
107+
if (!fs.existsSync(changelogPath)) return "";
108+
109+
const lines = fs.readFileSync(changelogPath, "utf8").split(/\r?\n/);
110+
const start = lines.findIndex(line => line.trim() === `## ${pkg.packageJson.version}`);
111+
if (start === -1) return "";
112+
113+
let end = lines.findIndex((line, i) => i > start && /^##?\s/.test(line));
114+
if (end === -1) end = lines.length;
115+
return lines.slice(start + 1, end).join("\n").trim();
116+
}
117+
103118
function findChangedPackageNames() {
104119
const releaseBranch = `refs/heads/${readJson(path.join(root, ".changeset/config.json")).baseBranch}`;
105120
const sourceBranch = process.env.BUILD_SOURCEBRANCH;
@@ -157,6 +172,7 @@ for (const name of packedNames) {
157172
name,
158173
version: packedPackage.version,
159174
filename,
175+
releaseNotes: readReleaseNotes(packages.get(name)),
160176
});
161177
}
162178

‎build/azure-pipelines/release-helper.mjs‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,84 @@ async function prepareStage(stage) {
6767
setVariable(`PublishStage${stage}`, pending > 0);
6868
}
6969

70+
function requireEnv(name) {
71+
const value = process.env[name];
72+
if (!value) throw new Error(`${name} is required.`);
73+
return value;
74+
}
75+
76+
async function githubRequest(route, method = "GET", body) {
77+
const repository = requireEnv("GITHUB_REPOSITORY");
78+
const response = await fetch(`https://api.github.com/repos/${repository}/${route}`, {
79+
method,
80+
headers: {
81+
Accept: "application/vnd.github+json",
82+
Authorization: `Bearer ${requireEnv("GH_TOKEN")}`,
83+
"Content-Type": "application/json",
84+
"X-GitHub-Api-Version": "2022-11-28",
85+
},
86+
body: body === undefined ? undefined : JSON.stringify(body),
87+
});
88+
if (method === "GET" && response.status === 404) return undefined;
89+
if (!response.ok) {
90+
throw new Error(`${method} ${route}: ${response.status} ${await response.text()}`);
91+
}
92+
return response.json();
93+
}
94+
95+
async function ensureTag(tag, commit) {
96+
const ref = await githubRequest(`git/ref/tags/${tag}`);
97+
if (!ref) {
98+
await githubRequest("git/refs", "POST", { ref: `refs/tags/${tag}`, sha: commit });
99+
console.log(`Created tag ${tag}.`);
100+
return;
101+
}
102+
103+
let object = ref.object;
104+
while (object.type === "tag") {
105+
object = (await githubRequest(`git/tags/${object.sha}`)).object;
106+
}
107+
if (object.type !== "commit" || object.sha !== commit) {
108+
throw new Error(`Tag ${tag} already exists but does not point to ${commit}.`);
109+
}
110+
console.log(`Tag ${tag} already exists.`);
111+
}
112+
113+
async function ensureRelease(tag, pkg) {
114+
const release = await githubRequest(`releases/tags/${tag}`);
115+
if (release) {
116+
console.log(`GitHub release ${tag} already exists: ${release.html_url}`);
117+
return;
118+
}
119+
120+
const created = await githubRequest("releases", "POST", {
121+
tag_name: tag,
122+
name: tag,
123+
body: pkg.releaseNotes,
124+
prerelease: pkg.version.includes("-"),
125+
});
126+
console.log(`Created GitHub release: ${created.html_url}`);
127+
}
128+
129+
// Recreate what changesets/action does after `changeset publish`: a
130+
// name@version tag and a GitHub release for every published package.
131+
async function finalize() {
132+
const commit = requireEnv("RELEASE_SOURCE_COMMIT");
133+
for (const pkg of manifest.stages.flat()) {
134+
const tag = spec(pkg);
135+
await ensureTag(tag, commit);
136+
await ensureRelease(tag, pkg);
137+
}
138+
}
139+
70140
const [command, ...args] = process.argv.slice(2);
71141
switch (command) {
72142
case "prepare-stage":
73143
await prepareStage(Number(args[0]));
74144
break;
145+
case "finalize":
146+
await finalize();
147+
break;
75148
default:
76149
throw new Error(`Unknown command ${command}`);
77150
}

‎build/azure-pipelines/release.yml‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ variables:
1212
ReleaseOwners: "jabaile@microsoft.com"
1313
ReleaseApprovers: "alexchi@microsoft.com"
1414
NpmArtifactName: npm-packages
15+
GitHubRepository: microsoft/TypeScript-Website
16+
TYPESCRIPT_AUTOMATION_GITHUB_APP_CLIENT_ID: Iv23li4GolzJSEp1mzHI
17+
TYPESCRIPT_AUTOMATION_GITHUB_APP_KEY_ID: https://jststeam-passwords.vault.azure.net/keys/typescript-automation
1518

1619
parameters:
1720
- name: publishStages
@@ -144,3 +147,41 @@ extends:
144147
waitForReleaseCompletion: true
145148
owners: "$(ReleaseOwners)"
146149
approvers: "$(ReleaseApprovers)"
150+
151+
- stage: Finalize
152+
displayName: Create tags and GitHub releases
153+
dependsOn:
154+
- Build
155+
- Publish
156+
condition: and(succeeded(), eq(dependencies.Build.outputs['Build.packChangedPackages.ShouldPublish'], 'true'))
157+
jobs:
158+
- job: CreateGitHubReleases
159+
displayName: Create tags and GitHub releases
160+
templateContext:
161+
type: releaseJob
162+
isProduction: true
163+
inputs:
164+
- input: pipelineArtifact
165+
artifactName: $(NpmArtifactName)
166+
targetPath: $(Pipeline.Workspace)/$(NpmArtifactName)
167+
steps:
168+
- checkout: none
169+
170+
- task: UseNode@1
171+
inputs:
172+
version: "22.x"
173+
displayName: "Install Node.js"
174+
175+
- template: /build/azure-pipelines/steps/create-github-app-token.yml@self
176+
parameters:
177+
azureSubscription: TypeScript-DevDiv-KeyVault
178+
repositories: TypeScript-Website
179+
permissions: contents:write
180+
insertSteps:
181+
- script: node "$(Pipeline.Workspace)/$(NpmArtifactName)/release-helper.mjs" finalize
182+
displayName: Create tags and GitHub releases
183+
env:
184+
GH_TOKEN: $(GH_TOKEN)
185+
GITHUB_REPOSITORY: $(GitHubRepository)
186+
RELEASE_SOURCE_COMMIT: $(Build.SourceVersion)
187+
NODE_USE_ENV_PROXY: "1"

0 commit comments

Comments
 (0)