Skip to content

ci: never block a core merge; one promotion gate arms the fleet - #5824

Merged
rbuergi merged 3 commits into
mainfrom
ci/never-block-core-merge
Sep 28, 2026
Merged

rbuergi merged 3 commits into
mainfrom
ci/never-block-core-merge

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Implements the maintainer's 2026-09-27 decisions "never block a core merge" and "can't we fast forward and always build latest?" (answered yes), as four registered policies — core-merge-never-blocked, build-latest-green, one-promotion-gate, paired-change-sets (+ content-neutral-push-keeps-run, whose mechanism is in Plugins). dependent-suites-gate is retired. Design of record: Doc/Architecture/OnePromotionGate (new) and Doc/Architecture/PairedChangeSets (new). Measurements: Systemorph/Memex docs/build-bottlenecks-2026-09-27.md. Plugins half: Systemorph/MeshWeaver.Plugins#2461. Refs #5801, #5807.

Why (measured)

#5807 was green at 13:21Z (run 36320927353); its merge-queue entries failed at 15:25, 16:00 and 16:43 (runs 36325608971, 36327871720, 36330515253), each on Dependent suites (MeshWeaver.Plugins) — "did not answer within 42 min … Silence is not a pass". It merged at 17:51 only after the queue was switched off: 4 h 30 min green-to-merged, blocked by another repository's runner capacity.

Before → after

before after
Required checks on main (ruleset 2128472) Consolidate test results + Automatic review answered; Consolidate needed preflight, dependent-suites-dispatch, dependent-suites and failed on a Plugins silence same two contexts; Consolidate needs nothing cross-repo. No ruleset change needed — measured: no merge_queue rule, no cross-repo context
Dependent suites on a PR on every merge-queue entry (blocking) + label ADVISORY: label dependent-suites, or a declared Pairs-with: Systemorph/MeshWeaver.Plugins#<n> → run against the core candidate and that Plugins PR's head (paired change set tested together)
promote phases A–D: identity/pair tags, CI pointers, the portal's <version> (the self-updater's roll target) and 3.0.0-latest phases A+B only, plus a promotion record artifact (both full commits, base, versions, verdict key pair-<core7>-p<plugins7>)
Arming the fleet in promote, unconditionally new arm job, every CD run: the newest promoted set, newer than every armed one, whose Plugins dependent-suites verdict for exactly its pair is green (arm-promoted-set.py). Waits on silence, refuses red, newer green supersedes. Override: dispatch arm_override + arm_reason
Release event (notify-platform-update) after promote; a leg of delivery-verdict after arm (only for an armed set), stamps coreSha/pluginsSha; NOT a leg of delivery-verdict (platform delivery never waits on Plugins)
CI resolution newest sealed set unchanged mechanism = the newest green core main build; an unarmed set resolves by the portal's identity tag (new self-test cases: latest-green, red-main fallback)

Also: gate's bake-only version recovery reads memex-migration (phase A) instead of the portal's arming tag; check-image-set.sh --pointers no longer asserts the portal's <version>.

Verification

  • MeshWeaver.Documentation.Test (Release, -warnaserror): 650/650 pass (guards updated: PlatformReleaseNotifyGuard, PlatformDeliveryNeverWaitsOnPluginsGuard + new TheArmingIsNeverAPlatformDeliveryLeg, PlatformNeverDependsOnPluginsGuard ApiReadLedger, PlatformBakeLaneGuard).
  • arm-promoted-set.py --self-test (20 cases), resolve-platform.py --self-test (194), test-check-image-set.py (9), test-cd-steps.py, await-dependent-verdict.py --self-test, workflow lints (timeouts, permission pairing, PR secret preflight, yaml keys, guard-step masking).

Precondition — met

Systemorph/MeshWeaver.Plugins#2461 is merged (2026-09-27 22:56Z), so the verdicts arming reads now exist.

Adopted — what was added since (2026-09-28)

