Skip to content

QVAC-21554 feat: canonical license compliance reusable workflow#12

Merged
GSServita merged 2 commits into
mainfrom
feat/license-compliance-ci
Jul 20, 2026
Merged

QVAC-21554 feat: canonical license compliance reusable workflow#12
GSServita merged 2 commits into
mainfrom
feat/license-compliance-ci

Conversation

@GSServita

Copy link
Copy Markdown
Contributor

🎯 What problem does this PR solve?

  • License / NOTICE / compliance review on PRs relies on a human remembering to run the compliance SKILL. It is opt-in, non-blocking, and leaves no durable record — a disallowed license (AGPL, SSPL, GPL on a runtime path) can reach a shipped artifact unnoticed.
  • The approved Q2 design (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?

  • Adds the reusable workflow public-reusable-license.yml, consumed by Tier-1 repos via a single uses: line (mirrors public-reusable-security.yml):
    • Engine Aactions/dependency-review-action enumerates the PR dependency diff (license + lockfile drift); license-policy.mjs then classifies every added dependency against the allow / deny / review policy, the consumer's exception allowlist, and the per-PR license-override label, into the Critical / High / Medium matrix from the design doc.
    • Engine B — an advisory NOTICE presence check (verifies dependency-bearing manifests ship a NOTICE; generation stays out of scope).
    • Writes a job-summary table and upserts a single PR comment. warn-only supports the recommended shadow-mode rollout.
  • Adds 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 via job.workflow_repository / job.workflow_sha, so the classifier always matches the pinned workflow version.
  • Adds license-self-test-negative.yml (asserts the full fail-vs-warn matrix on synthetic fixtures) and license-self-test.yml (end-to-end smoke run in warn-only mode).
  • Flips docs/license-compliance-ci.md status to v0 (implemented) and adds a usage / inputs / versioning section.

🧪 How was it tested?

  • actionlint clean on all new workflows, except two known false positives in public-reusable-license.yml for job.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.mjs validated locally against the full matrix: disallowed-on-runtime → Critical (exit 1); Critical not overridable; warn-only never blocks; unknown/review → High (blocks, then clears via license-override label or allowlist); permissive → clean; MIT OR AGPL-3.0 correctly resolves to allowed; GPL on a dev-only path → Medium (warn).
  • license-self-test-negative.yml codifies that matrix as a CI regression guard; license-self-test.yml exercises the job.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.0
  • actions/checkout and actions/github-script reuse this repo's existing pins (34e114876b0b11c390a56381ad16ebd13914f8d5 # v4, f28e40c7f34bde8b3046d885e986cb6290c5673b # v7).

🛡️ Permissions changes

  • Scope: new reusable workflow public-reusable-license.yml, job license.
  • Before: n/a (new workflow).
  • After: contents: read, pull-requests: write.
  • Justification: contents: read for checkout + the dependency-review graph API; pull-requests: write only to upsert the single summary comment. No write to code. Documented as the required caller permissions in the workflow header.

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.
@GSServita
GSServita requested review from a team as code owners July 14, 2026 10:24
@github-actions

Copy link
Copy Markdown

Review Status

Current Status: ❌ PENDING
Approvals so far: none

Pending reviews: Needs 1 Management or Team Lead, and 1 more from Management, Team Lead, or Member.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

License compliance — findings detected (warn-only)

Critical: 0 · High: 3 · Medium: 0

Dependency License Scope Severity Outcome
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 (none detected) runtime High blocks
actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 (none detected) runtime High blocks
actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b (none detected) runtime High blocks

How to resolve a blocking finding:

  • Remove or replace the disallowed dependency, or
  • If the license is genuinely acceptable, run the compliance SKILL and record the decision in .github/license-allowlist.yml (CODEOWNERS-reviewed), or
  • For a one-off, a maintainer can apply the license-override label (High findings only; Critical cannot be overridden).

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):

  • ./poc-apps-rpchook-server
  • ./hyperbee-models-server

@Proletter

Proletter commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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:

  1. pull_request_target in an org-wide reusable workflow. The job's if: conditions include github.event_name == 'pull_request_target' even though the current consumer (qvac#3270) only calls it under plain pull_request. Since this workflow is org-wide and requests pull-requests: write, worth confirming a future caller can't invoke it under pull_request_target on fork PRs and inherit write + secret context. Consider documenting in the header that callers MUST use pull_request, or defensively handling the pull_request_target path.

  2. Coverage is manifest/direct-dep only. The gate reads GitHub's dependency graph; in the first consumer (qvac) lockfiles are gitignored, so the classifier only sees direct/manifest-declared deps, not the full transitive tree — a disallowed transitive license can still pass. This is acknowledged and routed to the SKILL fallback, which is reasonable, but given this ticket's DevOps Type = Security it's worth stating the limitation prominently in docs/license-compliance-ci.md (not only in the consumer SKILL doc).

  3. Actionlint false positives. Agreed on the job.workflow_repository / job.workflow_sha false positives being canonical context properties; no change needed, just flagging for the next reviewer so it isn't "fixed" away.

…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.
@GSServita

Copy link
Copy Markdown
Contributor Author

Thanks @Proletter — addressed in ce61672c:

1. pull_request_target hardening. Done, defensively. Added a fail-fast guard step at the top of the license job that errors out if the workflow is invoked on pull_request_target, and removed pull_request_target from the step if: conditions so it's not implicitly supported. The header now carries a SECURITY: note stating callers MUST use pull_request (or push) and why (it requests pull-requests: write, so pull_request_target on a fork PR would hand write + secret context to untrusted code). So a future caller can't quietly wire it under pull_request_target — the check goes red loudly.

2. Manifest-only / transitive coverage. Agreed it belongs here given DevOps Type = Security. Added a prominent "Known limitation (security-relevant): transitive coverage" callout in docs/license-compliance-ci.md (Usage section), and a COVERAGE LIMITATION note in the workflow header. Both state that in lockfile-less repos the dependency graph is manifest-only, so a disallowed transitive license can still pass, and that full transitive resolution stays with the SKILL fallback — with the recommendation to weigh lockfile adoption per Tier-1 repo before flipping to blocking. Also surfaced consumer-side in qvac#3270 (caller header + TIER-1-SCOPE §B1).

3. Actionlint false positives. Left as-is per your note — job.workflow_repository / job.workflow_sha are the canonical context props; flagging here so they aren't "fixed" away by the next reviewer.

@GSServita
GSServita merged commit 5fc9d2a into main Jul 20, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants