CI: fail when a changed upstream file lacks the AGPL 5(a) notice #84

Merged
jcoffey-dev merged 1 commits from ci/notice-check into main 2026-10-06 01:40:41 +00:00
3 changed files with 115 additions and 1 deletions

No files matched your search

+15
View File
@@ -68,6 +68,21 @@ jobs:
- run: npm test
- run: npm run build
# A changed upstream file without the AGPL 5(a) notice, as inbuxa-server's
# fork-checks job catches for the server. Seconds and no npm install, so it
# runs here whatever BUILD_ON says. It diffs against the upstream release
# this fork is built on, hence the full fetch.
notices:
runs-on: light
container:
image: node:22-bookworm-slim@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9 # 22-bookworm-slim
steps:
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
with:
fetch-depth: 0
- run: command -v git >/dev/null || { apt-get update -qq && apt-get install -y -qq --no-install-recommends git >/dev/null; }
- run: git config --global --add safe.directory "$PWD" && node tools/notice-check.mjs
# ----------------------------------------------------------- publish ------
# Port of publish.yml, to the owner's own registry now that GHCR went with
# the GitHub account: <REGISTRY>/inbuxa/inbuxa-admin, the same path the
+3 -1
View File
@@ -15,7 +15,9 @@
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"i18n": "node tools/i18n-extract.mjs",
"i18n:check": "node tools/i18n-extract.mjs --check"
"i18n:check": "node tools/i18n-extract.mjs --check",
"notice:check": "node tools/notice-check.mjs",
"notice:fix": "node tools/notice-check.mjs --fix"
},
"dependencies": {
"@daypicker/react": "^10.0.1",
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env node
// SPDX-FileCopyrightText: 2026 Coffey Labs
// SPDX-License-Identifier: AGPL-3.0-only
/**
* inbuxa: fails when an upstream file this fork changed doesn't say so
* (AGPL section 5(a)), as inbuxa-server's tools/fork/notice-check.py does
* for the server.
*
* node tools/notice-check.mjs check; exit 1 on a missing notice
* node tools/notice-check.mjs --fix add the notice where it's missing
*
* The AGPL asks a modified work to carry a prominent notice that it was
* modified, with a date. Every upstream file this fork changes carries one
* beneath upstream's own notice:
*
* * Modified by Coffey Labs in 2026 for INBUXA.
*
* "Changed" is measured against BASE, the upstream release this fork is
* built on, so the list is what actually differs rather than a guess. A file
* counts as upstream's when its header names Stalwart Labs as a copyright
* holder; files the fork wrote carry their own copyright and need nothing.
* When a newer upstream release is merged, move BASE to it.
*/
import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
// Upstream's web interface v1.0.11, the release this fork started from.
const BASE = process.env.NOTICE_BASE || 'dc462b137fbee9c2193946a6db554ff20bcc9b07';
const HEADER_LINES = 15;
const UPSTREAM_HOLDER = /SPDX-FileCopyrightText:.*Stalwart Labs/;
const NOTICE = /Modified by Coffey Labs in \d{4}/;
const LICENSE_LINE = /^(\s*(?:\*|\/\/|#)\s*)SPDX-License-Identifier:.*$/;
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' });
function changedUpstreamFiles() {
try {
git('cat-file', '-e', `${BASE}^{commit}`);
} catch {
console.error(`notice-check: ${BASE} is not in this history; fetch it in full first`);
process.exit(2);
}
// Against the working tree, so it also checks work not yet committed; in
// CI the two are the same. Renames count: a moved and edited file is
// still upstream's.
const out = [];
for (const line of git('diff', '--name-status', '-M', '--diff-filter=MR', BASE).split('\n')) {
if (!line) continue;
const parts = line.split('\t');
if (parts[0] === 'R100') continue; // moved, not changed
const path = parts[parts.length - 1];
let head;
try {
head = readFileSync(path, 'utf8').split('\n').slice(0, HEADER_LINES);
} catch {
continue;
}
if (head.some((l) => UPSTREAM_HOLDER.test(l))) out.push({ path, head });
}
return out;
}
/** Put the notice under the license line, in that comment's own style. */
function addNotice(path) {
const lines = readFileSync(path, 'utf8').split('\n');
for (let n = 0; n < Math.min(lines.length, HEADER_LINES); n++) {
const m = LICENSE_LINE.exec(lines[n]);
if (m) {
const prefix = m[1];
const year = new Date().getFullYear();
lines.splice(n + 1, 0, prefix.trimEnd(), `${prefix}Modified by Coffey Labs in ${year} for INBUXA.`);
writeFileSync(path, lines.join('\n'));
return true;
}
}
return false;
}
const fix = process.argv.includes('--fix');
const files = changedUpstreamFiles();
let missing = files.filter(({ head }) => !head.some((l) => NOTICE.test(l))).map(({ path }) => path);
if (fix) {
const unfixable = missing.filter((p) => !addNotice(p));
for (const p of missing.filter((p) => !unfixable.includes(p)).sort()) console.log(`added: ${p}`);
missing = unfixable;
}
if (missing.length) {
console.log(`${missing.length} upstream file(s) changed against ${BASE.slice(0, 7)} without the modification notice:\n`);
for (const p of missing) console.log(` ${p}`);
console.log('\nAdd "Modified by Coffey Labs in <year> for INBUXA." under the license line, or run npm run notice:fix.');
process.exit(1);
}
console.log(`notice check: clean (${files.length} changed upstream file(s), all marked; against ${BASE.slice(0, 7)}).`);