Skip to content

Commit 66f7811

Browse files
authored
ci: fix dry-run job graph, embed deploy commands in DEPLOY.md, correct runbook (#4040)
## Summary of Changes * **Fix dry runs dead-ending after `stage-programs`** (found on runs [29106365876](https://github.com/malbeclabs/doublezero/actions/runs/29106365876) and [29109225537](https://github.com/malbeclabs/doublezero/actions/runs/29109225537)): the job-level `if: !inputs.dry_run` skip on `push-tags` transitively skipped every downstream job with a default status condition — a skipped ancestor poisons `success()` for the whole graph below it, even past `verify-cloudsmith`'s own `if:` override — so `gate-programs` through `announce` could never run in dry-run mode (confirmed: no deployment record was ever created for `gate-programs`; it skipped in graph evaluation before environment processing). The tag reusable workflow gains a `dry_run` input that makes each tag job a validated no-op, `push-tags` now runs in every mode, `verify-cloudsmith`'s special-case condition is deleted, and the tag-approval Slack nudge fires in dry runs too. Net effect: dry runs now traverse the full graph, and exercise the same `testnet` tag-approval prompt as a real release (one extra approval click per dry run). * The generated `DEPLOY.md` embeds the exact program-deploy commands — `cd` into the staged release dir, the three `solana program deploy` invocations with keypairs under `~/testnet-ops/` (absolute: the [infra runbook](https://github.com/malbeclabs/infra/blob/main/docs/runbooks/deploys/solana-programs-testnet.md)'s relative `./testnet-ops/` paths assume `~`, no longer true once the operator cds into `/opt/doublezero/program-releases/vX.Y.Z/`), and `doublezero init` — replacing the retired Notion reference. * Runbook: dry-run section documents the no-op tag jobs and full approval sequence; recovery guidance prefers "Re-run all jobs" after mid-pipeline failures and says to verify a re-run actually executed the expected jobs before trusting its green check. ## Testing Verification * The dry-run dead-end reproduced identically on a fresh dispatch (not just re-runs), with `gate-programs` skipped the second its needs completed, zero annotations, and no deployment record — ruling out environment protection and matching transitive-skip semantics exactly. * `DEPLOY.md` generation block extracted from the YAML and rendered in a sandbox: multi-line commands, literal `~` paths, and interpolated version all correct. * actionlint on both workflows: only the pre-existing SC2086 (agave install idiom, untouched). * Full validation is the next dry run after merge (fresh dispatch — workflow-file changes don't apply to re-runs). Follow-up (not in this PR): a `pipeline-complete` guard job so a hollow "success" fails loudly.
1 parent 3c17fbd commit 66f7811

4 files changed

Lines changed: 70 additions & 21 deletions

File tree

.github/workflows/release.testnet.push.tags.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ on:
1616
required: false
1717
type: boolean
1818
default: false
19+
dry_run:
20+
description: 'Validate inputs and succeed without pushing anything (for orchestrator dry runs)'
21+
required: false
22+
type: boolean
23+
default: false
1924
secrets:
2025
DOUBLEZERO_PAT:
2126
description: 'PAT to push tags to the repository'
@@ -48,6 +53,11 @@ jobs:
4853
fi
4954
echo "Version format is valid."
5055
56+
if [ "${{ inputs.dry_run }}" = "true" ]; then
57+
echo "Dry run — not pushing tag '$TAG_NAME'."
58+
exit 0
59+
fi
60+
5161
if EXISTING=$(git rev-parse -q --verify "refs/tags/$TAG_NAME^{commit}"); then
5262
if [ "${{ inputs.skip_existing }}" = "true" ]; then
5363
HEAD_SHA=$(git rev-parse HEAD)

.github/workflows/release.testnet.yml

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,7 @@ jobs:
205205
fi
206206
# The tag jobs request the `testnet` environment prompt again right after
207207
# this job completes; nudge the thread so that second approval isn't missed.
208-
# Skipped in dry runs, where push-tags doesn't run at all.
209208
- name: Post tag-approval nudge to Slack thread
210-
if: ${{ !inputs.dry_run }}
211209
uses: ./.github/actions/slack-thread-post
212210
with:
213211
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -216,8 +214,11 @@ jobs:
216214
text: |-
217215
Version PRs confirmed merged. The 9 tag jobs will request the `testnet` environment prompt momentarily: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
218216
217+
# Runs in dry runs too (as a validated no-op via dry_run): a job-level skip
218+
# here would transitively skip every downstream job with a default status
219+
# condition — a skipped ancestor poisons `success()` for the whole graph
220+
# below it, even past jobs that override their own condition.
219221
push-tags:
220-
if: ${{ !inputs.dry_run }}
221222
needs: gate-tags
222223
uses: ./.github/workflows/release.testnet.push.tags.yml
223224
permissions:
@@ -238,14 +239,13 @@ jobs:
238239
version: v${{ inputs.version }}
239240
component: ${{ matrix.component }}
240241
skip_existing: true
242+
dry_run: ${{ inputs.dry_run }}
241243
secrets:
242244
DOUBLEZERO_PAT: ${{ secrets.DOUBLEZERO_PAT }}
243245

244246
verify-cloudsmith:
245247
runs-on: ubuntu-latest
246248
needs: [preflight, gate-tags, push-tags]
247-
# In dry-run, push-tags is skipped; still verify (against the current version) to exercise the query logic.
248-
if: ${{ !cancelled() && needs.gate-tags.result == 'success' && (needs.push-tags.result == 'success' || (inputs.dry_run && needs.push-tags.result == 'skipped')) }}
249249
timeout-minutes: 75
250250
steps:
251251
- name: Install Cloudsmith CLI
@@ -305,6 +305,7 @@ jobs:
305305
cp target/deploy/doublezero_serviceability.so staged/
306306
cp target/deploy/doublezero_telemetry.so staged/
307307
cp target/deploy/doublezero_geolocation.so staged/
308+
# shellcheck disable=SC1003 # trailing backslashes inside single quotes are literal line-continuations for the generated markdown
308309
{
309310
echo "# Testnet program deploy v${VERSION}"
310311
echo
@@ -318,12 +319,33 @@ jobs:
318319
echo '```'
319320
echo
320321
echo "## Deploy"
321-
echo "Follow the Notion runbook 'Build and deploy DoubleZero solana programs - testnet',"
322-
echo "using these prebuilt artifacts instead of building locally."
323-
echo "After deploying, set the serviceability program version:"
324-
echo " doublezero --env testnet global-config set-version --min-compatible-version <X.Y.Z>"
325-
echo "(serviceability is the only program with a settable version account; telemetry has"
326-
echo "none and geolocation has no CLI setter). Verify with: doublezero --env testnet version"
322+
echo
323+
echo "Runbook: https://github.com/malbeclabs/infra/blob/main/docs/runbooks/deploys/solana-programs-testnet.md"
324+
echo "(its Build/scp section is superseded by these prebuilt, staged artifacts)"
325+
echo
326+
echo '```bash'
327+
echo "cd /opt/doublezero/program-releases/v${VERSION}"
328+
echo
329+
echo 'solana program deploy \'
330+
echo ' --program-id ~/testnet-ops/serviceability-program-keypair.json \'
331+
echo ' -k ~/testnet-ops/serviceability-program-authority.json \'
332+
echo ' doublezero_serviceability.so'
333+
echo
334+
echo 'solana program deploy \'
335+
echo ' --program-id ~/testnet-ops/telemetry-program-keypair.json \'
336+
echo ' -k ~/.config/doublezero/id.json \'
337+
echo ' doublezero_telemetry.so'
338+
echo
339+
echo 'solana program deploy \'
340+
echo ' --program-id ~/testnet-ops/geolocation-program-testnet-keypair.json \'
341+
echo ' -k ~/.config/doublezero/id.json \'
342+
echo ' doublezero_geolocation.so'
343+
echo
344+
echo '# refresh the onchain program version'
345+
echo 'doublezero init'
346+
echo '```'
347+
echo
348+
echo "Verify with: doublezero --env testnet version"
327349
echo
328350
echo "Then approve the waiting 'gate-programs' job (testnet environment) on the orchestrator run above."
329351
} > staged/DEPLOY.md

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ All notable changes to this project will be documented in this file.
4747
- Auto-publish the mainnet-beta client base image daily when the stable Cloudsmith channel advances (idempotency-gated so a run with no new version is a no-op), and notify `doublezero-edge-connect` to rebuild its testnet and mainnet-beta variants when a new base image is published. Serialize publishes with per-job concurrency groups, keep the notify steps non-fatal to the publish, and skip Debian pre-release versions when resolving the mainnet-beta tag. (#3990)
4848
- Add a testnet release orchestrator workflow (`release.testnet.yml`) that drives the release end to end: preflight checks, version-bump PRs for doublezero and infra, a human-approved gate before pushing the 9 component tags, CloudSmith package verification, Solana program build and staging with a manual deploy gate, onchain version verification, infra core/client deploys, QA, and Slack notifications. Supports `dry_run` for plumbing validation and safe re-runs (existing PRs are reused; already-pushed tags are skipped via a new `skip_existing` input on the tag workflow). Runbook at `docs/testnet-release.md`.
4949
- Thread all testnet release orchestrator Slack posts under a single per-run parent message in `#bots`, posted via the Slack Web API (`chat.postMessage` with a bot token) instead of incoming webhooks, which cannot start threads. Adds a threaded PR-links post after `open-prs` covering the merge-both-PRs / approve-gate-1 human steps, and a tag-approval nudge after `gate-tags` for the tag jobs' second `testnet` environment prompt. Slack failures degrade to workflow warnings and flat posts, never failing the release. (#4036)
50+
- Testnet release: the generated `DEPLOY.md` now embeds the exact program-deploy commands (keypairs under `~/testnet-ops/`, artifacts from the staged release directory, `doublezero init` to refresh the onchain version) and links the infra deploy runbook, which replaced the Notion doc. Runbook recovery guidance updated to prefer "Re-run all jobs" after mid-pipeline failures.
51+
- Fix dry runs dead-ending after `stage-programs`: the job-level skip of `push-tags` transitively skipped every downstream default-condition job (a skipped ancestor poisons `success()` for the whole graph, past `verify-cloudsmith`'s own override), so `gate-programs` through `announce` never ran in dry-run mode. The tag workflow gains a `dry_run` input and the tag jobs now run as validated no-ops instead of skipping — which also means dry runs exercise the `testnet` tag-approval prompt — and `verify-cloudsmith`'s special-case condition is deleted.
5052

5153
## [v0.29.0](https://github.com/malbeclabs/doublezero/compare/client/v0.28.0...client/v0.29.0) - 2026-07-02
5254

docs/testnet-release.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ gh workflow run release.testnet.yml -R malbeclabs/doublezero -f version=X.Y.Z
5555
| `push-tags` | Pushes the 9 component tags (`controller`, `internet-latency-collector`, `agent`, `device-telemetry-agent`, `geoprobe-agent`, `geoprobe-target`, `funder`, `monitor`, `client`) via the reusable tag workflow, which runs in the protected `testnet` environment. | **Approve the `testnet` environment prompt** on the tag jobs (nudged in the Slack thread). |
5656
| `verify-cloudsmith` | Polls CloudSmith (up to ~60 min) until all 9 packages exist at the new version. ||
5757
| `build-programs` | Builds the three Solana programs (`serviceability` default features; `telemetry` and `geolocation` with `--features testnet`) from main and uploads them with checksums and a `DEPLOY.md` manifest. ||
58-
| `stage-programs` | Dispatches the infra `stage-programs.testnet.yml` workflow, which copies the artifacts to `nyc-tn-bm2:/opt/doublezero/program-releases/vX.Y.Z/`, then pings Slack. | **Approve infra's `testnet` environment** on the dispatched run (link posted to `#bots`). Then **deploy the programs** on nyc-tn-bm2 from that directory per the Notion runbook ("Build and deploy DoubleZero solana programs - testnet"), and set the onchain version. |
58+
| `stage-programs` | Dispatches the infra `stage-programs.testnet.yml` workflow, which copies the artifacts to `nyc-tn-bm2:/opt/doublezero/program-releases/vX.Y.Z/`, then pings Slack. | **Approve infra's `testnet` environment** on the dispatched run (link posted to `#bots`). Then **deploy the programs** on nyc-tn-bm2 following the `DEPLOY.md` staged alongside them (commands mirror the [infra runbook](https://github.com/malbeclabs/infra/blob/main/docs/runbooks/deploys/solana-programs-testnet.md)), and refresh the onchain version (`doublezero init`). |
5959
| `gate-programs` | Waits on the `testnet` environment. | **Approve gate 2** once the programs are deployed and the onchain version is set. |
6060
| `verify-onchain` | Installs the released client from CloudSmith and checks `doublezero --env testnet version` reports the new program version. ||
6161
| `deploy-core` | Dispatches infra `deploy-core.testnet.yml` and waits for it. | **Approve infra's `testnet` environment** on the dispatched run (link posted to `#bots`). |
@@ -78,26 +78,41 @@ advancing the release.
7878

7979
- Both version PRs are opened as **drafts** with `[DRY RUN]` titles. Do not merge them;
8080
gate 1 only checks that they were not closed.
81-
- No tags are pushed. `verify-cloudsmith` still runs, querying the **current** (previous)
82-
version to exercise the CloudSmith query logic.
81+
- No tags are pushed: the tag jobs run as validated no-ops (`dry_run=true` on the tag
82+
workflow). They run rather than skip because a job-level skip would transitively
83+
skip every downstream job — a skipped ancestor poisons the default `success()`
84+
condition for the whole graph below it. `verify-cloudsmith` still runs, querying
85+
the **current** (previous) version to exercise the CloudSmith query logic.
8386
- Programs are still built and staged, but `verify-onchain` is skipped, and the infra
8487
deploy workflows are dispatched in their check mode (`mode=dry-run`).
85-
- The approval prompts are **not** skipped: gate 1, gate 2, the `testnet` environment
86-
on this repo, and infra's `testnet` environment all still require approval even
87-
though nothing is deployed.
88+
- The approval prompts are **not** skipped: gate 1, the `testnet` prompt on the
89+
(no-op) tag jobs, gate 2, and infra's `testnet` environment all still require
90+
approval even though nothing is deployed. A dry run exercises the same approval
91+
sequence as a real release.
8892

8993
Cleanup after a dry run: close both draft PRs and delete their branches
9094
(`release/vX.Y.Z` in doublezero, `release/testnet-vX.Y.Z` in infra).
9195

9296
## Recovery / re-running
9397

94-
Use "Re-run failed jobs" on the orchestrator run to resume from where it stopped:
98+
After a mid-pipeline failure, prefer **"Re-run all jobs"** (or a fresh dispatch) over
99+
"Re-run failed jobs". Full re-runs are cheap and always correct here; partial re-runs
100+
depend on GitHub reviving the failure-skipped downstream jobs, and a hollow
101+
"success" that silently skips deploys/QA/announce is the failure mode to avoid
102+
(run 29106365876 ended exactly that way — root cause was a dry-run-only job-skip
103+
bug, since fixed, but the conservative habit stands). After any re-run, confirm the
104+
run actually executed the jobs you expected before trusting its green check.
105+
106+
Full re-runs are safe by design:
95107

96108
- The version PRs are reused if they already exist (the branch is force-pushed and the
97109
open PR is found by head branch).
98110
- Already-pushed tags are skipped (`skip_existing=true` on the tag workflow), so a
99111
partially completed tag matrix is safe to re-run.
100-
- Environment gates prompt for approval again on re-run.
112+
- Program staging re-copies the same artifacts; environment gates prompt again.
101113

102-
If a downstream infra workflow failed, fix the cause there first; re-running the
103-
orchestrator job dispatches a fresh run of that workflow.
114+
If a dispatched infra workflow failed, fix the cause there first — the orchestrator
115+
dispatches those workflows fresh from infra `main` at runtime, so infra-side fixes
116+
apply on the next re-run without any doublezero change. Fixes to
117+
`release.testnet.yml` itself always need a fresh dispatch: any re-run (failed or all)
118+
executes the workflow snapshot from the original dispatch.

0 commit comments

Comments
 (0)