Skip to content

Commit aa639ff

Browse files
committed
chore: merge main into val/perf-command-integration-audit; resolve conflicts
- TUI files: took main's trusted_command_path helpers over HEAD's hardcoded cmds - app.rs: kept HistorySearch::default impl, took main's clipboard delegation - cli/main.rs: took main (already has #27 + #38 fixes); fixed EffortLevel::from_str → ::parse - All other Rust files: took main (formatting/reformatting only) cargo check clean.
2 parents 8bce63e + b98e662 commit aa639ff

25 files changed

Lines changed: 5190 additions & 5537 deletions

.github/workflows/auto-release.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Auto Release (commit marker)
22

33
# Two flows, both driven by markers in the head commit message:
44
#
5-
# --release vMAJOR.MINOR.PATCH
5+
# --release vMAJOR.MINOR.PATCH (restricted to OpenCoven release bot)
66
# Cut a brand-new release. The requested tag must be strictly greater
77
# (semver) than the highest existing tag, and must not already exist.
88
# Stamps the version across Cargo.toml/lock, npm, README, docs, and the
@@ -36,8 +36,11 @@ concurrency:
3636
cancel-in-progress: false
3737

3838
jobs:
39+
# Restricted to the release bot so contributor-controlled PR titles or
40+
# commit messages cannot authorize a release when a maintainer merges them.
3941
auto-release:
4042
runs-on: ubuntu-latest
43+
if: github.actor == 'opencoven-bot'
4144

4245
steps:
4346
# ── 0. Skip our own bump commits ──────────────────────────────────

.github/workflows/npm-publish.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,49 @@ jobs:
165165
"
166166
echo "Publishing coven-code@${VERSION}"
167167
168+
- name: Generate npm checksum manifest
169+
env:
170+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
171+
run: |
172+
set -euo pipefail
173+
TAG="${{ steps.version.outputs.tag }}"
174+
mkdir -p release-assets
175+
gh release download "$TAG" \
176+
--repo "${{ github.repository }}" \
177+
--pattern 'coven-code-*.tar.gz' \
178+
--pattern 'coven-code-*.zip' \
179+
--dir release-assets
180+
181+
node <<'NODE'
182+
const fs = require('fs');
183+
const path = require('path');
184+
const crypto = require('crypto');
185+
186+
const expectedArchives = [
187+
'coven-code-windows-x86_64.zip',
188+
'coven-code-linux-x86_64.tar.gz',
189+
'coven-code-linux-aarch64.tar.gz',
190+
'coven-code-macos-x86_64.tar.gz',
191+
'coven-code-macos-aarch64.tar.gz',
192+
];
193+
194+
const manifest = {};
195+
for (const archive of expectedArchives) {
196+
const archivePath = path.join('release-assets', archive);
197+
if (!fs.existsSync(archivePath)) {
198+
console.error(`::error::Missing release archive ${archive}`);
199+
process.exit(1);
200+
}
201+
const hash = crypto.createHash('sha256');
202+
hash.update(fs.readFileSync(archivePath));
203+
manifest[archive] = { sha256: hash.digest('hex') };
204+
}
205+
206+
fs.writeFileSync('npm/checksums.json', JSON.stringify(manifest, null, 2) + '\n');
207+
NODE
208+
209+
cat npm/checksums.json
210+
168211
- name: Publish to npm
169212
working-directory: npm
170213
env:

.github/workflows/patch-release.yml

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ permissions:
4242

4343
env:
4444
CARGO_TERM_COLOR: always
45+
PATCH_RELEASE_VERSION: ${{ inputs.version }}
4546

4647
jobs:
4748
# ── Preflight ───────────────────────────────────────────────────────
@@ -53,6 +54,14 @@ jobs:
5354
with:
5455
fetch-depth: 0
5556

