QVAC-21554 feat: canonical license compliance reusable workflow#12
Conversation
Implements the v0 license/compliance CI gate from docs/license-compliance-ci.md (the approved Q2 design). Mirrors the security baseline: one reusable workflow consumed by Tier-1 repos via a single `uses:` line, a job-summary table, and a single upserted PR comment. - public-reusable-license.yml: Engine A enumerates the PR dependency diff via actions/dependency-review-action, then license-policy.mjs classifies every added dependency (allow/deny/review + exception allowlist + license-override label) into the Critical/High/Medium matrix. Engine B is an advisory NOTICE presence check. warn-only supports shadow-mode rollout. - license-policy.mjs: dependency-free classifier (single source of truth for the gate and the self-test). - license-self-test-negative.yml: asserts the fail-vs-warn matrix on synthetic fixtures. license-self-test.yml: end-to-end smoke run in warn-only mode. - docs/license-compliance-ci.md: status flipped to v0 (implemented) with a usage/inputs/versioning section. The gate is primary; the compliance SKILL is the human fallback for the long tail the policy cannot classify.
Review StatusCurrent Status: ❌ PENDING Pending reviews: Needs 1 Management or Team Lead, and 1 more from Management, Team Lead, or Member. |
License compliance — findings detected (warn-only)Critical: 0 · High: 3 · Medium: 0
How to resolve a blocking finding:
Warn-only (shadow) mode — this check does not block merges yet. Updated automatically by the canonical license compliance workflow. NOTICE presence (advisory)Missing NOTICE (advisory, does not block):
|
|
Reviewed against QVAC-21554 ("License / compliance enforced in CI on Tier-1 — SKILL becomes fallback"). This PR (the reusable engine) plus qvac#3270 (the consumer wiring + SKILL demotion + TIER-1-SCOPE §B1) together cover the ticket cleanly, and the split mirrors the security baseline (QVAC-21550). A few notes from the review:
|
…ment transitive limit Addresses Olu's review on #12: - Guard: fail fast if the reusable workflow is called on pull_request_target (it requests pull-requests: write, so a fork PR under pull_request_target would inherit write + secret context). Header documents that callers MUST use pull_request; the pull_request_target branches are removed from the step `if:` conditions. - Doc: add a prominent security-relevant "Known limitation: transitive coverage" callout to docs/license-compliance-ci.md — in lockfile-less repos the dependency graph is manifest-only, so a disallowed transitive license can still pass; full transitive resolution stays with the SKILL fallback.
|
Thanks @Proletter — addressed in 1. 2. Manifest-only / transitive coverage. Agreed it belongs here given DevOps Type = Security. Added a prominent "Known limitation (security-relevant): transitive coverage" callout in 3. Actionlint false positives. Left as-is per your note — |
🎯 What problem does this PR solve?
docs/license-compliance-ci.md) specified turning that SKILL into a deterministic CI gate on Tier-1 repos, with the SKILL demoted to the fallback for the long tail. This PR is the v0 implementation of that design (QVAC-21554).📝 How does it solve it?
public-reusable-license.yml, consumed by Tier-1 repos via a singleuses:line (mirrorspublic-reusable-security.yml):actions/dependency-review-actionenumerates the PR dependency diff (license + lockfile drift);license-policy.mjsthen classifies every added dependency against the allow / deny / review policy, the consumer's exception allowlist, and the per-PRlicense-overridelabel, into the Critical / High / Medium matrix from the design doc.warn-onlysupports the recommended shadow-mode rollout.license-policy.mjs, a dependency-free classifier that is the single source of truth for both the workflow and the self-test. The reusable workflow fetches it from its own pinned commit viajob.workflow_repository/job.workflow_sha, so the classifier always matches the pinned workflow version.license-self-test-negative.yml(asserts the full fail-vs-warn matrix on synthetic fixtures) andlicense-self-test.yml(end-to-end smoke run in warn-only mode).docs/license-compliance-ci.mdstatus to v0 (implemented) and adds a usage / inputs / versioning section.🧪 How was it tested?
actionlintclean on all new workflows, except two known false positives inpublic-reusable-license.ymlforjob.workflow_repository/job.workflow_sha— these are the GitHub-canonical, documented context properties (github.com, since 2026-04) for a reusable workflow to check out its own source; actionlint 1.7.11's type DB has not caught up. No shellcheck findings.license-policy.mjsvalidated locally against the full matrix: disallowed-on-runtime → Critical (exit 1); Critical not overridable;warn-onlynever blocks; unknown/review → High (blocks, then clears vialicense-overridelabel or allowlist); permissive → clean;MIT OR AGPL-3.0correctly resolves to allowed; GPL on a dev-only path → Medium (warn).license-self-test-negative.ymlcodifies that matrix as a CI regression guard;license-self-test.ymlexercises thejob.workflow_*script fetch + dependency-review + classifier plumbing end-to-end on this repo.🔐 Action pinning
actions/dependency-review-action(new third-party action):a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0actions/checkoutandactions/github-scriptreuse this repo's existing pins (34e114876b0b11c390a56381ad16ebd13914f8d5 # v4,f28e40c7f34bde8b3046d885e986cb6290c5673b # v7).🛡️ Permissions changes
public-reusable-license.yml, joblicense.contents: read,pull-requests: write.contents: readfor checkout + the dependency-review graph API;pull-requests: writeonly to upsert the single summary comment. No write to code. Documented as the required caller permissions in the workflow header.