diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 510b6109db..9d4c6f80bb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,8 @@ name: Release on: + push: + branches: [main] workflow_dispatch: inputs: release_branch: @@ -18,12 +20,27 @@ env: HUSKY: "0" concurrency: - group: release-${{ inputs.release_branch }} + group: release-${{ inputs.release_branch || github.ref_name }} cancel-in-progress: false jobs: + release-proposal: + name: Update release proposal + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + target-branch: main + skip-github-release: true + release-please: name: Release Please + if: github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca5fff69df..a36554490b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,11 +138,13 @@ Describe the behavior change and verification performed in the pull request. Nev ## Maintainer release checklist -Use the [checkpoint release guide](docs/releasing.md). Releases start from an -explicit `release/VERSION` branch cut at a recorded commit. Manually prepare -the bot PR, wait for its exact-head CI and review, merge into that frozen -branch, and manually publish through the official Release workflow. Main -pushes do not update release PRs or publish packages. Both publishers build +Use the [checkpoint release guide](docs/releasing.md). Main pushes automatically +refresh a release proposal with the next version and changelog. Keep this preview +PR unmerged until it is superseded by the published release's metadata sync. +Publication starts from an explicit `release/VERSION` branch cut at a recorded +application commit. Manually prepare its bot PR, wait for its exact-head CI and +review, merge into that frozen branch, and manually publish through the official +Release workflow. Main pushes cannot publish packages. Both publishers build the exact SHA tagged by Release Please. Keep new implementation commits on main for the next release. The workflow diff --git a/docs/releasing.md b/docs/releasing.md index a8859808da..26dc8ee206 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,8 +1,12 @@ # Releasing from a checkpoint -REA releases use an explicit source checkpoint. Main can continue accepting -changes while a release is tested and published. The Release workflow runs -only when a maintainer dispatches it; main pushes do not refresh release PRs. +Pushes to main automatically open or refresh a Release Please proposal with +the next version and changelog. This proposal cannot create a GitHub release +or publish packages. It previews the next release as main continues changing. + +Publication uses an explicit source checkpoint. A maintainer selects a frozen +release branch and dispatches preparation and publication, so main can keep +accepting changes without changing the candidate being tested. ## 1. Select the source @@ -11,6 +15,12 @@ breaking changes. Record the full source SHA and create `release/VERSION` at that commit. The version is a maintainer choice; Release Please's proposed version must agree before publication. +Use the automatic main proposal to review the suggested version and notes. +Keep that proposal unmerged: select the application commit from main, create +the frozen branch, and prepare its own release PR using the steps below. +Merging the proposal into main would advance the release baseline before +publication. The reviewed frozen-branch PR is the publication candidate. + `release/VERSION` also sets the expected version for the workflow. Use an exact SemVer such as `release/6.1.0` or `release/6.1.0-rc.1`, without build metadata. The checkpoint validator rejects a candidate whose package, lockfile, manifest, @@ -163,8 +173,10 @@ git fetch origin main --tags git merge-base --is-ancestor rea-agents-5.0.0 origin/main ``` -Close any superseded rolling release PR. Future releases repeat the checkpoint -procedure from main; never resume automatic release-PR refreshes on main pushes. +Close the superseded main proposal before merging the synchronization PR. +That merge's main push opens a proposal for the following release using the +updated baseline. Future publication repeats the checkpoint procedure; +automatic proposals do not move frozen branches. Preparation generates and validates the catalog, portable conformance projections, and packaged skill from the candidate checkout without committing them. The checkpoint validator binds tracked version authority to the source diff --git a/tests/boundary/filesystem/releaseWorkflow.test.ts b/tests/boundary/filesystem/releaseWorkflow.test.ts index bb0e86b782..aff2fbb7be 100644 --- a/tests/boundary/filesystem/releaseWorkflow.test.ts +++ b/tests/boundary/filesystem/releaseWorkflow.test.ts @@ -27,7 +27,9 @@ async function readReleaseWorkflow() { return z .object({ on: z.record(z.string(), z.unknown()), + concurrency: z.object({ group: z.string() }), jobs: z.object({ + "release-proposal": jobSchema, "release-please": jobSchema, publish: jobSchema, "publish-mcp": jobSchema, @@ -80,9 +82,33 @@ it("validates the source before release creation and the prepared candidate befo } }); -it("requires explicit release preparation or publication instead of main pushes", async () => { +it("refreshes a release proposal on main pushes without creating or publishing a release", async () => { + const workflow = await readReleaseWorkflow(); + expect(workflow.on.push).toEqual({ branches: ["main"] }); + const proposal = workflow.jobs["release-proposal"]; + expect(proposal.if).toBe("github.event_name == 'push'"); + const action = proposal.steps.find((step) => + step.uses?.startsWith("googleapis/release-please-action@"), + ); + expect(action?.with).toMatchObject({ + token: "${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}", + "target-branch": "main", + "skip-github-release": true, + }); + expect(action?.with?.["skip-github-pull-request"]).not.toBe(true); + expect(workflow.jobs["release-please"].if).toBe( + "github.event_name == 'workflow_dispatch'", + ); + for (const job of [workflow.jobs.publish, workflow.jobs["publish-mcp"]]) { + expect(job.if).toContain("inputs.phase == 'publish'"); + } + expect(workflow.concurrency.group).toBe( + "release-${{ inputs.release_branch || github.ref_name }}", + ); +}); + +it("keeps frozen candidate preparation and publication explicit", async () => { const workflow = await readReleaseWorkflow(); - expect(Object.keys(workflow.on)).toEqual(["workflow_dispatch"]); expect(workflow.on.workflow_dispatch).toMatchObject({ inputs: { release_branch: { required: true, type: "string" },