diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e30a5cc22c..0ef16299fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,10 @@ name: CI on: push: - branches: [master, 11.0_release] + branches: [master, maintenance/*] pull_request: - branches: [master, 11.0_release] + branches: [master, maintenance/*] # Release builds are started by publish.yml after a maintainer indicates # whether the package version is current stable. The npm tag is derived from @@ -32,6 +32,7 @@ jobs: build-compiler: outputs: api-docs-artifact-id: ${{ steps.upload-api-docs.outputs.artifact-id }} + website-branch: ${{ steps.api-docs-plan.outputs.website-branch }} strategy: fail-fast: false matrix: @@ -381,6 +382,11 @@ jobs: !packages/@rescript/runtime/lib/ocaml/*.ast !packages/@rescript/runtime/lib/ocaml/*.iast + - name: Select API docs website branch + id: api-docs-plan + if: ${{ matrix.generate_api_docs }} + run: node scripts/releasePlan.js --github-output "$GITHUB_OUTPUT" + - name: Generate API Docs if: ${{ matrix.generate_api_docs }} run: yarn apidocs:generate @@ -488,6 +494,7 @@ jobs: uses: actions/checkout@v7 with: repository: rescript-lang/rescript-lang.org + ref: ${{ needs.build-compiler.outputs.website-branch }} - name: Download artifacts uses: actions/download-artifact@v8 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e42924a2a7..fb3be6c787 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,6 +19,7 @@ jobs: version: ${{ steps.plan.outputs.version }} npm-tag: ${{ steps.plan.outputs.npm-tag }} publish-tag: ${{ steps.plan.outputs.publish-tag }} + website-branch: ${{ steps.plan.outputs.website-branch }} steps: - name: Checkout uses: actions/checkout@v7 @@ -156,6 +157,7 @@ jobs: uses: actions/checkout@v7 with: repository: rescript-lang/rescript-lang.org + ref: ${{ needs.release-plan.outputs.website-branch }} path: rescript-lang.org ssh-key: ${{ secrets.RESCRIPT_LANG_ORG_DEPLOY_KEY }} @@ -194,12 +196,16 @@ jobs: - name: Publish API docs if: steps.api-docs-diff.outputs.changed == 'true' + env: + WEBSITE_BRANCH: ${{ needs.release-plan.outputs.website-branch }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions@rescript-lang.org" git add data/api git commit -m "Update API docs for v${{ needs.release-plan.outputs.version }}" - git push + # The deploy key lets this push trigger the website branch's + # deployment workflow. + git push origin "HEAD:$WEBSITE_BRANCH" working-directory: rescript-lang.org - name: Update Website Playground diff --git a/changes/versioned-api-docs.internal.md b/changes/versioned-api-docs.internal.md new file mode 100644 index 0000000000..e3c3823f82 --- /dev/null +++ b/changes/versioned-api-docs.internal.md @@ -0,0 +1,3 @@ +- Route API documentation checks and publication to the website branch matching + the compiler major (`v12`, `v13`, and later), keeping the target unchanged when + a release becomes current stable or moves to a maintenance branch. diff --git a/scripts/releasePlan.js b/scripts/releasePlan.js index 058d69f88c..f2e1267e97 100644 --- a/scripts/releasePlan.js +++ b/scripts/releasePlan.js @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import process from "node:process"; +import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; import semver from "semver"; @@ -36,6 +37,7 @@ export function createReleasePlan(version, currentStable = false) { currentStable, npmTag, publishTag: currentStable ? null : npmTag, + websiteBranch: `v${parsedVersion.major}`, }; } @@ -112,6 +114,7 @@ async function main() { `git-tag=${plan.gitTag}`, `npm-tag=${plan.npmTag}`, `publish-tag=${plan.publishTag ?? ""}`, + `website-branch=${plan.websiteBranch}`, ].join("\n"); await fs.appendFile(values["github-output"], `${output}\n`); } @@ -125,6 +128,7 @@ async function main() { `| Version | \`${plan.version}\` |`, `| Git tag | \`${plan.gitTag}\` |`, `| npm tag | \`${plan.npmTag}\` |`, + `| Website branch | \`${plan.websiteBranch}\` |`, `| Source commit | \`${process.env.GITHUB_SHA ?? "local"}\` |`, "", "The protected `npm-release` job publishes this plan after approval.", @@ -136,7 +140,10 @@ async function main() { console.log(JSON.stringify(plan, null, 2)); } -if (import.meta.url === `file://${process.argv[1]}`) { +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { main().catch(error => { console.error(error.message || error); process.exitCode = 1; diff --git a/scripts/releasePlan.test.js b/scripts/releasePlan.test.js index 18b01d168c..41d244b652 100644 --- a/scripts/releasePlan.test.js +++ b/scripts/releasePlan.test.js @@ -1,14 +1,26 @@ import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; +import { fileURLToPath } from "node:url"; import { createReleasePlan, findFirstReleasedVersion, findPendingFragments, } from "./releasePlan.js"; +const websiteTargets = [ + ["12.3.1", "v12"], + ["12.4.0-alpha.1", "v12"], + ["13.0.0-alpha.7", "v13"], + ["13.0.0", "v13"], + ["13.0.1", "v13"], + ["14.0.0-alpha.1", "v14"], + ["14.0.0", "v14"], +]; + test("plans a maintenance release", () => { assert.deepEqual(createReleasePlan("12.3.0"), { version: "12.3.0", @@ -16,6 +28,7 @@ test("plans a maintenance release", () => { currentStable: false, npmTag: "latest-12", publishTag: "latest-12", + websiteBranch: "v12", }); }); @@ -23,6 +36,7 @@ test("plans a current stable release", () => { const plan = createReleasePlan("13.0.0", true); assert.equal(plan.npmTag, "latest"); assert.equal(plan.publishTag, null); + assert.equal(plan.websiteBranch, "v13"); }); test("derives the prerelease tag", () => { @@ -33,6 +47,100 @@ test("derives the prerelease tag for an older major", () => { assert.equal(createReleasePlan("12.4.0-beta.1").npmTag, "next-12"); }); +test("routes stable and prerelease API docs by compiler major", () => { + for (const [version, branch] of websiteTargets) { + assert.equal(createReleasePlan(version).websiteBranch, branch); + } +}); + +test("promoting a release changes its npm tag without moving its docs", () => { + for (const [version, branch] of websiteTargets) { + if (version.includes("-")) continue; + const maintenance = createReleasePlan(version); + const stable = createReleasePlan(version, true); + assert.notEqual(maintenance.npmTag, stable.npmTag); + assert.equal(maintenance.websiteBranch, branch); + assert.equal(stable.websiteBranch, branch); + } +}); + +test("exports the selected website branch for release workflows", async t => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "rescript-api-docs-")); + t.after(() => fs.rm(root, { recursive: true, force: true })); + const script = fileURLToPath(new URL("./releasePlan.js", import.meta.url)); + + for (const [version, branch] of websiteTargets) { + const output = path.join(root, `${version}.output`); + const summary = path.join(root, `${version}.summary`); + const result = spawnSync( + process.execPath, + [ + script, + "--version", + version, + "--github-output", + output, + "--github-summary", + summary, + ], + { encoding: "utf8" }, + ); + assert.equal(result.status, 0, result.stderr); + assert.ok( + (await fs.readFile(output, "utf8")).includes( + `website-branch=${branch}\n`, + ), + ); + assert.ok( + (await fs.readFile(summary, "utf8")).includes( + `| Website branch | \`${branch}\` |`, + ), + ); + } +}); + +test("keeps CI routing when master advances and a maintenance branch is cut", async t => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "rescript-api-docs-")); + t.after(() => fs.rm(root, { recursive: true, force: true })); + const cases = [ + ["maintenance/12.x", "12.3.1", "v12"], + ["master", "13.0.0-alpha.7", "v13"], + ["maintenance/13.x", "13.0.1", "v13"], + ["master", "14.0.0-alpha.1", "v14"], + ]; + for (const [index, [ref, version, branch]] of cases.entries()) { + await fs.writeFile( + path.join(root, "package.json"), + JSON.stringify({ version }), + ); + const output = path.join(root, `github-output-${index}`); + const result = spawnSync( + process.execPath, + [ + fileURLToPath(new URL("./releasePlan.js", import.meta.url)), + "--github-output", + output, + ], + { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + GITHUB_REF: `refs/heads/${ref}`, + GITHUB_REF_NAME: ref, + }, + }, + ); + assert.equal(result.status, 0, result.stderr); + assert.ok( + (await fs.readFile(output, "utf8")).includes( + `website-branch=${branch}\n`, + ), + `${ref} at ${version} should use ${branch}`, + ); + } +}); + test("rejects promoting a prerelease to current stable", () => { assert.throws( () => createReleasePlan("13.0.0-alpha.6", true),