Files
inbuxa-server/tools/fork/notice-check.py
jcoffey-dev 4799d191a0
ci / fork-checks (pull_request) Successful in 18s
ci / build (pull_request) Successful in 7m11s
Fork tooling: a build check and a rename pass in the strip, a notice check in CI
strip.py compiles the stripped tree, so a dual-licensed file that only
serves an Enterprise feature fails the import instead of the merge, as
v0.16.23's tests/src/directory/issuer.rs does. Upstream's tests of the
features the fork rebuilt are expected not to compile there and are listed
in build-check-known.txt; an error anywhere else fails the run. Checked
against both imports: v0.16.22 passes with its 16 expected errors, v0.16.23
fails on issuer.rs alone. Imports the strip leaves unused are reported.

It also renames the upstream name where clients, users or operators meet
it as an identifier, from tools/fork/renames.py: wire-protocol names, the
web interface's client id, store keys, configuration defaults and the
served schema. main is renamed with the same module, so a re-import
arrives purged and those lines don't conflict.

notice-check.py fails CI when an upstream file the fork changed, measured
against the upstream branch, lacks its AGPL 5(a) notice; --fix adds it.
It runs beside the name check in a renamed fork-checks job.

Also commits v0.16.23's strip report under docs/fork/strip-reports/, which
the import in #18 left out.
2026-09-22 19:02:34 -07:00

105 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Coffey Labs
# SPDX-License-Identifier: AGPL-3.0-only
"""
Fail when an upstream file the fork changed doesn't say so (AGPL section 5(a)).
tools/fork/notice-check.py # check; exit 1 on a missing notice
tools/fork/notice-check.py --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 the `upstream` branch, which holds the stripped
upstream release `main` was last merged with (docs/spec/SPEC.md §2.2a), 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. Files with no
comment header at all (README, manifests) are covered by the README's prose.
"""
import argparse
import datetime
import re
import subprocess
import sys
HEADER_LINES = 15
UPSTREAM_HOLDER = re.compile(r'SPDX-FileCopyrightText:.*Stalwart Labs')
NOTICE = re.compile(r'Modified by Coffey Labs in \d{4}')
LICENSE_LINE = re.compile(r'^(\s*(?:\*|//|#)\s*)SPDX-License-Identifier:.*$')
def git(*args):
return subprocess.run(['git', *args], check=True, capture_output=True, text=True).stdout
def snapshot_ref():
for ref in ('origin/upstream', 'upstream'):
if subprocess.run(['git', 'rev-parse', '--verify', '--quiet', f'{ref}^{{commit}}'],
capture_output=True).returncode == 0:
return ref
sys.exit('notice-check: no upstream snapshot branch (origin/upstream or upstream); fetch it first')
def changed_upstream_files(ref):
# Against the working tree, not HEAD, so it also checks work not yet
# committed; in CI the two are the same.
for path in git('diff', '--name-only', '--diff-filter=M', ref).splitlines():
try:
head = open(path, encoding='utf-8').read().split('\n')[:HEADER_LINES]
except (OSError, UnicodeDecodeError):
continue
if any(UPSTREAM_HOLDER.search(line) for line in head):
yield path, head
def add_notice(path):
"""Put the notice under the license line, in that comment's own style."""
lines = open(path, encoding='utf-8').read().split('\n')
for n, line in enumerate(lines[:HEADER_LINES]):
m = LICENSE_LINE.match(line)
if m:
prefix = m.group(1)
blank = prefix.rstrip()
year = datetime.date.today().year
lines[n + 1:n + 1] = [blank, f'{prefix}Modified by Coffey Labs in {year} for INBUXA.']
open(path, 'w', encoding='utf-8').write('\n'.join(lines))
return True
return False
def main():
ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0].strip())
ap.add_argument('--fix', action='store_true', help='add the notice to every file missing it')
args = ap.parse_args()
ref = snapshot_ref()
checked, missing = 0, []
for path, head in changed_upstream_files(ref):
checked += 1
if not any(NOTICE.search(line) for line in head):
missing.append(path)
if args.fix:
unfixable = [p for p in missing if not add_notice(p)]
for p in sorted(set(missing) - set(unfixable)):
print(f'added: {p}')
missing = unfixable
if missing:
print(f'{len(missing)} upstream file(s) changed against {ref} without the modification notice:\n')
for p in missing:
print(f' {p}')
print('\nAdd "Modified by Coffey Labs in <year> for INBUXA." under the license line, '
'or run tools/fork/notice-check.py --fix.')
return 1
print(f'notice check: clean ({checked} changed upstream file(s), all marked; against {ref}).')
return 0
if __name__ == '__main__':
sys.exit(main())