* ci: reduce PR runner overhead and package compression time * ci: validate mobile when its dependency action changes
8.1 KiB
Windows signing without occupying a runner during approval
Status: implementation proposal; production signing behavior is unchanged.
Measured cost
In release run 33821033674 (September 4, 2026), the Windows job took 21m56s. The inner-binary download step took 13m19s and the installer download step took 40s: 13m59s, or 64% of the job, was spent in the signing download/wait steps. These durations include the download itself, so they are an upper bound on removable idle time, not a prediction of net savings after transferring state between jobs.
release-cut.yml submits both requests with wait-for-completion: false, but
then invokes Get-SignedArtifact on the same Windows runner with one-hour and
four-hour completion timeouts. The six-hour job timeout accommodates both
waits. Changing the submission flag again, polling less often, or running the
wait inside a container does not release the runner slot.
This is runner occupancy, not a billing estimate. Standard GitHub-hosted runners in a public repository may be free; removing the waits still releases concurrency for other work. Check actual billing before assigning dollar savings.
The same release also occupied an Ubuntu runner for 11m38s while
run-release-mac-build-workflow.mjs waited on the isolated macOS workflow.
That is a separate orchestration optimization. Windows development-channel
builds deliberately ship unsigned and have no SignPath wait to remove.
Proposed execution graph
Keep all Windows stages in the original release-cut.yml run to preserve the
current SignPath GitHub artifact provenance boundary:
build-windowsbuilds and uploads the unpacked app, original installer, updater metadata, and inner-signing manifest. It submits the inner request, sends the existing notification, exposes the request ID, and finishes.package-windowsdepends on that job and uses a protected environment namedwindows-inner-signing. Its runner is allocated only after GitHub approval. It restores the exact build, downloads the signed binaries with a short, bounded completion wait, applies the existing signature restoration and signedelevate.execache replacement, builds the NSIS installer, uploads it, submits the second signing request, notifies approvers, and finishes.finalize-windowsdepends on packaging and uses a second protected environment namedwindows-installer-signing. After approval it downloads the signed installer, regenerates its blockmap andlatest.yml, runs existing outer and inner signature checks, uploads evidence, and uploads the assets to the draft.publish-releasedepends on finalization as well as the existing Linux, macOS, and blocking release gates. It remains the only job that publishes the draft.
The approver signs in SignPath, waits for that request to finish, and then approves the corresponding pending GitHub job. Each notification should link to both places and explain the order. GitHub approval is an extra action; approving in SignPath alone does not release an environment gate.
Required configuration
The repository environments were inspected through the GitHub API on
September 5, 2026. Neither Windows environment exists. adhoc-mac-build has no
protection rules; it cannot be reused as an approval gate. No SignPath callback
handler was found in the repository's workflows, scripts, application, or cloud
code.
Before enabling the graph:
- Create both environments in repository Settings → Environments.
- Add the release approvers as required reviewers for each environment. Decide whether a release initiator may approve their own job, and configure that consistently with the existing SignPath policy.
- Restrict deployment branches to the trusted refs used to dispatch release workflows, and check that the release workflow's ref passes the restriction. The workflow ref and the checked-out release tag are different concepts.
- Read back both environments through the API and verify that
required_reviewersrules exist before changing the release graph. Merely referring to a new environment name in YAML can create an unprotected environment and silently leave the wait on the runner. - Add a preflight assertion for those rules so accidental removal fails before
any signing request is submitted. Verify the API access required for this
assertion using the release workflow's token; do not assume an administrator's
local
ghaccess proves workflow-token access.
An automatic alternative requires a SignPath completion callback and an authenticated integration that releases the corresponding deployment gate. Confirm the Foundation plan supports the necessary callback before choosing that architecture. Do not introduce a long-running GitHub polling job as the callback substitute: it would continue occupying a slot.
State and failure contracts
- Use artifacts from this exact run and attempt, with a manifest containing the tag, tag commit SHA, workflow SHA, request IDs, artifact IDs, and SHA-256 hashes. Artifact names alone are insufficient. Preserve the original unsigned installer for the existing inner-signing fallback.
- Restore
dist/win-unpacked, the staging list, the installer, and updater metadata as one checkpoint. Use an archive to preserve the tree. Do not ship a fresh rebuild of the app after approving a different binary tree. - Each new Windows runner needs the pinned Node/pnpm toolchain, build
dependencies, SignPath module, and electron-builder tool cache. The second
runner must populate the NSIS cache before replacing
elevate.exe; the old code assumes the first installer build already populated that cache. - Retain checkout-from-tag behavior and the existing support for release tags that predate the composite action. Explicitly restore new orchestration code from the workflow SHA when necessary.
- Preserve the rule that rerunning a workflow never submits a new signing request. A resume must consume the recorded request and artifacts. Test failed stage reruns, whole-workflow reruns, and missing/expired checkpoints separately.
- Keep installer signature checks blocking. Keep inner verification evidence and its current warning-only policy unless changed in a separate decision.
- Resolve the current one-hour inner-signing fallback deliberately: an environment approval can remain pending longer than one hour and rejection skips dependent jobs. It cannot reproduce the existing automatic timeout fallback by itself. A first migration should explicitly document the new manual release/cancellation behavior; silently treating rejected approval as permission to ship is not acceptable.
- Keep the release-wide concurrency lock while the graph waits, preventing another release from overtaking this draft. This saves worker occupancy, but does not shorten the serialized release queue's human approval time.
Validation before production
First adapt windows-signing-rehearsal.yml to exercise the same staged code
using the auto-approved test-signing policy. Then run a manual rehearsal with
the protected environments and confirm that pending approval has no allocated
Windows runner. Verify signed bytes through the existing extraction-based
installer checks, not only the outer installer signature.
Cover approval before SignPath completion, rejected approval, missing signed files, changed checkpoint hashes, lost checkpoints, expired artifacts, failed packaging, and stage reruns without duplicate submissions. Confirm no release becomes public until all platform and signature gates pass. Compare transferred artifact/setup time with the original 13m59s wait sample to measure net savings.
A separate workflow_dispatch continuation can avoid environment provisioning,
but changes this design substantially: the original release run finishes,
workflow-level concurrency no longer protects the pending draft, and SignPath
must accept artifacts assembled from a prior run. That option needs a durable
release state machine and provenance validation before production use; it is
not a drop-in replacement for the two download steps.