Skip to content

fix: prevent install/update 404 on releases caught mid-publish - #18

Merged
elkaix merged 3 commits into
mainfrom
fix/release-asset-race-windows-install
May 29, 2026
Merged

fix: prevent install/update 404 on releases caught mid-publish#18
elkaix merged 3 commits into
mainfrom
fix/release-asset-race-windows-install

Conversation

@elkaix

@elkaix elkaix commented May 29, 2026

Copy link
Copy Markdown
Member

Problem

Windows irm https://pythinker.com/install.ps1 | iex 404s on PythinkerSetup-<ver>.exe when run shortly after a release.

install.ps1 (and install-native.sh) resolve GET /releases/latest, which is date-based — it returns a release the instant it is published, before all platform assets finish uploading. The Windows .exe is the last asset attached (verified on v0.25.0: published 19:43:32Z, .exe uploaded 19:44:43Z), so any install in that ~71s window 404s.

The make_latest gate intended to prevent this was dead code: it ran only on release: published, which a GITHUB_TOKEN-created release never fires, and make_latest does not affect /releases/latest anyway (only draft/prerelease exclude a release from it).

Fix

Pipeline (root cause)

  • The three build workflows now create the Release as prerelease: "true", keeping it out of /releases/latest for every consumer (install scripts, in-app updater, direct /latest/download).
  • New promote-release.yml runs on the tag push (so it always fires), waits for all 9 platform asset fragments, then clears prerelease + sets make_latest — the single atomic point a version becomes resolvable. workflow_dispatch allows manual re-promote.
  • dispatch-pythinker-home-sync.yml drops the dead release: trigger.

Installers (defense-in-depth)

  • install.ps1 resolves the newest release that actually carries PythinkerSetup-<ver>.exe + .sha256 (skips draft/prerelease) — never 404s on a mid-publish release.
  • install-native.sh waits for this version's archive + checksum before downloading. Mirrors the readiness gate the in-app updater already has.

Verification

  • install.ps1 resolver: live (→0.25.0, asset HEAD 200) + 5 fabricated cases (mid-publish / missing-sha / prerelease / draft all fall back; newest-complete picked). pwsh parse OK.
  • install-native.sh: bash -n OK + live present/absent check.
  • All 19 workflow YAMLs valid; promote-release.yml's 9-asset required[] list confirmed against v0.25.0's actual assets.
  • 66 pytest pass (test_release_update_pipeline.py updated to the new architecture + install-script readiness test added).

Operator notes

  • Re-running a single builder against an already-promoted tag flips it back to prerelease (drops from /releases/latest); recover via promote-release.yml → Run workflow.
  • If a build target permanently fails, promotion times out (20 min, Slack alert) and the release stays prerelease — fail-closed: users keep the prior version rather than a broken one.

Summary by CodeRabbit

  • New Features

    • Installers now wait for platform assets to be fully published before downloading.
    • Releases are initially created as prereleases and promoted to “latest” only after all platform assets are present.
  • Bug Fixes

    • Prevents installer failures caused by mid-publish/partial asset availability.
  • Documentation

    • Standardized 0.MINOR.PATCH versioning and updated release procedures.
  • Tests

    • Added/updated tests to validate prerelease/promote behavior and install-script gating.

Review Change Stack

elkaix added 2 commits May 29, 2026 15:52
…cess

AGENTS.md documented a stale Keep-a-Changelog style that no longer matches
the repo: a MAJOR.MINOR.PATCH scheme with 1.x examples, bump-* branches,
renaming [Unreleased] to [X.Y] - YYYY-MM-DD, and git tag X.Y / git push --tags.
The actual process is 0.MINOR.PATCH, release/X.Y.Z branches, a retained empty
## Unreleased plus a ## X.Y.Z (YYYY-MM-DD) section, the npm run sync step for
the docs changelog, and annotated vX.Y.Z tags. Correct the Versioning and
Release workflow sections to match.

