Skip to content

ci(nightly-next): decide's light jobs take a free mini, Blacksmith last - #29859

Closed
teamleaderleo wants to merge 3 commits into
feat-cmux-nextfrom
lane/nightly-fleet-runs-on
Closed

teamleaderleo wants to merge 3 commits into
feat-cmux-nextfrom
lane/nightly-fleet-runs-on

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

HOLD: superseded by #29864; do not land.

Leo, 2026-10-31: "FOR OUR NIGHTLIES ARE THEY END TO END ON OUR FLEET? WE CAN'T BE RELYING ON BLACKSMITH THAT CUTS OUT"

What

resolve-nightly-cmux-tui-client and find-nightly-next-recovery need only decide, hold no environment and no secret, and run git, gh, curl, bash and stdlib Python. On every nightly-next run they took blacksmith-32vcpu-ubuntu-2404 with no fleet route (run 41254086065: both started on Blacksmith 32 vCPU at 23:53Z, right after decide finished on an owned mini).

decide (which on attempt 1 already runs on an owned mini) now ends with the same two steps build-nightly-app uses for its light jobs (#29436): mint the read-only routing token, then scripts/ci/portable_job_placement.py with NIGHTLY=1. Each of the two jobs takes a mini that is idle at that moment, or its Blacksmith Linux runner-set member right away. Both steps are continue-on-error and run after the decision, so a failed placement leaves the old route in place and never fails decide. A re-run of failed jobs keeps the Blacksmith route (light_attempt must equal the run attempt).

  • tests/test_ci_nightly_off_blacksmith.py: both jobs leave BLACKSMITH_LINUX_ONLY_DEBT, which goes from 32 to 30 jobs. The light-jobs-on-Linux test accepts only decide's gated placement term ahead of the Linux set and still refuses any macOS label.
  • tests/test_nightly_trusted_routing.py: new DecideLightJobsOnAFreeMini covers routing, a stale or missing placement, re-runs, forks, no environment or secret on the placed jobs, and the token's read-only permissions.
  • .github/runners.json: both jobs are portable-checks. scripts/ci/runner_routing_table.py accepts decide's placement output as the nightly mini term.

Audit: NEXT nightly runs-on, feat-cmux-next at 07efced

  • Owned Macs first, then Blacksmith macOS: decide (control runner or nightly slot, then Blacksmith Linux on the rescue re-run), build-nightly-app and build-nightly-ghostty-cli-helper (picker owned, then Blacksmith), build-sign-notarize-nightly, verify-*, signer-dmg-image (sign picker), sign-nightly-identity-legacy, verify-nightly-identity-legacy and generate-nightly-deltas (owned slot or side lane only, no Blacksmith).
  • A free mini first, then Blacksmith Linux: build-nightly-remote-daemon and await-nightly-tree-marker (ci(nightly-next): the nightly's light Linux-hop jobs take a free mini, Blacksmith last #29436). This PR adds resolve-nightly-cmux-tui-client and find-nightly-next-recovery.
  • Blacksmith Linux only (30 jobs, the ratchet's debt): both promote jobs, the two pickers, build-nightly-inputs, resolve-nightly-ghostty-cli-helper, both prepare jobs, the signer-* Linux jobs, attest-nightly-remote-daemon, the visual-gate router, verdict and report, every publish job, release notes, the reel, and the failure reporting. Most of them hold an environment secret (publish, signing, routing) or produce Linux-only output (signer-build builds a Linux rcodesign), so each needs its own design and is not a mechanical move. fix(nightly): keep DMG signer on trusted Linux route #29821 just moved the DMG signer back to Linux for this reason.
  • Display Mac: nothing waits on one. visual-gate-nightly runs only when NIGHTLY_NEXT_VISUAL_GATE is set (ci(nightly): nightly-next runs stop skipping their own builds; no 32 vCPU pins; display visual gate opt-in #28961). The launch smoke is a separate post-publish workflow.

Prior art: #29436 and #29650 (same pattern, merged), #29821 (open, DMG signer, separate jobs). #28967's failures are already gone on base; the current test_ci_runs_on_fallback red is signer-build-macos, which #29821 removes.

Tests

Run locally: python3 -m unittest tests/test_ci_nightly_off_blacksmith.py tests/test_nightly_trusted_routing.py tests/test_ci_portable_job_placement.py tests/test_nightly_workflow_size.py tests/test_ci_runs_on_values.py tests/test_publish_app_token_workflow.py tests/test_ci_warm_sources.py tests/test_ci_linux_single_size.py tests/test_ci_signing_owned_mac.py, all OK. tests/test_runner_routing_table.py, tests/test_ci_runs_on_fallback.py and tests/test_ci_macos_xcode_selection.py fail the same way on base and on this head: signer-build-macos and signer-dmg, from #29801 and fixed by #29821. tests/test_runner_label_policy.py fails on base and here on CI_CONTROL_RUNNER (base red).

nightly.yml is now 501,561 bytes, under the 508,000 headroom line.

Risk: both jobs can now run on an owned mini (macOS). Their scripts already handle macOS (pin-cmux-tui.sh uses shasum there, for example), and test_ci_macos_xcode_selection already lists resolve-nightly-cmux-tui-client as a git, gh and Python control job.

Agent session: agent session
🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Routes resolve-nightly-cmux-tui-client and find-nightly-next-recovery to a free owned mini first, keeping Blacksmith as fallback, so nightly-next jobs no longer rely on Blacksmith by default.

  • decide mints a read-only routing token and runs portable_job_placement.py (NIGHTLY=1) as its last steps, the same pattern build-nightly-app uses for its light jobs.
  • Each placed job takes a mini idle when decide finishes, or its Blacksmith runner right away; failed or re-run jobs keep the old Blacksmith route.
  • Both placement steps are continue-on-error and run after decide's decision, so a failed placement never fails decide; decide's token has actions: read so the placement can read the queue janitor's snapshot, and both steps are registered in nightly-step-coverage.json.
  • Updates runners.json, runner_routing_table.py, and the placement/routing tests; Blacksmith-only debt drops from 32 to 30 jobs.
  • resolve-nightly-cmux-tui-client and find-nightly-next-recovery continue to hold no environment and no secret.

Written for commit d32c268. Summary will update on new commits.

View guided diff Turn on auto-fix

resolve-nightly-cmux-tui-client and find-nightly-next-recovery need only
decide, hold no environment and no secret, and run git, gh, curl, bash and
stdlib Python. On every nightly-next run they took blacksmith-32vcpu-ubuntu-2404
with no fleet route (run 41254086065). decide's last steps now place them with
scripts/ci/portable_job_placement.py (NIGHTLY=1), the way build-nightly-app
places its light jobs: a mini idle now, else their Blacksmith Linux set member
at once. A re-run of failed jobs keeps the Blacksmith route (light_attempt).

Both leave the Blacksmith-only debt in test_ci_nightly_off_blacksmith.py and
take class portable-checks in runners.json; runner_routing_table accepts
decide's placement output as the nightly mini term.

Leo, 2026-10-31: "FOR OUR NIGHTLIES ARE THEY END TO END ON OUR FLEET? WE CAN'T
BE RELYING ON BLACKSMITH THAT CUTS OUT".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-4fbe72147bb64b0edb5d
@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
… janitor snapshot

Review at a581f92 (gpt-6.1-sol xhigh): the two new decide steps had no
nightly-step-coverage entry (release-notary guard), and decide's token lacked
actions: read, so the placement could not read the queue janitor's snapshot
artifact and would have kept both jobs on Blacksmith. The light-on-Linux test's
silent needs now applies only to the decide-placed jobs, and the decide test
pins the placement as decide's last step, after the mint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-4fbe72147bb64b0edb5d
@cursor

cursor Bot commented Nov 1, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

4 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/ci/runner_routing_table.py">

<violation number="1" location="scripts/ci/runner_routing_table.py:85">
P3: The comment above this regex still says only build-nightly-app places the light jobs. The regex now also accepts decide's `light_mini_runner`, so update the comment to name both producers and which jobs each places.</violation>
</file>

<file name="tests/test_nightly_trusted_routing.py">

<violation number="1" location="tests/test_nightly_trusted_routing.py:519">
P2: These assertions lock in a Blacksmith fallback for two nightly jobs. AGENTS.md forbids Blacksmith routes on nightly jobs, including fallbacks. If the 'Blacksmith last' design is intended, the AGENTS.md exception should be recorded and the policy test updated to match.</violation>
</file>

<file name="tests/test_ci_nightly_off_blacksmith.py">

<violation number="1" location="tests/test_ci_nightly_off_blacksmith.py:226">
P3: The docstring and test name still say these light jobs stay on Linux, but the new mini branch lets two of them run on a Mac mini first. Update the docstring and rename the test to match the placement exception.</violation>
</file>

<file name=".github/workflows/nightly.yml">

<violation number="1" location=".github/workflows/nightly.yml:835">
P1: The placement snapshot read uses `github.token`, but decide's explicit `permissions:` block does not grant `actions: read`, so the artifact listing fails. The failure is swallowed, so no job is placed on a mini and the feature silently does nothing. Add `permission-actions: read` to the `light-route-token` mint step (as `snapshot-read-token` does) and pass that token as `SNAPSHOT_TOKEN`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

FLEET_LINUX: ""
SIDE_LABEL: ${{ vars.CI_SIDE_LANE_RUNNER }}
ROUTE_TOKEN: ${{ steps.light-route-token.outputs.token }}
SNAPSHOT_TOKEN: ${{ github.token }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The placement snapshot read uses github.token, but decide's explicit permissions: block does not grant actions: read, so the artifact listing fails. The failure is swallowed, so no job is placed on a mini and the feature silently does nothing. Add permission-actions: read to the light-route-token mint step (as snapshot-read-token does) and pass that token as SNAPSHOT_TOKEN.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At .github/workflows/nightly.yml, line 834:

<comment>The placement snapshot read uses `github.token`, but decide's explicit `permissions:` block does not grant `actions: read`, so the artifact listing fails. The failure is swallowed, so no job is placed on a mini and the feature silently does nothing. Add `permission-actions: read` to the `light-route-token` mint step (as `snapshot-read-token` does) and pass that token as `SNAPSHOT_TOKEN`.</comment>

<file context>
@@ -802,6 +806,34 @@ jobs:
+          FLEET_LINUX: ""
+          SIDE_LABEL: ${{ vars.CI_SIDE_LANE_RUNNER }}
+          ROUTE_TOKEN: ${{ steps.light-route-token.outputs.token }}
+          SNAPSHOT_TOKEN: ${{ github.token }}
+        run: python3 scripts/ci/portable_job_placement.py
+
</file context>

def test_no_placement_a_stale_one_or_a_fork_keeps_the_old_route(self):
for job in self.LIGHT:
for changes, expected in (
({'needs.decide.outputs.light_mini_jobs': ''}, 'blacksmith-32vcpu-ubuntu-2404'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: These assertions lock in a Blacksmith fallback for two nightly jobs. AGENTS.md forbids Blacksmith routes on nightly jobs, including fallbacks. If the 'Blacksmith last' design is intended, the AGENTS.md exception should be recorded and the policy test updated to match.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test_nightly_trusted_routing.py, line 519:

<comment>These assertions lock in a Blacksmith fallback for two nightly jobs. AGENTS.md forbids Blacksmith routes on nightly jobs, including fallbacks. If the 'Blacksmith last' design is intended, the AGENTS.md exception should be recorded and the policy test updated to match.</comment>

<file context>
@@ -482,6 +483,78 @@ def test_the_build_job_mints_only_the_routing_token_for_the_placement(self):
+    def test_no_placement_a_stale_one_or_a_fork_keeps_the_old_route(self):
+        for job in self.LIGHT:
+            for changes, expected in (
+                    ({'needs.decide.outputs.light_mini_jobs': ''}, 'blacksmith-32vcpu-ubuntu-2404'),
+                    ({'needs.decide.outputs.light_attempt': ''}, 'blacksmith-32vcpu-ubuntu-2404'),
+                    # A re-run of failed jobs keeps decide's earlier outputs: the mini may be busy by now.
</file context>

# nightly.yml's light jobs: build-nightly-app's last step places them (NIGHTLY=1) on a free mini, else
# Blacksmith. They have no fleet Linux step: a nightly job never takes vars.LINUX_RUNNER as it is.
NIGHTLY_MINI = re.compile(r"needs\.build-nightly-app\.outputs\.light_mini_runner$")
NIGHTLY_MINI = re.compile(r"needs\.(?:build-nightly-app|decide)\.outputs\.light_mini_runner$")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The comment above this regex still says only build-nightly-app places the light jobs. The regex now also accepts decide's light_mini_runner, so update the comment to name both producers and which jobs each places.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At scripts/ci/runner_routing_table.py, line 85:

<comment>The comment above this regex still says only build-nightly-app places the light jobs. The regex now also accepts decide's `light_mini_runner`, so update the comment to name both producers and which jobs each places.</comment>

<file context>
@@ -82,7 +82,7 @@
 # nightly.yml's light jobs: build-nightly-app's last step places them (NIGHTLY=1) on a free mini, else
 # Blacksmith. They have no fleet Linux step: a nightly job never takes vars.LINUX_RUNNER as it is.
-NIGHTLY_MINI = re.compile(r"needs\.build-nightly-app\.outputs\.light_mini_runner$")
+NIGHTLY_MINI = re.compile(r"needs\.(?:build-nightly-app|decide)\.outputs\.light_mini_runner$")
 # nightly.yml's first job (decide) has no job before it to place it: attempt 1 of a nightly-next push asks for
 # glaeda's nightly slot, which only nightly jobs take, and the owned-pool rescue moves it to Blacksmith when no
</file context>

or runs_on.endswith("|| 'blacksmith-32vcpu-ubuntu-2404' }}") and "LINUX_RUNNER" not in runs_on, runs_on)
self.assertNotRegex(runs_on, r"vars\.LINUX_RUNNER \|\|")
self.assertNotIn("macos", runs_on)
# decide's light-job placement (a mini idle when decide finished) is the one fleet route ahead of

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The docstring and test name still say these light jobs stay on Linux, but the new mini branch lets two of them run on a Mac mini first. Update the docstring and rename the test to match the placement exception.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test_ci_nightly_off_blacksmith.py, line 226:

<comment>The docstring and test name still say these light jobs stay on Linux, but the new mini branch lets two of them run on a Mac mini first. Update the docstring and rename the test to match the placement exception.</comment>

<file context>
@@ -223,8 +223,14 @@ def test_light_nightly_jobs_stay_on_linux_while_the_overflow_switch_is_on(self)
                                 or runs_on.endswith("|| 'blacksmith-32vcpu-ubuntu-2404' }}") and "LINUX_RUNNER" not in runs_on, runs_on)
                 self.assertNotRegex(runs_on, r"vars\.LINUX_RUNNER \|\|")
                 self.assertNotIn("macos", runs_on)
+                # decide's light-job placement (a mini idle when decide finished) is the one fleet route ahead of
+                # the Linux set; with no placement (needs read as '') the job takes the set as before.
+                if "needs.decide.outputs.light_mini_runner" in runs_on:
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/test_nightly_trusted_routing.py">

<violation number="1" location="tests/test_nightly_trusted_routing.py:486">
P3: This class copies `LightJobsOnAFreeMini` almost verbatim: `runs_on`, the placement test, and the no-placement/stale/fork test differ only in the `needs.<job>` key. Move the shared helpers and the three test methods into a base mixin or parameterized base class, and keep only the decide-specific test in the new class.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

self.assertTrue(all(value == 'read' for key, value in mint['with'].items() if key.startswith('permission-')))


class DecideLightJobsOnAFreeMini(unittest.TestCase):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This class copies LightJobsOnAFreeMini almost verbatim: runs_on, the placement test, and the no-placement/stale/fork test differ only in the needs.<job> key. Move the shared helpers and the three test methods into a base mixin or parameterized base class, and keep only the decide-specific test in the new class.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/test_nightly_trusted_routing.py, line 486:

<comment>This class copies `LightJobsOnAFreeMini` almost verbatim: `runs_on`, the placement test, and the no-placement/stale/fork test differ only in the `needs.<job>` key. Move the shared helpers and the three test methods into a base mixin or parameterized base class, and keep only the decide-specific test in the new class.</comment>

<file context>
@@ -482,6 +483,81 @@ def test_the_build_job_mints_only_the_routing_token_for_the_placement(self):
         self.assertTrue(all(value == 'read' for key, value in mint['with'].items() if key.startswith('permission-')))
 
 
+class DecideLightJobsOnAFreeMini(unittest.TestCase):
+    """decide's light dependents take a mini idle when decide finished, else Blacksmith.
+
</file context>

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (independent Codex gpt-6.1-sol, high, via the review router; full diff from merge base 07efced to this head): SHIP at d32c268.

No P1/P2 findings.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6.1-sol xhigh, router-review): SHIP at d32c268

No correctness findings. Focused routing and placement tests pass.
Follow-ups: none. The earlier BLOCK at a581f92 (no nightly-step-coverage entries for the new decide steps; decide lacked actions: read for the janitor snapshot) is fixed in d32c268.

Also: Review (subagent opus-5.5 medium): SHIP at a581f92. Follow-ups from it: check that the minis' python3 is 3.11 or newer (chief-input-key's tomllib; it is continue-on-error), and resolve-nightly-cmux-tui-client can hold a side slot for up to its 75-minute wait.

…y-fleet-runs-on

# Conflicts:
#	tests/test_ci_nightly_off_blacksmith.py
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high, router-review): SHIP at 70711df

No correctness findings. Relevant routing, placement, workflow, resolver and recovery tests pass.
Follow-ups: none. The head merges feat-cmux-next after #29821. The only conflict was the Blacksmith-only debt list, which keeps signer-dmg (back on Linux per #29821) and drops find-nightly-next-recovery (this PR).

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 70711df: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr29859 -f ref=lane/nightly-fleet-runs-on -f platform=macos -f backend_mode=local

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by #29864 ("ci(nightly): place early controls on fleet before overflow"), which landed on feat-cmux-next while this waited for the merge. It gives decide an early placement step that covers both jobs this PR moved (resolve-nightly-cmux-tui-client, find-nightly-next-recovery) plus pick-nightly-build-runner, resolve-nightly-ghostty-cli-helper, build-nightly-inputs and signer-build. Each tries fleet Linux first, then a free owned mini, then Blacksmith. decide already has actions: read and the routing environment on base. Merging this on top would only add a second, narrower placement for the same two jobs.

@lawrencecchen

Copy link
Copy Markdown
Contributor

Conflict wave: not resolved, because feat-cmux-next already has this change. #29864 (b85875a "ci(nightly): place early controls on fleet before overflow") gives decide an early placement step that covers both jobs this PR moves, as the author's comment above says. A merge conflicts in nightly.yml, nightly-step-coverage.json, runner_routing_table.py and test_ci_nightly_off_blacksmith.py against that rewrite. I pushed nothing. This PR has no ready-to-land label. I did not close it; a person can close it.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 70711dfa Deployed Nov 1, 2026 by teamleaderleo via wait for same-key tree publisher #92536
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants