diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0fa69b0..d307a37 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,6 @@ permissions: - main tags: - '[0-9]+.[0-9]+.[0-9]+*' - - 'v[0-9]+.[0-9]+.[0-9]+*' pull_request: null concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -380,6 +379,8 @@ jobs: - name: List packages run: ls -R ./npm shell: bash + - name: Install an npm with trusted publishing support + run: npm install -g npm@latest - name: Publish run: | VERSION="${GITHUB_REF_NAME#v}" @@ -388,12 +389,29 @@ jobs: echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $PACKAGE_VERSION" exit 1 fi - npm config set provenance true - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + PACKAGE_NAME="$(node -p "require('./package.json').name")" + if npm view "$PACKAGE_NAME@$VERSION" version >/dev/null 2>&1; then + echo "$PACKAGE_NAME@$VERSION is already published, skipping" + exit 0 + fi case "$VERSION" in *-*) npm publish --tag next --access public ;; *) npm publish --access public ;; esac env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Create the GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${GITHUB_REF_NAME#v}" + node scripts/changelog-section.mjs "$VERSION" > release-notes.md + case "$VERSION" in + *-*) PRERELEASE=--prerelease ;; + *) PRERELEASE= ;; + esac + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + gh release edit "$GITHUB_REF_NAME" --title "$VERSION" --notes-file release-notes.md $PRERELEASE + else + gh release create "$GITHUB_REF_NAME" --title "$VERSION" --notes-file release-notes.md $PRERELEASE + fi diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d1b009d..eaac992 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -33,3 +33,15 @@ Pushing the tag builds all six targets, runs the tests and examples, and publish The tag must match the version in `package.json` or the publish step fails. A tag with a prerelease suffix, such as `0.24.0-rc.1`, publishes under the `next` dist-tag instead of `latest`. + +The GitHub release is created from the tag with the matching `CHANGELOG.md` section as its +notes, extracted by `scripts/changelog-section.mjs`. A release whose version has no section +there fails rather than publishing empty notes. `napi prepublish` runs with +`--no-gh-release` because it would otherwise create its own release under a `v`-prefixed +tag. + +Publishing uses npm trusted publishing over OIDC, so there is no npm token in the +repository. Each of the seven published packages (`@ai-coustics/aic-sdk` and its six +platform packages) needs a trusted publisher on npmjs.com naming this repository and the +workflow file `CI.yml`. npm rejects a publish whose workflow file does not match, with a +404 on the `PUT` rather than a permission error. diff --git a/package.json b/package.json index 230ea40..20a5519 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "typecheck:test": "tsc --noEmit -p __test__/tsconfig.json", "typecheck:bench": "tsc --noEmit -p benchmark/tsconfig.json", "typecheck:examples": "tsc --noEmit -p examples/tsconfig.json", - "prepublishOnly": "napi prepublish -t npm", + "prepublishOnly": "napi prepublish -t npm --no-gh-release", "pretest": "node --import @oxc-node/core/register scripts/fetch-test-models.mjs", "test": "ava", "version": "napi version" diff --git a/scripts/changelog-section.mjs b/scripts/changelog-section.mjs new file mode 100644 index 0000000..8668dd7 --- /dev/null +++ b/scripts/changelog-section.mjs @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs' + +const version = process.argv[2] + +if (!version) { + console.error('usage: changelog-section.mjs ') + process.exit(1) +} + +const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n') +const start = lines.findIndex((line) => line === `## ${version}` || line.startsWith(`## ${version} `)) + +if (start === -1) { + console.error(`CHANGELOG.md has no section for ${version}`) + process.exit(1) +} + +const rest = lines.slice(start + 1) +const end = rest.findIndex((line) => line.startsWith('## ')) +const body = (end === -1 ? rest : rest.slice(0, end)).join('\n').trim() + +if (!body) { + console.error(`The CHANGELOG.md section for ${version} is empty`) + process.exit(1) +} + +process.stdout.write(`${body}\n`)