57+
- name: Validate version input
58+
run: |
59+
if [[ ! "$PATCH_RELEASE_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then
60+
echo "::error::Invalid version: $PATCH_RELEASE_VERSION"
61+
echo "::error::Expected a tag like v1.2.3 or v1.2.3-rc.1."
62+
exit 1
63+
fi
64+
5665
- name: Must be on main branch
5766
run: |
5867
if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then
@@ -64,17 +73,17 @@ jobs:
6473
env:
6574
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6675
run: |
67-
if ! gh release view "${{ inputs.version }}" \
76+
if ! gh release view "$PATCH_RELEASE_VERSION" \
6877
--repo "${{ github.repository }}" >/dev/null 2>&1; then
69-
echo "::error::Release ${{ inputs.version }} does not exist."
78+
echo "::error::Release $PATCH_RELEASE_VERSION does not exist."
7079
echo "::error::To cut a brand-new release, use the regular Release workflow instead."
7180
exit 1
7281
fi
7382
7483
- name: Cargo.toml version must still match the patched tag
7584
run: |
7685
CARGO_VERSION=$(grep '^version' src-rust/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
77-
TAG_VERSION="${{ inputs.version }}"
86+
TAG_VERSION="$PATCH_RELEASE_VERSION"
7887
TAG_VERSION="${TAG_VERSION#v}" # strip leading v
7988
if [[ "$CARGO_VERSION" != "$TAG_VERSION" ]]; then
8089
echo "::error::Cargo.toml ($CARGO_VERSION) does not match patched tag ($TAG_VERSION)."
@@ -263,9 +272,9 @@ jobs:
263272
run: |
264273
git config user.name "github-actions[bot]"
265274
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
266-
git tag -f "${{ inputs.version }}" "${{ github.sha }}"
267-
git push -f origin "refs/tags/${{ inputs.version }}"
268-
echo "Tag ${{ inputs.version }} now points at ${{ github.sha }}."
275+
git tag -f "$PATCH_RELEASE_VERSION" "${{ github.sha }}"
276+
git push -f origin "refs/tags/$PATCH_RELEASE_VERSION"
277+
echo "Tag $PATCH_RELEASE_VERSION now points at ${{ github.sha }}."
269278
270279
# `gh release upload --clobber` overwrites assets one at a time without
271280
# touching the release body, name, draft state, or prerelease flag.
@@ -279,7 +288,7 @@ jobs:
279288
for f in release/*; do
280289
echo "→ Uploading $(basename "$f")"
281290
done
282-
gh release upload "${{ inputs.version }}" release/* \
291+
gh release upload "$PATCH_RELEASE_VERSION" release/* \
283292
--repo "${{ github.repository }}" \
284293
--clobber
285294
@@ -298,13 +307,15 @@ jobs:
298307
run: python3 scripts/append-patch-note.py
299308

300309
- name: Summary
310+
env:
311+
PATCH_RELEASE_NOTE: ${{ inputs.patch_note }}
301312
run: |
302313
{
303-
echo "## ✅ Patched ${{ inputs.version }} in place"
314+
echo "## ✅ Patched $PATCH_RELEASE_VERSION in place"
304315
echo
305316
echo "- Tag force-moved to commit \`${{ github.sha }}\`."
306317
echo "- 5 binary archives + install scripts rebuilt and re-uploaded."
307-
if [[ -n "${{ inputs.patch_note }}" ]]; then
318+
if [[ -n "$PATCH_RELEASE_NOTE" ]]; then
308319
echo "- Release body received a new bullet under \`## 🩹 Patches\` at the top."
309320
else
310321
echo "- Release title, body, contributors, and Full Changelog link unchanged."

.github/workflows/release.yml

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ permissions:
2222

2323
env:
2424
CARGO_TERM_COLOR: always
25+
RELEASE_VERSION: ${{ inputs.version }}
2526

2627
jobs:
2728
# ── Preflight: validate branch, tag, and version match ──────────────
@@ -32,6 +33,14 @@ jobs:
3233
- uses: actions/checkout@v4
3334
with:
3435
fetch-depth: 0
36+
persist-credentials: false
37+
38+
- name: Validate version input
39+
run: |
40+
if [[ ! "$RELEASE_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
41+
echo "::error::Version must be a v-prefixed SemVer tag, for example v0.1.0."
42+
exit 1
43+
fi
3544
3645
- name: Must be on main branch
3746
run: |
@@ -42,15 +51,15 @@ jobs:
4251
4352
- name: Tag must not already exist
4453
run: |
45-
if git ls-remote --exit-code --tags origin "refs/tags/${{ inputs.version }}" >/dev/null 2>&1; then
46-
echo "::error::Tag ${{ inputs.version }} already exists"
54+
if git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_VERSION" >/dev/null 2>&1; then
55+
echo "::error::Tag $RELEASE_VERSION already exists"
4756
exit 1
4857
fi
4958
5059
- name: Version tag must match Cargo.toml
5160
run: |
5261
CARGO_VERSION=$(grep '^version' src-rust/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
53-
TAG_VERSION="${{ inputs.version }}"
62+
TAG_VERSION="$RELEASE_VERSION"
5463
TAG_VERSION="${TAG_VERSION#v}" # strip leading v
5564
if [[ "$CARGO_VERSION" != "$TAG_VERSION" ]]; then
5665
echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml ($CARGO_VERSION). Update Cargo.toml first."
@@ -92,6 +101,8 @@ jobs:
92101

93102
steps:
94103
- uses: actions/checkout@v4
104+
with:
105+
persist-credentials: false
95106

96107
- name: Install Rust toolchain
97108
uses: dtolnay/rust-toolchain@stable
@@ -181,6 +192,7 @@ jobs:
181192
- uses: actions/checkout@v4
182193
with:
183194
fetch-depth: 0
195+
persist-credentials: false
184196

185197
- name: Download all artifacts
186198
uses: actions/download-artifact@v4
@@ -258,7 +270,7 @@ jobs:
258270
run: |
259271
set -euo pipefail
260272
261-
CURRENT_TAG="${{ inputs.version }}"
273+
CURRENT_TAG="$RELEASE_VERSION"
262274
PREVIOUS_TAG="$(git tag --sort=-v:refname | grep -vxF "$CURRENT_TAG" | head -n 1 || true)"
263275
264276
# ── Pass 1: GitHub-generated PR notes ──────────────────────────
@@ -361,9 +373,9 @@ jobs:
361373
- name: Create GitHub Release
362374
uses: softprops/action-gh-release@v2
363375
with:
364-
tag_name: ${{ inputs.version }}
376+
tag_name: ${{ env.RELEASE_VERSION }}
365377
target_commitish: ${{ github.sha }}
366-
name: Coven Code ${{ inputs.version }}
378+
name: Coven Code ${{ env.RELEASE_VERSION }}
367379
draft: false
368380
prerelease: false
369381
body_path: release-notes.md
@@ -387,5 +399,5 @@ jobs:
387399
gh workflow run npm-publish.yml \
388400
--repo "${{ github.repository }}" \
389401
--ref main \
390-
-f version="${{ inputs.version }}"
391-
echo "Dispatched npm-publish.yml for ${{ inputs.version }}."
402+
-f version="$RELEASE_VERSION"
403+
echo "Dispatched npm-publish.yml for $RELEASE_VERSION."

npm/bin/coven-code

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ const os = require('os');
77
const fs = require('fs');
88

99
const ext = os.platform() === 'win32' ? '.exe' : '';
10-
const binary = path.join(__dirname, '..', 'native', `coven-code`);
10+
const binary = path.join(__dirname, '..', 'native', `coven-code${ext}`);
1111

1212
if (!fs.existsSync(binary)) {
1313
console.error(
1414
'coven-code: native binary not found.\n' +
15-
'Try reinstalling: npm install -g /coven-code\n' +
15+
'Try reinstalling: npm install -g @opencoven/coven-code\n' +
1616
`Expected: ${binary}`
1717
);
1818
process.exit(1);

npm/checksums.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

npm/install.js

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
'use strict';
33

44
const https = require('https');
5-
const http = require('http');
65
const fs = require('fs');
76
const path = require('path');
87
const os = require('os');
8+
const crypto = require('crypto');
99
const { execFileSync } = require('child_process');
1010

1111
const pkg = require('./package.json');
12+
const checksums = require('./checksums.json');
1213
const VERSION = pkg.version;
1314
const REPO = 'OpenCoven/coven-code';
1415
const BASE_URL = `https://github.com/${REPO}/releases/download/v${VERSION}`;
@@ -41,13 +42,29 @@ function getPlatform() {
4142

4243
function download(url, dest) {
4344
return new Promise((resolve, reject) => {
45+
const parsed = new URL(url);
46+
if (parsed.protocol !== 'https:') {
47+
reject(new Error(`Refusing to download non-HTTPS URL: ${url}`));
48+
return;
49+
}
50+
4451
const file = fs.createWriteStream(dest);
45-
const get = url.startsWith('https') ? https : http;
46-
get.get(url, (res) => {
47-
if (res.statusCode === 301 || res.statusCode === 302) {
52+
https.get(url, (res) => {
53+
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
4854
file.close();
4955
try { fs.unlinkSync(dest); } catch (_) {}
50-
download(res.headers.location, dest).then(resolve).catch(reject);
56+
if (!res.headers.location) {
57+
reject(new Error(`Redirect without Location header downloading ${url}`));
58+
return;
59+
}
60+
let location;
61+
try {
62+
location = new URL(res.headers.location, url).toString();
63+
} catch (err) {
64+
reject(err);
65+
return;
66+
}
67+
download(location, dest).then(resolve).catch(reject);
5168
return;
5269
}
5370
if (res.statusCode !== 200) {
@@ -69,6 +86,31 @@ function download(url, dest) {
6986
});
7087
}
7188

89+
function sha256File(filePath) {
90+
const hash = crypto.createHash('sha256');
91+
const bytes = fs.readFileSync(filePath);
92+
hash.update(bytes);
93+
return hash.digest('hex');
94+
}
95+
96+
function expectedSha256(archiveName) {
97+
const entry = checksums[archiveName];
98+
if (!entry || typeof entry.sha256 !== 'string') {
99+
throw new Error(`Missing SHA-256 checksum for ${archiveName} in checksums.json`);
100+
}
101+
return entry.sha256;
102+
}
103+
104+
function verifyChecksum(filePath, archiveName) {
105+
const expected = expectedSha256(archiveName).toLowerCase();
106+
const actual = sha256File(filePath).toLowerCase();
107+
if (actual !== expected) {
108+
throw new Error(
109+
`Checksum mismatch for ${archiveName}: expected ${expected}, got ${actual}`
110+
);
111+
}
112+
}
113+
72114
async function main() {
73115
const { artifact, ext, archive } = getPlatform();
74116
const archiveName = `${artifact}${archive}`;
@@ -87,6 +129,9 @@ async function main() {
87129
console.log(` ${url}`);
88130
await download(url, tmpPath);
89131

132+
console.log('coven-code: verifying checksum...');
133+
verifyChecksum(tmpPath, archiveName);
134+
90135
console.log('coven-code: extracting...');
91136
if (archive === '.zip') {
92137
execFileSync('powershell', [

npm/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"files": [
3232
"bin/",
3333
"install.js",
34+
"checksums.json",
3435
"README.md",
3536
"ATTRIBUTION.md"
3637
],

0 commit comments

Comments
 (0)