Also drop the now-dead title-rewrite and subsection-strip transforms in
sync-changelog.mjs: the root CHANGELOG already uses ## X.Y.Z (date) headers and
### What changed in this release subsections, so both regexes were no-ops. The
generated docs changelog is byte-identical before and after this change.
install.ps1 and install-native.sh resolve GET /releases/latest, which is
date-based and returns a release the instant it is published — before all
platform assets finish uploading. The Windows .exe is the last asset attached
(~70s after publish), so `irm | iex` run in that window 404s on
PythinkerSetup-<ver>.exe. The make_latest gate meant to prevent this was dead
code: it ran only on `release: published`, which a GITHUB_TOKEN-created
release never fires, and make_latest does not affect /releases/latest anyway.

Pipeline (root fix): the build workflows now create the Release as a
prerelease, keeping it out of /releases/latest for every consumer. A new
promote-release.yml runs on the tag push, waits for all platform assets, then
clears prerelease and marks the release latest — the single point a version
becomes resolvable by the installers and the in-app updater.
dispatch-pythinker-home-sync.yml drops the dead release trigger.

Installers (defense-in-depth): install.ps1 resolves the newest release that
actually carries PythinkerSetup-<ver>.exe + .sha256 (skipping draft and
prerelease); install-native.sh waits for its archive + checksum before
downloading, mirroring the readiness gate the in-app updater already has.
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c3e2b76a-afd5-4ace-8cd0-18b6e5ca48df

📥 Commits

Reviewing files that changed from the base of the PR and between b0a03a2 and 68751f3.

📒 Files selected for processing (5)
  • .github/workflows/promote-release.yml
  • AGENTS.md
  • docs/public/install.sh
  • scripts/install-native.sh
  • web/public/install.sh

📝 Walkthrough

Walkthrough

Builders mark per-platform releases as prerelease; a new promote-release workflow polls until all platform assets exist, then clears prerelease and marks latest. Dispatch workflow triggers and installer scripts are updated to rely on asset readiness checks. Docs and tests updated to reflect the new release coordination.

Changes

Release Asset Coordination and Promotion

Layer / File(s) Summary
Release promotion workflow orchestration
.github/workflows/promote-release.yml
New workflow validates semver tags, polls the release-by-tag API until required platform assets appear, PATCHes the release to clear prerelease and set make_latest=true, conditionally dispatches pythinker-home, and posts Slack failure alerts.
Dispatch workflow transition
.github/workflows/dispatch-pythinker-home-sync.yml
Removes release: published trigger, restricts to workflow_dispatch and push on main with file filters, reduces contents permission to read, and sets RELEASE_TAG to github.sha.
Builder workflows mark releases as prerelease
.github/workflows/release-pythinker-cli.yml, .github/workflows/linux-installer.yml, .github/workflows/windows-installer.yml
Builder workflows now pass prerelease: "true" when creating/updating the GitHub Release to avoid premature inclusion in /releases/latest during multi-platform uploads.
Installation scripts gate on asset readiness
docs/public/install.ps1, docs/public/install.sh, scripts/install.ps1, scripts/install-native.sh, web/public/install.ps1, web/public/install.sh
All installer variants add release_has_assets() checks against the release-by-tag API to confirm the platform archive and its .sha256 are present. Scripts retry with 10s sleeps (fail after attempts) before downloading to avoid 404s during mid-publish windows.
Release procedure documentation and validation tests
AGENTS.md, docs/scripts/sync-changelog.mjs, tests/test_release_update_pipeline.py
AGENTS.md documents the 0.MINOR.PATCH scheme and release branch/tag flow; changelog sync preserves headers verbatim; tests assert builders mark prerelease, promote-release gates on assets before marking latest, and install scripts gate on readiness and keep canonical copies under docs/public/web/public.

Sequence Diagram(s)

sequenceDiagram
  participant TagPush
  participant PromoteWorkflow
  participant GitHubAPI
  participant PythinkerHomeRepo
  
  TagPush->>PromoteWorkflow: trigger vX.Y.Z push
  PromoteWorkflow->>PromoteWorkflow: resolve and validate semver tag
  PromoteWorkflow->>GitHubAPI: poll /releases/tags/{tag} for all platform assets
  GitHubAPI-->>PromoteWorkflow: asset list (retry with delays until complete)
  PromoteWorkflow->>GitHubAPI: PATCH release: prerelease=false, make_latest=true
  GitHubAPI-->>PromoteWorkflow: release promoted
  PromoteWorkflow->>PythinkerHomeRepo: dispatch sync event with tag
