Skip to content
Open
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
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,31 @@ jobs:
# surfaces missing-file / bundler-config regressions before the
# (slower) full Vitest suite runs.
- run: npm run build
# Guard against committed-bundle drift: GitHub's Actions runner
# executes whatever is *checked in* under `lib/`, not whatever CI
# rebuilds. Without this check a PR could change `src/` + rebuild
# locally, commit only the `src/` change, and still pass CI while
# shipping a stale bundle to every consumer.
#
# Two complementary checks:
# 1. `git diff --exit-code -- lib/` catches modifications to
# tracked files (today: `lib/main.js`).
# 2. `git ls-files --others --exclude-standard -- lib/` catches
# brand-new untracked files the bundler might start emitting
# (e.g. sourcemaps, additional chunks) that a future `src/`
# change could introduce without the author noticing.
- name: Ensure lib/ is up to date with src/
run: |
git diff --stat -- lib/ || true
if ! git diff --exit-code -- lib/; then
echo "::error::Files under \`lib/\` are stale. Run \`npm run build\` locally and commit the updated bundle."
exit 1
fi
untracked=$(git ls-files --others --exclude-standard -- lib/)
if [ -n "$untracked" ]; then
echo "::error::The build produced new files under \`lib/\` that are not committed:"
printf ' %s\n' $untracked
echo "::error::Commit the new artefact(s) or add them to \`.gitignore\`."
exit 1
fi
- run: npm test
8 changes: 8 additions & 0 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ export default async function main() {
// `preset: 'conventionalcommits'` would trigger a dynamic
// `import-from-esm` lookup that fails on the Actions runner
// where no `node_modules` directory exists.
//
// Note: upstream `load-changelog-config.js` only reads
// `loadedConfig.commits` (never merging a plugin-supplied `commits`
// field), so when no `preset` / `config` is passed `commitOpts`
// always comes from the angular default preset. That is harmless
// today — angular and conventionalcommits both default to
// `{ ignore: undefined, merges: false }` — but is worth knowing if
// those defaults ever diverge upstream.
const resolvedPreset = conventionalCommitsPreset({
types: mergeWithDefaultChangelogRules(mappedReleaseRules),
});
Expand Down
49 changes: 39 additions & 10 deletions tests/bundle.smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ interface ChildResult {
* Drain stdout/stderr asynchronously while the child runs so large
* bursts of startup output can't deadlock the parent. Always resolves
* (never rejects) so the caller can assert on the captured streams even
* if the child crashed, exited non-zero, or had to be SIGKILLed.
* if the child crashed, exited non-zero, had to be SIGKILLed, or failed
* to spawn in the first place.
*/
function runChildAndCapture(
child: ChildProcess,
Expand All @@ -63,21 +64,39 @@ function runChildAndCapture(
return new Promise((resolve) => {
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
let settled = false;

const finish = (overrides: Partial<ChildResult>): void => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve({
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
stderr: Buffer.concat(stderrChunks).toString('utf8'),
status: null,
signal: null,
...overrides,
});
};

child.stdout?.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
child.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk));

// If spawn itself fails (e.g. ENOENT for the node binary), Node emits
// `error` and never a `close`. Fold the error into stderr so the
// caller still sees a useful message instead of hanging until the
// SIGKILL timer fires.
child.on('error', (err) => {
stderrChunks.push(Buffer.from(`\n${String(err)}\n`));
finish({});
});

const timer = setTimeout(() => {
child.kill('SIGKILL');
}, timeoutMs);

child.on('close', (status, signal) => {
clearTimeout(timer);
resolve({
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
stderr: Buffer.concat(stderrChunks).toString('utf8'),
status,
signal,
});
finish({ status, signal });
});
});
}
Expand Down Expand Up @@ -209,8 +228,18 @@ describe('bundled action artefact', () => {
};

const server = createServer(handleRequest);
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', resolve);
await new Promise<void>((resolve, reject) => {
// Without an `error` listener, a failed `listen` (e.g. EADDRINUSE
// on a constrained CI host) would never reject and the test would
// silently hang until vitest's test timeout.
const onError = (err: Error): void => {
reject(err);
};
server.once('error', onError);
server.listen(0, '127.0.0.1', () => {
server.off('error', onError);
resolve();
});
});

try {
Expand Down