Skip to content

Commit a90fb76

Browse files
authored
Merge pull request #252 from athombv/chore/release-notes
ci: Create a GitHub release on publish and drop the testing branch
2 parents 4b642ca + 5dc68a5 commit a90fb76

8 files changed

Lines changed: 174 additions & 35 deletions

File tree

‎.github/release.yml‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Categories for the notes GitHub generates when the deploy workflow creates a release.
2+
# The labels are set from the conventional-commit prefix of the pull request title by
3+
# .github/workflows/label-pr.yml.
4+
changelog:
5+
exclude:
6+
labels:
7+
- internal
8+
- release
9+
categories:
10+
- title: Breaking changes
11+
labels:
12+
- breaking
13+
- title: New features
14+
labels:
15+
- feature
16+
- title: Fixes
17+
labels:
18+
- fix
19+
# Titles without a conventional prefix, typically outside contributions. Retitle the pull
20+
# request to move it.
21+
- title: Other changes
22+
labels:
23+
- '*'

‎.github/workflows/deploy.yml‎

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
name: Deploy
22

33
# About this workflow:
4-
# It is triggered on push events to branches production and testing. Then it performs a checkout of the current repo
4+
# It is triggered on push events to the production branch. Then it performs a checkout of the current repo
55
# and sets up a node environment (see .nvmrc). Following, it will run `npm ci` to build the package. Next, it will look at your
66
# commit message, if it includes '#patch', '#minor', or '#major' it will bump the package version accordingly.
7-
# Finally, the `npm publish` command will be run, when on branch testing it will run `npm publish --tag beta` to publish
8-
# it under the beta flag on npm. Note: if no '#patch', '#minor', or '#major' flag is present in the latest commit
9-
# AND the package version is not bumped manually the publish step will fail because we can not publish to an existing
10-
# version.
7+
# Finally, the `npm publish` command will be run. Note: if no '#patch', '#minor', or '#major' flag is present in the latest
8+
# commit AND the package version is not bumped manually the publish step will fail because we can not publish to an
9+
# existing version.
10+
# Once npm has accepted the version, a GitHub release with generated notes is created.
11+
# See .github/release.yml for how the notes are grouped.
1112

1213
# GitHub repo configuration:
1314
# 1. Go to Manage access and add 'Github Actions' team with role: admin.
@@ -16,13 +17,12 @@ name: Deploy
1617

1718
# Note: make sure to commit package-lock.json, this is needed for `npm ci`.
1819

19-
# Defines the trigger for this action, in general you want it to run when pushing to production | testing. For more
20+
# Defines the trigger for this action, in general you want it to run when pushing to production. For more
2021
# information see: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#about-workflow-events)
2122
on:
2223
push:
2324
branches:
2425
- production
25-
- testing
2626
workflow_dispatch:
2727
inputs:
2828
versionBumpType:
@@ -33,9 +33,9 @@ jobs:
3333
deploy_to_npm:
3434
name: Version Bump & Deploy to NPM
3535

36-
# 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
36+
# 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
3737
if: github.actor != 'homey-bot'
38-
&& (github.ref == 'refs/heads/production' || github.ref == 'refs/heads/testing')
38+
&& github.ref == 'refs/heads/production'
3939
&& (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'))
4040

4141
runs-on: ubuntu-latest
@@ -53,6 +53,8 @@ jobs:
5353
with:
5454
# The token below is only necessary if you want to push the version bump to a protected branch
5555
token: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }}
56+
# Full history and tags, so the release step can find the previous tag.
57+
fetch-depth: 0
5658

5759
# Set git config to reflect Homey Github Actions Bot user
5860
- name: Set up HomeyGithubActionsBot git user
@@ -79,8 +81,8 @@ jobs:
7981
run: npm ci
8082

