From 467e01af64ead879ad7262bfc8b20803fb9cf671 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 6 May 2026 21:13:12 +0000 Subject: [PATCH] CI(lint): accept GNU long-form license headers (AGPL/LGPL/GPL) The license-header check missed two more legitimate header families that are committed to the repo today: - LGPL-3.0 long form: e.g. unsloth/kernels/rope_embedding.py opens with "GNU Lesser General Public License" -- 7 such files under unsloth/kernels/. - AGPL-3.0 long form: e.g. unsloth/kernels/moe/autotune_cache.py opens with "GNU Affero General Public License" -- 2 such files under unsloth/kernels/moe/. Both got flagged as drift on the previous run because the check only knew about the SPDX one-liner and the Apache-2.0 preamble. Add a third accepted marker, the substring "General Public License", which appears in all three GNU long-form preambles (GPL, LGPL, AGPL) and nothing else. Repo inventory: spdx (one-liner) 193 files (mostly studio/) apache-longform 55 files (unsloth/, unsloth_cli/) agpl-longform 2 files (unsloth/kernels/moe/) lgpl/gpl-longform 7 files (unsloth/kernels/) no recognised header 85 files (real drift -- mostly tests/) So the warning count drops from 94 -> 85 with this commit; the remaining 85 are actual missing headers, surfaced as a non-blocking warning until the cleanup PR lands. --- .github/workflows/lint-ci.yml | 43 +++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index 3896c07868..1e8fab2149 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -114,23 +114,32 @@ jobs: PY - name: License-header drift (informational; whole repo) - # The repo currently uses two header families: - # 1. AGPL-3.0-only with `# SPDX-License-Identifier:` - # across studio/ (every committed .py opens with it). - # 2. Apache-2.0 with the - # `# Licensed under the Apache License, Version 2.0` - # preamble across unsloth/, unsloth_cli/, scripts/. - # Either is acceptable. Empty files (mainly empty - # __init__.py) are skipped. We surface the count without - # blocking; cleaning up the missing files is real work and - # belongs in its own PR. + # Three header families are accepted across the repo: + # 1. SPDX one-liner: `# SPDX-License-Identifier: ...` + # Used across studio/ (AGPL-3.0-only) and a few new + # files elsewhere. + # 2. Apache-2.0 long form, marker phrase + # "Licensed under the Apache License". Used across + # unsloth/ and unsloth_cli/. + # 3. GNU long form, marker phrase "General Public License". + # That single substring covers GPL, LGPL ("GNU Lesser + # General Public License") and AGPL ("GNU Affero + # General Public License") preambles, all three of + # which appear in unsloth/kernels/* (LGPL/AGPL) without + # the SPDX line. + # Empty files (mainly empty __init__.py) are skipped. + # Surfaced as a warning; cleaning up the actual misses is a + # follow-up PR, not a CI fix. continue-on-error: true run: | python <<'PY' import pathlib - SPDX = "SPDX-License-Identifier" - APACHE = "Licensed under the Apache License" + ACCEPTED = ( + "SPDX-License-Identifier", # any SPDX line + "Licensed under the Apache License", # Apache-2.0 long form + "General Public License", # GPL / LGPL / AGPL long form + ) SKIP_PARTS = {".venv", "venv", "build", "dist", ".git", "unsloth_compiled_cache", "node_modules", "unsloth.egg-info"} @@ -143,8 +152,8 @@ jobs: text = path.read_text(encoding="utf-8", errors="replace") if not text.strip(): continue # empty __init__.py etc. - head = "\n".join(text.splitlines()[:20]) - if SPDX in head or APACHE in head: + head = "\n".join(text.splitlines()[:25]) + if any(marker in head for marker in ACCEPTED): continue if "studio" in path.parts: studio_missing.append(path) @@ -155,9 +164,9 @@ jobs: if total == 0: print("every committed .py has a recognised license header") else: - print(f"::warning::{total} Python files are missing both SPDX-License-Identifier " - f"and the Apache-2.0 preamble (studio={len(studio_missing)}, " - f"other={len(other_missing)})") + print(f"::warning::{total} Python files have no recognised license " + f"header (SPDX / Apache-2.0 / GNU long form): " + f"studio={len(studio_missing)}, other={len(other_missing)}") for path in (studio_missing + other_missing)[:30]: print(f" {path}") if total > 30: