From c631d5aada53678a2901bc3b113230031dccc74a Mon Sep 17 00:00:00 2001 From: Daniel Meppiel <51440732+danielmeppiel@users.noreply.github.com> Date: Thu, 30 Apr 2026 18:44:33 +0200 Subject: [PATCH] chore(git-blame): add formatting commits to ignore revs --- .git-blame-ignore-revs | 11 ++++ .github/workflows/notice-drift.yml | 12 ++--- CHANGELOG.md | 2 + Makefile | 2 +- NOTICE.md => NOTICE | 12 +++++ scripts/generate-notice.py | 82 +++++++++++++++++++++++------- scripts/notice-metadata.yaml | 16 ++++++ 7 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 .git-blame-ignore-revs rename NOTICE.md => NOTICE (99%) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..9c3092272 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,11 @@ +# This file lists bulk formatting or mechanical refactoring commits that should +# be ignored by git blame. +# +# To enable locally: +# git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Apply code formatting with Black +2340fc778563d8b5c78b3458b9c26c5441c96c4b + +# Apply Black formatting to new files +962fffd3e6974ed1e0cc44170fe796541d213739 diff --git a/.github/workflows/notice-drift.yml b/.github/workflows/notice-drift.yml index 9ce3f854d..f4500e1ea 100644 --- a/.github/workflows/notice-drift.yml +++ b/.github/workflows/notice-drift.yml @@ -1,10 +1,10 @@ -# NOTICE Drift Check -- guards the third-party attribution file (NOTICE.md) +# NOTICE Drift Check -- guards the third-party attribution file (NOTICE) # against silent drift on every PR and every merge-queue entry. Also runs # a license-policy gate (dependency-review-action) on PR-time only. # # Why this gate exists # -------------------- -# `NOTICE.md` is a legally significant artifact: it lists every third-party +# `NOTICE` is a legally significant artifact: it lists every third-party # OSS component shipped inside the apm-cli wheel, with verbatim license # texts. Microsoft CELA's "Manual NOTICE Generation" process makes this # file *normative* -- if it drifts from the actual install graph (someone @@ -81,7 +81,7 @@ jobs: steps: - uses: actions/checkout@v4 - # Pinned to the same Python version as ci.yml. NOTICE.md content + # Pinned to the same Python version as ci.yml. NOTICE content # depends on which dist-info layout the installed wheels use, and # different interpreters can resolve different conditional deps # (e.g. tomli is only installed under python_version<'3.11'). @@ -101,14 +101,14 @@ jobs: # --extra dev brings in ruamel.yaml that the generator imports. # --frozen guarantees we resolve to the exact versions that the # maintainer locked, so the LICENSE text read from dist-info - # matches what NOTICE.md was generated against locally. + # matches what NOTICE was generated against locally. run: uv sync --frozen --extra dev - # Drift check: regenerate NOTICE.md to memory and diff against the + # Drift check: regenerate NOTICE to memory and diff against the # committed copy. Exit 1 with a unified diff to stderr if they # differ -- the diff lands in the GitHub Actions log so the PR # author can see exactly what to regenerate. - - name: Verify NOTICE.md is up to date + - name: Verify NOTICE is up to date run: uv run python scripts/generate-notice.py --check # Supply-chain hygiene: surface any newly-introduced dep whose diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff49d92e..d0eca2924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Renamed `NOTICE.md` -> `NOTICE`** to follow the Apache / CNCF convention used by upstream third-party-attribution files (e.g. `kubernetes-sigs/kro`, `kubernetes-sigs/headlamp`). The generator (`scripts/generate-notice.py`), `make notice` target, and `NOTICE Drift Check` workflow now operate on the extension-less path. (#1073) +- **NOTICE: added "Submitted on behalf of a third-party" section** crediting five contributors whose pull requests landed before the `microsoft-github-policy-service` CLA bot recorded a signature on file -- in keeping with the section-7 wording adopted by CNCF NOTICE files. Driven by a new `_third_party_submissions` block in `scripts/notice-metadata.yaml`. (#1073) - **BREAKING: `apm pack` now produces a Claude Code plugin directory by default — zero extra flags, schema-validated `plugin.json`, convention dirs auto-discovered.** The legacy APM bundle layout is preserved under `--format apm`. Migration: CI workflows and scripts that consume the legacy bundle must add `--format apm` (the [`microsoft/apm-action`](https://github.com/microsoft/apm-action) wrapper has been updated accordingly). (#1061) - **Plugin manifest schema conformance.** The synthesized/written `plugin.json` no longer emits `agents`/`skills`/`commands`/`instructions` keys pointing at the convention directories — these are auto-discovered by Claude Code, and per the [official schema](https://json.schemastore.org/claude-code-plugin.json) those array entries must be `./*.md` paths to *additional* files. The convention dirs themselves are still copied to disk. When stripping such keys from an authored `plugin.json`, `apm pack` now emits a warning so authors can clean up their source. (#1061) diff --git a/Makefile b/Makefile index bdacbe96d..e58e7a81c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ .PHONY: notice notice-check -# Regenerate NOTICE.md from pyproject.toml + scripts/notice-metadata.yaml. +# Regenerate NOTICE from pyproject.toml + scripts/notice-metadata.yaml. # Run this whenever you add / remove / bump a runtime dependency. notice: uv run python scripts/generate-notice.py diff --git a/NOTICE.md b/NOTICE similarity index 99% rename from NOTICE.md rename to NOTICE index 7fc71876e..8f9dd545c 100644 --- a/NOTICE.md +++ b/NOTICE @@ -1215,3 +1215,15 @@ _Copyright (c) 2014-2026 Anthon van der Neut, Ruamel bvba_ --- +Submitted on behalf of a third-party + +The contributions below are identified as submitted on behalf of a +third party. If you are listed here and have since signed the Microsoft +CLA (or wish to), please open an issue so we can update this file. + +Submitted on behalf of a third-party: @pofallon (PRs: #4) +Submitted on behalf of a third-party: @richgo (PRs: #8, #25, #26, #33, #34) +Submitted on behalf of a third-party: @ryanfk (PRs: #92) +Submitted on behalf of a third-party: @foutoucour (PRs: #108) +Submitted on behalf of a third-party: @Jah-yee (PRs: #184) + diff --git a/scripts/generate-notice.py b/scripts/generate-notice.py index 5afaee93a..4a69ea066 100644 --- a/scripts/generate-notice.py +++ b/scripts/generate-notice.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -"""Generate / verify the project NOTICE.md. +"""Generate / verify the project NOTICE. WHY this script exists ---------------------- Microsoft CELA's third-party-notices process requires a per-component -attribution document (`NOTICE.md`) covering every open-source dependency +attribution document (`NOTICE`) covering every open-source dependency distributed with the project (the runtime deps of `apm-cli`). Maintaining that file by hand drifts the moment anyone bumps a version: the version string, the copyright snippet, and even the *license text* can change @@ -43,8 +43,8 @@ Modes ----- - default : regenerate NOTICE.md (overwrite on disk). - --check : regenerate to memory, compare to NOTICE.md on disk, exit 1 + default : regenerate NOTICE (overwrite on disk). + --check : regenerate to memory, compare to NOTICE on disk, exit 1 with a unified diff to stderr if they differ. Used by .github/workflows/notice-drift.yml. @@ -79,9 +79,9 @@ REPO_ROOT = Path(__file__).resolve().parent.parent PYPROJECT = REPO_ROOT / "pyproject.toml" METADATA_YAML = REPO_ROOT / "scripts" / "notice-metadata.yaml" -NOTICE_OUT = REPO_ROOT / "NOTICE.md" +NOTICE_OUT = REPO_ROOT / "NOTICE" -# Header that prefixes every NOTICE.md. The explanatory paragraph is in +# Header that prefixes every NOTICE file. The explanatory paragraph is in # the YAML (`_header.preamble`) so legal-review of wording lives next # to the per-component data, not in code. _FIXED_TOP = ( @@ -104,7 +104,7 @@ class DepSpec: `raw_specifier` preserves everything after the package name -- including PEP 508 environment markers (e.g. `; python_version<'3.11'`). We keep - it verbatim because the NOTICE.md "Version requirement" line is meant + it verbatim because the NOTICE "Version requirement" line is meant to communicate *intent* (what the project asks for), not the resolved version (which lives in `uv.lock` / GitHub's dependency graph). """ @@ -117,7 +117,7 @@ class DepSpec: class ComponentMeta: """Curated per-component data loaded from notice-metadata.yaml.""" - name: str # canonical/display name as it appears in NOTICE.md + name: str # canonical/display name as it appears in NOTICE pyproject_name: str # exact name used in pyproject.toml deps upstream: str spdx: str @@ -153,8 +153,16 @@ def parse_dependencies(pyproject_path: Path) -> list[DepSpec]: return out -def load_metadata(yaml_path: Path) -> tuple[str, dict[str, ComponentMeta]]: - """Return (preamble_text, name->ComponentMeta map keyed by pyproject_name).""" +@dataclass(frozen=True) +class ThirdPartySubmissions: + preamble: str + contributors: list[dict] # [{github: str, prs: [int, ...]}, ...] + + +def load_metadata( + yaml_path: Path, +) -> tuple[str, dict[str, ComponentMeta], ThirdPartySubmissions | None]: + """Return (preamble, name->ComponentMeta map, third-party submissions block).""" yaml = YAML(typ="rt") data = yaml.load(yaml_path.read_text(encoding="utf-8")) preamble = str(data["_header"]["preamble"]).rstrip("\n") @@ -186,7 +194,20 @@ def load_metadata(yaml_path: Path) -> tuple[str, dict[str, ComponentMeta]]: else None ), ) - return preamble, out + raw_tps = data.get("_third_party_submissions") + tps: ThirdPartySubmissions | None = None + if raw_tps is not None: + tps = ThirdPartySubmissions( + preamble=str(raw_tps["preamble"]).rstrip("\n"), + contributors=[ + { + "github": str(c["github"]), + "prs": [int(n) for n in c.get("prs", [])], + } + for c in raw_tps.get("contributors", []) + ], + ) + return preamble, out, tps # --------------------------------------------------------------------------- @@ -289,8 +310,31 @@ def _normalize(name: str) -> str: return re.sub(r"[-_.]+", "-", name).lower() -def render_notice(deps: list[DepSpec], preamble: str, - metas: dict[str, ComponentMeta]) -> str: +def render_third_party_submissions(tps: ThirdPartySubmissions) -> str: + # Each component already ends with `---\n\n` (see render_component + + # the trailing blank line in render_notice), so we do NOT emit a + # leading separator here -- doing so would produce two consecutive + # `---` lines in the file. + lines = [ + "Submitted on behalf of a third-party\n", + "\n", + tps.preamble, + "\n\n", + ] + for entry in tps.contributors: + prs = ", ".join(f"#{n}" for n in entry["prs"]) + suffix = f" (PRs: {prs})" if prs else "" + lines.append(f"Submitted on behalf of a third-party: @{entry['github']}{suffix}\n") + lines.append("\n") + return "".join(lines) + + +def render_notice( + deps: list[DepSpec], + preamble: str, + metas: dict[str, ComponentMeta], + tps: ThirdPartySubmissions | None = None, +) -> str: norm_metas = {_normalize(k): v for k, v in metas.items()} out: list[str] = [_FIXED_TOP, preamble, "\n\n---\n\n"] for dep in deps: @@ -317,6 +361,8 @@ def render_notice(deps: list[DepSpec], preamble: str, # Trailing blank line between components (and after the last one) # to match the CELA template -- the file ends `---\n\n`. out.append("\n") + if tps is not None and tps.contributors: + out.append(render_third_party_submissions(tps)) return "".join(out) @@ -326,19 +372,19 @@ def render_notice(deps: list[DepSpec], preamble: str, def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser( - description="Regenerate or validate NOTICE.md for apm-cli.", + description="Regenerate or validate NOTICE for apm-cli.", ) p.add_argument( "--check", action="store_true", - help="Do not write; exit 1 with unified diff if NOTICE.md is stale.", + help="Do not write; exit 1 with unified diff if NOTICE is stale.", ) args = p.parse_args(argv) try: deps = parse_dependencies(PYPROJECT) - preamble, metas = load_metadata(METADATA_YAML) - rendered = render_notice(deps, preamble, metas) + preamble, metas, tps = load_metadata(METADATA_YAML) + rendered = render_notice(deps, preamble, metas, tps) except SystemExit: raise except Exception as e: # pragma: no cover -- defensive @@ -356,7 +402,7 @@ def main(argv: list[str] | None = None) -> int: tofile=str(NOTICE_OUT.relative_to(REPO_ROOT)) + " (regenerated)", ) sys.stderr.write( - "NOTICE.md is out of date with pyproject.toml + notice-metadata.yaml.\n" + "NOTICE is out of date with pyproject.toml + notice-metadata.yaml.\n" "Run `make notice` (or `python scripts/generate-notice.py`) and commit.\n\n" ) sys.stderr.writelines(diff) diff --git a/scripts/notice-metadata.yaml b/scripts/notice-metadata.yaml index 9b16ee4a8..edab77841 100644 --- a/scripts/notice-metadata.yaml +++ b/scripts/notice-metadata.yaml @@ -10,6 +10,22 @@ _header: repository. The `apm` source code itself is licensed under the MIT License; see `LICENSE`. +_third_party_submissions: + preamble: | + The contributions below are identified as submitted on behalf of a + third party. If you are listed here and have since signed the Microsoft + CLA (or wish to), please open an issue so we can update this file. + contributors: + - github: pofallon + prs: [4] + - github: richgo + prs: [8, 25, 26, 33, 34] + - github: ryanfk + prs: [92] + - github: foutoucour + prs: [108] + - github: Jah-yee + prs: [184] components: - name: click pyproject_name: click