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 | |
| # Manual, one-button release. Pick a version (a semver bump keyword or an exact | |
| # version), and this workflow: runs the full CI gate, bumps package.json, | |
| # writes the changelog from the commits since the last tag, commits + tags, | |
| # publishes to public npm, and cuts a GitHub Release whose body is that | |
| # changelog. Run `dry_run: true` first to preview without pushing anything. | |
| # | |
| # Publishing uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret. Configure | |
| # a trusted publisher for @copse/streaming-markdown at npmjs.com (Settings -> | |
| # Trusted Publisher) pointing at this repo + workflow file (release.yml). The | |
| # package must exist first, so the initial publish is a one-time manual | |
| # `npm publish` (interactive 2FA); every release after that flows through here. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| bump: | |
| description: 'Semver bump to apply (ignored when "exact version" is set below)' | |
| required: true | |
| default: patch | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| - prepatch | |
| - preminor | |
| - premajor | |
| - prerelease | |
| exact_version: | |
| description: 'Optional: exact version to release (e.g. 1.2.0). Overrides the bump dropdown when set.' | |
| required: false | |
| default: '' | |
| type: string | |
| prerelease: | |
| description: 'Mark the GitHub Release as a pre-release' | |
| required: false | |
| default: false | |
| type: boolean | |
| dry_run: | |
| description: 'Build the release notes and version bump but do not commit, tag, publish, or release' | |
| required: false | |
| default: false | |
| type: boolean | |
| # Least privilege: write to the repo (commit + tag + release). id-token: write | |
| # lets the runner mint the GitHub OIDC token that npm Trusted Publishing uses to | |
| # authenticate the publish (and to attach provenance automatically). | |
| permissions: | |
| contents: write | |
| id-token: write | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # Full history + tags so the changelog can diff against the last tag. | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| registry-url: https://registry.npmjs.org | |
| scope: '@copse' | |
| - run: npm ci | |
| # Release gate: never cut a release from a red tree. | |
| - name: Typecheck | |
| run: npm run typecheck | |
| - name: Test | |
| run: npm test | |
| - name: Build | |
| run: npm run build | |
| - name: Normalizer parity | |
| run: npm run check:normalizer-parity | |
| - name: Resolve version | |
| id: version | |
| run: | | |
| set -euo pipefail | |
| # Prefer the free-text exact version when provided; otherwise use the | |
| # bump keyword from the dropdown. `npm version` accepts both forms and | |
| # prints the resulting `vX.Y.Z`. --no-git-tag-version edits | |
| # package.json + package-lock.json without tagging (we tag ourselves). | |
| TARGET="${{ inputs.exact_version || inputs.bump }}" | |
| TAG=$(npm version "$TARGET" --no-git-tag-version) | |
| VERSION="${TAG#v}" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Releasing $TAG" | |
| - name: Generate changelog | |
| run: npx tsx scripts/gen-changelog.mts "${{ steps.version.outputs.version }}" > RELEASE_NOTES.md | |
| - name: Update CHANGELOG.md | |
| run: | | |
| set -euo pipefail | |
| DATE=$(date -u +%Y-%m-%d) | |
| npx tsx scripts/update-changelog.mts "${{ steps.version.outputs.version }}" "$DATE" RELEASE_NOTES.md | |
| - name: Preview release notes | |
| run: | | |
| { | |
| echo "## Release ${{ steps.version.outputs.tag }}" | |
| echo | |
| cat RELEASE_NOTES.md | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Commit and tag | |
| if: ${{ !inputs.dry_run }} | |
| 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 package.json package-lock.json CHANGELOG.md | |
| git commit -m "chore(release): ${{ steps.version.outputs.tag }}" | |
| git tag -a "${{ steps.version.outputs.tag }}" -m "${{ steps.version.outputs.tag }}" | |
| # Publish BEFORE pushing so a failed publish (e.g. bad token) leaves main | |
| # and the tag list untouched — the commit + tag exist only locally on the | |
| # runner and vanish with it, leaving nothing to clean up before a retry. | |
| - name: Publish to npm | |
| if: ${{ !inputs.dry_run }} | |
| run: | | |
| set -euo pipefail | |
| # Trusted Publishing authenticates via the GitHub OIDC token (see the | |
| # id-token permission above) — no NPM_TOKEN needed. It requires npm | |
| # >= 11.5.1; the Node 22 runner ships npm 10, so upgrade first. | |
| npm install -g npm@latest | |
| # publishConfig pins the public registry + `--access public`, and | |
| # provenance is attached automatically under trusted publishing. | |
| npm publish | |
| - name: Push commit and tag | |
| if: ${{ !inputs.dry_run }} | |
| run: | | |
| set -euo pipefail | |
| git push origin HEAD | |
| git push origin "${{ steps.version.outputs.tag }}" | |
| - name: Create GitHub Release | |
| if: ${{ !inputs.dry_run }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| gh release create "${{ steps.version.outputs.tag }}" \ | |
| --title "${{ steps.version.outputs.tag }}" \ | |
| --notes-file RELEASE_NOTES.md \ | |
| ${{ inputs.prerelease && '--prerelease' || '' }} |