diff --git a/.github/workflows/release-zed.yml b/.github/workflows/release-zed.yml new file mode 100644 index 0000000..f7dd0e7 --- /dev/null +++ b/.github/workflows/release-zed.yml @@ -0,0 +1,69 @@ +name: Release Zed extension + +# Merging a Zed release PR bumps this file; the run tags whatever version main carries that has no tag yet. +on: + push: + branches: [main] + paths: [crates/zed-extension/extension.toml] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-zed + cancel-in-progress: false + +jobs: + tag: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Resolve tag + id: tag + working-directory: crates/zed-extension + run: | + crate_version=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[0].version') + extension_version=$(sed -n 's/^version = "\(.*\)"/\1/p' extension.toml) + [ "$crate_version" = "$extension_version" ] || + { echo "::error::Cargo.toml $crate_version != extension.toml $extension_version"; exit 1; } + + tag="zed-v$crate_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 `just bump-zed-version` stops here instead of tagging. + - 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-zed-\($tag | ltrimstr("zed-v"))" + and .title == "chore(zed): release \($tag | ltrimstr("zed-"))")' > /dev/null || + { echo "::error::$GITHUB_SHA is not a merged chore/release-${TAG#zed-v} PR titled \"chore(zed): release ${TAG#zed-}\""; exit 1; } + + - name: Create tag + if: steps.tag.outputs.due == 'true' + run: gh api "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$TAG" -f sha="$GITHUB_SHA" + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91bf90a..a7afe54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,32 +1,69 @@ 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: [packages/css-var-kit/package.json] + workflow_dispatch: permissions: contents: read +concurrency: + group: release + cancel-in-progress: false + env: CARGO_TERM_COLOR: always jobs: - # Every publish below trusts the tag, so a tag that is off main or ahead of the versions stops here. verify: + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: + tag: ${{ steps.tag.outputs.tag }} + due: ${{ steps.tag.outputs.due }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-depth: 0 persist-credentials: false - - name: Verify tag is on main + - name: Resolve tag + id: tag run: | - git fetch --quiet origin main - git merge-base --is-ancestor HEAD FETCH_HEAD || - { echo "::error::tag $GITHUB_REF_NAME is not reachable from main"; exit 1; } + tag="v$(node -p "require('./packages/css-var-kit/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 `just 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; } - name: Verify tag matches package versions + env: + TAG: ${{ steps.tag.outputs.tag }} run: | set -o pipefail { @@ -40,10 +77,11 @@ jobs: ' cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "css-var-kit") | "Cargo.toml \(.version)"' - } | awk -v tag="${GITHUB_REF_NAME#v}" '$NF != tag { print "::error::" $0 " != " tag; failed = 1 } END { exit failed }' + } | awk -v tag="${TAG#v}" '$NF != tag { print "::error::" $0 " != " tag; failed = 1 } END { exit failed }' build: needs: verify + if: needs.verify.outputs.due == 'true' strategy: matrix: include: @@ -100,8 +138,21 @@ jobs: name: ${{ matrix.npm-pkg }} path: packages/${{ matrix.npm-pkg }}/${{ matrix.binary }} + # Tags are immutable, so tagging only after the builds pass keeps a broken build from burning its version. + tag: + needs: [verify, build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - 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 }} + TAG: ${{ needs.verify.outputs.tag }} + publish: - needs: build + needs: tag runs-on: ubuntu-latest permissions: contents: read @@ -170,7 +221,7 @@ jobs: fi publish-vscode: - needs: build + needs: tag runs-on: ubuntu-latest strategy: matrix: @@ -235,7 +286,7 @@ jobs: path: packages/vscode/*.vsix github-release: - needs: [build, publish-vscode] + needs: [verify, publish-vscode] runs-on: ubuntu-latest permissions: contents: write @@ -261,6 +312,7 @@ jobs: # The notes come from the PRs merged since the previous tag, grouped by `.github/release.yml`; # edit them on the release page afterwards if needed. - name: Create GitHub Release - run: gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes dist/* + run: gh release create "$TAG" --verify-tag --generate-notes dist/* env: GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.verify.outputs.tag }} diff --git a/justfile b/justfile index ceaa6ff..789fa5f 100644 --- a/justfile +++ b/justfile @@ -8,7 +8,8 @@ package-jsons := "packages/css-var-kit/package.json \ zed-pkg := "crates/zed-extension" -# Bumps the version on a release branch cut from the latest main and opens its PR; without `level`, bumpp prompts for it. +# Bumps the version on a release branch cut from the latest main and opens its PR; merging it releases. +# Without `level`, bumpp prompts for it. bump-version level="": _latest-main #!/usr/bin/env sh set -eu @@ -28,15 +29,7 @@ bump-version level="": _latest-main git push -u origin HEAD gh pr create --fill --label skip-changelog -# Tags the merged release on main; the pushed tag triggers the release workflow. -push-tag: _latest-main - #!/usr/bin/env sh - set -eu - tag="v$(node -p "require('./packages/css-var-kit/package.json').version")" - git tag "$tag" - git push origin "$tag" - -# Bumps the Zed extension version on a release branch cut from the latest main and opens its PR. +# Bumps the Zed extension version on a release branch cut from the latest main and opens its PR; merging it tags the release. bump-zed-version level: _latest-main #!/usr/bin/env sh set -eu @@ -54,14 +47,6 @@ bump-zed-version level: _latest-main git push -u origin HEAD gh pr create --fill --label skip-changelog -# Tags the merged Zed extension release on main. -push-zed-tag: _latest-main - #!/usr/bin/env sh - set -eu - tag="zed-v$(cd {{zed-pkg}} && cargo metadata --format-version 1 --no-deps | jq -r '.packages[0].version')" - git tag "$tag" - git push origin "$tag" - [private] _latest-main: #!/usr/bin/env sh