mirror of
https://github.com/aphrody-labs/opentui.git
synced 2026-10-13 21:08:53 +00:00
ci(release): fix gaps found in review
- Pushes to main get their own CI concurrency group. In the shared
group, a push cancelled the waiting runs of the previous commit, so
that commit got no CI (cbec2687c), and a release built on it failed
its check wait.
- release.yml releases only when the version goes up. A reverted
release commit started a release that failed at the tag step.
- The tag step keeps a tag that already points at the release commit,
annotated or not, and fails with a clear error for any other commit.
- release.ts deletes a branch or tag on failure only if it created it,
and retries a failed read of the required checks.
- prepare-release prints the new release steps. Comments now say that
a dry run creates a GitHub prerelease, that the tag step needs the
"Main tags" ruleset to allow tag creation, and what signing rezips.
- The npm package extraction loops over packages, and build-examples
requires the artifact name its only caller passes.
This commit is contained in:
12 files changed
+106
-124
No files matched your search
@@ -28,72 +28,27 @@ runs:
|
||||
echo "Extracting npm packages..."
|
||||
unzip -q npm-packages.zip
|
||||
|
||||
# Copy core dist
|
||||
if [ -d "npm-packages/core-dist" ]; then
|
||||
cp -r npm-packages/core-dist packages/core/dist
|
||||
echo "Copied core dist"
|
||||
fi
|
||||
for package in core three qrcode react solid keymap ssh; do
|
||||
if [ -d "npm-packages/$package-dist" ]; then
|
||||
cp -r "npm-packages/$package-dist" "packages/$package/dist"
|
||||
echo "Copied $package dist"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -d "npm-packages/three-dist" ]; then
|
||||
cp -r npm-packages/three-dist packages/three/dist
|
||||
echo "Copied three dist"
|
||||
fi
|
||||
|
||||
if [ -d "npm-packages/qrcode-dist" ]; then
|
||||
cp -r npm-packages/qrcode-dist packages/qrcode/dist
|
||||
echo "Copied qrcode dist"
|
||||
fi
|
||||
|
||||
# Copy native packages to core's node_modules
|
||||
# Replace Bun-managed symlinks with the packaged native directories.
|
||||
if [ -d "npm-packages/core-native-packages" ]; then
|
||||
mkdir -p packages/core/node_modules/@opentui
|
||||
|
||||
# Replace Bun-managed symlinks with the packaged native directories.
|
||||
rm -rf packages/core/node_modules/@opentui/core-darwin-x64
|
||||
rm -rf packages/core/node_modules/@opentui/core-darwin-arm64
|
||||
rm -rf packages/core/node_modules/@opentui/core-linux-x64
|
||||
rm -rf packages/core/node_modules/@opentui/core-linux-arm64
|
||||
rm -rf packages/core/node_modules/@opentui/core-linux-x64-musl
|
||||
rm -rf packages/core/node_modules/@opentui/core-linux-arm64-musl
|
||||
rm -rf packages/core/node_modules/@opentui/core-win32-x64
|
||||
rm -rf packages/core/node_modules/@opentui/core-win32-arm64
|
||||
|
||||
rm -rf packages/core/node_modules/@opentui/core-*
|
||||
cp -r npm-packages/core-native-packages/* packages/core/node_modules/@opentui/
|
||||
echo "Copied native packages"
|
||||
fi
|
||||
|
||||
# Copy other package dists
|
||||
if [ -d "npm-packages/react-dist" ]; then
|
||||
cp -r npm-packages/react-dist packages/react/dist
|
||||
echo "Copied react dist"
|
||||
fi
|
||||
|
||||
if [ -d "npm-packages/solid-dist" ]; then
|
||||
cp -r npm-packages/solid-dist packages/solid/dist
|
||||
echo "Copied solid dist"
|
||||
fi
|
||||
|
||||
if [ -d "npm-packages/keymap-dist" ]; then
|
||||
cp -r npm-packages/keymap-dist packages/keymap/dist
|
||||
echo "Copied keymap dist"
|
||||
fi
|
||||
|
||||
if [ -d "npm-packages/ssh-dist" ]; then
|
||||
cp -r npm-packages/ssh-dist packages/ssh/dist
|
||||
echo "Copied ssh dist"
|
||||
fi
|
||||
|
||||
echo "Package extraction complete"
|
||||
|
||||
- name: Run packed Node smoke tests
|
||||
if: inputs.smoke-test == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
cd packages/core
|
||||
bun run test:dist --skip-build
|
||||
cd ../solid
|
||||
bun run test:dist --skip-build
|
||||
cd ../keymap
|
||||
bun run test:dist --skip-build
|
||||
cd ../ssh
|
||||
bun run test:dist --skip-build
|
||||
for package in core solid keymap ssh; do
|
||||
(cd "packages/$package" && bun run test:dist --skip-build)
|
||||
done
|
||||
@@ -10,7 +10,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# Each push to main gets its own group. In a shared group, the next push cancels a waiting run, and
|
||||
# that commit gets no CI.
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
|
||||
@@ -8,10 +8,9 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
npmPackagesArtifact:
|
||||
description: "Name of the npm packages artifact. Defaults to npm-packages-<version>."
|
||||
required: false
|
||||
description: "Name of the npm packages artifact"
|
||||
required: true
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -41,7 +40,7 @@ jobs:
|
||||
- name: Download npm packages artifact
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: ${{ inputs.npmPackagesArtifact || format('npm-packages-{0}', inputs.version) }}
|
||||
name: ${{ inputs.npmPackagesArtifact }}
|
||||
|
||||
- name: Extract and install npm packages
|
||||
run: |
|
||||
|
||||
@@ -10,7 +10,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# Each push to main gets its own group. In a shared group, the next push cancels a waiting run, and
|
||||
# that commit gets no CI.
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
|
||||
@@ -10,7 +10,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# Each push to main gets its own group. In a shared group, the next push cancels a waiting run, and
|
||||
# that commit gets no CI.
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
|
||||
@@ -10,7 +10,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# Each push to main gets its own group. In a shared group, the next push cancels a waiting run, and
|
||||
# that commit gets no CI.
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
|
||||
@@ -10,7 +10,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# Each push to main gets its own group. In a shared group, the next push cancels a waiting run, and
|
||||
# that commit gets no CI.
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
|
||||
@@ -10,7 +10,9 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# Each push to main gets its own group. In a shared group, the next push cancels a waiting run, and
|
||||
# that commit gets no CI.
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
|
||||
@@ -40,6 +40,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.4.1
|
||||
|
||||
- name: Extract version and dry-run flag
|
||||
id: extract
|
||||
env:
|
||||
@@ -57,15 +62,16 @@ jobs:
|
||||
TAG=$(jq -r .version packages/core/package.json)
|
||||
git fetch --quiet --no-tags --depth=1 origin "$BEFORE"
|
||||
PREVIOUS=$(git show "$BEFORE:packages/core/package.json" | jq -r .version)
|
||||
if [ "$TAG" = "$PREVIOUS" ]; then
|
||||
echo "The version is still $TAG, so this push is not a release"
|
||||
# A version that does not go up, such as a reverted release commit, is not a release.
|
||||
if ! TAG="$TAG" PREVIOUS="$PREVIOUS" bun -e 'import { compareVersions } from "./scripts/npm-publish.ts"; process.exit(compareVersions(process.env.TAG ?? "", process.env.PREVIOUS ?? "") > 0 ? 0 : 1)'; then
|
||||
echo "The version went from $PREVIOUS to $TAG, so this push is not a release"
|
||||
echo "release=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
IS_DRY_RUN=false
|
||||
VERSION="$TAG"
|
||||
RELEASE_TITLE="Release v$TAG"
|
||||
echo "The version changed from $PREVIOUS to $TAG. This is a PRODUCTION release"
|
||||
echo "The version went from $PREVIOUS to $TAG. This is a PRODUCTION release"
|
||||
fi
|
||||
|
||||
echo "release=true" >> "$GITHUB_OUTPUT"
|
||||
@@ -79,27 +85,31 @@ jobs:
|
||||
echo "Is Dry Run: $IS_DRY_RUN"
|
||||
echo "Release Title: $RELEASE_TITLE"
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.extract.outputs.release == 'true' && steps.extract.outputs.isDryRun == 'false'
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.4.1
|
||||
|
||||
# main can move between opening a release pull request and merging it.
|
||||
- name: Wait for the checks of the previous commit
|
||||
if: steps.extract.outputs.release == 'true' && steps.extract.outputs.isDryRun == 'false'
|
||||
if: steps.extract.outputs.isDryRun == 'false'
|
||||
env:
|
||||
BEFORE: ${{ github.event.before }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: bun scripts/release.ts wait-checks "$BEFORE"
|
||||
|
||||
# Before the build, so a release stops early when the tag cannot be created. A rerun finds the
|
||||
# tag at this commit and continues. A tag pushed with GITHUB_TOKEN starts no workflow.
|
||||
# Before the build, so a release stops early when the tag cannot be created. Needs the "Main tags"
|
||||
# ruleset to allow creating v* tags. A tag already at this commit, from a rerun or pushed by hand,
|
||||
# is kept. A tag pushed with GITHUB_TOKEN starts no workflow.
|
||||
- name: Tag the release commit
|
||||
if: steps.extract.outputs.release == 'true' && steps.extract.outputs.isDryRun == 'false'
|
||||
if: steps.extract.outputs.isDryRun == 'false'
|
||||
env:
|
||||
TAG: v${{ steps.extract.outputs.fullTag }}
|
||||
run: git push origin "$GITHUB_SHA:refs/tags/$TAG"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The commit of the tag: an annotated tag lists it as the peeled ^{} line, which sorts last.
|
||||
existing=$(git ls-remote --tags origin "refs/tags/$TAG" "refs/tags/$TAG^{}" | sort -k2 | tail -n 1 | cut -f1)
|
||||
if [ -z "$existing" ]; then
|
||||
git push origin "$GITHUB_SHA:refs/tags/$TAG"
|
||||
elif [ "$existing" != "$GITHUB_SHA" ]; then
|
||||
echo "::error::$TAG already exists at $existing, not at $GITHUB_SHA"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Version validation check
|
||||
validate-version:
|
||||
@@ -169,8 +179,8 @@ jobs:
|
||||
version: ${{ needs.prepare.outputs.fullTag }}
|
||||
npmPackagesArtifact: ${{ needs.sign-windows-artifacts.outputs.npmPackagesArtifact }}
|
||||
|
||||
# Tests the packages that npm-publish publishes. They do not load the Windows DLLs, the only files
|
||||
# that signing changes, so they run while signing runs.
|
||||
# Tests the packages before signing, so the tests run while signing runs. Signing replaces only the
|
||||
# Windows DLLs, which the tests do not load, but it unzips and rezips every package on Windows.
|
||||
smoke-test:
|
||||
name: Smoke Test Packages
|
||||
if: ${{ needs.prepare.outputs.release == 'true' }}
|
||||
|
||||
@@ -70,6 +70,34 @@ export function publishDirs(names: readonly string[] = []): string[] {
|
||||
)
|
||||
}
|
||||
|
||||
// Semver precedence: negative when left is older than right.
|
||||
export function compareVersions(left: string, right: string): number {
|
||||
const [leftCore = "", leftPre] = left.split(/-(.*)/)
|
||||
const [rightCore = "", rightPre] = right.split(/-(.*)/)
|
||||
const leftParts = leftCore.split(".").map(Number)
|
||||
const rightParts = rightCore.split(".").map(Number)
|
||||
for (let index = 0; index < 3; index++) {
|
||||
const difference = leftParts[index]! - rightParts[index]!
|
||||
if (difference !== 0) return difference
|
||||
}
|
||||
if (leftPre === undefined || rightPre === undefined)
|
||||
return (leftPre === undefined ? 1 : 0) - (rightPre === undefined ? 1 : 0)
|
||||
const leftIds = leftPre.split(".")
|
||||
const rightIds = rightPre.split(".")
|
||||
for (let index = 0; index < Math.max(leftIds.length, rightIds.length); index++) {
|
||||
const leftId = leftIds[index]
|
||||
const rightId = rightIds[index]
|
||||
if (leftId === undefined || rightId === undefined) return leftId === undefined ? -1 : 1
|
||||
if (leftId === rightId) continue
|
||||
const leftNumeric = /^\d+$/.test(leftId)
|
||||
const rightNumeric = /^\d+$/.test(rightId)
|
||||
if (leftNumeric && rightNumeric) return Number(leftId) - Number(rightId)
|
||||
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1
|
||||
return leftId < rightId ? -1 : 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function isSnapshotVersion(version: string): boolean {
|
||||
return version.includes("-snapshot") || /^0\.0\.0-\d{8}-[a-f0-9]{8}$/.test(version)
|
||||
}
|
||||
|
||||
@@ -98,11 +98,11 @@ ${lockstepPackages.map((pkg) => `- ${pkg.name}`).join("\n")}
|
||||
|
||||
Next steps:
|
||||
1. Review the changes: git diff
|
||||
2. Build the packages: bun run build
|
||||
3. Commit the changes: git add -A && git commit -m "Release v${version}"
|
||||
4. Push the commit: git push
|
||||
5. Tag the release after the commit: git tag v${version} -m "Release v${version}"
|
||||
6. Push the tag to trigger the release workflow: git push origin v${version}
|
||||
2. Commit the changes: git add -A && git commit -m "Release v${version}"
|
||||
3. Get the commit onto main, pushed or through a pull request. release.yml releases each commit on
|
||||
main that changes the version, and creates the v${version} tag.
|
||||
|
||||
\`bun run release\` does all of this and follows the release.
|
||||
`)
|
||||
|
||||
function getRequestedReleaseType(args: string[], explicitVersion?: string): ReleaseType | null {
|
||||
|
||||
+15
-37
@@ -5,7 +5,7 @@ import process from "node:process"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import { registryIntegrity } from "./npm-publish"
|
||||
import { compareVersions, registryIntegrity } from "./npm-publish"
|
||||
|
||||
// Releases main. release.yml publishes each commit on main that changes the version, so a release is
|
||||
// a "Release vX.Y.Z" commit on main, pushed directly or merged from a pull request.
|
||||
@@ -23,8 +23,9 @@ import { registryIntegrity } from "./npm-publish"
|
||||
// --pr pushes the commit to a release/vX.Y.Z branch and opens a pull request instead. Merging it
|
||||
// releases. The "Prepare Release" workflow, the release button, runs this mode.
|
||||
//
|
||||
// --dry-run tags the commit vX.Y.Z-dry.N and pushes only the tag. main does not change, and the
|
||||
// release run builds and packs every package but publishes nothing. --no-watch stops after the push.
|
||||
// --dry-run tags the commit vX.Y.Z-dry.N and pushes only the tag. main does not change. The release
|
||||
// run builds and packs every package but publishes nothing to npm; it creates a GitHub prerelease.
|
||||
// --no-watch stops after the push.
|
||||
//
|
||||
// bun scripts/release.ts wait-checks <sha>
|
||||
//
|
||||
@@ -253,9 +254,10 @@ function pendingChecks(repo: string, sha: string, required: readonly RequiredChe
|
||||
}
|
||||
|
||||
async function waitForChecks(repo: string, sha: string): Promise<void> {
|
||||
const required = requiredChecks(repo)
|
||||
let required: RequiredCheck[] | undefined
|
||||
let reported = ""
|
||||
await poll(`the checks of ${short(sha)}`, CHECKS_TIMEOUT_MS, () => {
|
||||
required ??= requiredChecks(repo)
|
||||
const pending = pendingChecks(repo, sha, required)
|
||||
if (pending.length === 0) return true
|
||||
const summary = pending.join(", ")
|
||||
@@ -271,34 +273,6 @@ function coreVersion(): string {
|
||||
.version
|
||||
}
|
||||
|
||||
// Semver precedence: negative when left is older than right.
|
||||
function compareVersions(left: string, right: string): number {
|
||||
const [leftCore = "", leftPre] = left.split(/-(.*)/)
|
||||
const [rightCore = "", rightPre] = right.split(/-(.*)/)
|
||||
const leftParts = leftCore.split(".").map(Number)
|
||||
const rightParts = rightCore.split(".").map(Number)
|
||||
for (let index = 0; index < 3; index++) {
|
||||
const difference = leftParts[index]! - rightParts[index]!
|
||||
if (difference !== 0) return difference
|
||||
}
|
||||
if (leftPre === undefined || rightPre === undefined)
|
||||
return (leftPre === undefined ? 1 : 0) - (rightPre === undefined ? 1 : 0)
|
||||
const leftIds = leftPre.split(".")
|
||||
const rightIds = rightPre.split(".")
|
||||
for (let index = 0; index < Math.max(leftIds.length, rightIds.length); index++) {
|
||||
const leftId = leftIds[index]
|
||||
const rightId = rightIds[index]
|
||||
if (leftId === undefined || rightId === undefined) return leftId === undefined ? -1 : 1
|
||||
if (leftId === rightId) continue
|
||||
const leftNumeric = /^\d+$/.test(leftId)
|
||||
const rightNumeric = /^\d+$/.test(rightId)
|
||||
if (leftNumeric && rightNumeric) return Number(leftId) - Number(rightId)
|
||||
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1
|
||||
return leftId < rightId ? -1 : 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function nextDryRunTag(version: string): string {
|
||||
const prefix = `v${version}-dry.`
|
||||
const numbers = git("ls-remote", "--tags", "--refs", "origin", `refs/tags/${prefix}*`)
|
||||
@@ -367,18 +341,22 @@ async function pushRelease(options: Options, base: string): Promise<Release> {
|
||||
if ((await registryIntegrity("@opentui/core", version)) !== undefined) {
|
||||
throw new ReleaseError(`@opentui/core@${version} is already on npm`)
|
||||
}
|
||||
// The branch and the tag are recorded only once created, so that a failure never deletes one that
|
||||
// existed before.
|
||||
if (options.mode === "pr") {
|
||||
branch = `release/v${version}`
|
||||
if (remoteSha(`refs/heads/${branch}`)) throw new ReleaseError(`Branch ${branch} already exists on origin`)
|
||||
git("switch", "--quiet", "--create", branch)
|
||||
const name = `release/v${version}`
|
||||
if (remoteSha(`refs/heads/${name}`)) throw new ReleaseError(`Branch ${name} already exists on origin`)
|
||||
git("switch", "--quiet", "--create", name)
|
||||
branch = name
|
||||
}
|
||||
stopIfInterrupted()
|
||||
|
||||
console.log(`Prepared ${previous} -> ${version}. Committing...`)
|
||||
run("git", ["commit", "--quiet", "--all", "--message", `Release v${version}`], { inherit: true })
|
||||
if (options.mode === "dry-run") {
|
||||
tag = nextDryRunTag(version)
|
||||
run("git", ["tag", "--annotate", tag, "--message", `Release ${tag}`], { inherit: true })
|
||||
const name = nextDryRunTag(version)
|
||||
run("git", ["tag", "--annotate", name, "--message", `Release ${name}`], { inherit: true })
|
||||
tag = name
|
||||
}
|
||||
const sha = git("rev-parse", "HEAD")
|
||||
stopIfInterrupted()
|
||||
|
||||
Reference in new issue
Block a user