8183
# Nothing below this point runs if the package does not lint, format or test
82-
# cleanly, so a push to testing cannot publish a beta that was never checked.
83-
# Ahead of the docs build, which is the slowest step here.
84+
# cleanly, so nothing reaches npm that was never checked. Ahead of the docs build,
85+
# which is the slowest step here.
8486
- name: Validate
8587
run: |
8688
npm run lint
@@ -111,20 +113,33 @@ jobs:
111113
git pull
112114
git push --follow-tags
113115
114-
# Publish when this action is running on branch production.
115116
# No NPM_AUTH_TOKEN: auth is brokered via OIDC against the npm
116117
# Trusted Publisher configured on the package on npmjs.com.
117118
- name: Publish
118-
if: github.ref == 'refs/heads/production'
119+
run: npm publish --provenance
120+
121+
# After the publish, and starting from the previous release rather than the previous tag,
122+
# because a publish that fails after the version bump leaves a tag without an npm version
123+
# behind (v2.7.0, v3.1.0). Until the first release exists, the start is the last version npm
124+
# actually has, for the same reason. If this step fails, create the release by hand: a rerun
125+
# would bump again.
126+
- name: Create GitHub release
127+
# Explicit, because only then does Actions add pipefail: without it a failing gh api still
128+
# feeds the Node filter an empty input, and an empty release gets published.
129+
shell: bash
130+
env:
131+
GH_TOKEN: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }}
119132
run: |
120-
npm publish --provenance
121133
VERSION="$(node -p "require('./package.json').version")"
122-
echo package_version=${VERSION} >> $GITHUB_ENV
123-
124-
# Publish to beta when this action is running on branch testing.
125-
- name: Publish to beta
126-
if: github.ref == 'refs/heads/testing'
127-
run: |
128-
npm publish --provenance --tag beta
129-
VERSION="$(node -p "require('./package.json').version")@beta"
130-
echo package_version=${VERSION} >> $GITHUB_ENV
134+
TAG="v$VERSION"
135+
if ! PREVIOUS_TAG="$(gh release view --json tagName -q .tagName)"; then
136+
PREVIOUS_TAG="v$(npm view "zigbee-clusters@<$VERSION" version --json \
137+
| node -p "[].concat(JSON.parse(require('node:fs').readFileSync(0, 'utf8'))).at(-1)")"
138+
fi
139+
# The notes list each pull request title as is. Drop its conventional-commit prefix, which the
140+
# category heading above it already says.
141+
gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" \
142+
-f tag_name="$TAG" -f previous_tag_name="$PREVIOUS_TAG" --jq .body \
143+
| node -e "process.stdout.write(require('node:fs').readFileSync(0, 'utf8').replace(/^\* \w+(\([^)]*\))?!?: (.)/gm, (_, _scope, first) => '* ' + first.toUpperCase()))" \
144+
> release-notes.md
145+
gh release create "$TAG" --verify-tag --title "$TAG" --notes-file release-notes.md

‎.github/workflows/generate-types.yml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches-ignore:
66
- production
7-
- testing
87
paths:
98
- 'lib/clusters/**/*.js'
109
- 'scripts/generate-types.mts'

‎.github/workflows/label-pr.yml‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: Label pull request
2+
3+
# Labels each pull request from the conventional-commit prefix of its title, so the release
4+
# notes can sort it (see .github/release.yml). Re-runs when the title or base branch is edited.
5+
# The five labels below follow the title alone: to move a pull request, retitle it.
6+
#
7+
# pull_request_target so pull requests from forks get a token that can label. This is safe
8+
# because the job never checks out or runs code from the pull request.
9+
on:
10+
pull_request_target:
11+
types: [opened, edited, reopened]
12+
13+
permissions:
14+
pull-requests: write
15+
16+
# One run at a time per pull request, so two quick edits cannot each add a label the other
17+
# should have removed.
18+
concurrency:
19+
group: label-pr-${{ github.event.pull_request.number }}
20+
21+
jobs:
22+
label:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/github-script@v9
26+
with:
27+
script: |
28+
const MANAGED_LABELS = ['breaking', 'feature', 'fix', 'internal', 'release'];
29+
const INTERNAL_TYPES = ['build', 'chore', 'ci', 'docs', 'refactor', 'style', 'test'];
30+
31+
function labelFor(title, baseRef) {
32+
if (baseRef === 'production') return 'release';
33+
const match = /^(\w+)(?:\([^)]*\))?(!)?:/.exec(title);
34+
if (!match) return null;
35+
const [, type, breaking] = match;
36+
if (breaking) return 'breaking';
37+
if (type === 'feat') return 'feature';
38+
if (type === 'fix' || type === 'perf') return 'fix';
39+
if (INTERNAL_TYPES.includes(type)) return 'internal';
40+
return null;
41+
}
42+
43+
const { action, changes = {} } = context.payload;
44+
// The label depends on the title and the base branch; other edits change nothing.
45+
if (action === 'edited' && !changes.title && !changes.base) return;
46+
47+
// The payload is a snapshot from when the event fired. Read the pull request as it is
48+
// now, so labels an earlier run added are seen and the latest title wins.
49+
const { data: pullRequest } = await github.rest.pulls.get({
50+
...context.repo,
51+
pull_number: context.payload.pull_request.number,
52+
});
53+
54+
// The managed labels are a function of the current title and base only, so whichever
55+
// runs happen to execute, the last one leaves them right. No prefix means none of them.
56+
const wanted = labelFor(pullRequest.title, pullRequest.base.ref);
57+
const current = pullRequest.labels.map(label => label.name);
58+
const issue = { ...context.repo, issue_number: pullRequest.number };
59+
60+
for (const name of current) {
61+
if (MANAGED_LABELS.includes(name) && name !== wanted) {
62+
await github.rest.issues.removeLabel({ ...issue, name });
63+
}
64+
}
65+
if (wanted && !current.includes(wanted)) {
66+
await github.rest.issues.addLabels({ ...issue, labels: [wanted] });
67+
}

