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
11 changes: 11 additions & 0 deletions .github/workflows/repo-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
207 changes: 207 additions & 0 deletions .github/workflows/reusable-org-adr-auto-sync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
name: Reusable Org ADR Auto Sync
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

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}" <<EOF
Synchronizes org ADR mirrors from \`${SOURCE_REPO}@${source_sha}\`.

This PR was opened by the namespace-local org ADR auto-sync workflow.
It copies only \`docs/decision-records/org/\` mirror targets from the
source baseline manifest and updates the detector workflow source pin
when that workflow exists in this repository.
EOF

existing_pr="$(gh pr list \
--head "${BRANCH_NAME}" \
--base "${TARGET_BRANCH}" \
--json number \
--jq '.[0].number')"
if [[ -n "${existing_pr}" ]]; then
gh pr edit "${existing_pr}" --title "${PR_TITLE}" --body-file "${body_file}"
echo "Updated PR #${existing_pr}"
else
gh pr create \
--title "${PR_TITLE}" \
--body-file "${body_file}" \
--head "${BRANCH_NAME}" \
--base "${TARGET_BRANCH}"
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
!/.github/workflows/reusable-auto-merge.yaml
!/.github/workflows/reusable-codeql.yaml
!/.github/workflows/reusable-iac-security.yaml
!/.github/workflows/reusable-org-adr-auto-sync.yaml
!/.github/workflows/reusable-release-please.yaml
!/.github/workflows/reusable-repo-hygiene.yaml
!/.github/workflows/reusable-scorecard.yaml
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/mirroring.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ Consumers mirror what they inherit as governance or directly run in their own li

Org governance is namespace-local. A `NWarila/*` repository mirrors org ADRs and community files from `NWarila/.github`. A repository under another namespace mirrors the same categories from that namespace's `.github` control plane. Cross-namespace references remain valid for explicit type-template or tool dependencies, but not for org-control-plane governance.

## 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.

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.

This auto-sync supplements the drift-gate detector; it does not replace review. The detector stays responsible for byte-identity verification, while the auto-sync keeps the repair path small and namespace-scoped.

## Byte-Identity Rule

Use byte identity when local edits would be drift. Do not use byte identity when local edits would be maturity.
Expand Down