Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,52 @@ jobs:

- name: Build web
run: bun run --cwd web build

desktop-cli-artifacts:
name: desktop-cli-${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
runner: macos-15
- target: x86_64-apple-darwin
runner: macos-15-intel
- target: x86_64-pc-windows-msvc
runner: windows-latest
- target: x86_64-unknown-linux-gnu
runner: ubuntu-22.04

steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version

- name: Install production dependency closure
run: bun install --frozen-lockfile --production

- name: Build and test desktop distribution
run: |
bun run build
bun run desktop:test

- name: Package native desktop CLI
run: bun run desktop:package -- --target "${{ matrix.target }}" --output-dir .artifacts/desktop-cli --node-executable node

- name: Upload native archive
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: desktop-cli-${{ matrix.target }}
path: .artifacts/desktop-cli/verboo-cli-*
if-no-files-found: error
retention-days: 7
146 changes: 146 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ jobs:

- name: Run release-critical tests
run: |
bun run desktop:test
bun test --max-concurrency=1 \
src/commands/effort/effort.verboo.test.ts \
src/commands/logout/logoutState.test.ts \
Expand Down Expand Up @@ -197,3 +198,148 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

desktop-cli-artifacts:
name: Build desktop CLI (${{ matrix.target }})
needs: verify
if: ${{ github.repository == 'verbeux-ai/code' }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
runner: macos-15
- target: x86_64-apple-darwin
runner: macos-15-intel
- target: x86_64-pc-windows-msvc
runner: windows-latest
- target: x86_64-unknown-linux-gnu
runner: ubuntu-22.04
permissions:
contents: read

steps:
- name: Check out release tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.verify.outputs.tag }}
fetch-depth: 0

- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version

- name: Install production dependency closure
run: bun install --frozen-lockfile --production

- name: Build and test desktop distribution
run: |
bun run build
bun run desktop:test

- name: Package native desktop CLI
run: bun run desktop:package -- --target "${{ matrix.target }}" --output-dir .artifacts/desktop-cli --node-executable node

- name: Upload native archive
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: desktop-cli-${{ matrix.target }}
path: .artifacts/desktop-cli/verboo-cli-*
if-no-files-found: error
retention-days: 7

publish-desktop-cli:
name: Sign and publish desktop CLI assets
needs:
- verify
- desktop-cli-artifacts
if: ${{ github.repository == 'verbeux-ai/code' }}
runs-on: ubuntu-22.04
environment:
name: release
url: https://github.com/verbeux-ai/code/releases/tag/${{ needs.verify.outputs.tag }}
permissions:
contents: write
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERBOO_DESKTOP_MINISIGN_SECRET_KEY_B64: ${{ secrets.VERBOO_DESKTOP_MINISIGN_SECRET_KEY_B64 }}
VERBOO_DESKTOP_MINISIGN_PUBLIC_KEY: ${{ secrets.VERBOO_DESKTOP_MINISIGN_PUBLIC_KEY }}

steps:
- name: Check out release tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.verify.outputs.tag }}
fetch-depth: 0

- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24

- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version

- name: Download native archives
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: desktop-cli-*
path: .artifacts/desktop-cli
merge-multiple: true

- name: Install Minisign
run: |
sudo apt-get update
sudo apt-get install -y minisign

- name: Require protected signing configuration
run: |
if [ -z "$VERBOO_DESKTOP_MINISIGN_SECRET_KEY_B64" ] || [ -z "$VERBOO_DESKTOP_MINISIGN_PUBLIC_KEY" ]; then
echo "Protected desktop CLI signing configuration is missing" >&2
exit 1
fi
printf '%s' "$VERBOO_DESKTOP_MINISIGN_SECRET_KEY_B64" | base64 --decode > "$RUNNER_TEMP/verboo-desktop-cli.key"
printf '%s\n' "$VERBOO_DESKTOP_MINISIGN_PUBLIC_KEY" > "$RUNNER_TEMP/verboo-desktop-cli.pub"

