From 1d434009042538112844241affbc9d68d6041732 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 10:54:51 +0200 Subject: [PATCH 1/9] ci: Create a GitHub release with grouped notes on publish Publishing put a version on npm and a tag in the repository, but no GitHub release, so there was nowhere to read what changed. The deploy job now creates one once npm has accepted the version, from GitHub's generated notes. It starts from the previous release rather than the previous tag, because a publish that fails after the version bump leaves a tag behind without a release, and starting from that tag would leave its changes out of the next notes. .github/release.yml groups the notes into breaking changes, new features, fixes and other changes, and leaves internal work out. The groups key off labels, which label-pr.yml sets from the conventional commit prefix of each pull request title: feat, fix and perf, a breaking marker, and build, chore, ci, docs, refactor, style and test as internal. Titles without a prefix land under other changes, so outside contributions stay visible. The release pull request into production is labeled too and excluded. The notes keep each pull request title as is, so the prefix is stripped from every line: the heading above it already says it. --- .github/release.yml | 23 +++++++++++++++ .github/workflows/deploy.yml | 22 ++++++++++++++ .github/workflows/label-pr.yml | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 .github/release.yml create mode 100644 .github/workflows/label-pr.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..f98c27c --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,23 @@ +# Categories for the notes GitHub generates when the deploy workflow creates a release. +# The labels are set from the conventional-commit prefix of the pull request title by +# .github/workflows/label-pr.yml. +changelog: + exclude: + labels: + - internal + - release + categories: + - title: Breaking changes + labels: + - breaking + - title: New features + labels: + - feature + - title: Fixes + labels: + - fix + # Titles without a conventional prefix, typically outside contributions. Relabel or + # retitle the pull request before releasing to move it. + - title: Other changes + labels: + - '*' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 805d7bf..13992b7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,6 +8,8 @@ name: Deploy # it under the beta flag on npm. Note: if no '#patch', '#minor', or '#major' flag is present in the latest commit # AND the package version is not bumped manually the publish step will fail because we can not publish to an existing # version. +# On production, a GitHub release with generated notes is created once npm has accepted the version. +# See .github/release.yml for how the notes are grouped. # GitHub repo configuration: # 1. Go to Manage access and add 'Github Actions' team with role: admin. @@ -53,6 +55,8 @@ jobs: with: # The token below is only necessary if you want to push the version bump to a protected branch token: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }} + # Full history and tags, so the release step can find the previous tag. + fetch-depth: 0 # Set git config to reflect Homey Github Actions Bot user - name: Set up HomeyGithubActionsBot git user @@ -128,3 +132,21 @@ jobs: npm publish --provenance --tag beta VERSION="$(node -p "require('./package.json').version")@beta" echo package_version=${VERSION} >> $GITHUB_ENV + + # After the publish, and starting from the previous release rather than the previous tag, + # because a failed publish leaves an orphan tag (v2.7.0, v3.1.0). The fallback is for the + # first run. If this step fails, create the release by hand: a rerun would bump again. + - name: Create GitHub release + if: github.ref == 'refs/heads/production' + env: + GH_TOKEN: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }} + run: | + TAG="v$(node -p "require('./package.json').version")" + PREVIOUS_TAG="$(gh release view --json tagName -q .tagName || git describe --tags --abbrev=0 "$TAG^")" + # The notes list each pull request title as is. Drop its conventional-commit prefix, which the + # category heading above it already says. + gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ + -f tag_name="$TAG" -f previous_tag_name="$PREVIOUS_TAG" --jq .body \ + | node -e "process.stdout.write(require('node:fs').readFileSync(0, 'utf8').replace(/^\* \w+(\([^)]*\))?!?: (.)/gm, (_, _scope, first) => '* ' + first.toUpperCase()))" \ + > release-notes.md + gh release create "$TAG" --verify-tag --title "$TAG" --notes-file release-notes.md diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml new file mode 100644 index 0000000..4e87d88 --- /dev/null +++ b/.github/workflows/label-pr.yml @@ -0,0 +1,53 @@ +name: Label pull request + +# Labels each pull request from the conventional-commit prefix of its title, so the release +# notes can sort it (see .github/release.yml). Re-runs when the title is edited. +# +# pull_request_target so pull requests from forks get a token that can label. This is safe +# because the job never checks out or runs code from the pull request. +on: + pull_request_target: + types: [opened, edited, reopened] + +permissions: + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v9 + with: + script: | + const MANAGED_LABELS = ['breaking', 'feature', 'fix', 'internal', 'release']; + const INTERNAL_TYPES = ['build', 'chore', 'ci', 'docs', 'refactor', 'style', 'test']; + + function labelFor(title, baseRef) { + if (baseRef === 'production') return 'release'; + const match = /^(\w+)(?:\([^)]*\))?(!)?:/.exec(title); + if (!match) return null; + const [, type, breaking] = match; + if (breaking) return 'breaking'; + if (type === 'feat') return 'feature'; + if (type === 'fix' || type === 'perf') return 'fix'; + if (INTERNAL_TYPES.includes(type)) return 'internal'; + return null; + } + + if (context.payload.action === 'edited' && !context.payload.changes.title) return; + + const pullRequest = context.payload.pull_request; + const wanted = labelFor(pullRequest.title, pullRequest.base.ref); + // No conventional prefix: leave any label a maintainer set by hand. + if (!wanted) return; + const current = pullRequest.labels.map(label => label.name); + const issue = { ...context.repo, issue_number: pullRequest.number }; + + for (const name of current) { + if (MANAGED_LABELS.includes(name) && name !== wanted) { + await github.rest.issues.removeLabel({ ...issue, name }); + } + } + if (!current.includes(wanted)) { + await github.rest.issues.addLabels({ ...issue, labels: [wanted] }); + } From 276dcb488aa8156a26664384b27d2f18386a432a Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 10:55:43 +0200 Subject: [PATCH 2/9] ci: Drop the testing branch and beta publishing The deploy workflow published betas from a testing branch, and Lint, Test and Generate TypeScript Types listed it as a trigger. That branch no longer exists and npm has no beta dist-tag, so the triggers, the job condition and the beta publish step go. The deploy job now runs on production only, which also makes the per-step production checks on Publish and on the release step redundant. Publish also stops writing package_version to the job environment; nothing read it. --- .github/workflows/deploy.yml | 38 ++++++++-------------------- .github/workflows/generate-types.yml | 1 - .github/workflows/lint.yml | 1 - .github/workflows/test.yml | 1 - 4 files changed, 11 insertions(+), 30 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 13992b7..ccf976e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,14 +1,13 @@ name: Deploy # About this workflow: -# It is triggered on push events to branches production and testing. Then it performs a checkout of the current repo +# It is triggered on push events to the production branch. Then it performs a checkout of the current repo # and sets up a node environment (see .nvmrc). Following, it will run `npm ci` to build the package. Next, it will look at your # commit message, if it includes '#patch', '#minor', or '#major' it will bump the package version accordingly. -# Finally, the `npm publish` command will be run, when on branch testing it will run `npm publish --tag beta` to publish -# it under the beta flag on npm. Note: if no '#patch', '#minor', or '#major' flag is present in the latest commit -# AND the package version is not bumped manually the publish step will fail because we can not publish to an existing -# version. -# On production, a GitHub release with generated notes is created once npm has accepted the version. +# Finally, the `npm publish` command will be run. Note: if no '#patch', '#minor', or '#major' flag is present in the latest +# commit AND the package version is not bumped manually the publish step will fail because we can not publish to an +# existing version. +# Once npm has accepted the version, a GitHub release with generated notes is created. # See .github/release.yml for how the notes are grouped. # GitHub repo configuration: @@ -18,13 +17,12 @@ name: Deploy # Note: make sure to commit package-lock.json, this is needed for `npm ci`. -# Defines the trigger for this action, in general you want it to run when pushing to production | testing. For more +# Defines the trigger for this action, in general you want it to run when pushing to production. For more # information see: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#about-workflow-events) on: push: branches: - production - - testing workflow_dispatch: inputs: versionBumpType: @@ -35,9 +33,9 @@ jobs: deploy_to_npm: name: Version Bump & Deploy to NPM - # Only run this job if initiator is not the Homey Github Actions Bot to prevent loops and check if if a version bump is provided and current branch is production or testing + # Only run this job if initiator is not the Homey Github Actions Bot to prevent loops and check if a version bump is provided and current branch is production if: github.actor != 'homey-bot' - && (github.ref == 'refs/heads/production' || github.ref == 'refs/heads/testing') + && github.ref == 'refs/heads/production' && (github.event.inputs.versionBumpType == 'patch' || github.event.inputs.versionBumpType == 'minor' || github.event.inputs.versionBumpType == 'major' || contains(github.event.head_commit.message, '#patch') || contains(github.event.head_commit.message, '#minor') || contains(github.event.head_commit.message, '#major')) runs-on: ubuntu-latest @@ -83,8 +81,8 @@ jobs: run: npm ci # Nothing below this point runs if the package does not lint, format or test - # cleanly, so a push to testing cannot publish a beta that was never checked. - # Ahead of the docs build, which is the slowest step here. + # cleanly, so nothing reaches npm that was never checked. Ahead of the docs build, + # which is the slowest step here. - name: Validate run: | npm run lint @@ -115,29 +113,15 @@ jobs: git pull git push --follow-tags - # Publish when this action is running on branch production. # No NPM_AUTH_TOKEN: auth is brokered via OIDC against the npm # Trusted Publisher configured on the package on npmjs.com. - name: Publish - if: github.ref == 'refs/heads/production' - run: | - npm publish --provenance - VERSION="$(node -p "require('./package.json').version")" - echo package_version=${VERSION} >> $GITHUB_ENV - - # Publish to beta when this action is running on branch testing. - - name: Publish to beta - if: github.ref == 'refs/heads/testing' - run: | - npm publish --provenance --tag beta - VERSION="$(node -p "require('./package.json').version")@beta" - echo package_version=${VERSION} >> $GITHUB_ENV + run: npm publish --provenance # After the publish, and starting from the previous release rather than the previous tag, # because a failed publish leaves an orphan tag (v2.7.0, v3.1.0). The fallback is for the # first run. If this step fails, create the release by hand: a rerun would bump again. - name: Create GitHub release - if: github.ref == 'refs/heads/production' env: GH_TOKEN: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }} run: | diff --git a/.github/workflows/generate-types.yml b/.github/workflows/generate-types.yml index 3dc4791..aa17c5a 100644 --- a/.github/workflows/generate-types.yml +++ b/.github/workflows/generate-types.yml @@ -4,7 +4,6 @@ on: push: branches-ignore: - production - - testing paths: - 'lib/clusters/**/*.js' - 'scripts/generate-types.mts' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 39e9edd..a738130 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,6 @@ on: branches: - master - production - - testing pull_request: jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 540f04d..1eb649a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,6 @@ on: branches: - master - production - - testing pull_request: jobs: From ff36f246c744fd9329042fcd3e89b30270228a3b Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 11:04:10 +0200 Subject: [PATCH 3/9] ci: Fail the release step when generating the notes fails Without an explicit shell, Actions runs a step with bash -e but no pipefail. When the generate-notes call failed, the Node filter after it still exited cleanly on empty input, so the step published a release with empty notes and reported success. That release then became the starting point for the next release's notes, which lost these changes too. shell: bash adds pipefail, so the step now stops instead. --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ccf976e..8a4d108 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -122,6 +122,9 @@ jobs: # because a failed publish leaves an orphan tag (v2.7.0, v3.1.0). The fallback is for the # first run. If this step fails, create the release by hand: a rerun would bump again. - name: Create GitHub release + # Explicit, because only then does Actions add pipefail: without it a failing gh api still + # feeds the Node filter an empty input, and an empty release gets published. + shell: bash env: GH_TOKEN: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }} run: | From b85552df4b39c25bb2dece50be06ea410c6ed866 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 11:04:10 +0200 Subject: [PATCH 4/9] ci: Relabel pull requests when the base branch or the prefix changes The labeler only reacted to title edits, so retargeting a pull request into or out of production kept its old label: a feature moved into production was not marked as a release, and one moved out stayed excluded from the notes. It now also reacts to base branch edits. A title that lost its conventional prefix kept the label the prefix had set, so a chore that became a user-facing change stayed hidden. That label is now removed. Labels a maintainer set by hand on a pull request without a prefix are still left alone. --- .github/workflows/label-pr.yml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 4e87d88..6a4362d 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -34,15 +34,27 @@ jobs: return null; } - if (context.payload.action === 'edited' && !context.payload.changes.title) return; + const { action, changes = {}, pull_request: pullRequest } = context.payload; + // The label depends on the title and the base branch; other edits change nothing. + if (action === 'edited' && !changes.title && !changes.base) return; - const pullRequest = context.payload.pull_request; const wanted = labelFor(pullRequest.title, pullRequest.base.ref); - // No conventional prefix: leave any label a maintainer set by hand. - if (!wanted) return; const current = pullRequest.labels.map(label => label.name); const issue = { ...context.repo, issue_number: pullRequest.number }; + if (!wanted) { + // No conventional prefix: keep a label a maintainer set by hand, but drop the one + // the previous title or base implied, so the change is not left misfiled. + const previous = labelFor( + changes.title?.from ?? pullRequest.title, + changes.base?.ref?.from ?? pullRequest.base.ref, + ); + if (previous && current.includes(previous)) { + await github.rest.issues.removeLabel({ ...issue, name: previous }); + } + return; + } + for (const name of current) { if (MANAGED_LABELS.includes(name) && name !== wanted) { await github.rest.issues.removeLabel({ ...issue, name }); From 224af3c7c7083e1107b97bcf8ab22d2a5bba45e9 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 11:13:43 +0200 Subject: [PATCH 5/9] ci: Start the first release notes from the last version on npm Until the first GitHub release exists, the release step fell back to the nearest earlier tag. A publish that fails after the version bump leaves such a tag behind without an npm version, and this repository has three of them. For v3.1.1 the fallback picked v3.1.0, an orphan, so the notes would have left out everything between v3.0.2 and v3.1.0. The fallback now asks npm for the highest version below the one being released, which only counts versions that were really published. --- .github/workflows/deploy.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8a4d108..7cbbc37 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -119,8 +119,10 @@ jobs: run: npm publish --provenance # After the publish, and starting from the previous release rather than the previous tag, - # because a failed publish leaves an orphan tag (v2.7.0, v3.1.0). The fallback is for the - # first run. If this step fails, create the release by hand: a rerun would bump again. + # because a publish that fails after the version bump leaves a tag without an npm version + # behind (v2.7.0, v3.1.0). Until the first release exists, the start is the last version npm + # actually has, for the same reason. If this step fails, create the release by hand: a rerun + # would bump again. - name: Create GitHub release # Explicit, because only then does Actions add pipefail: without it a failing gh api still # feeds the Node filter an empty input, and an empty release gets published. @@ -128,8 +130,12 @@ jobs: env: GH_TOKEN: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }} run: | - TAG="v$(node -p "require('./package.json').version")" - PREVIOUS_TAG="$(gh release view --json tagName -q .tagName || git describe --tags --abbrev=0 "$TAG^")" + VERSION="$(node -p "require('./package.json').version")" + TAG="v$VERSION" + if ! PREVIOUS_TAG="$(gh release view --json tagName -q .tagName)"; then + PREVIOUS_TAG="v$(npm view "zigbee-clusters@<$VERSION" version --json \ + | node -p "[].concat(JSON.parse(require('node:fs').readFileSync(0, 'utf8'))).at(-1)")" + fi # The notes list each pull request title as is. Drop its conventional-commit prefix, which the # category heading above it already says. gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \ From 537e4c17ed904ed08193992566623957165d1571 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 11:13:43 +0200 Subject: [PATCH 6/9] ci: Label pull requests from their current state, one run at a time The event payload holds the labels as they were when the event fired. A pull request opened as chore: and retitled to feat: before the first run finished produced two runs that both saw no labels: the first added internal, the second added feature without removing internal, and since internal is excluded from the notes, the feature disappeared from them. Each run now reads the pull request as it is when the run starts, and runs for the same pull request are serialized, so a later run sees the labels an earlier one added. --- .github/workflows/label-pr.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 6a4362d..fcda9e5 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -12,6 +12,11 @@ on: permissions: pull-requests: write +# One run at a time per pull request, so two quick edits cannot each add a label the other +# should have removed. +concurrency: + group: label-pr-${{ github.event.pull_request.number }} + jobs: label: runs-on: ubuntu-latest @@ -34,10 +39,17 @@ jobs: return null; } - const { action, changes = {}, pull_request: pullRequest } = context.payload; + const { action, changes = {} } = context.payload; // The label depends on the title and the base branch; other edits change nothing. if (action === 'edited' && !changes.title && !changes.base) return; + // The payload is a snapshot from when the event fired. Read the pull request as it is + // now, so labels an earlier run added are seen and the latest title wins. + const { data: pullRequest } = await github.rest.pulls.get({ + ...context.repo, + pull_number: context.payload.pull_request.number, + }); + const wanted = labelFor(pullRequest.title, pullRequest.base.ref); const current = pullRequest.labels.map(label => label.name); const issue = { ...context.repo, issue_number: pullRequest.number }; From ad97909bcc5897868ad5743f989e9ff28f701869 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 11:09:36 +0200 Subject: [PATCH 7/9] docs: Explain how to release and how the release notes come about The README's release section was a single line. It now covers the master to production pull request with its version marker, why it has to be merged with a merge commit or a squash merge, what the Deploy workflow then does, and how the release notes are grouped by the conventional commit prefix of each pull request title. A Changelog section near the top points users to the GitHub releases. The contributing notes mention Prettier and the pull request title convention, and AGENTS.md states that convention and links to the release section, since the title now decides where a change appears. --- AGENTS.md | 4 ++++ README.md | 42 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8e48c02..56ac6c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,10 @@ Run single test file: node --test test/onOff.js ``` +## Pull Requests and Releases + +Pull request titles use Conventional Commits, `type(scope): Description` with a capital letter after the prefix. The prefix decides where the change lands in the release notes: `feat` under New features, `fix` and `perf` under Fixes, a `!` before the colon under Breaking changes, and `build`, `chore`, `ci`, `docs`, `refactor`, `style` and `test` are left out. Use `feat` or `fix` for anything someone using the library would notice. How a release is cut is described under [Releasing](README.md#releasing). + ## Architecture Zigbee Cluster Library (ZCL) implementation for Node.js, designed for Homey's Zigbee stack. diff --git a/README.md b/README.md index ff4ef98..645d7b3 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ v2.0.0 zclNode.endpoints[1].clusters.basic.readAttributes(['modelId', 'manufacturerName']); ``` -## Release +## Changelog -Merge to production and include `#patch`, `#minor` or `#major` in the PR/commit message. Or merge to production and run the "Deploy" workflow and provide a version bump parameter. +What changed in each version is listed in the [GitHub releases](https://github.com/athombv/node-zigbee-clusters/releases). How those notes are put together is described under [Releasing](#releasing). ## About Zigbee Clusters @@ -338,6 +338,38 @@ The workflow commits updated types back to `develop` if changes are detected. Great if you'd like to contribute to this project, a few things to take note of before submitting a PR: -- This project enforces ESLint, validate by running `npm run lint`. -- This project implements a basic test framework based on node:test, see [test](https://github.com/athombv/node-zigbee-clusters/blob/production/test) directory. -- This project uses several [GitHub Action workflows](https://github.com/athombv/node-zigbee-clusters/blob/production/.github/workflows) (e.g. ESLint, running test and versioning/publishing). +- This project enforces ESLint and Prettier: check with `npm run lint` and `npm run format:check`, and fix formatting with `npm run format:write`. +- Tests use the built-in node:test runner, see the [test](https://github.com/athombv/node-zigbee-clusters/blob/production/test) directory. +- Pull request titles follow [Conventional Commits](https://www.conventionalcommits.org/), for example `fix(onOff): Handle a missing attribute`. The title becomes a line in the release notes, so write it for someone using the library. See [Release notes](#release-notes). +- [GitHub Action workflows](https://github.com/athombv/node-zigbee-clusters/blob/production/.github/workflows) run ESLint, Prettier and the tests, and publish releases. + +## Releasing + +Releases are published from the `production` branch by the Deploy workflow (`.github/workflows/deploy.yml`). + +1. Open a pull request from `master` into `production` with `#patch`, `#minor` or `#major` in its title, for example `Merge master into production (#minor)`. +2. Merge it with a merge commit or a squash merge, not a rebase merge. The workflow reads the marker from the commit message that lands on `production`, and only those two carry the pull request title into it. + +The workflow then lints, checks formatting and runs the tests, builds, bumps the version and pushes the version commit and tag, publishes to npm with [trusted publishing](https://docs.npmjs.com/trusted-publishers), and creates a GitHub release. + +Instead of using a marker, you can also start the Deploy workflow by hand on `production` and pass the version bump as input. + +If creating the GitHub release fails after the npm publish, create it by hand. Running the workflow again would bump the version a second time. + +### Release notes + +GitHub generates the notes from the titles of the pull requests merged since the previous release, grouped by label. A workflow sets that label from the title's prefix, so a pull request lands in the right section on its own: + +| Title prefix | Section | +| ----------------------------------------------------------- | ---------------- | +| `feat` | New features | +| `fix`, `perf` | Fixes | +| `!` before the colon, as in `feat!:` | Breaking changes | +| `build`, `chore`, `ci`, `docs`, `refactor`, `style`, `test` | Left out | +| No prefix | Other changes | + +The prefix itself is stripped from each line, and release pull requests into `production` are left out. + +To move a pull request to another section, change its title, also after it was merged: the label follows. To reword a line, edit the release on GitHub after it is published. + +The grouping is configured in `.github/release.yml`, the labeling in `.github/workflows/label-pr.yml`. From 0fe3fce38c8fa1a0dd36123d41337324c72fea61 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 11:09:55 +0200 Subject: [PATCH 8/9] docs: Correct the type generation section of the README It named scripts/generate-types.js, which is generate-types.mts, and said types regenerate on pushes to develop, a branch this repository does not have. The workflow runs on any branch other than production, for a wider set of paths than listed, and commits back to that branch. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 645d7b3..38fd07a 100644 --- a/README.md +++ b/README.md @@ -323,16 +323,17 @@ To regenerate TypeScript types after modifying cluster definitions: npm run generate-types ``` -This runs `scripts/generate-types.js` which loads all cluster modules and generates typed interfaces. +This runs `scripts/generate-types.mts` which loads all cluster modules and generates typed interfaces. ### Automatic Generation (GitHub Actions) -TypeScript types are automatically regenerated when changes are pushed to the `develop` branch that affect: +TypeScript types are automatically regenerated when a push to any branch other than `production` changes: - `lib/clusters/**/*.js` - cluster definitions -- `scripts/generate-types.js` - the generator script +- `lib/Cluster.js`, `lib/constants.js` and `index.js` +- `scripts/generate-types.mts` and `scripts/template.d.ts.txt` - the generator and its template -The workflow commits updated types back to `develop` if changes are detected. +The workflow commits updated types back to that branch if changes are detected. ## Contributing From 5dc68a5fa11a16cb82242599452698e6952c58c5 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Fri, 23 Oct 2026 11:22:36 +0200 Subject: [PATCH 9/9] ci: Derive the release labels from the title and base branch alone The labeler kept a label set by hand on a pull request without a conventional prefix, and to tell that apart from one it had set itself, it worked out the label the previous title implied. When runs overtook each other that went wrong: a pull request opened as chore: and retitled twice within one run could keep internal, and with it drop out of the release notes. The five managed labels now follow the current title and base branch and nothing else, so the last run always leaves them right, whichever runs before it were skipped. A title without a prefix gets none of them. To move a pull request to another section, retitle it, which is what the README already said. Other labels are left alone. --- .github/release.yml | 4 ++-- .github/workflows/label-pr.yml | 20 +++++--------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.github/release.yml b/.github/release.yml index f98c27c..5b7bfa8 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -16,8 +16,8 @@ changelog: - title: Fixes labels: - fix - # Titles without a conventional prefix, typically outside contributions. Relabel or - # retitle the pull request before releasing to move it. + # Titles without a conventional prefix, typically outside contributions. Retitle the pull + # request to move it. - title: Other changes labels: - '*' diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index fcda9e5..3123d34 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -1,7 +1,8 @@ name: Label pull request # Labels each pull request from the conventional-commit prefix of its title, so the release -# notes can sort it (see .github/release.yml). Re-runs when the title is edited. +# notes can sort it (see .github/release.yml). Re-runs when the title or base branch is edited. +# The five labels below follow the title alone: to move a pull request, retitle it. # # pull_request_target so pull requests from forks get a token that can label. This is safe # because the job never checks out or runs code from the pull request. @@ -50,28 +51,17 @@ jobs: pull_number: context.payload.pull_request.number, }); + // The managed labels are a function of the current title and base only, so whichever + // runs happen to execute, the last one leaves them right. No prefix means none of them. const wanted = labelFor(pullRequest.title, pullRequest.base.ref); const current = pullRequest.labels.map(label => label.name); const issue = { ...context.repo, issue_number: pullRequest.number }; - if (!wanted) { - // No conventional prefix: keep a label a maintainer set by hand, but drop the one - // the previous title or base implied, so the change is not left misfiled. - const previous = labelFor( - changes.title?.from ?? pullRequest.title, - changes.base?.ref?.from ?? pullRequest.base.ref, - ); - if (previous && current.includes(previous)) { - await github.rest.issues.removeLabel({ ...issue, name: previous }); - } - return; - } - for (const name of current) { if (MANAGED_LABELS.includes(name) && name !== wanted) { await github.rest.issues.removeLabel({ ...issue, name }); } } - if (!current.includes(wanted)) { + if (wanted && !current.includes(wanted)) { await github.rest.issues.addLabels({ ...issue, labels: [wanted] }); }