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
119 changes: 69 additions & 50 deletions .conductor/registry/scripts/manifest-bootstrap.ps1
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#requires -Version 7.0
<#
.SYNOPSIS
Initialize the per-run manifest (`.polyphony/run.yaml`) for an apex run,
or validate an existing manifest matches the requested apex.
Initialize the per-run manifest under
`<git-common-dir>/polyphony/<root_id>/run.yaml` for an apex run, or
validate an existing manifest matches the requested apex.

.DESCRIPTION
Routing-style helper used by the `init_manifest` agent in
Expand All @@ -12,42 +13,48 @@ Routing-style helper used by the `init_manifest` agent in
workflow's `organization` and `project` inputs (the work-item
tracker is always ADO via twig today, regardless of which PR
platform the workflow targets), validates inputs are non-empty,
and calls `polyphony manifest init`.
and calls `polyphony manifest init --root-id N`.

2. **Manifest present** → calls `polyphony manifest read` and verifies
`root_id` matches the requested `-ApexId`. A mismatch is a fatal
resume error: someone is trying to start a fresh apex run inside a
working directory still locked to a previous root.
2. **Manifest present** → calls `polyphony manifest read --root-id N`,
which performs the AB#3067 root-mismatch guard internally. The
verb refuses to proceed when `manifest.root_id != --root-id` —
the carry-over case where someone resumes against the wrong root.

Probe is performed entirely through the CLI (`manifest read`) rather
than `Test-Path` on a known location: the manifest now lives at
`<git-common-dir>/polyphony/<root_id>/run.yaml`, which the CLI
derives via `git rev-parse --path-format=absolute --git-common-dir`.
Pre-resolving that path here would duplicate the resolver and risk
drift.

Always exits 0 and writes a single JSON envelope to stdout. Routing
in the workflow keys off `output.success`. Failure variants surface
via `error_code`:

invalid_inputs — organization or project missing.
manifest_read_failed — `polyphony manifest read` exited non-zero.
manifest_parse_failed — `polyphony manifest read` stdout wasn't JSON.
manifest_root_mismatch — manifest root_id != ApexId.
manifest_init_failed — `polyphony manifest init` exited non-zero.
manifest_init_parse_failed — `polyphony manifest init` stdout wasn't JSON.
polyphony_unavailable — polyphony not on PATH.
invalid_inputs - organization or project missing.
manifest_read_failed - `polyphony manifest read` exited non-zero
for a reason OTHER than `manifest_not_found`
or `manifest_root_mismatch`.
manifest_parse_failed - `polyphony manifest read` stdout wasn't JSON.
manifest_root_mismatch - manifest root_id != ApexId (AB#3067 guard).
manifest_init_failed - `polyphony manifest init` exited non-zero.
manifest_init_parse_failed - `polyphony manifest init` stdout wasn't JSON.
polyphony_unavailable - polyphony not on PATH.

.NOTES
Topology-hash drift on resume (manifest topology vs current ADO tree) is
intentionally NOT validated here. That is a deferred follow-up see
intentionally NOT validated here. That is a deferred follow-up - see
`docs/decisions/branch-model.md` for the resume contract. Tracked in
the apex-driver pipeline-audit-fix PR body.

.PARAMETER ApexId
ADO work-item id of the apex (run-root) being executed.

.PARAMETER Organization
ADO organization name. Required.
ADO organization name. Required when initialising a fresh manifest.

.PARAMETER Project
ADO project name. Required.

.PARAMETER ManifestPath
Path to the manifest file. Defaults to `.polyphony/run.yaml`.
ADO project name. Required when initialising a fresh manifest.

.PARAMETER PolyphonyExe
Override for the polyphony executable path. Defaults to `polyphony`.
Expand All @@ -58,7 +65,6 @@ param(

[string]$Organization = '',
[string]$Project = '',
[string]$ManifestPath = '.polyphony/run.yaml',
[string]$PolyphonyExe = 'polyphony'
)

Expand Down Expand Up @@ -87,7 +93,7 @@ function Invoke-Polyphony {
}
}

