Skip to content

Commit 9accbb5

Browse files
committed
fix(ci): make fullrepo bootstrap race-tolerant
1 parent 698a800 commit 9accbb5

5 files changed

Lines changed: 328 additions & 6 deletions

File tree

.github/workflows/release.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ jobs:
4040
install-dart: "false"
4141

4242
- name: Bootstrap fullrepo agent context
43-
run: scripts/sync_fullrepo_branch.sh --bootstrap-init
43+
shell: bash
44+
run: |
45+
set -o pipefail
46+
mkdir -p dist/gates
47+
scripts/sync_fullrepo_branch.sh --bootstrap-ci 2>&1 | tee dist/gates/fullrepo-bootstrap.log
4448
4549
- name: Validate release version
4650
shell: bash

.github/workflows/validate.yml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ jobs:
4747
install-dart: "false"
4848

4949
- name: Bootstrap fullrepo agent context
50-
run: scripts/sync_fullrepo_branch.sh --bootstrap-init
50+
shell: bash
51+
run: |
52+
set -o pipefail
53+
mkdir -p dist/gates
54+
scripts/sync_fullrepo_branch.sh --bootstrap-ci 2>&1 | tee dist/gates/fullrepo-bootstrap.log
5155
5256
- name: Run fast validation
5357
shell: bash
@@ -65,6 +69,8 @@ jobs:
6569
pytest.xml
6670
coverage.xml
6771
pytest.stderr.log
72+
dist/gates/fullrepo-bootstrap.log
73+
dist/gates/fullrepo-bootstrap-ci.json
6874
retention-days: 14
6975

7076
runtime-ubuntu:
@@ -212,7 +218,11 @@ jobs:
212218
uses: ./.github/actions/setup-codex-runtime
213219

214220
- name: Bootstrap fullrepo agent context
215-
run: scripts/sync_fullrepo_branch.sh --bootstrap-init
221+
shell: bash
222+
run: |
223+
set -o pipefail
224+
mkdir -p dist/gates
225+
scripts/sync_fullrepo_branch.sh --bootstrap-ci 2>&1 | tee dist/gates/fullrepo-bootstrap.log
216226
217227
- name: Install rldyour Codex into temporary CODEX_HOME
218228
run: scripts/install_system_codex.sh --apply --codex-home "$CODEX_HOME"
@@ -236,5 +246,8 @@ jobs:
236246
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
237247
with:
238248
name: mcp-safe-calls-stderr
239-
path: mcp-safe-calls.stderr.log
249+
path: |
250+
mcp-safe-calls.stderr.log
251+
dist/gates/fullrepo-bootstrap.log
252+
dist/gates/fullrepo-bootstrap-ci.json
240253
retention-days: 14

plugins/rldyour-flow/scripts/fullrepo_sync.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import subprocess
1010
import sys
1111
import tempfile
12+
import time
1213
from pathlib import Path
1314
from typing import Iterable, cast
1415

@@ -27,6 +28,9 @@
2728
FULLREPO_PROTOCOL_VERSION = "1.0.0"
2829
FULLREPO_GENERATOR_DIGEST = "bb2036ac52e20f898a17ab9333099758da690f933a14e720133eb3ba967256a8"
2930
PUBLISH_RETRIES = 3
31+
BOOTSTRAP_CI_ATTEMPTS = 6
32+
BOOTSTRAP_CI_SLEEP_SECONDS = 10.0
33+
BOOTSTRAP_CI_RECEIPT = "dist/gates/fullrepo-bootstrap-ci.json"
3034
EXCLUDE_BEGIN = "# >>> rldyour fullrepo agent-only files >>>"
3135
EXCLUDE_END = "# <<< rldyour fullrepo agent-only files <<<"
3236

