Skip to content

Merge pull request #1563 from qawolf/changeset-release/main #156

Merge pull request #1563 from qawolf/changeset-release/main

Merge pull request #1563 from qawolf/changeset-release/main #156

Workflow file for this run

name: Release
on:
push:
branches: [main]
jobs:
# GitHub applies environment protection when a job starts, not when a step
# runs, so a gated job can only be avoided by never creating it.
mode:
name: Detect release mode
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
has_changesets: ${{ steps.detect.outputs.has_changesets }}
needs_publish: ${{ steps.detect.outputs.needs_publish }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 2 # HEAD^ for the version-bump comparison
persist-credentials: false
- name: Detect pending changesets and unpublished version
id: detect
run: |
shopt -s nullglob
has_changesets=false
for f in .changeset/*.md; do
if [[ "$(basename "$f")" != "README.md" ]]; then
has_changesets=true
break
fi
done
# Publish only when this push bumped the version and that version has
# no tag yet. The bump check stops a merge that lands while a publish
# waits for approval from raising a second prompt. The tag check stops
# a re-run after a successful publish from publishing twice. An
# unreadable parent leaves the tag check to decide. The repo is
# public, so the anonymous ls-remote needs no token.
version=$(jq -r .version package.json)
previous=$(git show HEAD^:package.json 2>/dev/null | jq -r .version || true)
needs_publish=false
if [[ "$has_changesets" == "false" && "$version" != "$previous" ]]; then
if [[ -z "$(git ls-remote --tags origin "refs/tags/v${version}")" ]]; then
needs_publish=true
fi
fi
echo "has_changesets=$has_changesets" >> "$GITHUB_OUTPUT"
echo "needs_publish=$needs_publish" >> "$GITHUB_OUTPUT"
echo "has_changesets=\`$has_changesets\` needs_publish=\`$needs_publish\`" >> "$GITHUB_STEP_SUMMARY"
version:
name: Version PR
needs: mode
if: needs.mode.outputs.has_changesets == 'true'
# The qa-wolf-ops credentials are environment secrets. This environment
# holds a copy with no reviewers, so the job reads them without a prompt.
environment: release-version
runs-on: ubuntu-latest
# Two version runs would race on force-pushing changeset-release/main.
concurrency:
group: release-version-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version-file: package.json
# No build: version-packages only runs `changeset version` and oxfmt.
- run: bun install --frozen-lockfile
# GITHUB_TOKEN can't create PRs under org policy, and PRs it opens don't
# trigger CI; the App token does both.
- name: Generate qa-wolf-ops token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.QA_WOLF_OPS_CLIENT_ID }}
private-key: ${{ secrets.QA_WOLF_OPS_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Create or update Version Packages PR
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0
with:
version: bun run version-packages
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
publish:
name: Publish
needs: mode
if: needs.mode.outputs.needs_publish == 'true'
environment: release
runs-on: ubuntu-latest
# Two publish runs would race on pushing the same tag.
concurrency:
group: release-publish-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
id-token: write # OIDC for npm provenance
outputs:
published: ${{ steps.changesets.outputs.published }}
tag: ${{ steps.tag.outputs.tag }}
slack_ts: ${{ steps.slack.outputs.ts }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version-file: package.json
- run: bun install --frozen-lockfile
- run: bun run build
# The App token creates the GitHub Release, so release-binaries picks it
# up: a release created by GITHUB_TOKEN triggers no workflow.
- name: Generate qa-wolf-ops token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.QA_WOLF_OPS_CLIENT_ID }}
private-key: ${{ secrets.QA_WOLF_OPS_PRIVATE_KEY }}
permission-contents: write
- name: Publish to npm
id: changesets
uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0
with:
publish: bunx changeset publish
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
- name: Resolve release tag
id: tag
if: steps.changesets.outputs.published == 'true'
env:
PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }}
run: |
VERSION=$(echo "$PUBLISHED_PACKAGES" | bun -e "const d = await Bun.stdin.text(); console.log(JSON.parse(d)[0].version)")
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
# continue-on-error: a Slack outage must never fail a release
- name: Notify Slack of release
id: slack
if: steps.changesets.outputs.published == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_RELEASE_CHANNEL_ID }}
RELEASE_TAG: ${{ steps.tag.outputs.tag }}
run: |
payload=$(gh release view "$RELEASE_TAG" --json body,url,tagName \
| bun scripts/slackReleaseMessage.ts --phase publish --channel "$SLACK_CHANNEL_ID")
response=$(curl -sf --max-time 30 -X POST https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
--data "$payload")
printf '%s' "$response" | jq -e '.ok' > /dev/null
echo "ts=$(printf '%s' "$response" | jq -r '.ts')" >> "$GITHUB_OUTPUT"
binaries:
name: Binaries
needs: publish
if: needs.publish.outputs.published == 'true'
permissions:
contents: write
uses: ./.github/workflows/release-binaries.yml
with:
tag: ${{ needs.publish.outputs.tag }}
# A sibling of binaries rather than a step in publish: a registry failure must
# not skip the binary build, and changeset publish only knows one registry.
github-packages:
name: GitHub Packages
needs: publish
if: needs.publish.outputs.published == 'true'
permissions:
contents: read
packages: write
uses: ./.github/workflows/publish-github-packages.yml
with:
tag: ${{ needs.publish.outputs.tag }}
notify-binaries:
name: Notify Slack of binaries
needs: [publish, binaries]
# always(): the binaries matrix runs with fail-fast off, so report partial
# failures too. Skips when nothing was published or the publish-time Slack
# message did not post (no ts to update).
if: always() && needs.publish.outputs.published == 'true' && needs.publish.outputs.slack_ts != ''
environment: release-notify
runs-on: ubuntu-latest
concurrency:
group: release-notify-${{ needs.publish.outputs.tag }}
cancel-in-progress: false
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version-file: package.json
# The release assets are the ground truth for which binaries built: a
# target only has an asset after its build, smoke test, and upload all
# succeeded. No bun install — the script is dependency-free.
- name: Update release message with binary status
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_RELEASE_CHANNEL_ID }}
RELEASE_TAG: ${{ needs.publish.outputs.tag }}
SLACK_TS: ${{ needs.publish.outputs.slack_ts }}
run: |
payload=$(gh release view "$RELEASE_TAG" --json body,url,tagName,assets \
| bun scripts/slackReleaseMessage.ts --phase binaries --channel "$SLACK_CHANNEL_ID" --ts "$SLACK_TS")
curl -sf --max-time 30 -X POST https://slack.com/api/chat.update \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
--data "$payload" | jq -e '.ok' > /dev/null