Skip to content
Merged
23 changes: 23 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -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. Retitle the pull
# request to move it.
- title: Other changes
labels:
- '*'
61 changes: 38 additions & 23 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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.
# 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:
# 1. Go to Manage access and add 'Github Actions' team with role: admin.
Expand All @@ -16,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:
Expand All @@ -33,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
Expand All @@ -53,6 +53,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
Expand All @@ -79,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
Expand Down Expand Up @@ -111,20 +113,33 @@ 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

# After the publish, and starting from the previous release rather than the previous tag,
# 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.
shell: bash
env:
GH_TOKEN: ${{ secrets.HOMEY_GITHUB_ACTIONS_BOT_PERSONAL_ACCESS_TOKEN }}
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
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" \
-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
1 change: 0 additions & 1 deletion .github/workflows/generate-types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches-ignore:
- production
- testing
paths:
- 'lib/clusters/**/*.js'
- 'scripts/generate-types.mts'
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/label-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 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.
on:
pull_request_target:
types: [opened, edited, reopened]

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
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;
}

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,
});

// 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 };

for (const name of current) {
if (MANAGED_LABELS.includes(name) && name !== wanted) {
await github.rest.issues.removeLabel({ ...issue, name });
}
}
if (wanted && !current.includes(wanted)) {
await github.rest.issues.addLabels({ ...issue, labels: [wanted] });
}
1 change: 0 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ on:
branches:
- master
- production
- testing
pull_request:

jobs:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ on:
branches:
- master
- production
- testing
pull_request:

jobs:
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
51 changes: 42 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -323,21 +323,54 @@ 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

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`.
Loading