release #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "Release mode" | |
| required: true | |
| default: "bump-and-publish" | |
| type: choice | |
| options: | |
| - "bump-and-publish" | |
| - "publish-existing-tag" | |
| version: | |
| description: "patch, minor, major, or explicit x.y.z for bump-and-publish" | |
| required: false | |
| default: "patch" | |
| ref: | |
| description: "Branch to bump for bump-and-publish" | |
| required: false | |
| default: "main" | |
| tag: | |
| description: "Existing tag for publish-existing-tag, for example v0.4.99" | |
| required: false | |
| permissions: | |
| contents: write | |
| id-token: write | |
| concurrency: | |
| group: release-${{ github.event_name }}-${{ github.ref_name }} | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| if: github.event_name != 'push' || github.actor != 'github-actions[bot]' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve release mode | |
| id: resolve | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF_NAME: ${{ github.ref_name }} | |
| INPUT_MODE: ${{ inputs.mode }} | |
| INPUT_VERSION: ${{ inputs.version }} | |
| INPUT_REF: ${{ inputs.ref }} | |
| INPUT_TAG: ${{ inputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$EVENT_NAME" == "push" ]]; then | |
| mode="publish-existing-tag" | |
| tag="$REF_NAME" | |
| checkout_ref="$REF_NAME" | |
| version_input="" | |
| target_ref="" | |
| else | |
| mode="${INPUT_MODE:-bump-and-publish}" | |
| version_input="${INPUT_VERSION:-patch}" | |
| target_ref="${INPUT_REF:-main}" | |
| tag="${INPUT_TAG:-}" | |
| if [[ "$mode" == "publish-existing-tag" ]]; then | |
| if [[ -z "$tag" ]]; then | |
| echo "::error::publish-existing-tag requires the tag input, for example v0.4.99" | |
| exit 1 | |
| fi | |
| checkout_ref="$tag" | |
| target_ref="" | |
| elif [[ "$mode" == "bump-and-publish" ]]; then | |
| if [[ "$target_ref" == refs/tags/* ]]; then | |
| echo "::error::bump-and-publish requires a branch ref, not a tag" | |
| exit 1 | |
| fi | |
| checkout_ref="$target_ref" | |
| else | |
| echo "::error::Unsupported release mode: $mode" | |
| exit 1 | |
| fi | |
| fi | |
| { | |
| echo "mode=$mode" | |
| echo "checkout_ref=$checkout_ref" | |
| echo "version_input=$version_input" | |
| echo "target_ref=$target_ref" | |
| echo "tag=$tag" | |
| } >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ steps.resolve.outputs.checkout_ref }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| registry-url: "https://registry.npmjs.org" | |
| - run: npm ci | |
| - run: npm ci | |
| working-directory: site | |
| - name: Prepare bumped release | |
| if: steps.resolve.outputs.mode == 'bump-and-publish' | |
| run: node scripts/prepare-release.mjs "${{ steps.resolve.outputs.version_input }}" | |
| - name: Resolve package identity | |
| id: package | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| name="$(node -p "JSON.parse(require('fs').readFileSync('package.json','utf8')).name")" | |
| version="$(node -p "JSON.parse(require('fs').readFileSync('package.json','utf8')).version")" | |
| tag="v${version}" | |
| if [[ "${{ steps.resolve.outputs.mode }}" == "publish-existing-tag" && "$tag" != "${{ steps.resolve.outputs.tag }}" ]]; then | |
| echo "::error::Package version ${version} does not match tag ${{ steps.resolve.outputs.tag }}" | |
| exit 1 | |
| fi | |
| { | |
| echo "name=$name" | |
| echo "version=$version" | |
| echo "tag=$tag" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Release gate | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| npm run typecheck | |
| npm run build | |
| npm test | |
| npm run prepublishOnly | |
| npm pack --dry-run | |
| npm --prefix site run typecheck | |
| npm --prefix site run build | |
| - name: Commit and tag bumped release | |
| if: steps.resolve.outputs.mode == 'bump-and-publish' | |
| shell: bash | |
| env: | |
| TAG: ${{ steps.package.outputs.tag }} | |
| TARGET_REF: ${{ steps.resolve.outputs.target_ref }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add \ | |
| CHANGELOG.md \ | |
| README.md \ | |
| package.json \ | |
| package-lock.json \ | |
| tests/core/package-audit.test.ts \ | |
| site/src/release-status.generated.ts \ | |
| site/src/coverage-stars.generated.json | |
| if git diff --cached --quiet; then | |
| echo "::error::No release preparation changes were staged" | |
| exit 1 | |
| fi | |
| git commit -m "chore(release): prepare ${TAG}" | |
| git tag -a "$TAG" -m "$TAG" | |
| git push origin "HEAD:${TARGET_REF}" | |
| git push origin "$TAG" | |
| - name: Publish to npm | |
| id: publish | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| PACKAGE_NAME: ${{ steps.package.outputs.name }} | |
| VERSION: ${{ steps.package.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| published="$(npm view "${PACKAGE_NAME}@${VERSION}" version 2>/dev/null || true)" | |
| if [[ "$published" == "$VERSION" ]]; then | |
| echo "published=false" >> "$GITHUB_OUTPUT" | |
| echo "${PACKAGE_NAME}@${VERSION} is already published; skipping npm publish." | |
| exit 0 | |
| fi | |
| if [[ -n "${NODE_AUTH_TOKEN:-}" ]]; then | |
| echo "Publishing with NPM_TOKEN fallback." | |
| else | |
| echo "::notice::NPM_TOKEN is not set; attempting npm trusted publishing through GitHub OIDC." | |
| echo "::notice::npm trusted publisher must authorize ken-jo/agent-connector, workflow release.yml, action npm publish." | |
| fi | |
| if ! npm publish --access public --provenance; then | |
| if [[ -z "${NODE_AUTH_TOKEN:-}" ]]; then | |
| echo "::error::npm publish failed without token auth. Configure npm Trusted Publisher for GitHub Actions (owner ken-jo, repo agent-connector, workflow release.yml, allowed action npm publish) or add an NPM_TOKEN repository secret." | |
| fi | |
| exit 1 | |
| fi | |
| echo "published=true" >> "$GITHUB_OUTPUT" | |
| - name: Create GitHub release | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ steps.package.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "GitHub release $TAG already exists; skipping create." | |
| else | |
| gh release create "$TAG" --verify-tag --generate-notes --title "$TAG" | |
| fi | |
| - name: Refresh site release status on main | |
| shell: bash | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ steps.package.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git fetch origin main | |
| git checkout -B main origin/main | |
| node scripts/generate-site-release-status.mjs | |
| if git diff --quiet -- site/src/release-status.generated.ts; then | |
| echo "Release status snapshot is already current." | |
| exit 0 | |
| fi | |
| git add site/src/release-status.generated.ts | |
| git commit -m "chore(site): refresh release status for ${TAG}" | |
| if git push origin main; then | |
| exit 0 | |
| fi | |
| branch="automation/release-status-${TAG#v}" | |
| git checkout -B "$branch" | |
| git push --force-with-lease origin "$branch" | |
| existing_pr="$(gh pr list --head "$branch" --state open --json number --jq '.[0].number // empty')" | |
| if [[ -n "$existing_pr" ]]; then | |
| echo "Release status refresh PR already exists: #${existing_pr}" | |
| exit 0 | |
| fi | |
| gh pr create \ | |
| --base main \ | |
| --head "$branch" \ | |
| --title "chore(site): refresh release status for ${TAG}" \ | |
| --body "Automated release workflow follow-up after ${TAG}. Direct push to protected main was unavailable, so this PR carries the generated release-status snapshot." |