- name: Generate, sign, and verify manifest
env:
RELEASE_TAG: ${{ needs.verify.outputs.tag }}
run: |
SOURCE_DATE=$(git show -s --format=%cI "$RELEASE_TAG")
RELEASED_AT=$(node -e 'console.log(new Date(process.argv[1]).toISOString())' "$SOURCE_DATE")
SIGNING_KEY_ID=$(node -e 'const crypto = require("node:crypto"); console.log(crypto.createHash("sha256").update(process.env.VERBOO_DESKTOP_MINISIGN_PUBLIC_KEY.trim(), "utf8").digest("hex").slice(0, 16))')
bun run desktop:manifest -- \
--assets-dir .artifacts/desktop-cli \
--repository verbeux-ai/code \
--tag "$RELEASE_TAG" \
--released-at "$RELEASED_AT" \
--signing-key-id "$SIGNING_KEY_ID"
minisign -Sm .artifacts/desktop-cli/verboo-cli-manifest.json \
-s "$RUNNER_TEMP/verboo-desktop-cli.key" \
-x .artifacts/desktop-cli/verboo-cli-manifest.minisig \
-t "Verboo desktop CLI $RELEASE_TAG"
bun run desktop:verify -- \
--assets-dir .artifacts/desktop-cli \
--public-key "$RUNNER_TEMP/verboo-desktop-cli.pub"

- name: Upload signed desktop CLI release assets
env:
RELEASE_TAG: ${{ needs.verify.outputs.tag }}
run: |
gh release upload "$RELEASE_TAG" \
.artifacts/desktop-cli/verboo-cli-*.tar.gz \
.artifacts/desktop-cli/verboo-cli-manifest.json \
.artifacts/desktop-cli/verboo-cli-manifest.minisig

- name: Remove signing key from runner
if: always()
run: rm -f "$RUNNER_TEMP/verboo-desktop-cli.key" "$RUNNER_TEMP/verboo-desktop-cli.pub"
73 changes: 73 additions & 0 deletions docs/desktop-cli-distribution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Signed Verboo Desktop CLI distribution

Verboo Desktop updates the CLI independently from the desktop application. A published CLI release therefore carries four target-specific archives plus a manifest and its detached Minisign signature.

## Release assets

Every eligible `vMAJOR.MINOR.PATCH` release contains exactly:

- `verboo-cli-<version>-aarch64-apple-darwin.tar.gz`
- `verboo-cli-<version>-x86_64-apple-darwin.tar.gz`
- `verboo-cli-<version>-x86_64-pc-windows-msvc.tar.gz`
- `verboo-cli-<version>-x86_64-unknown-linux-gnu.tar.gz`
- `verboo-cli-manifest.json`
- `verboo-cli-manifest.minisig`

The native jobs materialize `dist/cli.mjs` and the production dependency closure on matching runners. Archives contain no Node.js executable. Verboo Desktop supplies its own pinned Node runtime.

## One-time protected environment setup

The repository owner performs these steps. The private key must never be committed, stored in repository variables, uploaded as an Actions artifact, or exposed to pull-request workflows.

1. On a trusted offline machine, install Minisign and create an unencrypted automation key:

```bash
minisign -G -W -p verboo-desktop-cli.pub -s verboo-desktop-cli.key
```

2. Preserve `verboo-desktop-cli.key` in the project's encrypted key backup.

3. Base64-encode the entire secret-key file as one line:

```bash
base64 < verboo-desktop-cli.key | tr -d '\n'
```

4. In the protected GitHub Actions environment named `release`, create:

- `VERBOO_DESKTOP_MINISIGN_SECRET_KEY_B64`: the one-line base64 value from step 3.
- `VERBOO_DESKTOP_MINISIGN_PUBLIC_KEY`: the complete two-line contents of `verboo-desktop-cli.pub`.

5. Require maintainer approval for the `release` environment and restrict it to protected release tags.

The release job fails before signing when either secret is absent. Fork pull requests never receive these values and cannot publish GitHub release assets.

## Publishing

1. Ensure `package.json.version` is the intended version and the Git tag is exactly `v<package.json.version>`.
2. Publish the GitHub release through the normal project release flow.
3. Wait for `Deploy Release / Sign and publish desktop CLI assets` to finish.
4. Confirm that all six assets above are attached to the same immutable tag.
5. Confirm the job log includes `Verified Verboo CLI <version>` before the upload step.

The aggregator recalculates every archive's byte size and SHA-256 instead of trusting matrix-job metadata. It signs the exact bytes of `verboo-cli-manifest.json`, verifies that signature again, inspects archive paths and entry types, and only then uploads the set.

## Pull-request validation

Pull requests run the packaging scripts on the same four native runner families. These jobs exercise the platform-specific dependency closure and `--version` smoke but only upload short-lived Actions artifacts. They do not create or mutate a GitHub release.

The focused local gate is:

```bash
bun run desktop:test
bun run build
node dist/cli.mjs --version
```

