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
45 changes: 45 additions & 0 deletions .github/workflows/copyright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright header conformance - fast, GitHub-hosted, gate-friendly.
#
# Enforces the fork's two-header policy (AGENTS.md "Copyright rules for this fork"):
# upstream-derived files keep the Confluent header; fork-original files (added after
# the pinned fork point) must carry the fork header and must not claim Confluent
# copyright. The mycila license plugin cannot express this (single header template,
# git-based year auto-bumping), so it is skipped by default in the root pom and this
# scanner is the enforcement instead.
#
# fetch-depth: 0 because the scanner classifies provenance via the fork-point commit,
# which a shallow clone does not contain.

name: Copyright Headers

# push is scoped to master (matching maven.yml) so PR-branch commits run once via the
# pull_request event instead of twice.
on:
push:
branches: [ master ]
pull_request:
workflow_dispatch:

concurrency:
group: copyright-${{ github.head_ref || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
check:
name: "Copyright header check"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout (full history - scanner needs the fork-point commit)
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Self-test the scanner (fixture repos)
run: bash bin/test-check-copyright-headers.sh

- name: Check copyright headers
run: bash bin/check-copyright-headers.sh
20 changes: 17 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,27 @@ bin/performance-test.sh

- **Lombok**: Used extensively (builders, getters, logging). IntelliJ Lombok plugin required.
- **EditorConfig**: Enforced via `.editorconfig` - 4-space indent for Java, 120 char line length.
- **License headers**: Managed by `license-maven-plugin` (Mycila). Use `-Dlicense.skip` locally to skip checks.
- **License headers**: Enforced by `bin/check-copyright-headers.sh` (runs in CI via the
`Copyright Headers` workflow; run it locally before pushing header-related changes). The mycila
`license-maven-plugin` is skipped by default in the root pom - it knows only the Confluent header
template, so its `format` goal used to stamp the wrong attribution onto fork-original files and its
git-year resolver auto-bumped years and broke in worktrees. `-Dlicense.skip` on the command line is
no longer needed (harmless if still passed).
- **Copyright rules for this fork**:
- Do not change copyright headers on existing files unless the file has substantive code changes in the same commit
- Do not bump copyright years as an incidental or standalone change
- The `NOTICE` file at repo root contains the legal attribution structure for the fork
- New files written entirely for the fork should not claim Confluent copyright
- Always pass `-Dlicense.skip` to Maven to prevent the license plugin from auto-bumping years
- New files written entirely for the fork use `Copyright (C) <year> Antony Stubbs and contributors` -
never the Confluent header
- Upstream-derived files MODIFIED on the fork retain the Confluent notice and ADD
`Modifications Copyright (C) <year> Antony Stubbs and contributors` beneath it (Apache 2.0
4(b) retain-notices + 4(c) change-notice - the convention used by e.g. Amazon Corretto and
MariaDB for derived files). The scanner detects modification against the fork point
automatically, so forgetting the line fails CI
- Files renamed or extracted from upstream keep the Confluent header - register renames in
`RENAMED_FROM_UPSTREAM` (`newpath|oldpath` lines) and extractions in `EXTRACTED_FROM_UPSTREAM`
inside `bin/check-copyright-headers.sh`. Renames with content changes, and all extractions,
also require the modifications line
- **Google Truth**: Used for test assertions alongside JUnit 5 and Mockito.

## CI
Expand Down
132 changes: 132 additions & 0 deletions bin/check-copyright-headers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
#
# Copyright header conformance check for the fork.
#
# Policy (AGENTS.md, "Copyright rules for this fork"):
# - Upstream-derived files UNMODIFIED since the fork point keep the Confluent header as-is.
# - Upstream-derived files MODIFIED since the fork point (including fork-side renames with
# changes, and extractions of upstream code) must ALSO carry the modifications line:
# Modifications Copyright (C) <year> Antony Stubbs and contributors
# (Apache 2.0 s4(b) retain-notices + s4(c) prominent-change-notice, the convention used by
# e.g. Amazon Corretto and MariaDB for derived files.)
# - Fork-original files (added after the fork point) must use the fork header:
# Copyright (C) <year> Antony Stubbs and contributors
# and must NOT claim Confluent copyright.
#
# Provenance is derived from the pinned fork-point tree below, so the check is fully
# local and deterministic - no network, no upstream remote needed. It requires the
# fork-point commit to be present in history (CI: actions/checkout fetch-depth: 0).
# "Modified" is judged against the WORKING TREE, so local uncommitted edits count.
#
# Years are deliberately NOT policed: never bump copyright years as an incidental
# change (see AGENTS.md). This scanner replaces the mycila license-maven-plugin's
# check/format for headers - that plugin knows only ONE header template, so running
# its format goal is exactly what used to stamp Confluent headers onto fork files
# (it is now skipped by default via <license.skip> in the root pom).
#
# Usage: bin/check-copyright-headers.sh
# Exit codes: 0 = conformant, 1 = violations found, 2 = cannot run (shallow clone).
#
# Test-harness overrides (used by bin/test-check-copyright-headers.sh; not needed
# for normal use): COPYRIGHT_CHECK_FORK_POINT pins a different fork-point commit,
# COPYRIGHT_CHECK_EXTRA_RENAMES appends 'newpath|oldpath' lines,
# COPYRIGHT_CHECK_EXTRA_EXTRACTIONS appends extraction paths.

set -eu

cd "$(git rev-parse --show-toplevel)"

# merge-base of the fork's master and confluentinc/parallel-consumer master:
# "Migrate to V3 based sonatype secret (#916)", 2026-03-24.
FORK_POINT="${COPYRIGHT_CHECK_FORK_POINT:-7f2901226bccac68a2a71f0d9da343887b1abb46}"
FORK_HOLDER="Antony Stubbs and contributors"
HEADER_WINDOW=8 # lines from the top of the file searched for copyright notices

# Fork-side RENAMES of upstream files: 'newpath|oldpath-at-fork-point'. They keep the
# Confluent header; if their content has diverged from the fork-point blob they must
# also carry the modifications line. (These three were relocated in fface195.)
RENAMED_FROM_UPSTREAM="
parallel-consumer-examples/parallel-consumer-example-metrics/src/test/java/io/confluent/parallelconsumer/examples/metrics/integrationTests/CoreAppMetricsIntegrationTest.java|parallel-consumer-examples/parallel-consumer-example-metrics/src/test/java/io/confluent/parallelconsumer/examples/metrics/CoreAppMetricsIntegrationTest.java
parallel-consumer-examples/parallel-consumer-example-metrics/src/test/java/io/confluent/parallelconsumer/examples/metrics/integrationTests/PrometheusContainer.java|parallel-consumer-examples/parallel-consumer-example-metrics/src/test/java/io/confluent/parallelconsumer/examples/metrics/PrometheusContainer.java
parallel-consumer-examples/parallel-consumer-example-streams/src/test/java/io/confluent/parallelconsumer/examples/streams/integrationTests/StreamsAppTest.java|parallel-consumer-examples/parallel-consumer-example-streams/src/test/java/io/confluent/parallelconsumer/examples/streams/StreamsAppTest.java
${COPYRIGHT_CHECK_EXTRA_RENAMES:-}
"

# EXTRACTIONS of upstream-derived code into new files (no single origin path): always
# Confluent + modifications line. ManagedPCInstance lives on the chaos-suite branch
# stack, not yet on master - listing it early is harmless (only tracked files are
# checked) and stops the check ambushing the stack at merge (see PR #83).
EXTRACTED_FROM_UPSTREAM="
parallel-consumer-core/src/test-integration/java/io/confluent/parallelconsumer/integrationTests/utils/ManagedPCInstance.java
${COPYRIGHT_CHECK_EXTRA_EXTRACTIONS:-}
"

if ! git cat-file -e "${FORK_POINT}^{commit}" 2>/dev/null; then
echo "ERROR: fork-point commit ${FORK_POINT} not found in history." >&2
echo " Fetch full history first (CI: actions/checkout with fetch-depth: 0)." >&2
exit 2
fi

upstream_files=$(git ls-tree -r --name-only "$FORK_POINT")
# vs the working tree, so uncommitted local edits are judged too
modified_since_fork=$(git diff --name-only "$FORK_POINT" -- '*.java')

fails=0
checked=0

require_confluent() { # <file> <header>
if ! printf '%s' "$2" | grep -q "Copyright (C)"; then
echo "FAIL (upstream-derived file has no copyright header): $1"
return 1
elif ! printf '%s' "$2" | grep -q "Confluent"; then
echo "FAIL (upstream-derived file lost its Confluent header): $1"
return 1
fi
}

require_modifications_line() { # <file> <header> <reason>
if ! printf '%s' "$2" | grep -q "$FORK_HOLDER"; then
echo "FAIL ($3 but missing 'Modifications Copyright ... ${FORK_HOLDER}' line): $1"
return 1
fi
}

while IFS= read -r f; do
[ -f "$f" ] || continue
checked=$((checked + 1))
header=$(head -"$HEADER_WINDOW" "$f")

rename_entry=$(printf '%s\n' "$RENAMED_FROM_UPSTREAM" | grep -F "${f}|" || true)
if [ -n "$rename_entry" ]; then
old_path=${rename_entry#*|}
require_confluent "$f" "$header" || { fails=$((fails + 1)); continue; }
if ! git cat-file blob "${FORK_POINT}:${old_path}" 2>/dev/null | cmp -s - "$f"; then
require_modifications_line "$f" "$header" \
"renamed upstream file modified since the fork point" || fails=$((fails + 1))
fi
elif printf '%s\n' "$EXTRACTED_FROM_UPSTREAM" | grep -qxF "$f"; then
require_confluent "$f" "$header" || { fails=$((fails + 1)); continue; }
require_modifications_line "$f" "$header" \
"extraction of upstream-derived code" || fails=$((fails + 1))
elif printf '%s\n' "$upstream_files" | grep -qxF "$f"; then
require_confluent "$f" "$header" || { fails=$((fails + 1)); continue; }
if printf '%s\n' "$modified_since_fork" | grep -qxF "$f"; then
require_modifications_line "$f" "$header" \
"upstream-derived file modified since the fork point" || fails=$((fails + 1))
fi
else
# fork-original: fork header required, Confluent claim forbidden
if printf '%s' "$header" | grep -q "Confluent"; then
echo "FAIL (fork-original file claims Confluent copyright): $f"
fails=$((fails + 1))
elif ! printf '%s' "$header" | grep -q "$FORK_HOLDER"; then
echo "FAIL (fork-original file missing '${FORK_HOLDER}' header): $f"
fails=$((fails + 1))
fi
fi
done <<EOF
$(git ls-files '*.java')
EOF

echo "Checked ${checked} java files against fork point ${FORK_POINT} - ${fails} violation(s)."
[ "$fails" -eq 0 ]
141 changes: 141 additions & 0 deletions bin/test-check-copyright-headers.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
#
# Self-test for bin/check-copyright-headers.sh.
#
# Builds throwaway git repos that simulate the fork's provenance model (an "upstream"
# commit pinned as the fork point, then fork-original work layered on top) and asserts
# the scanner's verdict for every rule:
# 1. upstream-derived file, unmodified, Confluent header -> pass
# 2. upstream-derived file that LOST its header -> FAIL
# 3. upstream-derived file MODIFIED, no modifications line -> FAIL
# 4. upstream-derived file MODIFIED, dual header -> pass
# 5. renamed upstream file, content unchanged, Confluent-only -> pass
# 6. renamed upstream file, content changed, no mods line -> FAIL
# 7. extraction of upstream code, no mods line -> FAIL
# 8. fork-original file with the fork header -> pass
# 9. fork-original file claiming Confluent copyright -> FAIL
# 10. fork-original file with no recognised header -> FAIL
# 11. clean repo -> exit 0
# 12. fork-point commit missing from history (shallow clone) -> exit 2
#
# Run: bin/test-check-copyright-headers.sh (CI runs it before the real scan)

set -eu

SCANNER="$(cd "$(dirname "$0")" && pwd)/check-copyright-headers.sh"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT

failures=0

assert() { # <description> <expected> <actual>
if [ "$2" = "$3" ]; then
echo "ok: $1"
else
echo "FAIL: $1 (expected '$2', got '$3')"
failures=$((failures + 1))
fi
}

assert_contains() { # <description> <needle> <haystack>
case "$3" in
*"$2"*) echo "ok: $1" ;;
*) echo "FAIL: $1 (output missing '$2')"; failures=$((failures + 1)) ;;
esac
}

new_repo() { # <dir>
git init -q "$1"
git -C "$1" config user.email test@example.invalid
git -C "$1" config user.name "scanner-test"
}

confluent_file() { # <path> [body]
printf '/*-\n * Copyright (C) 2020-2022 Confluent, Inc.\n */\nclass X { %s }\n' "${2:-}" > "$1"
}

dual_file() { # <path> [body]
printf '/*-\n * Copyright (C) 2020-2022 Confluent, Inc.\n * Modifications Copyright (C) 2026 Antony Stubbs and contributors\n */\nclass X { %s }\n' "${2:-}" > "$1"
}

fork_file() { # <path>
printf '/*-\n * Copyright (C) 2026 Antony Stubbs and contributors\n */\nclass X {}\n' > "$1"
}

headerless_file() { # <path>
printf 'class X {}\n' > "$1"
}

# --- Fixture A: one repo exercising rules 1-10 ------------------------------------
repoA="$WORK/a"
new_repo "$repoA"
confluent_file "$repoA/Upstream.java" # 1: stays untouched
confluent_file "$repoA/LosesHeader.java" # 2: header removed post-fork
confluent_file "$repoA/ModifiedNoLine.java" # 3: modified post-fork, line forgotten
confluent_file "$repoA/ModifiedDual.java" # 4: modified post-fork, line added
confluent_file "$repoA/RenamedSameOld.java" # 5: renamed verbatim post-fork
confluent_file "$repoA/RenamedChangedOld.java" # 6: renamed WITH changes post-fork
git -C "$repoA" add -A && git -C "$repoA" commit -qm upstream
fork_point_a=$(git -C "$repoA" rev-parse HEAD)

headerless_file "$repoA/LosesHeader.java" # rule 2 violation
confluent_file "$repoA/ModifiedNoLine.java" "int changed;" # rule 3 violation
dual_file "$repoA/ModifiedDual.java" "int changed;" # rule 4 conformant
git -C "$repoA" mv RenamedSameOld.java RenamedSame.java # rule 5 conformant (verbatim)
git -C "$repoA" mv RenamedChangedOld.java RenamedChanged.java
confluent_file "$repoA/RenamedChanged.java" "int changed;" # rule 6 violation
confluent_file "$repoA/Extraction.java" "int extracted;" # rule 7 violation
fork_file "$repoA/ForkGood.java" # rule 8 conformant
confluent_file "$repoA/ForkClaimsConfluent.java" # rule 9 violation
headerless_file "$repoA/ForkNoHeader.java" # rule 10 violation
git -C "$repoA" add -A && git -C "$repoA" commit -qm fork

renames="RenamedSame.java|RenamedSameOld.java
RenamedChanged.java|RenamedChangedOld.java"

out=$( (cd "$repoA" && COPYRIGHT_CHECK_FORK_POINT="$fork_point_a" \
COPYRIGHT_CHECK_EXTRA_RENAMES="$renames" \
COPYRIGHT_CHECK_EXTRA_EXTRACTIONS="Extraction.java" \
bash "$SCANNER") 2>&1 ) && rc=0 || rc=$?
assert "violating repo exits 1" 1 "$rc"
assert_contains "detects upstream file losing its header" "upstream-derived file has no copyright header): LosesHeader.java" "$out"
assert_contains "detects modified upstream file w/o mods line" "upstream-derived file modified since the fork point but missing 'Modifications Copyright ... Antony Stubbs and contributors' line): ModifiedNoLine.java" "$out"
assert_contains "detects changed rename w/o mods line" "renamed upstream file modified since the fork point but missing 'Modifications Copyright ... Antony Stubbs and contributors' line): RenamedChanged.java" "$out"
assert_contains "detects extraction w/o mods line" "extraction of upstream-derived code but missing 'Modifications Copyright ... Antony Stubbs and contributors' line): Extraction.java" "$out"
assert_contains "detects fork file claiming Confluent" "fork-original file claims Confluent copyright): ForkClaimsConfluent.java" "$out"
assert_contains "detects fork file with no header" "missing 'Antony Stubbs and contributors' header): ForkNoHeader.java" "$out"
assert_contains "reports exactly 6 violations" "6 violation(s)" "$out"
case "$out" in
*"ForkGood.java"*|*"Upstream.java"*|*"ModifiedDual.java"*|*"RenamedSame.java"*)
echo "FAIL: conformant files were flagged"; failures=$((failures + 1)) ;;
*) echo "ok: conformant files (untouched, dual-header, verbatim rename, fork) not flagged" ;;
esac

