mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(ci): stop hourly versions dropping below a tagged or already-shipped build (#20699)
* fix(ci): stop hourly versions dropping below a tagged or already-shipped build Hourly/daily/adhoc based their X.Y.Z on GitHub releases, not git tags. When v1.4.202 was tagged and then its GitHub release vanished, the next hourlies shipped as 1.4.202-hourly — below both stable 1.4.202 and the 1.4.203-hourly builds already installed, so electron-updater stopped offering updates. Read main's v* tags and already-published channel tags instead. * docs(ci): record that 1.4.202's release was unpublished for a bug The leftover tag is what hourly must still honor; this was not a failed cut.
This commit is contained in:
@@ -235,11 +235,14 @@ jobs:
|
||||
fi
|
||||
done
|
||||
echo "head_sha=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT"
|
||||
# Why the main repo's tags: package.json on a branch is as stale as the
|
||||
# main it forked from, and stable patches never merge back into it.
|
||||
published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh release list \
|
||||
--repo "$GITHUB_REPOSITORY" --limit 100 --exclude-drafts \
|
||||
--json tagName --jq '.[].tagName' || true)"
|
||||
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
|
||||
# GitHub release and leaves the tag, which still owns that number.
|
||||
# Releases-only let adhoc sit on a number already taken, so the updater
|
||||
# would not install it. Empty on failure — the script then falls back
|
||||
# to package.json.
|
||||
published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
|
||||
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
|
||||
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
|
||||
ORCA_PUBLISHED_VERSIONS="$published" ORCA_ADHOC_LABEL="${LABEL:-$REF}" \
|
||||
node config/scripts/adhoc-build-version.mjs \
|
||||
>"$RUNNER_TEMP/adhoc-identity.txt"
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
# Version helpers only read HEAD; published versions come from the release API.
|
||||
# Version helpers only read HEAD; published versions come from git tags.
|
||||
fetch-depth: 1
|
||||
# Why: this job only reads stablyai/orca and never pushes; every write
|
||||
# goes to the daily repo through a minted App token passed by env.
|
||||
@@ -209,17 +209,21 @@ jobs:
|
||||
# number free", where a stranded draft still holds one.
|
||||
names="$(gh release list --repo "$DAILY_REPO" --limit 200 --json name \
|
||||
--jq '.[].name // empty')"
|
||||
# Why the main repo's tags decide the base version rather than
|
||||
# package.json: main's version only moves on `release:` commits, and
|
||||
# stable patches are cut from release branches that never merge back, so
|
||||
# package.json can sit several patches behind what users are running. A
|
||||
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
|
||||
# GitHub release and leaves the tag. That dragged hourlies backwards so
|
||||
# electron-updater stopped offering them; dailies would do the same. A
|
||||
# separate token because GH_TOKEN above is the App's, scoped to the
|
||||
# daily repo. Empty on failure — the script then falls back to
|
||||
# package.json, which is stale but never wrong enough to fail a build.
|
||||
published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh release list \
|
||||
--repo "$GITHUB_REPOSITORY" --limit 100 --exclude-drafts \
|
||||
--json tagName --jq '.[].tagName' || true)"
|
||||
echo "Highest published tag seen: $(head -1 <<<"$published")"
|
||||
main_tags="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
|
||||
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
|
||||
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
|
||||
# Already-shipped channel tags are a second floor so unpublishing a
|
||||
# buggy main release cannot drag this series below a daily already out.
|
||||
channel_tags="$(gh release list --repo "$DAILY_REPO" --limit 200 --json tagName \
|
||||
--jq '.[].tagName' || true)"
|
||||
published="$main_tags"$'\n'"$channel_tags"
|
||||
echo "Published version sources: $(grep -c . <<<"$main_tags" || true) main tags, $(grep -c . <<<"$channel_tags" || true) channel tags"
|
||||
ORCA_PUBLISHED_VERSIONS="$published" ORCA_DAILY_RELEASE_NAMES="$names" \
|
||||
node config/scripts/daily-build-version.mjs \
|
||||
>"$RUNNER_TEMP/daily-identity.txt"
|
||||
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preflight.outputs.head_sha }}
|
||||
# Version helpers only read HEAD; published versions come from the release API.
|
||||
# Version helpers only read HEAD; published versions come from git tags.
|
||||
fetch-depth: 1
|
||||
# Why: this job only reads stablyai/orca and never pushes; every write
|
||||
# goes to the hourly repo through a minted App token passed by env.
|
||||
@@ -213,17 +213,25 @@ jobs:
|
||||
# number free", where a stranded draft still holds one.
|
||||
names="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json name \
|
||||
--jq '.[].name // empty')"
|
||||
# Why the main repo's tags decide the base version rather than
|
||||
# package.json: main's version only moves on `release:` commits, and
|
||||
# stable patches are cut from release branches that never merge back, so
|
||||
# package.json can sit several patches behind what users are running. A
|
||||
# separate token because GH_TOKEN above is the App's, scoped to the
|
||||
# hourly repo. Empty on failure — the script then falls back to
|
||||
# package.json, which is stale but never wrong enough to fail a build.
|
||||
published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh release list \
|
||||
--repo "$GITHUB_REPOSITORY" --limit 100 --exclude-drafts \
|
||||
--json tagName --jq '.[].tagName' || true)"
|
||||
echo "Highest published tag seen: $(head -1 <<<"$published")"
|
||||
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
|
||||
# GitHub release and leaves the tag. On 2026-09-14 we deleted v1.4.202's
|
||||
# release for a bug; hourlies had already climbed to 1.4.203, then
|
||||
# `gh release list` fell back to v1.4.201 and the next hourlies shipped
|
||||
# as 1.4.202-hourly — which electron-updater will not install over
|
||||
# 1.4.203-hourly or over the still-tagged 1.4.202. A separate token
|
||||
# because GH_TOKEN above is the App's, scoped to the hourly repo. Empty
|
||||
# on failure — the script then falls back to package.json, which is
|
||||
# stale but never wrong enough to fail a build.
|
||||
main_tags="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
|
||||
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
|
||||
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
|
||||
# Already-shipped channel tags are a second floor: even if main's tag
|
||||
# list is empty this run, a 1.4.203-hourly already out must not be
|
||||
# followed by a 1.4.202-hourly.
|
||||
channel_tags="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json tagName \
|
||||
--jq '.[].tagName' || true)"
|
||||
published="$main_tags"$'\n'"$channel_tags"
|
||||
echo "Published version sources: $(grep -c . <<<"$main_tags" || true) main tags, $(grep -c . <<<"$channel_tags" || true) channel tags"
|
||||
ORCA_PUBLISHED_VERSIONS="$published" ORCA_HOURLY_RELEASE_NAMES="$names" \
|
||||
node config/scripts/hourly-build-version.mjs \
|
||||
>"$RUNNER_TEMP/hourly-identity.txt"
|
||||
|
||||
@@ -25,8 +25,11 @@ function compareTriples(a, b) {
|
||||
* 2026-08-03 main read `1.4.165-rc.0` for twenty hours while 1.4.165, 1.4.166 and
|
||||
* 1.4.167 all shipped — so hourlies built from that main claimed 1.4.165 while
|
||||
* carrying code newer than 1.4.167, and sorted *below* the stable their user was
|
||||
* already running. Published tags are the only honest answer to "what number is
|
||||
* taken"; package.json is a floor, not a source of truth.
|
||||
* already running. Git tags (not GitHub releases) are the honest answer to "what
|
||||
* number is taken": unpublishing a buggy cut deletes the GitHub release and
|
||||
* leaves the tag, which still owns that number. Channel tags (`1.4.203-hourly.*`)
|
||||
* are a second floor so that unpublish cannot drag the series backwards.
|
||||
* package.json is a floor, not a source of truth.
|
||||
*/
|
||||
export function resolveDevChannelBaseVersion(packageVersion, publishedVersions = []) {
|
||||
const fromPackage = parseVersionTriple(packageVersion)
|
||||
|
||||
@@ -39,6 +39,27 @@ describe('dev channel base version', () => {
|
||||
)
|
||||
})
|
||||
|
||||
// Why tags rather than GitHub releases: unpublishing a buggy cut deletes the
|
||||
// GitHub release and leaves the tag. Releases-only then treated 1.4.202 as
|
||||
// free, so hourlies sat on 1.4.202-hourly and sorted below that tagged stable.
|
||||
it('climbs past a tagged stable that has no GitHub release', () => {
|
||||
expect(resolveDevChannelBaseVersion('1.4.197', ['v1.4.201', 'v1.4.202'])).toBe('1.4.203')
|
||||
})
|
||||
|
||||
// 2026-09-14: v1.4.202's GitHub release was deleted for a bug after hourlies
|
||||
// had already shipped as 1.4.203. Without the channel tags as a floor, the
|
||||
// next hourlies would have been 1.4.202-hourly, which electron-updater will
|
||||
// not install over 1.4.203-hourly.
|
||||
it('does not drop below an already-published channel version', () => {
|
||||
expect(
|
||||
resolveDevChannelBaseVersion('1.4.197', [
|
||||
'v1.4.201',
|
||||
'v1.4.202-hourly.202609141912',
|
||||
'v1.4.203-hourly.202609140417'
|
||||
])
|
||||
).toBe('1.4.203')
|
||||
})
|
||||
|
||||
it('treats package.json as a floor when it leads the tags', () => {
|
||||
expect(resolveDevChannelBaseVersion('1.5.0-rc.0', ['v1.4.167'])).toBe('1.5.0')
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
createHourlyBuildVersion,
|
||||
formatHourlyReleaseName,
|
||||
getHourlyBuildIdentity,
|
||||
nextHourlyBuildNumber
|
||||
} from './hourly-build-version.mjs'
|
||||
import { compareAppVersions } from '../../src/shared/app-version'
|
||||
@@ -120,3 +121,26 @@ describe('nextHourlyBuildNumber', () => {
|
||||
expect(nextHourlyBuildNumber('1.4.163', ['v1.4.163-hourly.202607311354', null, ''])).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getHourlyBuildIdentity', () => {
|
||||
// 2026-09-14: v1.4.202's GitHub release was deleted for a bug after hourlies
|
||||
// had climbed to 1.4.203. Passing the leftover tag and the already-shipped
|
||||
// hourly keeps the next build on 1.4.203 so electron-updater will still
|
||||
// install it.
|
||||
it('stays on the already-shipped hourly base after a buggy main release is unpublished', () => {
|
||||
const identity = getHourlyBuildIdentity(new Date('2026-09-14T20:00:00Z'), {
|
||||
publishedVersions: [
|
||||
'v1.4.201',
|
||||
'v1.4.202',
|
||||
'v1.4.202-hourly.202609141912',
|
||||
'v1.4.203-hourly.202609140417'
|
||||
],
|
||||
releaseNames: [
|
||||
'1.4.202 • 14 • Sep 14, 12:12PM • 875b86d',
|
||||
'1.4.203 • 04 • Sep 13, 9:17PM • 2ce252f'
|
||||
]
|
||||
})
|
||||
expect(identity.version).toBe('1.4.203-hourly.202609142000')
|
||||
expect(identity.buildNumber).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,6 +30,25 @@ describe('ref-mirroring vet steps', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
// Why matching-refs rather than `gh release list` on the main repo: a tagged
|
||||
// stable still owns its number after its GitHub release is unpublished for a
|
||||
// bug, and that unpublish must not drag the channel backwards.
|
||||
it.each(['daily', 'hourly', 'adhoc'])(
|
||||
'%s versions from git tags, not main GitHub releases',
|
||||
(channel) => {
|
||||
const step = readWorkflow(`.github/workflows/${channel}-mac-build.yml`).jobs[
|
||||
`build-${channel}-mac`
|
||||
].steps.find((candidate) => candidate.name === `Compute ${channel} version`)
|
||||
expect(step.run).toContain('git/matching-refs/tags/v')
|
||||
expect(step.run).not.toMatch(
|
||||
/gh release list[\s\S]*--repo "\$GITHUB_REPOSITORY"[\s\S]*--json tagName/
|
||||
)
|
||||
if (channel !== 'adhoc') {
|
||||
expect(step.run).toContain('channel_tags=')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('retains release-cut history for version reservation and retry ancestry', () => {
|
||||
const checkout = readWorkflow('.github/workflows/release-cut.yml').jobs.cut.steps.find(
|
||||
(step) => step.uses === 'actions/checkout@v6'
|
||||
|
||||
Reference in New Issue
Block a user