feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(ci): download macOS preview DMGs without signing in - #111

Merged
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads
Aug 27, 2026
Merged

feat(ci): download macOS preview DMGs without signing in#111
rynfar merged 2 commits into
pylonfrom
upstream/2026-08-27-preview-dmg-downloads

Conversation

@rynfar

@rynfarrynfar commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in account to download those. That breaks fetching a build onto a headless box. For a public repo the login gate adds no real protection, since any GitHub account can already download them.

The workflow now publishes each DMG as an asset on a rolling desktop-preview prerelease. Release assets download anonymously from a stable URL, so curl from a headless machine works, and the PR comment links to it.

How it stays safe and tidy:

  • The build job keeps a read-only token and still runs PR code. A new publish job holds the write token; it only downloads the built artifact and talks to the GitHub API, and never checks out PR code.
  • One rolling prerelease holds all preview DMGs. Publishing uploads an asset rather than creating a release, so release watchers get no notification.
  • Each PR keeps one DMG. A new build replaces the old one, and the -pr.N. filename marker stops one PR's build from touching another's asset.
  • A cleanup job deletes the PR's DMG and updates the comment when the PR closes or the label comes off, replacing the old 7-day artifact expiry.

Build restrictions are unchanged: same-repo PRs with the preview:mac label only, unsigned build, no Apple secrets.

Pylon adaptation

The two new jobs run on ubuntu-latest instead of upstream's blacksmith-8vcpu-ubuntu-2404, matching the GitHub-hosted move in 124630c3f. The macos-26 build runner is unchanged. Nothing else needed adapting — cherry-picked clean otherwise.

Pylon-specific checks

  • No auto-updater interaction. Pylon publishes desktop releases and the updater feed to pylon-code/pylon-releases (per the PYLON_DESKTOP_UPDATE_REPOSITORY variable). The preview release lands on pylon-code/pylon, which has zero releases, so the nightly channel's allowPrerelease: true never sees it. Publishing to the releases repo instead would have been the dangerous option.
  • No release-workflow collision.release.yml only matches v*.*.* tags; desktop-preview does not match.
  • --target "$DEFAULT_BRANCH" resolves correctly — the repo default branch is pylon.
  • The -pr.N. marker exists. Pylon's version step already produces ${base_version}-pr.${PR_NUMBER}.${GITHUB_RUN_NUMBER}, which the anti-clobber guard depends on.
  • actionlint 1.7.12: clean on pylon and clean on this branch.

Two things to decide

  1. The preview:mac label does not exist on this repo. Every run of this workflow to date has been skipped for that reason, so the feature is currently inert — before and after this change. It needs the label created before it does anything. I did not create it; say the word and I will.
  2. pylon-code/pylon currently shows no releases at all. After the first preview build, its releases page and homepage sidebar would advertise an unsigned preview DMG as the repo's only release, while real builds continue to live in pylon-code/pylon-releases. That is cosmetic, but it is the one judgment call here.

Model: Claude Opus 5. Harness: Claude Code.


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Preview DMGs upload as Actions artifacts, and GitHub requires a signed-in
account to download those, which breaks testing on headless devices. The
workflow now publishes each DMG as an asset on a rolling `desktop-preview`
prerelease, which downloads anonymously from a stable URL.
Pylon adaptation: the two new jobs run on `ubuntu-latest` rather than
upstream's Blacksmith runners, matching the GitHub-hosted move in 124630c.
Upstream: pingdotgg/t3code#8243
(cherry picked from commit 33b650a5b3b27382b35d2182dec6b22438c3da56)
@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 27, 2026
@github-actions

github-actionsBot commented Aug 27, 2026

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ProviderMetricMain baselineThis PRImpactPR ceiling
CodexTotal thread wire13.3 KiB13.3 KiB+12 B (+0.1%)15.1 KiB
CodexThread snapshot wire6.9 KiB6.9 KiB+7 B (+0.1%)7.3 KiB
CodexLive turn WebSocket wire6.4 KiB6.4 KiB+5 B (+0.1%)7.8 KiB
CodexLive turn WebSocket decoded55.5 KiB55.5 KiB0 B (0.0%)66.4 KiB
CodexLive turn messages990 (0.0%)21
ClaudeTotal thread wire13.3 KiB13.3 KiB−2 B (−0.0%)15.1 KiB
ClaudeThread snapshot wire6.9 KiB6.9 KiB+2 B (+0.0%)7.3 KiB
ClaudeLive turn WebSocket wire6.4 KiB6.4 KiB−4 B (−0.1%)7.8 KiB
ClaudeLive turn WebSocket decoded56.4 KiB56.4 KiB0 B (0.0%)66.4 KiB
ClaudeLive turn messages10100 (0.0%)21

Baseline: 3ba1857 · PR result: 9d11232 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