# ── Polyphony availability ──────────────────────────────────────────────
# -- Polyphony availability ---------------------------------------------
if (-not (Get-Command $PolyphonyExe -ErrorAction SilentlyContinue)) {
Emit-Envelope @{
success = $false
Expand All @@ -97,18 +103,10 @@ if (-not (Get-Command $PolyphonyExe -ErrorAction SilentlyContinue)) {
exit 0
}

# ── Existing manifest path ──────────────────────────────────────────────
if (Test-Path $ManifestPath) {
$read = Invoke-Polyphony @('manifest', 'read', '--path', $ManifestPath)
if ($read.Exit -ne 0) {
Emit-Envelope @{
success = $false
error_code = 'manifest_read_failed'
error = "polyphony manifest read exited $($read.Exit). stderr: $($read.Stderr) stdout: $($read.Stdout)"
}
exit 0
}
# -- Probe via CLI (the manifest now lives under the git common dir) ----
$read = Invoke-Polyphony @('manifest', 'read', '--root-id', "$ApexId")

if ($read.Exit -eq 0) {
try {
$manifest = $read.Stdout | ConvertFrom-Json
}
Expand All @@ -121,32 +119,53 @@ if (Test-Path $ManifestPath) {
exit 0
}

if ($manifest.manifest.root_id -ne $ApexId) {
Emit-Envelope @{
success = $false
error_code = 'manifest_root_mismatch'
error = "manifest root_id=$($manifest.manifest.root_id) does not match requested apex_id=$ApexId"
manifest_root_id = $manifest.manifest.root_id
apex_id = $ApexId
manifest_path = $ManifestPath
}
exit 0
}

# NOTE: topology-hash validation against the current ADO tree is
# deferred see docstring. For now, matching root_id is sufficient
# to consider the manifest reusable.
# deferred - see docstring. For now, a successful root-id-checked
# read is sufficient to consider the manifest reusable.
Emit-Envelope @{
success = $true
action = 'reused'
path = $ManifestPath
path = $manifest.manifest.path
root_id = $manifest.manifest.root_id
platform_project = $manifest.manifest.platform_project
}
exit 0
}

# ── Manifest absent — synthesize platform-project, then init ────────────
# Read failed - distinguish "first run, file absent" from other errors.
$readErrorCode = $null
$readPayload = $null
try {
$readPayload = $read.Stdout | ConvertFrom-Json
$readErrorCode = $readPayload.error_code
}
catch {
# Non-JSON stdout - fall through with $readErrorCode = $null.
}

if ($readErrorCode -eq 'manifest_root_mismatch') {
# AB#3067 carry-over guard - surface verbatim to the operator.
Emit-Envelope @{
success = $false
error_code = 'manifest_root_mismatch'
error = "$($readPayload.error)"
manifest_root_id = $readPayload.manifest_root_id
apex_id = $ApexId
}
exit 0
}

if ($readErrorCode -ne 'manifest_not_found') {
# Anything other than missing-file is a hard read failure.
Emit-Envelope @{
success = $false
error_code = 'manifest_read_failed'
error = "polyphony manifest read exited $($read.Exit) (error_code=$readErrorCode). stderr: $($read.Stderr) stdout: $($read.Stdout)"
}
exit 0
}

# -- Manifest absent - synthesize platform-project, then init -----------
# Work items always come from ADO via twig today; the `platform` workflow
# input refers to the PR target platform, not the work-item tracker, so
# it is not consulted here. If polyphony ever supports non-ADO trackers
Expand All @@ -161,7 +180,7 @@ if (-not $Organization -or -not $Project) {
}
$platformProject = "dev.azure.com/$Organization/$Project"

$init = Invoke-Polyphony @('manifest', 'init', '--root-id', "$ApexId", '--platform-project', $platformProject, '--path', $ManifestPath)
$init = Invoke-Polyphony @('manifest', 'init', '--root-id', "$ApexId", '--platform-project', $platformProject)
if ($init.Exit -ne 0) {
Emit-Envelope @{
success = $false
Expand Down
47 changes: 2 additions & 45 deletions .conductor/registry/workflows/apex-driver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@
# Post: .polyphony/run.yaml exists AND its root_id matches apex_id.
- name: init_manifest
type: script
description: Initialize or validate the per-run manifest (.polyphony/run.yaml)
description: Initialize or validate the per-run manifest under <git-common-dir>/polyphony/<root_id>/run.yaml
command: pwsh
args:
- "-NoProfile"
Expand All @@ -341,43 +341,9 @@
- "{{ workflow.input.organization }}"
- "-Project"
- "{{ workflow.input.project }}"
routes:
- to: commit_and_push_manifest
when: "{{ init_manifest.output.success | string | lower == 'true' }}"
- to: preflight_failure_gate
# M4 catch-all
- to: preflight_failure_gate

# ── Preflight: commit + push the manifest to feature/{apex_id} ───────────
#
# Closes the structural manifest-lifecycle gap surfaced by bug #12 (#179).
# `init_manifest` writes `.polyphony/run.yaml` to the worktree filesystem
# but never commits it. Downstream verbs that enforce the branch-model
# invariant ("manifest lives on origin/feature/{root}") — most notably
# `polyphony pr open-plan-pr` — read from the remote and would fail the
# entire run on a fresh apex without this step.
#
# The verb is idempotent: a no-op when the manifest already matches HEAD
# on feature/{apex_id}. Refuses to run on the wrong branch (encodes the
# ADR's "manifest must be committed on feature branch" rule at the verb
# boundary, not in YAML).
#
# Invariants:
# Pre: init_manifest succeeded; worktree on feature/{apex_id}.
# Post: origin/feature/{apex_id}:.polyphony/run.yaml exists and matches
# the local file.
- name: commit_and_push_manifest
type: script
description: Commit and push .polyphony/run.yaml to feature/{apex_id}
command: polyphony
args:
- "manifest"
- "commit-and-push"
- "--root-id"
- "{{ workflow.input.apex_id }}"
routes:
- to: declare_root
when: "{{ commit_and_push_manifest.output.error_code is not defined or commit_and_push_manifest.output.error_code == '' }}"
when: "{{ init_manifest.output.success | string | lower == 'true' }}"
- to: preflight_failure_gate
# M4 catch-all
- to: preflight_failure_gate
Expand Down Expand Up @@ -921,7 +887,7 @@

```
{%- if build_worklist.output.error is defined -%}
{{ build_worklist.output.error }}

Check warning on line 890 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'build_worklist.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if build_worklist.output.error is defined %}', '{% if build_worklist.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error message captured — check polyphony logs)
{%- endif -%}
Expand Down Expand Up @@ -957,7 +923,7 @@
### `polyphony state next-ready` error
```
{%- if preflight_apex_state is defined and preflight_apex_state.output is defined and preflight_apex_state.output.error is defined and preflight_apex_state.output.error != '' -%}
{{ preflight_apex_state.output.error }}

Check warning on line 926 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'preflight_apex_state.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if preflight_apex_state.output.error is defined %}', '{% if preflight_apex_state.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error — this step succeeded or did not run)
{%- endif -%}
Expand All @@ -966,7 +932,7 @@
### `polyphony branch ensure-feature` error
```
{%- if preflight_ensure_branch is defined and preflight_ensure_branch.output is defined and preflight_ensure_branch.output.error is defined and preflight_ensure_branch.output.error != '' -%}
{{ preflight_ensure_branch.output.error }}

Check warning on line 935 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'preflight_ensure_branch.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if preflight_ensure_branch.output.error is defined %}', '{% if preflight_ensure_branch.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error — this step succeeded or did not run)
{%- endif -%}
Expand All @@ -981,19 +947,10 @@
{%- endif -%}
```

### `commit_and_push_manifest` error
```
{%- if commit_and_push_manifest is defined and commit_and_push_manifest.output is defined and commit_and_push_manifest.output.error is defined and commit_and_push_manifest.output.error != '' -%}
[{{ commit_and_push_manifest.output.error_code | default('?') }}] {{ commit_and_push_manifest.output.error }}
{%- else -%}
(no error — this step succeeded or did not run)
{%- endif -%}
```

### `declare_root` error
```
{%- if declare_root is defined and declare_root.output is defined and declare_root.output.error is defined and declare_root.output.error != '' -%}
{{ declare_root.output.error }}

Check warning on line 953 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'declare_root.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if declare_root.output.error is defined %}', '{% if declare_root.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error — this step succeeded or did not run)
{%- endif -%}
Expand Down
11 changes: 2 additions & 9 deletions .conductor/registry/workflows/cascade-remedy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@
type: number
required: true
description: Run-root work item id whose descendant tree to scan.
manifest_path:
type: string
required: false
default: ".polyphony/run.yaml"
description: Manifest path inside origin/feature/{root_id}.

# Workflow-scope output: bare Jinja template strings (M7). Auto-coerced
# by _maybe_parse_json — numeric strings become ints, lowercase
Expand All @@ -73,8 +68,6 @@
- "classify-stale-descendants"
- "--root-id"
- "{{ workflow.input.root_id }}"
- "--manifest-path"
- "{{ workflow.input.manifest_path }}"
routes:
# Verb-level error (manifest read/parse, repo slug, etc.) — surface
# to the cascade-error gate so the operator can decide.
Expand All @@ -95,11 +88,11 @@
`polyphony plan classify-stale-descendants` could not enumerate
stale descendants for root **{{ workflow.input.root_id }}**.

- **Error code:** `{{ classify.output.error_code }}`

Check warning on line 91 in .conductor/registry/workflows/cascade-remedy.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'classify.output.error_code' has can_omit_when_null=true and is not guarded. Wrap in '{% if classify.output.error_code is defined %}', '{% if classify.output is defined %}', or pipe through '| default(...)'.
- **Error:** {{ classify.output.error | default('(none)') }}

Common causes: manifest missing or malformed at
`origin/feature/{{ workflow.input.root_id }}:{{ workflow.input.manifest_path }}`,
Common causes: manifest missing or malformed in the per-root state
directory (`<git-common-dir>/polyphony/{{ workflow.input.root_id }}/run.yaml`),
repo slug not resolvable from `origin`, twig cache stale.

---
Expand Down
Loading
Loading