Loading
sequenceDiagram
  participant User
  participant Installer
  participant GitHubAPI
  participant Download
  
  User->>Installer: run install script
  Installer->>Installer: fetch/filter latest non-prerelease tag
  Installer->>GitHubAPI: query /releases/tags/{tag} for expected assets
  loop retry up to 5 times (10s interval)
    GitHubAPI-->>Installer: check tarball + .sha256 presence
    alt all assets found
      Installer->>Download: proceed to download and verify
    else assets incomplete
      Installer->>Installer: sleep 10s, retry
    end
  end
  Download-->>User: download complete or fail with guidance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

bug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format (type: description) and clearly summarizes the main change: preventing install 404 errors on releases caught mid-publish.
Description check ✅ Passed Description comprehensively documents the problem, root cause, fix across pipeline and installers, verification steps, and operator notes. All template sections are addressed with substantive detail.
Docstring Coverage ✅ Passed Docstring coverage is 71.43% which is sufficient. The required threshold is 70.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/release-asset-race-windows-install

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/release-pythinker-cli.yml (1)

540-553: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pin softprops/action-gh-release to a full commit SHA.
In .github/workflows/release-pythinker-cli.yml, the release step uses softprops/action-gh-release@v2 (mutable tag); pin it to an immutable full commit SHA to reduce supply-chain risk.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-pythinker-cli.yml around lines 540 - 553, The
workflow step named "Create GitHub Release and upload assets" currently uses the
mutable tag softprops/action-gh-release@v2; replace that with the action pinned
to an immutable full commit SHA (the specific commit SHA for
softprops/action-gh-release) so the workflow uses a fixed reference. Locate the
step by its name and the uses field (softprops/action-gh-release@v2) and update
the uses value to the exact commit SHA (e.g.,
softprops/action-gh-release@<full-commit-sha>) and commit the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/promote-release.yml:
- Around line 26-27: Move the top-level permissions block out of global scope
and instead add a scoped permissions entry granting contents: write only for the
promote job and minimal permissions for notify-failure; specifically, remove or
reduce the global permissions: contents: write and add a permissions: {
contents: write } under the promote job definition and minimally scoped
permissions under notify-failure so other jobs do not get write access. Update
any other instances (lines near 30-31 and 127-130) to follow the same pattern.
- Around line 15-25: Add a top-level concurrency block to serialize runs
per-release tag using a group name derived from the tag (covering both
push-triggered tags and manual workflow_dispatch). For example, add concurrency
with group set to something like promote-${{ github.ref_name ||
github.event.inputs.tag }} and cancel-in-progress: true at the root of the
workflow so overlapping runs for the same tag are serialized; also add the same
concurrency expression where similar dispatch logic appears later (the section
referenced around lines 29-31).
- Around line 136-139: The TAG env currently uses github.ref_name which for
workflow_dispatch can be a branch, so change the TAG assignment to prefer the
resolved input (github.event.inputs.tag) and fall back to github.ref_name;
update the TAG line to use an expression like github.event.inputs.tag ||
github.ref_name so failure notifications and Slack messages use the promoted tag
for manual runs; adjust any places referencing TAG (e.g., RUN_URL, REPO, TAG) to
rely on this updated TAG value.

In @.github/workflows/windows-installer.yml:
- Around line 92-103: The workflow step that currently uses the mutable
reference "uses: softprops/action-gh-release@v2" should be pinned to an
immutable full commit SHA; locate the step containing the "uses:
softprops/action-gh-release@v2" entry (the Attach to Release create-or-update
step using inputs tag_name, prerelease, fail_on_unmatched_files, make_latest)
and replace the "`@v2`" tag with the exact commit SHA for the desired release of
softprops/action-gh-release, keeping the existing input keys (tag_name,
prerelease, fail_on_unmatched_files, make_latest) unchanged so behavior remains
identical.

In `@AGENTS.md`:
- Around line 372-390: Add a new subsection explaining the prerelease/promotion
flow: describe that builders create GitHub releases with prerelease: "true"
during concurrent uploads (why builders mark prerelease), explain the
promote-release.yml workflow (what it does, that it waits for all 9 platform
assets before clearing prerelease and marking the release as latest), specify
the atomic promotion point (the step that flips prerelease->false once all
assets are present), and include recovery/run-after-promotion guidance (how to
handle a builder re-run that would attempt to re-create or flip prerelease,
including the mechanism to detect already-promoted releases and flip back to
prerelease if safe or skip and surface errors). Reference the terms builders,
prerelease: "true", promote-release.yml and the “9 platform assets” so operators
can map instructions to the automation.

