From 38181fa9a17e416ee01ae48465725a3929646a29 Mon Sep 17 00:00:00 2001 From: Joichiro Hayashi Date: Thu, 24 Sep 2026 19:03:03 +0900 Subject: [PATCH 1/2] ci(release): tag releases from merged release PRs in CI - replace `push-tag` and the pre-push claims hook - check README and package.json claims on release PRs --- .github/workflows/ci.yml | 4 ++ .github/workflows/release.yml | 97 ++++++++++++++++++++++++++--------- .vite-hooks/pre-push | 19 ------- package.json | 3 +- 4 files changed, 78 insertions(+), 45 deletions(-) delete mode 100755 .vite-hooks/pre-push diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a6dc3a..9f6ab94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,10 @@ jobs: - run: vp run replace-bundle-size-claims docs/src/introduction.md + # Merging a release PR releases it, so the claims it ships are checked before then. + - if: startsWith(github.head_ref, 'chore/release-') + run: vp run replace-bundle-size-claims README.md package.json + - run: vp run type-perf - run: vp run ts-compatibility diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 507097e..ed7d8b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,24 +1,77 @@ name: Release +# Merging a release PR bumps this file; the run releases whatever version main carries that has no tag yet. on: push: - tags: ["v*"] + branches: [main] + paths: [package.json] + workflow_dispatch: permissions: {} +concurrency: + group: release + cancel-in-progress: false + jobs: + verify: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read # the commit's PR is looked up below + outputs: + tag: ${{ steps.tag.outputs.tag }} + due: ${{ steps.tag.outputs.due }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + with: + persist-credentials: false + + - name: Resolve tag + id: tag + run: | + tag="v$(node -p "require('./package.json').version")" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + # ls-remote exits 2 when the tag is absent; any other failure must not pass for "absent". + status=0 + git ls-remote --exit-code --tags origin "refs/tags/$tag" > /dev/null || status=$? + case $status in + 0) echo "::notice::$tag already exists; nothing to release"; echo "due=false" >> "$GITHUB_OUTPUT" ;; + 2) echo "due=true" >> "$GITHUB_OUTPUT" ;; + *) exit "$status" ;; + esac + + # A version bump that did not come through `vp run bump-version` stops here instead of releasing. + - name: Verify commit is a merged release PR + if: steps.tag.outputs.due == 'true' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag }} + run: | + gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" | + jq -e --arg tag "$TAG" 'any(.[]; + .merged_at != null + and .head.repo.full_name == .base.repo.full_name + and .head.ref == "chore/release-\($tag[1:])" + and .title == "chore: release \($tag)")' > /dev/null || + { echo "::error::$GITHUB_SHA is not a merged chore/release-${TAG#v} PR titled \"chore: release $TAG\""; exit 1; } + release: + needs: verify + if: needs.verify.outputs.due == 'true' runs-on: ubuntu-latest permissions: - contents: write # the release is created below + contents: write # the tag and the release are created below id-token: write # npm trusted publishing and provenance + env: + TAG: ${{ needs.verify.outputs.tag }} steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 with: - fetch-depth: 0 # needed to check the tag is an ancestor of main persist-credentials: false - # No cache in this workflow: a cache another ref wrote must not reach what a tag publishes. + # No cache in this workflow: a cache another ref wrote must not reach what a release publishes. - uses: voidzero-dev/setup-vp@1b32467adbe183473499fd9d5d372c3ed9641754 # v1.18.0 with: node-version: "24" @@ -41,38 +94,35 @@ jobs: - run: vp run ts-compatibility - - name: Verify tag is on main - run: | - git fetch --quiet origin main - git merge-base --is-ancestor HEAD FETCH_HEAD || - { echo "tag $GITHUB_REF_NAME is not reachable from main"; exit 1; } - - - name: Verify tag matches package version - run: | - tag="${GITHUB_REF_NAME#v}" - pkg="$(node -p "require('./package.json').version")" - test "$tag" = "$pkg" || { echo "tag $tag != package.json $pkg"; exit 1; } + # Tags are immutable, so tagging only after the checks pass keeps a broken commit from burning + # its version. + - name: Create tag + run: gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$TAG" -f sha="$GITHUB_SHA" + env: + GH_TOKEN: ${{ github.token }} - # No token: npm trusts this workflow through OIDC. --no-git-checks: the tag is checked out - # detached, not on the publish branch. + # No token: npm trusts this workflow through OIDC. --no-git-checks: the verify job already + # checked this commit. - run: pnpm publish --provenance --no-git-checks # Last: a release pointing at a version npm rejected would be a lie. The notes come from the # PRs merged since the previous tag, grouped by `.github/release.yml`. - name: Create the GitHub release - run: gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes + run: gh release create "$TAG" --verify-tag --generate-notes env: GH_TOKEN: ${{ github.token }} # The book is published per tag under `{tag}/`, with `latest/` a copy of the newest release and a # bare index at the root. The site lives on `gh-pages`, which GitHub Pages serves as a branch. docs: - needs: release + needs: [verify, release] runs-on: ubuntu-latest permissions: contents: write # the site is pushed to gh-pages concurrency: group: gh-pages # two releases must not push the site at once + env: + TAG: ${{ needs.verify.outputs.tag }} steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 with: @@ -101,11 +151,10 @@ jobs: - name: Add this version run: | - tag="$GITHUB_REF_NAME" - rm -rf "site/$tag" - cp -r docs/book/html "site/$tag" + rm -rf "site/$TAG" + cp -r docs/book/html "site/$TAG" # A prerelease is reachable by its tag, but it is not what `latest/` means. - case "$tag" in + case "$TAG" in *-*) ;; *) rm -rf site/latest && cp -r docs/book/html site/latest ;; esac @@ -119,7 +168,7 @@ jobs: git -C site add --all git -C site -c user.name="github-actions[bot]" \ -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \ - commit --message "docs: publish $GITHUB_REF_NAME" + commit --message "docs: publish $TAG" git -C site push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:gh-pages env: GH_TOKEN: ${{ github.token }} diff --git a/.vite-hooks/pre-push b/.vite-hooks/pre-push deleted file mode 100755 index 8f1e546..0000000 --- a/.vite-hooks/pre-push +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env sh - -while read -r local_ref local_sha remote_ref remote_sha; do - case "$local_sha" in - 0000000000000000000000000000000000000000) continue ;; - esac - - case "$local_ref" in - refs/tags/v*) - vp run replace-bundle-size-claims README.md docs/src/introduction.md package.json - exit $? - ;; - esac - - if git tag --points-at "$local_sha" | grep -q '^v'; then - vp run replace-bundle-size-claims README.md docs/src/introduction.md package.json - exit $? - fi -done diff --git a/package.json b/package.json index 5caac51..4b3b15d 100644 --- a/package.json +++ b/package.json @@ -51,8 +51,7 @@ "type-perf": "node scripts/type-perf/index.ts", "prepublishOnly": "vp run build", "prepare": "vp config && vp fmt AGENTS.md", - "bump-version": "scripts/bump-version.sh", - "push-tag": "git diff --quiet HEAD -- || { echo 'tracked files differ from HEAD; commit them before tagging'; exit 1; }; git switch main && git pull --ff-only && tag=v$(node -p \"require('./package.json').version\") && git tag $tag && git push origin $tag" + "bump-version": "scripts/bump-version.sh" }, "devDependencies": { "@types/node": "^26.5.0", From 2aeaceaad7ec4d1c79329ea51ffb3120754e16b0 Mon Sep 17 00:00:00 2001 From: Joichiro Hayashi Date: Thu, 24 Sep 2026 19:03:56 +0900 Subject: [PATCH 2/2] docs: describe where release claims are checked now --- AGENTS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6960e4c..bc9a4ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,9 +13,9 @@ the production bundle against its budgets. It does not update release documentat The bundle-size claim in `docs/src/introduction.md` follows the current branch. Update it with `vp run replace-bundle-size-claims --write docs/src/introduction.md`; regular CI checks it. The -claims in `README.md` and the `package.json` description describe the latest release instead. The -Vite+ pre-push hook checks all three when a pushed commit has a `v*` tag. The release workflow -checks all three claims again from the tag. +claims in `README.md` and the `package.json` description describe the latest release instead. CI +checks them on the release PR that `vp run bump-version` opens. After the merge, the release +workflow checks all three again before it creates the tag. Run `vp run type-perf` after changing a type. It compiles the fixtures in `scripts/type-perf/` against the published declarations, and budgets the instantiation count and the size of