From 41d236a6f52d1726b9f7d501890f1be0932ea778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 2 Sep 2026 17:36:24 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9A=80=20First=20version=20of=20scrip?= =?UTF-8?q?t=20for=20checking/adding=20license=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .license-ignore | 37 +++++ scripts/add-license-headers.sh | 204 ++++++++++++++++++++++++++++ scripts/license-comment-styles.conf | 30 ++++ 3 files changed, 271 insertions(+) create mode 100644 .license-ignore create mode 100755 scripts/add-license-headers.sh create mode 100644 scripts/license-comment-styles.conf diff --git a/.license-ignore b/.license-ignore new file mode 100644 index 0000000..b3c64e9 --- /dev/null +++ b/.license-ignore @@ -0,0 +1,37 @@ +# Default exclusions for scripts/add-license-headers.sh (gitignore syntax). +# Copy this file into your repo root and extend it with repo-specific paths; +# see docs/license-headers.md. + +# VCS / CI config +.git/ +.github/ + +# Dependencies / vendored code +node_modules/ +vendor/ +.venv/ +venv/ + +# Build / generated output +dist/ +build/ +*.min.js +*.min.css + +# Python +__pycache__/ +*.pyc +migrations/ + +# Test fixtures deliberately missing/varying headers (scripts/tests/run-tests.sh) +scripts/tests/fixtures/ + +# Known lockfiles +package-lock.json +pnpm-lock.yaml +yarn.lock +poetry.lock +uv.lock +Pipfile.lock +composer.lock +Cargo.lock diff --git a/scripts/add-license-headers.sh b/scripts/add-license-headers.sh new file mode 100755 index 0000000..31adfb0 --- /dev/null +++ b/scripts/add-license-headers.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# Add or check MPL 2.0 license headers on source files. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git rev-parse --show-toplevel)" + +CHECK_MODE=0 +BASE_REF="" +IGNORE_FILE="$REPO_ROOT/.license-ignore" +[ -f "$IGNORE_FILE" ] || IGNORE_FILE="$(dirname "$SCRIPT_DIR")/.license-ignore" +STYLE_MAP="$REPO_ROOT/scripts/license-comment-styles.conf" +[ -f "$STYLE_MAP" ] || STYLE_MAP="$SCRIPT_DIR/license-comment-styles.conf" +FILES=() +EXPLICIT_FILES=0 + +# Content for help and usage +usage() { + cat <<'EOF' +Check or add MPL 2.0 license headers on source files. + +Usage: + add-license-headers.sh [--check] [--ignore-file PATH] [--style-map PATH] [--base REF] + add-license-headers.sh [--check] [--ignore-file PATH] [--style-map PATH] -- FILE... + add-license-headers.sh -h|--help + +Modes: + (default) Insert the MPL header into any relevant file that's missing one. + --check Report-only: exit 1 if any relevant file is missing its header. + +Target selection (mutually exclusive): + (no args) Scan every tracked file (git ls-files). + --base REF Only files changed relative to REF (git diff --name-only REF...HEAD). + -- FILE... Only the given file paths. + +Options: + --ignore-file PATH Override the gitignore-syntax exclusion list + (default: /.license-ignore, + falling back to the copy in this script's own repo). + --style-map PATH Override the extension-to-comment-style table + (default: /scripts/license-comment-styles.conf, + falling back to the copy bundled next to this script). + +Examples: + ./scripts/add-license-headers.sh # add headers repo-wide + ./scripts/add-license-headers.sh --check --base main # what CI runs + ./scripts/add-license-headers.sh -- foo.py bar.vue # only these files +EOF +} + +# Parse command-line arguments. +while [ $# -gt 0 ]; do + case "$1" in + --check) CHECK_MODE=1; shift ;; + --base) BASE_REF="$2"; shift 2 ;; + --ignore-file) IGNORE_FILE="$2"; shift 2 ;; + --style-map) STYLE_MAP="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + --) shift; FILES=("$@"); EXPLICIT_FILES=1; break ;; + *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;; + esac +done + +# Reject incompatible target-selection options. +if [ -n "$BASE_REF" ] && [ "$EXPLICIT_FILES" -eq 1 ]; then + echo "error: --base and -- FILE... are mutually exclusive" >&2 + usage >&2 + exit 2 +fi + +# Validate the base ref before it's used to select files. +if [ -n "$BASE_REF" ]; then + if ! git -C "$REPO_ROOT" rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null; then + echo "error: --base ref '$BASE_REF' does not resolve to a commit in this repository" >&2 + exit 2 + fi +fi + +# Determine which files to scan when none were given explicitly. +if [ ${#FILES[@]} -eq 0 ]; then + if [ -n "$BASE_REF" ]; then + mapfile -t FILES < <(git -C "$REPO_ROOT" diff --name-only --diff-filter=ACMR "${BASE_REF}...HEAD") + else + mapfile -t FILES < <(git -C "$REPO_ROOT" ls-files) + fi +fi + +# Substring shared verbatim by every style template below; used to detect existing license headers. +MARKER='This Source Code Form is subject to the terms of the Mozilla Public' + +# Look up the comment style for a file extension in the style map. +style_for_ext() { + local ext="$1" + [ -f "$STYLE_MAP" ] || return 1 + awk -v ext="$ext" '$1 == ext { print $2; found=1; exit } END { exit !found }' "$STYLE_MAP" +} + +# Check whether a path matches the ignore file's gitignore-style patterns. +is_ignored() { + local path="$1" + [ -f "$IGNORE_FILE" ] || return 1 + git -C "$REPO_ROOT" -c core.excludesFile="$IGNORE_FILE" check-ignore --no-index -q -- "$path" +} + +# Render the license header text for a given comment style. +header_for_style() { + case "$1" in + slash) + printf '// %s\n// License, v. 2.0. If a copy of the MPL was not distributed with this\n// file, You can obtain one at http://mozilla.org/MPL/2.0/.\n' "$MARKER" ;; + hash) + printf '# %s\n# License, v. 2.0. If a copy of the MPL was not distributed with this\n# file, You can obtain one at http://mozilla.org/MPL/2.0/.\n' "$MARKER" ;; + block) + printf '/*\n * %s\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at http://mozilla.org/MPL/2.0/.\n */\n' "$MARKER" ;; + html) + printf '\n' "$MARKER" ;; + jinja) + printf '{#\n %s\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at http://mozilla.org/MPL/2.0/.\n#}\n' "$MARKER" ;; + *) + return 1 ;; + esac +} + +# Insert after a shebang line if present, otherwise at the top of the file. +insert_header() { + local target="$1" header="$2" tmp mode + tmp="$(mktemp)" + if [[ "$(head -n1 -- "$target")" == '#!'* ]]; then + { head -n1 -- "$target"; printf '\n%s\n' "$header"; tail -n +2 -- "$target"; } > "$tmp" + else + { printf '%s\n' "$header"; cat -- "$target"; } > "$tmp" + fi + mode="$(stat -c '%a' -- "$target" 2>/dev/null || stat -f '%Lp' -- "$target")" + chmod "$mode" "$tmp" + mv -- "$tmp" "$target" +} + +# Memory for report +missing=() +already_had=0 +ignored_count=0 +skipped_symlinks=0 + +# Process each candidate file, adding or checking its license header. +for file in "${FILES[@]}"; do + # Resolve the absolute path differently depending on how the file was selected. + if [ "$EXPLICIT_FILES" -eq 1 ]; then + path="$file" + case "$path" in + /*) : ;; # already absolute + *) path="$PWD/$path" ;; # anchor to the invoking cwd, not $REPO_ROOT, so is_ignored's + # "git -C $REPO_ROOT check-ignore" resolves the right file + esac + else + path="$REPO_ROOT/$file" + fi + + # Handle symlinks: never flatten a tracked symlink into a regular file + if [ -L "$path" ]; then + skipped_symlinks=$((skipped_symlinks + 1)) + continue + fi + [ -f "$path" ] || continue + + base="${file##*/}" + ext="${base##*.}" + [ "$ext" = "$base" ] && continue # no dot at all, e.g. "Makefile" + [ -z "${base%.*}" ] && continue # dotfile with nothing before the (only) dot, e.g. ".gitignore" + + style="$(style_for_ext "$ext")" || continue # extension not in style map + + # Skip files excluded by the ignore list. + if is_ignored "$path"; then + ignored_count=$((ignored_count + 1)) + continue + fi + + # Skip files that already have the license header. + if grep -qF -- "$MARKER" "$path"; then + already_had=$((already_had + 1)) + continue + fi + + # Record as missing in check mode, otherwise insert the header now. + if [ "$CHECK_MODE" -eq 1 ]; then + missing+=("$file") + else + insert_header "$path" "$(header_for_style "$style")" + echo "added header: $file" + fi +done + +# Report results: fail with the missing-file list in check mode, else print a summary. +if [ "$CHECK_MODE" -eq 1 ]; then + if [ ${#missing[@]} -gt 0 ]; then + echo "Missing MPL license header in ${#missing[@]} file(s):" >&2 + printf ' %s\n' "${missing[@]}" >&2 + echo >&2 + echo "Re-run '$0' without --check to write them to the files, then commit." >&2 + exit 1 + fi + echo "All relevant files have license headers." +else + echo "Done. ${already_had} file(s) already had a header, ${ignored_count} ignored, ${skipped_symlinks} symlink(s) skipped." +fi diff --git a/scripts/license-comment-styles.conf b/scripts/license-comment-styles.conf new file mode 100644 index 0000000..01bcd1e --- /dev/null +++ b/scripts/license-comment-styles.conf @@ -0,0 +1,30 @@ +# Extension -> comment-style mapping used by add-license-headers.sh. +# One "ext style" pair per line. Extensions not listed here are skipped. +# Styles: slash (//), hash (#), block (/* */), html (), jinja ({# #}) +# +# A repo can override this file by placing its own copy at scripts/license-comment-styles.conf. + +js slash +jsx slash +ts slash +tsx slash +mjs slash +cjs slash + +py hash +sh hash +bash hash +yml hash +yaml hash +ftl hash + +css block +scss block +less block + +html html +htm html +vue html + +jinja2 jinja +j2 jinja From cd39c5c7732a585387dbf0370ce1bb89d384078a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 2 Sep 2026 20:29:56 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9E=95=EF=B8=8F=20Add=20workflow=20for?= =?UTF-8?q?=20license=20header=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/license-check.yml | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/license-check.yml diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml new file mode 100644 index 0000000..57602a6 --- /dev/null +++ b/.github/workflows/license-check.yml @@ -0,0 +1,35 @@ +--- +name: License Header Check + +# Runs on this repo's own PRs, and is also callable from other repos: +# jobs: +# license-check: +# uses: thunderbird/pro/.github/workflows/license-check.yml@main +on: + pull_request: + branches: [main] + workflow_call: + +permissions: + contents: read + +jobs: + license-check: + runs-on: ubuntu-latest + steps: + - name: Checkout repo under test + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Checkout license-header tooling from thunderbird/pro + uses: actions/checkout@v7 + with: + repository: thunderbird/pro + ref: ${{ github.action_ref || 'main' }} + path: .pro-license-tools + + - name: Check license headers on changed files + run: | + ./.pro-license-tools/scripts/add-license-headers.sh --check \ + --base "origin/${{ github.event.pull_request.base.ref }}" From bfce29dcfa269f4e44e3c20fd136c78f09b4ef04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 2 Sep 2026 20:31:37 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=93=9C=20Add=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ docs/license-headers.md | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 docs/license-headers.md diff --git a/README.md b/README.md index 2cfe15e..0e6b2dc 100755 --- a/README.md +++ b/README.md @@ -34,6 +34,9 @@ Regardless of which repo you plan to contribute to, general ettiquette for contr ### 4. Review & merge At least one maintainer must approve the pull request before the change is merged. Often additional changes are requested and can be made in the same PR by updating your feature branch on your fork. Once all changes and comments have been adressed and approved, the PR is ready to be merged into the main project. +### 5. License headers +Source files need an MPL license header; see [docs/license-headers.md](docs/license-headers.md) for the script that adds/checks them and how to adopt the CI check in another repo. + ## Need Help? Found a bug? Have an idea? Want to chat? If any of these apps are not behaving like it should, or you are not sure if you've encountered a bug: * Ask a question on our Matrix channel: [#tb-services:mozilla.org](https://matrix.to/#/#tb-services:mozilla.org) diff --git a/docs/license-headers.md b/docs/license-headers.md new file mode 100644 index 0000000..2168e1f --- /dev/null +++ b/docs/license-headers.md @@ -0,0 +1,57 @@ +# MPL license headers + +Thunderbird Pro repos are licensed under the [Mozilla Public License 2.0](../LICENSE), which is a per-file license: each covered source file should carry a short header pointing back at it. `scripts/add-license-headers.sh` adds and checks these headers, and `.github/workflows/license-check.yml` runs it in CI. + +## Using the script + +```sh +# Add missing headers to every tracked file in the repo +./scripts/add-license-headers.sh + +# Only check (used by CI) - exits 1 and lists offending files if any are missing a header +./scripts/add-license-headers.sh --check --base main + +# Only operate on specific files +./scripts/add-license-headers.sh -- path/to/file.py path/to/other.vue +``` + +Run `./scripts/add-license-headers.sh --help` for the full option list. + +The script only touches file types it knows a comment style for (see `scripts/license-comment-styles.conf`); everything else (images, lockfiles, Markdown, JSON, ...) is left alone. It's idempotent - re-running it never adds a duplicate header. + +## Excluding files + +Paths matching `.license-ignore` (plain `.gitignore` syntax) are skipped, in addition to any file type absent from `scripts/license-comment-styles.conf`. + +If a repo needs its own exclusions or comment-style entries, drop a same-named file at its own root - `.license-ignore` and/or `scripts/license-comment-styles.conf` - and it takes precedence over the copy bundled in `thunderbird/pro`. + +## Adopting this in another repo + +### Using CI + +Add a caller workflow that invokes the reusable workflow hosted here: + +```yaml +name: License Header Check +on: + pull_request: +jobs: + license-check: + uses: thunderbird/pro/.github/workflows/license-check.yml@main +``` + +This checks out your repo plus the tooling from `thunderbird/pro`, then runs `add-license-headers.sh --check` against the files your PR changed. + +### Running locally, without CI + +Clone `thunderbird/pro` next to your repo and invoke the script from your repo's root: + +```sh +# Clone thunderbird/pro as a sibling of your repo +git clone https://github.com/thunderbird/pro.git .pro-license-tools + +cd your-repo +../.pro-license-tools/scripts/add-license-headers.sh --check +``` + +`REPO_ROOT` is resolved from the repo you run it in (via `git rev-parse --show-toplevel`), so it scans and modifies only your repo's own tracked files - never the cloned tooling. If your repo doesn't have its own `.license-ignore` and/or `scripts/license-comment-styles.conf`, the script falls back to the copies bundled in `.pro-license-tools`; drop same-named files at your repo's root to override them. From 2d43444968f7b4bf1d86b6d5e2a493e591e42c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Wed, 2 Sep 2026 20:33:12 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9E=95=EF=B8=8F=20Add=20script=20tests?= =?UTF-8?q?=20with=20several=20file=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/tests/fixtures/.env | 1 + scripts/tests/fixtures/Makefile | 2 + scripts/tests/fixtures/already-has-header.sh | 5 + scripts/tests/fixtures/component.vue | 3 + scripts/tests/fixtures/no-shebang.js | 4 + scripts/tests/fixtures/notes.md | 3 + scripts/tests/fixtures/page.html | 6 + scripts/tests/fixtures/style.css | 3 + scripts/tests/fixtures/vendor/thirdparty.js | 2 + scripts/tests/fixtures/with-shebang.py | 6 + scripts/tests/run-tests.sh | 124 +++++++++++++++++++ 11 files changed, 159 insertions(+) create mode 100644 scripts/tests/fixtures/.env create mode 100644 scripts/tests/fixtures/Makefile create mode 100644 scripts/tests/fixtures/already-has-header.sh create mode 100644 scripts/tests/fixtures/component.vue create mode 100644 scripts/tests/fixtures/no-shebang.js create mode 100644 scripts/tests/fixtures/notes.md create mode 100644 scripts/tests/fixtures/page.html create mode 100644 scripts/tests/fixtures/style.css create mode 100644 scripts/tests/fixtures/vendor/thirdparty.js create mode 100644 scripts/tests/fixtures/with-shebang.py create mode 100755 scripts/tests/run-tests.sh diff --git a/scripts/tests/fixtures/.env b/scripts/tests/fixtures/.env new file mode 100644 index 0000000..4389735 --- /dev/null +++ b/scripts/tests/fixtures/.env @@ -0,0 +1 @@ +SAMPLE_VAR=1 diff --git a/scripts/tests/fixtures/Makefile b/scripts/tests/fixtures/Makefile new file mode 100644 index 0000000..0752b1f --- /dev/null +++ b/scripts/tests/fixtures/Makefile @@ -0,0 +1,2 @@ +all: + echo "no dot in this filename at all" diff --git a/scripts/tests/fixtures/already-has-header.sh b/scripts/tests/fixtures/already-has-header.sh new file mode 100644 index 0000000..597577d --- /dev/null +++ b/scripts/tests/fixtures/already-has-header.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +echo "already licensed" diff --git a/scripts/tests/fixtures/component.vue b/scripts/tests/fixtures/component.vue new file mode 100644 index 0000000..36516ad --- /dev/null +++ b/scripts/tests/fixtures/component.vue @@ -0,0 +1,3 @@ + diff --git a/scripts/tests/fixtures/no-shebang.js b/scripts/tests/fixtures/no-shebang.js new file mode 100644 index 0000000..9694934 --- /dev/null +++ b/scripts/tests/fixtures/no-shebang.js @@ -0,0 +1,4 @@ +// A tiny sample module used to test header insertion. +export function greet(name) { + return `Hello, ${name}!`; +} diff --git a/scripts/tests/fixtures/notes.md b/scripts/tests/fixtures/notes.md new file mode 100644 index 0000000..0a10be9 --- /dev/null +++ b/scripts/tests/fixtures/notes.md @@ -0,0 +1,3 @@ +# Notes + +Markdown has no comment style in `license-comment-styles.conf`, so this file must stay untouched. diff --git a/scripts/tests/fixtures/page.html b/scripts/tests/fixtures/page.html new file mode 100644 index 0000000..0b8ff2f --- /dev/null +++ b/scripts/tests/fixtures/page.html @@ -0,0 +1,6 @@ + + + +