I adopted this PR from the idle author session under a coordinator assignment. The additions:

  • The bundle's base is the newest ARMED set, never the first parent (was ci(promotion): the bundle's base is the newest armed set, never the first parent (stacked on #5824) #5848, now closed).
    • arm-promoted-set.py armed-base reads the armed set from memex-portal-ai's manifests.
    • promote records that base with a base_kind: armed, first-parent (nothing armed yet, or this commit IS the armed set) or unresolved.
    • An unresolved set is still promoted for CI, but it is never armed and never measured.
    • I dry-ran the record step against live ACR and GitHub, and all three branches came out correct.
  • The review's real defects are fixed.
    • packages: write added on arm.
    • V_PLUGIN is exported, and every selected field is asserted non-empty before any write.
    • arm is serialised across runs (job concurrency main-cd-arm).
    • Arming is complete only when the whole sequence has landed: marker arm-complete-<version>, armed-state, select --resume.
    • notify re-checks never-backwards before it posts.
    • --override matches the exact version.
    • A paired Plugins FORK head is refused (paired-plugins-head.py, self-tested in workflow-shell).
    • ContinuousDeliveryContract.md is reconciled with OnePromotionGate.md.
    • Policy dates in prose are replaced by the policy id.
  • All 17 review threads are answered on their threads.
  • Verified:
    • MeshWeaver.Documentation.Test passes 650/650 (Release, -warnaserror).
    • test-cd-steps.py and the self-tests for arm-promoted-set.py and paired-plugins-head.py pass.
    • The workflow guards (timeouts, yaml keys, guard-step masking, PR secret preflight, permission pairing) report 0 violations.
    • actionlint findings on main-cd.yml are byte-identical to before; dotnet-test.yml is clean.

Policies core-merge-never-blocked, build-latest-green, one-promotion-gate,
paired-change-sets (register: Doc/Architecture/PolicyNotProse).

- dotnet-test.yml: the dependent-suites run is ADVISORY. `Consolidate test
  results` no longer needs preflight / dependent-suites-dispatch /
  dependent-suites, and the step that failed on a Plugins silence is gone.
  The run is requested by the `dependent-suites` label or a declared
  `Pairs-with: Systemorph/MeshWeaver.Plugins#<n>` (then against that PR's
  head — a paired change set tested together). merge_group no longer runs it
  (the queue is off). MeshWeaver#5807 sat 4 h green and was ejected on a
  42-minute silence; that cannot recur.
- main-cd.yml: `promote` keeps phases A+B (CI-facing) and uploads a
  promotion record (both commits, base, versions, verdict key). Phases C+D
  (the portal's <version>, the line pointers) and the release event move to
  a new `arm` job that runs in every CD run and arms the newest promoted set,
  newer than every armed one, whose MeshWeaver.Plugins dependent-suites
  verdict for exactly its pair is green (arm-promoted-set.py). Override:
  dispatch input arm_override + arm_reason. notify-platform-update follows
  `arm` and stamps coreSha/pluginsSha into the build fact. delivery-verdict
  no longer needs notify (platform delivery never waits on Plugins).
  gate's bake-only version recovery reads memex-migration (phase A).
- check-image-set.sh --pointers no longer asserts the portal's version tag.
- resolve-platform.py: self-test cases for latest-green (an unarmed set
  resolves by identity tag) and red-main fallback.
- guards updated; docs: OnePromotionGate, PairedChangeSets (new), MergeQueue
  (queue OFF), CrossRepoPairGate, ImageTagContract, PolicyNotProse, AGENTS.md,
  ci/pullrequest skills.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 27, 2026 18:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical and moderate issues remain in fleet arming, workflow permissions, trust boundaries, and paired-change handling.

Review effort: Lite
Findings: 5 High severity · 1 Medium severity · 11 Low severity

Open (17)
What changed in this PR

This PR decouples core merges from Plugins validation and adds asynchronous, verdict-gated fleet promotion.

Changes:

  • Makes dependent suites advisory and adds paired-change-set support.
  • Adds promotion records and fleet arming.
  • Updates CI workflows, image validation, policies, documentation, and tests.
File Description
test/​MeshWeaver.Documentation.Test/​PlatformReleaseNotifyGuard.cs Updates release notification guards.
test/​MeshWeaver.Documentation.Test/​PlatformNeverDependsOnPluginsGuard.cs Updates dependency expectations.
test/​MeshWeaver.Documentation.Test/​PlatformDeliveryNeverWaitsOnPluginsGuard.cs Guards delivery independence.
test/​MeshWeaver.Documentation.Test/​PlatformBakeLaneGuard.cs Updates bake-version assertions.
src/​MeshWeaver.Documentation/​Data/​Architecture/​PolicyNotProse.md Registers promotion policies.
src/​MeshWeaver.Documentation/​Data/​Architecture/​PairedChangeSets.md Documents paired changes.
src/​MeshWeaver.Documentation/​Data/​Architecture/​OnePromotionGate.md Documents promotion and arming.
src/​MeshWeaver.Documentation/​Data/​Architecture/​MergeQueue.md Records merge-queue status.
src/​MeshWeaver.Documentation/​Data/​Architecture/​ImageTagContract.md Updates tag-phase contracts.
src/​MeshWeaver.Documentation/​Data/​Architecture/​CrossRepoPairGate.md Documents advisory cross-repository testing.
src/​MeshWeaver.Documentation/​Data/​Architecture.md Updates architecture links.
AGENTS.md Updates repository guidance.
.github/​workflows/​main-cd.yml Adds promotion arming and notification flow.
.github/​workflows/​dotnet-test.yml Adds advisory dependent-suite pairing.
.github/​scripts/​test-check-image-set.py Updates image-set tests.
.github/​scripts/​resolve-platform.py Tests latest-green resolution.
.github/​scripts/​check-image-set.sh Updates image pointer validation.
.github/​scripts/​arm-promoted-set.py Selects verdict-qualified promoted sets.
.claude/​skills/​pullrequest/​SKILL.md Updates merge guidance.
.claude/​skills/​ci/​SKILL.md Updates CI guidance.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/scripts/arm-promoted-set.py Outdated
n = run_number_of(override)
if n is None:
raise ValueError(f"--override {override!r} is not a set name X.Y.Z[-pre]-ci.N")
hit = next((r for r in records if int(r["run_number"]) == n), None)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a. --override now matches r["v_portal"] == override exactly and never the run number alone. A new self-test row feeds an override naming the right run on another version line (3.0.1-ci.9459) and checks that it is RED.

Comment thread .github/workflows/dotnet-test.yml Outdated
Comment on lines +3717 to +3719
read -r state head < <(gh api "repos/Systemorph/MeshWeaver.Plugins/pulls/$PAIRED" --jq '"\(.state) \(.head.sha)"') \
|| { echo "::error::cannot read MeshWeaver.Plugins#$PAIRED with the dispatch App's token — the pair declared in this PR's body cannot be tested together."; exit 1; }
[[ "$head" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::MeshWeaver.Plugins#$PAIRED has no readable head sha ('$head')"; exit 1; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a. The paired head now resolves through .github/scripts/paired-plugins-head.py:

  • An open head is taken only when head.repo.full_name is Systemorph/MeshWeaver.Plugins (compared case-insensitively).
  • A fork's head, or a deleted head repository, is REFUSED, red, and the message names where the head lives.
  • A closed or merged pair falls back to Plugins' default branch.

It has 7 self-test rows, and workflow-shell runs the self-test on every PR. Checked live: MeshWeaver.Plugins#2489 (open, same repository) resolves its head, and #2479 (merged) falls back to the default branch.

Comment on lines +2597 to +2600
permissions:
contents: read
id-token: write
actions: read # the promotion records are run ARTIFACTS of this workflow

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a: arm now declares packages: write, the grant phase D's GHCR imagetools create needs. The job-level map replaces the workflow-level one, as you say.

Comment on lines +2680 to +2688
V_PORTAL_SEL: ${{ steps.select.outputs.v_portal }}
V_MIGRATION_SEL: ${{ steps.select.outputs.v_migration }}
SHA_SEL: ${{ steps.select.outputs.short }}
PLUGINS_SEL: ${{ steps.select.outputs.plugins_short }}
run: |
{
echo "V_PORTAL=$V_PORTAL_SEL"; echo "V_MIGRATION=$V_MIGRATION_SEL"; echo "SHA=$SHA_SEL"
echo "SRC_PORTAL_TAG=$SHA_SEL-p$PLUGINS_SEL"
} >> "$GITHUB_ENV"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a. Point the arming phases now exports V_PLUGIN from steps.select.outputs.v_plugin along with the other fields. Before writing anything, it also asserts that every selected field (V_PORTAL, V_MIGRATION, V_PLUGIN, SHA, the Plugins short sha) is non-empty, so a missing one fails before any registry write instead of after. I executed the extracted step: with all fields present it exports V_PLUGIN=… and exits 0; with V_PLUGIN_SEL empty it exits 1, naming the field.

echo "SRC_PORTAL_TAG=$SHA_SEL-p$PLUGINS_SEL"
} >> "$GITHUB_ENV"
- name: "Phase C — arm the release: memex-portal-ai:<version>"
if: steps.select.outputs.armed == 'true'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a. arm now carries a job-level concurrency: {group: main-cd-arm, cancel-in-progress: false}}, shared by every main-cd run, so reading the cursor, selecting and writing phase C happen one run at a time, and select always sees the previous run's cursor. The release event is a separate job, so it also re-checks never-backwards at the send: if a newer set was armed meanwhile, it sends nothing (the new step "Is this still the newest armed set?").

Comment on lines +84 to +90
### One promotion gate

`promote` (phases A and B) makes a set usable by CI. It no longer ARMS it. The arming writes —
phase C, `memex-portal-ai:<version>` (what `SelfUpdateHostedService` lists, pattern `3.0.0-ci*`);
phase D, the `<major>-latest` / `<major.minor>-latest` / `3.0.0-latest` pointers; and the signed
release event (`notify-platform-update`) — moved to the `arm` job, which runs in EVERY main-cd run
and arms the newest promoted set that is

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a. ContinuousDeliveryContract.md § "Rollback by ordering" now describes the split: promote runs phases A and B for CI, and arm runs phases C and D plus the event for the fleet. The phase table adds a Job column and rows for D and the event. The rule now reads: phase C never before phase A's tags exist, and nothing an INSTALL selects is written before it. What follows C is not selectable, but it is never forgotten, because of the completion marker and resume. The "Changing the pipeline" checklist is updated to match. OnePromotionGate.md gains the same completion, resume and serialisation section, so the two pages now agree.

Comment on lines +140 to +141
// 🚨 From memex-MIGRATION's tag set, not the portal's (policy `one-promotion-gate`,
// 2026-09-27): the portal's `<version>` is the ARMING write `arm` makes only once the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a: the date is removed and the line cites the policy id alone. The effective date now lives only in the PolicyNotProse register row.

Comment on lines +108 to +109
"`arm` (policy `one-promotion-gate`, 2026-09-27) reads the dependent-suites verdict "
+ "MeshWeaver.Plugins writes at refs/core-candidate/pair-<core7>-p<plugins7> for a "

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a: the ledger string now reads "Under policy core-merge-never-blocked it is ADVISORY" and "arm (policy one-promotion-gate) reads", with no date.

Comment on lines +263 to +264
/// CANDIDATE — advisory since policy <c>core-merge-never-blocked</c> (2026-09-27). The promotion
/// gate's verdict (main-cd <c>arm</c>) is requested by MeshWeaver.Plugins' own poller, so

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d75a92a: the date is removed and the line cites the policy id alone. The effective date now lives only in the PolicyNotProse register row.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files    1 suites   2m 57s ⏱️
348 tests 348 ✅ 0 💤 0 ❌
352 runs  352 ✅ 0 💤 0 ❌

Results for commit d75a92a.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

461 tests   461 ✅  51s ⏱️
  3 suites    0 💤
  3 files      0 ❌

Results for commit d75a92a.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 701 tests   1 701 ✅  4m 49s ⏱️
    2 suites      0 💤
    2 files        0 ❌

Results for commit d75a92a.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files      3 suites   6m 33s ⏱️
2 256 tests 2 256 ✅ 0 💤 0 ❌
2 257 runs  2 257 ✅ 0 💤 0 ❌

Results for commit d75a92a.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

770 tests   579 ✅  7m 7s ⏱️
  3 suites  191 💤
  3 files      0 ❌

Results for commit d75a92a.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    5 files      5 suites   14m 10s ⏱️
4 291 tests 4 289 ✅ 2 💤 0 ❌
4 295 runs  4 293 ✅ 2 💤 0 ❌

Results for commit d75a92a.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   17 files     17 suites   36m 31s ⏱️
9 827 tests 9 634 ✅ 193 💤 0 ❌
9 836 runs  9 643 ✅ 193 💤 0 ❌

Results for commit d75a92a.

♻️ This comment has been updated with latest results.

@rbuergi

rbuergi commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Coordination: the maintainer kept this design (core #5824 + Plugins #2461). I closed #5817 and Plugins #2455, and I've been asked to port three gaps onto this design as small follow-ups. One of them changes files in this PR, so I'm raising it here before touching them. I won't push to ci/never-block-core-merge. I'll open the change as a follow-up against main once this PR merges, unless you'd rather take it into this PR.

(1) The bundle base: the newest ARMED set, not the first parent (touches main-cd.yml and arm-promoted-set.py)

promote writes the record with base = parents[0] of the core commit, and judge() pins the verdict to rec["base"]. The Plugins control arm and core-candidate-scope.py therefore both see base..candidate as one merge.

A promoted set usually carries several merges, because of the hourly batch window and the fact that pending only takes the newest pair and supersedes older ones. The core merges between the last armed set and this one are then never measured, yet arm arms them. The 09-22..25 cascade (#5635/#5647/#5655) came from exactly those merges.

Proposed fix:

  • The record's base becomes the core commit of the newest armed set. arm already derives that as steps.armed.outputs.max; resolve it once in gate, pre-promote, from the armed portal version tag and the sha tag on the same manifest, and thread it to the record step.
  • base..candidate is then the whole bundle since the fleet's current set. The verdict's pin to rec["base"] needs no change.
  • Fallback when no set has been armed yet (the first run after rollout): use the first parent, with baseKind: "first-parent" in the record so the report says so. A read failure, as opposed to "none armed", is red and never falls back.
  • Tests: two self-test rows in arm-promoted-set.py, plus one test-cd-steps.py case.

(2) and (3): Plugins only, on main there (#2461's files), and no change to this PR

  • (2) Attribution for a held pair. For a red pair- verdict, core-candidate.yml maps each drifted suite to the bundle's PRs (scripts/core-release-attribution.py) and files ONE core-release-held issue plus a signed triage event through node-repo-ci-failure.yml. A green pair closes that issue. This needs a pending: row in core's .github/lane-caller-grants.yml, which is a separate tiny core PR, not this one.
  • (3) Stand-down and cancellation.
    • Each leg re-asks, before its build and before every suite, whether its requester is still waiting, instead of only at leg start.
    • promotion-candidate.yml dispatches cd-promotion with cancel=true, so a newer promoted pair cancels an older measurement already in flight. Its bundle is a superset from the same armed base, and the maintainer asked for this explicitly.
    • This reverses promotion-candidate.yml's "NOTHING IS CANCELLED". Tell me if you see a reason that must stand.

🤖 Generated with Claude Code

@rbuergi

rbuergi commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

The gap-1 fix is now ready as #5848, a draft stacked on this branch. Its base is ci/never-block-core-merge, and I haven't pushed anything here.

It records the bundle base as the core commit of the newest armed set instead of the first parent:

  • arm-promoted-set.py armed-base reads memex-portal-ai's manifests. The armed version tag shares a manifest with the sha tag, which I measured on 3.0.0-ci.9538 / b9fe5ed. The function is pure and self-tested.
  • promote's record step writes the new base plus a base_kind field:
    • armed in the normal case;
    • first-parent when nothing was ever armed, or the candidate is the armed set;
    • unresolved otherwise. Such a set is still promoted for CI, but select never arms it and pending never measures it.
  • OnePromotionGate.md gets a section on it.

I dry-ran the step against live ACR and GitHub, and all three branches came out correct. The details are in #5848.

When this PR merges I'll retarget #5848 to main, mark it ready and drive it to merge. If you'd rather fold it into this PR, say so and I'll close #5848.

@rbuergi

rbuergi commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Adopting this PR (coordinator assignment, 2026-09-28): the author session has been idle since 07:49Z and the maintainer kept this design. From here I push to ci/never-block-core-merge directly. I will fix the real defects Copilot found, with a test or self-test for each, answer all 17 threads on their threads, and fold in the gap-1 fix from #5848, which I will then close pointing here. It stays armed and merges on green.

rbuergi and others added 2 commits September 28, 2026 12:43
…irst parent

Gap 1 of 3 ported onto one-promotion-gate (maintainer decision 2026-09-27). promote wrote the record
with base = the core commit's first parent, and the verdict is pinned to rec["base"] — so Plugins'
scope diffed, and its control arm re-ran at, ONE merge of a set that usually carries several, and
`arm` then armed every other merge unmeasured.

- arm-promoted-set.py `armed-base`: names the newest armed set from memex-portal-ai's manifest
  metadata (the armed <version> tag shares its manifest with the 7-char sha tag — measured on
  3.0.0-ci.9538 / b9fe5ed); pure, self-tested (6 rows incl. uninterpretable manifests = RED).
- promote's record step: base = that commit when it resolves AND is an ancestor of the candidate
  (base_kind `armed`); `first-parent` when nothing was ever armed or the candidate IS the armed set;
  `unresolved` otherwise — CI still promotes, but select REFUSES to arm it and pending does not
  measure it (the next promoted set retries). Legacy records without base_kind judge as before.
- OnePromotionGate.md: "The bundle's base is the newest ARMED set".

Dry-run against live ACR + GitHub: main 003b70d → armed, base b9fe5ed; b9fe5ed itself →
first-parent; the older 9357060 → unresolved (compare: behind), with the warning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erialised, exports V_PLUGIN, can write GHCR; fork heads refused

Adopted from the idle author session; every Copilot finding on #5824 addressed.

- arm: `packages: write` (phase D pushes the line pointers to GHCR with GITHUB_TOKEN); V_PLUGIN
  exported with the rest (phase C's summary expanded it unbound under `set -u`, after the registry
  writes) and every selected field asserted non-empty before any write.
- arm is serialised across runs (job concurrency `main-cd-arm`, never cancelled): the push and
  reconcile lanes are separate workflow groups, so two runs could select different sets and the
  older write phase C after the newer.
- The cursor no longer advances past an incomplete arming: notify-platform-update writes the
  non-selectable marker `arm-complete-<version>` only after the build fact was delivered;
  `armed-state` reads it; `select --resume` re-arms the incomplete newest set when nothing newer is
  green (a newer green set still wins; a set armed before the marker, with no record, is reported,
  never red). notify re-checks never-backwards at the send.
- --override matches the EXACT version, never the run number alone.
- dotnet-test: a paired Plugins FORK head is refused (paired-plugins-head.py, self-tested and run
  in workflow-shell) — it would be built beside Plugins' private source.
- ContinuousDeliveryContract: phases C/D/event now in `arm`, with the completion/resume rule —
  no longer contradicting OnePromotionGate (which documents the same).
- Policy dates in prose replaced by the policy id (the register keeps the date).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi merged commit 677c82b into main Sep 28, 2026
51 of 53 checks passed
@rbuergi
rbuergi deleted the ci/never-block-core-merge branch October 10, 2026 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants