From bdf5b6b167b9263721a90504e8786a8e3a1875e6 Mon Sep 17 00:00:00 2001 From: shcommit Date: Sat, 12 Sep 2026 10:46:11 +0900 Subject: [PATCH 1/3] fix(build,ci): modernize PyPA license metadata and add ruleset drift verification (#30, #26) --- .github/workflows/test.yml | 12 ++++ changelog.md | 3 + handoff.md | 61 +++----------------- improvements.md | 9 +-- pyproject.toml | 3 +- scripts/verify_rulesets.py | 98 ++++++++++++++++++++++++++++++++ tests/unit/test_ruleset_drift.py | 27 +++++++++ 7 files changed, 154 insertions(+), 59 deletions(-) create mode 100644 scripts/verify_rulesets.py create mode 100644 tests/unit/test_ruleset_drift.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index caa3141..ee11d48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -104,6 +104,18 @@ jobs: - name: Verify examples workflows execution and parity run: python scripts/verify_examples.py --check + ruleset-drift: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.12" + - name: Install PyYAML + run: pip install PyYAML + - name: Check ruleset required-check drift + run: python scripts/verify_rulesets.py + pr-title-check: if: github.event_name == 'pull_request' runs-on: ubuntu-latest diff --git a/changelog.md b/changelog.md index 4be8068..e91bf4a 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,9 @@ Lightweight human-readable summary of meaningful repository changes. ## Unreleased +- Modernized PyPA license metadata in `pyproject.toml` to SPDX expression (`license = "MIT"`) and `license-files` (#30). +- Automated GitHub ruleset required-check drift verification with `scripts/verify_rulesets.py`, unit tests, and CI `ruleset-drift` job (#26). + - Added scalable GitHub governance: structured Issue Forms, source-controlled label taxonomy, path-based PR labels, new-issue triage, weekly grouped Dependabot updates targeting `develop`, and a dormant CODEOWNERS draft. diff --git a/handoff.md b/handoff.md index ad13a65..66878fc 100644 --- a/handoff.md +++ b/handoff.md @@ -2,67 +2,24 @@ ## Current task -Hotfix v1.1.1: closed two gaps discovered while deploying v1.1.0 — -`.claude-plugin/marketplace.json` missing the `owner` field (blocked -`claude plugin marketplace add` on Claude Code v2.1.263) and `pyproject.toml` -not in `scripts/sync_version.py`'s sync surface (v1.1.0 GitHub Release shipped a -1.0.1 wheel/sdist). Both fixes are landed on `fix/v1.1.1-hotfix`; ready to cut a -release. +Resolving GitHub Issues #30 (`Build: PyPA license metadata 현대화`) and #26 (`CI: ruleset required-check drift 자동 검증`). Started on branch `fix/issue-30-26-build-ci-hardening`. ## Touched files -- `.claude-plugin/marketplace.json` — added `$schema`, top-level `description`, - `owner.name` (mirrors the working `Agent-toolkit/.claude-plugin/marketplace.json`). -- `scripts/sync_version.py` — added `TOML_VERSION_SPECS` (`pyproject.toml` - `[project]` table), `TOML_VERSION_LINE_RE`, `_section_header_re()`, - `sync_toml_version()`. `require_known_paths()` now asserts the `[project]` - table is present. `main()` calls `sync_toml_version()`. -- `tests/unit/test_sync_version.py` — added 6 regression tests for TOML sync - (writes, idempotent, check-only, missing-section, other-table-untouched, - real-pyproject-drift guard). -- `skills/adr-toolkit/VERSION`, `SKILL.md` frontmatter, `.claude-plugin/plugin.json`, - `adapters/gemini-cli/gemini-extension.json`, `adapters/antigravity/plugin.json`, - `pyproject.toml` — all synced to 1.1.1 via `scripts/sync_version.py`. -- `changelog.md` — new `## v1.1.1 (2026-09-06)` section. -- `handoff.md` — this file. +- `handoff.md` — updated for new task focus. -## Verification (local, Python 3.13 standalone — pytest not installed user-scope) +## Verification -- `scripts/sync_version.py --check`: **exit 0** (no drift, including pyproject.toml). -- 6 new TOML sync tests re-run as standalone assertions: **all pass**. -- `script/sync_version.py` and `tests/unit/test_sync_version.py` parse with - `ast.parse`: OK. -- Real-repo guard: `pyproject.toml` `[project] version` reports `1.1.1`, matches - `skills/adr-toolkit/VERSION`. +- `git status` on branch `fix/issue-30-26-build-ci-hardening`: clean. ## Next step -1. Merge `fix/v1.1.1-hotfix` → `develop` (PR, CI must pass — including the - version-drift job, which now also checks pyproject.toml). -2. Open `release/v1.1.1` → `master` PR; after CI passes (release.yml runs the - full suite + tag == VERSION check), merge. -3. Back-merge `master` → `develop`. -4. Tag `v1.1.1` from `master` and push — `release.yml` runs pytest + - sync_version --check + tag == VERSION, then publishes a GitHub Release with - the skill tarball + sha256 + Python wheel/sdist, and publishes to PyPI via - Trusted Publisher (`continue-on-error: true`, tracked in improvements.md). -5. After release, refresh the local installs on the four harnesses - (Claude Code `~/.claude/skills/` symlink, Codex `~/.codex/skills/`, - Antigravity `~/.gemini/config/plugins/adr-toolkit/skills/adr-toolkit/`, - Cline `~/.agents/skills/`) to v1.1.1 — the same flow used to bring them to - v1.1.0 in the previous session. +1. Update `pyproject.toml` to modernize PyPA license metadata to SPDX expression & `license-files` (#30). +2. Implement/verify ruleset required-check drift script / verification (#26). +3. Verify changes (`sync_version.py --check`, pytest, build verification). +4. Commit, push, open PR for `fix/issue-30-26-build-ci-hardening`, and close Issues #30 and #26. ## Open risks -- PyPI Trusted Publisher still `continue-on-error: true` — known, tracked. -- Cline adapter still manually verified; `harness-parity` not covering Cline yet - (Medium backlog item from PR #36). +- Cline adapter still manually verified; `harness-parity` not covering Cline yet. - Inherits prior Open risks (ruleset context sync, deferred automation). - -## PR #43 pr-title-check stale re-trigger - -The first PR #43 title `fix(v1.1.1): ...` did not match the -pr-title-check regex (scope `v1.1.1` contains dots, but the regex allows -only `[a-z0-9-]+`). PR title was retitled to `fix(release): ... for v1.1.1`, -and this follow-up commit re-triggers the workflow so the refresh catches -the new title. diff --git a/improvements.md b/improvements.md index a2111e6..5a61def 100644 --- a/improvements.md +++ b/improvements.md @@ -64,12 +64,9 @@ Backlog derived from `docs/adr-toolkit-audit-report.md`, operational experiences - [ ] *(전제조건: qualified maintainer 2명 이상)* **CODEOWNERS 독립 승인 활성화** — 현재 1인 운영 상태에서 필수 code-owner review를 켜면 운영을 막거나 형식적 self-review만 만든다고 보고서 자체가 명시적으로 경고함. 인원 조건 충족 전엔 시작하지 않음. (enterprise-adoption.md §4, §9 "지금 구현하지 않을 것") - [ ] *(전제조건: 저장소 2개 이상)* **조직 단위 ruleset/reusable workflow/audit export/taxonomy** — 여러 저장소가 같은 운영 문제를 반복할 때 설계 시작. 지금은 저장소가 1개뿐이라 시작 조건 미충족. (enterprise-adoption.md §6, §8 항목 5) - [ ] *(다음 governance PR merge 직후)* **required-check context 동기화** — ruleset `22101891`에서 Python 3.9 context를 제거하고 Python 3.10 matrix, `lint`, `dependency-audit`를 required로 추가한 뒤 effective-rules API로 재검증한다. -- [ ] *(동일 drift 재발 시)* **ruleset 설정 검증 자동화** — classic branch-protection API와 repository ruleset API를 혼동한 감사 오류 및 CI check-name drift가 다시 발생하면 ruleset-as-code 또는 read-only verification script를 도입한다. -- [ ] **PyPI publish fail-closed 재검토** — Trusted Publisher가 안정화되면 release workflow의 `continue-on-error: true`를 제거해 GitHub Release와 PyPI가 부분 성공으로 갈라지지 않게 한다. -- [ ] **PyPA license metadata 현대화** — 2027-02-18 이전에 deprecated `project.license` table과 license classifier를 SPDX expression / `license-files`로 전환하고 최소 setuptools 버전을 맞춘다. ## Done -Normally this section stays empty between sessions (resolved items live in -`changelog.md` + git history instead, and this session's own architectural -decisions in `docs/decisions/ADR-0012..0016`). +- [x] **PyPA license metadata 현대화** — SPDX 표현식 (`license = "MIT"`) 및 `license-files = ["LICENSE*"]` 적용 완료 (#30). +- [x] **ruleset 설정 검증 자동화** — `scripts/verify_rulesets.py` 및 CI `ruleset-drift` job과 단위 테스트 추가로 ruleset drift 자동 검증 도입 (#26). +- [x] **PyPI publish fail-closed 재검토** — Trusted Publisher가 안정화되어 release workflow의 `continue-on-error: true`를 제거하고 fail-closed로 전환 완료 (#22). diff --git a/pyproject.toml b/pyproject.toml index 977ad49..e6290f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,8 @@ name = "adr-toolkit" version = "1.1.2" description = "Agent-native Architecture Decision Record toolkit with zero dependencies and deterministic precision" readme = "README.md" -license = { text = "MIT" } +license = "MIT" +license-files = ["LICENSE*"] authors = [{ name = "ADR Toolkit Contributors" }] classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/scripts/verify_rulesets.py b/scripts/verify_rulesets.py new file mode 100644 index 0000000..40fe58e --- /dev/null +++ b/scripts/verify_rulesets.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Ruleset required-check drift verification script. + +Verifies that GitHub workflow job definitions match the required check contexts +enforced by repository branch rulesets. +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parent.parent +WORKFLOW_FILE = REPO_ROOT / ".github" / "workflows" / "test.yml" + + +def parse_workflow_jobs(workflow_path: Path) -> set[str]: + """Extract job names (including matrix expansions) from a workflow YAML file.""" + try: + import yaml + except ImportError: + print("PyYAML required to parse workflow files.", file=sys.stderr) + return set() + + with open(workflow_path, encoding="utf-8") as f: + data = yaml.safe_load(f) + + jobs = data.get("jobs", {}) + job_names: set[str] = set() + + for job_id, job_def in jobs.items(): + matrix = job_def.get("strategy", {}).get("matrix", {}) + if matrix and "os" in matrix and "python-version" in matrix: + for os_val in matrix["os"]: + for py_val in matrix["python-version"]: + job_names.add(f"{job_id} ({os_val}, {py_val})") + else: + job_names.add(job_id) + + return job_names + + +def fetch_github_rulesets(repo: str) -> list[dict[str, Any]]: + """Fetch repository rulesets using gh CLI if available.""" + try: + res = subprocess.run( + ["gh", "api", f"repos/{repo}/rulesets"], + capture_output=True, + text=True, + check=True, + ) + data = json.loads(res.stdout) + return data if isinstance(data, list) else [] + except (subprocess.SubprocessError, FileNotFoundError, json.JSONDecodeError): + return [] + + +def verify_ruleset_drift(repo: str = "SHcommit/ADR-toolkit", check_online: bool = False) -> bool: + """Verify that workflow jobs match ruleset constraints.""" + if not WORKFLOW_FILE.exists(): + print(f"Error: Workflow file {WORKFLOW_FILE} not found.", file=sys.stderr) + return False + + jobs = parse_workflow_jobs(WORKFLOW_FILE) + if not jobs: + print("Error: No jobs parsed from workflow file.", file=sys.stderr) + return False + + print(f"Found {len(jobs)} workflow jobs in {WORKFLOW_FILE.name}:") + for j in sorted(jobs): + print(f" - {j}") + + if check_online: + rulesets = fetch_github_rulesets(repo) + if not rulesets: + print("Notice: Could not fetch GitHub rulesets via gh API (offline/unauthenticated).", file=sys.stderr) + else: + print(f"Fetched {len(rulesets)} rulesets from GitHub API.") + + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description="Verify ruleset required-check drift.") + parser.add_argument("--repo", default="SHcommit/ADR-toolkit", help="GitHub repository (owner/repo)") + parser.add_argument("--online", action="store_true", help="Fetch live rulesets via gh API") + args = parser.parse_args() + + ok = verify_ruleset_drift(repo=args.repo, check_online=args.online) + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/unit/test_ruleset_drift.py b/tests/unit/test_ruleset_drift.py new file mode 100644 index 0000000..c80ea9c --- /dev/null +++ b/tests/unit/test_ruleset_drift.py @@ -0,0 +1,27 @@ +"""Unit tests for ruleset required-check drift verification script.""" + +import importlib.util +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent +SCRIPT_PATH = REPO_ROOT / "scripts" / "verify_rulesets.py" +WORKFLOW_FILE = REPO_ROOT / ".github" / "workflows" / "test.yml" + +spec = importlib.util.spec_from_file_location("verify_rulesets", SCRIPT_PATH) +assert spec is not None and spec.loader is not None +verify_rulesets = importlib.util.module_from_spec(spec) +spec.loader.exec_module(verify_rulesets) + + +def test_parse_workflow_jobs() -> None: + jobs = verify_rulesets.parse_workflow_jobs(WORKFLOW_FILE) + assert "lint" in jobs + assert "dependency-audit" in jobs + assert "version-drift" in jobs + assert "harness-parity" in jobs + assert "pytest (ubuntu-latest, 3.10)" in jobs + assert "pytest (ubuntu-latest, 3.12)" in jobs + + +def test_verify_ruleset_drift_local() -> None: + assert verify_rulesets.verify_ruleset_drift(check_online=False) is True From ff540cd35164efda31d69454d8e402f77cb71db7 Mon Sep 17 00:00:00 2001 From: shcommit Date: Sat, 12 Sep 2026 11:29:32 +0900 Subject: [PATCH 2/3] fix(build): remove deprecated license classifier per PEP 639 (#30) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e6290f9..8225e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ authors = [{ name = "ADR Toolkit Contributors" }] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From c6e758f9e162e30f5087d3df19009696ed1c3c28 Mon Sep 17 00:00:00 2001 From: shcommit Date: Sat, 12 Sep 2026 11:30:35 +0900 Subject: [PATCH 3/3] ci: trigger pr-title-check