@@ -705,6 +709,33 @@ def restore_local(remote: str, branch: str, dry_run: bool = False, *, ignore_pro
705709
print(f"restored {len(remote_paths)} agent-only files from local {remote}/{branch}@{resolved_ref[:12]} ({resolved['mode']})")
706710

707711

712+
def restore_resolved_overlay(
713+
resolved: dict[str, object],
714+
*,
715+
label: str,
716+
dry_run: bool = False,
717+
) -> list[str]:
718+
resolved_ref = str(resolved["commit"])
719+
remote_paths = tracked_agent_paths(resolved_ref)
720+
if not remote_paths:
721+
print(f"resolved fullrepo overlay {resolved_ref[:12]} has no agent-only files")
722+
return []
723+
724+
if dry_run:
725+
print(
726+
f"dry-run: would restore {len(remote_paths)} agent-only files from "
727+
f"{label}@{resolved_ref[:12]} ({resolved['mode']})"
728+
)
729+
return remote_paths
730+
731+
for index in range(0, len(remote_paths), 64):
732+
chunk = remote_paths[index : index + 64]
733+
_git("restore", "--source", resolved_ref, "--worktree", "--", *chunk)
734+
735+
print(f"restored {len(remote_paths)} agent-only files from {label}@{resolved_ref[:12]} ({resolved['mode']})")
736+
return remote_paths
737+
738+
708739
def migrate_main(dry_run: bool = False, *, ignore_project_policy: bool = False) -> None:
709740
policy = _project_policy()
710741
enforce_fullrepo_policy(policy, "migrate-main", ignore_project_policy=ignore_project_policy)
@@ -772,6 +803,144 @@ def bootstrap_init(
772803
print_status(payload, as_json=False)
773804

774805

806+
def _env_int(name: str, default: int) -> int:
807+
raw = os.environ.get(name)
808+
if not raw:
809+
return default
810+
try:
811+
value = int(raw)
812+
except ValueError:
813+
raise FullrepoError(f"{name} must be an integer")
814+
if value < 1:
815+
raise FullrepoError(f"{name} must be >= 1")
816+
return value
817+
818+
819+
def _positive_int(value: int, name: str) -> int:
820+
if value < 1:
821+
raise FullrepoError(f"{name} must be >= 1")
822+
return value
823+
824+
825+
def _env_float(name: str, default: float) -> float:
826+
raw = os.environ.get(name)
827+
if not raw:
828+
return default
829+
try:
830+
value = float(raw)
831+
except ValueError:
832+
raise FullrepoError(f"{name} must be a number")
833+
if value < 0:
834+
raise FullrepoError(f"{name} must be >= 0")
835+
return value
836+
837+
838+
def _non_negative_float(value: float, name: str) -> float:
839+
if value < 0:
840+
raise FullrepoError(f"{name} must be >= 0")
841+
return value
842+
843+
844+
def write_bootstrap_ci_receipt(path: Path, payload: dict[str, object]) -> None:
845+
path.parent.mkdir(parents=True, exist_ok=True)
846+
tmp = path.with_name(f".{path.name}.tmp")
847+
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
848+
os.replace(tmp, path)
849+
850+
851+
def bootstrap_ci(
852+
remote: str,
853+
branch: str,
854+
dry_run: bool = False,
855+
*,
856+
attempts: int | None = None,
857+
sleep_seconds: float | None = None,
858+
receipt_path: Path | None = None,
859+
ignore_project_policy: bool = False,
860+
) -> None:
861+
policy = _project_policy()
862+
enforce_fullrepo_policy(policy, "restore", ignore_project_policy=ignore_project_policy)
863+
if tracked_agent_paths_in_index():
864+
raise FullrepoError("CI bootstrap refuses to migrate source-tracked agent-only files")
865+
if _policy_value(policy, "install_exclude", True) or ignore_project_policy:
866+
install_exclude(dry_run=dry_run)
867+
868+
max_attempts = (
869+
_positive_int(attempts, "--bootstrap-attempts")
870+
if attempts is not None
871+
else _env_int("RLDYOUR_FULLREPO_BOOTSTRAP_CI_ATTEMPTS", BOOTSTRAP_CI_ATTEMPTS)
872+
)
873+
delay = (
874+
_non_negative_float(sleep_seconds, "--bootstrap-sleep-seconds")
875+
if sleep_seconds is not None
876+
else _env_float("RLDYOUR_FULLREPO_BOOTSTRAP_CI_SLEEP_SECONDS", BOOTSTRAP_CI_SLEEP_SECONDS)
877+
)
878+
receipt = receipt_path or (repo_root() / BOOTSTRAP_CI_RECEIPT)
879+
remote_ref = f"refs/remotes/{remote}/{branch}"
880+
source_head = commit_sha("HEAD")
881+
source_tree = ref_tree_sha(source_head) if source_head else ""
882+
errors: list[str] = []
883+
884+
for attempt in range(1, max_attempts + 1):
885+
fetched = fetch_fullrepo(remote, branch)
886+
if not fetched:
887+
errors.append(f"attempt {attempt}: fullrepo branch {remote}/{branch} does not exist or could not be fetched")
888+
else:
889+
try:
890+
resolved = resolve_fullrepo_ref(remote_ref, "HEAD")
891+
except FullrepoError as exc:
892+
errors.append(f"attempt {attempt}: {exc}")
893+
else:
894+
restored_paths = restore_resolved_overlay(
895+
resolved,
896+
label=f"{remote}/{branch}",
897+
dry_run=dry_run,
898+
)
899+
payload: dict[str, object] = {
900+
"schema_version": 1,
901+
"state": "PASS",
902+
"mode": "bootstrap-ci",
903+
"remote": remote,
904+
"fullrepo_branch": branch,
905+
"attempt": attempt,
906+
"attempts": max_attempts,
907+
"source_commit": source_head,
908+
"source_tree": source_tree,
909+
"resolved_fullrepo_commit": str(resolved.get("commit", "")),
910+
"resolution_mode": str(resolved.get("mode", "")),
911+
"agent_tree": str(resolved.get("agent_tree", "")),
912+
"restored_agent_paths": restored_paths,
913+
"restore_count": len(restored_paths),
914+
"dry_run": dry_run,
915+
"errors": errors,
916+
}
917+
write_bootstrap_ci_receipt(receipt, payload)
918+
print(json.dumps(payload, indent=2, sort_keys=True))
919+
return
920+
if attempt < max_attempts and delay > 0:
921+
print(
922+
f"fullrepo overlay for {source_head[:12]} not ready; retrying "
923+
f"({attempt}/{max_attempts}) after {delay:g}s",
924+
file=sys.stderr,
925+
)
926+
time.sleep(delay)
927+
928+
payload = {
929+
"schema_version": 1,
930+
"state": "FAIL",
931+
"mode": "bootstrap-ci",
932+
"remote": remote,
933+
"fullrepo_branch": branch,
934+
"attempts": max_attempts,
935+
"source_commit": source_head,
936+
"source_tree": source_tree,
937+
"dry_run": dry_run,
938+
"errors": errors,
939+
}
940+
write_bootstrap_ci_receipt(receipt, payload)
941+
raise FullrepoError("; ".join(errors) if errors else "CI bootstrap failed without diagnostic")
942+
943+
775944
def status(remote: str, branch: str, *, local_only: bool = False) -> dict[str, object]:
776945
root = repo_root()
777946
policy = _project_policy()
@@ -924,6 +1093,24 @@ def parse_args() -> argparse.Namespace:
9241093
actions.add_argument("--resolve-json", action="store_true")
9251094
actions.add_argument("--migrate-main", action="store_true")
9261095
actions.add_argument("--bootstrap-init", action="store_true")
1096+
actions.add_argument("--bootstrap-ci", action="store_true")
1097+
parser.add_argument(
1098+
"--bootstrap-attempts",
1099+
type=int,
1100+
default=None,
1101+
help="Maximum fetch/resolve attempts for --bootstrap-ci. Defaults to RLDYOUR_FULLREPO_BOOTSTRAP_CI_ATTEMPTS or 6.",
1102+
)
1103+
parser.add_argument(
1104+
"--bootstrap-sleep-seconds",
1105+
type=float,
1106+
default=None,
1107+
help="Delay between --bootstrap-ci attempts. Defaults to RLDYOUR_FULLREPO_BOOTSTRAP_CI_SLEEP_SECONDS or 10.",
1108+
)
1109+
parser.add_argument(
1110+
"--bootstrap-receipt",
1111+
default=BOOTSTRAP_CI_RECEIPT,
1112+
help=f"Receipt path for --bootstrap-ci, relative to the repository root by default ({BOOTSTRAP_CI_RECEIPT}).",
1113+
)
9271114
return parser.parse_args()
9281115

9291116

@@ -974,6 +1161,19 @@ def main() -> int:
9741161
create_missing=args.create_missing,
9751162
ignore_project_policy=args.ignore_project_policy,
9761163
)
1164+
elif args.bootstrap_ci:
1165+
receipt_path = Path(args.bootstrap_receipt)
1166+
if not receipt_path.is_absolute():
1167+
receipt_path = repo_root() / receipt_path
1168+
bootstrap_ci(
1169+
args.remote,
1170+
args.branch,
1171+
dry_run=args.dry_run,
1172+
attempts=args.bootstrap_attempts,
1173+
sleep_seconds=args.bootstrap_sleep_seconds,
1174+
receipt_path=receipt_path,
1175+
ignore_project_policy=args.ignore_project_policy,
1176+
)
9771177
except FullrepoError as exc:
9781178
print(str(exc), file=sys.stderr)
9791179
return 1

scripts/classify_ci_noise.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ class NoiseRule:
8484
NoiseRule(
8585
"chrome-devtools-update-advisory",
8686
re.compile(
87-
r"Update available: \d+(?:\.\d+){1,3} -> \d+(?:\.\d+){1,3}|"
88-
r"Run `npm install chrome-devtools-mcp@latest` to update\."
87+
r"^Update available: \d+(?:\.\d+){1,3} -> \d+(?:\.\d+){1,3}$|"
88+
r"^Run `npm install chrome-devtools-mcp@latest` to update\.$"
8989
),
9090
"Chrome DevTools MCP prints a non-blocking package update advisory to stderr.",
9191
),

0 commit comments

Comments
 (0)