…sing
Adversarial review of the port found four ways this could leave a wrong final
state behind a green check. All of them come from treating a failed API call
as an answer.
`preview_eligible` was `[[ "$(gh pr view ...)" == "OPEN true" ]]`. Under
`set -e` a command substitution that fails inside `[[ ]]` yields an empty
string rather than aborting, so a transient 502 reads as "PR closed". In
publish that silently skipped a 25-minute build, or deleted the asset it had
just uploaded while the PR was open and labeled. In cleanup the same empty
string fell the other way and deleted a live download. Both now go through
`preview_state`, which retries, tells "no answer" apart from "not eligible",
and refuses to act on a guess.
Cleanup also wrote `removed=true` before touching the release, could not tell
a missing release from a broken API, and swallowed every delete error as
"already removed by a concurrent run". A 502 there left a public DMG
downloadable forever while the comment claimed it was gone. It now
distinguishes 404 from failure, tracks whether deletes actually succeeded, and
only claims removal once they did.
Publish deleted the old asset before uploading the new one. Unlike cleanup it
sits in the build concurrency group, where a fresh push does cancel it, so
that window left the PR comment pointing at a 404. Asset names carry the run
number, so uploading first and pruning after closes it with no behavior change.
Cleanup's `closed` branch no longer also requires the label. GitHub cancels a
pending run when a newer one joins its group, so unlabel-then-close can cancel
the queued unlabel cleanup and leave the close event as the only survivor,
by which point the label is gone and the DMG is stranded.
Finally, the DMG name comes out of the build, which runs PR code, and reaches
a public URL and bot-authored Markdown. It is now pinned to a plain filename,
which also closes a link-escape in the PR comment.
Verified by extracting both scripts and running them against a stubbed gh: 7
cleanup cases and 7 publish cases, including the prune deleting only the stale
asset and never its own upload or another PR's. actionlint clean.
@rynfar

Copy link
Copy Markdown
CollaboratorAuthor

Adversarial review before merge found four ways this could leave a wrong final state behind a green check. All fixed in 9d112329e. Every one is inherited from upstream rather than introduced by the Pylon adaptation, and every one traces to the same root cause: treating a failed API call as an answer.

The root cause.preview_eligible() was [[ "$(gh pr view ...)" == "OPEN true" ]]. Under set -euo pipefail, a command substitution that fails inside a condition does not trip errexit — it just yields an empty string. Confirmed locally: a failing gh produces "treated as ineligible" and the script still exits 0. So a transient 502 is indistinguishable from "the PR closed", and the two jobs then fall in opposite directions:

  • publish silently no-ops after a ~25-minute macOS build, logging a message that is actively false; or, on the post-upload re-check, deletes the asset it just uploaded while the PR is open and labeled — leaving the existing comment pointing at a 404 with no event left to fix it.
  • cleanup falls the other way: empty string is not "OPEN true", so it proceeds to delete a live download during an unlabel→relabel race.

Both now go through a preview_state helper that retries, distinguishes "no answer" from "not eligible", and refuses to act on a guess. It is duplicated in both jobs on purpose — neither may check out the repo, so there is no file to source.

Cleanup could not honor its own promise. It wrote removed=truebefore inspecting the release, could not tell a missing release from a broken API (gh release view >/dev/null 2>&1 collapses 404 and 502), and swallowed every delete error as "already removed by a concurrent run". A 502 there left a public DMG downloadable forever while the comment claimed it was gone — the exact one-way door AGENTS.md calls a bug. It now distinguishes 404 from failure, tracks whether deletes actually succeeded, fails loudly if any did not, and only claims removal once it is true.

Publish deleted before uploading. The header comment reasons carefully that a cleanup must never be cancelled mid-delete — but publish deletes too, and unlike cleanup it sits in the build concurrency group, where synchronize evaluates cancel-in-progress: true. A push landing between the delete and the upload leaves a 404 in the comment for the length of the next build. Asset names carry GITHUB_RUN_NUMBER, so uploading first and pruning after closes it with no behavior change.

Unlabel-then-close could strand a DMG permanently. GitHub cancels a pending run when a newer one joins its group. Both events share the cleanup group, so a queued unlabel cleanup gets cancelled by the close event — and the close branch's if required preview:mac, which is now gone. Nothing would ever delete that asset. The closed branch no longer requires the label; the job is idempotent and keyed by -pr.N., so an unrelated close finds nothing and stops.

Filename hardening. The DMG name comes out of the build, which runs PR code, and becomes a release asset name, a public URL, and Markdown in a bot-authored comment. A name like x-pr.42.7)](https://evil.example)(.dmg satisfies the old substring guard and escapes the link. It is now pinned to ^[A-Za-z0-9][A-Za-z0-9._-]*\.dmg$.

Verification. Both run: bodies were extracted and executed against a stubbed gh: 7 cleanup cases (deletes succeed / relabeled / PR API down / no release / release API down / asset already gone / delete 502) and 7 publish cases (happy path / closed during build / API down / markdown-escape name / wrong PR marker / no DMG / prune selectivity). The prune deletes only the stale pr.42.6 — never its own pr.42.7 upload, never PR 99's asset. actionlint reports 0 findings including shellcheck.

Confirmed clean and left alone: the -pr.N. trailing-dot reasoning genuinely holds (-pr.12. matches neither -pr.1. nor -pr.123.); the privilege boundary is sound (pull_request, not pull_request_target; build keeps contents: read; all attacker-controlled values reach github-script via env:, never ${{ }} inside run:); size:* label churn cannot trigger this workflow at all, because GITHUB_TOKEN-authored label events do not start runs; and the auto-updater cannot see the rolling prerelease, since it uses provider: "generic" and real releases live in pylon-code/pylon-releases.

Still blocking real use: the preview:mac label does not exist on this repo, so the build has never once executed here. That also makes archive: false on the artifact upload an untested assumption — if it is wrong, pattern: "*.dmg" matches nothing and the run dies loudly at "No DMG found", which is fail-safe but means the first labeled PR is the test. Worth driving one PR through label → push → unlabel → relabel → close before trusting this.

@rynfar
rynfar merged commit 1434797 into pylonAug 27, 2026
17 checks passed
@rynfar
rynfar deleted the upstream/2026-08-27-preview-dmg-downloads branch August 27, 2026 06:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@rynfar@t3dotgg