Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/reusable-org-adr-auto-sync.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ jobs:
source_repo = os.environ["SOURCE_REPO"]
source_sha = os.environ["SOURCE_SHA"]

manifest = json.loads((source_dir / "baseline-manifest.json").read_text(encoding="utf-8"))
manifest_path = source_dir / "org-adr-manifest.json"
if not manifest_path.is_file():
manifest_path = source_dir / "baseline-manifest.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
print(f"using source manifest {manifest_path.name}")
entries = []
for item in manifest.get("files", []):
source = item.get("source")
Expand Down Expand Up @@ -142,11 +146,15 @@ jobs:
lines = text.splitlines(keepends=True)
changed = False
source_pattern = re.compile(rf"^\s*source-repo:\s*{re.escape(source_repo)}\s*(?:#.*)?$")
manifest_pattern = re.compile(r"^\s*manifest:\s*org-adr-manifest\.json\s*(?:#.*)?$")
ref_pattern = re.compile(r"^(\s*source-ref:\s*)([^\s#]+)(?:\s*#.*)?(\r?\n?)$")
for index, line in enumerate(lines):
if not source_pattern.match(line):
continue
for ref_index in range(index + 1, min(index + 8, len(lines))):
window = lines[index + 1 : min(index + 12, len(lines))]
if not any(manifest_pattern.match(candidate) for candidate in window):
continue
for ref_index in range(index + 1, min(index + 12, len(lines))):
match = ref_pattern.match(lines[ref_index])
if match:
lines[ref_index] = f"{match.group(1)}{source_sha}{match.group(3) or os.linesep}"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@

# Allow baseline manifest so drift-gate has an authoritative org source list.
!/baseline-manifest.json
!/org-adr-manifest.json

# Allow README so the special repo's operating model stays documented.
!/README.md
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/mirroring.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Org governance is namespace-local. A `NWarila/*` repository mirrors org ADRs and

## Org ADR Auto-Sync

Repositories that already mirror org ADRs should carry a scheduled caller for the namespace-local `reusable-org-adr-auto-sync.yaml`. The caller runs from the adopting repository, so its sync token can only update that repository. It fetches the owning namespace `.github` baseline manifest, copies only `docs/decision-records/org/` targets, removes stale mirrored ADR Markdown files, updates the adopting repository's `org-adr-sync.yaml` source pin when present, and opens or refreshes a PR.
Repositories that already mirror org ADRs should carry a scheduled caller for the namespace-local `reusable-org-adr-auto-sync.yaml`. The caller runs from the adopting repository, so its sync token can only update that repository. It fetches the owning namespace `.github` `org-adr-manifest.json`, copies only `docs/decision-records/org/` targets, removes stale mirrored ADR Markdown files, updates the adopting repository's ADR-only detector source pin when present, and opens or refreshes a PR.

The reusable keeps `GITHUB_TOKEN` read-only. Real sync writes require the caller to pass an explicit `sync_token` secret with permission to push the sync branch and open the PR.

