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
2 changes: 1 addition & 1 deletion .github/workflows/boatstack-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
if: github.repository == 'operatorstack/intelligence-flow'
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/interlock-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
if: github.repository == 'operatorstack/intelligence-flow'
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pitot-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
if: github.repository == 'operatorstack/intelligence-flow'
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/value-map-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
if: github.repository == 'operatorstack/intelligence-flow'
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
21 changes: 21 additions & 0 deletions labkit/src/labkit/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ def _fetch_remote_workflow(target_slug: str, name: str) -> Optional[str]:
return None


def _fetch_remote_json(endpoint: str) -> Optional[object]:
"""Fetch one GitHub API document via gh, returning None on access/error."""
result = subprocess.run(["gh", "api", endpoint], capture_output=True, text=True)
if result.returncode != 0:
return None
try:
return json.loads(result.stdout)
except ValueError:
return None


def check_remote(config: PublishConfig) -> list[str]:
"""Diff the live public control planes against the generated canonical copies."""
problems: list[str] = []
Expand All @@ -85,6 +96,16 @@ def check_remote(config: PublishConfig) -> list[str]:
problems.append(f"{config.target_slug}: could not read live verify.yml (not installed?)")
elif live_verify != render_verify_automerge(config):
problems.append(f"{config.target_slug}: live verify.yml differs from the generated copy")
settings = _fetch_remote_json(f"repos/{config.target_slug}")
if not isinstance(settings, dict) or settings.get("allow_auto_merge") is not True:
problems.append(f"{config.target_slug}: native auto-merge is not enabled")
protection = _fetch_remote_json(
f"repos/{config.target_slug}/branches/main/protection/required_status_checks"
)
contexts = protection.get("contexts") if isinstance(protection, dict) else None
checks = protection.get("checks") if isinstance(protection, dict) else None
if not contexts and not checks:
problems.append(f"{config.target_slug}: main has no required status checks")
return problems


Expand Down
4 changes: 2 additions & 2 deletions labkit/src/labkit/gen_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def render_publish_dispatcher(config: PublishConfig) -> str:
if: github.repository == '{config.source.repo}'
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down Expand Up @@ -185,7 +185,7 @@ def render_sync_upstream(config: PublishConfig) -> str:
sync:
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
16 changes: 14 additions & 2 deletions labkit/src/labkit/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,27 @@ def _legacy_manifest_equivalent(repo: Path, projection: Projection, value: dict)
if set(value) != {"schema_version", "files"} or value.get("schema_version") != 1:
return False
owned = value.get("files")
if not isinstance(owned, dict) or set(owned) != set(projection.files):
if not isinstance(owned, dict):
return False
# Existing projected destinations must either be absent (safe creation) or
# be owned by the legacy manifest with bytes equal to the new projection.
for relative, content in projection.files.items():
expected = sha256_bytes(content)
target = repo / relative
if owned.get(relative) != expected or not target.is_file():
if not target.exists():
continue
if not target.is_file() or owned.get(relative) != expected:
return False
if sha256_bytes(target.read_bytes()) != expected:
return False
# A legacy-owned path removed by the new projection may be deleted only
# while its bytes still match the ownership proof.
for relative, old_hash in owned.items():
if relative in projection.files:
continue
target = repo / relative
if target.exists() and (not target.is_file() or sha256_bytes(target.read_bytes()) != old_hash):
return False
return True


Expand Down
22 changes: 21 additions & 1 deletion labkit/tests/test_labkit_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from labkit import load_config
from labkit.config import PublishConfig
from labkit.doctor import check_config, check_projection, main
from labkit.doctor import check_config, check_projection, check_remote, main
from labkit.gen_workflows import generated_artifacts
from labkit.project import build_projection, write_repo

Expand Down Expand Up @@ -121,3 +121,23 @@ def test_check_projection_flags_stale_source_commit(tmp_path):
def test_doctor_repo_requires_project(tmp_path):
(tmp_path / "labs").mkdir()
assert main(["--repo-root", str(tmp_path), "--repo", str(tmp_path)]) == 1


