Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b250794a7c
|
||
|
|
bf9c093bdd | ||
|
|
a4750b68ad
|
||
|
|
1c54e7fae0
|
||
|
|
bcd3468751
|
||
|
|
0414e46a60
|
||
|
|
2f50a10543
|
||
|
|
b68a609240
|
||
|
|
c9f033192e
|
||
|
|
61c645d581
|
||
|
|
9890fb1300
|
||
|
|
d0d8776838 | ||
|
|
219d2669c3
|
||
|
|
4828a73d32
|
||
|
|
2c156f3884
|
||
|
|
b40c291646
|
||
|
|
29f5b2a542
|
||
|
|
991950505d
|
No files matched your search
@@ -1,14 +0,0 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /.*/
|
||||
@@ -0,0 +1,17 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
jobs:
|
||||
node:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@v1
|
||||
with:
|
||||
node-version: '22'
|
||||
# The TUI (packages/tui) is not an npm workspace and needs Bun; build and test cover the four
|
||||
# Node workspaces only, as the old CI did.
|
||||
commands: |
|
||||
npm run generate
|
||||
git diff --exit-code -- packages/ui-api-client/src/schema.ts packages/core/src/api/operations.ts
|
||||
npm run build
|
||||
npm test
|
||||
npm run lint
|
||||
@@ -1,17 +0,0 @@
|
||||
name: Original CLI existing-tag packages
|
||||
on:
|
||||
create:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository == 'QRun-IO/qqq-cli' && startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'create' || github.event_name == 'workflow_dispatch')
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qqq-cli.yaml@qrun-release-v1
|
||||
with:
|
||||
runner: linux-small
|
||||
capture: true
|
||||
postpublish:
|
||||
needs: [release]
|
||||
if: needs.release.result == 'success'
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-cli-completion.yaml@qrun-release-v1
|
||||
@@ -0,0 +1,304 @@
|
||||
# Copyright 2026 QRun.IO, Inc.
|
||||
# Licensed under the Apache License, Version 2.0 (see LICENSE).
|
||||
#
|
||||
# Release on a v<CLI version> tag (docs/release-runbook.md):
|
||||
# linux scripts/release.mjs gate; linux-x64, linux-arm64, darwin-x64 binaries (cross); draft Gitea release
|
||||
# (no musl builds: bun installs no OpenTUI musl native package on a glibc host)
|
||||
# macos native darwin-arm64 binary, built and smoke-tested on the macOS runner
|
||||
# windows native windows-x64 binary, built and smoke-tested on the Windows runner
|
||||
# publish SHA256SUMS; publish the release and check its downloads; the four npm packages to the
|
||||
# QRun-IO Gitea npm registry; for a final version pull requests to the Homebrew tap and the Scoop bucket
|
||||
name: release
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.23.4
|
||||
BUN_VERSION: 1.4.4
|
||||
BUN_LINUX_SHA256: e0446eb55995772eb9894bebae9a6be368d985a8b9271ee124486f9946eedd76 # bun-linux-x64-baseline.zip
|
||||
BUN_DARWIN_SHA256: c240d7f0ccb90c263a849fcdb09b7ee62630d921348ed6aafda86b9d577bc510 # bun-darwin-aarch64.zip
|
||||
BUN_WINDOWS_SHA256: 0f20ce6674d4f2dbcc8c5d59fdf99f7ba45f8c3b367e0f5781cbfa30a1c78d99 # bun-windows-x64-baseline.zip
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: container-build
|
||||
timeout-minutes: 60
|
||||
container:
|
||||
image: node:22.23.4-bookworm
|
||||
volumes:
|
||||
- /var/lib/gitea-job-workspace:${{ github.workspace }}
|
||||
env:
|
||||
HOME: /tmp
|
||||
CI: 'true'
|
||||
outputs:
|
||||
release-id: ${{ steps.draft.outputs.id }}
|
||||
version: ${{ steps.plan.outputs.version }}
|
||||
sums: ${{ steps.draft.outputs.sums }}
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check the tag
|
||||
id: plan
|
||||
shell: bash
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
version=$(node -p 'require("./packages/cli/package.json").version')
|
||||
[[ $TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { echo "$TAG is not a release tag"; exit 1; }
|
||||
[[ $TAG == "v$version" ]] || { echo "Tag $TAG does not match packages/cli/package.json $version"; exit 1; }
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install Bun
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p /tmp/bun && cd /tmp/bun
|
||||
curl -fsSLo bun.zip "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/bun-linux-x64-baseline.zip"
|
||||
echo "$BUN_LINUX_SHA256 bun.zip" | sha256sum -c -
|
||||
python3 -c 'import sys, zipfile; zipfile.ZipFile(sys.argv[1]).extractall(".")' bun.zip
|
||||
chmod +x bun-linux-x64-baseline/bun && echo /tmp/bun/bun-linux-x64-baseline >> "$GITHUB_PATH"
|
||||
|
||||
- name: Release gate (scripts/release.mjs)
|
||||
# npm ci, generate drift, build, readiness (final versions), test, lint, TUI tests and type check, pack dry runs
|
||||
run: node scripts/release.mjs --skip-binaries
|
||||
|
||||
- name: Linux and darwin-x64 binaries
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/build-binaries.mjs --no-build --targets linux-x64,linux-arm64,darwin-x64
|
||||
scripts/verify-binary.sh dist/qqq-linux-x64
|
||||
|
||||
- name: Draft Gitea release
|
||||
id: draft
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
VERSION: ${{ steps.plan.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
api="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
auth="Authorization: token $TOKEN"
|
||||
if curl -fsS -o /dev/null -H "$auth" "$api/releases/tags/$TAG" 2>/dev/null; then
|
||||
echo "A release for $TAG already exists; a version is released once"; exit 1
|
||||
fi
|
||||
if [[ -f docs/release-notes/$VERSION.md ]]; then notes=$(cat "docs/release-notes/$VERSION.md"); else
|
||||
notes=$(awk -v v="[$VERSION]" '/^## / { if (found) exit; if (index($0, "## " v) == 1) { found = 1; next } } found { print }' CHANGELOG.md)
|
||||
fi
|
||||
[[ -n ${notes//[[:space:]]/} ]] || notes="qqq $VERSION"
|
||||
body=$(NOTES="$notes" node -e 'process.stdout.write(JSON.stringify({ tag_name: process.env.TAG,
|
||||
name: `qqq ${process.env.VERSION}`, body: process.env.NOTES, draft: true, prerelease: process.env.VERSION.includes("-") }))')
|
||||
id=$(curl -fsS -X POST -H "$auth" -H 'Content-Type: application/json' -d "$body" "$api/releases" |
|
||||
node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(0, "utf8")).id))')
|
||||
echo "id=$id" >> "$GITHUB_OUTPUT"
|
||||
echo "sums=$(cd dist && sha256sum qqq-* | tr '\n' ';')" >> "$GITHUB_OUTPUT"
|
||||
for f in dist/qqq-*; do
|
||||
curl -fsS -o /dev/null -X POST -H "$auth" -F "attachment=@$f" "$api/releases/$id/assets?name=$(basename "$f")"
|
||||
echo "Attached $(basename "$f") to draft release $id"
|
||||
done
|
||||
|
||||
macos:
|
||||
needs: linux
|
||||
runs-on: qqq-macos-arm64
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
CI: 'true'
|
||||
outputs:
|
||||
sums: ${{ steps.attach.outputs.sums }}
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
|
||||
- name: Install Node.js and Bun
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tools="$(mktemp -d)" && cd "$tools"
|
||||
node_file="node-v$NODE_VERSION-darwin-arm64.tar.gz"
|
||||
curl -fsSLO "https://nodejs.org/dist/v$NODE_VERSION/$node_file" && curl -fsSLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt"
|
||||
grep " $node_file\$" SHASUMS256.txt | shasum -a 256 -c -
|
||||
tar -xzf "$node_file"
|
||||
curl -fsSLo bun.zip "https://github.com/oven-sh/bun/releases/download/bun-v$BUN_VERSION/bun-darwin-aarch64.zip"
|
||||
echo "$BUN_DARWIN_SHA256 bun.zip" | shasum -a 256 -c -
|
||||
unzip -q bun.zip
|
||||
echo "$tools/node-v$NODE_VERSION-darwin-arm64/bin" >> "$GITHUB_PATH"
|
||||
echo "$tools/bun-darwin-aarch64" >> "$GITHUB_PATH"
|
||||
echo "TOOLS_DIR=$tools" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build, test, darwin-arm64 binary
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --version && bun --version
|
||||
npm ci
|
||||
npm run build
|
||||
npm test
|
||||
npm run lint
|
||||
node scripts/build-binaries.mjs --no-build --host
|
||||
scripts/verify-binary.sh dist/qqq-darwin-arm64
|
||||
|
||||
- name: Attach to the draft release
|
||||
id: attach
|
||||
shell: bash
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
RELEASE_ID: ${{ needs.linux.outputs.release-id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $TOKEN" -F "attachment=@dist/qqq-darwin-arm64" \
|
||||
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=qqq-darwin-arm64"
|
||||
echo "Attached qqq-darwin-arm64"
|
||||
echo "sums=$(cd dist && shasum -a 256 qqq-darwin-arm64 | tr '\n' ';')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Remove the tools
|
||||
if: always()
|
||||
shell: bash
|
||||
run: if [[ -n ${TOOLS_DIR:-} ]]; then rm -rf "$TOOLS_DIR"; fi
|
||||
|
||||
windows:
|
||||
needs: linux
|
||||
runs-on: qqq-windows-vm6218
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
CI: 'true'
|
||||
outputs:
|
||||
sums: ${{ steps.attach.outputs.sums }}
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
|
||||
- name: Install Node.js and Bun
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'
|
||||
$tools = Join-Path ([IO.Path]::GetTempPath()) ("qqq-tools-" + [guid]::NewGuid())
|
||||
New-Item -ItemType Directory -Path $tools | Out-Null
|
||||
$nodeFile = "node-v$env:NODE_VERSION-win-x64.zip"
|
||||
Invoke-WebRequest "https://nodejs.org/dist/v$env:NODE_VERSION/$nodeFile" -OutFile "$tools\$nodeFile"
|
||||
Invoke-WebRequest "https://nodejs.org/dist/v$env:NODE_VERSION/SHASUMS256.txt" -OutFile "$tools\SHASUMS256.txt"
|
||||
$want = ((Get-Content "$tools\SHASUMS256.txt") | Where-Object { $_ -match " $([regex]::Escape($nodeFile))$" }).Split(' ')[0]
|
||||
if ((Get-FileHash "$tools\$nodeFile" -Algorithm SHA256).Hash -ne $want) { throw "$nodeFile checksum mismatch" }
|
||||
Expand-Archive "$tools\$nodeFile" -DestinationPath $tools
|
||||
Invoke-WebRequest "https://github.com/oven-sh/bun/releases/download/bun-v$env:BUN_VERSION/bun-windows-x64-baseline.zip" -OutFile "$tools\bun.zip"
|
||||
if ((Get-FileHash "$tools\bun.zip" -Algorithm SHA256).Hash -ne $env:BUN_WINDOWS_SHA256) { throw "bun.zip checksum mismatch" }
|
||||
Expand-Archive "$tools\bun.zip" -DestinationPath $tools
|
||||
Add-Content $env:GITHUB_PATH "$tools\node-v$env:NODE_VERSION-win-x64"
|
||||
Add-Content $env:GITHUB_PATH "$tools\bun-windows-x64-baseline"
|
||||
Add-Content $env:GITHUB_ENV "TOOLS_DIR=$tools"
|
||||
|
||||
- name: Build, test, windows-x64 binary
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'; $PSNativeCommandUseErrorActionPreference = $true
|
||||
node --version; bun --version
|
||||
npm ci
|
||||
npm run build
|
||||
npm test
|
||||
npm run lint
|
||||
node scripts/build-binaries.mjs --no-build --host
|
||||
pwsh -NoProfile -File scripts/verify-binary.ps1 -Binary dist\qqq-windows-x64.exe
|
||||
|
||||
- name: Attach to the draft release
|
||||
id: attach
|
||||
shell: pwsh
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
RELEASE_ID: ${{ needs.linux.outputs.release-id }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'; $PSNativeCommandUseErrorActionPreference = $true
|
||||
curl.exe -fsS -o NUL -X POST -H "Authorization: token $env:TOKEN" -F "attachment=@dist/qqq-windows-x64.exe" `
|
||||
"$env:GITHUB_SERVER_URL/api/v1/repos/$env:GITHUB_REPOSITORY/releases/$env:RELEASE_ID/assets?name=qqq-windows-x64.exe"
|
||||
Write-Output "Attached qqq-windows-x64.exe"
|
||||
$hash = (Get-FileHash dist\qqq-windows-x64.exe -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
Add-Content $env:GITHUB_OUTPUT "sums=$hash qqq-windows-x64.exe;"
|
||||
|
||||
- name: Remove the tools
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: if ($env:TOOLS_DIR) { Remove-Item -Recurse -Force $env:TOOLS_DIR -ErrorAction SilentlyContinue }
|
||||
|
||||
publish:
|
||||
needs: [linux, macos, windows]
|
||||
runs-on: container-build
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: node:22.23.4-bookworm
|
||||
volumes:
|
||||
- /var/lib/gitea-job-workspace:${{ github.workspace }}
|
||||
env:
|
||||
HOME: /tmp
|
||||
CI: 'true'
|
||||
TOKEN: ${{ github.token }}
|
||||
PACKAGES_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
||||
RELEASE_ID: ${{ needs.linux.outputs.release-id }}
|
||||
VERSION: ${{ needs.linux.outputs.version }}
|
||||
steps:
|
||||
- uses: builtin:checkout
|
||||
|
||||
- name: SHA256SUMS
|
||||
shell: bash
|
||||
env:
|
||||
SUMS: ${{ needs.linux.outputs.sums }}${{ needs.macos.outputs.sums }}${{ needs.windows.outputs.sums }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
mkdir -p /tmp/assets
|
||||
tr ';' '\n' <<< "$SUMS" | sed '/^$/d' | sort -k2 > /tmp/assets/SHA256SUMS
|
||||
cat /tmp/assets/SHA256SUMS
|
||||
for want in qqq-linux-x64 qqq-linux-arm64 qqq-darwin-x64 qqq-darwin-arm64 qqq-windows-x64.exe; do
|
||||
grep -Eq "^[0-9a-f]{64} $want\$" /tmp/assets/SHA256SUMS || { echo "No checksum for $want"; exit 1; }
|
||||
done
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $TOKEN" -F "attachment=@/tmp/assets/SHA256SUMS" \
|
||||
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=SHA256SUMS"
|
||||
|
||||
- name: Publish the release and check the downloads
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
prerelease=false; [[ $VERSION == *-* ]] && prerelease=true
|
||||
curl -fsS -o /dev/null -X PATCH -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d "{\"draft\":false,\"prerelease\":$prerelease}" "$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID"
|
||||
# What users download (the formula, the manifest, install.sh) must match SHA256SUMS.
|
||||
cd /tmp/assets
|
||||
while read -r _ file; do
|
||||
curl -fsSL -o "$file" "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/download/v$VERSION/$file"
|
||||
done < SHA256SUMS
|
||||
sha256sum --check --strict SHA256SUMS
|
||||
echo "Released: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/v$VERSION"
|
||||
|
||||
- name: Publish the npm packages
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -z ${PACKAGES_TOKEN:-} ]]; then echo "PACKAGES_TOKEN is empty"; exit 1; fi
|
||||
npm ci
|
||||
npm run build
|
||||
registry="$GITHUB_SERVER_URL/api/packages/$GITHUB_REPOSITORY_OWNER/npm/"
|
||||
export NPM_CONFIG_USERCONFIG=/tmp/publish.npmrc
|
||||
printf '//%s:_authToken=${PACKAGES_TOKEN}\n@qrun-io:registry=%s\n' "${registry#*://}" "$registry" > "$NPM_CONFIG_USERCONFIG"
|
||||
# In dependency order. A version already in the registry is left alone (versions are immutable),
|
||||
# so a release republishes nothing that did not change; each package's own version picks its dist-tag.
|
||||
for dir in packages/ui-api-client packages/core packages/conformance packages/cli; do
|
||||
name=$(node -p "require('./$dir/package.json').name")
|
||||
version=$(node -p "require('./$dir/package.json').version")
|
||||
if npm view "$name@$version" version --registry "$registry" > /dev/null 2>&1; then
|
||||
echo "$name@$version is already published; skipped"; continue
|
||||
fi
|
||||
tag=latest; [[ $version == *-* ]] && tag=next
|
||||
(cd "$dir" && npm publish --registry "$registry" --tag "$tag")
|
||||
echo "Published $name@$version ($tag): $(npm view "$name@$version" version --registry "$registry")"
|
||||
done
|
||||
|
||||
- name: Homebrew tap and Scoop bucket (pull requests)
|
||||
# Final versions: branch release/qqq-<version> and a pull request "qqq <version>" in QRun-IO/homebrew-tap
|
||||
# and QRun-IO/scoop-bucket (PACKAGES_TOKEN); a human merges. Prereleases, or a refused push, attach
|
||||
# qqq.rb / qqq.json to the release instead. Never fails the release.
|
||||
shell: bash
|
||||
run: bash scripts/publish-packaging.sh
|
||||
@@ -1,8 +0,0 @@
|
||||
profile: cli
|
||||
orb_version: '0.8'
|
||||
default_branch: main
|
||||
publish:
|
||||
release: true
|
||||
publish_context: qqq-maven-registry-credentials
|
||||
# Native target notes; separate binary entrypoints do not read this CI manifest.
|
||||
# platforms: [macos-arm64, windows-x64]
|
||||
@@ -86,6 +86,29 @@ All notable changes to the qqq CLI and its packages. Versions follow [semver](ht
|
||||
position reports. They are rewritten on the input stream into `ESC [13;2~` outside the startup
|
||||
cursor-query window.
|
||||
|
||||
## [1.0.1-rc.1] - 2026-11-08
|
||||
|
||||
Release candidate built and published by Gitea CI from the tag `v1.0.1-rc.1`, the first through the new
|
||||
release path: `@qrun-io/qqq-cli` 1.0.1-rc.1, `@qrun-io/qqq-core` 1.0.1-rc.1 and
|
||||
`@qrun-io/qqq-ui-api-client` 1.8.1-rc.1 (dist-tag `next`) with `@qrun-io/qqq-ui-api-conformance` 1.6.0 on
|
||||
the QRun-IO Gitea npm registry, and the binaries with `SHA256SUMS` on the Gitea release. It carries the
|
||||
changes listed under Unreleased above as of this tag.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Releases move to Gitea.** A `v<CLI version>` tag runs `.gitea/workflows/release.yaml`: the
|
||||
`scripts/release.mjs` gate, the Linux, macOS arm64 (native) and Windows x64 (native) binaries, the npm
|
||||
packages on `https://git.kof22.com/api/packages/QRun-IO/npm/`, the Gitea release, and for final versions
|
||||
the Homebrew tap and Scoop bucket. The `cli-v*` tags and GitHub releases are retired; `scripts/install.sh`,
|
||||
the formula and the Scoop manifest download from `https://git.kof22.com/QRun-IO/qqq-cli/releases`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Windows: the DPAPI keyring works when `qqq` is started from PowerShell 7.** The inherited `PSModulePath`
|
||||
named PowerShell 7's modules, so Windows PowerShell 5.1 could not load `Microsoft.PowerShell.Security` and every
|
||||
sign-in failed with "Windows credential protection failed". The keyring now starts PowerShell without it.
|
||||
Found by the first native Windows release lane.
|
||||
|
||||
## [1.0.0] - 2026-11-01
|
||||
|
||||
`qqq` 1.0: the command-line client and the full-screen terminal UI (`qqq ui`), shipped together as one
|
||||
|
||||
@@ -324,8 +324,8 @@ scripts/verify-binary.sh dist/qqq-darwin-arm64 --server http://localhost:8077
|
||||
```
|
||||
|
||||
The CLI runs on Node 20 and later and uses no Bun-only API; Bun is needed only for the TUI and the
|
||||
binaries. CI is the qqq-orb `cli` profile (`.qqq-ci.yml`); a tag `v<client version>` publishes the
|
||||
packages to GitHub Packages ([`docs/ci-lanes.md`](docs/ci-lanes.md) says what it does and does not cover).
|
||||
binaries. CI is the shared Gitea `node.yaml` workflow (`.gitea/workflows/build.yaml`): build, test, lint and
|
||||
a generated-code drift check on every push; it publishes nothing ([`docs/ci-lanes.md`](docs/ci-lanes.md)).
|
||||
|
||||
## Contributing, security, license
|
||||
|
||||
|
||||
+2
-2
@@ -23,9 +23,9 @@ Everything under `docs/`:
|
||||
| [release-notes/1.0.0.md](release-notes/1.0.0.md) | the GitHub release body for `cli-v1.0.0` (the release commands are in an HTML comment at the top) |
|
||||
| [release-notes/1.0.0-rc.1.md](release-notes/1.0.0-rc.1.md) | the GitHub release body for the release candidate `cli-v1.0.0-rc.1`, kept for history |
|
||||
| [packaging/README.md](packaging/README.md) | the install paths (curl installer, Homebrew, Scoop, npm), where the formula and manifest live, platform notes (Gatekeeper, SmartScreen, musl), and how the terminal UI is bundled |
|
||||
| [packaging/homebrew-tap/](packaging/homebrew-tap/README.md) | the complete `QRun-IO/homebrew-tap` repository, ready to copy: `Formula/qqq.rb`, README, LICENSE and its qqq-orb CI files |
|
||||
| [packaging/homebrew-tap/](packaging/homebrew-tap/README.md) | the complete `QRun-IO/homebrew-tap` repository, ready to copy: `Formula/qqq.rb`, README, LICENSE (its old qqq-orb CI files were removed with CircleCI) |
|
||||
| [packaging/qqq.json](packaging/qqq.json) | the Scoop manifest, copied to `bucket/qqq.json` in `QRun-IO/scoop-bucket` |
|
||||
| [ci-lanes.md](ci-lanes.md) | what CircleCI (the qqq-orb `cli` profile) runs: the Linux verify-and-publish job and the macOS arm64 and Windows x64 lanes, what is still outside CI, and the history of the orb changes |
|
||||
| [ci-lanes.md](ci-lanes.md) | what CI runs (the shared Gitea `node.yaml` workflow), what is outside CI, and the history of the retired CircleCI orb lanes |
|
||||
|
||||
`scripts/update-packaging.mjs` fills the version and checksums of the formula and manifest from a
|
||||
release's `SHA256SUMS`.
|
||||
|
||||
+10
-21
@@ -2,30 +2,19 @@
|
||||
|
||||
## What runs today
|
||||
|
||||
`.circleci/config.yml` is the qqq-orb setup stub on the `0.8` track (it picks up every 0.8.x release by
|
||||
itself); `.qqq-ci.yml` selects the orb's `cli` profile and, since qqq-orb **0.8.33**, turns on the
|
||||
platform lanes with `platforms: [macos-arm64, windows-x64]`. The profile generates three jobs:
|
||||
CircleCI and the qqq-orb were retired on 2026-11-07. CI is the shared Gitea workflow
|
||||
[`KofTwentyTwo/ci-workflows` `node.yaml`](https://git.kof22.com/KofTwentyTwo/ci-workflows), called from
|
||||
`.gitea/workflows/build.yaml` on every push, Linux only (`node:22-bookworm`):
|
||||
|
||||
| Job | Executor | What it runs |
|
||||
|---|---|---|
|
||||
| `cli-macos-arm64` (`qqq-orb/cli_platform`) | macOS, Xcode 16.4, `m4pro.medium` | Node 20.20.2 (the `engines` floor) with nvm, `npm ci`, `npm run build && npm test && npm run lint` |
|
||||
| `cli-windows-x64` (`qqq-orb/cli_platform`) | Windows Server 2022, `windows.medium`, steps in Git Bash | the same, on Windows (keyring DPAPI path, Windows paths) |
|
||||
| `qqq-orb/cli_verify_publish` | Linux, `cimg/node:22.23` | `npm ci`; `npm run generate` then `git diff --exit-code` on `schema.ts` and `operations.ts`; build, test, lint; a Prism smoke (`GET /qqq/v1/version`, `/metaData/authentication`); on a tag `v<client version>` with `publish.release: true`, publishing (below) |
|
||||
1. `npm ci`
|
||||
2. `npm run generate`, then `git diff --exit-code` on `schema.ts` and `operations.ts` (generated-code drift)
|
||||
3. `npm run build`, `npm test`, `npm run lint`
|
||||
|
||||
On branches and pull requests the three jobs run side by side. On a release tag `cli_verify_publish`
|
||||
**requires both platform lanes**, so nothing is published unless macOS and Windows are green. Publishing
|
||||
puts all four packages on GitHub Packages (client, core, conformance, CLI, in that order) with the dist-tag
|
||||
`latest` when all four versions are final and `rc` when any of them has a prerelease part. The CircleCI
|
||||
plan must include the `m4pro.medium` and `windows.medium` resource classes.
|
||||
Not in CI: publishing (packages, binaries, Homebrew/Scoop updates are release-machine steps in
|
||||
`docs/release-runbook.md` until a release workflow is decided), the macOS arm64 and Windows x64 lanes,
|
||||
the Prism smoke, the TUI (`npm run test:tui`, `typecheck:tui`, which need Bun) and the platform binaries.
|
||||
|
||||
The orb's generator accepts this manifest: rendered with `src/scripts/generate_pipeline.py` from qqq-orb
|
||||
v0.8.33, a push to `main` gives `cli-macos-arm64`, `cli-windows-x64` and `cli_verify_publish` in
|
||||
`test_only`, and the tag `v1.8.0` gives the same in `publish_release` with `cli_verify_publish` requiring
|
||||
both lanes and bound to the `qqq-maven-registry-credentials` context.
|
||||
|
||||
Still not in CI: the TUI (`npm run test:tui`, `typecheck:tui`, which need Bun), and building and smoking
|
||||
the platform binaries (`scripts/build-binaries.mjs --host`, `scripts/verify-binary.sh` / `.ps1`); the
|
||||
lanes run the Node packages only. Those stay release-machine steps in `docs/release-runbook.md`.
|
||||
Everything below describes the retired CircleCI setup and is kept for the record.
|
||||
|
||||
### History
|
||||
|
||||
|
||||
@@ -36,10 +36,8 @@ checkpoint the sums here first, not there.
|
||||
## Where the formula and manifest live
|
||||
|
||||
- **`QRun-IO/homebrew-tap`** is laid out under `homebrew-tap/` here (the repository is created by copying
|
||||
that directory). Its CI is the qqq-orb `homebrew-tap` profile (`.qqq-ci.yml`, with the same setup
|
||||
`.circleci/config.yml` as `QRun-IO/homebrew-qctl`). That CI fetches and checksums every platform's
|
||||
download, but it installs the formula and runs `brew test` with the **Linux x64 binary only**; the
|
||||
macOS (and Linux arm64) binaries are not executed there. qctl stays in `QRun-IO/homebrew-qctl` until it
|
||||
that directory). Its old CI (the qqq-orb `homebrew-tap` profile on CircleCI) was retired on
|
||||
2026-11-07 and the seed no longer carries CI files; tap CI on Gitea is not decided yet. qctl stays in `QRun-IO/homebrew-qctl` until it
|
||||
moves over.
|
||||
- **`QRun-IO/scoop-bucket`** is created from
|
||||
[ScoopInstaller/BucketTemplate](https://github.com/ScoopInstaller/BucketTemplate), with `qqq.json` as
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
ignore: /.*/
|
||||
@@ -1,5 +0,0 @@
|
||||
profile: homebrew-tap
|
||||
orb_version: '0.8'
|
||||
default_branch: main
|
||||
tap: qrun-io/tap
|
||||
formula: qqq
|
||||
@@ -2,31 +2,31 @@
|
||||
|
||||
# qqq: the QQQ command-line client and terminal UI (qqq ui) in one binary.
|
||||
# Kept in qqq-cli at docs/packaging/homebrew-tap/Formula/qqq.rb; scripts/update-packaging.mjs fills
|
||||
# version and sha256 from the SHA256SUMS of a GitHub release, then the file is copied to the tap.
|
||||
# version and sha256 from the SHA256SUMS of a Gitea release; the release workflow then copies it to the tap.
|
||||
class Qqq < Formula
|
||||
desc "Command-line client and terminal UI for QQQ applications"
|
||||
homepage "https://github.com/QRun-IO/qqq-cli"
|
||||
homepage "https://git.kof22.com/QRun-IO/qqq-cli"
|
||||
version "1.0.0"
|
||||
license "Apache-2.0"
|
||||
|
||||
on_macos do
|
||||
on_arm do
|
||||
url "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v#{version}/qqq-darwin-arm64"
|
||||
url "https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v#{version}/qqq-darwin-arm64"
|
||||
sha256 "5017a32ac5031d9146d541381099a959e5c19bbf04c48f725045d89d6007b211"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v#{version}/qqq-darwin-x64"
|
||||
url "https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v#{version}/qqq-darwin-x64"
|
||||
sha256 "eb9b1d43d24b639118d94b175c343080a39ae9ba64c24645c1792ff3b16a37f1"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
on_arm do
|
||||
url "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v#{version}/qqq-linux-arm64"
|
||||
url "https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v#{version}/qqq-linux-arm64"
|
||||
sha256 "25f513bdda13bbe40cf536844eebc1e3a7e21cf7804024f25ebe7c42793d7ebc"
|
||||
end
|
||||
on_intel do
|
||||
url "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v#{version}/qqq-linux-x64"
|
||||
url "https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v#{version}/qqq-linux-x64"
|
||||
sha256 "804d06cb7c0012c2f185ae5d94c6bb8f9e2482a440bb3c165361f1a14c941e41"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,9 +20,9 @@ here (`Formula/qctl.rb`) once this tap exists, with `homebrew-qctl` kept as a po
|
||||
|
||||
The formula is generated in qqq-cli: after a `cli-v<version>` GitHub release, run
|
||||
`node scripts/update-packaging.mjs <release SHA256SUMS>` there and copy
|
||||
`docs/packaging/homebrew-tap/Formula/qqq.rb` here. CI (qqq-orb `homebrew-tap` profile) fetches every
|
||||
platform's download, installs the formula and runs its test, which fails on a wrong or all-zero sum, but
|
||||
only once the pull request here is opened and its checks are read. The script refuses to write missing or
|
||||
`docs/packaging/homebrew-tap/Formula/qqq.rb` here. The tap's old CI (qqq-orb `homebrew-tap` profile,
|
||||
which installed the formula and ran its test) was retired on 2026-11-07; Gitea CI for the tap is not
|
||||
decided yet, so check the formula by hand (`brew install --build-from-source ./Formula/qqq.rb && brew test qqq`). The script refuses to write missing or
|
||||
malformed sums; still compare the sums in the diff with the release's `SHA256SUMS` before opening the PR.
|
||||
|
||||
Apache-2.0
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"description": "Command-line client and terminal UI (qqq ui) for QQQ applications, in one binary",
|
||||
"homepage": "https://github.com/QRun-IO/qqq-cli",
|
||||
"homepage": "https://git.kof22.com/QRun-IO/qqq-cli",
|
||||
"license": "Apache-2.0",
|
||||
"notes": [
|
||||
"qqq ui (the terminal UI) needs Windows Terminal or another ConPTY terminal; the legacy console is not supported.",
|
||||
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v1.0.0/qqq-windows-x64.exe",
|
||||
"url": "https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v1.0.0/qqq-windows-x64.exe",
|
||||
"hash": "cf418d7842a227cee412171878228004f9bc5098a272c64b89512d1e2d06a176"
|
||||
}
|
||||
},
|
||||
@@ -20,13 +20,14 @@
|
||||
]
|
||||
],
|
||||
"checkver": {
|
||||
"github": "https://github.com/QRun-IO/qqq-cli",
|
||||
"regex": "cli-v([\\d.]+(?:-rc\\.\\d+)?)"
|
||||
"url": "https://git.kof22.com/api/v1/repos/QRun-IO/qqq-cli/releases/latest",
|
||||
"jsonpath": "$.tag_name",
|
||||
"regex": "v([\\d.]+)"
|
||||
},
|
||||
"autoupdate": {
|
||||
"architecture": {
|
||||
"64bit": {
|
||||
"url": "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v$version/qqq-windows-x64.exe",
|
||||
"url": "https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v$version/qqq-windows-x64.exe",
|
||||
"hash": {
|
||||
"url": "$baseurl/SHA256SUMS"
|
||||
}
|
||||
|
||||
+95
-195
@@ -1,240 +1,140 @@
|
||||
# Release runbook: qqq 1.0.0
|
||||
# Release runbook
|
||||
|
||||
From "the branch is green" to "1.0.0 is published and installable everywhere". It is written for
|
||||
1.0.0 but holds for any release; replace the versions. Nothing in the repository tags, pushes or
|
||||
publishes by itself: `scripts/release.mjs` checks and prints, people run the commands.
|
||||
From "main is green" to "qqq X.Y.Z is published and installable everywhere". A release is one tag,
|
||||
`v<CLI version>`, on the merge commit; Gitea CI (`.gitea/workflows/release.yaml`) does the rest. Nothing
|
||||
publishes from a branch.
|
||||
|
||||
> **Since 2026-11-08** releases run on Gitea. The CircleCI/qqq-orb path (npm packages on a
|
||||
> `v<client version>` tag, binaries on a GitHub release `cli-v<version>`, hand-made tap and bucket pull
|
||||
> requests) is retired. Older releases (1.0.0 and before) stay on GitHub.
|
||||
|
||||
What ships:
|
||||
|
||||
| Artifact | Version for 1.0 | Where | Made by |
|
||||
|---|---|---|---|
|
||||
| `qqq` binaries with the terminal UI, 7 targets, plus `SHA256SUMS` | 1.0.0 | GitHub release `cli-v1.0.0` | `scripts/build-binaries.mjs` (via `release.mjs`) |
|
||||
| `@qrun-io/qqq-cli` (CLI only, Node 20+) | 1.0.0 | GitHub Packages | the orb, on tag `v1.8.0` |
|
||||
| `@qrun-io/qqq-core` | 1.0.0 | GitHub Packages | the orb (with PR #165), else by hand |
|
||||
| `@qrun-io/qqq-ui-api-client` | 1.8.0 | GitHub Packages | the orb |
|
||||
| `@qrun-io/qqq-ui-api-conformance` | 1.6.0 | GitHub Packages | the orb |
|
||||
| Artifact | Where | Made by |
|
||||
|---|---|---|
|
||||
| `qqq` binaries with the terminal UI, 5 targets (linux x64/arm64, darwin x64/arm64, windows x64), plus `SHA256SUMS` | Gitea release `v<version>` of `QRun-IO/qqq-cli` | `release.yaml`: Linux targets and darwin-x64 (cross) in the container, darwin-arm64 on the macOS runner, windows-x64 on the Windows runner |
|
||||
| `@qrun-io/qqq-ui-api-client`, `qqq-core`, `qqq-ui-api-conformance`, `qqq-cli` | QRun-IO Gitea npm registry, `https://git.kof22.com/api/packages/QRun-IO/npm/` | `release.yaml` publish job |
|
||||
| Homebrew formula `qqq` | `QRun-IO/homebrew-tap`, `Formula/qqq.rb` | pull request from `release.yaml` (final versions), merged by a human |
|
||||
| Scoop manifest `qqq` | `QRun-IO/scoop-bucket`, `bucket/qqq.json` | pull request from `release.yaml` (final versions), merged by a human |
|
||||
|
||||
**All four npm packages go final together.** The orb (PR #165) derives the dist-tag from all four
|
||||
versions: if any of client, core, conformance or CLI has a prerelease part, all four are published as
|
||||
`rc`; only when all four are final do they go out as `latest`. So 1.0.0 ships client 1.8.0 and
|
||||
conformance 1.6.0 as finals beside CLI and core 1.0.0, under the tags `v1.8.0` (orb publish) and
|
||||
`cli-v1.0.0` (GitHub release). `release.mjs` refuses a final CLI version while any of the four is still a
|
||||
prerelease (the readiness gate's "package versions" line).
|
||||
| Homebrew formula `qqq` | 1.0.0 | `QRun-IO/homebrew-tap` | `scripts/update-packaging.mjs`, then a PR |
|
||||
| Scoop manifest `qqq` | 1.0.0 | `QRun-IO/scoop-bucket` | `scripts/update-packaging.mjs`, then a PR |
|
||||
## 1. Versions
|
||||
|
||||
## Checklist: what needs James
|
||||
|
||||
Nobody else can do these; each says what happens without it.
|
||||
|
||||
- [x] **Orb changes released** in `kingsrook/qqq-orb` 0.8.33 (`docs/ci-lanes.md`): publishing
|
||||
`@qrun-io/qqq-core`, lint, the `operations.ts` drift check, the dist-tag rule above (PR #165), and
|
||||
the macOS arm64 and Windows x64 lanes, which `.qqq-ci.yml` turns on (`platforms:`) and a release tag
|
||||
requires before publishing.
|
||||
- [ ] **CircleCI plan** includes the `m4pro.medium` (macOS) and `windows.medium` resource classes. Without
|
||||
them the platform lanes cannot start, and because a release tag requires them, the publish waits;
|
||||
remove `platforms:` from `.qqq-ci.yml` to publish without them.
|
||||
- [ ] **Apple Developer ID and notarization**: a "Developer ID Application" certificate and a notary
|
||||
credential (App Store Connect API key) on the release machine. Without it the macOS binaries are
|
||||
only ad-hoc signed. Homebrew and the curl installer are not affected (they do not set the
|
||||
quarantine flag), but a binary downloaded with a browser is stopped by Gatekeeper ("Apple cannot
|
||||
check it for malicious software"). Workaround, documented in the README and the packaging notes:
|
||||
`xattr -d com.apple.quarantine ./qqq-darwin-arm64`, or Finder, right-click, Open.
|
||||
- [ ] **Windows Authenticode certificate** (the KofTwentyTwo release standard). Without it SmartScreen
|
||||
warns on first run ("Windows protected your PC", More info, Run anyway); Scoop installs still work.
|
||||
- [x] **`QRun-IO/homebrew-tap` and `QRun-IO/scoop-bucket` exist** (created from `docs/packaging/homebrew-tap/` and
|
||||
`docs/packaging/qqq.json`). After the GitHub release, run `scripts/update-packaging.mjs` on the uploaded
|
||||
`SHA256SUMS` and commit the filled `Formula/qqq.rb` and `bucket/qqq.json` to them. Without that commit
|
||||
`brew install` and `scoop install` fail their checksum (the curl installer and the download still work).
|
||||
- [ ] **CircleCI context** `qqq-maven-registry-credentials` holds a `GITHUB_TOKEN` that can write the
|
||||
four `@qrun-io` packages (the orb checks it authenticates; write access is only proven by a publish).
|
||||
- [ ] **Go for 1.0.0 final**: the 1.0.0-rc.1 feedback is accepted and no blocker is open.
|
||||
|
||||
## Not verified before 1.0
|
||||
|
||||
The platform lanes run the Node packages' build, tests and lint on macOS arm64 and Windows x64 (Node
|
||||
20), which covers the keyring code paths there, but they do not build or run the binaries.
|
||||
|
||||
- `qqq-windows-x64.exe` (CLI and UI) has never run; `scripts/verify-binary.ps1` is the smoke to run on
|
||||
a Windows machine.
|
||||
- `qqq-darwin-x64` has never run (no Intel Mac or Rosetta on the build machine).
|
||||
- Windows Terminal rendering of `qqq ui`, and the DPAPI keyring outside unit tests.
|
||||
|
||||
Verified on the build machine: darwin-arm64 (CLI, keyring, completion, `qqq ui` tmux smoke and width
|
||||
smoke against the sample server), linux-x64 glibc, linux-arm64 glibc and linux-arm64-musl in Docker
|
||||
(CLI and `qqq ui` in tmux), the npm tarballs installed on Node 20, the Homebrew formula in the
|
||||
`homebrew/brew` container, and the curl installer from a local mirror.
|
||||
|
||||
## 1. Branch green
|
||||
|
||||
- `feat/m5-cli-release` (and whatever else is in the release) is merged to `main`; the orb pipeline on
|
||||
`main` is green: `cli_verify_publish` (Linux, `cimg/node:22.23`) and the `cli-macos-arm64` and
|
||||
`cli-windows-x64` lanes.
|
||||
- Locally on `main`, clean tree: `npm ci && npm run build && npm test && npm run lint && npm run test:tui`.
|
||||
|
||||
## 2. Versions: release candidate to final
|
||||
|
||||
**For 1.0.0 this step is done** on the branch `release/1.0.0` (34bbd2a, on top of the rc.1 commit
|
||||
6517373): all four packages final, the exact workspace pins, the `## [1.0.0]` CHANGELOG entry,
|
||||
`docs/release-notes/1.0.0.md`, and the READMEs at 1.0.0. That branch (with the release docs merged into
|
||||
it) is fast-forwarded onto `main`; nothing else changes versions:
|
||||
|
||||
```sh
|
||||
git checkout main && git pull --ff-only
|
||||
git merge --ff-only release/1.0.0 # fails if main moved: rebase release/1.0.0 onto main first
|
||||
git push origin main
|
||||
```
|
||||
|
||||
For later releases, the table below is the recipe.
|
||||
|
||||
On a branch `release/1.0.0` from `main`:
|
||||
On a branch from `main`:
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/cli/package.json`, `packages/cli/src/version.ts` (`CLI_VERSION`) | `1.0.0-rc.N` → `1.0.0` |
|
||||
| `packages/core/package.json` | `1.0.0-rc.N` → `1.0.0` |
|
||||
| `packages/ui-api-client/package.json` | `1.8.0-rc.N` → `1.8.0` (the client follows the UI API version) |
|
||||
| `packages/conformance/package.json` | `1.6.0-rc.N` → `1.6.0` |
|
||||
| `packages/cli/package.json`, `packages/cli/src/version.ts` (`CLI_VERSION`) | the CLI version, e.g. `1.1.0-rc.1` or `1.1.0` |
|
||||
| `packages/core/package.json`, `packages/conformance/package.json` | bump any package whose content changed |
|
||||
| `packages/ui-api-client/package.json` | follows the UI API version of `openapi/qqq-v1.json` |
|
||||
| workspace dependencies in `packages/cli` and `packages/core` | the same versions, exact |
|
||||
| `CHANGELOG.md` | a `## [1.0.0] - <date>` entry above the rc entries |
|
||||
| `README.md`, `packages/cli/README.md` | the version and `@rc` in the npm command (`@latest`, or no tag) |
|
||||
| `CHANGELOG.md` | a `## [X.Y.Z] - <date>` entry (it becomes the release notes; `docs/release-notes/X.Y.Z.md` wins if present) |
|
||||
|
||||
All four packages move to finals in the same commit; none may stay `-rc` (the orb would then publish all
|
||||
four as `rc`, and `release.mjs` refuses). Then `npm install` (refreshes `package-lock.json`),
|
||||
`npm run lint`, which fails if any version disagrees, and `npm run release -- --check`, which must say
|
||||
READY (parity 100% in both columns, every finding filed, the CHANGELOG entry, documented commands,
|
||||
package versions). Commit (`git commit -S -m "release: 1.0.0"`), merge to `main` through a pull request.
|
||||
Then `npm install` (refreshes `package-lock.json`), `npm run lint` (fails if any version disagrees) and
|
||||
`node scripts/release.mjs` (the same gate CI runs; add `--skip-binaries` to save time). A final version
|
||||
must pass the readiness gate (`npm run release -- --check`: parity, findings filed, CHANGELOG entry,
|
||||
documented commands, all four packages final); a prerelease is not gated. Merge through a pull request.
|
||||
|
||||
## 3. Release check and binaries
|
||||
Each package version is published once. The publish job skips a package whose exact version is already
|
||||
in the registry (unchanged packages ride along), so bump every package whose content changed.
|
||||
|
||||
On `main` at the release commit, clean tree:
|
||||
## 2. Tag
|
||||
|
||||
```sh
|
||||
node scripts/release.mjs # the readiness gate (final versions), npm ci, generate drift, build, test,
|
||||
# lint, TUI tests and type check, pack dry runs, the 7 binaries with the UI
|
||||
# and dist/SHA256SUMS
|
||||
scripts/verify-binary.sh dist/qqq-darwin-arm64 --server http://localhost:8077 # sample server running
|
||||
git checkout main && git pull --ff-only
|
||||
git tag -s v1.1.0 -m "qqq 1.1.0" && git push origin v1.1.0
|
||||
```
|
||||
|
||||
`release.mjs` prints the commands for steps 5 and 6 with the right versions.
|
||||
Or create the tag on the merge commit through the Gitea API (`POST /repos/QRun-IO/qqq-cli/tags`). The tag
|
||||
must equal `packages/cli/package.json` or the release fails at once. A version with a `-` (`-rc.N`) is a
|
||||
prerelease: npm dist-tag `next` for prerelease packages, a Gitea prerelease, and no tap or bucket update.
|
||||
|
||||
## 4. Sign (James's credentials)
|
||||
## 3. What CI does
|
||||
|
||||
Signing changes the files, so it comes before the checksums are final. Each script reads its
|
||||
credentials from the environment, does nothing (and says what is missing) without them, has
|
||||
`--dry-run`, and recomputes `dist/SHA256SUMS`.
|
||||
`.gitea/workflows/release.yaml`, in order; any failure stops it:
|
||||
|
||||
1. **linux** (`container-build`, `node:22.23.4-bookworm`, Bun 1.4.4 checksum-pinned):
|
||||
`scripts/release.mjs --skip-binaries` (npm ci, generate drift, build, readiness for finals, tests, lint,
|
||||
TUI tests and type check, pack dry runs), then the linux-x64, linux-arm64 and darwin-x64
|
||||
binaries (`scripts/build-binaries.mjs`), the `scripts/verify-binary.sh` smoke on linux-x64, and a
|
||||
**draft** Gitea release with those binaries. An existing release for the tag fails the run.
|
||||
2. **macos** (`qqq-macos-arm64`) and **windows** (`qqq-windows-vm6218`), in parallel: pinned Node.js
|
||||
(`SHASUMS256.txt`) and Bun (pinned sha256), `npm ci`, build, test, lint, the native binary
|
||||
(`--host`), the `verify-binary.sh` / `verify-binary.ps1` smoke, and the upload to the draft.
|
||||
3. **publish** (`container-build`): `SHA256SUMS` from the three jobs' checksums, uploaded; the release is
|
||||
published and every download is checked against `SHA256SUMS`; the four packages go to the Gitea npm
|
||||
registry with `PACKAGES_TOKEN`; `scripts/update-packaging.mjs` fills the formula and the manifest.
|
||||
For a final version `scripts/publish-packaging.sh` pushes branch `release/qqq-<version>` to
|
||||
`QRun-IO/homebrew-tap` and `QRun-IO/scoop-bucket` with `PACKAGES_TOKEN` and opens a pull request
|
||||
`qqq <version>` in each; a human checks the sums and merges. When the push or the pull request is refused
|
||||
(or for a prerelease) `qqq.rb` and `qqq.json` are attached to the release instead (step 4); the release
|
||||
never fails over this. Dry run, no writes: `DRY_RUN=1 VERSION=<version> SUMS_FILE=<SHA256SUMS> scripts/publish-packaging.sh`.
|
||||
|
||||
Watch it under Actions in `QRun-IO/qqq-cli`. The release page is
|
||||
`https://git.kof22.com/QRun-IO/qqq-cli/releases/tag/v<version>`.
|
||||
|
||||
**No musl binaries.** `build-binaries.mjs --musl` is best effort: on the glibc build container Bun does not
|
||||
install OpenTUI's musl native packages, so the release carries no `qqq-linux-*-musl`. On Alpine and other
|
||||
musl hosts use the npm package (`@qrun-io/qqq-cli`, CLI only) until a musl build lane exists.
|
||||
|
||||
## 4. Homebrew tap and Scoop bucket (when CI could not push)
|
||||
|
||||
`PACKAGES_TOKEN` is the package-scoped publisher token (user `qqq-package-publisher`); unless it is granted
|
||||
write on the tap and the bucket, CI attaches the filled files to the release instead. Their `main` branches
|
||||
should be protected so CI can only propose. For a final version, download them and open a
|
||||
pull request on each:
|
||||
|
||||
```sh
|
||||
QQQ_MACOS_SIGN_IDENTITY="Developer ID Application: <name> (<team>)" QQQ_NOTARY_PROFILE=qqq-notary \
|
||||
scripts/sign-macos.sh # codesign, hardened runtime + scripts/macos-entitlements.plist, notarytool --wait
|
||||
QQQ_WINDOWS_PFX=<cert.pfx> QQQ_WINDOWS_PFX_PASSWORD_FILE=<file> \
|
||||
scripts/sign-windows.sh # osslsigncode with an RFC 3161 timestamp
|
||||
scripts/verify-binary.sh dist/qqq-darwin-arm64 --server http://localhost:8077 # still runs after signing
|
||||
spctl --assess --type execute -vv dist/qqq-darwin-arm64 # "accepted, source=Notarized Developer ID"
|
||||
curl -fsSLO https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v1.1.0/qqq.rb
|
||||
curl -fsSLO https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v1.1.0/qqq.json
|
||||
# QRun-IO/homebrew-tap: Formula/qqq.rb QRun-IO/scoop-bucket: bucket/qqq.json
|
||||
```
|
||||
|
||||
A bare executable cannot be stapled: Gatekeeper fetches the notarization ticket online the first time a
|
||||
quarantined (browser-downloaded) copy runs, and refuses offline until it can. Homebrew and the curl
|
||||
installer do not quarantine, so they work signed or not. Tried here without real credentials: an ad-hoc
|
||||
hardened-runtime signature with the entitlements (the CLI and the `qqq ui` smoke still pass), and an
|
||||
osslsigncode signature with a throwaway certificate that verifies. A real Developer ID, notarization
|
||||
and an Authenticode certificate are untried until James's credentials are used.
|
||||
Compare each sum with the release's `SHA256SUMS`. A wrong sum makes `brew install` and `scoop install`
|
||||
fail their checksum; the curl installer and direct downloads are unaffected.
|
||||
|
||||
## 5. Tags and packages
|
||||
## 5. Signing (not automated)
|
||||
|
||||
```sh
|
||||
git tag -s v1.8.0 -m "UI API client 1.8.0, qqq 1.0.0" # the orb publishes on v<client version>
|
||||
git tag -s cli-v1.0.0 -m "qqq 1.0.0" # the GitHub release with the binaries
|
||||
git push origin v1.8.0 cli-v1.0.0
|
||||
```
|
||||
The binaries are not signed by CI: there is no Apple Developer ID / notary credential or Authenticode
|
||||
certificate on the runners. Homebrew and the curl installer do not quarantine, so they work unsigned; a
|
||||
browser-downloaded macOS binary needs `xattr -d com.apple.quarantine ./qqq-darwin-arm64`, and Windows
|
||||
SmartScreen warns on first run. `scripts/sign-macos.sh` and `scripts/sign-windows.sh` remain for a manual
|
||||
signed release (they recompute `dist/SHA256SUMS`); wiring them into the native jobs needs the credentials
|
||||
as runner-side secrets (owner decision).
|
||||
|
||||
The tag `v1.8.0` makes the orb's `cli` profile verify the commit again and publish client, core,
|
||||
conformance and CLI to GitHub Packages, all as `latest` because all four are final. Watch the
|
||||
`publish_release` workflow. Only if the orb release with PR #165 is not out yet, finish by hand:
|
||||
Gitea has no equivalent of GitHub's artifact attestations; `SHA256SUMS` on the release and the signed tag
|
||||
are the provenance.
|
||||
|
||||
```sh
|
||||
npm publish -w @qrun-io/qqq-core --registry https://npm.pkg.github.com --tag latest
|
||||
for p in qqq-ui-api-client qqq-core qqq-ui-api-conformance qqq-cli; do
|
||||
npm dist-tag add "@qrun-io/$p@<its version>" latest --registry https://npm.pkg.github.com
|
||||
done
|
||||
```
|
||||
|
||||
A version can never be published twice: if the orb job fails after publishing some packages, bump the
|
||||
patch version of the rest instead of retrying the same one.
|
||||
|
||||
## 6. GitHub release
|
||||
|
||||
```sh
|
||||
gh release create cli-v1.0.0 --repo QRun-IO/qqq-cli --latest --title "qqq 1.0.0" \
|
||||
--notes-file <(sed -n '/^## \[1\.0\.0\]/,/^## \[/p' CHANGELOG.md | sed '$d') \
|
||||
dist/qqq-* dist/SHA256SUMS
|
||||
```
|
||||
|
||||
`dist/qqq-*` is the seven binaries (source maps are in `dist/sourcemaps/`, not uploaded). For a release
|
||||
candidate use `--prerelease` instead of `--latest`. The curl installer takes the newest stable `cli-v*`
|
||||
release (the channel Homebrew and Scoop follow); a release candidate is reached with `--pre` or
|
||||
`--version 1.x.y-rc.N`.
|
||||
|
||||
## 7. Homebrew tap and Scoop bucket
|
||||
|
||||
```sh
|
||||
gh release download cli-v1.0.0 --repo QRun-IO/qqq-cli -p SHA256SUMS -D /tmp/qqq-1.0.0
|
||||
node scripts/update-packaging.mjs /tmp/qqq-1.0.0/SHA256SUMS
|
||||
git diff docs/packaging # every sha256 differs from the placeholder, none is all zeros
|
||||
git commit -S -am "packaging: qqq 1.0.0" && git push # in qqq-cli, through a pull request
|
||||
```
|
||||
|
||||
The script exits 1 and writes nothing if `SHA256SUMS` lacks a target or has a malformed sum. Compare each sum
|
||||
in the diff with the release's `SHA256SUMS` by eye before committing: the tap's CI and the bucket's manifest
|
||||
tests catch a wrong sum (the download will not match) only when someone opens those pull requests and reads
|
||||
their checks.
|
||||
|
||||
Then copy `docs/packaging/homebrew-tap/Formula/qqq.rb` to `Formula/qqq.rb` in `QRun-IO/homebrew-tap`
|
||||
and `docs/packaging/qqq.json` to `bucket/qqq.json` in `QRun-IO/scoop-bucket`, a pull request on each. The tap's
|
||||
CI fetches every platform's download, installs the formula and runs its test (which insists on
|
||||
`binary with ui`). Scoop's `checkver`/`autoupdate` can take later versions from `SHA256SUMS` by itself.
|
||||
|
||||
## 8. Smoke after publishing
|
||||
## 6. Smoke after publishing
|
||||
|
||||
Against a real server (`--server`), from clean machines or containers:
|
||||
|
||||
```sh
|
||||
# curl installer (macOS, Linux)
|
||||
curl -fsSL https://raw.githubusercontent.com/QRun-IO/qqq-cli/main/scripts/install.sh | sh -s -- --version 1.0.0
|
||||
curl -fsSL https://git.kof22.com/QRun-IO/qqq-cli/raw/branch/main/scripts/install.sh | sh -s -- --version 1.1.0
|
||||
scripts/verify-binary.sh ~/.local/bin/qqq --server https://<server>
|
||||
|
||||
# Homebrew
|
||||
brew install qrun-io/tap/qqq && brew test qrun-io/tap/qqq && scripts/verify-binary.sh "$(brew --prefix)/bin/qqq"
|
||||
brew install qrun-io/tap/qqq && brew test qrun-io/tap/qqq
|
||||
|
||||
# npm, Node 20
|
||||
echo "@qrun-io:registry=https://npm.pkg.github.com" >> ~/.npmrc
|
||||
npm install -g @qrun-io/qqq-cli@1.0.0 && scripts/verify-binary.sh qqq --cli-only --server https://<server>
|
||||
|
||||
# Docker (glibc and musl)
|
||||
docker run --rm --platform linux/amd64 -v "$PWD/dist:/d:ro" -v "$PWD/scripts:/s:ro" debian:bookworm-slim bash /s/verify-binary.sh /d/qqq-linux-x64
|
||||
# npm, Node 20+
|
||||
echo "@qrun-io:registry=https://git.kof22.com/api/packages/QRun-IO/npm/" >> ~/.npmrc
|
||||
npm install -g @qrun-io/qqq-cli@1.1.0 && scripts/verify-binary.sh qqq --cli-only --server https://<server>
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows, PowerShell
|
||||
scoop bucket add qrun https://github.com/QRun-IO/scoop-bucket; scoop install qqq
|
||||
scoop bucket add qrun https://git.kof22.com/QRun-IO/scoop-bucket; scoop install qqq
|
||||
pwsh scripts\verify-binary.ps1 -Binary (Get-Command qqq).Source -Server https://<server>
|
||||
```
|
||||
|
||||
And by hand once: download `qqq-darwin-arm64` with a browser on a Mac and run it (notarized: it runs;
|
||||
not notarized: the Gatekeeper prompt, cleared by `xattr -d com.apple.quarantine`), then `qqq ui` in
|
||||
Windows Terminal.
|
||||
|
||||
## 9. After
|
||||
|
||||
- Merge `main` back into the development branch; the next development version can stay at 1.0.0 until
|
||||
the next bump (lint keeps the versions consistent either way).
|
||||
- Announce with the release link and the install commands from the README.
|
||||
Users who added the tap or the bucket from GitHub re-add them from Gitea
|
||||
(`brew tap qrun-io/tap https://git.kof22.com/QRun-IO/homebrew-tap`).
|
||||
|
||||
## When something goes wrong
|
||||
|
||||
- **A package is broken**: `npm deprecate @qrun-io/<pkg>@<version> "<reason>"` and publish a patch; a
|
||||
version cannot be unpublished from GitHub Packages without breaking installs that pinned it.
|
||||
- **A binary is broken**: delete the asset from the GitHub release, rebuild, re-sign, upload, recompute
|
||||
`SHA256SUMS` and rerun step 7 (the formula and manifest carry the sums).
|
||||
- **The tap's CI fails on the new formula**: do not merge its pull request; `brew install` keeps
|
||||
serving the previous version.
|
||||
- **The run fails before the publish job**: nothing is public. Delete the draft release (and the tag, if
|
||||
the version must change), fix, and tag again.
|
||||
- **A package is broken**: `npm deprecate @qrun-io/<pkg>@<version> "<reason>"` against the Gitea registry
|
||||
and publish a new version; a version is never republished.
|
||||
- **A binary is broken**: delete the release, bump the version and release again; the formula and the
|
||||
manifest carry the sums, so never replace an asset in place.
|
||||
- **The tap's or bucket's check fails on the new file**: do not merge; users keep the previous version.
|
||||
Generated
+6
-6
@@ -2954,11 +2954,11 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@qrun-io/qqq-cli",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1-rc.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@qrun-io/qqq-core": "1.0.0",
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.0",
|
||||
"@qrun-io/qqq-core": "1.0.1-rc.1",
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.1-rc.1",
|
||||
"@qrun-io/qqq-ui-api-conformance": "1.6.0",
|
||||
"commander": "15.0.0"
|
||||
},
|
||||
@@ -2986,10 +2986,10 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@qrun-io/qqq-core",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1-rc.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.0",
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.1-rc.1",
|
||||
"jmespath": "0.16.0",
|
||||
"yaml": "2.9.1"
|
||||
},
|
||||
@@ -3002,7 +3002,7 @@
|
||||
},
|
||||
"packages/ui-api-client": {
|
||||
"name": "@qrun-io/qqq-ui-api-client",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.1-rc.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"openapi-fetch": "0.17.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qrun-io/qqq-cli",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1-rc.1",
|
||||
"description": "Reference command-line client for QQQ applications, built only on the QQQ UI API (/qqq/v1).",
|
||||
"keywords": [
|
||||
"qqq",
|
||||
@@ -50,7 +50,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
"registry": "https://git.kof22.com/api/packages/QRun-IO/npm/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
@@ -58,8 +58,8 @@
|
||||
"prepack": "node ../../scripts/copy-license.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@qrun-io/qqq-core": "1.0.0",
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.0",
|
||||
"@qrun-io/qqq-core": "1.0.1-rc.1",
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.1-rc.1",
|
||||
"@qrun-io/qqq-ui-api-conformance": "1.6.0",
|
||||
"commander": "15.0.0"
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { mkdtempSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { runCli, sandbox, startFakeServer, type FakeServer } from './harness.js'
|
||||
import { CLI_VERSION } from '../version.js'
|
||||
|
||||
type J = Record<string, unknown>
|
||||
|
||||
@@ -266,7 +267,7 @@ describe('qqq print and qqq printer', () => {
|
||||
assert.deepEqual(filter.orderBys, [{ fieldName: 'id', isAscending: false }])
|
||||
assert.equal(run.values.filterDescription, 'lastName EQUALS Odd')
|
||||
// the app header and footer
|
||||
assert.match(String(run.values.footerLeft), /^qqq \d+\.\d+\.\d+ · 127\.0\.0\.1:\d+$/)
|
||||
assert.equal(String(run.values.footerLeft).replace(/:\d+$/, ':PORT'), `qqq ${CLI_VERSION} · 127.0.0.1:PORT`)
|
||||
assert.match(String(run.values.headerServer), /^127\.0\.0\.1:\d+$/)
|
||||
assert.equal(run.values.headerTitle, 'Person')
|
||||
assert.match(String(run.values.headerPrintedAt), /\d/)
|
||||
|
||||
@@ -18,7 +18,7 @@ declare const QQQ_BUILD_SHA: string | undefined
|
||||
declare const QQQ_BUILD_TARGET: string | undefined
|
||||
declare const QQQ_BUILD_UI: boolean | undefined
|
||||
|
||||
export const CLI_VERSION = '1.0.0'
|
||||
export const CLI_VERSION = '1.0.1-rc.1'
|
||||
|
||||
/** The oldest UI API a server may serve for this CLI (operationIds, clientCapabilities and traceId since 1.6.0). */
|
||||
export const UI_API_MIN_VERSION = '1.6.0'
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
"registry": "https://git.kof22.com/api/packages/QRun-IO/npm/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qrun-io/qqq-core",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1-rc.1",
|
||||
"description": "Client kernel for QQQ terminal clients: sessions, metadata, filter grammar, records, processes, jobs, render model and output formats. No terminal code.",
|
||||
"keywords": [
|
||||
"qqq",
|
||||
@@ -42,7 +42,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
"registry": "https://git.kof22.com/api/packages/QRun-IO/npm/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
@@ -50,7 +50,7 @@
|
||||
"prepack": "node ../../scripts/copy-license.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.0",
|
||||
"@qrun-io/qqq-ui-api-client": "1.8.1-rc.1",
|
||||
"jmespath": "0.16.0",
|
||||
"yaml": "2.9.1"
|
||||
},
|
||||
|
||||
@@ -110,7 +110,10 @@ export class DpapiFileStore implements SecretStore {
|
||||
+ '[Console]::Write((ConvertTo-Json -InputObject $qqqResult -Compress))'
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const failed = () => reject(new Error('Windows credential protection failed; check PowerShell and your Windows profile'))
|
||||
const child = execFile(shell, ['-NoProfile', '-NonInteractive', '-Command', command], { timeout: 15000 }, (error, stdout) => {
|
||||
// Started from PowerShell 7, the inherited PSModulePath names PowerShell 7's modules, and Windows PowerShell 5.1
|
||||
// then cannot load Microsoft.PowerShell.Security (ConvertTo-SecureString). Without it, each shell uses its own.
|
||||
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => name.toUpperCase() !== 'PSMODULEPATH'))
|
||||
const child = execFile(shell, ['-NoProfile', '-NonInteractive', '-Command', command], { timeout: 15000, env }, (error, stdout) => {
|
||||
if (error) { failed(); return }
|
||||
try {
|
||||
const result: unknown = JSON.parse(stdout.trim())
|
||||
|
||||
@@ -231,7 +231,7 @@ test('Ctrl+P on a record prints its record sheet; F8 says Print where there is n
|
||||
expect(String(run.values.headerServer)).toContain('fake.test')
|
||||
expect(run.values.headerUser).toBe(h.store.status.user)
|
||||
expect(String(run.values.headerPrintedAt)).toMatch(/\d/)
|
||||
expect(String(run.values.footerLeft)).toMatch(/^qqq ui \d+\.\d+\.\d+ · .*fake\.test/)
|
||||
expect(String(run.values.footerLeft)).toMatch(/^qqq ui \d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)? · .*fake\.test/)
|
||||
})
|
||||
|
||||
test('Print what? → Screen prints the terminal as drawn; a record print carries the breadcrumb', async () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ test('one profile: the splash shows Query, the wordmark, the version and the boo
|
||||
const f = await waitFor(h, (x) => x.includes('Terminal client for QQQ'))
|
||||
expect(h.store.screen?.kind).toBe('splash')
|
||||
expect(f).toContain(versionLine())
|
||||
expect(f).toMatch(/qqq \d+\.\d+\.\d+ · UI API client \d+\.\d+\.\d+/)
|
||||
expect(f).toMatch(/qqq \d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)? · UI API client \d+\.\d+\.\d+/)
|
||||
expect(f).toContain('█')
|
||||
// no title bar, status line or key bar under the splash
|
||||
expect(f).not.toMatch(/10\s*Quit/)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qrun-io/qqq-ui-api-client",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.1-rc.1",
|
||||
"description": "Typed TypeScript client for the QQQ UI API (/qqq/v1), generated from the merged OpenAPI document (core + extensions), with OAuth2 device-flow sign-in, token refresh and a realtime (SSE) reader.",
|
||||
"keywords": [
|
||||
"qqq",
|
||||
@@ -47,7 +47,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
"registry": "https://git.kof22.com/api/packages/QRun-IO/npm/"
|
||||
},
|
||||
"scripts": {
|
||||
"generate": "openapi-typescript openapi/qqq-v1.json --output src/schema.ts && node ../../scripts/fix-recursive-types.mjs src/schema.ts",
|
||||
|
||||
+21
-21
@@ -2,13 +2,13 @@
|
||||
# Copyright 2026 QRun.IO, Inc.
|
||||
# Licensed under the Apache License, Version 2.0 (see LICENSE).
|
||||
#
|
||||
# Installs the qqq CLI single-file binary for this machine from the GitHub releases of
|
||||
# QRun-IO/qqq-cli, after checking it against the release's SHA256SUMS.
|
||||
# Installs the qqq CLI single-file binary for this machine from the Gitea releases of
|
||||
# QRun-IO/qqq-cli (git.kof22.com), after checking it against the release's SHA256SUMS.
|
||||
#
|
||||
# curl -fsSL https://raw.githubusercontent.com/QRun-IO/qqq-cli/main/scripts/install.sh | sh
|
||||
# curl -fsSL https://git.kof22.com/QRun-IO/qqq-cli/raw/branch/main/scripts/install.sh | sh
|
||||
# curl -fsSL .../install.sh | sh -s -- --version 1.0.0 --dir /usr/local/bin
|
||||
#
|
||||
# By default it installs the newest stable cli-v* release (the channel Homebrew and Scoop follow);
|
||||
# By default it installs the newest stable v* release (the channel Homebrew and Scoop follow);
|
||||
# --pre takes the newest release including prereleases. --help lists the options.
|
||||
|
||||
set -eu
|
||||
@@ -16,31 +16,31 @@ set -eu
|
||||
repo="QRun-IO/qqq-cli"
|
||||
version="${QQQ_VERSION:-}"
|
||||
dir="${QQQ_INSTALL_DIR:-$HOME/.local/bin}"
|
||||
base="${QQQ_DOWNLOAD_BASE:-https://github.com/$repo/releases/download}"
|
||||
base="${QQQ_DOWNLOAD_BASE:-https://git.kof22.com/$repo/releases/download}"
|
||||
musl=""
|
||||
pre=""
|
||||
dry_run=""
|
||||
releases_url="${QQQ_RELEASES_URL:-https://api.github.com/repos/$repo/releases?per_page=30}"
|
||||
releases_url="${QQQ_RELEASES_URL:-https://git.kof22.com/api/v1/repos/$repo/releases?limit=30}"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Install qqq (the CLI and the terminal UI, one binary) from https://github.com/QRun-IO/qqq-cli/releases
|
||||
Install qqq (the CLI and the terminal UI, one binary) from https://git.kof22.com/QRun-IO/qqq-cli/releases
|
||||
|
||||
curl -fsSL https://raw.githubusercontent.com/QRun-IO/qqq-cli/main/scripts/install.sh | sh
|
||||
curl -fsSL https://raw.githubusercontent.com/QRun-IO/qqq-cli/main/scripts/install.sh | sh -s -- [options]
|
||||
curl -fsSL https://git.kof22.com/QRun-IO/qqq-cli/raw/branch/main/scripts/install.sh | sh
|
||||
curl -fsSL https://git.kof22.com/QRun-IO/qqq-cli/raw/branch/main/scripts/install.sh | sh -s -- [options]
|
||||
|
||||
Options (or environment):
|
||||
--version V (QQQ_VERSION) a release such as 1.0.0; default: the newest stable cli-v* release
|
||||
--version V (QQQ_VERSION) a release such as 1.0.0; default: the newest stable v* release
|
||||
--pre (QQQ_PRE=1) with no --version, take the newest release including prereleases
|
||||
--dir D (QQQ_INSTALL_DIR) where qqq goes; default: ~/.local/bin
|
||||
--musl take the musl build on Linux (detected on Alpine and other musl hosts)
|
||||
--dry-run print the version and download URL, change nothing
|
||||
-h, --help this help
|
||||
QQQ_DOWNLOAD_BASE release download root (default https://github.com/QRun-IO/qqq-cli/releases/download),
|
||||
QQQ_DOWNLOAD_BASE release download root (default https://git.kof22.com/QRun-IO/qqq-cli/releases/download),
|
||||
for mirrors and testing
|
||||
|
||||
Supported: macOS arm64 and x64, Linux x64 and arm64 (glibc and musl). On Windows use Scoop:
|
||||
scoop bucket add qrun https://github.com/QRun-IO/scoop-bucket
|
||||
scoop bucket add qrun https://git.kof22.com/QRun-IO/scoop-bucket
|
||||
scoop install qqq
|
||||
USAGE
|
||||
}
|
||||
@@ -73,7 +73,7 @@ fi
|
||||
case "$(uname -s)" in
|
||||
Darwin) os=darwin ;;
|
||||
Linux) os=linux ;;
|
||||
MINGW*|MSYS*|CYGWIN*) die "on Windows install with Scoop (scoop bucket add qrun https://github.com/QRun-IO/scoop-bucket; scoop install qqq) or download qqq-windows-x64.exe from https://github.com/$repo/releases" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) die "on Windows install with Scoop (scoop bucket add qrun https://git.kof22.com/QRun-IO/scoop-bucket; scoop install qqq) or download qqq-windows-x64.exe from https://git.kof22.com/$repo/releases" ;;
|
||||
*) die "unsupported operating system $(uname -s)" ;;
|
||||
esac
|
||||
case "$(uname -m)" in
|
||||
@@ -91,26 +91,26 @@ fi
|
||||
|
||||
[ -n "${QQQ_PRE:-}" ] && [ "${QQQ_PRE}" != 0 ] && pre=1
|
||||
if [ -z "$version" ]; then
|
||||
# The releases API lists newest first; each release's "tag_name" comes before its "prerelease".
|
||||
# The Gitea releases API lists newest first; each release's "tag_name" comes before its "prerelease".
|
||||
# Escaped quotes inside release notes never match these patterns.
|
||||
version="$(fetch_text "$releases_url" | tr ',' '\n' | awk -v pre="$pre" '
|
||||
/"tag_name": *"/ { tag = $0; sub(/.*"tag_name": *"/, "", tag); sub(/".*/, "", tag); next }
|
||||
/"prerelease": *(true|false)/ {
|
||||
if (tag ~ /^cli-v/ && (pre != "" || $0 ~ /false/)) { sub(/^cli-v/, "", tag); print tag; exit }
|
||||
if (tag ~ /^v[0-9]/ && (pre != "" || $0 ~ /false/)) { sub(/^v/, "", tag); print tag; exit }
|
||||
tag = ""
|
||||
}')" || true
|
||||
if [ -z "$version" ]; then
|
||||
if [ -n "$pre" ]; then die "could not find a cli-v* release of $repo; pass --version"; fi
|
||||
die "could not find a stable cli-v* release of $repo; pass --version, or --pre for prereleases"
|
||||
if [ -n "$pre" ]; then die "could not find a v* release of $repo; pass --version"; fi
|
||||
die "could not find a stable v* release of $repo; pass --version, or --pre for prereleases"
|
||||
fi
|
||||
fi
|
||||
version="${version#v}"
|
||||
version="${version#cli-v}"
|
||||
|
||||
file="qqq-$os-$arch$musl"
|
||||
url="$base/cli-v$version/$file"
|
||||
url="$base/v$version/$file"
|
||||
if [ -n "$dry_run" ]; then
|
||||
say "qqq $version: $url (checked against $base/cli-v$version/SHA256SUMS), into $dir/qqq"
|
||||
say "qqq $version: $url (checked against $base/v$version/SHA256SUMS), into $dir/qqq"
|
||||
exit 0
|
||||
fi
|
||||
tmp="$(mktemp -d 2>/dev/null || mktemp -d -t qqq)"
|
||||
@@ -118,8 +118,8 @@ trap 'rm -rf "$tmp"' EXIT
|
||||
trap 'exit 130' INT TERM
|
||||
|
||||
say "Downloading qqq $version ($file)"
|
||||
fetch "$url" "$tmp/$file" || die "download failed: $url"
|
||||
fetch "$base/cli-v$version/SHA256SUMS" "$tmp/SHA256SUMS" || die "download failed: $base/cli-v$version/SHA256SUMS"
|
||||
fetch "$url" "$tmp/$file" || die "download failed: $url${musl:+ (releases since 1.0.1 have no musl build; on musl hosts install the CLI with npm: @qrun-io/qqq-cli)}"
|
||||
fetch "$base/v$version/SHA256SUMS" "$tmp/SHA256SUMS" || die "download failed: $base/v$version/SHA256SUMS"
|
||||
|
||||
expected="$(awk -v f="$file" '$2 == f { print $1 }' "$tmp/SHA256SUMS")"
|
||||
[ -n "$expected" ] || die "SHA256SUMS has no entry for $file"
|
||||
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 QRun.IO, Inc.
|
||||
# Licensed under the Apache License, Version 2.0 (see LICENSE).
|
||||
#
|
||||
# Offer the filled Homebrew formula and Scoop manifest (scripts/update-packaging.mjs) to the tap and the
|
||||
# bucket as pull requests; a human merges them. Called by the release workflow's publish job.
|
||||
#
|
||||
# final version push branch release/qqq-<version> to QRun-IO/homebrew-tap and QRun-IO/scoop-bucket with
|
||||
# PACKAGES_TOKEN and open a pull request "qqq <version>" in each
|
||||
# otherwise (prerelease, or the push or the pull request is refused) attach qqq.rb / qqq.json to the
|
||||
# release instead, for a hand-made pull request (docs/release-runbook.md step 4)
|
||||
#
|
||||
# Never fails the release over the tap or the bucket. DRY_RUN=1 clones and commits locally but pushes,
|
||||
# opens and attaches nothing; it needs only VERSION (and GITHUB_SERVER_URL, default https://git.kof22.com):
|
||||
# DRY_RUN=1 VERSION=1.2.3 scripts/publish-packaging.sh
|
||||
#
|
||||
# Environment: VERSION, GITHUB_SERVER_URL, GITHUB_REPOSITORY, RELEASE_ID, TOKEN (job token, release assets),
|
||||
# PACKAGES_TOKEN (tap and bucket), DRY_RUN.
|
||||
set -euo pipefail
|
||||
|
||||
server=${GITHUB_SERVER_URL:-https://git.kof22.com}
|
||||
api="$server/api/v1"
|
||||
dry=${DRY_RUN:-0}
|
||||
: "${VERSION:?VERSION is required}"
|
||||
[[ $dry == 1 ]] || : "${TOKEN:?TOKEN is required}" "${RELEASE_ID:?RELEASE_ID is required}" "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
|
||||
branch="release/qqq-$VERSION"
|
||||
title="qqq $VERSION"
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
json() { node -e 'const o = {}; for (let i = 1; i < process.argv.length; i += 2) o[process.argv[i]] = process.argv[i + 1];
|
||||
process.stdout.write(JSON.stringify(o))' -- "$@"; }
|
||||
field() { node -e 'try { const v = JSON.parse(require("fs").readFileSync(0, "utf8")); const r = [].concat(v)
|
||||
.find((p) => !process.argv[2] || (p.head && p.head.ref === process.argv[2])); process.stdout.write(String(r ? r[process.argv[1]] ?? "" : "")) }
|
||||
catch { }' -- "$@"; }
|
||||
|
||||
attach() { # attach <file>: the fallback, a hand-made pull request from the release asset
|
||||
local src=$1 name
|
||||
name=$(basename "$src")
|
||||
if [[ $dry == 1 ]]; then echo "DRY RUN: would attach $name to the release"; return 0; fi
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $TOKEN" -F "attachment=@$src" \
|
||||
"$api/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$name" ||
|
||||
{ echo "::warning::could not attach $name to the release"; return 0; }
|
||||
echo "Attached $name to the release for a hand-made pull request"
|
||||
}
|
||||
|
||||
propose() { # propose <repo> <src> <dest>: branch + pull request; non-zero means fall back
|
||||
local repo=$1 src=$2 dest=$3 dir="$work/${1##*/}" header=() base pr body
|
||||
[[ $dry == 1 || -n ${PACKAGES_TOKEN:-} ]] || { echo "PACKAGES_TOKEN is empty"; return 1; }
|
||||
[[ -n ${PACKAGES_TOKEN:-} ]] && header=(-c "http.extraHeader=Authorization: Basic $(printf 'ci:%s' "$PACKAGES_TOKEN" | base64 | tr -d '\n')")
|
||||
git "${header[@]}" clone -q --depth 1 "$server/$repo.git" "$dir" || return 1
|
||||
base=$(git -C "$dir" rev-parse --abbrev-ref HEAD)
|
||||
git -C "$dir" checkout -q -b "$branch"
|
||||
mkdir -p "$(dirname "$dir/$dest")"
|
||||
cp "$src" "$dir/$dest"
|
||||
git -C "$dir" add "$dest"
|
||||
if git -C "$dir" diff --cached --quiet; then echo "$repo $base already has qqq $VERSION"; return 0; fi
|
||||
git -C "$dir" -c user.name=CI -c user.email=ci@kof22.com commit -q -m "$title"
|
||||
git -C "$dir" --no-pager show --stat --format='%s' HEAD
|
||||
if [[ $dry == 1 ]]; then
|
||||
echo "DRY RUN: would push $branch to $repo and open pull request \"$title\" into $base"; return 0
|
||||
fi
|
||||
# CI owns release/qqq-<version>: a re-run replaces it.
|
||||
git -C "$dir" "${header[@]}" push -q --force origin "HEAD:refs/heads/$branch" || { echo "push to $repo refused"; return 1; }
|
||||
body="Formula/manifest for qqq $VERSION, filled by the qqq-cli release workflow from the release's SHA256SUMS: $server/$GITHUB_REPOSITORY/releases/tag/v$VERSION. Check the sums, then merge."
|
||||
pr=$(curl -fsS -X POST -H "Authorization: token $PACKAGES_TOKEN" -H 'Content-Type: application/json' \
|
||||
-d "$(json head "$branch" base "$base" title "$title" body "$body")" "$api/repos/$repo/pulls" 2>/dev/null | field html_url) || pr=''
|
||||
if [[ -z $pr ]]; then # a re-run: the pull request may already be open
|
||||
pr=$(curl -fsS -H "Authorization: token $PACKAGES_TOKEN" "$api/repos/$repo/pulls?state=open&limit=50" 2>/dev/null |
|
||||
field html_url "$branch") || pr=''
|
||||
fi
|
||||
[[ -n $pr ]] || { echo "pushed $branch to $repo but could not open the pull request"; return 1; }
|
||||
echo "Opened $pr"
|
||||
}
|
||||
|
||||
node "$(dirname "$0")/update-packaging.mjs" "${SUMS_FILE:-/tmp/assets/SHA256SUMS}"
|
||||
for spec in "QRun-IO/homebrew-tap docs/packaging/homebrew-tap/Formula/qqq.rb Formula/qqq.rb" \
|
||||
"QRun-IO/scoop-bucket docs/packaging/qqq.json bucket/qqq.json"; do
|
||||
read -r repo src dest <<< "$spec"
|
||||
# Final versions only: the tap and the bucket follow the stable channel, like scripts/install.sh.
|
||||
if [[ $VERSION == *-* ]]; then
|
||||
echo "$repo not updated: $VERSION is a prerelease"; attach "$src"; continue
|
||||
fi
|
||||
propose "$repo" "$src" "$dest" || { echo "$repo not updated: falling back to the release asset"; attach "$src"; }
|
||||
done
|
||||
+18
-44
@@ -5,18 +5,20 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Release check for the CLI packages. Runs everything a release needs locally and then prints the
|
||||
* commands that tag and publish it. It never tags, pushes or publishes.
|
||||
* 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 tags are free
|
||||
* 2. npm ci, then the steps the qqq-orb `cli` profile runs: npm run generate with no drift in
|
||||
* 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 the tag, GitHub release and npm publish commands
|
||||
* 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);
|
||||
@@ -40,11 +42,6 @@ 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']
|
||||
|
||||
/** A literal text as a sed basic regular expression (its metacharacters and the `/` delimiter escaped). */
|
||||
function sedPattern(text) {
|
||||
return text.replace(/[\\/.*[\]^$]/g, '\\$&')
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(`\nrelease: ${message}`)
|
||||
process.exit(1)
|
||||
@@ -62,10 +59,8 @@ function step(title, command, args) {
|
||||
|
||||
const pkg = (dir) => JSON.parse(readFileSync(join(root, dir, 'package.json'), 'utf8'))
|
||||
const versions = Object.fromEntries(PUBLISHED.map((dir) => [dir, pkg(dir)]))
|
||||
const client = versions['packages/ui-api-client']
|
||||
const cli = versions['packages/cli']
|
||||
const orbTag = `v${client.version}`
|
||||
const cliTag = `cli-v${cli.version}`
|
||||
const tag = `v${cli.version}`
|
||||
|
||||
if (argv.has('--check')) {
|
||||
if (!existsSync(join(root, 'packages/cli/dist/program.js'))) step('build', 'npm', ['run', 'build'])
|
||||
@@ -79,8 +74,8 @@ for (const { name, version } of Object.values(versions)) console.log(` ${name.p
|
||||
|
||||
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)`)
|
||||
for (const tag of [orbTag, cliTag]) {
|
||||
if (git('tag', '--list', tag)) fail(`tag ${tag} already exists; bump the versions first`)
|
||||
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')
|
||||
@@ -102,42 +97,21 @@ 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 registry = 'https://npm.pkg.github.com'
|
||||
// the orb publishes all four packages as rc when any of them is a prerelease, and as latest only when
|
||||
// all four are final; the GitHub release follows the CLI version
|
||||
const prerelease = Object.values(versions).some((p) => p.version.includes('-'))
|
||||
const distTag = prerelease ? 'rc' : 'latest'
|
||||
const releaseKind = prerelease ? '--prerelease' : '--latest'
|
||||
console.log(`
|
||||
All checks passed at ${branch}@${sha}. Nothing was tagged or published. To release:
|
||||
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:
|
||||
|
||||
# 1. tag (signed) and push. ${orbTag} makes the qqq-orb cli profile publish the client, conformance
|
||||
# and CLI packages to GitHub Packages with dist-tag ${distTag} (the orb checks ${orbTag} = client version).${prerelease ? '' : '\n # All four versions are final, so the orb publishes them as latest (qqq-orb with PR #165).'}
|
||||
git tag -s ${orbTag} -m "UI API client ${client.version}, qqq-cli ${cli.version}"
|
||||
git tag -s ${cliTag} -m "qqq-cli ${cli.version}"
|
||||
git push origin ${orbTag} ${cliTag}
|
||||
git tag -s ${tag} -m "qqq ${cli.version}" && git push origin ${tag}
|
||||
|
||||
# 2. until the orb release with PR #165, the orb does not publish @qrun-io/qqq-core, and the CLI
|
||||
# depends on it: publish it by hand right after the orb job, or the CLI cannot be installed.
|
||||
npm publish -w ${versions['packages/core'].name} --registry ${registry} --tag ${distTag}
|
||||
|
||||
# without the orb (all four, in dependency order):
|
||||
${PUBLISHED.map((dir) => ` npm publish -w ${versions[dir].name} --registry ${registry} --tag ${distTag}`).join('\n')}
|
||||
|
||||
# 3. the binaries with their checksums (${prerelease ? "a prerelease" : "the latest release"})
|
||||
gh release create ${cliTag} --repo QRun-IO/qqq-cli ${releaseKind} --title "qqq ${cli.version}" \\
|
||||
--notes-file ${existsSync(join(root, `docs/release-notes/${cli.version}.md`)) ? `docs/release-notes/${cli.version}.md` : `<(sed -n '/^## \\[${sedPattern(cli.version)}\\]/,/^## \\[/p' CHANGELOG.md | sed '$d')`} \\
|
||||
dist/qqq-* dist/SHA256SUMS
|
||||
|
||||
# 4. node scripts/update-packaging.mjs <SHA256SUMS of the GitHub release>, then copy
|
||||
# docs/packaging/homebrew-tap/Formula/qqq.rb to the tap and docs/packaging/qqq.json to the Scoop
|
||||
# bucket (docs/release-runbook.md steps 7 and 8). With the certificates, sign the macOS and Windows
|
||||
# binaries and recompute dist/SHA256SUMS before step 3 (runbook step 4).
|
||||
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('')
|
||||
}
|
||||
const conformance = versions['packages/conformance']
|
||||
console.log(`Note: the orb also publishes ${conformance.name} ${conformance.version} on ${orbTag}; bump it first if that version is already in the registry.`)
|
||||
console.log('Note: every package version must be new in the registry; bump any that is already published.')
|
||||
@@ -7,8 +7,9 @@
|
||||
/**
|
||||
* Fills the version and sha256 values of the packaging files (docs/packaging/homebrew-tap/Formula/qqq.rb for Homebrew,
|
||||
* docs/packaging/qqq.json for Scoop) from packages/cli/package.json and a release's SHA256SUMS.
|
||||
* Run it on the binaries that were uploaded to the GitHub release, not on a local rebuild: the
|
||||
* commit is stamped into each binary, so every build has different checksums.
|
||||
* The release workflow (.gitea/workflows/release.yaml) runs it on the SHA256SUMS of the binaries uploaded to
|
||||
* the Gitea release, never on a local rebuild: the commit is stamped into each binary, so every build has
|
||||
* different checksums.
|
||||
*
|
||||
* Refuses to write anything (exit 1, both files untouched) when a sum is missing, is not 64 lowercase hex digits,
|
||||
* is all zeros, or when a placeholder in the files is left unfilled.
|
||||
@@ -20,6 +21,8 @@ import { readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
|
||||
const HEX64 = /^[0-9a-f]{64}$/
|
||||
/** Release downloads on Gitea: <RELEASES>/v<version>/<file> (the release tag is v<CLI version>). */
|
||||
export const RELEASES = 'https://git.kof22.com/QRun-IO/qqq-cli/releases/download'
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const FORMULA_SUMS = /(url "[^"]*\/(qqq-[a-z0-9-]+)"\n\s*sha256 ")([0-9a-f]{64})"/g
|
||||
|
||||
@@ -58,7 +61,7 @@ export function updatePackaging({ version, sums, formula, manifest }) {
|
||||
|
||||
const parsed = JSON.parse(manifest)
|
||||
parsed.version = version
|
||||
parsed.architecture['64bit'].url = `https://github.com/QRun-IO/qqq-cli/releases/download/cli-v${version}/qqq-windows-x64.exe`
|
||||
parsed.architecture['64bit'].url = `${RELEASES}/v${version}/qqq-windows-x64.exe`
|
||||
parsed.architecture['64bit'].hash = sumFor(sums, 'qqq-windows-x64.exe')
|
||||
return { formula: newFormula, manifest: JSON.stringify(parsed, null, 4) + '\n', formulaCount }
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const sumsText = (omit = []) =>
|
||||
[...FILES, 'qqq-linux-x64-musl', 'qqq-windows-x64.exe'].filter((f) => !omit.includes(f)).map((f, i) => `${'abcdef0123456'[i].repeat(64)} ${f}`).join('\n') + '\n'
|
||||
const formula = `class Qqq < Formula
|
||||
version "0.0.0"
|
||||
${FILES.map((f) => ` url "https://github.com/QRun-IO/qqq-cli/releases/download/cli-v#{version}/${f}"\n sha256 "${ZERO}"`).join('\n')}
|
||||
${FILES.map((f) => ` url "https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v#{version}/${f}"\n sha256 "${ZERO}"`).join('\n')}
|
||||
end
|
||||
`
|
||||
const manifest = JSON.stringify({ version: '0.0.0', architecture: { '64bit': { url: 'x', hash: ZERO } } }, null, 4) + '\n'
|
||||
@@ -32,6 +32,7 @@ test('fills every sum from shasum output and never leaves zeros', () => {
|
||||
assert.match(out.formula, /version "1\.0\.0"/)
|
||||
assert.ok(out.formula.includes(`qqq-darwin-arm64"\n sha256 "${h('a')}"`))
|
||||
assert.equal(JSON.parse(out.manifest).architecture['64bit'].hash, h('f'))
|
||||
assert.equal(JSON.parse(out.manifest).architecture['64bit'].url, 'https://git.kof22.com/QRun-IO/qqq-cli/releases/download/v1.0.0/qqq-windows-x64.exe')
|
||||
})
|
||||
|
||||
test('a missing target throws', () => {
|
||||
|
||||
Reference in new issue
Block a user