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
3 changes: 3 additions & 0 deletions .github/scripts/bump_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/usr/bin/env python3
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Compute and commit the next release version.

Reads the current version from pyproject.toml, computes the next version per
Expand Down
182 changes: 182 additions & 0 deletions .github/scripts/check_license_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Check (or fix) SPDX license headers on code files.

Every code file must begin with the two-line header (after an optional shebang):

Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0

rendered in the file's native comment syntax. This script is stdlib-only so it can
run in CI without installing dependencies.

Scope (matches what the headers were originally applied to):
- Included extensions: .py .sh .ts .tsx .js .mjs .css
- Excluded: everything under docs/, all Jinja templates, and non-code file types.

Usage:
python3 .github/scripts/check_license_headers.py --check # CI: exit 1 if any missing
python3 .github/scripts/check_license_headers.py --fix # insert/upgrade in place
"""

from __future__ import annotations

import argparse
import subprocess
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent.parent

COPYRIGHT = "Copyright IBM Corp. All Rights Reserved."
SPDX = "SPDX-License-Identifier: Apache-2.0"

# Comment prefix per extension family.
HASH_EXTS = {".py", ".sh"}
SLASH_EXTS = {".ts", ".tsx", ".js", ".mjs"}
CSS_EXTS = {".css"}
ALL_EXTS = HASH_EXTS | SLASH_EXTS | CSS_EXTS

# Paths (relative to repo root) whose subtrees are exempt from the header rule.
EXCLUDED_PREFIXES = ("docs/",)

# How many leading lines to scan for the header (tolerates a shebang + blank line).
SCAN_LINES = 6


def tracked_files() -> list[Path]:
"""Return git-tracked files in scope: target extensions, minus excluded subtrees."""
patterns = [f"*{ext}" for ext in ALL_EXTS]
out = subprocess.run(
["git", "ls-files", "-z", *patterns],
cwd=REPO,
capture_output=True,
text=True,
check=True,
).stdout
result: list[Path] = []
for rel in out.split("\0"):
if not rel:
continue
if rel.startswith(EXCLUDED_PREFIXES):
continue
result.append(REPO / rel)
return result


def header_lines(ext: str) -> list[str]:
"""Build the header lines (no trailing newlines) for a file extension."""
if ext in CSS_EXTS:
return [f"/* {COPYRIGHT} */", f"/* {SPDX} */"]
prefix = "//" if ext in SLASH_EXTS else "#"
return [f"{prefix} {COPYRIGHT}", f"{prefix} {SPDX}"]


def has_header(lines: list[str]) -> bool:
"""True if both the copyright and SPDX lines appear near the top of the file."""
head = "\n".join(lines[:SCAN_LINES])
return COPYRIGHT in head and SPDX in head


def fix_file(path: Path) -> str:
"""Insert or upgrade the header in a single file. Returns an action string."""
ext = path.suffix
text = path.read_text(encoding="utf-8")
lines = text.split("\n")
had_trailing_newline = text.endswith("\n")
if had_trailing_newline and lines and lines[-1] == "":
lines = lines[:-1]

hdr = header_lines(ext)

if has_header(lines):
return "ok"

# Upgrade: a bare one-line SPDX with no copyright line — replace it with the pair,
# keeping the comment style already present on that line.
for i, line in enumerate(lines[:SCAN_LINES]):
if SPDX in line and COPYRIGHT not in line:
lines[i : i + 1] = hdr
new_text = "\n".join(lines) + ("\n" if had_trailing_newline else "")
path.write_text(new_text, encoding="utf-8")
return "upgraded"

# Fresh insert, after a shebang if present.
insert_at = 1 if lines and lines[0].startswith("#!") else 0
block = list(hdr)
following = lines[insert_at] if insert_at < len(lines) else ""
if following.strip() != "":
block.append("")
lines[insert_at:insert_at] = block
new_text = "\n".join(lines) + ("\n" if had_trailing_newline else "")
path.write_text(new_text, encoding="utf-8")
return "inserted"


def run_check(files: list[Path]) -> int:
"""Report files missing the header. Return process exit code."""
missing = [
p for p in files if not has_header(p.read_text(encoding="utf-8").split("\n"))
]
if not missing:
print(f"All {len(files)} code files have SPDX license headers.")
return 0

print(
f"{len(missing)} file(s) are missing the SPDX license header:\n",
file=sys.stderr,
)
for p in missing:
print(f" {p.relative_to(REPO)}", file=sys.stderr)
print(
"\nEvery code file must begin with (after any shebang):\n\n"
f" {COPYRIGHT}\n"
f" {SPDX}\n\n"
"in the file's comment syntax (# for .py/.sh, // for .ts/.tsx/.js/.mjs, /* */ for .css).\n\n"
"Fix locally with:\n\n"
" python3 .github/scripts/check_license_headers.py --fix\n",
file=sys.stderr,
)
return 1


def run_fix(files: list[Path]) -> int:
"""Insert/upgrade headers in place. Return process exit code (always 0)."""
counts: dict[str, int] = {}
for p in files:
action = fix_file(p)
counts[action] = counts.get(action, 0) + 1
if action != "ok":
print(f"{action:9} {p.relative_to(REPO)}")
changed = counts.get("inserted", 0) + counts.get("upgraded", 0)
print(
f"\nDone: {changed} file(s) updated "
f"({counts.get('inserted', 0)} inserted, {counts.get('upgraded', 0)} upgraded), "
f"{counts.get('ok', 0)} already compliant."
)
return 0


def main() -> int:
"""Parse args and dispatch to check or fix."""
parser = argparse.ArgumentParser(description=__doc__)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--check",
action="store_true",
help="Report files missing the header and exit non-zero (default).",
)
group.add_argument(
"--fix", action="store_true", help="Insert or upgrade headers in place."
)
args = parser.parse_args()

files = tracked_files()
if args.fix:
return run_fix(files)
return run_check(files)


if __name__ == "__main__":
sys.exit(main())
3 changes: 3 additions & 0 deletions .github/scripts/cut_release_branch.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/bin/bash
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# Cut a release branch off main and apply matched version bumps to both branches.
#
# Expected state before running:
Expand Down
3 changes: 3 additions & 0 deletions .github/scripts/release.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#!/bin/bash
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# Publish a release from the currently checked-out branch. Uses the version
# already written to pyproject.toml; does not modify it.
#
Expand Down
3 changes: 3 additions & 0 deletions .github/scripts/set-last-version.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright IBM Corp. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import {readFileSync, writeFileSync} from 'node:fs';

const [, , version] = process.argv;
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ jobs:
- name: Lint GitHub Actions workflows
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2

license-headers:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Check SPDX license headers
run: python3 .github/scripts/check_license_headers.py --check

quality:
runs-on: ubuntu-latest
timeout-minutes: 180
Expand Down
3 changes: 3 additions & 0 deletions test/backends/test_audio_openai.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""E2E tests for audio-text-to-text with OpenAI-compatible backends.

These tests require a local llama-server with an audio-capable model.
Expand Down
3 changes: 3 additions & 0 deletions test/backends/test_audio_openai_unit.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Unit tests for audio payload shape on OpenAI-compatible backends (mocked).

Verifies that `AudioBlock` is correctly serialised into the ``input_audio``
Expand Down
3 changes: 3 additions & 0 deletions test/backends/test_reasoning_replay.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Per-backend multi-turn reasoning-replay policy tests (no live model).

These tests assert the *messages array actually sent to the provider* on a
Expand Down
3 changes: 3 additions & 0 deletions test/backends/test_reasoning_replay_e2e.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Live multi-turn reasoning-replay verification against a real thinking model.

This is the "quick verification after the fact" for issue #1201: the mechanical
Expand Down
3 changes: 3 additions & 0 deletions test/backends/test_watsonx_multimodal_unit.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Unit tests verifying WatsonxAIBackend rejects image and audio inputs.

Watsonx does not support image or audio inputs. These tests confirm that
Expand Down
3 changes: 3 additions & 0 deletions test/stdlib/test_session_audio_unit.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright IBM Corp. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Unit tests verifying MelleaSession forwards audio params to mfuncs — no backend required."""

import base64
Expand Down
Loading