# --- Fixture B: clean repo (rule 11) -----------------------------------------------
repoB="$WORK/b"
new_repo "$repoB"
confluent_file "$repoB/Upstream.java"
confluent_file "$repoB/Modified.java"
git -C "$repoB" add -A && git -C "$repoB" commit -qm upstream
fork_point_b=$(git -C "$repoB" rev-parse HEAD)
dual_file "$repoB/Modified.java" "int changed;"
fork_file "$repoB/ForkGood.java"
git -C "$repoB" add -A && git -C "$repoB" commit -qm fork

out=$( (cd "$repoB" && COPYRIGHT_CHECK_FORK_POINT="$fork_point_b" bash "$SCANNER") 2>&1 ) && rc=0 || rc=$?
assert "clean repo exits 0" 0 "$rc"
assert_contains "clean repo reports zero violations" "0 violation(s)" "$out"

# --- Fixture C: fork point not in history (rule 12) ---------------------------------
out=$( (cd "$repoB" && COPYRIGHT_CHECK_FORK_POINT=0000000000000000000000000000000000000000 \
bash "$SCANNER") 2>&1 ) && rc=0 || rc=$?
assert "missing fork point exits 2" 2 "$rc"
assert_contains "missing fork point explains the fix" "fetch-depth: 0" "$out"

echo
if [ "$failures" -eq 0 ]; then
echo "All scanner self-tests passed."
else
echo "$failures scanner self-test(s) FAILED."
exit 1
fi
13 changes: 13 additions & 0 deletions docs/inflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
> can see them. Records work that is parked, in progress on other branches, or otherwise not obvious
> from `git log`. Keep it current when context-switching. Last updated: 2026-07-28.
>
> **Scope rule: this file records ONLY inflight work, or context required for something inflight.**
> No completed-work narratives, root-cause write-ups, or policy documentation - those belong in
> AGENTS.md (policy), PR descriptions/commit messages (history), or `docs/solutions/` (lessons).
> When work finishes, delete or shrink its entry rather than converting it into a record.
>
> **The durable fork↔upstream mapping now lives in a machine-readable cache:**
> [`src/docs/development/upstream-map.yaml`](../src/docs/development/upstream-map.yaml) is the
> source of truth for which fork branch/PR maps to which upstream issue/PR and its status
Expand Down Expand Up @@ -443,3 +448,11 @@ all are pre-existing job/gate problems. Only three checks actually gate merge (r
PRs, but Integration / PIT / Kafka-Compat are *not* filtered the same way, so they run and fail on
changes that touch no code. **Fix:** align the `paths`/`paths-ignore` filters across these jobs so a
docs-only change runs a consistent (or fully skipped) set.

## Copyright headers - open follow-up (2026-07-31)

- **Chaos stack must pass the new header check once PR #90 merges.** The scanner
(`bin/check-copyright-headers.sh`, policy in AGENTS.md) registers `ManagedPCInstance` as an
upstream EXTRACTION, requiring Confluent + the `Modifications Copyright (C) 2026 Antony Stubbs
and contributors` line - PR #83 currently has it Confluent-only, so add that line on the stack.
The stack's other fork-header fixes are already committed (#83) or in flight (#85).
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/*-
* Copyright (C) 2020-2022 Confluent, Inc.
* Modifications Copyright (C) 2026 Antony Stubbs and contributors
*/


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

/*-
* Copyright (C) 2020-2022 Confluent, Inc.
* Modifications Copyright (C) 2026 Antony Stubbs and contributors
*/


Expand Down
Loading
Loading