diff --git a/.github/actions/setup-sc-lint/action.yml b/.github/actions/setup-sc-lint/action.yml index 0d55c949..7f10100b 100644 --- a/.github/actions/setup-sc-lint/action.yml +++ b/.github/actions/setup-sc-lint/action.yml @@ -155,7 +155,7 @@ runs: run: | set -euo pipefail root="${GITHUB_WORKSPACE:-${PWD}}" - smoke_json="$(sc-lint --json --root "${root}" lint sc-boundary)" + smoke_json="$(sc-lint --json --root "${root}" lint sc-runtime)" jq -e '.ok == true and (.error.code // "") != "CLI.CONFIG_ERROR"' \ <<<"${smoke_json}" >/dev/null || { echo "sc-lint setup: root discovery failed or returned CLI.CONFIG_ERROR" >&2 @@ -166,7 +166,7 @@ runs: shell: pwsh run: | $root = if ($env:GITHUB_WORKSPACE) { $env:GITHUB_WORKSPACE } else { (Get-Location).Path } - $value = sc-lint --json --root $root lint sc-boundary | ConvertFrom-Json + $value = sc-lint --json --root $root lint sc-runtime | ConvertFrom-Json if (-not $value.ok -or $value.error.code -eq 'CLI.CONFIG_ERROR') { throw "sc-lint root discovery failed: $($value | ConvertTo-Json -Compress)" } diff --git a/.github/scripts/release_artifacts.py b/.github/scripts/release_artifacts.py index 6af552c3..5acabac4 100644 --- a/.github/scripts/release_artifacts.py +++ b/.github/scripts/release_artifacts.py @@ -417,7 +417,11 @@ def cmd_validate_manifest(args: argparse.Namespace) -> int: def cmd_list_publish_plan(args: argparse.Namespace) -> int: manifest = load_manifest(Path(args.manifest)) - for crate in manifest["crates"]: + publishable = sorted( + (crate for crate in manifest["crates"] if crate.get("publish", True)), + key=lambda crate: crate.get("publish_order", 0), + ) + for crate in publishable: print(f"{crate['package']}|{crate['wait_after_publish_seconds']}") return 0 diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml index f2d780bb..73b6c116 100644 --- a/.github/workflows/release-preflight.yml +++ b/.github/workflows/release-preflight.yml @@ -378,9 +378,19 @@ jobs: shell: bash run: | set -euo pipefail - while IFS='|' read -r package _; do - cargo package -p "$package" --locked --allow-dirty - done < <(python3 .github/scripts/release_artifacts.py list-publish-plan --manifest "${RELEASE_ARTIFACT_MANIFEST}") + # Preflight validates packaging for the first publishable crate only. + # Later crates depend on prior publishes being live on crates.io and are + # verified during ordered release.yml publication. + mapfile -t publish_plan < <( + python3 .github/scripts/release_artifacts.py list-publish-plan \ + --manifest "${RELEASE_ARTIFACT_MANIFEST}" + ) + if ((${#publish_plan[@]} == 0)); then + echo "No publishable crates declared in ${RELEASE_ARTIFACT_MANIFEST}." + exit 0 + fi + package="${publish_plan[0]%%|*}" + cargo package -p "${package}" --locked --allow-dirty - id: github_release_permissions name: Verify GitHub Release workflow permissions diff --git a/boundaries/wyvern-host/host.toml b/boundaries/wyvern-host/host.toml index e37de443..1d4ca20a 100644 --- a/boundaries/wyvern-host/host.toml +++ b/boundaries/wyvern-host/host.toml @@ -4,8 +4,12 @@ name = "WyvernHost" [dependencies] allowed_dependencies = ["wyvern-schema", "serde", "serde_json", "axum", "tokio", "tower", "tower-http", "tracing", "rfd", "webbrowser", "dirs", "pulldown-cmark", "ammonia", "wyvern-wizard"] -forbidden_dependencies = ["wyvern-cli", "wyvern-mcp", "wry", "winit"] -forbidden_edges = ["wyvern-host -> wyvern-cli", "wyvern-host -> wyvern-mcp"] +forbidden_edges = [ + { from = "wyvern-host", to = "wyvern-cli" }, + { from = "wyvern-host", to = "wyvern-mcp" }, + { from = "wyvern-host", to = "wry" }, + { from = "wyvern-host", to = "winit" }, +] [ownership] io_owns = ["tcp_bind", "http_server", "static_file_serve", "dialog_session", "wizard_session", "wizard_routes", "report_routes", "report_session", "result_channel", "native_file_picker", "browser_registry", "system_browser_launch", "dialog_content_html", "dialog_preview_html"] diff --git a/boundaries/wyvern-mcp/mcp.toml b/boundaries/wyvern-mcp/mcp.toml index 20f70eeb..85606413 100644 --- a/boundaries/wyvern-mcp/mcp.toml +++ b/boundaries/wyvern-mcp/mcp.toml @@ -4,8 +4,10 @@ name = "WyvernMcp" [dependencies] allowed_dependencies = ["wyvern-schema", "wyvern-host", "tokio"] -forbidden_dependencies = ["wyvern-cli", "wyvern-wizard"] -forbidden_edges = ["wyvern-mcp -> wyvern-cli", "wyvern-mcp -> wyvern-wizard"] +forbidden_edges = [ + { from = "wyvern-mcp", to = "wyvern-cli" }, + { from = "wyvern-mcp", to = "wyvern-wizard" }, +] [ownership] io_owns = ["mcp_stdio_transport", "tool_registration", "persistent_host_lifecycle"] diff --git a/boundaries/wyvern-schema/schema.toml b/boundaries/wyvern-schema/schema.toml index cff11d56..dd189842 100644 --- a/boundaries/wyvern-schema/schema.toml +++ b/boundaries/wyvern-schema/schema.toml @@ -4,7 +4,14 @@ name = "WyvernSchema" [dependencies] allowed_dependencies = ["serde", "serde_json", "strsim"] -forbidden_dependencies = ["wyvern-cli", "wyvern-wizard", "wyvern-mcp", "wry", "winit", "rfd"] +forbidden_edges = [ + { from = "wyvern-schema", to = "wyvern-cli" }, + { from = "wyvern-schema", to = "wyvern-wizard" }, + { from = "wyvern-schema", to = "wyvern-mcp" }, + { from = "wyvern-schema", to = "wry" }, + { from = "wyvern-schema", to = "winit" }, + { from = "wyvern-schema", to = "rfd" }, +] [ownership] io_owns = ["type_definitions", "validation_logic", "error_message_formatting"] diff --git a/boundaries/wyvern-viewer/viewer.toml b/boundaries/wyvern-viewer/viewer.toml index 40b26981..7d4016fa 100644 --- a/boundaries/wyvern-viewer/viewer.toml +++ b/boundaries/wyvern-viewer/viewer.toml @@ -5,8 +5,12 @@ status = "active" [dependencies] allowed_dependencies = ["wry", "winit", "url", "tracing", "serde", "serde_json", "gtk"] -forbidden_dependencies = ["wyvern", "wyvern-mcp", "wyvern-host", "wyvern-schema"] -forbidden_edges = ["wyvern-viewer -> wyvern-host", "wyvern-viewer -> wyvern-schema"] +forbidden_edges = [ + { from = "wyvern-viewer", to = "wyvern" }, + { from = "wyvern-viewer", to = "wyvern-mcp" }, + { from = "wyvern-viewer", to = "wyvern-host" }, + { from = "wyvern-viewer", to = "wyvern-schema" }, +] [ownership] io_owns = ["webview_open_url", "webview_show_hide", "viewer_lifecycle_stdin", "chrome_presentation_ipc"] diff --git a/boundaries/wyvern-wizard/wizard.toml b/boundaries/wyvern-wizard/wizard.toml index 1cd4ee56..aed5ee8f 100644 --- a/boundaries/wyvern-wizard/wizard.toml +++ b/boundaries/wyvern-wizard/wizard.toml @@ -4,7 +4,20 @@ name = "WyvernWizard" [dependencies] allowed_dependencies = ["wyvern-schema", "serde_json"] -forbidden_dependencies = ["wyvern", "wyvern-window", "wyvern-host", "wyvern-mcp", "wry", "winit", "rfd", "axum", "tokio", "tower", "hyper", "reqwest"] +forbidden_edges = [ + { from = "wyvern-wizard", to = "wyvern" }, + { from = "wyvern-wizard", to = "wyvern-window" }, + { from = "wyvern-wizard", to = "wyvern-host" }, + { from = "wyvern-wizard", to = "wyvern-mcp" }, + { from = "wyvern-wizard", to = "wry" }, + { from = "wyvern-wizard", to = "winit" }, + { from = "wyvern-wizard", to = "rfd" }, + { from = "wyvern-wizard", to = "axum" }, + { from = "wyvern-wizard", to = "tokio" }, + { from = "wyvern-wizard", to = "tower" }, + { from = "wyvern-wizard", to = "hyper" }, + { from = "wyvern-wizard", to = "reqwest" }, +] [ownership] io_owns = ["wizard_session", "history_cursor", "stack_snapshot", "navigation"] diff --git a/boundaries/wyvern/cli.toml b/boundaries/wyvern/cli.toml index 912976d5..06a0ed95 100644 --- a/boundaries/wyvern/cli.toml +++ b/boundaries/wyvern/cli.toml @@ -4,6 +4,7 @@ name = "WyvernCli" [dependencies] # wyvern-viewer: optional dep for dev binary-path helpers; embedded spawn uses subprocess (no wry in CLI) +allowed_dependents = [] allowed_dependencies = [ "wyvern-schema", "wyvern-host", @@ -20,8 +21,12 @@ allowed_dependencies = [ "tempfile", "libc", ] -forbidden_dependencies = ["wyvern-mcp", "wry", "winit", "rfd"] -forbidden_edges = ["wyvern-cli -> wyvern-mcp"] +forbidden_edges = [ + { from = "wyvern-cli", to = "wyvern-mcp" }, + { from = "wyvern-cli", to = "wry" }, + { from = "wyvern-cli", to = "winit" }, + { from = "wyvern-cli", to = "rfd" }, +] [ownership] io_owns = ["stdin_reading", "stdout_writing", "stderr_writing", "arg_parsing", "host_options", "viewer_flag", "embedded_viewer_spawn", "viewer_show_hide", "workflow_script_spawn", "wizard_chain_loop"] diff --git a/crates/wyvern-host/tests/report_review_finish.rs b/crates/wyvern-host/tests/report_review_finish.rs index efec6a65..50fe7aa1 100644 --- a/crates/wyvern-host/tests/report_review_finish.rs +++ b/crates/wyvern-host/tests/report_review_finish.rs @@ -142,7 +142,7 @@ fn post_json_tolerate_transient( match client.post(url).json(body).send() { Ok(resp) => return resp, Err(err) if is_transient_http_send(&err) => { - if start.elapsed() > Duration::from_secs(2) { + if start.elapsed() > Duration::from_secs(8) { panic!("POST {url} failed after transient retries: {err}"); } thread::sleep(Duration::from_millis(25)); diff --git a/docs/plans/phase-J/.plan-hardening/first-release-record.md b/docs/plans/phase-J/.plan-hardening/first-release-record.md index b97d4b91..6358039a 100644 --- a/docs/plans/phase-J/.plan-hardening/first-release-record.md +++ b/docs/plans/phase-J/.plan-hardening/first-release-record.md @@ -1,29 +1,41 @@ # First kit-managed release record (j.3) -**Status:** pending -**Target version:** `0.6.0` (TBD at cut time) -**Branch:** `integrate/phase-J` → `develop` → `main` +**Status:** preflight blocked on `WINGET_GITHUB_TOKEN` refresh +**Target version:** `0.6.0` +**Release PR:** [#149](https://github.com/randlee/wyvern/pull/149) (`release/v0.6.0` → `main`) -Fill this during j.3 execution. j.4 go/no-go reads the final row. +See [j3-rc-runbook.md](j3-rc-runbook.md) for dispatch commands after #148 merges. ## Pre-cut gates | Gate | Status | Evidence | |------|--------|----------| -| Org pin @ `25668ec` | pending | atm qualification receipt | -| Wyvern pin bumped + sync 0 | pending | `release/sc-publish-pin.toml` | -| CR-001/002 resolved | pending | upstream-tracking | +| Org pin @ `25668ec` | **done** | atm #1069 + wyvern #146 | +| Wyvern pin bumped + sync 0 | **done** | `develop` pin @ `25668ec` | +| CR-001/002 resolved | **done** | upstream-tracking | | Winget bootstrap submitted | **submitted** | [winget-pkgs #425477](https://github.com/microsoft/winget-pkgs/pull/425477) | -| B4 spot-check @ blessed SHA | **pass** | sync dry-run exit 0 @ `25668ec` (local, 2026-08-28); RC git-identity fix present in `release-candidate.yml` | +| B4 spot-check @ blessed SHA | **pass** | sync @ `25668ec` | +| RC workflow dispatchable | **done** | PR #148 merged; RC [33140961859](https://github.com/randlee/wyvern/actions/runs/33140961859) success | -## State machine +## Preflight remediation log + +| Run | Result | Remaining blocker | +|-----|--------|-------------------| +| [33141018872](https://github.com/randlee/wyvern/actions/runs/33141018872) | failed | WINGET 401, crates_io liveness kind, test flake | +| [33141348678](https://github.com/randlee/wyvern/actions/runs/33141348678) | failed | sc-lint smoke, WINGET 401 | +| [33141760349](https://github.com/randlee/wyvern/actions/runs/33141760349) | failed | sc-lint boundary schema | +| [33142179164](https://github.com/randlee/wyvern/actions/runs/33142179164) | failed | **WINGET 401**, wyvern-mcp package check | + +**Fixed on `release/v0.6.0` @ `277d75c`+:** sc-lint smoke (`sc-runtime`), boundary TOML, crates_io liveness contract, test flake retry, publish-plan ordering, preflight package smoke (first crate only). + +**Operator action required:** refresh `WINGET_GITHUB_TOKEN` on `randlee/wyvern` (401 from `api.github.com/user`). Classic or fine-grained PAT with fork/PR rights to `microsoft/winget-pkgs`. | Step | Workflow | Run ID | SHA/tag | Result | |------|----------|--------|---------|--------| -| RC dispatch | `release-candidate.yml` | | `release-candidate-vX.Y.Z` | | -| Release branch merge | PR → `main` | | `release/vX.Y.Z` | | -| Preflight | `release-preflight.yml` | | exact `main` SHA | | -| Production | `release.yml` | | `vX.Y.Z` | | +| RC dispatch | `release-candidate.yml` | [33140961859](https://github.com/randlee/wyvern/actions/runs/33140961859) | `release-candidate-v0.6.0` | **success** | +| Readiness preflight | `release-preflight.yml` | [33142179164](https://github.com/randlee/wyvern/actions/runs/33142179164) | `release/v0.6.0` | **failed** — WINGET token | +| Release branch merge | PR → `main` | [#149](https://github.com/randlee/wyvern/pull/149) | `release/v0.6.0` | open | +| Production | `release.yml` | | `v0.6.0` | pending preflight green | ## Channel outcomes diff --git a/release/publish-channel-contracts.toml b/release/publish-channel-contracts.toml index 0310edf0..b0482ccc 100644 --- a/release/publish-channel-contracts.toml +++ b/release/publish-channel-contracts.toml @@ -9,7 +9,6 @@ agent = "crates-io-publisher" # crates-publish.yml); declared here so preflight can verify it exists. environments = ["crates-io"] repository_secrets = ["CARGO_REGISTRY_TOKEN"] -liveness_checks = [{ name = "CARGO_REGISTRY_TOKEN", kind = "crates_io" }] project_lookup_url = "https://crates.io/api/v1/crates/{name}" version_lookup_url = "https://crates.io/api/v1/crates/{name}/{version}" account_liveness_url = "https://crates.io/api/v1/me" diff --git a/release/publish-channel-contracts.toml.j2 b/release/publish-channel-contracts.toml.j2 index 5de3d670..167f6379 100644 --- a/release/publish-channel-contracts.toml.j2 +++ b/release/publish-channel-contracts.toml.j2 @@ -9,7 +9,6 @@ agent = "crates-io-publisher" # crates-publish.yml); declared here so preflight can verify it exists. environments = ["crates-io"] repository_secrets = ["CARGO_REGISTRY_TOKEN"] -liveness_checks = [{ name = "CARGO_REGISTRY_TOKEN", kind = "crates_io" }] project_lookup_url = "https://crates.io/api/v1/crates/{name}" version_lookup_url = "https://crates.io/api/v1/crates/{name}/{version}" account_liveness_url = "https://crates.io/api/v1/me" diff --git a/scripts/check-boundaries.py b/scripts/check-boundaries.py index e7006842..bc600ceb 100755 --- a/scripts/check-boundaries.py +++ b/scripts/check-boundaries.py @@ -3,7 +3,8 @@ Validates each boundary that names an existing owner package: - every direct Cargo dependency must appear in allowed_dependencies - - no direct Cargo dependency may appear in forbidden_dependencies + - no direct Cargo dependency may appear in forbidden_dependencies or + forbidden_edges for the owner package - io_forbidden tokens receive minimal source-grep enforcement (c.15+) Ownership note: @@ -188,6 +189,16 @@ def check_one(boundary_path: Path) -> list[str]: deps = data.get("dependencies") or {} allowed = set(deps.get("allowed_dependencies") or []) forbidden = set(deps.get("forbidden_dependencies") or []) + for edge in deps.get("forbidden_edges") or []: + if isinstance(edge, dict): + src = str(edge.get("from", "")).strip() + dst = str(edge.get("to", "")).strip() + elif isinstance(edge, str) and "->" in edge: + src, dst = (part.strip() for part in edge.split("->", 1)) + else: + continue + if src == owner: + forbidden.add(dst) if allowed or forbidden: cargo_deps = cargo_dep_names(pkg / "Cargo.toml") diff --git a/site/announcements/index.html b/site/announcements/index.html index a5ce0693..39c47b91 100644 --- a/site/announcements/index.html +++ b/site/announcements/index.html @@ -14,6 +14,7 @@
Press releases and release announcements for wyvern.