Skip to content

Commit e1a6b00

Browse files
mschmickingclaude
andcommitted
ci: add release automation and CodeQL
Ports the release workflow set from iobroker-sync, adapted for a native addon. - release-please.yml keeps an open release PR on master, deriving the version and changelog from conventional commits. Merging it tags and publishes a GitHub Release. - release.yml publishes to npm on that Release event via trusted publishing (OIDC), so no npm token is stored. Dispatchable by hand, defaulting to a dry run. - pr-title.yml validates PR titles, since release-please reads them to work out the next version. Scope vocabulary retuned for this repo, and 'master' is allowed because release-please titles its own PR chore(master). - codeql.yml analyses c-cpp and javascript. The c-cpp run compiles the addon so CodeQL can observe it, which is where the value is: src/ hand-manages stack indices, buffers and object lifetimes. The tarball guard in release.yml is the important part. This package builds from source on the user's machine, so a tarball missing the vendored Lua sources is unbuildable for everyone who installs it, and npm versions are immutable. It asserts all 29 Lua translation units are present and that build output and node_modules are not. It reads npm pack --json, whose shape changed: npm 11 and earlier emit an array, npm 12 an object keyed by package name. Handled both, verified against real output plus a synthesised array payload and a negative case, rather than assuming the shape the pinned npm happens to produce today. Bootstraps the manifest at 2.0.0 and writes CHANGELOG.md by hand for that version; release-please takes over from 2.0.1. Aligns ci.yml on the same action versions and adds prepublishOnly so a manual publish cannot skip the tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 96cc357 commit e1a6b00

10 files changed

Lines changed: 429 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ jobs:
1919
node: [20, 22, 24]
2020

2121
steps:
22-
- uses: actions/checkout@v4
22+
- uses: actions/checkout@v7
2323

24-
- uses: actions/setup-node@v4
24+
- uses: actions/setup-node@v7
2525
with:
2626
node-version: ${{ matrix.node }}
2727

