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
43 changes: 43 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,49 @@ jobs:
"
echo "Publishing coven-code@${VERSION}"

- name: Generate npm checksum manifest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${{ steps.version.outputs.tag }}"
mkdir -p release-assets
gh release download "$TAG" \
--repo "${{ github.repository }}" \
--pattern 'coven-code-*.tar.gz' \
--pattern 'coven-code-*.zip' \
--dir release-assets
Comment on lines +126 to +137

node <<'NODE'
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const expectedArchives = [
'coven-code-windows-x86_64.zip',
'coven-code-linux-x86_64.tar.gz',
'coven-code-linux-aarch64.tar.gz',
'coven-code-macos-x86_64.tar.gz',
'coven-code-macos-aarch64.tar.gz',
];

const manifest = {};
for (const archive of expectedArchives) {
const archivePath = path.join('release-assets', archive);
if (!fs.existsSync(archivePath)) {
console.error(`::error::Missing release archive ${archive}`);
process.exit(1);
}
const hash = crypto.createHash('sha256');
hash.update(fs.readFileSync(archivePath));
manifest[archive] = { sha256: hash.digest('hex') };
}

fs.writeFileSync('npm/checksums.json', JSON.stringify(manifest, null, 2) + '\n');
NODE

cat npm/checksums.json

- name: Publish to npm
working-directory: npm
env:
Expand Down
4 changes: 2 additions & 2 deletions npm/bin/coven-code
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ const os = require('os');
const fs = require('fs');

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

if (!fs.existsSync(binary)) {
console.error(
'coven-code: native binary not found.\n' +
'Try reinstalling: npm install -g /coven-code\n' +
'Try reinstalling: npm install -g @opencoven/coven-code\n' +
`Expected: ${binary}`
);
process.exit(1);
Expand Down
1 change: 1 addition & 0 deletions npm/checksums.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
55 changes: 50 additions & 5 deletions npm/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
'use strict';

const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');
const os = require('os');
const crypto = require('crypto');
const { execFileSync } = require('child_process');

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

function download(url, dest) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
reject(new Error(`Refusing to download non-HTTPS URL: ${url}`));
return;
}

const file = fs.createWriteStream(dest);
const get = url.startsWith('https') ? https : http;
get.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
https.get(url, (res) => {
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
file.close();
try { fs.unlinkSync(dest); } catch (_) {}
download(res.headers.location, dest).then(resolve).catch(reject);
if (!res.headers.location) {
Comment on lines +52 to +56
reject(new Error(`Redirect without Location header downloading ${url}`));
return;
}
let location;
try {
location = new URL(res.headers.location, url).toString();
} catch (err) {
reject(err);
return;
}
download(location, dest).then(resolve).catch(reject);
return;
}
if (res.statusCode !== 200) {
Expand All @@ -69,6 +86,31 @@ function download(url, dest) {
});
}

function sha256File(filePath) {
const hash = crypto.createHash('sha256');
const bytes = fs.readFileSync(filePath);
hash.update(bytes);
return hash.digest('hex');
}

function expectedSha256(archiveName) {
const entry = checksums[archiveName];
if (!entry || typeof entry.sha256 !== 'string') {
throw new Error(`Missing SHA-256 checksum for ${archiveName} in checksums.json`);
}
return entry.sha256;
}
Comment on lines +96 to +102

function verifyChecksum(filePath, archiveName) {
const expected = expectedSha256(archiveName).toLowerCase();
const actual = sha256File(filePath).toLowerCase();
if (actual !== expected) {
throw new Error(
`Checksum mismatch for ${archiveName}: expected ${expected}, got ${actual}`
);
}
}

async function main() {
const { artifact, ext, archive } = getPlatform();
const archiveName = `${artifact}${archive}`;
Expand All @@ -87,6 +129,9 @@ async function main() {
console.log(` ${url}`);
await download(url, tmpPath);

console.log('coven-code: verifying checksum...');
verifyChecksum(tmpPath, archiveName);

Comment on lines +132 to +134
console.log('coven-code: extracting...');
if (archive === '.zip') {
execFileSync('powershell', [
Expand Down
1 change: 1 addition & 0 deletions npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"files": [
"bin/",
"install.js",
"checksums.json",
"README.md",
"ATTRIBUTION.md"
],
Expand Down