Skip to content

Release: promote main's CI and Mobile runs instead of racing them - #151

Open
itz4blitz wants to merge 4 commits into
mainfrom
ci/release-promotion
Open

itz4blitz wants to merge 4 commits into
mainfrom
ci/release-promotion

Conversation

@itz4blitz

Copy link
Copy Markdown
Collaborator

Release promotes main's runs instead of racing them.

  • wait-for-ci finds main's CI run (ci.yml, push, main, this SHA), retrying for a few minutes on a tag since both pushes start together, and waits for it. A failed run fails the release, so nothing is published from a commit main rejected. No run (docs-only commit, branch dispatch) means compiling as before.
  • macOS, Linux (x86_64 and aarch64) and Windows get the run id and reuse its packages through reuse-binaries.sh; a missing package still falls back to compiling. aarch64 reuse starts once ci/speed adds tinyclient-linux-aarch64.
  • phones does the same with the Mobile run, dispatching mobile.yml on the ref when main never ran it, and uploads tinyclient-$LABEL-android-debug.apk and tinyclient-$LABEL-ios-unsigned.zip as release-phones. On a tag those names match the existing release assets, and publish adds unversioned copies (tinyclient-android-debug.apk) like every other file.
  • mobile.yml: no tag trigger, no attach-to-release.

Checked locally against real runs: 1d4acaa -> run 41479383097, success, id passed on; f542c04 -> run 41506153717, failure, exit 1.

🤖 Generated with Claude Code

itz4blitz and others added 4 commits November 2, 2026 22:30
The tag push and main's push of the same commit start together, so the
old lookup for a successful CI run always came up empty and every
platform recompiled. Release also published commits whose main CI
failed, and Mobile rebuilt the phones on the tag only to time out
waiting for the release.

A wait-for-ci job now finds main's CI run for the commit, waits for it,
and hands its id to the build jobs; a failed run fails the release, and
no run means compiling as before. A phones job does the same with the
Mobile run (starting one on the ref when main never ran it) and adds the
APK and the unsigned iOS app to the release. Mobile no longer runs on
tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dispatched Mobile run took 2h24m while macOS runners queued, past the
phones job's 120 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two jobs polling every 30 seconds came close to the token's hourly API
allowance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@itz4blitz
itz4blitz force-pushed the ci/release-promotion branch from bc49e7a to ca0d3f8 Compare November 3, 2026 04:04

@unRekable unRekable left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Approved ✅ (0 critical, 2 warnings — both are deliberate fail-closed residue, not correctness bugs) — read both scripts in full and compared release.yml/mobile.yml against origin/main.

⚠️ Warnings

  • wait-for-run.sh:31 — the "never publish from a rejected commit" guarantee is still probabilistic: if main's push run takes longer than the ~3.5 min search window to register, run= comes back empty and the release silently compiles itself, publishing alongside a CI run that could later fail. A final re-check of the run's conclusion in publish (or a longer window) would make the guarantee unconditional.
  • wait-for-run.sh:30 — a transient gh failure (rate limit, network blip) inside the search propagates via set -eu and fails the whole release job, which then skips every build. Fail-closed is the right direction, but the loop only retries "run absent", not API errors.

💡 Suggestions

  • wait-for-run.sh:44 — on a "Re-run all jobs" of a release that dispatched a phone build, the search (push-only) comes up empty and gh workflow run fires a second ~2.5 h Mobile run for the same SHA. Correctness is fine (the newest run is still downloaded); when --dispatch is set, probing for an existing workflow_dispatch run first avoids the duplicate.
  • release.yml:59 — timeout-minutes: 240 for the CI wait is tighter than the phones job's 300, while the comment above itself mentions macOS queues over an hour; aligning at 300 removes an avoidable late release failure.

✅ Looks Good

  • The gate is fail-closed and the conclusion is read back from the run rather than trusting gh run watch's exit status; drop-outs are resumed; non-success exits 1 and blocks publish through needs. Two long waits at 60 s cadence stay well inside the token's API allowance.
  • Artifact reuse is honest per platform: all four binaries are verified present (and executable) before an artifact is trusted; anything missing falls back to compiling just that platform. Artifact names match ci.yml exactly, including the new linux-aarch64 arm.
  • The phones job fully replaces the old tag-triggered attach-to-release (which raced release.yml by design); versioned/unversioned names and SHA256SUMS coverage match the existing release pipeline, so there is no double-publish.
  • Source-run selection is pinned to --event push --branch main (or the self-dispatched run), so PR runs and other events can never feed binaries into a release; no pull_request trigger on release.yml.
  • Permissions stay least-privilege: contents: read/actions: read by default, actions: write granted only to phones.

Your local validation against real runs (success and failure cases) covers the happy path; the two warnings are follow-up hardening for the rare tails. Good, well-commented change.

run=
attempt=1
while :; do
run=$(newest --event push --branch main)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Warning: with a 10x20s search window, a slowly-registered main run (GitHub event lag) makes run= empty and the release compiles itself — publishing in parallel with a CI run that could later fail, which is exactly the race this PR is titled against. Low probability, and fail-closed otherwise; a final re-check of the source run's conclusion inside publish (or a longer search window) would make the guarantee unconditional.


run=
attempt=1
while :; do

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Warning: any transient gh error here (rate limit, network blip) propagates via set -eu and fails the whole release after possibly hours of waiting. Fail-closed is correct, but the loop retries only "run absent", not API errors — retrying those too (or a single retry-after-60s) would save an otherwise doomed run.

since=$(date -u -d "@$(($(date +%s) - 60))" +%Y-%m-%dT%H:%M:%SZ)
echo "wait-for-run.sh: no $workflow run on main for $sha; starting one on $GITHUB_REF_NAME"
gh workflow run "$workflow" --ref "$GITHUB_REF_NAME"
for attempt in 1 2 3 4 5 6 7 8 9 10; do

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 Suggestion: on a "Re-run all jobs" of a release that dispatched a phone build, this push-only search comes up empty again and gh workflow run fires a second ~2.5 h Mobile run for the same SHA. Correctness is fine (the newest run is still downloaded), but when --dispatch is set, probing for an existing --event workflow_dispatch run for the SHA first avoids the duplicate.

wait-for-ci:
if: github.event_name == 'push' || inputs.announce == ''
runs-on: ubuntu-latest
timeout-minutes: 240

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

💡 Suggestion: timeout-minutes: 240 is tighter than the phones job's 300, and the comment above itself mentions macOS queues over an hour — a worst-case queue could fail the release at the last step. Aligning at 300 costs nothing.

# The run is the one wait-for-run.sh found and waited for; release.yml passes its id.
# found=true means target/release holds tinyclient, chatd, mediad and tinyclient-login.
# A commit CI did not build (or whose CI failed) leaves found=false, and the caller compiles.
# With no run id, or a run without this platform's package, found=false and the caller compiles.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ Nice: the take() side verifies all four binaries exist (and are executable / present as .exe) after extraction before trusting the artifact, and any missing piece falls back to compiling just that one platform — a partial extraction can never pass as a built stage. Solid design.

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