Skip to content

chore(notices): automate NOTICE.md generation + CI drift gate#1045

Merged
danielmeppiel merged 2 commits intomainfrom
chore/notice-automation
Apr 29, 2026
Merged

chore(notices): automate NOTICE.md generation + CI drift gate#1045
danielmeppiel merged 2 commits intomainfrom
chore/notice-automation

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

@danielmeppiel danielmeppiel commented Apr 29, 2026

Closes #1044.

Summary

Make NOTICE.md self-maintaining. After PR #1043 landed the file by hand, every future dep change would otherwise require a contributor to remember to update it. This PR makes the file generated and gates every PR on a drift check, with an additional license-policy gate as supply-chain hygiene.

What changes

File Why
scripts/generate-notice.py Renders NOTICE.md from pyproject.toml deps + curated per-component metadata, reading verbatim license text from the installed .dist-info/licenses/ dir of the uv-managed env. --check mode for CI.
scripts/notice-metadata.yaml Per-component overrides authored once per dep: SPDX choice, upstream URL, copyright snippet, optional NOTICE/AUTHORS attribution block. The few things PyPI metadata can't reliably give us.
.github/workflows/notice-drift.yml New required check. pull_request + merge_group triggers (matches ci.yml pattern). uv sync --frozen for reproducibility. actions/dependency-review-action@v4 on PR-time only as a license-policy gate.
.github/workflows/merge-gate.yml Adds NOTICE Drift Check to EXPECTED_CHECKS in both event branches so the unified gate waits for it.
Makefile make notice for local regeneration before pushing.
pyproject.toml (no new deps — ruamel.yaml is already a runtime dep, reused for the metadata YAML)

Why no SBOM in this PR

GitHub's dependency graph already exposes an SPDX SBOM for this repo on demand (GET /repos/{owner}/{repo}/dependency-graph/sbom), and Dependabot owns vulnerability + version updates over the same graph. Adding a second generator here would duplicate that surface without adding value. If a CycloneDX-format SBOM becomes required for compliance reporting, add a separate workflow keyed off release events.

Lenses applied

GitHub Actions specialization — minimum permissions (contents: read), uv sync --frozen (no silent lockfile mutation in CI), @major action pinning matching repo style, dual pull_request + merge_group trigger so the merge queue temp-branch gets the check (post-mortem on PR #899 in merge-gate.yml is the template), wired into merge-gate.yml EXPECTED_CHECKS so it's a real required check rather than an advisory one.

Supply chain & licensing — the generator reads license text from the actual installed .dist-info, so an upstream re-license caught by Dependabot triggers our drift check on the same PR (no separate manual review). dependency-review-action denies the strong-copyleft families (GPL / AGPL / SSPL) that would virally relicense the apm-cli binary; explicitly allows permissive + MPL-2.0 (file-level copyleft, no viral effect on combined work). Frozen syncs prevent CI from silently pulling a new LICENSE.

Proof of working

1. Byte-parity with the manually-authored NOTICE.md

The generator was developed against a captured copy (NOTICE.md.golden) of the file PR #1043 landed. Iterated until diff -u NOTICE.md NOTICE.md.golden produced empty output, then deleted the golden. This commit's NOTICE.md is unchanged from the file that landed in PR #1043.

$ git diff --stat 41af4d7c..HEAD -- NOTICE.md
(empty -- not modified)

2. --check mode behavior

$ uv run python scripts/generate-notice.py --check
$ echo $?
0

$ echo 'garbage_line' >> NOTICE.md
$ uv run python scripts/generate-notice.py --check
NOTICE.md is out of date with pyproject.toml + notice-metadata.yaml.
Run `make notice` (or `python scripts/generate-notice.py`) and commit.

--- NOTICE.md (on disk)
+++ NOTICE.md (regenerated)
@@ -1215,4 +1215,3 @@

 ---

-garbage_line
$ echo $?
1

$ git checkout NOTICE.md
$ uv run python scripts/generate-notice.py --check
$ echo $?
0

3. actionlint clean

$ actionlint .github/workflows/notice-drift.yml .github/workflows/merge-gate.yml
(no output -- both pass)

4. Drift simulation -- adding a dep

If a contributor adds a new dep without regenerating NOTICE, the gate fails the PR with the exact diff in the Actions log, plus a fix command in the error message (make notice and commit). The make notice target makes the local fix one command.

Out of scope (deferred)

  • Transitive-dep NOTICE expansion (current direct-only scope is industry-standard; expand only if CELA mandates).
  • Publishing CycloneDX/SPDX SBOM as a release artifact on build-release.yml — separate, release-event-keyed workflow if/when needed.
  • Vulnerability scanning over the SBOM — already covered by Dependabot.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copilot AI review requested due to automatic review settings April 29, 2026 15:18
Adds an automated NOTICE.md maintenance pipeline so the file stays in
sync with the install graph as deps evolve, plus a license-policy gate
on PRs.

Pieces:

1. scripts/generate-notice.py -- renders NOTICE.md from
   [project] dependencies in pyproject.toml + a curated
   scripts/notice-metadata.yaml of per-component overrides
   (SPDX, upstream URL, copyright snippet, optional NOTICE/AUTHORS
   attribution). Reads the verbatim license text from the installed
   .dist-info/licenses/ directory in the uv-managed env so it tracks
   whatever uv.lock resolves to. Default mode regenerates; --check
   diffs against the committed file and exits 1 with a unified diff
   if they differ.

2. .github/workflows/notice-drift.yml -- pull_request + merge_group
   workflow that runs the generator in --check mode and surfaces the
   diff in the Actions log if NOTICE.md is stale. Adds
   actions/dependency-review-action@v4 on PR-time only as a
   license-policy gate (denies GPL/AGPL/SSPL additions; allows
   permissive + MPL-2.0 explicitly).

3. .github/workflows/merge-gate.yml -- adds 'NOTICE Drift Check'
   to EXPECTED_CHECKS in both the pull_request and merge_group
   branches so the new check participates in the unified merge gate.

4. Makefile target 'notice' for local DX
   (`make notice` regenerates the file).

Why no SBOM emission: GitHub's dependency graph already produces an
SPDX SBOM for this repo (REST: GET /repos/{owner}/{repo}/dependency-graph/sbom)
and Dependabot owns vulnerability + version updates over the same graph.
A second SBOM generator here would duplicate that surface without value.
If a CycloneDX SBOM is later required for compliance reporting, add a
separate workflow keyed off release events.

Closes #1044

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel force-pushed the chore/notice-automation branch from 9184e0d to 8495b2c Compare April 29, 2026 15:22
@danielmeppiel danielmeppiel changed the title chore(notices): automate NOTICE.md generation + CycloneDX SBOM gate chore(notices): automate NOTICE.md generation + CI drift gate Apr 29, 2026
The action rejects specifying both `allow-licenses` and `deny-licenses`
('You cannot specify both'). Drop the allow-list -- the deny-list
already enforces the policy intent (block strong-copyleft families)
and avoids noisy false positives on novel-but-permissive SPDX
identifiers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel merged commit 28ac823 into main Apr 29, 2026
9 checks passed
@danielmeppiel danielmeppiel deleted the chore/notice-automation branch April 29, 2026 15:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an automated pipeline for keeping NOTICE.md (third-party attributions) in sync with the repo’s direct runtime dependencies, and adds CI gating (plus SBOM emission) so drift is caught on every PR and merge-queue entry.

Changes:

  • Add a NOTICE generator script driven by pyproject.toml deps + curated scripts/notice-metadata.yaml, with --check mode and CycloneDX SBOM emission.
  • Add a new required CI workflow (NOTICE Drift Check) and wire it into merge-gate.yml aggregation.
  • Add local developer shortcuts via make notice / make notice-check.
Show a summary per file
File Description
scripts/notice-metadata.yaml Adds curated metadata/overrides per dependency to render deterministic NOTICE sections.
scripts/generate-notice.py Implements NOTICE rendering, license text extraction from dist-info, and SBOM emission.
Makefile Adds DX targets to run the generator locally.
.github/workflows/notice-drift.yml Adds PR + merge-queue gate to enforce NOTICE drift checks and license policy checks, plus SBOM artifact.
.github/workflows/merge-gate.yml Adds NOTICE Drift Check to the set of checks aggregated by the PR-time gate.

Copilot's findings

Comments suppressed due to low confidence (1)

scripts/generate-notice.py:63

  • tempfile is imported but not used (the code renders to memory). Please drop the unused import (and any related code/comments) to keep the script tidy.
import difflib
import importlib.metadata as ilmd
import re
  • Files reviewed: 5/5 changed files
  • Comments generated: 4

Comment on lines +349 to +355
on_disk = NOTICE_OUT.read_text(encoding="utf-8") if NOTICE_OUT.exists() else ""
if on_disk == rendered:
return 0
diff = difflib.unified_diff(
on_disk.splitlines(keepends=True),
rendered.splitlines(keepends=True),
fromfile=str(NOTICE_OUT.relative_to(REPO_ROOT)) + " (on disk)",
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PackageNotFoundError path, version = dep.raw_specifier.lstrip(...) can produce values like "1.2.0; python_version<'3.11'" (tomli under Python 3.12 in CI), which includes spaces/markers and is not a real version. This can yield an invalid/misleading PURL and potentially break SBOM consumers. Consider omitting the version (pass None), or properly parse the requirement (split on ; and extract a concrete specifier) when the dist isn't installed.

Copilot uses AI. Check for mistakes.
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@v4
with:
deny-licenses: GPL-2.0, GPL-3.0, AGPL-3.0, SSPL-1.0
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency-review-action config appears to specify both a deny-list and an allow-list of licenses (allow-licenses is present later in this same with: block). The action errors if both are set, so the workflow will fail at runtime; keep only one of these inputs.

Copilot uses AI. Check for mistakes.


def _is_license_file(rel_path: str) -> bool:
fname = rel_path.rsplit("/", 1)[-1]
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_is_license_file() splits paths using only '/' (rel_path.rsplit("/", 1)), but importlib.metadata file paths can contain backslashes on Windows. That will cause license discovery to fail on Windows even when the license file exists. Normalize separators (e.g., replace \\ with / before splitting) or use PurePosixPath/PurePath for robust filename extraction.

Suggested change
fname = rel_path.rsplit("/", 1)[-1]
"""Return True when ``rel_path`` points to a plausible license file.
``importlib.metadata`` file paths may contain either forward slashes or
backslashes depending on platform and representation. Normalize to ``/``
before extracting the filename so license discovery works consistently on
Windows and POSIX.
"""
normalized_path = rel_path.replace("\\", "/")
fname = normalized_path.rsplit("/", 1)[-1]

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +43
Security > Software bill of materials, or via the REST API at
GET /repos/{owner}/{repo}/dependency-graph/sbom). Adding a second SBOM
generator here would duplicate that surface without adding value.

Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says --check regenerates to a tempfile, but the implementation compares an in-memory rendered string against NOTICE.md on disk. Please update the docstring to match reality (or actually use a tempfile) so future maintainers don't chase a non-existent behavior.

This issue also appears on line 61 of the same file.

Copilot uses AI. Check for mistakes.
danielmeppiel pushed a commit that referenced this pull request Apr 29, 2026
Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps
pyproject.toml + uv.lock to 0.11.0.

Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because
this release ships one BREAKING removal (`apm marketplace build` -> exits 2,
use `apm pack`) plus several net-new features (Dev Container Feature,
Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack`
unification, multi-org `apps[]`). Strict semver in 0.x: minor for
features-with-break, patch only for bugfixes.

Milestone admin (done out-of-band):
- Renamed milestone #8 `0.10.1` -> `0.11.0`
- Created milestone #9 `0.12.0` as next-up bucket
- Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0`
- 6 closed items stay in `0.11.0`

PRs shipping in 0.11.0 (22 commits since v0.10.0):

User-facing features:
- #1042/#722 `apm pack` unifies bundle + marketplace.json
                   (BREAKING: `apm marketplace build` removed)
- #1038       `marketplace:` block in apm.yml + `apm marketplace migrate`
- #803  /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives
- #861        Dev Container Feature `ghcr.io/microsoft/apm/apm-cli`
- #982/#984   shared/apm.md `apps:` array for cross-org private packages
- #820        `target:` in apm.yml validates at parse time
- #1032       `apm marketplace add` honors manifest.name (Claude Code parity)
- #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms

User-facing fixes:
- #1015 ADO Entra ID auth + `apm install --update` pre-flight abort
- #1019/#1020 GEMINI.md only created when target requested
- #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms
- #1018 POSIX paths in auto-discovery output (Windows compat)
- #996  drop stray 'specify' from generated file footer

Maintainer tooling:
- #1043 NOTICE.md per CELA template
- #1045/#1044 NOTICE drift gate + license-policy gate in CI
- #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut)
- #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1
- #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file
- #1022 review-panel: true fan-out + binary verdict + label automation
- #918  complexity audit + benchmarks suite
- #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder)

Files changed:
- pyproject.toml: 0.10.0 -> 0.11.0
- uv.lock:        regenerated (version field only)
- CHANGELOG.md:   [Unreleased] promoted to [0.11.0] - 2026-04-29

NOTICE drift check passes against the bumped lockfile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel added this to the 0.11.0 milestone Apr 29, 2026
danielmeppiel pushed a commit that referenced this pull request Apr 29, 2026
Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps
pyproject.toml + uv.lock to 0.11.0.

Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because
this release ships one BREAKING removal (`apm marketplace build` -> exits 2,
use `apm pack`) plus several net-new features (Dev Container Feature,
Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack`
unification, multi-org `apps[]`). Strict semver in 0.x: minor for
features-with-break, patch only for bugfixes.

Milestone admin (done out-of-band):
- Renamed milestone #8 `0.10.1` -> `0.11.0`
- Created milestone #9 `0.12.0` as next-up bucket
- Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0`
- 6 closed items stay in `0.11.0`

PRs shipping in 0.11.0 (22 commits since v0.10.0):

User-facing features:
- #1042/#722 `apm pack` unifies bundle + marketplace.json
                   (BREAKING: `apm marketplace build` removed)
- #1038       `marketplace:` block in apm.yml + `apm marketplace migrate`
- #803  /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives
- #861        Dev Container Feature `ghcr.io/microsoft/apm/apm-cli`
- #982/#984   shared/apm.md `apps:` array for cross-org private packages
- #820        `target:` in apm.yml validates at parse time
- #1032       `apm marketplace add` honors manifest.name (Claude Code parity)
- #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms

User-facing fixes:
- #1015 ADO Entra ID auth + `apm install --update` pre-flight abort
- #1019/#1020 GEMINI.md only created when target requested
- #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms
- #1018 POSIX paths in auto-discovery output (Windows compat)
- #996  drop stray 'specify' from generated file footer

Maintainer tooling:
- #1043 NOTICE.md per CELA template
- #1045/#1044 NOTICE drift gate + license-policy gate in CI
- #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut)
- #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1
- #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file
- #1022 review-panel: true fan-out + binary verdict + label automation
- #918  complexity audit + benchmarks suite
- #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder)

Files changed:
- pyproject.toml: 0.10.0 -> 0.11.0
- uv.lock:        regenerated (version field only)
- CHANGELOG.md:   [Unreleased] promoted to [0.11.0] - 2026-04-29

NOTICE drift check passes against the bumped lockfile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel added a commit that referenced this pull request Apr 29, 2026
* chore(release): cut 0.11.0

Promotes [Unreleased] -> [0.11.0] - 2026-04-29 and bumps
pyproject.toml + uv.lock to 0.11.0.

Version-bump rationale: 0.11.0 (minor bump) chosen over 0.10.1 because
this release ships one BREAKING removal (`apm marketplace build` -> exits 2,
use `apm pack`) plus several net-new features (Dev Container Feature,
Codex project-scoped MCP, `marketplace:` block in apm.yml, `apm pack`
unification, multi-org `apps[]`). Strict semver in 0.x: minor for
features-with-break, patch only for bugfixes.

Milestone admin (done out-of-band):
- Renamed milestone #8 `0.10.1` -> `0.11.0`
- Created milestone #9 `0.12.0` as next-up bucket
- Moved 43 open items (42 issues + 1 open PR #999) from `0.11.0` -> `0.12.0`
- 6 closed items stay in `0.11.0`

PRs shipping in 0.11.0 (22 commits since v0.10.0):

User-facing features:
- #1042/#722 `apm pack` unifies bundle + marketplace.json
                   (BREAKING: `apm marketplace build` removed)
- #1038       `marketplace:` block in apm.yml + `apm marketplace migrate`
- #803  /#502 Codex project-scoped MCP (`.codex/config.toml`) + user-scope primitives
- #861        Dev Container Feature `ghcr.io/microsoft/apm/apm-cli`
- #982/#984   shared/apm.md `apps:` array for cross-org private packages
- #820        `target:` in apm.yml validates at parse time
- #1032       `apm marketplace add` honors manifest.name (Claude Code parity)
- #1000/#998/#994 unified `--policy` / `--policy-source` accepted forms

User-facing fixes:
- #1015 ADO Entra ID auth + `apm install --update` pre-flight abort
- #1019/#1020 GEMINI.md only created when target requested
- #1008 marketplace producer respects GITHUB_HOST + multi-host URL forms
- #1018 POSIX paths in auto-discovery output (Windows compat)
- #996  drop stray 'specify' from generated file footer

Maintainer tooling:
- #1043 NOTICE.md per CELA template
- #1045/#1044 NOTICE drift gate + license-policy gate in CI
- #1033 shared/apm.md `[a b]` import-input repair (gh-aw#29076 paper-cut)
- #1030 panel workflows skip-don't-fail on unmatched labels; gh-aw v0.71.1
- #1026 shared/apm.md recompiled to apm-action v1.5.0 + bundles-file
- #1022 review-panel: true fan-out + binary verdict + label automation
- #918  complexity audit + benchmarks suite
- #1002 CodeQL clear-text-storage false-positive resolved (token -> placeholder)

Files changed:
- pyproject.toml: 0.10.0 -> 0.11.0
- uv.lock:        regenerated (version field only)
- CHANGELOG.md:   [Unreleased] promoted to [0.11.0] - 2026-04-29

NOTICE drift check passes against the bumped lockfile.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(changelog): tighten 0.11.0 entries to lead with user impact

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(changelog): move Dev Container Feature to Maintainer tooling (not yet published)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(changelog): de-dupe within 0.11.0 (combine #722 Removed bullets, drop #820 Fixed pointer)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Automate NOTICE.md maintenance with SBOM-driven generator + CI drift gate

3 participants