diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9bc27a7..1cc8a59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,47 +1,104 @@ name: Release +# Creates the vX.Y.Z tag and GitHub Release whenever package.json's version +# changes on main (i.e. a release PR is merged). Ordinary merges are no-ops +# because the version guard skips versions that already have a release. +# +# Split into two jobs so dependency code never runs where the write token +# exists: `build` installs and runs everything read-only; `release` waits for +# approval of the `release` environment, installs nothing, and publishes the +# prebuilt files. + on: push: - tags: ["v*.*.*"] + branches: [main] permissions: contents: read +concurrency: + group: release + cancel-in-progress: false + jobs: build: runs-on: ubuntu-latest + outputs: + tag: ${{ steps.v.outputs.tag }} + released: ${{ steps.v.outputs.released }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 cache: npm - - name: Tag matches package.json + + - name: Determine version and whether it is already released + id: v env: - TAG: ${{ github.ref_name }} - run: test "$TAG" = "v$(node -p "require('./package.json').version")" - - run: npm ci --ignore-scripts - - run: npm run lint - - run: npm run bundle - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + GH_TOKEN: ${{ github.token }} + run: | + TAG="v$(node -p "require('./package.json').version")" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "released=true" >> "$GITHUB_OUTPUT" + echo "::notice::$TAG already released, skipping." + else + echo "released=false" >> "$GITHUB_OUTPUT" + echo "::notice::Releasing $TAG." + fi + + - name: Install dependencies + if: steps.v.outputs.released == 'false' + run: npm ci --ignore-scripts + + - name: Verify and bundle + if: steps.v.outputs.released == 'false' + run: | + npm run lint + npm run bundle + + - name: Upload bundle + if: steps.v.outputs.released == 'false' + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: spec path: dist/ + retention-days: 1 release: - # Holds the write token; installs and runs nothing from npm. needs: build + # Only when there is a new version, so ordinary merges don't wait for approval. + if: needs.build.outputs.released == 'false' + # Requires a maintainer's approval before this job (and its write token) + # runs, so a stolen GitHub login can't publish a release on its own. + environment: release runs-on: ubuntu-latest permissions: - contents: write + contents: write # create the release tag + GitHub Release steps: - - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + - name: Download bundle + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: spec - path: dist - - env: + path: dist/ + + # Idempotent: creates the tag and Release only if missing. --target + # creates the tag at this commit through the API, so no git credentials + # are needed. + - name: Ensure release tag and GitHub Release exist + env: GH_TOKEN: ${{ github.token }} - TAG: ${{ github.ref_name }} - run: gh release create "$TAG" dist/openapi.json dist/openapi.yaml --repo "$GITHUB_REPOSITORY" --title "$TAG" --generate-notes + TAG: ${{ needs.build.outputs.tag }} + REPO: ${{ github.repository }} + run: | + if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + gh release create "$TAG" dist/openapi.json dist/openapi.yaml \ + --repo "$REPO" \ + --target "$GITHUB_SHA" \ + --title "$TAG" \ + --notes "See [CHANGELOG.md](https://github.com/${REPO}/blob/main/CHANGELOG.md) for details." + fi diff --git a/AGENTS.md b/AGENTS.md index c7878e7..b9e191c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,5 +12,8 @@ OpenAPI 3.0 spec for the Nomba API, generated from the API reference at develope - The docs are the source of truth. Prefer fixing the docs and running `npm run sync` over hand-editing `openapi/`; a hand edit makes `npm run sync -- --check` fail until the docs catch up. - Never add entries to `.redocly.lint-ignore.yaml`. Remove them as problems are fixed, and update `ALIGNMENT.md`. +## Releasing +Never create tags or Releases by hand; see `RELEASING.md`. + ## Before pushing `npm run lint && npm run bundle` diff --git a/CHANGELOG.md b/CHANGELOG.md index df44b24..363b04b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## 0.1.0 (unreleased) +## [0.1.0] - 2026-11-03 -- First release. 100 operations across 94 paths, generated from the developer.nomba.com API reference. -- Adds global payout, global collection, direct debits, betting, terminal payment requests and the v2 bills and transfers endpoints, none of which are in `kudi-inc/vendor-openapi-spec`. +First release. 100 operations across 94 paths, generated from the developer.nomba.com API reference. + +### Added + +- Global payout and global collection, direct debits, betting, terminal payment requests and the v2 bills and transfers endpoints, none of which are in `kudi-inc/vendor-openapi-spec`. diff --git a/README.md b/README.md index d744523..865c9d3 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ CI lints and bundles every pull request and checks for drift against the docs ev ## Releasing -Bump `version` in `package.json`, add a `CHANGELOG.md` entry, merge, then tag `vX.Y.Z` on main. The release workflow checks the tag matches `package.json`, bundles the spec and attaches both files to a GitHub release. +Automated: merge a `chore: release X.Y.Z` PR and approve the `release` deployment. See [RELEASING.md](RELEASING.md). ## License diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..d68a4b1 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,32 @@ +# Releasing + +Releasing is automated; you never create tags or Releases by hand. + +## Steps + +1. Merge your PRs to `main`. Main squash-merges, so each PR title must be a valid Conventional Commit. +2. Open a `chore: release X.Y.Z` PR that bumps the version with `npm version X.Y.Z --no-git-tag-version` and adds a `## [X.Y.Z] - YYYY-MM-DD` section to `CHANGELOG.md` (Breaking, Added, Fixed, Changed, in plain language). +3. Merge it. `release.yml` lints and bundles the spec, then waits. Open the run in **Actions** and approve the **release** deployment. Only then does it create the `vX.Y.Z` tag and GitHub Release with `openapi.json` and `openapi.yaml` attached. + +## Choosing the version + +| Change since the last release | Bump | +|-------------------------------|------| +| An operation, parameter or schema field removed or renamed, or a type changed | major | +| An operation, parameter or schema field added | minor | +| Descriptions, examples or lint fixes only | patch | +| only `chore:` `docs:` `refactor:` `test:` `ci:` with no spec change | no release | + +Before 1.0.0, breaking changes bump the minor version. + +## After releasing + +Confirm the tag, the Release and both files: + +```bash +curl -sIL https://github.com/NombaOSS/openapi/releases/latest/download/openapi.json | grep -E '^HTTP' | tail -1 +``` + +## One-time setup (repo owner) + +- GitHub: an environment named `release` with yourself as required reviewer, limited to protected branches. Done for this repo.