Sample page.

+ + diff --git a/scripts/tests/fixtures/style.css b/scripts/tests/fixtures/style.css new file mode 100644 index 0000000..0307e17 --- /dev/null +++ b/scripts/tests/fixtures/style.css @@ -0,0 +1,3 @@ +.sample { + color: red; +} diff --git a/scripts/tests/fixtures/vendor/thirdparty.js b/scripts/tests/fixtures/vendor/thirdparty.js new file mode 100644 index 0000000..cb2dbb2 --- /dev/null +++ b/scripts/tests/fixtures/vendor/thirdparty.js @@ -0,0 +1,2 @@ +// Pretend third-party code that lives under a vendor/ path matched by .license-ignore. +module.exports = function noop() {}; diff --git a/scripts/tests/fixtures/with-shebang.py b/scripts/tests/fixtures/with-shebang.py new file mode 100644 index 0000000..01af0da --- /dev/null +++ b/scripts/tests/fixtures/with-shebang.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 +"""A tiny sample script used to test header insertion after a shebang.""" + + +def greet(name): + return f"Hello, {name}!" diff --git a/scripts/tests/run-tests.sh b/scripts/tests/run-tests.sh new file mode 100755 index 0000000..3f717d1 --- /dev/null +++ b/scripts/tests/run-tests.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Exercise add-license-headers.sh against scripts/tests/fixtures and assert the results. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ADD_HEADERS="$REPO_ROOT/scripts/add-license-headers.sh" +FIXTURES="$SCRIPT_DIR/fixtures" +MARKER='This Source Code Form is subject to the terms of the Mozilla Public' +cd "$REPO_ROOT" + +pass=0 +fail=0 +ok() { echo " ok - $1"; pass=$((pass + 1)); } +bad() { echo " FAIL - $1"; fail=$((fail + 1)); } + +# Files expected to get a header inserted by a default-mode run. +HEADERED_FILES=(no-shebang.js with-shebang.py style.css page.html component.vue) +# Files expected to stay byte-identical to the fixture (no comment style, no extension, +# dotfile, or ignored path). +UNTOUCHED_FILES=(already-has-header.sh notes.md Makefile .env vendor/thirdparty.js) +ALL_FILES=("${HEADERED_FILES[@]}" "${UNTOUCHED_FILES[@]}" symlink-file.js) + +scratch="$(mktemp -d -p "$SCRIPT_DIR")" +trap 'rm -rf "$scratch"' EXIT + +cp -a "$FIXTURES/." "$scratch/" +ln -s no-shebang.js "$scratch/symlink-file.js" + +abs_files=() +for f in "${ALL_FILES[@]}"; do abs_files+=("$scratch/$f"); done + +echo "== default mode: add headers ==" +"$ADD_HEADERS" -- "${abs_files[@]}" >"$scratch/.add-output.txt" 2>&1 +add_status=$? +[ "$add_status" -eq 0 ] && ok "default-mode run exits 0" || bad "default-mode run exited $add_status" + +for f in "${HEADERED_FILES[@]}"; do + if grep -qF -- "$MARKER" "$scratch/$f"; then + ok "$f: header inserted" + else + bad "$f: header NOT inserted" + fi +done + +if [ "$(sed -n '1p' "$scratch/with-shebang.py")" = "#!/usr/bin/env python3" ] \ + && sed -n '3p' "$scratch/with-shebang.py" | grep -qF -- "$MARKER"; then + ok "with-shebang.py: header inserted after the shebang, not before" +else + bad "with-shebang.py: header not correctly placed after the shebang" +fi + +if sed -n '1p' "$scratch/no-shebang.js" | grep -qF -- "$MARKER"; then + ok "no-shebang.js: header inserted at the top of the file" +else + bad "no-shebang.js: header not at the top of the file" +fi + +for f in "${UNTOUCHED_FILES[@]}"; do + if diff -q "$FIXTURES/$f" "$scratch/$f" >/dev/null 2>&1; then + ok "$f: left untouched" + else + bad "$f: was modified but should have been left alone" + fi +done + +if [ -L "$scratch/symlink-file.js" ]; then + ok "symlink-file.js: still a symlink after the run" +else + bad "symlink-file.js: was flattened into a regular file" +fi + +echo "== default mode re-run: idempotency ==" +"$ADD_HEADERS" -- "${abs_files[@]}" >"$scratch/.rerun-output.txt" 2>&1 +for f in "${HEADERED_FILES[@]}" already-has-header.sh; do + count="$(grep -cF -- "$MARKER" "$scratch/$f")" + [ "$count" -eq 1 ] && ok "$f: header appears exactly once after re-run" || bad "$f: header appears $count times after re-run" +done +if grep -q '^added header:' "$scratch/.rerun-output.txt"; then + bad "re-run: reported adding a header to an already-headered file" +else + ok "re-run: added no duplicate headers" +fi + +echo "== --check mode: fresh (headerless) fixtures ==" +check_scratch="$(mktemp -d -p "$SCRIPT_DIR")" +cp -a "$FIXTURES/." "$check_scratch/" +check_abs=() +for f in "${ALL_FILES[@]}"; do + [ "$f" = symlink-file.js ] && continue + check_abs+=("$check_scratch/$f") +done +"$ADD_HEADERS" --check -- "${check_abs[@]}" >"$scratch/.check-output.txt" 2>&1 +check_status=$? +[ "$check_status" -eq 1 ] && ok "--check on fresh fixtures exits 1" || bad "--check on fresh fixtures exited $check_status (expected 1)" +for f in "${HEADERED_FILES[@]}"; do + if grep -qF -- "$check_scratch/$f" "$scratch/.check-output.txt"; then + ok "--check: reports $f missing" + else + bad "--check: did not report $f missing" + fi +done +for f in "${UNTOUCHED_FILES[@]}"; do + if grep -qF -- "$check_scratch/$f" "$scratch/.check-output.txt"; then + bad "--check: incorrectly reported $f as missing a header" + else + ok "--check: correctly did not report $f" + fi +done +rm -rf "$check_scratch" + +echo "== --check mode: already-headered files ==" +"$ADD_HEADERS" --check -- "${abs_files[@]}" >"$scratch/.check-ok-output.txt" 2>&1 +check_ok_status=$? +[ "$check_ok_status" -eq 0 ] && ok "--check on headered files exits 0" || bad "--check on headered files exited $check_ok_status (expected 0)" +if grep -q 'All relevant files have license headers.' "$scratch/.check-ok-output.txt"; then + ok "--check: reports success message" +else + bad "--check: missing success message" +fi + +echo +echo "$pass passed, $fail failed" +[ "$fail" -eq 0 ] From 123c25bf2ba5b00e40ecaac7b079d59e1b54a181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Thu, 3 Sep 2026 11:45:20 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=94=A8=20Fix=20workflow=20triggered?= =?UTF-8?q?=20by=20other=20repos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/license-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml index 57602a6..5d64c44 100644 --- a/.github/workflows/license-check.yml +++ b/.github/workflows/license-check.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v7 with: repository: thunderbird/pro - ref: ${{ github.action_ref || 'main' }} + ref: ${{ github.workflow_sha }} path: .pro-license-tools - name: Check license headers on changed files