mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
* feat(updater): name hourly releases by version, build number, time, and sha Hourly releases were titled with their raw tag (`v1.4.163-hourly.202607312054`), which reads as one opaque digit run and does not say which commit it came from. Title them `1.4.163 • 01 • 07-31 13:54 • e698241` instead, and show that same string in the in-app build picker by having the picker render the release's stored name rather than deriving its own label. Composing it in one place means the two surfaces cannot drift. The build number is monotonic across the channel. It is read as the highest number already in use rather than as a count of releases: the prune step trims to 72, so a count would roll backwards after three days and reissue numbers. Drafts count toward it — unlike in the freshness check, which asks whether a commit shipped, this asks whether a number is free, and a stranded draft still holds one. Times are Pacific while the tag's stamp stays UTC. The stamp is a sort key and a local one would repeat an hour at every DST fall-back, making two distinct builds compare equal; the title is only ever read. * fix(updater): fail the hourly build when the release name is missing The workflow checks out `ref: main`, but a workflow_dispatch runs the workflow file from whatever branch was dispatched. A branch that edits this step while main still carries the old script produces an empty name and an untitled release — silent, and only visible once someone opens the releases page. Verified by hitting exactly that on run 30665586904. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
87 lines
3.5 KiB
JavaScript
87 lines
3.5 KiB
JavaScript
import { execFileSync } from 'node:child_process'
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
|
|
/** `1.4.160-hourly.202607281400` — UTC to the minute, so tags sort chronologically
|
|
* by semver and every build is uniquely versioned. */
|
|
export function createHourlyBuildVersion(baseVersion, date) {
|
|
const match = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(baseVersion)
|
|
if (!match) {
|
|
throw new Error(`Package version is not valid semver: ${baseVersion}`)
|
|
}
|
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
throw new Error('Hourly build timestamp is invalid.')
|
|
}
|
|
const pad = (value, width = 2) => String(value).padStart(width, '0')
|
|
const stamp = [
|
|
pad(date.getUTCFullYear(), 4),
|
|
pad(date.getUTCMonth() + 1),
|
|
pad(date.getUTCDate()),
|
|
pad(date.getUTCHours()),
|
|
pad(date.getUTCMinutes())
|
|
].join('')
|
|
// Why: drop any -rc.N tail. Keeping it makes every hourly semver-NEWER than the
|
|
// RC it was cut from (1.4.160-rc.3-hourly.X > 1.4.160-rc.3), which would let an
|
|
// ordinary RC-channel check offer untested hourly builds to RC users. Stripping
|
|
// to the base parks hourlies below both rc.N and stable ('hourly' < 'rc'
|
|
// alphabetically), reachable only by an explicit pinned jump.
|
|
return `${match[1]}-hourly.${stamp}`
|
|
}
|
|
|
|
const RELEASE_NAME_TIME_ZONE = 'America/Los_Angeles'
|
|
|
|
/**
|
|
* `1.4.163 • 01 • 07-31 13:54 • e698241` — the human-facing release title, shown
|
|
* verbatim in both the GitHub releases list and the in-app build picker.
|
|
*
|
|
* Why Pacific while the tag's stamp stays UTC: the stamp is a sort key, and a
|
|
* local one would repeat an hour at every DST fall-back, making two distinct
|
|
* builds compare equal. The title is only ever read, so it uses the timezone the
|
|
* people reading it are in. The two therefore disagree by the current offset.
|
|
*/
|
|
export function formatHourlyReleaseName(version, buildNumber, commit, date) {
|
|
if (!Number.isInteger(buildNumber) || buildNumber < 1) {
|
|
throw new Error(`Hourly build number must be a positive integer: ${buildNumber}`)
|
|
}
|
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
throw new Error('Hourly build timestamp is invalid.')
|
|
}
|
|
const parts = Object.fromEntries(
|
|
new Intl.DateTimeFormat('en-US', {
|
|
timeZone: RELEASE_NAME_TIME_ZONE,
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
// Why h23 rather than hour12: false: some ICU builds render midnight as 24.
|
|
hourCycle: 'h23'
|
|
})
|
|
.formatToParts(date)
|
|
.map((part) => [part.type, part.value])
|
|
)
|
|
return [
|
|
version.split('-')[0],
|
|
String(buildNumber).padStart(2, '0'),
|
|
`${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`,
|
|
commit.slice(0, 7)
|
|
].join(' • ')
|
|
}
|
|
|
|
export function getHourlyBuildIdentity(now = new Date(), buildNumber = 1) {
|
|
const packageJson = JSON.parse(readFileSync(resolve('package.json'), 'utf8'))
|
|
const commit = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], {
|
|
encoding: 'utf8'
|
|
}).trim()
|
|
const version = createHourlyBuildVersion(packageJson.version, now)
|
|
return { commit, version, name: formatHourlyReleaseName(version, buildNumber, commit, now) }
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) {
|
|
const buildNumber = Number(process.env.ORCA_HOURLY_BUILD_NUMBER ?? '1')
|
|
const identity = getHourlyBuildIdentity(new Date(), buildNumber)
|
|
// Consumed by the workflow via $GITHUB_OUTPUT.
|
|
process.stdout.write(
|
|
`version=${identity.version}\ncommit=${identity.commit}\nname=${identity.name}\n`
|
|
)
|
|
}
|