From 4b8416497c37e37ee4c69701b0e4a2ec7efce2bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:06:46 +0900 Subject: [PATCH 01/17] fix(metadata): preserve workflow-backed Pages mode --- scripts/ci/reconcile_repository_metadata.py | 102 ++++++++++++++++---- 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 4f2e649253..adaf6f08c6 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -25,6 +25,7 @@ TOPIC_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,49}$") MAX_DESCRIPTION_CHARS = 350 PAGES_BASE_URL = f"https://{ORGANIZATION.casefold()}.github.io" +PAGES_MODES = {"legacy", "workflow"} class ManifestError(ValueError): @@ -54,9 +55,12 @@ def _validate_repository(name: str, raw: Any) -> dict[str, Any]: if not isinstance(name, str) or not REPOSITORY_RE.fullmatch(name): raise ManifestError("repository names must preserve exact GitHub-safe casing") item = _require_exact_dict(raw, field=f"repositories.{name}") - expected = {"description", "topics", "deepwiki", "pages"} - if set(item) != expected: - raise ManifestError(f"repositories.{name} must contain exactly {sorted(expected)}") + required = {"description", "topics", "deepwiki", "pages"} + allowed = required | {"pages_mode"} + if not required.issubset(item) or not set(item).issubset(allowed): + raise ManifestError( + f"repositories.{name} must contain exactly {sorted(required)} plus optional pages_mode" + ) description = item["description"] if ( @@ -90,12 +94,25 @@ def _validate_repository(name: str, raw: Any) -> dict[str, Any]: raise ManifestError( f"repositories.{name} deepwiki/pages flags must be booleans" ) - return { + pages_mode = item.get("pages_mode", "legacy") + if type(pages_mode) is not str or pages_mode not in PAGES_MODES: + raise ManifestError( + f"repositories.{name}.pages_mode must be one of {sorted(PAGES_MODES)}" + ) + if not item["pages"] and "pages_mode" in item: + raise ManifestError( + f"repositories.{name}.pages_mode is only valid when Pages is enabled" + ) + + validated = { "description": description, "topics": list(topics), "deepwiki": item["deepwiki"], "pages": item["pages"], } + if "pages_mode" in item: + validated["pages_mode"] = pages_mode + return validated def load_manifest(path: Path) -> dict[str, dict[str, Any]]: @@ -196,6 +213,12 @@ def _pages_configuration_matches(current: dict[str, Any], default_branch: str) - ) +def _workflow_pages_configuration_matches(current: dict[str, Any]) -> bool: + """Return whether Pages is explicitly owned by a GitHub Actions deployment.""" + + return current.get("build_type") == "workflow" + + def _pages_url_is_expected(url: Any) -> bool: """Return whether a URL is confined to the organization-owned Pages origin.""" @@ -225,12 +248,10 @@ def _pages_publication_ready(repository: str, current: dict[str, Any]) -> None: raise RuntimeError(f"GitHub Pages is not reachable for {repository}") from exc -def _docs_index_exists(repository: str, default_branch: str) -> bool: - """Return whether the reviewed default branch contains docs/index.md.""" +def _repository_file_exists(repository: str, default_branch: str, path: str) -> bool: + """Return whether a reviewed default-branch file exists at the exact path.""" - endpoint = ( - f"repos/{ORGANIZATION}/{repository}/contents/docs/index.md?ref={default_branch}" - ) + endpoint = f"repos/{ORGANIZATION}/{repository}/contents/{path}?ref={default_branch}" command = ["gh", "api", endpoint] completed = subprocess.run( command, @@ -244,7 +265,21 @@ def _docs_index_exists(repository: str, default_branch: str) -> bool: combined = f"{completed.stdout}\n{completed.stderr}" if "HTTP 404" in combined or "Not Found" in combined: return False - raise RuntimeError(f"Pages source state could not be resolved for {repository}") + raise RuntimeError(f"repository file state could not be resolved for {repository}:{path}") + + +def _docs_index_exists(repository: str, default_branch: str) -> bool: + """Return whether the reviewed default branch contains docs/index.md.""" + + return _repository_file_exists(repository, default_branch, "docs/index.md") + + +def _workflow_pages_definition_exists(repository: str, default_branch: str) -> bool: + """Return whether the standard reviewed Pages workflow exists on the default branch.""" + + return _repository_file_exists( + repository, default_branch, ".github/workflows/pages.yml" + ) def _deepwiki_badge_linked(readme: str, repository: str) -> bool: @@ -289,6 +324,24 @@ def _deepwiki_badge_exists(repository: str, default_branch: str) -> bool: return _deepwiki_badge_linked(completed.stdout, repository) +def _pages_precondition(repository: str, default_branch: str, desired: dict[str, Any]) -> None: + """Require the reviewed source contract for the selected Pages deployment mode.""" + + if not desired["pages"]: + return + pages_mode = desired.get("pages_mode", "legacy") + if pages_mode == "workflow": + if not _workflow_pages_definition_exists(repository, default_branch): + raise RuntimeError( + f"workflow Pages requested for {repository} but .github/workflows/pages.yml is not on {default_branch}" + ) + return + if not _docs_index_exists(repository, default_branch): + raise RuntimeError( + f"Pages requested for {repository} but docs/index.md is not on {default_branch}" + ) + + def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: """Apply one validated desired-state record through least-privilege GitHub APIs.""" @@ -308,10 +361,7 @@ def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: raise RuntimeError( f"DeepWiki badge is disabled for {repository} but the exact badge is still on {default_branch}" ) - if desired["pages"] and not _docs_index_exists(repository, default_branch): - raise RuntimeError( - f"Pages requested for {repository} but docs/index.md is not on {default_branch}" - ) + _pages_precondition(repository, default_branch, desired) if repository_payload.get("description") != desired["description"]: _gh_api( @@ -332,6 +382,19 @@ def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: pages_exists = _pages_exists(repository) if desired["pages"]: + pages_mode = desired.get("pages_mode", "legacy") + if pages_mode == "workflow": + if not pages_exists: + raise RuntimeError( + f"workflow Pages requested for {repository} but Pages is not configured" + ) + current_pages = _pages_configuration(repository) + if not _workflow_pages_configuration_matches(current_pages): + raise RuntimeError( + f"workflow Pages requested for {repository} but live Pages is not Actions-backed" + ) + return + pages_body = { "build_type": "legacy", "source": {"branch": default_branch, "path": "/docs"}, @@ -375,15 +438,20 @@ def verify_repository(repository: str, desired: dict[str, Any]) -> None: badge_exists = _deepwiki_badge_exists(repository, default_branch) if badge_exists != desired["deepwiki"]: raise RuntimeError(f"DeepWiki state did not converge for {repository}") - if desired["pages"] and not _docs_index_exists(repository, default_branch): - raise RuntimeError(f"Pages source did not converge for {repository}") + _pages_precondition(repository, default_branch, desired) pages_exists = _pages_exists(repository) if desired["pages"]: if not pages_exists: raise RuntimeError(f"GitHub Pages was not published for {repository}") current_pages = _pages_configuration(repository) - if not _pages_configuration_matches(current_pages, default_branch): + pages_mode = desired.get("pages_mode", "legacy") + if pages_mode == "workflow": + if not _workflow_pages_configuration_matches(current_pages): + raise RuntimeError( + f"GitHub Pages deployment mode did not converge for {repository}" + ) + elif not _pages_configuration_matches(current_pages, default_branch): raise RuntimeError(f"GitHub Pages configuration did not converge for {repository}") _pages_publication_ready(repository, current_pages) elif pages_exists: From 1e2e95a14866e2867430e3877b55d08cdb2e2ff3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:07:11 +0900 Subject: [PATCH 02/17] test(metadata): cover workflow-backed Pages preservation --- ...test_repository_metadata_workflow_pages.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/test_repository_metadata_workflow_pages.py diff --git a/tests/test_repository_metadata_workflow_pages.py b/tests/test_repository_metadata_workflow_pages.py new file mode 100644 index 0000000000..b8f5597e18 --- /dev/null +++ b/tests/test_repository_metadata_workflow_pages.py @@ -0,0 +1,147 @@ +"""Contracts for preserving GitHub Actions-backed Pages deployments.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "ci" / "reconcile_repository_metadata.py" +SPEC = importlib.util.spec_from_file_location("reconcile_repository_metadata_pages", SCRIPT) +assert SPEC and SPEC.loader +RECONCILER = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(RECONCILER) + + +def desired(**overrides): + """Return a minimal valid workflow-Pages desired-state record.""" + + state = { + "description": "Useful product.", + "topics": ["python"], + "deepwiki": False, + "pages": True, + "pages_mode": "workflow", + } + state.update(overrides) + return state + + +def test_manifest_accepts_explicit_workflow_pages_mode() -> None: + """Workflow-backed Pages intent is explicit without changing legacy records.""" + + state = desired() + assert RECONCILER._validate_repository("Repo", state) == state + legacy = {key: value for key, value in state.items() if key != "pages_mode"} + assert RECONCILER._validate_repository("Repo", legacy) == legacy + + with pytest.raises(RECONCILER.ManifestError, match="pages_mode"): + RECONCILER._validate_repository("Repo", desired(pages_mode="other")) + with pytest.raises(RECONCILER.ManifestError, match="only valid"): + RECONCILER._validate_repository("Repo", desired(pages=False)) + + +def test_workflow_pages_reconcile_preserves_live_actions_mode(monkeypatch) -> None: + """A reviewed Actions-backed Pages site is verified rather than rewritten to legacy.""" + + calls = [] + + def gh_api(method, endpoint, **kwargs): + calls.append((method, endpoint, kwargs)) + if endpoint.endswith("/topics"): + return json.dumps({"names": ["python"]}) + if endpoint.endswith("/pages"): + return json.dumps({"build_type": "workflow"}) + return json.dumps( + {"default_branch": "main", "description": "Useful product."} + ) + + monkeypatch.setattr(RECONCILER, "_gh_api", gh_api) + monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) + monkeypatch.setattr( + RECONCILER, "_workflow_pages_definition_exists", lambda *args: True + ) + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: True) + + RECONCILER.reconcile_repository("Repo", desired()) + + page_writes = [ + call + for call in calls + if call[1].endswith("/pages") and call[0] in {"POST", "PUT", "DELETE"} + ] + assert page_writes == [] + + +def test_workflow_pages_reconcile_fails_closed_on_missing_or_wrong_mode(monkeypatch) -> None: + """Workflow intent never creates or converts Pages through the legacy settings API.""" + + monkeypatch.setattr( + RECONCILER, + "_gh_api", + lambda method, endpoint, **kwargs: ( + json.dumps({"names": ["python"]}) + if endpoint.endswith("/topics") + else json.dumps({"default_branch": "main", "description": "Useful product."}) + ), + ) + monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) + monkeypatch.setattr( + RECONCILER, "_workflow_pages_definition_exists", lambda *args: True + ) + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: False) + with pytest.raises(RuntimeError, match="not configured"): + RECONCILER.reconcile_repository("Repo", desired()) + + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: True) + monkeypatch.setattr( + RECONCILER, + "_pages_configuration", + lambda *args: {"build_type": "legacy", "source": {"branch": "main", "path": "/docs"}}, + ) + with pytest.raises(RuntimeError, match="not Actions-backed"): + RECONCILER.reconcile_repository("Repo", desired()) + + +def test_workflow_pages_verification_requires_live_publication(monkeypatch) -> None: + """Workflow mode still requires exact live configuration and published content evidence.""" + + monkeypatch.setattr( + RECONCILER, + "_gh_api", + lambda method, endpoint, **kwargs: ( + json.dumps({"names": ["python"]}) + if endpoint.endswith("/topics") + else json.dumps({"default_branch": "main", "description": "Useful product."}) + ), + ) + monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) + monkeypatch.setattr( + RECONCILER, "_workflow_pages_definition_exists", lambda *args: True + ) + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: True) + current = { + "build_type": "workflow", + "status": "built", + "html_url": "https://contextualwisdomlab.github.io/repo/", + } + monkeypatch.setattr(RECONCILER, "_pages_configuration", lambda *args: current) + seen = [] + monkeypatch.setattr( + RECONCILER, + "_pages_publication_ready", + lambda repository, pages: seen.append((repository, pages)), + ) + + RECONCILER.verify_repository("Repo", desired()) + assert seen == [("Repo", current)] + + monkeypatch.setattr( + RECONCILER, "_pages_configuration", lambda *args: {"build_type": "legacy"} + ) + with pytest.raises(RuntimeError, match="deployment mode"): + RECONCILER.verify_repository("Repo", desired()) From 4ba7483d9053f83eaeeb0afd2f1b4c740b409035 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:18:37 +0900 Subject: [PATCH 03/17] fix(metadata): preserve verification compatibility --- scripts/ci/reconcile_repository_metadata.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index adaf6f08c6..696a12bc98 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -265,7 +265,9 @@ def _repository_file_exists(repository: str, default_branch: str, path: str) -> combined = f"{completed.stdout}\n{completed.stderr}" if "HTTP 404" in combined or "Not Found" in combined: return False - raise RuntimeError(f"repository file state could not be resolved for {repository}:{path}") + raise RuntimeError( + f"GitHub Pages source state could not be resolved for {repository}:{path}" + ) def _docs_index_exists(repository: str, default_branch: str) -> bool: @@ -438,7 +440,15 @@ def verify_repository(repository: str, desired: dict[str, Any]) -> None: badge_exists = _deepwiki_badge_exists(repository, default_branch) if badge_exists != desired["deepwiki"]: raise RuntimeError(f"DeepWiki state did not converge for {repository}") - _pages_precondition(repository, default_branch, desired) + if desired["pages"]: + pages_mode = desired.get("pages_mode", "legacy") + if pages_mode == "workflow": + if not _workflow_pages_definition_exists(repository, default_branch): + raise RuntimeError( + f"Pages workflow source did not converge for {repository}" + ) + elif not _docs_index_exists(repository, default_branch): + raise RuntimeError(f"Pages source did not converge for {repository}") pages_exists = _pages_exists(repository) if desired["pages"]: From dfbc481ae20444494da3e02f7feabca03a01c6eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 02:19:32 +0900 Subject: [PATCH 04/17] ci(metadata): gate workflow Pages contracts --- .github/workflows/repository-metadata-reconcile.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index 90b3a1b7e8..b2076542ef 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -11,6 +11,7 @@ on: - "tests/test_repository_metadata_convergence.py" - "tests/test_repository_metadata_identity.py" - "tests/test_repository_metadata_live_verification.py" + - "tests/test_repository_metadata_workflow_pages.py" - "tests/test_repository_label_taxonomy.py" - "tests/test_repository_label_reconciliation.py" - "tests/test_repository_label_convergence.py" @@ -72,7 +73,8 @@ jobs: -m pytest -q \ tests/test_repository_metadata_reconciliation.py \ tests/test_repository_metadata_identity.py \ - tests/test_repository_metadata_live_verification.py + tests/test_repository_metadata_live_verification.py \ + tests/test_repository_metadata_workflow_pages.py python -m coverage report \ --fail-under=100 \ --show-missing \ From 064049bb0f142eb2c9ad60e1a803cefef4193a07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:07:35 +0900 Subject: [PATCH 05/17] test(metadata): cover workflow Pages fail-closed branches --- ...test_repository_metadata_workflow_pages.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_repository_metadata_workflow_pages.py b/tests/test_repository_metadata_workflow_pages.py index b8f5597e18..8be1d8059f 100644 --- a/tests/test_repository_metadata_workflow_pages.py +++ b/tests/test_repository_metadata_workflow_pages.py @@ -45,6 +45,32 @@ def test_manifest_accepts_explicit_workflow_pages_mode() -> None: RECONCILER._validate_repository("Repo", desired(pages=False)) +def test_workflow_pages_definition_probe_uses_standard_reviewed_path(monkeypatch) -> None: + """Workflow-mode source discovery probes only the standard reviewed Pages path.""" + + seen = [] + + def repository_file_exists(repository, default_branch, path): + seen.append((repository, default_branch, path)) + return True + + monkeypatch.setattr(RECONCILER, "_repository_file_exists", repository_file_exists) + + assert RECONCILER._workflow_pages_definition_exists("Repo", "main") + assert seen == [("Repo", "main", ".github/workflows/pages.yml")] + + +def test_workflow_pages_precondition_rejects_missing_reviewed_workflow(monkeypatch) -> None: + """Workflow intent fails before mutation when the reviewed Pages workflow is absent.""" + + monkeypatch.setattr( + RECONCILER, "_workflow_pages_definition_exists", lambda *args: False + ) + + with pytest.raises(RuntimeError, match=r"\.github/workflows/pages\.yml"): + RECONCILER._pages_precondition("Repo", "main", desired()) + + def test_workflow_pages_reconcile_preserves_live_actions_mode(monkeypatch) -> None: """A reviewed Actions-backed Pages site is verified rather than rewritten to legacy.""" @@ -107,6 +133,27 @@ def test_workflow_pages_reconcile_fails_closed_on_missing_or_wrong_mode(monkeypa RECONCILER.reconcile_repository("Repo", desired()) +def test_workflow_pages_verification_rejects_missing_reviewed_source(monkeypatch) -> None: + """Live verification fails closed if the declared workflow source disappears.""" + + monkeypatch.setattr( + RECONCILER, + "_gh_api", + lambda method, endpoint, **kwargs: ( + json.dumps({"names": ["python"]}) + if endpoint.endswith("/topics") + else json.dumps({"default_branch": "main", "description": "Useful product."}) + ), + ) + monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) + monkeypatch.setattr( + RECONCILER, "_workflow_pages_definition_exists", lambda *args: False + ) + + with pytest.raises(RuntimeError, match="workflow source did not converge"): + RECONCILER.verify_repository("Repo", desired()) + + def test_workflow_pages_verification_requires_live_publication(monkeypatch) -> None: """Workflow mode still requires exact live configuration and published content evidence.""" From 63ab12eee1fb990645a109e10b217157f1f4f73c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:11:05 +0900 Subject: [PATCH 06/17] test(metadata): require stale PR validation cancellation --- tests/test_repository_metadata_workflow_pages.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_repository_metadata_workflow_pages.py b/tests/test_repository_metadata_workflow_pages.py index 8be1d8059f..374a9ea410 100644 --- a/tests/test_repository_metadata_workflow_pages.py +++ b/tests/test_repository_metadata_workflow_pages.py @@ -11,6 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "ci" / "reconcile_repository_metadata.py" +WORKFLOW = ROOT / ".github" / "workflows" / "repository-metadata-reconcile.yml" SPEC = importlib.util.spec_from_file_location("reconcile_repository_metadata_pages", SCRIPT) assert SPEC and SPEC.loader RECONCILER = importlib.util.module_from_spec(SPEC) @@ -31,6 +32,20 @@ def desired(**overrides): return state +def test_metadata_pr_validation_cancels_superseded_head_runs() -> None: + """A new PR head must retire the older metadata-validation run, not the hourly apply.""" + + workflow = WORKFLOW.read_text(encoding="utf-8") + concurrency = workflow.split("concurrency:", 1)[1].split("jobs:", 1)[0] + + assert ( + "repository-metadata-reconcile-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }}" + in concurrency + ) + assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in concurrency + assert "github.event.pull_request.head.sha" not in concurrency + + def test_manifest_accepts_explicit_workflow_pages_mode() -> None: """Workflow-backed Pages intent is explicit without changing legacy records.""" From 008d14c724950ab53f01a7020a89c28272b8e2f0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:11:24 +0900 Subject: [PATCH 07/17] fix(metadata): cancel superseded PR validation runs --- .../repository-metadata-reconcile.yml | 87 ++++--------------- 1 file changed, 19 insertions(+), 68 deletions(-) diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index b2076542ef..a28ccc3059 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -25,8 +25,8 @@ permissions: contents: read concurrency: - group: repository-metadata-reconcile-${{ github.ref }} - cancel-in-progress: false + group: repository-metadata-reconcile-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: validate: @@ -50,9 +50,7 @@ jobs: with: python-version: "3.12" - name: Install hash-locked test tooling - run: >- - python -m pip install --disable-pip-version-check --require-hashes - --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt + run: python -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt - name: Validate desired state run: | set -euo pipefail @@ -100,84 +98,37 @@ jobs: git diff --check apply: - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + if: github.event_name == 'schedule' needs: validate runs-on: ubuntu-24.04 - timeout-minutes: 45 - environment: repository-metadata-maintenance + timeout-minutes: 30 + permissions: + contents: read + issues: write steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - - name: Check out trusted default branch + - name: Check out trusted protected main uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.sha }} persist-credentials: false - - name: Verify exact revision + - name: Verify protected-main identity shell: bash - run: test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" - - name: Set up Python - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - with: - python-version: "3.12" - - name: Reconcile and verify repository public surfaces + run: test "$(git rev-parse HEAD)" = "${{ github.sha }}" + - name: Apply repository metadata env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + GH_TOKEN: ${{ secrets.REPOSITORY_SETTINGS_TOKEN || github.token }} run: | - set +e + set -euo pipefail python scripts/ci/reconcile_repository_metadata.py \ --manifest config/repository-metadata.json - metadata_apply_status=$? + - name: Apply repository labels + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail python scripts/ci/reconcile_repository_labels.py \ --taxonomy config/repository-label-taxonomy.json - label_apply_status=$? - python scripts/ci/reconcile_repository_labels.py \ - --taxonomy config/repository-label-taxonomy.json \ - --verify-only - label_verify_status=$? - - metadata_verify_status=1 - metadata_verify_attempt=1 - metadata_verify_limit=12 - while (( metadata_verify_attempt <= metadata_verify_limit )); do - metadata_verify_output="$( - python scripts/ci/reconcile_repository_metadata.py \ - --manifest config/repository-metadata.json \ - --verify-only 2>&1 - )" - metadata_verify_status=$? - printf '%s\n' "${metadata_verify_output}" - if (( metadata_verify_status == 0 )); then - break - fi - - metadata_failure_lines="$( - printf '%s\n' "${metadata_verify_output}" \ - | grep '^repository metadata reconciliation failed for ' || true - )" - if [[ -z "${metadata_failure_lines}" ]] \ - || printf '%s\n' "${metadata_failure_lines}" \ - | grep -Evq 'GitHub Pages (was not published|configuration did not converge|is not built|is not reachable)'; then - break - fi - if (( metadata_verify_attempt == metadata_verify_limit )); then - break - fi - sleep 15 - ((metadata_verify_attempt += 1)) - done - - set -e - if (( metadata_apply_status != 0 \ - || label_apply_status != 0 \ - || metadata_verify_status != 0 \ - || label_verify_status != 0 )); then - printf 'metadata_apply=%s label_apply=%s metadata_verify=%s label_verify=%s\n' \ - "${metadata_apply_status}" \ - "${label_apply_status}" \ - "${metadata_verify_status}" \ - "${label_verify_status}" >&2 - exit 1 - fi From 020c2a3e4151d8fe4a129532b960f57d338d1315 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:12:08 +0900 Subject: [PATCH 08/17] fix(metadata): preserve apply path while cancelling stale PR validation --- .../repository-metadata-reconcile.yml | 83 +++++++++++++++---- 1 file changed, 66 insertions(+), 17 deletions(-) diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index a28ccc3059..4815cc4a8f 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -50,7 +50,9 @@ jobs: with: python-version: "3.12" - name: Install hash-locked test tooling - run: python -m pip install --disable-pip-version-check --require-hashes --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt + run: >- + python -m pip install --disable-pip-version-check --require-hashes + --only-binary=:all: -r requirements-opencode-review-ci-hashes.txt - name: Validate desired state run: | set -euo pipefail @@ -98,37 +100,84 @@ jobs: git diff --check apply: - if: github.event_name == 'schedule' + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' needs: validate runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - issues: write + timeout-minutes: 45 + environment: repository-metadata-maintenance steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - - name: Check out trusted protected main + - name: Check out trusted default branch uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.sha }} persist-credentials: false - - name: Verify protected-main identity + - name: Verify exact revision shell: bash - run: test "$(git rev-parse HEAD)" = "${{ github.sha }}" - - name: Apply repository metadata + run: test "$(git rev-parse HEAD)" = "${GITHUB_SHA}" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + - name: Reconcile and verify repository public surfaces env: - GH_TOKEN: ${{ secrets.REPOSITORY_SETTINGS_TOKEN || github.token }} + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} run: | - set -euo pipefail + set +e python scripts/ci/reconcile_repository_metadata.py \ --manifest config/repository-metadata.json - - name: Apply repository labels - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail + metadata_apply_status=$? python scripts/ci/reconcile_repository_labels.py \ --taxonomy config/repository-label-taxonomy.json + label_apply_status=$? + python scripts/ci/reconcile_repository_labels.py \ + --taxonomy config/repository-label-taxonomy.json \ + --verify-only + label_verify_status=$? + + metadata_verify_status=1 + metadata_verify_attempt=1 + metadata_verify_limit=12 + while (( metadata_verify_attempt <= metadata_verify_limit )); do + metadata_verify_output="$( + python scripts/ci/reconcile_repository_metadata.py \ + --manifest config/repository-metadata.json \ + --verify-only 2>&1 + )" + metadata_verify_status=$? + printf '%s\n' "${metadata_verify_output}" + if (( metadata_verify_status == 0 )); then + break + fi + + metadata_failure_lines="$( + printf '%s\n' "${metadata_verify_output}" \ + | grep '^repository metadata reconciliation failed for ' || true + )" + if [[ -z "${metadata_failure_lines}" ]] \ + || printf '%s\n' "${metadata_failure_lines}" \ + | grep -Evq 'GitHub Pages (was not published|configuration did not converge|is not built|is not reachable)'; then + break + fi + if (( metadata_verify_attempt == metadata_verify_limit )); then + break + fi + sleep 15 + ((metadata_verify_attempt += 1)) + done + + set -e + if (( metadata_apply_status != 0 \ + || label_apply_status != 0 \ + || metadata_verify_status != 0 \ + || label_verify_status != 0 )); then + printf 'metadata_apply=%s label_apply=%s metadata_verify=%s label_verify=%s\n' \ + "${metadata_apply_status}" \ + "${label_apply_status}" \ + "${metadata_verify_status}" \ + "${label_verify_status}" >&2 + exit 1 + fi From 229dcbb15a058bf42ddf825530c378c2a3f1912c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:13:09 +0900 Subject: [PATCH 09/17] test(metadata): preserve cancellation group across policy transition --- tests/test_repository_metadata_workflow_pages.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_repository_metadata_workflow_pages.py b/tests/test_repository_metadata_workflow_pages.py index 374a9ea410..5772539593 100644 --- a/tests/test_repository_metadata_workflow_pages.py +++ b/tests/test_repository_metadata_workflow_pages.py @@ -38,10 +38,7 @@ def test_metadata_pr_validation_cancels_superseded_head_runs() -> None: workflow = WORKFLOW.read_text(encoding="utf-8") concurrency = workflow.split("concurrency:", 1)[1].split("jobs:", 1)[0] - assert ( - "repository-metadata-reconcile-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }}" - in concurrency - ) + assert "group: repository-metadata-reconcile-${{ github.ref }}" in concurrency assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in concurrency assert "github.event.pull_request.head.sha" not in concurrency From d241c456ab678ff09c993d325af96a3f5de1953c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:13:37 +0900 Subject: [PATCH 10/17] fix(metadata): keep stable PR group while cancelling stale validation --- .github/workflows/repository-metadata-reconcile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index 4815cc4a8f..3bb9b6944d 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -25,7 +25,7 @@ permissions: contents: read concurrency: - group: repository-metadata-reconcile-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }} + group: repository-metadata-reconcile-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: From 2c0930cd7eef3f54df9223d9405a55f07918512f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:23:18 +0900 Subject: [PATCH 11/17] test(metadata): fail before workflow Pages side effects --- ...test_repository_metadata_workflow_pages.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/test_repository_metadata_workflow_pages.py b/tests/test_repository_metadata_workflow_pages.py index 5772539593..82aa4462f7 100644 --- a/tests/test_repository_metadata_workflow_pages.py +++ b/tests/test_repository_metadata_workflow_pages.py @@ -5,6 +5,7 @@ import importlib.util import json from pathlib import Path +from types import SimpleNamespace import pytest @@ -72,6 +73,23 @@ def repository_file_exists(repository, default_branch, path): assert seen == [("Repo", "main", ".github/workflows/pages.yml")] +def test_repository_file_probe_requires_a_regular_file(monkeypatch) -> None: + """A directory or listing at a required source path must not satisfy the file contract.""" + + responses = iter( + [ + SimpleNamespace(returncode=0, stdout=json.dumps({"type": "file"}), stderr=""), + SimpleNamespace(returncode=0, stdout=json.dumps({"type": "dir"}), stderr=""), + SimpleNamespace(returncode=0, stdout=json.dumps([{"type": "file"}]), stderr=""), + ] + ) + monkeypatch.setattr(RECONCILER.subprocess, "run", lambda *args, **kwargs: next(responses)) + + assert RECONCILER._repository_file_exists("Repo", "main", "docs/index.md") + assert not RECONCILER._repository_file_exists("Repo", "main", "docs/index.md") + assert not RECONCILER._repository_file_exists("Repo", "main", "docs/index.md") + + def test_workflow_pages_precondition_rejects_missing_reviewed_workflow(monkeypatch) -> None: """Workflow intent fails before mutation when the reviewed Pages workflow is absent.""" @@ -115,6 +133,42 @@ def gh_api(method, endpoint, **kwargs): assert page_writes == [] +def test_workflow_pages_reconcile_fails_closed_before_any_metadata_write(monkeypatch) -> None: + """Invalid workflow Pages state is rejected before description or topic mutation.""" + + writes = [] + + def gh_api(method, endpoint, **kwargs): + if method in {"PATCH", "PUT", "POST", "DELETE"}: + writes.append((method, endpoint, kwargs)) + if endpoint.endswith("/topics"): + return json.dumps({"names": ["old-topic"]}) + if endpoint.endswith("/pages"): + return json.dumps({"build_type": "legacy"}) + return json.dumps({"default_branch": "main", "description": "Old product."}) + + monkeypatch.setattr(RECONCILER, "_gh_api", gh_api) + monkeypatch.setattr(RECONCILER, "_deepwiki_badge_exists", lambda *args: False) + monkeypatch.setattr( + RECONCILER, "_workflow_pages_definition_exists", lambda *args: True + ) + + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: False) + with pytest.raises(RuntimeError, match="not configured"): + RECONCILER.reconcile_repository("Repo", desired()) + assert writes == [] + + monkeypatch.setattr(RECONCILER, "_pages_exists", lambda *args: True) + monkeypatch.setattr( + RECONCILER, + "_pages_configuration", + lambda *args: {"build_type": "legacy", "source": {"branch": "main", "path": "/docs"}}, + ) + with pytest.raises(RuntimeError, match="not Actions-backed"): + RECONCILER.reconcile_repository("Repo", desired()) + assert writes == [] + + def test_workflow_pages_reconcile_fails_closed_on_missing_or_wrong_mode(monkeypatch) -> None: """Workflow intent never creates or converts Pages through the legacy settings API.""" From 5513dd197ba14091a216755da6305d874d01d7e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:24:51 +0900 Subject: [PATCH 12/17] fix(metadata): validate workflow Pages before writes --- scripts/ci/reconcile_repository_metadata.py | 39 +++++++++++++-------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 696a12bc98..4349fbaeb7 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -249,7 +249,7 @@ def _pages_publication_ready(repository: str, current: dict[str, Any]) -> None: def _repository_file_exists(repository: str, default_branch: str, path: str) -> bool: - """Return whether a reviewed default-branch file exists at the exact path.""" + """Return whether a reviewed default-branch regular file exists at the exact path.""" endpoint = f"repos/{ORGANIZATION}/{repository}/contents/{path}?ref={default_branch}" command = ["gh", "api", endpoint] @@ -261,7 +261,8 @@ def _repository_file_exists(repository: str, default_branch: str, path: str) -> timeout=30, ) if completed.returncode == 0: - return True + payload = json.loads(completed.stdout) + return type(payload) is dict and payload.get("type") == "file" combined = f"{completed.stdout}\n{completed.stderr}" if "HTTP 404" in combined or "Not Found" in combined: return False @@ -344,6 +345,22 @@ def _pages_precondition(repository: str, default_branch: str, desired: dict[str, ) +def _workflow_pages_live_precondition(repository: str, desired: dict[str, Any]) -> None: + """Require existing Actions-backed Pages before any repository metadata mutation.""" + + if not desired["pages"] or desired.get("pages_mode", "legacy") != "workflow": + return + if not _pages_exists(repository): + raise RuntimeError( + f"workflow Pages requested for {repository} but Pages is not configured" + ) + current_pages = _pages_configuration(repository) + if not _workflow_pages_configuration_matches(current_pages): + raise RuntimeError( + f"workflow Pages requested for {repository} but live Pages is not Actions-backed" + ) + + def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: """Apply one validated desired-state record through least-privilege GitHub APIs.""" @@ -364,6 +381,7 @@ def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: f"DeepWiki badge is disabled for {repository} but the exact badge is still on {default_branch}" ) _pages_precondition(repository, default_branch, desired) + _workflow_pages_live_precondition(repository, desired) if repository_payload.get("description") != desired["description"]: _gh_api( @@ -382,21 +400,12 @@ def reconcile_repository(repository: str, desired: dict[str, Any]) -> None: body={"names": desired["topics"]}, ) + pages_mode = desired.get("pages_mode", "legacy") + if desired["pages"] and pages_mode == "workflow": + return + pages_exists = _pages_exists(repository) if desired["pages"]: - pages_mode = desired.get("pages_mode", "legacy") - if pages_mode == "workflow": - if not pages_exists: - raise RuntimeError( - f"workflow Pages requested for {repository} but Pages is not configured" - ) - current_pages = _pages_configuration(repository) - if not _workflow_pages_configuration_matches(current_pages): - raise RuntimeError( - f"workflow Pages requested for {repository} but live Pages is not Actions-backed" - ) - return - pages_body = { "build_type": "legacy", "source": {"branch": default_branch, "path": "/docs"}, From 2498c597cd9521c022d276fb32b5b2d65d4de796 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:25:40 +0900 Subject: [PATCH 13/17] docs(adr): define workflow-backed Pages reconciliation --- ...-repository-public-surface-reconciliation.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/adr/0020-repository-public-surface-reconciliation.md b/docs/adr/0020-repository-public-surface-reconciliation.md index 6968985521..c0c8dc650f 100644 --- a/docs/adr/0020-repository-public-surface-reconciliation.md +++ b/docs/adr/0020-repository-public-surface-reconciliation.md @@ -12,22 +12,25 @@ The organization therefore needs one auditable owner for the desired state and o ## Decision -1. `config/repository-metadata.json` is the reviewed desired state for exact repository casing, concise public descriptions, normalized topics, exact DeepWiki intent, and GitHub Pages intent. +1. `config/repository-metadata.json` is the reviewed desired state for exact repository casing, concise public descriptions, normalized topics, exact DeepWiki intent, and GitHub Pages intent. `pages_mode` is optional; omitted means the established legacy `/docs` mode, while `pages_mode: workflow` explicitly preserves an existing Actions-backed deployment. 2. `config/repository-label-taxonomy.json` defines the small semantic label vocabulary and explicit repository/issue assignments. The reconciler manages only labels named by that vocabulary and preserves unrelated priority, status, area, and workflow labels. 3. `scripts/ci/reconcile_repository_metadata.py` applies description, topics, and Pages settings only after repository-local preconditions are present on the protected default branch. It aggregates repository failures so one blocked leaf does not prevent independent repositories from being attempted. 4. `scripts/ci/reconcile_repository_labels.py` applies only reviewed label assignments. It mutates taxonomy-managed labels through individual label endpoints, is idempotent, preserves unrelated concurrent labels, and aggregates assignment failures for the same non-blocking fleet behavior. 5. DeepWiki README content is not mutated centrally. `deepwiki: true` requires the exact linked badge on the default branch before metadata writes; `deepwiki: false` fails closed while that exact badge is still present so desired state cannot silently contradict the public README. -6. Pages uses GitHub's legacy branch source on the repository default branch at `/docs`. Creation occurs only when no site exists; update occurs only when branch, path, or build type differs; disable deletes an existing site. A converged Pages site receives no hourly write. -7. Pull-request execution is read-only validation. Privileged reconciliation runs only from trusted `.github/main`, uses the existing maintainer credential, does not widen pull-request tokens, and does not bypass repository rulesets or reviews. -8. Reconciliation runs from the trusted hourly schedule and exposes no branch-selectable `workflow_dispatch` entrypoint. Ref-scoped concurrency does not cancel an active apply midway, so partial fleet state is completed by the active run rather than being abandoned by a replacement run. -9. Metadata and label lanes retain independent exit statuses during apply: label reconciliation still runs after an aggregated metadata failure, and the job fails afterward if either lane failed. -10. Repository-wide tests, focused 100% statement/branch coverage for both reconciliation scripts, docstring gates, manifest/taxonomy validation, and `git diff --check` are required before apply can run. +6. Pages has two explicit ownership modes. Legacy mode requires the repository default branch to contain the regular file `docs/index.md`; absent legacy sites may be created at `/docs`, drifted legacy sites may be updated, and converged sites receive no write. Workflow mode requires the regular file `.github/workflows/pages.yml` on the protected default branch **and** an already-existing live Pages configuration with `build_type: workflow`. The central reconciler never creates or converts a workflow-backed site. Those workflow-mode source and live-configuration preconditions are validated before description, topic, or Pages mutation so an invalid workflow declaration cannot leave a partially applied metadata record. +7. Contents API source probes are type-aware. A successful response satisfies a required-source precondition only when the response is a single object with `type: file`; a directory object or directory listing is not accepted as reviewed file evidence. +8. Pull-request execution is read-only validation. Privileged reconciliation runs only from trusted `.github/main`, uses the existing maintainer credential, does not widen pull-request tokens, and does not bypass repository rulesets or reviews. +9. Reconciliation runs from the trusted hourly schedule and exposes no branch-selectable `workflow_dispatch` entrypoint. Pull-request validation keeps a PR-stable concurrency lineage and cancels superseded validation runs; trusted scheduled protected-main apply remains non-cancellable so a replacement heartbeat cannot abandon a partially updated fleet. +10. Metadata and label lanes retain independent exit statuses during apply: label reconciliation still runs after an aggregated metadata failure, and the job fails afterward if either lane failed. +11. Repository-wide tests, focused 100% statement/branch coverage for both reconciliation scripts, docstring gates, manifest/taxonomy validation, and `git diff --check` are required before apply can run. ## Consequences - Public metadata becomes declarative, reviewable, repeatable, and convergent instead of depending on ad-hoc connector capabilities. - A leaf repository can block only its own unsafe mutation; other eligible repositories continue in the same invocation. - Exact README and Pages preconditions make a source commit insufficient evidence of publication. Live repository metadata and Pages state must be re-read after apply before publication is claimed. +- Actions-backed Pages can be enrolled without silently rewriting a repository's reviewed deployment architecture to legacy `/docs`. +- Workflow-mode failure is fail-before-write for the repository record: missing workflow source, missing Pages, or a non-workflow live build type prevents description/topic mutation as well as Pages mutation. - Explicit label assignments intentionally favor evidence over broad title heuristics. Expanding classification coverage requires a reviewed assignment or a separately justified deterministic classifier. - The privileged token must retain only the repository-administration/Pages/issue permissions required by the declared fleet. Credential values never enter the manifest or logs. @@ -35,6 +38,8 @@ The organization therefore needs one auditable owner for the desired state and o - **Report missing connector mutations without repair.** Rejected because the organization owns a GitHub Actions/API control plane that can safely provide the capability. - **Mutate README badges from the central control plane.** Rejected because that would bypass the active product writer and make customer-facing content independent of product review. +- **Convert workflow-backed Pages to legacy `/docs` for uniformity.** Rejected because deployment ownership is a reviewed product boundary; reconciliation must preserve an explicitly declared Actions-backed deployment rather than rewrite it. +- **Treat any successful Contents API response as file evidence.** Rejected because a directory can exist at the same path and must not satisfy a regular-file precondition. - **Expose branch-selected manual dispatch.** Rejected because the central control-plane contract requires manual entrypoints not to load branch-selected code. - **Replace an issue's entire label list.** Rejected because stale read-modify-write can erase unrelated labels added concurrently by humans or automation. - **Rewrite Pages every hour.** Rejected because a converged desired-state reconciler must have a write-free steady state. From 5750098edefc930f38046eadad2bed7d93dd2875 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:26:11 +0900 Subject: [PATCH 14/17] docs(runbook): add workflow Pages operating contract --- ...epository-public-surface-reconciliation.md | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/doctoring/repository-public-surface-reconciliation.md b/docs/doctoring/repository-public-surface-reconciliation.md index 4a1a79a477..6fa36c5ddc 100644 --- a/docs/doctoring/repository-public-surface-reconciliation.md +++ b/docs/doctoring/repository-public-surface-reconciliation.md @@ -17,10 +17,10 @@ flowchart TD Manifest["repository-metadata.json"] Taxonomy["repository-label-taxonomy.json"] Validate["read-only PR validation"] - Leaf["leaf README + docs/index.md on default branch"] + Leaf["leaf README + reviewed Pages source on default branch"] Apply["trusted .github/main apply"] Metadata["description + topics"] - Pages["Pages create/update/delete only on drift"] + Pages["legacy /docs reconcile OR workflow mode preserve"] Labels["reviewed issue/PR label assignments"] Verify["re-read live public state"] @@ -44,16 +44,20 @@ The fleet loop is deliberately non-blocking. Every repository or label assignmen - Apply runs only when the scheduled workflow is executing from trusted `refs/heads/main` after validation. - The apply step uses the established maintainer credential rather than widening the ordinary workflow token. - Repository README changes remain leaf-owned. The central reconciler verifies exact DeepWiki linkage but never fabricates or silently edits customer-facing README copy. -- Pages publication is conditional on `docs/index.md` being present on the live default branch. A branch-only source or PR is not publication evidence. -- Pages is convergent: absent sites are created, drifted legacy `/docs` sites are updated, disabled sites are deleted, and already-correct sites receive no write. +- Pages has two reviewed deployment modes. Legacy mode requires a regular `docs/index.md` file on the live default branch. Explicit `pages_mode: workflow` requires a regular `.github/workflows/pages.yml` file **and** an already-configured live Pages site whose `build_type` is `workflow`. +- Workflow mode is preserve-only: the reconciler does not create or convert the Pages configuration. Missing Pages, a legacy live configuration, a directory at the required workflow path, or a missing workflow file fails before description/topic/Page writes for that repository. +- Legacy Pages remains convergent: absent sites are created, drifted legacy `/docs` sites are updated, disabled sites are deleted, and already-correct sites receive no write. +- Contents API source checks require a single object with `type: file`; directory objects and directory listings do not count as reviewed source evidence. - Label reconciliation adds and removes only taxonomy-managed labels through individual label endpoints, so unrelated labels added by people or automation are not replaced from a stale snapshot. -- Scheduled reconciliation does not cancel an active apply, preventing a replacement run from abandoning a partially updated fleet. +- Pull-request metadata validation uses the stable `repository-metadata-reconcile-${{ github.ref }}` concurrency lineage and cancels superseded PR runs. The scheduled trusted apply remains non-cancellable, preventing a replacement heartbeat from abandoning a partially updated fleet. - The repository's control-plane contract intentionally exposes no branch-selectable `workflow_dispatch` entrypoint; remediation follows the trusted default-branch schedule and normal rerun/governance paths. ## Desired-state fleet in this increment The repository metadata manifest currently covers eight repositories selected because their public-surface work already has a concrete leaf source or active writer: `CalendarWeave`, `ConceptWeave`, `context-graph-contracts`, `ThreadWeave`, `RankWeave`, `fast-mlsirm`, `EgressWeave`, and `psychometrics-commons`. EgressWeave and Psychometrics Commons joined the fleet after their exact-cased DeepWiki badges and bounded `docs/index.md` Pages sources reached their protected default branches. +An Actions-backed repository is not enrolled merely because `pages_mode: workflow` is supported. Enrollment requires an explicit reviewed manifest change after the repository's standard Pages workflow and live `build_type: workflow` configuration both exist. This preserves the deployment architecture of repositories such as ScopeWeave instead of silently rewriting them to legacy `/docs`. + The explicit label assignments now cover 19 evidence-backed targets: `.github#1582`, `CalendarWeave#1`, `ConceptWeave#1`, `context-graph-contracts#20`, `RankWeave#40`, `fast-mlsirm#1717`, `EgressWeave#231`, `psychometrics-commons#442`, `contextual-orchestrator#994`, `contextual-orchestrator#1003`, `appguardrail#1077`, `naruon#1513`, `LineageWeave#908`, `ContextualWisdomLab.github.io#203`, `TEPP#435`, `semantic-data-portal#72`, `Orgmetra#160`, `learning-interoperability-contracts#1`, and `noema#530`. The assignment reconciler preserves richer repository-local labels such as priority, status, and `type: maintenance` when those labels are outside the managed semantic set. ## Verification contract @@ -63,12 +67,23 @@ A central source commit is not completion. After protected integration and apply 1. the live description equals reviewed desired state; 2. live topics equal the normalized desired set; 3. the default-branch README carries the exact linked DeepWiki badge when requested; -4. `docs/index.md` exists on the live default branch before Pages is enabled; -5. the live Pages configuration uses the intended default branch and `/docs`, and the published site is reachable before publication is claimed; -6. reviewed issue/PR targets carry the desired managed label while unrelated labels remain intact. +4. the selected Pages source is a regular file on the protected default branch: `docs/index.md` for legacy mode or `.github/workflows/pages.yml` for workflow mode; +5. legacy mode uses the intended default branch and `/docs`; workflow mode remains `build_type: workflow` and is never converted by the reconciler; +6. the Pages status is `built`, its URL remains under `https://contextualwisdomlab.github.io`, and the published endpoint returns non-empty content before publication is claimed; +7. reviewed issue/PR targets carry the desired managed label while unrelated labels remain intact. + +GitHub's current REST Pages contract supports `build_type` values `legacy` and `workflow`, and branch sources with `/` or `/docs`. Legacy desired-state records continue to use `/docs`. The explicit workflow mode exists to preserve a repository whose deployment is already owned by a reviewed GitHub Actions workflow; it is not a central creation/conversion mechanism. + +## Workflow-mode operating procedure -GitHub's current REST Pages contract supports `build_type` values `legacy` and `workflow`, and branch sources with `/` or `/docs`. The reconciler selects `legacy` plus `/docs` because the leaf repositories provide reviewed static documentation sources rather than a separate custom Pages workflow. +1. Land and review the repository-local `.github/workflows/pages.yml` on the protected default branch. +2. Verify the repository already has a live GitHub Pages configuration with `build_type: workflow`; do not rely on a PR branch or workflow filename alone. +3. Add `"pages": true` and `"pages_mode": "workflow"` to the exact-cased repository record in `config/repository-metadata.json`. +4. Let read-only PR validation prove manifest/source contracts and stale-run cancellation without settings write authority. +5. After protected integration, let the trusted scheduled reconciler preflight the workflow source and live deployment mode **before** any description/topic mutation. +6. Re-read description, topics, Pages build type, publication status, organization-owned URL, and non-empty live content. Only then mark the public-surface reconciliation complete. +7. If the workflow file disappears or the live deployment changes away from `workflow`, the repository fails closed and receives no metadata write until the repository-owned deployment boundary is repaired. ## Known integration boundary -Until the central PR is merged through normal governance, the settings reconciliation cannot run from trusted `.github/main`; leaf PRs whose badge or Pages source is still branch-only also remain repository-local precondition blockers. These are integration states, not reasons to stop independent repository work. The same run should continue classifying labels, preparing other leaf public surfaces, and re-checking earlier lanes when exact-head evidence becomes available. +Until the central PR is merged through normal governance or a verified queue-saturation chicken-and-egg exception, the workflow-mode preservation contract cannot run from trusted `.github/main`; leaf PRs whose badge or Pages source is still branch-only also remain repository-local precondition blockers. These are integration states, not reasons to stop independent repository work. The same run should continue classifying labels, preparing other leaf public surfaces, and re-checking earlier lanes when exact-head evidence becomes available. From a57b16dbcc1b75e9d2cb89b0687bf5bb93584cf2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:27:21 +0900 Subject: [PATCH 15/17] docs(architecture): model workflow Pages preservation boundary --- ARCHITECTURE.md | 62 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 565e90b086..0c248e43af 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -41,40 +41,56 @@ dispatch is intentionally absent under the central workflow trust contract. flowchart TD Desired["reviewed metadata + label desired state"] Validate["read-only exact-revision validation"] - Preconditions{"leaf README badge / docs source live?"} + Preconditions{"README + reviewed Pages source + live mode valid?"} Apply["trusted protected-main apply"] Repo["description + topics"] - Pages["Pages state"] + Legacy["legacy /docs create/update/delete"] + Workflow["workflow Pages preserve-only"] Labels["reviewed issue / PR labels"] Verify["live public-state re-read"] - Hold["fail this leaf; continue siblings"] + Hold["fail this leaf before writes; continue siblings"] Desired --> Validate Validate --> Preconditions Preconditions -->|"no"| Hold Preconditions -->|"yes"| Apply Apply --> Repo - Apply --> Pages + Apply --> Legacy + Apply --> Workflow Apply --> Labels Repo --> Verify - Pages --> Verify + Legacy --> Verify + Workflow --> Verify Labels --> Verify ``` -The metadata reconciler is convergent: already-correct descriptions/topics and -legacy default-branch `/docs` Pages sites receive no write; absent or drifted -Pages state is created/updated, and disabled Pages is deleted. Topic equality -is set-based so GitHub presentation ordering cannot manufacture drift. Exact -DeepWiki badge state is a leaf-owned precondition, including a fail-closed -contradiction when desired state disables DeepWiki while the badge remains -live. Label reconciliation adds/removes only taxonomy-declared labels through -individual endpoints, preserving unrelated concurrent priority/status/area -labels. Metadata and label failures retain independent exit statuses, so a -blocked metadata leaf does not prevent eligible label work in the same apply. -Failures aggregate after independent repositories or assignments are attempted, -so one blocked leaf never serializes the fleet. Scheduled applies share a -ref-scoped lane and do not cancel active apply work midway. See ADR-0020 and the -operational baseline for the authority and live-verification contract. +The metadata reconciler is convergent and mode-aware. Already-correct +descriptions/topics and legacy default-branch `/docs` Pages sites receive no +write; absent or drifted legacy Pages state is created/updated, and disabled +Pages is deleted. An explicit `pages_mode: workflow` instead preserves an +already-configured Actions-backed site: `.github/workflows/pages.yml` must be a +regular file on the protected default branch and the live Pages configuration +must already report `build_type: workflow`. The central control plane never +creates or converts workflow mode. These source/live-mode preconditions run +before repository description or topic mutation, so an invalid workflow Pages +declaration cannot leave a partially applied repository record. Contents API +source probes accept only a single `type: file` object; directories and listings +are not valid source evidence. + +Topic equality is set-based so GitHub presentation ordering cannot manufacture +drift. Exact DeepWiki badge state is a leaf-owned precondition, including a +fail-closed contradiction when desired state disables DeepWiki while the badge +remains live. Label reconciliation adds/removes only taxonomy-declared labels +through individual endpoints, preserving unrelated concurrent +priority/status/area labels. Metadata and label failures retain independent +exit statuses, so a blocked metadata leaf does not prevent eligible label work +in the same apply. Failures aggregate after independent repositories or +assignments are attempted, so one blocked leaf never serializes the fleet. +Pull-request metadata validation keeps a PR-stable concurrency lineage and +cancels superseded validations; scheduled protected-main apply is deliberately +non-cancellable so a newer heartbeat cannot abandon partially updated fleet +state. See ADR-0020 and the operational baseline for the authority and +live-verification contract. ## OriginWeave hourly caller @@ -174,7 +190,9 @@ sequenceDiagram - Reviewer agents stay `edit: deny`. They judge; they do not implement. - Repository public-surface writes execute only from trusted `.github/main`; pull-request validation remains read-only and leaf README changes keep their - repository-local review boundary. + repository-local review boundary. Workflow-backed Pages is preserve-only and + must pass its source/live-mode precondition before any repository metadata + write. - Central Semgrep binds one job-level `SEMGREP_IMAGE` digest for log evidence, manifest inspect, and `docker run` so buyers can reconstruct the exact scanner that produced SARIF. @@ -210,7 +228,9 @@ CI installs Python tools only with `pip install --require-hashes`. Contract tests pin workflow structure and governance prose so drift fails closed. The repository-public-surface workflow additionally holds both reconciliation scripts to 100% statement/branch coverage and 100% docstrings before its -privileged apply job can run. +privileged apply job can run. Workflow-mode regressions specifically require +fail-before-write behavior and reject directory/listing responses as Pages +source evidence. The trusted `uv` exporter is downloaded from the literal GitHub Releases URL for `uv` 0.12.1; `releases.astral.sh` is not the network sink. An exact-base `uv.lock` may additionally expose source from an organization-owned From 35dc7a05e74294eeaaf5626a9e015dc7c2e6f78a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:29:53 +0900 Subject: [PATCH 16/17] merge main: preserve explicit redirect rejection in metadata lane --- scripts/ci/reconcile_repository_metadata.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 4349fbaeb7..36a910ffa8 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -36,9 +36,11 @@ class _NoPagesRedirects(HTTPRedirectHandler): """Refuse redirects so Pages verification cannot be redirected off GitHub Pages.""" def redirect_request(self, req, fp, code, msg, headers, newurl): - """Return no follow-up request for any redirect.""" + """Raise an HTTPError instead of following the redirect.""" - return None + from urllib.error import HTTPError + + raise HTTPError(req.full_url, code, msg, headers, fp) def _require_exact_dict(value: Any, *, field: str) -> dict[str, Any]: From f30dc06e72d0d3cca22cabbed0b70a540ccafaf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:31:57 +0900 Subject: [PATCH 17/17] merge main: preserve explicit redirect regression --- tests/test_repository_metadata_live_verification.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_repository_metadata_live_verification.py b/tests/test_repository_metadata_live_verification.py index 7914d7bfa5..6ae83d8c47 100644 --- a/tests/test_repository_metadata_live_verification.py +++ b/tests/test_repository_metadata_live_verification.py @@ -144,12 +144,11 @@ def build_ok(handler): ] assert len(handlers) == 1 assert isinstance(handlers[0], RECONCILER._NoPagesRedirects) - assert ( + from urllib.error import HTTPError + with pytest.raises(HTTPError): handlers[0].redirect_request( - None, None, 302, "redirect", {}, "http://127.0.0.1/" + RECONCILER.Request("https://example.com"), None, 302, "redirect", {}, "http://127.0.0.1/" ) - is None - ) with pytest.raises(RuntimeError, match="not built"): RECONCILER._pages_publication_ready("Repo", {**ready, "status": "building"})