Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ jobs:
node: [20, 22, 24]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}

Expand Down
78 changes: 78 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CodeQL

# GitHub's own static analysis. Free on public repositories; on a private repo it
# requires GitHub Advanced Security, so this will simply not run until the
# repository is public.
#
# Worth more here than on a pure JavaScript project: src/ is C++ driving a raw C API
# with hand-managed stack indices, string buffers and object lifetimes, which is
# exactly what the c-cpp queries are built to find.
on:
push:
branches: [master]
pull_request:
branches: [master]
# Lets the first scan be triggered without waiting for a push, and lets a rerun be
# forced after a rule update.
workflow_dispatch:
schedule:
# Rules are updated continuously, so a weekly run finds things that did not exist
# as findings when the code was written.
- cron: '0 7 * * 1'

jobs:
analyze:
name: Analyze ${{ matrix.language }}
runs-on: ubuntu-latest
# Skip rather than fail while the repository is private: code scanning needs
# GitHub Advanced Security there, and a permanently red workflow trains people to
# ignore red workflows.
if: ${{ !github.event.repository.private }}
permissions:
security-events: write
contents: read

strategy:
fail-fast: false
matrix:
include:
# The addon has to be compiled for CodeQL to observe it, so the build runs
# between init and analyze below.
- language: c-cpp
build-mode: manual
- language: javascript-typescript
build-mode: none

steps:
- uses: actions/checkout@v7