def test_remote_requires_native_auto_merge_and_protected_checks(monkeypatch):
config = load_config(REPO_ROOT / "labs/14-product-value-projection/publish.config.json")
canonical = generated_artifacts(config, REPO_ROOT)
sync = next(content for path, content in canonical.items() if path.name == "sync-upstream.yml")

monkeypatch.setattr("labkit.doctor._fetch_remote_workflow", lambda *_args: sync)
monkeypatch.setattr(
"labkit.doctor._fetch_remote_json",
lambda endpoint: (
{"allow_auto_merge": False}
if endpoint == f"repos/{config.target_slug}"
else {"contexts": [], "checks": []}
),
)

problems = check_remote(config)
assert f"{config.target_slug}: native auto-merge is not enabled" in problems
assert f"{config.target_slug}: main has no required status checks" in problems
47 changes: 47 additions & 0 deletions labkit/tests/test_labkit_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,53 @@ def test_equivalent_legacy_surface_manifest_is_adopted_automatically(tmp_path):
assert adopted["source"]["repository"] == "operatorstack/intelligence-flow"


def test_equivalent_legacy_manifest_allows_absent_new_projection_files(tmp_path):
lab = _make_lab(tmp_path)
(lab / "NEW.md").write_text("new projected file\n")
config = _projection_config(
[
{"kind": "copy", "source": "README.md", "dest": "README.md"},
{"kind": "copy", "source": "NEW.md", "dest": "NEW.md"},
],
generator="demo:gen",
)
proj = build_projection(config, lab, "a" * 40)
repo = tmp_path / "public"
repo.mkdir()
(repo / "README.md").write_bytes(proj.files["README.md"])
(repo / "UPSTREAM.json").write_text(json.dumps({
"schema_version": 1,
"files": {"README.md": sha256_bytes(proj.files["README.md"])},
}))

assert collisions(repo, proj, adopt=False) == []
assert write_repo(repo, proj, adopt=False) == 0
assert (repo / "NEW.md").read_bytes() == proj.files["NEW.md"]


def test_legacy_manifest_growth_fails_closed_on_unowned_destination(tmp_path):
lab = _make_lab(tmp_path)
(lab / "NEW.md").write_text("new projected file\n")
config = _projection_config(
[
{"kind": "copy", "source": "README.md", "dest": "README.md"},
{"kind": "copy", "source": "NEW.md", "dest": "NEW.md"},
],
generator="demo:gen",
)
proj = build_projection(config, lab, "a" * 40)
repo = tmp_path / "public"
repo.mkdir()
(repo / "README.md").write_bytes(proj.files["README.md"])
(repo / "NEW.md").write_text("downstream-owned\n")
(repo / "UPSTREAM.json").write_text(json.dumps({
"schema_version": 1,
"files": {"README.md": sha256_bytes(proj.files["README.md"])},
}))

assert sorted(collisions(repo, proj, adopt=False)) == ["NEW.md", "UPSTREAM.json"]


def test_legacy_manifest_adoption_fails_closed_on_any_hash_mismatch(tmp_path):
lab = _make_lab(tmp_path)
config = _projection_config([{"kind": "copy", "source": "README.md", "dest": "README.md"}],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Native auto-merge policy is now conformance-checked

Boatstack's publisher workflow identifies the Operator Stack Publisher
explicitly, and fleet doctor checks now fail when native auto-merge or required
status checks are missing from the live repository.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion labs/15-pitot/.labkit/generated/sync-upstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Legacy projection growth can now be adopted safely

Pitot can migrate an equivalent legacy manifest when newly projected
destinations are absent. Existing and stale paths remain protected by exact
hash proofs, so downstream-owned or modified files still fail closed.
2 changes: 1 addition & 1 deletion labs/21-interlock/.labkit/generated/sync-upstream.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Create publisher token
- name: Create Operator Stack Publisher token
id: app-token
uses: actions/create-github-app-token@v3
with:
Expand Down
Loading