‎.github/workflows/lint.yml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ on:
1313
branches:
1414
- master
1515
- production
16-
- testing
1716
pull_request:
1817

1918
jobs:

‎.github/workflows/test.yml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ on:
1313
branches:
1414
- master
1515
- production
16-
- testing
1716
pull_request:
1817

1918
jobs:

‎AGENTS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ Run single test file:
2222
node --test test/onOff.js
2323
```
2424

25+
## Pull Requests and Releases
26+
27+
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).
28+
2529
## Architecture
2630

2731
Zigbee Cluster Library (ZCL) implementation for Node.js, designed for Homey's Zigbee stack.

‎README.md‎

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ v2.0.0
3333
zclNode.endpoints[1].clusters.basic.readAttributes(['modelId', 'manufacturerName']);
3434
```
3535

36-
## Release
36+
## Changelog
3737

38-
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.
38+
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).
3939

4040
## About Zigbee Clusters
4141

@@ -323,21 +323,54 @@ To regenerate TypeScript types after modifying cluster definitions:
323323
npm run generate-types
324324
```
325325

326-
This runs `scripts/generate-types.js` which loads all cluster modules and generates typed interfaces.
326+
This runs `scripts/generate-types.mts` which loads all cluster modules and generates typed interfaces.
327327

328328
### Automatic Generation (GitHub Actions)
329329

330-
TypeScript types are automatically regenerated when changes are pushed to the `develop` branch that affect:
330+
TypeScript types are automatically regenerated when a push to any branch other than `production` changes:
331331

332332
- `lib/clusters/**/*.js` - cluster definitions
333-
- `scripts/generate-types.js` - the generator script
333+
- `lib/Cluster.js`, `lib/constants.js` and `index.js`
334+
- `scripts/generate-types.mts` and `scripts/template.d.ts.txt` - the generator and its template
334335

335-
The workflow commits updated types back to `develop` if changes are detected.
336+
The workflow commits updated types back to that branch if changes are detected.
336337

337338
## Contributing
338339

339340
Great if you'd like to contribute to this project, a few things to take note of before submitting a PR:
340341

341-
- This project enforces ESLint, validate by running `npm run lint`.
342-
- This project implements a basic test framework based on node:test, see [test](https://github.com/athombv/node-zigbee-clusters/blob/production/test) directory.
343-
- 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).
342+
- This project enforces ESLint and Prettier: check with `npm run lint` and `npm run format:check`, and fix formatting with `npm run format:write`.
343+
- Tests use the built-in node:test runner, see the [test](https://github.com/athombv/node-zigbee-clusters/blob/production/test) directory.
344+
- 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).
345+
- [GitHub Action workflows](https://github.com/athombv/node-zigbee-clusters/blob/production/.github/workflows) run ESLint, Prettier and the tests, and publish releases.
346+
347+
## Releasing
348+
349+
Releases are published from the `production` branch by the Deploy workflow (`.github/workflows/deploy.yml`).
350+
351+
1. Open a pull request from `master` into `production` with `#patch`, `#minor` or `#major` in its title, for example `Merge master into production (#minor)`.
352+
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.
353+
354+
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.
355+
356+
Instead of using a marker, you can also start the Deploy workflow by hand on `production` and pass the version bump as input.
357+
358+
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.
359+
360+
### Release notes
361+
362+
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:
363+
364+
| Title prefix | Section |
365+
| ----------------------------------------------------------- | ---------------- |
366+
| `feat` | New features |
367+
| `fix`, `perf` | Fixes |
368+
| `!` before the colon, as in `feat!:` | Breaking changes |
369+
| `build`, `chore`, `ci`, `docs`, `refactor`, `style`, `test` | Left out |
370+
| No prefix | Other changes |
371+
372+
The prefix itself is stripped from each line, and release pull requests into `production` are left out.
373+
374+
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.
375+
376+
The grouping is configured in `.github/release.yml`, the labeling in `.github/workflows/label-pr.yml`.

0 commit comments

Comments
 (0)