4949 # published nightly, so stable only ever ships a build that nightly users
5050 # have already run. Scheduled runs also decide here whether a nightly is due.
5151 # The PAUSE_NIGHTLY repository variable skips scheduled runs only; every later
52- # job needs this one, so a skip here skips the whole run.
52+ # job needs this one, so a skip here skips the whole run. `update_feed` runs
53+ # without it and checks the variable itself.
5354 resolve_commit :
5455 name : Resolve release commit
5556 if : github.repository == 'pingdotgg/t3code' && (github.event_name != 'schedule' || vars.PAUSE_NIGHTLY != 'true')
@@ -165,14 +166,12 @@ jobs:
165166 echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
166167 echo "make_latest=false" >> "$GITHUB_OUTPUT"
167168 elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${DISPATCH_CHANNEL:-preview}" == "preview" ]]; then
168- # Manual-only test train: exercises the whole release flow for a
169- # commit end users must never receive. Never scheduled.
169+ # Manual-only test train: exercises the release flow up to a draft
170+ # release for a commit end users must never receive. Never scheduled.
170171 # Same versioning as nightly under its own prerelease identifier.
171- # A preview release is reachable only by asking for it: npm gets it
172- # under the `preview` dist-tag, which nothing resolves by default,
173- # its desktop builds carry no update feed, and no updater manifest
174- # is attached to the release, so neither stable nor nightly
175- # installs can ever be offered one.
172+ # A preview release stays a draft (see "Publish prerelease"), its
173+ # desktop builds carry no update feed, no updater manifest is
174+ # attached, and npm gets nothing, so no install can be offered one.
176175 nightly_date="$(date -u -d "$NIGHTLY_DATE" +%Y%m%d)"
177176
178177 node scripts/resolve-nightly-release.ts \
@@ -183,7 +182,6 @@ jobs:
183182 --github-output
184183
185184 echo "release_channel=preview" >> "$GITHUB_OUTPUT"
186- echo "cli_dist_tag=preview" >> "$GITHUB_OUTPUT"
187185 echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
188186 echo "make_latest=false" >> "$GITHUB_OUTPUT"
189187 else
@@ -1038,22 +1036,93 @@ jobs:
10381036 # The action keeps a stable release a draft until its assets finish uploading, but publishes a
10391037 # prerelease first. Desktop clients update connected servers from the CLI archives as soon as
10401038 # they see the release, so a prerelease also goes public only once every asset is attached.
1039+ # Previews stay drafts, because a draft has no git tag. releases.atom lists the 10 newest tags,
1040+ # a day's `preview` tags can sort above its `nightly` ones, and older updaters find nightlies only there.
10411041 - name : Publish prerelease
1042- if : needs.preflight.outputs.is_prerelease == 'true'
1042+ if : needs.preflight.outputs.is_prerelease == 'true' && needs.preflight.outputs.release_channel != 'preview'
10431043 env :
10441044 GH_TOKEN : ${{ github.token }}
10451045 GH_REPO : ${{ github.repository }}
10461046 RELEASE_TAG : ${{ needs.preflight.outputs.tag }}
10471047 run : gh release edit "$RELEASE_TAG" --draft=false
10481048
1049+ # A nightly install finds updates in releases.atom, which holds the 10 newest git tags of any
1050+ # kind, so a burst of other tags hides every nightly. The `update-feed` release is the fixed URL
1051+ # newer installs also read. Every unpaused scheduled tick and every nightly repairs it, then fails when
1052+ # the feed's first nightly is not the newest one. See docs/operations/release.md#update-feed.
1053+ update_feed :
1054+ name : Check the update feed
1055+ needs : [preflight, release]
1056+ if : ${{ !cancelled() && github.repository == 'pingdotgg/t3code' && ((github.event_name == 'schedule' && vars.PAUSE_NIGHTLY != 'true') || needs.preflight.outputs.release_channel == 'nightly') }}
1057+ runs-on : blacksmith-2vcpu-ubuntu-2404
1058+ timeout-minutes : 5
1059+ permissions :
1060+ contents : write
1061+ steps :
1062+ - name : Checkout
1063+ uses : actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
1064+ with :
1065+ sparse-checkout : .github/scripts
1066+
1067+ - id : feed
1068+ name : Read the update feed
1069+ uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
1070+ with :
1071+ retries : 3
1072+ script : |
1073+ const { readNightlyUpdateFeed } = require('./.github/scripts/check-nightly-release.cjs');
1074+ const { tag, pointerTag, feedNightly, feedTags } = await readNightlyUpdateFeed({ github, context });
1075+ if (!tag) return;
1076+ core.info(`Newest nightly: ${tag}. Pointer: ${pointerTag}. Feed: ${feedTags?.join(', ')}.`);
1077+ core.setOutput('tag', tag);
1078+ core.setOutput('pointer_stale', pointerTag !== tag);
1079+ core.setOutput('feed', feedTags === undefined ? 'unreadable' : feedNightly === tag ? 'ok' : 'hidden');
1080+
1081+ - name : Point the update feed at the newest nightly
1082+ if : steps.feed.outputs.pointer_stale == 'true'
1083+ env :
1084+ GH_TOKEN : ${{ github.token }}
1085+ GH_REPO : ${{ github.repository }}
1086+ RELEASE_TAG : ${{ steps.feed.outputs.tag }}
1087+ run : |
1088+ gh api "repos/$GH_REPO/releases/tags/$RELEASE_TAG" --jq '{tag_name, name, body}' > nightly.json
1089+ gh release view update-feed > /dev/null 2>&1 ||
1090+ gh release create update-feed --prerelease --latest=false --title "Update feed" \
1091+ --notes "The desktop updater reads this release to find the newest nightly. Do not delete it. See docs/operations/release.md."
1092+ gh release upload update-feed nightly.json --clobber
1093+
1094+ # Every run, because the pointer can be current while the release's flags are wrong. A draft's
1095+ # files are not public, and a release marked "latest" is the one stable installs update to.
1096+ - name : Keep the update feed release public and never latest
1097+ if : steps.feed.outputs.tag != ''
1098+ env :
1099+ GH_TOKEN : ${{ github.token }}
1100+ GH_REPO : ${{ github.repository }}
1101+ run : gh release edit update-feed --draft=false --prerelease --latest=false
1102+
1103+ # An unreadable feed says nothing about its tags and the next tick reads it again, so only a
1104+ # feed that was read and names another nightly first fails the run.
1105+ - name : Fail when the feed hides the newest nightly
1106+ if : steps.feed.outputs.tag != '' && steps.feed.outputs.feed != 'ok'
1107+ env :
1108+ RELEASE_TAG : ${{ steps.feed.outputs.tag }}
1109+ FEED : ${{ steps.feed.outputs.feed }}
1110+ run : |
1111+ if [[ "$FEED" == "unreadable" ]]; then
1112+ echo "::warning::releases.atom could not be read, so this run did not check it."
1113+ exit 0
1114+ fi
1115+ echo "::error::The first nightly in releases.atom is not $RELEASE_TAG. Nightly installs older than 0.1.10-nightly.20261112.4603 cannot reach it, and neither can later builds that do not read the pointer when the feed names an older nightly. A preview from a branch not rebased onto main is the usual cause. See docs/operations/release.md#update-feed."
1116+ exit 1
1117+
10491118 # The `t3` npm package is a launcher that downloads this version's CLI
10501119 # archive from the GitHub Release, so it publishes only once that release
1051- # exists. Preview publishes too, under the `preview` dist-tag, which
1052- # nothing resolves unless asked for by name .
1120+ # exists. A preview release stays a draft, whose files are not public, so
1121+ # a preview never publishes the launcher .
10531122 publish_cli :
10541123 name : Publish CLI to npm
10551124 needs : [preflight, build_bundle, release]
1056- if : ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }}
1125+ if : ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel != 'preview' }}
10571126 runs-on : ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
10581127 timeout-minutes : 15
10591128 permissions :
0 commit comments