ci(signing): the nightly identity signing job may take a drained owned Mac, held whole

Merged
TE
9d ago
+719-11

Leo, 2026-10-28, on letting the release signing jobs run on our own Macs while Blacksmith is starved: "ok. idk what the "risk" was before but like.. we can just read the code and stuff." The routing order is Linux first, then owned Macs, then Blacksmith macOS last.

What changed

  • scripts/ci/signing_runner.py (new) offers a drained owned Mac to a signing job: a mini none of whose online runners is busy.
    • The job is pinned to that mini by ["<pool label>", "glaeda-runner-<name>"]. The pool label keeps the owned-pool rescue watching the job.
    • It picks a Mac only while vars.CI_SIGN_OWNED is 1, only on attempt 1, and only with a live runner read. In every other case the answer is empty, and the job takes the moving Blacksmith label as before.
  • nightly.yml:
    • pick-nightly-sign-runner prints that pick as sign_owned. Its budget goes from 10 to 12 minutes for the extra 2-minute step.
    • sign-nightly-identity-legacy takes sign_owned on attempt 1, then the Blacksmith pick.
  • The signing job's first step is an inline claim gate, before the checkout (which holds the job token) and every other step. On an owned Mac it fails unless glaeda's hook gave the job the whole mini.
  • scripts/ci/signing-runner-claim.sh (new) runs after checkout and before any secret.
    • On an owned Mac it fails unless glaeda's hook gave the job the whole mini (GLAEDA_JOB_CLAIM=exclusive, from glaeda's exclusive class for signing jobs: every capacity unit, so nothing else runs beside it).
    • It also removes a signing keychain that a killed job left behind.
    • On Blacksmith it does nothing.
  • scripts/ci/signing-keychain.sh:
    • create saves the user keychain search list, and delete puts it back. This keeps the login keychain in the list for the next job on an owned Mac.
    • The key still goes only into the per-job temporary keychain. The decoded certificate file is still removed by create and again by delete.
  • legacy-identity-sign-build.sh removes its decoded provisioning profile in an EXIT trap, so a failed run leaves nothing on disk.
  • tests/test_ci_signing_owned_mac.py (new, in ci-guards and the test registry). Every signing job routed to an owned Mac must:
    • reach it only through signing_runner.py, as github.run_attempt == 1 && <pick> && fromJSON(<pick>). A picker output from anything else, or from nightly_sign_runner.py (Blacksmith only), fails the test;
    • start with the claim gate word for word, unconditionally, and run the claim script before any step that names a secret, with no secret in job-level or workflow-level env;
    • make its keychain only with signing-keychain.sh and delete it in an if: always() step;
    • be on the SIGNING_JOBS list that glaeda's hook gives the whole mini.

Before / after

  • Before: every signing job waited on Blacksmith macOS.
  • After: with CI_SIGN_OWNED=1, the nightly identity signing job takes an idle owned Mac first. The other signing jobs are unchanged here; a follow-up PR gives them their own picker.

Tests

These ran locally and pass:

  • New: test_ci_signing_owned_mac.py (6 tests).
  • Updated or affected: test_ci_signing_keychain_is_deleted_before_bundle_code.py, test_nightly_sign_runner.py, test_ci_runs_on_fallback.py, test_runs_on_labels.py, test_ci_nightly_off_blacksmith.py, test_nightly_signer_jobs.py, test_supplychain_security.py, test_ci_owned_pool_rescue.py, test_nightly_workflow_size.py, plus the nightly workflow tests.

Rollout

Owned signing stays off until both of these hold:

  1. The glaeda hook with the exclusive class is on the minis.
  2. CI_SIGN_OWNED is set to 1.

Before that, a stale hook cannot sign beside another job, because the claim step fails.

Risk: medium. Signing moves onto owned Macs behind a variable. Without the variable, nothing changes.

Agent session: agent session

šŸ¤– Generated with Claude Code