.github/workflows/codeql.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: CodeQL
2+
3+
# GitHub's own static analysis. Free on public repositories; on a private repo it
4+
# requires GitHub Advanced Security, so this will simply not run until the
5+
# repository is public.
6+
#
7+
# Worth more here than on a pure JavaScript project: src/ is C++ driving a raw C API
8+
# with hand-managed stack indices, string buffers and object lifetimes, which is
9+
# exactly what the c-cpp queries are built to find.
10+
on:
11+
push:
12+
branches: [master]
13+
pull_request:
14+
branches: [master]
15+
# Lets the first scan be triggered without waiting for a push, and lets a rerun be
16+
# forced after a rule update.
17+
workflow_dispatch:
18+
schedule:
19+
# Rules are updated continuously, so a weekly run finds things that did not exist
20+
# as findings when the code was written.
21+
- cron: '0 7 * * 1'
22+
23+
jobs:
24+
analyze:
25+
name: Analyze ${{ matrix.language }}
26+
runs-on: ubuntu-latest
27+
# Skip rather than fail while the repository is private: code scanning needs
28+
# GitHub Advanced Security there, and a permanently red workflow trains people to
29+
# ignore red workflows.
30+
if: ${{ !github.event.repository.private }}
31+
permissions:
32+
security-events: write
33+
contents: read
34+
35+
strategy:
36+
fail-fast: false
37+
matrix:
38+
include:
39+
# The addon has to be compiled for CodeQL to observe it, so the build runs
40+
# between init and analyze below.
41+
- language: c-cpp
42+
build-mode: manual
43+
- language: javascript-typescript
44+
build-mode: none
45+
46+
steps:
47+
- uses: actions/checkout@v7
48+
49+
- uses: github/codeql-action/init@v4.37.3
50+
with:
51+
languages: ${{ matrix.language }}
52+
build-mode: ${{ matrix.build-mode }}
53+
# security-extended adds lower-severity rules, worth it for code that does
54+
# its own memory and stack bookkeeping.
55+
queries: security-extended
56+
# NOTE: path filters are not honoured for compiled languages, so alerts in
57+
# vendor/ will still appear for c-cpp. Dismiss those as "used in tests" or
58+
# "won't fix": Lua 5.1.5 and LuaFileSystem are vendored verbatim and are not
59+
# patched here, so a finding in them is upstream's, not ours. The filter is
60+
# kept because it does apply to the javascript-typescript run.
61+
config: |
62+
paths-ignore:
63+
- vendor/**
64+
65+
- uses: actions/setup-node@v7
66+
if: matrix.build-mode == 'manual'
67+
with:
68+
node-version: 24
69+
70+
- name: Build the addon
71+
if: matrix.build-mode == 'manual'
72+
run: |
73+
npm install --ignore-scripts
74+
npx --yes node-gyp@12 rebuild
75+
76+
- uses: github/codeql-action/analyze@v4.37.3
77+
with:
78+
category: /language:${{ matrix.language }}

.github/workflows/pr-title.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: PR title
2+
3+
# The PR title becomes the squashed commit message, and release-please derives the
4+
# next version from it. A title that does not parse means a release that silently
5+
# does not happen, so it is validated before merge rather than discovered after.
6+
on:
7+
pull_request_target:
8+
types: [opened, edited, synchronize, reopened]
9+
10+
permissions:
11+
pull-requests: read
12+
13+
jobs:
14+
conventional-commit:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: amannn/action-semantic-pull-request@v6
18+
env:
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
with:
21+
# feat -> minor, fix/perf -> patch, feat! or a BREAKING CHANGE footer -> major.
22+
# The rest are patch-or-nothing and never drive a release on their own.
23+
types: |
24+
feat
25+
fix
26+
perf
27+
refactor
28+
docs
29+
test
30+
build
31+
ci
32+
chore
33+
revert
34+
# Optional, but keep the vocabulary small so it stays meaningful. A scope is
35+
# not required, but any scope used must appear here.
36+
#
37+
# 'master' is not a component — it is the branch name release-please puts in
38+
# its own PR title ("chore(master): release 2.1.0"). Without it that title
39+
# fails this check, and once the check is required by a branch ruleset the
40+
# release PR becomes unmergeable.
41+
scopes: |
42+
lua
43+
napi
44+
build
45+
vendor
46+
lfs
47+
deps
48+
docs
49+
release
50+
master
51+
requireScope: false
52+
# Only the trailing full stop is rejected. A lower-case rule is deliberately
53+
# not used: Dependabot capitalises some of its titles ("Bump x from 1 to 2")
54+
# and not others, so it fails dependency PRs for a reason unrelated to
55+
# anything a human chose.
56+
subjectPattern: ^(?!.*\.$).+$
57+
subjectPatternError: |
58+
The subject "{subject}" must not end with a full stop, e.g.
59+
"fix(lua): resolve the stack index before pushing".
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Release PR
2+
3+
# Maintains an open "chore(master): release x.y.z" pull request that accumulates every
4+
# merged change, derives the next version from the conventional-commit history, and
5+
# rewrites CHANGELOG.md. Nothing is versioned or tagged until that PR is merged.
6+
#
7+
# Chosen over fully automatic publishing on purpose: releasing is irreversible on npm,
8+
# so there is a human gate. Merging the release PR tags the commit and publishes the
9+
# GitHub Release, which is what release.yml listens for.
10+
on:
11+
push:
12+
branches: [master]
13+
14+
permissions:
15+
contents: write
16+
pull-requests: write
17+
18+
jobs:
19+
release-please:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: googleapis/release-please-action@v5
23+
with:
24+
# A PAT, not GITHUB_TOKEN. GitHub refuses to trigger workflows from events
25+
# raised with GITHUB_TOKEN, so the release pull request would arrive with
26+
# every check stuck in "action_required" and never run. Falls back to
27+
# GITHUB_TOKEN so the workflow still functions without the secret; the PR
28+
# just will not get checks.
29+
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
30+
# Configuration lives in release-please-config.json and the current version
31+
# in .release-please-manifest.json. Without the manifest the action logs
32+
# 'No version for path .' and never opens a release PR — it cannot know what
33+
# the previous version was.
34+
config-file: release-please-config.json
35+
manifest-file: .release-please-manifest.json

.github/workflows/release.yml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
name: Release to npm
2+
3+
# Fires when release-please publishes a GitHub Release — that only happens when a
4+
# release pull request is merged, which is itself a deliberate act with the version
5+
# and changelog visible for review. So merging the release PR is the single action
6+
# that ships a version.
7+
#
8+
# Still dispatchable by hand, defaulting to a dry run, for re-publishing after a
9+
# failure or validating the tarball without shipping.
10+
on:
11+
release:
12+
types: [published]
13+
workflow_dispatch:
14+
inputs:
15+
dry_run:
16+
description: 'Pack and validate without publishing'
17+
type: boolean
18+
default: true
19+
20+
jobs:
21+
release:
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: read
25+
# Required for OIDC — this is what npm exchanges for a short-lived publish
26+
# credential, and what provenance is derived from. Only works on a public
27+
# repository.
28+
id-token: write
29+
30+
steps:
31+
- uses: actions/checkout@v7
32+
33+
- uses: actions/setup-node@v7
34+
with:
35+
# 24 rather than 22: trusted publishing needs npm >= 11.5.1, and Node 22
36+
# still ships npm 10.x.
37+
node-version: 24
38+
cache: npm
39+
registry-url: https://registry.npmjs.org
40+
41+
- name: Ensure an npm new enough for trusted publishing
42+
run: |
43+
npm install -g npm@^11
44+
npm --version
45+
46+
# Also compiles the addon, since the install script runs node-gyp. Ubuntu is
47+
# unaffected by the Visual Studio detection problem that forces ci.yml to pin
48+
# its own node-gyp on Windows.
49+
- run: npm ci
50+
51+
- name: Test
52+
run: npm test
53+
54+
- name: Refuse to publish a version that already exists
55+
run: |
56+
NAME=$(node -p "require('./package.json').name")
57+
VERSION=$(node -p "require('./package.json').version")
58+
echo "Preparing $NAME@$VERSION"
59+
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
60+
echo "::error::$NAME@$VERSION is already published. Bump the version first."
61+
exit 1
62+
fi
63+
64+
# This package builds from source on the user's machine, so the vendored Lua
65+
# and LuaFileSystem sources are not an optional extra — a tarball missing them
66+
# is unbuildable for everyone who installs it, and npm versions are immutable.
67+
# Cheapest possible check against the most expensive possible mistake.
68+
- name: Verify the tarball can actually build
69+
run: |
70+
npm pack --dry-run --json > pack.json
71+
node -e "
72+
// npm 11 and earlier emit an array of results; npm 12 emits an object
73+
// keyed by package name. Accept either, so an npm upgrade cannot turn
74+
// this guard into a crash — or, worse, into a silent pass.
75+
const raw = require('./pack.json');
76+
const entry = Array.isArray(raw) ? raw[0] : Object.values(raw)[0];
77+
if (!entry || !Array.isArray(entry.files)) {
78+
console.error('could not read the file list from npm pack --json');
79+
process.exit(1);
80+
}
81+
const files = entry.files.map(f => f.path);
82+
83+
const needed = ['index.js', 'binding.gyp', 'src/luastate.cc', 'src/nodelua.cc', 'src/utils.cc', 'vendor/lfs/lfs.c', 'README.md', 'LICENSE.md'];
84+
const missing = needed.filter(n => !files.includes(n));
85+
if (missing.length) {
86+
console.error('missing from tarball:', missing.join(', '));
87+
process.exit(1);
88+
}
89+
90+
// Lua 5.1.5 is 29 translation units; a partial copy links with undefined
91+
// symbols rather than failing loudly at pack time.
92+
const lua = files.filter(f => /^vendor\/lua\/.+\.c\$/.test(f));
93+
if (lua.length !== 29) {
94+
console.error('expected 29 vendored Lua sources, found ' + lua.length);
95+
process.exit(1);
96+
}
97+
98+
// Build output is platform-specific and must never ship; node_modules
99+
// would bloat the tarball and shadow the consumer's own tree.
100+
const leaked = files.filter(f => /^(build|node_modules|test|examples)\//.test(f));
101+
if (leaked.length) {
102+
console.error('unexpected files in tarball:', leaked.join(', '));
103+
process.exit(1);
104+
}
105+
106+
console.log(files.length + ' files, all expected');
107+
"
108+
109+
- name: Pack (dry run)
110+
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
111+
run: npm publish --dry-run
112+
113+
# No NODE_AUTH_TOKEN. Publishing uses npm trusted publishing (OIDC): npm
114+
# verifies this workflow's identity against the trusted publisher configured on
115+
# the package, so there is no long-lived token to leak or rotate.
116+
#
117+
# Provenance is automatic under OIDC for a public package from a public repo,
118+
# so --provenance is not passed explicitly.
119+
# On a release event inputs.dry_run is undefined, so this must not rely on
120+
# negating it — an undefined input would otherwise read as "not a dry run" by
121+
# luck rather than intent.
122+
- name: Publish
123+
if: ${{ github.event_name == 'release' || !inputs.dry_run }}
124+
run: npm publish --access public
125+
126+
# No tagging step: release-please already created the tag and the GitHub
127+
# Release that triggered this run.

.release-please-manifest.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
".": "2.0.0"
3+
}

CHANGELOG.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Changelog
2+
3+
## [2.0.0](https://github.com/mschmicking/node-lua-runner/releases/tag/v2.0.0)
4+
5+
Maintenance release that makes the package build and run on current Node.js.
6+
7+
### Changed
8+
9+
- **Migrated the binding from NAN to Node-API** (`node-addon-api`). NAN tracks V8's unstable C++
10+
API and no longer compiles on current Node — the build failed inside `nan.h` itself. Node-API is
11+
ABI-stable, so a build keeps working across future Node major versions.
12+
- **Vendored Lua 5.1.5 and LuaFileSystem 1.8.0**, compiled into the addon. The prebuilt libraries
13+
are gone. The package now builds on Linux, macOS and Windows, on x64 and ARM64, with no system
14+
Lua to install.
15+
- **LuaJIT replaced by stock Lua 5.1.5.** Only Windows ever linked LuaJIT; macOS already shipped
16+
stock Lua 5.1.5. In exchange, Apple Silicon and ARM64 Linux build at all, which they previously
17+
could not.
18+
- **`require('lfs')` now works on every platform.** It was a Windows-only prebuilt DLL loaded via an
19+
`LUA_CPATH` hack; LuaFileSystem is now compiled in and registered through `package.preload`.
20+
- Node.js 18 or newer is required.
21+
22+
### Fixed
23+
24+
Each of these changes observable behaviour, hence the major version.
25+
26+
- `SetField` pushed its **key** argument as the value, so every assignment wrote the field name into
27+
the field. It also failed to resolve a relative stack index before pushing the value, which put
28+
Lua into an unprotected error and **aborted the process**.
29+
- `LoadFile` and `LoadString` were bound to the `DoFile`/`DoString` handlers, so they executed the
30+
chunk instead of only compiling it. The correct implementations were unreachable.
31+
- Lua booleans converted to the numbers `1` and `0` rather than `true` and `false`.
32+
- `Push` truncated numbers through `lua_pushinteger`, turning `3.5` into `3`.
33+
- `AddPackagePath` appended to `package.path` without a separator, corrupting the last entry so
34+
`require` usually failed, and interpolated the path into generated Lua source where a quote could
35+
break out of the string literal.
36+
- Table conversion used a hardcoded relative stack index and only worked when the table happened to
37+
be on top of the stack.
38+
- `get_str` allocated on every string argument and never freed it.
39+
- Six `sprintf` calls formatted arbitrary-length Lua error messages into a fixed 1024-byte stack
40+
buffer.
41+
- `~LuaState` never called `lua_close`, leaking the interpreter; calling `Close` twice was a
42+
use-after-free. `Close` is now idempotent and later use of a closed state throws.
43+
- `SetField` and `GetField` reject non-table targets instead of letting Lua abort the process.
44+
- Registered callbacks are looked up through a closure upvalue rather than a global singleton, so
45+
separate `LuaState` instances no longer clash.
46+
47+
### Added
48+
49+
- Test suite covering the public API, with regression tests pinning each fix above.
50+
- CI across Linux, macOS and Windows on Node 20, 22 and 24.
51+
52+
---
53+
54+
Releases from 2.0.1 onward are generated by release-please from conventional commits.

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,20 @@ npm install
334334
npm test
335335
```
336336

337+
### Releasing
338+
339+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/); the pull request
340+
title is what matters, since it becomes the squashed commit message.
341+
342+
release-please keeps an open `chore(master): release x.y.z` pull request that accumulates merged
343+
changes, works out the next version and rewrites `CHANGELOG.md`. Merging that pull request tags the
344+
commit and publishes a GitHub Release, which is what triggers the npm publish. So merging the
345+
release pull request is the single deliberate act that ships a version — nothing publishes on an
346+
ordinary merge to `master`.
347+
348+
Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) over OIDC, so
349+
there is no npm token stored in this repository.
350+
337351
## License
338352

339353
ISC — see [LICENSE.md](LICENSE.md), which also covers the vendored Lua and LuaFileSystem sources.

0 commit comments

Comments
 (0)