Expand Down
49 changes: 49 additions & 0 deletions org-adr-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"version": "1",
"files": [
{
"source": "docs/decision-records/0001-use-architecture-decision-records.md",
"target": "docs/decision-records/org/0001-use-architecture-decision-records.md"
},
{
"source": "docs/decision-records/0002-adopt-diataxis-documentation-framework.md",
"target": "docs/decision-records/org/0002-adopt-diataxis-documentation-framework.md"
},
{
"source": "docs/decision-records/0003-use-deny-all-gitignore-strategy.md",
"target": "docs/decision-records/org/0003-use-deny-all-gitignore-strategy.md"
},
{
"source": "docs/decision-records/0004-use-renovate-for-dependency-updates.md",
"target": "docs/decision-records/org/0004-use-renovate-for-dependency-updates.md"
},
{
"source": "docs/decision-records/0005-pin-terraform-and-provider-versions-exactly.md",
"target": "docs/decision-records/org/0005-pin-terraform-and-provider-versions-exactly.md"
},
{
"source": "docs/decision-records/0006-keep-github-control-planes-namespace-local.md",
"target": "docs/decision-records/org/0006-keep-github-control-planes-namespace-local.md"
},
{
"source": "docs/decision-records/0007-centralize-universal-ci-reusables-within-each-namespace.md",
"target": "docs/decision-records/org/0007-centralize-universal-ci-reusables-within-each-namespace.md"
},
{
"source": "docs/decision-records/0008-enforce-repo-hygiene-by-repo-type.md",
"target": "docs/decision-records/org/0008-enforce-repo-hygiene-by-repo-type.md"
},
{
"source": "docs/decision-records/0009-classify-baseline-manifest-byte-identity.md",
"target": "docs/decision-records/org/0009-classify-baseline-manifest-byte-identity.md"
},
{
"source": "docs/decision-records/0010-keep-ai-attribution-out-of-version-control.md",
"target": "docs/decision-records/org/0010-keep-ai-attribution-out-of-version-control.md"
},
{
"source": "docs/decision-records/org/.gitkeep",
"target": "docs/decision-records/org/.gitkeep"
}
]
}
38 changes: 23 additions & 15 deletions tools/check_baseline_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@


ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "baseline-manifest.json"
MANIFESTS = (
ROOT / "baseline-manifest.json",
ROOT / "org-adr-manifest.json",
)
ADR_ROOT = ROOT / "docs" / "decision-records"


Expand All @@ -25,50 +28,55 @@ def manifest_path(field: str, value: Any) -> str:
return value


def main() -> None:
def validate_manifest(manifest: Path) -> None:
try:
raw = json.loads(MANIFEST.read_text(encoding="utf-8"))
raw = json.loads(manifest.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
fail(f"{MANIFEST.name} is not valid JSON: {exc}")
fail(f"{manifest.name} is not valid JSON: {exc}")

if not isinstance(raw, dict) or set(raw) != {"version", "files"}:
fail("root must contain exactly 'version' and 'files'")
fail(f"{manifest.name}: root must contain exactly 'version' and 'files'")
if raw["version"] != "1":
fail(f"unsupported manifest version: {raw['version']!r}")
fail(f"{manifest.name}: unsupported manifest version: {raw['version']!r}")
files = raw["files"]
if not isinstance(files, list) or not files:
fail("'files' must be a non-empty list")
fail(f"{manifest.name}: 'files' must be a non-empty list")

sources: list[str] = []
targets: set[str] = set()
for idx, item in enumerate(files):
if not isinstance(item, dict) or set(item) != {"source", "target"}:
fail(f"files[{idx}] must contain exactly 'source' and 'target'")
source = manifest_path(f"files[{idx}].source", item["source"])
target = manifest_path(f"files[{idx}].target", item["target"])
fail(f"{manifest.name}: files[{idx}] must contain exactly 'source' and 'target'")
source = manifest_path(f"{manifest.name}: files[{idx}].source", item["source"])
target = manifest_path(f"{manifest.name}: files[{idx}].target", item["target"])
if target in targets:
fail(f"duplicate target path: {target!r}")
fail(f"{manifest.name}: duplicate target path: {target!r}")
sources.append(source)
targets.add(target)

missing = [source for source in sources if not (ROOT / source).is_file()]
if missing:
fail(f"sources missing: {missing}")
fail(f"{manifest.name}: sources missing: {missing}")

org_adrs = sorted(path.relative_to(ROOT).as_posix() for path in ADR_ROOT.glob("000*.md"))
unlisted_adrs = [path for path in org_adrs if path not in sources]
if unlisted_adrs:
fail(f"org ADRs missing from baseline manifest: {unlisted_adrs}")
fail(f"{manifest.name}: org ADRs missing from manifest: {unlisted_adrs}")

expected_targets = {
f"docs/decision-records/org/{Path(source).name}"
for source in org_adrs
}
missing_targets = sorted(expected_targets - targets)
if missing_targets:
fail(f"org ADR mirror targets missing from baseline manifest: {missing_targets}")
fail(f"{manifest.name}: org ADR mirror targets missing from manifest: {missing_targets}")

print(f"{manifest.name} check passed: {len(files)} files")

print(f"baseline manifest check passed: {len(files)} files")

def main() -> None:
for manifest in MANIFESTS:
validate_manifest(manifest)


if __name__ == "__main__":
Expand Down