fix: prevent install/update 404 on releases caught mid-publish - #18
Conversation
…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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughBuilders 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. ChangesRelease Asset Coordination and Promotion
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 winPin
softprops/action-gh-releaseto a full commit SHA.
In.github/workflows/release-pythinker-cli.yml, the release step usessoftprops/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
📒 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.ymlAGENTS.mddocs/public/install.ps1docs/public/install.shdocs/scripts/sync-changelog.mjsscripts/install-native.shscripts/install.ps1tests/test_release_update_pipeline.pyweb/public/install.ps1web/public/install.sh
- 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.
|
Re: CodeRabbit's suggestion to pin |
Problem
Windows
irm https://pythinker.com/install.ps1 | iex404s onPythinkerSetup-<ver>.exewhen run shortly after a release.install.ps1(andinstall-native.sh) resolveGET /releases/latest, which is date-based — it returns a release the instant it is published, before all platform assets finish uploading. The Windows.exeis the last asset attached (verified on v0.25.0: published19:43:32Z,.exeuploaded19:44:43Z), so any install in that ~71s window 404s.The
make_latestgate intended to prevent this was dead code: it ran only onrelease: published, which aGITHUB_TOKEN-created release never fires, andmake_latestdoes not affect/releases/latestanyway (onlydraft/prereleaseexclude a release from it).Fix
Pipeline (root cause)
prerelease: "true", keeping it out of/releases/latestfor every consumer (install scripts, in-app updater, direct/latest/download).promote-release.ymlruns on the tag push (so it always fires), waits for all 9 platform asset fragments, then clearsprerelease+ setsmake_latest— the single atomic point a version becomes resolvable.workflow_dispatchallows manual re-promote.dispatch-pythinker-home-sync.ymldrops the deadrelease:trigger.Installers (defense-in-depth)
install.ps1resolves the newest release that actually carriesPythinkerSetup-<ver>.exe+.sha256(skips draft/prerelease) — never 404s on a mid-publish release.install-native.shwaits for this version's archive + checksum before downloading. Mirrors the readiness gate the in-app updater already has.Verification
install.ps1resolver: live (→0.25.0, asset HEAD 200) + 5 fabricated cases (mid-publish / missing-sha / prerelease / draft all fall back; newest-complete picked).pwshparse OK.install-native.sh:bash -nOK + live present/absent check.promote-release.yml's 9-assetrequired[]list confirmed against v0.25.0's actual assets.test_release_update_pipeline.pyupdated to the new architecture + install-script readiness test added).Operator notes
/releases/latest); recover viapromote-release.yml→ Run workflow.Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests