Files
james.maesandClaude Opus 5.5 9890fb1300
node / node (push) Failing after 45s
build / node (push) Failing after 45s
release: Gitea release pipeline on v<CLI version> tags; 1.0.1-rc.1
.gitea/workflows/release.yaml replaces the orb publish and the GitHub release:
the scripts/release.mjs gate, Linux + darwin-x64 binaries in the container,
darwin-arm64 and windows-x64 built and smoke-tested on the native runners, a
draft Gitea release, SHA256SUMS checked against the published downloads, the
four packages to the QRun-IO Gitea npm registry (an existing version is
skipped, each package's own version picks next/latest), and for final versions
the Homebrew tap and Scoop bucket (pushed when PACKAGES_TOKEN can write them,
else the filled files are attached to the release).

release.mjs accepts its tag when it points at HEAD and prints the new path;
install.sh, update-packaging, the formula and the manifest use Gitea
downloads under v<version>; publishConfig points at the Gitea registry;
build.yaml now runs on branches only; the runbook describes the new path.
Versions: cli and core 1.0.1-rc.1, client 1.8.1-rc.1 (content changed since
1.0.0), conformance 1.6.0 unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:37:03 -06:00

118 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
/*
* Copyright 2026 QRun.IO, Inc.
* Licensed under the Apache License, Version 2.0 (see LICENSE).
*/
/**
* Release check for the CLI packages. Runs everything a release needs and then prints how to tag it.
* It never tags, pushes or publishes; the release workflow (.gitea/workflows/release.yaml) runs it as
* its gate on the tag and then publishes.
*
* 1. the working tree is clean (no staged, unstaged or untracked changes) and the tag v<CLI version>
* is free, or already points at this commit (the release workflow runs on the tag)
* 2. npm ci, then the CI steps: npm run generate with no drift in
* packages/ui-api-client/src/schema.ts or packages/core/src/api/operations.ts, build, test;
* plus npm run lint (versions, headers) and the TUI's Bun tests and type check (npm run test:tui,
* typecheck:tui; the TUI ships inside the binaries)
* 3. npm pack --dry-run for each published package
* 4. the single-file binaries with the terminal UI (scripts/build-binaries.mjs; --cli-only leaves it
* out) and dist/SHA256SUMS
* 5. prints how to tag the release
*
* A final (non-prerelease) version also has to pass the readiness gate (scripts/readiness.mjs: parity
* 100% in both columns, every server finding filed, a CHANGELOG entry, documented commands exist);
* a prerelease prints the same report as information.
*
* Usage: node scripts/release.mjs [--allow-dirty] [--skip-binaries] [--skip-install] [--cli-only]
* node scripts/release.mjs --check only the readiness report (builds first if needed)
* --cli-only build the binaries without the terminal UI (the release artifact bundles it)
*/
import { execFileSync, spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { printReport, readiness } from './readiness.mjs'
const root = resolve(import.meta.dirname, '..')
const argv = new Set(process.argv.slice(2))
for (const a of argv) if (!['--allow-dirty', '--skip-binaries', '--skip-install', '--cli-only', '--check'].includes(a)) fail(`unknown option ${a}`)
const shell = process.platform === 'win32'
/** Published packages, in publish order (each depends only on the ones before it). */
const PUBLISHED = ['packages/ui-api-client', 'packages/core', 'packages/conformance', 'packages/cli']
function fail(message) {
console.error(`\nrelease: ${message}`)
process.exit(1)
}
function git(...args) {
return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim()
}
function step(title, command, args) {
console.log(`\n== ${title}: ${command} ${args.join(' ')}`)
const result = spawnSync(command, args, { cwd: root, stdio: 'inherit', shell })
if (result.status !== 0) fail(`${title} failed (exit ${result.status})`)
}
const pkg = (dir) => JSON.parse(readFileSync(join(root, dir, 'package.json'), 'utf8'))
const versions = Object.fromEntries(PUBLISHED.map((dir) => [dir, pkg(dir)]))
const cli = versions['packages/cli']
const tag = `v${cli.version}`
if (argv.has('--check')) {
if (!existsSync(join(root, 'packages/cli/dist/program.js'))) step('build', 'npm', ['run', 'build'])
const report = await readiness()
printReport(report)
process.exit(report.ready ? 0 : 1)
}
console.log('Release check')
for (const { name, version } of Object.values(versions)) console.log(` ${name.padEnd(34)} ${version}`)
const status = git('status', '--porcelain')
if (status && !argv.has('--allow-dirty')) fail(`the working tree is not clean:\n${status}\n(commit or stash first; --allow-dirty for a trial run)`)
if (git('tag', '--list', tag) && !git('tag', '--points-at', 'HEAD').split(/\s+/).includes(tag)) {
fail(`tag ${tag} already exists on another commit; bump the versions first`)
}
const sha = git('rev-parse', '--short=12', 'HEAD')
const branch = git('rev-parse', '--abbrev-ref', 'HEAD')
if (!argv.has('--skip-install')) step('install', 'npm', ['ci'])
step('generate', 'npm', ['run', 'generate'])
const drift = git('status', '--porcelain', '--', 'packages/ui-api-client/src/schema.ts', 'packages/core/src/api/operations.ts')
if (drift) fail(`generated sources drifted from the OpenAPI document (commit them):\n${drift}`)
step('build', 'npm', ['run', 'build'])
const report = await readiness()
if (!cli.version.includes('-') && !report.ready) {
printReport(report)
fail(`${cli.version} is a final version and the readiness gate is not met (above); a prerelease (-rc.N) is not gated`)
}
step('test', 'npm', ['test'])
step('lint', 'npm', ['run', 'lint'])
step('TUI tests', 'npm', ['run', 'test:tui'])
step('TUI type check', 'npm', ['run', 'typecheck:tui'])
for (const dir of PUBLISHED) step(`pack ${versions[dir].name}`, 'npm', ['pack', '--dry-run', '--silent', '-w', versions[dir].name])
if (!argv.has('--skip-binaries')) step('binaries', 'node', ['scripts/build-binaries.mjs', '--no-build', '--musl', ...(argv.has('--cli-only') ? ['--cli-only'] : [])])
const prerelease = Object.values(versions).some((p) => p.version.includes('-'))
console.log(`
All checks passed at ${branch}@${sha}. Nothing was tagged or published. To release, with this commit
merged to main, tag the merge commit and push the tag:
git tag -s ${tag} -m "qqq ${cli.version}" && git push origin ${tag}
The release workflow (.gitea/workflows/release.yaml) then reruns this check, builds the binaries (Linux
targets in the container, macOS arm64 and Windows x64 on native runners), publishes the four packages to
the QRun-IO Gitea npm registry with dist-tag ${prerelease ? 'next' : 'latest'}, creates the Gitea ${prerelease ? 'prerelease' : 'release'} ${tag} with
the binaries and SHA256SUMS${prerelease ? '' : ', and updates the Homebrew tap and the Scoop bucket'} (docs/release-runbook.md).
`)
if (!report.ready) {
console.log('Readiness for a final release (not required for this prerelease):')
printReport(report, { limit: 5 })
console.log('')
}
console.log('Note: every package version must be new in the registry; bump any that is already published.')