From 7fdadd8d9e2e972e114478976b6b2e6b78156238 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 23 Aug 2026 14:05:25 +0900 Subject: [PATCH] Create release tags in upstream CI Signed-off-by: LunaStev --- .github/workflows/release.yml | 114 +++++++++++++++++++++++++-------- README.md | 13 ++-- RELEASING.md | 74 +++++++++++---------- tests/xpy/test_release_tool.py | 14 +++- 4 files changed, 147 insertions(+), 68 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d692bd..8bff603 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,7 @@ name: Release on: - push: - tags: - - "v*" workflow_dispatch: - inputs: - tag: - description: Existing annotated release tag (for example, v0.0.1) - required: true - type: string permissions: contents: read @@ -18,7 +10,7 @@ env: CARGO_TERM_COLOR: always concurrency: - group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} + group: release-${{ github.repository }}-${{ github.ref_name }} cancel-in-progress: false jobs: @@ -27,11 +19,12 @@ jobs: runs-on: ubuntu-24.04 outputs: tag: ${{ steps.release.outputs.tag }} + commit: ${{ steps.release.outputs.commit }} steps: - - name: Check out release tag + - name: Check out release commit uses: actions/checkout@v7 with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }} + ref: ${{ github.sha }} fetch-depth: 0 - name: Install Rust toolchain @@ -44,13 +37,40 @@ jobs: with: python-version: "3.11" - - name: Verify tag and source tree + - name: Verify authoritative release source id: release shell: bash run: | - python x.py verify-release + if [[ "$GITHUB_REPOSITORY" != "wavefnd/Vex" ]]; then + echo "error: official releases must run in wavefnd/Vex, not $GITHUB_REPOSITORY" >&2 + exit 1 + fi + if [[ "$GITHUB_REF" != "refs/heads/master" ]]; then + echo "error: official releases must be dispatched from master, not $GITHUB_REF" >&2 + exit 1 + fi + + commit="$(git rev-parse HEAD)" + git fetch --force --tags origin + remote_master="$(git ls-remote origin refs/heads/master | awk '{print $1}')" + if [[ -z "$remote_master" || "$commit" != "$remote_master" ]]; then + echo "error: release commit $commit is not current wavefnd/Vex:master ($remote_master)" >&2 + exit 1 + fi + version="$(python x.py --version | awk '{print $2}')" - echo "tag=v${version}" >> "$GITHUB_OUTPUT" + tag="v${version}" + if git show-ref --verify --quiet "refs/tags/$tag"; then + tag_type="$(git cat-file -t "refs/tags/$tag")" + tag_commit="$(git rev-list -n 1 "$tag")" + if [[ "$tag_type" != "tag" || "$tag_commit" != "$commit" ]]; then + echo "error: existing $tag is not an annotated tag for release commit $commit" >&2 + exit 1 + fi + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "commit=$commit" >> "$GITHUB_OUTPUT" - name: Run release validation run: python x.py check @@ -88,10 +108,10 @@ jobs: extension: tar.gz runs-on: ${{ matrix.os }} steps: - - name: Check out release tag + - name: Check out release commit uses: actions/checkout@v7 with: - ref: ${{ needs.validate.outputs.tag }} + ref: ${{ needs.validate.outputs.commit }} fetch-depth: 0 - name: Install RISC-V system tools @@ -127,7 +147,7 @@ jobs: retention-days: 14 draft-release: - name: Attest and create draft release + name: Tag, attest, and create draft release needs: - validate - build @@ -137,10 +157,10 @@ jobs: contents: write id-token: write steps: - - name: Check out release tag + - name: Check out release commit uses: actions/checkout@v7 with: - ref: ${{ needs.validate.outputs.tag }} + ref: ${{ needs.validate.outputs.commit }} fetch-depth: 0 - name: Install Python @@ -168,6 +188,29 @@ jobs: cd dist sha256sum --check SHA256SUMS + - name: Create authoritative annotated release tag + env: + RELEASE_COMMIT: ${{ needs.validate.outputs.commit }} + RELEASE_TAG: ${{ needs.validate.outputs.tag }} + shell: bash + run: | + git fetch --force --tags origin + if git show-ref --verify --quiet "refs/tags/$RELEASE_TAG"; then + tag_type="$(git cat-file -t "refs/tags/$RELEASE_TAG")" + tag_commit="$(git rev-list -n 1 "$RELEASE_TAG")" + if [[ "$tag_type" != "tag" || "$tag_commit" != "$RELEASE_COMMIT" ]]; then + echo "error: existing $RELEASE_TAG is not an annotated tag for $RELEASE_COMMIT" >&2 + exit 1 + fi + echo "Reusing existing authoritative tag $RELEASE_TAG" + else + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$RELEASE_TAG" "$RELEASE_COMMIT" -m "Vex $RELEASE_TAG" + git push origin "refs/tags/$RELEASE_TAG" + fi + python x.py verify-release + - name: Generate build provenance attestations uses: actions/attest@v4 with: @@ -182,11 +225,28 @@ jobs: RELEASE_TAG: ${{ needs.validate.outputs.tag }} shell: bash run: | - gh release create "$RELEASE_TAG" \ - --verify-tag \ - --draft \ - --notes-file RELEASE_NOTES.md \ - --title "Vex $RELEASE_TAG" \ - dist/vex-*.tar.gz \ - dist/vex-*.zip \ - dist/SHA256SUMS + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + is_draft="$(gh release view "$RELEASE_TAG" --json isDraft --jq .isDraft)" + if [[ "$is_draft" != "true" ]]; then + echo "error: release $RELEASE_TAG is already published" >&2 + exit 1 + fi + gh release upload "$RELEASE_TAG" \ + --clobber \ + dist/vex-*.tar.gz \ + dist/vex-*.zip \ + dist/SHA256SUMS + gh release edit "$RELEASE_TAG" \ + --draft \ + --notes-file RELEASE_NOTES.md \ + --title "Vex $RELEASE_TAG" + else + gh release create "$RELEASE_TAG" \ + --verify-tag \ + --draft \ + --notes-file RELEASE_NOTES.md \ + --title "Vex $RELEASE_TAG" \ + dist/vex-*.tar.gz \ + dist/vex-*.zip \ + dist/SHA256SUMS + fi diff --git a/README.md b/README.md index d1293a1..9928d8e 100644 --- a/README.md +++ b/README.md @@ -239,11 +239,14 @@ linker to be installed. `VEX_RELEASE_HOST` exists for release infrastructure that must override host-target detection; normal development should not set it. `python3 x.py verify-release` checks that the source tree is clean and `HEAD` -has the annotated `v` tag required by the release workflow. The -workflow builds every release target before creating one checksum manifest and -a draft GitHub Release. It also generates GitHub build-provenance attestations; -publishing the reviewed draft remains a separate maintainer action. See -[RELEASING.md](RELEASING.md) for the complete procedure. +has the annotated `v` tag. For an official release, a maintainer +dispatches the Release workflow from `wavefnd/Vex:master`; the workflow refuses +forks and non-`master` refs. It validates and packages the exact upstream +commit, verifies the complete archive set, and only then creates the +authoritative annotated tag in `wavefnd/Vex`. It subsequently verifies that +tag, generates GitHub build-provenance attestations, and prepares a draft +GitHub Release. Publishing the reviewed draft remains a separate maintainer +action. See [RELEASING.md](RELEASING.md) for the complete procedure. Verify downloaded archives from the directory containing `SHA256SUMS`: diff --git a/RELEASING.md b/RELEASING.md index c207b40..b4b4c0f 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,17 +2,21 @@ This document is the maintainer procedure for producing an official Vex release. The release workflow builds reproducible archives from an existing -annotated version tag, creates provenance attestations, and prepares a draft -GitHub Release. It never publishes a release automatically. +authoritative `master` commit, creates the version tag in `wavefnd/Vex` only +after every target packages successfully, creates provenance attestations, and +prepares a draft GitHub Release. It never publishes a release automatically. ## Release contract - `Cargo.toml` is the single source of truth for the Vex version. - The release tag must be the exact `v` tag, point at the release - commit, and be annotated. A signed annotated tag is preferred. + commit, and be annotated. The upstream Release workflow creates it using the + GitHub Actions identity; maintainers do not push the official tag from a + local checkout or personal fork. - The release commit and committed `Cargo.lock` must be used without changes. -- Every configured target must build and package successfully before a draft - release is created. Partial releases are not supported. +- Every configured target must build and package successfully, and the complete + archive set must be verified, before the tag or draft release is created. + Partial releases are not supported. - Release archives and `SHA256SUMS` receive GitHub build-provenance attestations. - Publishing the reviewed draft is a separate, intentional maintainer action. @@ -51,35 +55,32 @@ checklist before tagging: 7. Merge the release-candidate pull request and wait for every required CI check on `master` to pass. -Do not create a release tag from a feature branch, a dirty checkout, or a -commit that has not passed the required checks. +Do not create or push the release tag locally. The workflow guard ensures the +official tag is created only in `wavefnd/Vex`, from its current `master`. -## 2. Create and push the version tag +## 2. Dispatch the upstream Release workflow -Fetch the authoritative repository and verify the commit before tagging: +After the release-candidate pull request is merged and the required `master` +checks pass, dispatch `.github/workflows/release.yml` in the authoritative +repository: ```sh -git remote add upstream https://github.com/wavefnd/Vex.git # if not already configured -git fetch upstream -git switch master -git merge --ff-only upstream/master -git status --short --branch -git log -1 --oneline -python3 x.py check -git tag -s v0.0.1 -m "Vex v0.0.1" -python3 x.py verify-release -git push upstream v0.0.1 +gh workflow run release.yml --repo wavefnd/Vex --ref master +gh run list --repo wavefnd/Vex --workflow release.yml --limit 1 ``` -If signed tags are not available in the maintainer environment, `git tag -a` -meets the automation's minimum annotated-tag requirement. Record that exception -in the release notes. Never replace or move a published release tag. +The workflow has no user-supplied tag input. It derives `v` from the +checked-in `Cargo.toml` and records the exact current `wavefnd/Vex:master` +commit. It fails before building if it is dispatched in a fork, from another +branch, or from a stale commit. -Pushing `v*` to `wavefnd/Vex` starts `.github/workflows/release.yml`. Pushing a -tag only to a personal fork does not create the official release. A maintainer -can rerun the same workflow manually with an existing annotated tag; the -workflow still checks that the tag matches `Cargo.toml` and the checked-out -commit. +The platform matrix builds and smoke-tests that exact commit without a tag. +After all targets succeed and the complete archive set passes checksum +verification, the final job creates an annotated tag through the +`wavefnd/Vex` workflow token, verifies it with `python3 x.py verify-release`, +attests the artifacts, and creates the draft release. A retry may reuse only an +existing annotated tag that points to the same release commit; it never moves +or replaces a tag. An already published release also cannot be overwritten. ## 3. Review the draft release @@ -130,13 +131,16 @@ project smoke tests using assets downloaded from the public release. ## Failure and recovery -- A failed matrix does not create a GitHub Release. Fix the problem in a new - commit and use a new pre-release version or tag; do not move a public tag. -- If the workflow fails before the draft is created, inspect the failed target, - correct the release commit, and restart the release process with an - appropriate new tag. -- If review finds a problem in an unpublished draft, delete the draft and its - unadvertised tag only after confirming no user depends on it, then prepare a - corrected release commit and tag. +- A failed validation, build, package, or checksum step creates neither a tag + nor a GitHub Release. Fix the problem in a new pull request, merge it, and + dispatch the workflow again. +- If the workflow fails after creating the tag but before creating the draft, + rerun it from the unchanged release commit. It safely reuses only the same + annotated tag. If source changes are required, increment the version; never + move an existing official tag. +- If review finds a problem in an unpublished draft, do not publish it. Remove + the draft and unadvertised tag only after confirming no user depends on them, + then prepare a corrected release commit with a new version and dispatch the + upstream workflow again. - Never publish a partial set of target archives or hand-edit generated archives and checksums. diff --git a/tests/xpy/test_release_tool.py b/tests/xpy/test_release_tool.py index 2432ea1..7e8b5f5 100644 --- a/tests/xpy/test_release_tool.py +++ b/tests/xpy/test_release_tool.py @@ -248,7 +248,7 @@ def test_release_requires_version_tag_at_head(self) -> None: ): release_tool.require_release_tag("0.0.1") - def test_release_workflow_covers_every_supported_target(self) -> None: + def test_release_workflow_covers_targets_and_creates_the_upstream_tag(self) -> None: workflow = (ROOT / ".github/workflows/release.yml").read_text( encoding="utf-8" ) @@ -259,6 +259,18 @@ def test_release_workflow_covers_every_supported_target(self) -> None: self.assertIn("uses: actions/attest@v4", workflow) self.assertIn("--draft", workflow) self.assertIn("--notes-file RELEASE_NOTES.md", workflow) + self.assertIn("workflow_dispatch:", workflow) + self.assertNotIn(" push:\n", workflow) + self.assertNotIn(" inputs:\n", workflow) + self.assertIn('GITHUB_REPOSITORY" != "wavefnd/Vex', workflow) + self.assertIn('git tag -a "$RELEASE_TAG"', workflow) + self.assertIn('git push origin "refs/tags/$RELEASE_TAG"', workflow) + self.assertIn("ref: ${{ needs.validate.outputs.commit }}", workflow) + self.assertIn("python x.py verify-release", workflow) + self.assertLess( + workflow.index("sha256sum --check SHA256SUMS"), + workflow.index('git tag -a "$RELEASE_TAG"'), + ) @staticmethod def make_package_inputs(root: Path, target: object, binary: bytes) -> None: