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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,10 @@ jobs:
- name: Check out code
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-depth: 2

- name: Run license check
run: ./scripts/license.sh .
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.sha || github.event.merge_group.head_sha }}
run: ./scripts/license.sh . "$BASE_SHA" "$HEAD_SHA"
92 changes: 55 additions & 37 deletions scripts/license.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,26 @@
# Copyright APROINT, s.r.o. in modifications to this fork.
# SPDX-License-Identifier: Apache-2.0
#
# Check tracked Go and JavaScript files against the tree before the first APROINT commit.
# Check Go and JavaScript files changed by a pull request or merge group.

if [ "$#" -ne 1 ]; then
echo "Usage: $0 rootdir" >&2
if [ "$#" -ne 3 ]; then
echo "Usage: $0 rootdir base-sha head-sha" >&2
exit 1
fi

python3 - "$1" <<'PY'
python3 - "$1" "$2" "$3" <<'PY'
import pathlib
import subprocess
import sys

FORK_POINT = "a0dbe68908e55c4292e5838bd6cfbaea38364879"
AMAZON = "// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."
APROINT = "// Copyright APROINT, s.r.o."
MODIFICATIONS = "// Copyright APROINT, s.r.o. in modifications to this fork."
SPDX = "// SPDX-License-Identifier: Apache-2.0"
DERIVED = "// Derived from AWS Copilot CLI source."

root = pathlib.Path(sys.argv[1]).resolve()
base, head = sys.argv[2:4]


def git(*args):
Expand All @@ -32,15 +32,12 @@ def git(*args):


try:
git("cat-file", "-e", f"{FORK_POINT}^{{commit}}")
except subprocess.CalledProcessError:
sys.exit(f"license check: fork-point commit {FORK_POINT} is unavailable; fetch it before checking licenses")

try:
inherited = set(git("ls-tree", "-r", "--name-only", "-z", FORK_POINT).split(b"\0"))
tracked = git("ls-files", "-z", "--", "*.go", "*.js").split(b"\0")
changes = git(
"diff", "--no-renames", "--diff-filter=AM", "--name-status", "-z",
base, head, "--", "*.go", "*.js"
).split(b"\0")
except subprocess.CalledProcessError as error:
sys.exit(f"license check: git failed: {error.stderr.decode(errors='replace').strip()}")
sys.exit(f"license check: cannot compare {base} and {head}: {error.stderr.decode(errors='replace').strip()}")


def in_scope(path):
Expand All @@ -53,41 +50,62 @@ def in_scope(path):
)


failures = 0
for raw_path in tracked:
if not raw_path:
continue
path = raw_path.decode("utf-8", errors="surrogateescape")
if not in_scope(path):
continue

lines = (root / path).read_text(encoding="utf-8").splitlines()
def header_kind(lines, path):
offset = 0
if path.endswith(".go") and lines and lines[0].startswith("//go:build "):
offset = 1
while offset < len(lines) and lines[offset].startswith("// +build "):
offset += 1
if offset >= len(lines) or lines[offset] != "":
failures += 1
print(f"{path}: expected a blank line after Go build tags")
continue
return None, "expected a blank line after Go build tags"
offset += 1

if raw_path in inherited:
expected = [AMAZON, MODIFICATIONS, SPDX]
elif lines[offset:offset + 1] == [AMAZON]:
expected = [AMAZON, MODIFICATIONS, SPDX, DERIVED]
else:
expected = [APROINT, SPDX]
formats = {
"derived": [AMAZON, MODIFICATIONS, SPDX, DERIVED],
"inherited": [AMAZON, MODIFICATIONS, SPDX],
"new": [APROINT, SPDX],
"legacy": [AMAZON, SPDX],
}
for kind, expected in formats.items():
if lines[offset:offset + len(expected)] == expected:
following = lines[offset + len(expected):offset + len(expected) + 1]
if following and following[0].startswith(
("// Copyright ", "// SPDX-License-Identifier:", "// Derived from ")
):
return None, "unexpected extra license line after header"
return kind, None
return None, f"invalid header at line {offset + 1}"


failures = 0
for status, raw_path in zip(changes[0::2], changes[1::2]):
path = raw_path.decode("utf-8", errors="surrogateescape")
if not in_scope(path):
continue

lines = (root / path).read_text(encoding="utf-8").splitlines()
kind, error = header_kind(lines, path)
if error or kind == "legacy":
failures += 1
print(f"{path}: {error or 'Amazon-only header is no longer valid'}")
continue

if status == b"A":
if kind not in ("new", "derived"):
failures += 1
print(f"{path}: new files need the APROINT header, or the derived-source marker")
continue

if lines[offset:offset + len(expected)] != expected:
try:
previous = git("show", f"{base}:{path}").decode("utf-8").splitlines()
except subprocess.CalledProcessError:
failures += 1
print(f"{path}: expected header at line {offset + 1}: {' | '.join(expected)}")
elif len(lines) > offset + len(expected) and lines[offset + len(expected)].startswith(
("// Copyright ", "// SPDX-License-Identifier:", "// Derived from ")
):
print(f"{path}: cannot read the base version")
continue
previous_kind, _ = header_kind(previous, path)
if previous_kind in ("new", "inherited", "derived") and kind != previous_kind:
failures += 1
print(f"{path}: unexpected extra license line after header")
print(f"{path}: expected the {previous_kind} header from the base version")

if failures:
sys.exit(f"license check: {failures} file(s) have invalid headers")
Expand Down