diff --git a/.github/workflows/repo-ci.yml b/.github/workflows/repo-ci.yml index c7029fd..57b610c 100644 --- a/.github/workflows/repo-ci.yml +++ b/.github/workflows/repo-ci.yml @@ -163,3 +163,14 @@ jobs: # Validates the reusable's wiring/inputs; the release-please action and # evidence dispatch are skipped (side effects), so no release is made. smoke: true + + org-adr-auto-sync-smoke: + name: Smoke-test reusable-org-adr-auto-sync (self) + permissions: + contents: read + pull-requests: read + uses: ./.github/workflows/reusable-org-adr-auto-sync.yaml + with: + source_repo: NWarila/.github + source_ref: main + smoke: true diff --git a/.github/workflows/reusable-org-adr-auto-sync.yaml b/.github/workflows/reusable-org-adr-auto-sync.yaml new file mode 100644 index 0000000..0c8e887 --- /dev/null +++ b/.github/workflows/reusable-org-adr-auto-sync.yaml @@ -0,0 +1,207 @@ +name: Reusable Org ADR Auto Sync + +on: + workflow_call: + inputs: + source_repo: + description: "Namespace control-plane repository that owns the org ADRs." + required: true + type: string + source_ref: + description: "Source ref to sync from. Use main for scheduled sync." + required: false + type: string + default: main + target_branch: + description: "Repository branch that receives sync PRs." + required: false + type: string + default: main + branch_name: + description: "Sync branch to create or update in the caller repository." + required: false + type: string + default: sync/org-adrs + pr_title: + description: "Pull request title for ADR mirror sync changes." + required: false + type: string + default: "[sync] refresh org ADR mirrors" + smoke: + description: "Exercise sync logic without committing, pushing, or opening a PR." + required: false + type: boolean + default: false + secrets: + sync_token: + description: "Token for pushing sync branches and opening PRs." + required: false + +permissions: + contents: read + pull-requests: read + +jobs: + sync: + name: org ADR mirror sync + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout caller repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Sync org ADR mirrors + env: + BRANCH_NAME: ${{ inputs.branch_name }} + GH_TOKEN: ${{ secrets.sync_token || github.token }} + PR_TITLE: ${{ inputs.pr_title }} + SMOKE: ${{ inputs.smoke }} + SYNC_TOKEN: ${{ secrets.sync_token }} + SOURCE_REF: ${{ inputs.source_ref }} + SOURCE_REPO: ${{ inputs.source_repo }} + TARGET_BRANCH: ${{ inputs.target_branch }} + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git fetch --no-tags origin "${TARGET_BRANCH}" + git checkout -B "${BRANCH_NAME}" "origin/${TARGET_BRANCH}" + + source_dir="${RUNNER_TEMP}/org-adr-source" + mkdir -p "${source_dir}" + git -C "${source_dir}" init + git -C "${source_dir}" remote add origin "https://github.com/${SOURCE_REPO}.git" + git -C "${source_dir}" fetch --depth=1 origin "${SOURCE_REF}" + git -C "${source_dir}" checkout --detach FETCH_HEAD + source_sha="$(git -C "${source_dir}" rev-parse HEAD)" + export SOURCE_SHA="${source_sha}" + echo "Resolved ${SOURCE_REPO}@${SOURCE_REF} to ${source_sha}" + + python - <<'PY' + import json + import os + import re + import shutil + from pathlib import Path + + root = Path.cwd() + source_dir = Path(os.environ["RUNNER_TEMP"]) / "org-adr-source" + 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")) + entries = [] + for item in manifest.get("files", []): + source = item.get("source") + target = item.get("target") + if not isinstance(source, str) or not isinstance(target, str): + continue + if not target.startswith("docs/decision-records/org/"): + continue + if Path(target).name != ".gitkeep" and not Path(target).name.startswith("000"): + continue + entries.append((source, target)) + + if not entries: + raise SystemExit("source manifest contains no org ADR mirror entries") + + expected_targets = {target for _, target in entries} + target_dir = root / "docs" / "decision-records" / "org" + target_dir.mkdir(parents=True, exist_ok=True) + + for stale in target_dir.glob("000*.md"): + rel = stale.relative_to(root).as_posix() + if rel not in expected_targets: + stale.unlink() + print(f"removed stale mirror {rel}") + + for source, target in entries: + src = source_dir / source + dst = root / target + if not src.is_file(): + raise SystemExit(f"manifest source is missing: {source}") + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(src, dst) + print(f"synced {target}") + + detector = root / ".github" / "workflows" / "org-adr-sync.yaml" + if detector.is_file(): + text = detector.read_text(encoding="utf-8") + lines = text.splitlines(keepends=True) + changed = False + source_pattern = re.compile(rf"^\s*source-repo:\s*{re.escape(source_repo)}\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))): + match = ref_pattern.match(lines[ref_index]) + if match: + lines[ref_index] = f"{match.group(1)}{source_sha}{match.group(3) or os.linesep}" + changed = True + break + break + if changed: + detector.write_text("".join(lines), encoding="utf-8") + print(f"updated {detector.relative_to(root).as_posix()} source-ref") + else: + print("org-adr-sync.yaml did not contain a matching source-ref to update") + PY + + if [[ "${SMOKE}" == "true" ]]; then + echo "Smoke mode: sync logic completed; not committing, pushing, or opening a PR." + git status --short + exit 0 + fi + + if git diff --quiet; then + echo "Org ADR mirrors are already current." + exit 0 + fi + + if [[ -z "${SYNC_TOKEN}" ]]; then + echo "A sync_token secret is required to push sync branches and open PRs." + exit 1 + fi + + git status --short + git add docs/decision-records/org + if [[ -f .github/workflows/org-adr-sync.yaml ]]; then + git add .github/workflows/org-adr-sync.yaml + fi + git commit -m "sync org ADR mirrors" + git remote set-url origin "https://x-access-token:${SYNC_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git push --force-with-lease origin "HEAD:${BRANCH_NAME}" + + body_file="${RUNNER_TEMP}/org-adr-sync-pr.md" + cat > "${body_file}" <