- uses: github/codeql-action/init@v4.37.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# security-extended adds lower-severity rules, worth it for code that does
# its own memory and stack bookkeeping.
queries: security-extended
# NOTE: path filters are not honoured for compiled languages, so alerts in
# vendor/ will still appear for c-cpp. Dismiss those as "used in tests" or
# "won't fix": Lua 5.1.5 and LuaFileSystem are vendored verbatim and are not
# patched here, so a finding in them is upstream's, not ours. The filter is
# kept because it does apply to the javascript-typescript run.
config: |
paths-ignore:
- vendor/**

- uses: actions/setup-node@v7
if: matrix.build-mode == 'manual'
with:
node-version: 24

- name: Build the addon
if: matrix.build-mode == 'manual'
run: |
npm install --ignore-scripts
npx --yes node-gyp@12 rebuild

- uses: github/codeql-action/analyze@v4.37.3
with:
category: /language:${{ matrix.language }}
59 changes: 59 additions & 0 deletions .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: PR title

# The PR title becomes the squashed commit message, and release-please derives the
# next version from it. A title that does not parse means a release that silently
# does not happen, so it is validated before merge rather than discovered after.
on:
pull_request_target:
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: read

jobs:
conventional-commit:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# feat -> minor, fix/perf -> patch, feat! or a BREAKING CHANGE footer -> major.
# The rest are patch-or-nothing and never drive a release on their own.
types: |
feat
fix
perf
refactor
docs
test
build
ci
chore
revert
# Optional, but keep the vocabulary small so it stays meaningful. A scope is
# not required, but any scope used must appear here.
#
# 'master' is not a component — it is the branch name release-please puts in
# its own PR title ("chore(master): release 2.1.0"). Without it that title
# fails this check, and once the check is required by a branch ruleset the
# release PR becomes unmergeable.
scopes: |
lua
napi
build
vendor
lfs
deps
docs
release
master
requireScope: false
# Only the trailing full stop is rejected. A lower-case rule is deliberately
# not used: Dependabot capitalises some of its titles ("Bump x from 1 to 2")
# and not others, so it fails dependency PRs for a reason unrelated to
# anything a human chose.
subjectPattern: ^(?!.*\.$).+$
subjectPatternError: |
The subject "{subject}" must not end with a full stop, e.g.
"fix(lua): resolve the stack index before pushing".
35 changes: 35 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Release PR

# Maintains an open "chore(master): release x.y.z" pull request that accumulates every
# merged change, derives the next version from the conventional-commit history, and
# rewrites CHANGELOG.md. Nothing is versioned or tagged until that PR is merged.
#
# Chosen over fully automatic publishing on purpose: releasing is irreversible on npm,
# so there is a human gate. Merging the release PR tags the commit and publishes the
# GitHub Release, which is what release.yml listens for.
on:
push:
branches: [master]

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v5
with:
# A PAT, not GITHUB_TOKEN. GitHub refuses to trigger workflows from events
# raised with GITHUB_TOKEN, so the release pull request would arrive with
# every check stuck in "action_required" and never run. Falls back to
# GITHUB_TOKEN so the workflow still functions without the secret; the PR
# just will not get checks.
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
# Configuration lives in release-please-config.json and the current version
# in .release-please-manifest.json. Without the manifest the action logs
# 'No version for path .' and never opens a release PR — it cannot know what
# the previous version was.
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
127 changes: 127 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: Release to npm

# Fires when release-please publishes a GitHub Release — that only happens when a
# release pull request is merged, which is itself a deliberate act with the version
# and changelog visible for review. So merging the release PR is the single action
# that ships a version.
#
# Still dispatchable by hand, defaulting to a dry run, for re-publishing after a
# failure or validating the tarball without shipping.
on:
release:
types: [published]
workflow_dispatch:
inputs:
dry_run:
description: 'Pack and validate without publishing'
type: boolean
default: true

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: read
# Required for OIDC — this is what npm exchanges for a short-lived publish
# credential, and what provenance is derived from. Only works on a public
# repository.
id-token: write

steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
# 24 rather than 22: trusted publishing needs npm >= 11.5.1, and Node 22
# still ships npm 10.x.
node-version: 24
cache: npm
registry-url: https://registry.npmjs.org

- name: Ensure an npm new enough for trusted publishing
run: |
npm install -g npm@^11
npm --version

# Also compiles the addon, since the install script runs node-gyp. Ubuntu is
# unaffected by the Visual Studio detection problem that forces ci.yml to pin
# its own node-gyp on Windows.
- run: npm ci

- name: Test
run: npm test

- name: Refuse to publish a version that already exists
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
echo "Preparing $NAME@$VERSION"
if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
echo "::error::$NAME@$VERSION is already published. Bump the version first."
exit 1
fi

# This package builds from source on the user's machine, so the vendored Lua
# and LuaFileSystem sources are not an optional extra — a tarball missing them
# is unbuildable for everyone who installs it, and npm versions are immutable.
# Cheapest possible check against the most expensive possible mistake.
- name: Verify the tarball can actually build
run: |
npm pack --dry-run --json > pack.json
node -e "
// npm 11 and earlier emit an array of results; npm 12 emits an object
// keyed by package name. Accept either, so an npm upgrade cannot turn
// this guard into a crash — or, worse, into a silent pass.
const raw = require('./pack.json');
const entry = Array.isArray(raw) ? raw[0] : Object.values(raw)[0];
if (!entry || !Array.isArray(entry.files)) {
console.error('could not read the file list from npm pack --json');
process.exit(1);
}
const files = entry.files.map(f => f.path);

const needed = ['index.js', 'binding.gyp', 'src/luastate.cc', 'src/nodelua.cc', 'src/utils.cc', 'vendor/lfs/lfs.c', 'README.md', 'LICENSE.md'];
const missing = needed.filter(n => !files.includes(n));
if (missing.length) {
console.error('missing from tarball:', missing.join(', '));
process.exit(1);
}

// Lua 5.1.5 is 29 translation units; a partial copy links with undefined
// symbols rather than failing loudly at pack time.
const lua = files.filter(f => /^vendor\/lua\/.+\.c\$/.test(f));
if (lua.length !== 29) {
console.error('expected 29 vendored Lua sources, found ' + lua.length);
process.exit(1);
}

// Build output is platform-specific and must never ship; node_modules
// would bloat the tarball and shadow the consumer's own tree.
const leaked = files.filter(f => /^(build|node_modules|test|examples)\//.test(f));
if (leaked.length) {
console.error('unexpected files in tarball:', leaked.join(', '));
process.exit(1);
}

console.log(files.length + ' files, all expected');
"

- name: Pack (dry run)
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
run: npm publish --dry-run

# No NODE_AUTH_TOKEN. Publishing uses npm trusted publishing (OIDC): npm
# verifies this workflow's identity against the trusted publisher configured on
# the package, so there is no long-lived token to leak or rotate.
#
# Provenance is automatic under OIDC for a public package from a public repo,
# so --provenance is not passed explicitly.
# On a release event inputs.dry_run is undefined, so this must not rely on
# negating it — an undefined input would otherwise read as "not a dry run" by
# luck rather than intent.
- name: Publish
if: ${{ github.event_name == 'release' || !inputs.dry_run }}
run: npm publish --access public

# No tagging step: release-please already created the tag and the GitHub
# Release that triggered this run.
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "2.0.0"
}
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Changelog

## [2.0.0](https://github.com/mschmicking/node-lua-runner/releases/tag/v2.0.0)

Maintenance release that makes the package build and run on current Node.js.

### Changed

- **Migrated the binding from NAN to Node-API** (`node-addon-api`). NAN tracks V8's unstable C++
API and no longer compiles on current Node — the build failed inside `nan.h` itself. Node-API is
ABI-stable, so a build keeps working across future Node major versions.
- **Vendored Lua 5.1.5 and LuaFileSystem 1.8.0**, compiled into the addon. The prebuilt libraries
are gone. The package now builds on Linux, macOS and Windows, on x64 and ARM64, with no system
Lua to install.
- **LuaJIT replaced by stock Lua 5.1.5.** Only Windows ever linked LuaJIT; macOS already shipped
stock Lua 5.1.5. In exchange, Apple Silicon and ARM64 Linux build at all, which they previously
could not.
- **`require('lfs')` now works on every platform.** It was a Windows-only prebuilt DLL loaded via an
`LUA_CPATH` hack; LuaFileSystem is now compiled in and registered through `package.preload`.
- Node.js 18 or newer is required.

### Fixed

Each of these changes observable behaviour, hence the major version.

- `SetField` pushed its **key** argument as the value, so every assignment wrote the field name into
the field. It also failed to resolve a relative stack index before pushing the value, which put
Lua into an unprotected error and **aborted the process**.
- `LoadFile` and `LoadString` were bound to the `DoFile`/`DoString` handlers, so they executed the
chunk instead of only compiling it. The correct implementations were unreachable.
- Lua booleans converted to the numbers `1` and `0` rather than `true` and `false`.
- `Push` truncated numbers through `lua_pushinteger`, turning `3.5` into `3`.
- `AddPackagePath` appended to `package.path` without a separator, corrupting the last entry so
`require` usually failed, and interpolated the path into generated Lua source where a quote could
break out of the string literal.
- Table conversion used a hardcoded relative stack index and only worked when the table happened to
be on top of the stack.
- `get_str` allocated on every string argument and never freed it.
- Six `sprintf` calls formatted arbitrary-length Lua error messages into a fixed 1024-byte stack
buffer.
- `~LuaState` never called `lua_close`, leaking the interpreter; calling `Close` twice was a
use-after-free. `Close` is now idempotent and later use of a closed state throws.
- `SetField` and `GetField` reject non-table targets instead of letting Lua abort the process.
- Registered callbacks are looked up through a closure upvalue rather than a global singleton, so
separate `LuaState` instances no longer clash.

### Added

- Test suite covering the public API, with regression tests pinning each fix above.
- CI across Linux, macOS and Windows on Node 20, 22 and 24.

---

Releases from 2.0.1 onward are generated by release-please from conventional commits.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,20 @@ npm install
npm test
```

### Releasing

Commits follow [Conventional Commits](https://www.conventionalcommits.org/); the pull request
title is what matters, since it becomes the squashed commit message.

release-please keeps an open `chore(master): release x.y.z` pull request that accumulates merged
changes, works out the next version and rewrites `CHANGELOG.md`. Merging that pull request tags the
commit and publishes a GitHub Release, which is what triggers the npm publish. So merging the
release pull request is the single deliberate act that ships a version — nothing publishes on an
ordinary merge to `master`.

Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) over OIDC, so
there is no npm token stored in this repository.

## License

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