The open-source mirror currently has unrelated repository-wide TypeScript errors, so this distribution work relies on the release build, native matrix, and focused tests rather than claiming a clean global `tsc --noEmit` baseline.

## Key rotation and compromise

For planned rotation, first ship a desktop application release that trusts both the current and replacement public keys. Only after that desktop release is available should the release environment switch to the replacement secret/public pair. A later desktop release may remove the retired key.

If the private key may be compromised, stop publishing desktop CLI assets immediately. Ship a desktop trust-root update that rejects the compromised key, then configure a replacement key and resume CLI publication. Do not publish a replacement signature under the old tag or reuse an existing asset name for different bytes.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
],
"scripts": {
"build": "bun run scripts/build.ts",
"desktop:contract:test": "bun test scripts/desktop-release/contract.test.ts",
"desktop:package": "bun run scripts/desktop-release/package.ts",
"desktop:manifest": "bun run scripts/desktop-release/manifest.ts",
"desktop:verify": "bun run scripts/desktop-release/verify-release.ts",
"desktop:test": "bun test scripts/desktop-release/*.test.ts",
"postinstall": "node scripts/postinstall.mjs || true",
"integrations:generate": "bun run scripts/generate-integrations-artifacts.ts",
"integrations:check": "bun run scripts/generate-integrations-artifacts.ts --check",
Expand Down
71 changes: 71 additions & 0 deletions scripts/desktop-release/contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, test } from 'bun:test'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'

import {
DESKTOP_TARGETS,
artifactName,
manifestBytes,
parseDesktopTarget,
releaseAssetUrl,
} from './contract.js'

describe('desktop CLI release contract', () => {
test('covers every Verboo Desktop target exactly once', () => {
expect(DESKTOP_TARGETS.map(item => item.target)).toEqual([
'aarch64-apple-darwin',
'x86_64-apple-darwin',
'x86_64-pc-windows-msvc',
'x86_64-unknown-linux-gnu',
])
expect(new Set(DESKTOP_TARGETS.map(item => item.target)).size).toBe(4)
})

test('uses target-qualified immutable artifact names', () => {
expect(artifactName('0.15.5', 'aarch64-apple-darwin')).toBe(
'verboo-cli-0.15.5-aarch64-apple-darwin.tar.gz',
)
expect(
releaseAssetUrl(
'verbeux-ai/code',
'v0.15.5',
'verboo-cli-0.15.5-aarch64-apple-darwin.tar.gz',
),
).toBe(
'https://github.com/verbeux-ai/code/releases/download/v0.15.5/verboo-cli-0.15.5-aarch64-apple-darwin.tar.gz',
)
})

test('rejects mutable repositories, malformed versions, and unknown targets', () => {
expect(() => artifactName('latest', 'aarch64-apple-darwin')).toThrow(
'Invalid CLI version',
)
expect(() => releaseAssetUrl('graseeel/code', 'v0.15.5', 'asset')).toThrow(
'Unexpected release repository',
)
expect(() => parseDesktopTarget('arm64')).toThrow('Unsupported desktop target')
})

test('serializes the exact signed bytes with stable indentation and one final newline', () => {
const raw = manifestBytes({ schemaVersion: 1 } as never)
expect(new TextDecoder().decode(raw)).toBe('{\n "schemaVersion": 1\n}\n')
})

test('keeps release publication out of pull-request jobs and behind upstream signing', async () => {
const repositoryRoot = resolve(import.meta.dir, '..', '..')
const [releaseWorkflow, pullRequestWorkflow] = await Promise.all([
readFile(resolve(repositoryRoot, '.github/workflows/release.yml'), 'utf8'),
readFile(resolve(repositoryRoot, '.github/workflows/pr-checks.yml'), 'utf8'),
])
expect(releaseWorkflow).toContain("github.repository == 'verbeux-ai/code'")
expect(releaseWorkflow).toContain('VERBOO_DESKTOP_MINISIGN_SECRET_KEY_B64')
expect(releaseWorkflow).toContain('VERBOO_DESKTOP_MINISIGN_PUBLIC_KEY')
expect(releaseWorkflow).toContain('gh release upload')
expect(releaseWorkflow).not.toContain('--clobber')
expect(pullRequestWorkflow).not.toContain('gh release upload')
for (const { target } of DESKTOP_TARGETS) {
expect(pullRequestWorkflow).toContain(`target: ${target}`)
expect(releaseWorkflow).toContain(`target: ${target}`)
}
})
})
Loading
Loading