diff --git a/.github/workflows/finalise-release.yml b/.github/workflows/finalise-release.yml index 31cad3fa..9f07c7bc 100644 --- a/.github/workflows/finalise-release.yml +++ b/.github/workflows/finalise-release.yml @@ -21,6 +21,7 @@ jobs: runs-on: ubuntu-latest environment: release permissions: + actions: write contents: write steps: - name: Resolve release branch @@ -78,6 +79,22 @@ jobs: git tag "${VERSION}-cli" git push origin "${VERSION}" "${VERSION}-cli" + - name: Dispatch release workflows + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + gh workflow run release.yml \ + --ref "${VERSION}" \ + --field tag="${VERSION}" \ + --field draft=true \ + --field prerelease=false + gh workflow run cli-docker.yml \ + --ref "${VERSION}-cli" \ + --field dry_run=false \ + --field force=false + - name: Summarise next steps env: VERSION: ${{ inputs.version }} @@ -85,8 +102,8 @@ jobs: { echo "Created tags \`${VERSION}\` and \`${VERSION}-cli\`." echo "" - echo "The plug-in release workflow is triggered by the \`${VERSION}\` tag and creates a draft release by default." - echo "The CLI Docker workflow is triggered by the \`${VERSION}-cli\` tag and publishes the version, major-minor, latest, and SHA-qualified image tags." + echo "Dispatched the plug-in release workflow for \`${VERSION}\`. After approval for the release environment, it creates a draft GitHub Release." + echo "Dispatched the CLI Docker workflow for \`${VERSION}-cli\`. It publishes the version, major-minor, latest, and SHA-qualified image tags." echo "" - echo "To create a pre-release instead of the default draft flow, run \`Release Obsidian Plugin\` manually with \`tag=${VERSION}\`, \`draft=false\`, and \`prerelease=true\`." + echo "Keep the release pull request in draft after publishing the GitHub Release as the latest stable release. Mark it ready and merge it only after BRAT validation succeeds." } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index a69cff49..25505d14 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -91,6 +91,11 @@ jobs: RELEASE_BRANCH: ${{ steps.prepare.outputs.branch }} run: | cat > /tmp/release-pr-body.md < [!IMPORTANT] + > **Merge intentionally on hold** + > + > Publishing the GitHub Release does not unblock this pull request. Keep this pull request in draft, and leave \`main\` on the previous release, until the published build has passed BRAT validation. + ## Release checklist - [ ] Review and polish \`updates.md\` @@ -98,7 +103,10 @@ jobs: - [ ] Confirm \`manifest.json\`, \`versions.json\`, workspace package versions, and generated \`_types\` - [ ] Confirm CI has passed - [ ] Run the finalise release workflow with this PR's fixed head SHA - - [ ] Merge this PR with a merge commit after the draft or pre-release has been created + - [ ] Confirm the draft GitHub Release assets and the published CLI image + - [ ] Publish the GitHub Release as the latest stable release while keeping this pull request in draft + - [ ] Validate the published release with BRAT + - [ ] Mark this pull request ready and merge it with a merge commit EOF gh pr create \ diff --git a/devs.md b/devs.md index 7ac3a447..d066bc60 100644 --- a/devs.md +++ b/devs.md @@ -274,9 +274,12 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - Run the `Prepare Release PR` workflow with the target version. It creates the release branch, updates versions, regenerates the `_types` fallback definitions used by the community plug-in scan, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. - Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`. -- Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch and pushes both the plug-in tag (for example, `0.25.81`) and the CLI tag (for example, `0.25.81-cli`) to that commit. -- The plug-in tag triggers the release workflow and creates a draft GitHub Release by default. The CLI tag triggers the Docker workflow and publishes the fixed version tag, the major-minor moving tag (for example, `0.25-cli`), `latest`, and the SHA-qualified tag. -- Merge the release PR with a merge commit after the draft or pre-release has been created. This keeps the tagged release commit in the `main` history. +- Once the release PR head is fixed, run the `Finalise Release Tags` workflow with its full head commit SHA. It validates the release branch, pushes both the plug-in tag (for example, `0.25.81`) and the CLI tag (for example, `0.25.81-cli`) to that commit, and explicitly dispatches the plug-in and CLI publishing workflows. An explicit dispatch is required because tags pushed with `GITHUB_TOKEN` do not trigger tag-push workflows. +- Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. Confirm that the CLI workflow has published the fixed version tag, the major-minor moving tag (for example, `0.25-cli`), `latest`, and the SHA-qualified tag. +- Publish the draft GitHub Release as the latest stable release while keeping the release PR in draft and leaving `main` unchanged. Record the state in the PR with: 'Release `` has been published as the latest stable release. This pull request intentionally remains in draft, and `main` has not yet been updated. Merge is on hold until BRAT validation is complete.' +- Validate the published release through BRAT. Confirm start-up, ordinary bidirectional synchronisation, and any regression scenario relevant to the release. +- After BRAT validation succeeds, mark the release PR ready and merge it with a merge commit. This keeps the tagged release commit in the `main` history. +- If BRAT validation fails, keep the release PR in draft. Do not move published tags; prepare and publish a new patch release instead. - If a pre-release is needed, run the `Release Obsidian Plugin` workflow manually with the target tag, `draft=false`, and `prerelease=true`. ### Release Cheat Sheet @@ -297,10 +300,14 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel - `version`: the same target version. - `release_branch`: leave blank unless the release branch used a custom name. - `expected_head_sha`: the full head commit SHA reviewed in the release PR. -5. Check the generated draft GitHub Release for the plug-in tag. -6. Check the CLI Docker workflow started from the `*-cli` tag. -7. Publish the draft GitHub Release when ready, then merge the release PR into `main` with a merge commit. -8. If the release should be a pre-release instead of a draft release, run `Release Obsidian Plugin` manually with the target `tag`, `draft=false`, and `prerelease=true`. +5. Approve the `Release Obsidian Plugin` workflow for the `release` environment, then check the generated draft GitHub Release. +6. Confirm that the explicitly dispatched CLI Docker workflow has published the expected image tags. +7. Publish the draft GitHub Release as the latest stable release, but keep the release PR in draft and leave `main` unchanged. +8. Update the PR state message to say that the release is now the latest stable release and that merging is intentionally on hold until BRAT validation is complete. +9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario. +10. After BRAT validation succeeds, mark the release PR ready and merge it into `main` with a merge commit. +11. If validation fails, leave the PR in draft and prepare a new patch release without moving the published tags. +12. If the release should be a pre-release instead of a draft release, run `Release Obsidian Plugin` manually with the target `tag`, `draft=false`, and `prerelease=true`. ## Contribution Guidelines diff --git a/utils/release-process.unit.spec.ts b/utils/release-process.unit.spec.ts index b6803bae..cc50248f 100644 --- a/utils/release-process.unit.spec.ts +++ b/utils/release-process.unit.spec.ts @@ -10,6 +10,7 @@ const versionBumpScript = process.env.VERSION_BUMP_SCRIPT || fileURLToPath(new URL("../version-bump.mjs", import.meta.url)); const workspaceUpdateScript = fileURLToPath(new URL("../update-workspaces.mjs", import.meta.url)); const prepareReleaseWorkflow = fileURLToPath(new URL("../.github/workflows/prepare-release.yml", import.meta.url)); +const finaliseReleaseWorkflow = fileURLToPath(new URL("../.github/workflows/finalise-release.yml", import.meta.url)); const temporaryDirectories: string[] = []; afterEach(() => { @@ -111,6 +112,26 @@ describe("release workflow", () => { expect(workflow).toContain("npm run build:lib:types"); expect(workflow).toMatch(/git add[^\n]*_types/); }); + + it("keeps the release PR in draft until BRAT validation", () => { + const workflow = readFileSync(prepareReleaseWorkflow, "utf8"); + + expect(workflow).toContain("Merge intentionally on hold"); + expect(workflow).toContain( + "Publish the GitHub Release as the latest stable release while keeping this pull request in draft" + ); + expect(workflow).toContain("Validate the published release with BRAT"); + expect(workflow).toContain("Mark this pull request ready and merge it with a merge commit"); + }); + + it("explicitly dispatches publishing workflows after creating tags", () => { + const workflow = readFileSync(finaliseReleaseWorkflow, "utf8"); + + expect(workflow).toContain("actions: write"); + expect(workflow).toContain("gh workflow run release.yml"); + expect(workflow).toContain("gh workflow run cli-docker.yml"); + expect(workflow).toContain("dry_run=false"); + }); }); describe("version bump", () => {