In `@scripts/install-native.sh`:
- Around line 226-227: The asset name checks in release_has_assets use grep -q
which treats ${tarball} as a regex and can mis-match names containing dots;
update both checks that search _body for the tarball and the tarball.sha256 to
use fixed-string matching (grep -Fq) instead of grep -q so the literal value of
${tarball} and \"${tarball}.sha256\" are matched exactly; modify the lines that
currently call grep -q to call grep -Fq for both occurrences (referencing the
variables _body and ${tarball} in the release_has_assets logic).

In `@web/public/install.sh`:
- Around line 226-227: Replace the regex greps that use "\"name\":
*\"${tarball}\"" with fixed-string greps so the dot in ${tarball} is treated
literally: change the two checks to use grep -F -q and match the exact literals
"\"name\": \"${tarball}\"" and "\"name\": \"${tarball}.sha256\"" (operate on the
same _body variable), e.g. update the two grep invocations around _body and
tarball to use grep -F -q with those exact strings.

---

Outside diff comments:
In @.github/workflows/release-pythinker-cli.yml:
- Around line 540-553: The workflow step named "Create GitHub Release and upload
assets" currently uses the mutable tag softprops/action-gh-release@v2; replace
that with the action pinned to an immutable full commit SHA (the specific commit
SHA for softprops/action-gh-release) so the workflow uses a fixed reference.
Locate the step by its name and the uses field (softprops/action-gh-release@v2)
and update the uses value to the exact commit SHA (e.g.,
softprops/action-gh-release@<full-commit-sha>) and commit the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2045ae4a-8fb9-4c70-9f47-9a7c520b30ec

📥 Commits

Reviewing files that changed from the base of the PR and between cbab4e6 and b0a03a2.

📒 Files selected for processing (14)
  • .github/workflows/dispatch-pythinker-home-sync.yml
  • .github/workflows/linux-installer.yml
  • .github/workflows/promote-release.yml
  • .github/workflows/release-pythinker-cli.yml
  • .github/workflows/windows-installer.yml
  • AGENTS.md
  • docs/public/install.ps1
  • docs/public/install.sh
  • docs/scripts/sync-changelog.mjs
  • scripts/install-native.sh
  • scripts/install.ps1
  • tests/test_release_update_pipeline.py
  • web/public/install.ps1
  • web/public/install.sh

Comment thread .github/workflows/promote-release.yml
Comment thread .github/workflows/promote-release.yml Outdated
Comment thread .github/workflows/promote-release.yml
Comment thread .github/workflows/windows-installer.yml
Comment thread AGENTS.md
Comment thread scripts/install-native.sh Outdated
Comment thread web/public/install.sh Outdated
- promote-release.yml: serialize per-tag with a concurrency group (push vs
  manual re-promote), scope `contents: write` to the promote job only
  (notify-failure is read-only), and report the resolved tag in failure
  notifications for manual runs.
- install-native.sh: match release asset names with fixed strings (grep -Fq)
  so the dots in the version don't act as regex wildcards.
- AGENTS.md: document the prerelease -> promote release-asset flow.
@elkaix

elkaix commented May 29, 2026

Copy link
Copy Markdown
Member Author

Re: CodeRabbit's suggestion to pin softprops/action-gh-release@v2 to a full commit SHA — deferring, with reason. Every release workflow in this repo (release-pythinker-cli, linux-installer, windows-installer, plus the core/host/sdk release workflows) uses the mutable @v2 tag. Pinning only the two files this PR touches would be inconsistent and leave the rest unpinned. SHA-pinning is worth doing repo-wide in a dedicated supply-chain-hardening PR, not folded into this race-condition fix. All other actionable findings (concurrency, scoped permissions, resolved-tag in notifications, fixed-string asset matching, AGENTS.md docs) are addressed in 68751f3.

@elkaix
elkaix merged commit e76da39 into main May 29, 2026
27 checks passed
@elkaix
elkaix deleted the fix/release-asset-race-windows-install branch May 29, 2026 21:04
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.

1 participant