5 Commits
Author SHA1 Message Date
lzstealth 2fba6fa9af chore: secure dependencies and bump version to 2.1.13
dependency-security / audit (push) Canceled after 0s
build-windows-exe / build (push) Successful in 45s
2026-10-22 17:28:24 +01:00
lzstealth 0cc2e4f71a chore: use version tag as release title 2026-10-18 16:49:09 +01:00
lzstealth 2af31a09a9 fix: build Windows exe from Windows node binary on non-Windows CI
build-windows-exe / build (push) Successful in 48s
2026-10-18 16:47:14 +01:00
lzstealth 54a0be4836 chore: fix release workflow node version
build-windows-exe / build (push) Successful in 47s
2026-08-15 14:56:47 +01:00
lzstealth ea283dd490 Release v2.1.11
build-windows-exe / build (push) Failing after 9s
2026-08-15 14:55:06 +01:00
8 changed files with 583 additions and 1541 deletions

No files matched your search

+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
node-version: '26'
cache: npm
- name: Install dependencies
@@ -79,7 +79,7 @@ jobs:
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: gSheets-Rate-Assistant ${{ github.ref_name }}
name: ${{ github.ref_name }}
body_path: release-notes.md
files: dist/*.exe
@@ -104,7 +104,7 @@ jobs:
raise SystemExit(0)
tag = os.environ["BUILD_VERSION"]
release_name = f"gSheets-Rate-Assistant {tag}"
release_name = tag
asset_path = next((os.path.join("dist", name) for name in os.listdir("dist") if name.endswith(".exe")), None)
with open("release-notes.md", "r", encoding="utf-8") as notes_file:
release_notes = notes_file.read()
+26
View File
@@ -0,0 +1,26 @@
name: dependency-security
on:
push:
pull_request:
schedule:
- cron: '0 9 * * 1'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '26'
cache: npm
- name: Install dependencies
run: npm ci
- name: Audit dependencies
run: npm audit
+2 -1
View File
@@ -10,6 +10,7 @@ const stringifyModule = require('./node_modules/csv-stringify/dist/cjs/sync.cjs'
const stringify = typeof stringifyModule === 'function'
? stringifyModule
: stringifyModule.default || stringifyModule.stringify || stringifyModule;
const {version: packageVersion} = require('./package.json');
const {loadConfig, resolveOutputPath} = require('./lib/config');
const {buildThrottle, applyRateLimitBuffer, getTotalRateLimitPerMinute} = require('./lib/rate');
const {runOnboarding} = require('./lib/onboarding');
@@ -336,7 +337,7 @@ async function scheduleRuns(config) {
throw new Error('No sheets configured to pull in config.json');
}
console.log('Google Sheets Rate Assistant');
console.log(`Google Sheets Rate Assistant v${packageVersion}`);
const firstApiKey = getFirstApiKey(config);
const titleMap = {};
+10 -2
View File
@@ -1,6 +1,14 @@
const fs = require('fs');
const path = require('path');
let isSingleExecutableApp = false;
try {
const sea = require('node:sea');
isSingleExecutableApp = typeof sea.isSea === 'function' && sea.isSea();
} catch {
isSingleExecutableApp = false;
}
const CONFIG_FILE_NAME = 'config.json';
const DEFAULT_CONFIG_TEMPLATE = JSON.stringify({
apiKeys: [
@@ -56,12 +64,12 @@ const DEFAULT_CONFIG_TEMPLATE = JSON.stringify({
}, null, 2) + '\n';
function getDefaultConfigPath() {
const packagedApp = Boolean(process.pkg);
const packagedApp = isSingleExecutableApp;
return path.resolve(packagedApp ? path.dirname(process.execPath) : process.cwd(), CONFIG_FILE_NAME);
}
function getConfigPathCandidates() {
const packagedApp = Boolean(process.pkg);
const packagedApp = isSingleExecutableApp;
const externalDefault = getDefaultConfigPath();
return [
+463 -1526
View File
File diff suppressed because it is too large. Load diff
+2 -7
View File
@@ -1,6 +1,6 @@
{
"name": "google-sheets-rate-assistant",
"version": "2.1.10",
"version": "2.1.13",
"description": "Pull Google Sheets data and export to CSV with configurable schedules and rate limiting.",
"main": "index.js",
"bin": "index.js",
@@ -14,11 +14,6 @@
"p-throttle": "^8.1.0"
},
"devDependencies": {
"pkg": "^5.8.1"
},
"pkg": {
"assets": [
"node_modules/csv-stringify/dist/cjs/sync.cjs"
]
"esbuild": "^0.25.9"
}
}
+4
View File
@@ -63,6 +63,10 @@ If `config.json` is missing, the app creates a starter file next to the executab
If you choose the wizard, it will ask for the auth method, credential count, document count, and sheet names. It will not ask for output directories or file names.
## Build Note
The Windows packaging script in `scripts/build-win.js` now builds a single executable using Node 26's SEA workflow.
## AI Disclosure
A large chunk of the project was coded using AI, this is the first pass at integrating some AI assistance into my projects. However all code written has been verified and checked before submission. AI use has only been included since the v2 rewrite.
+73 -2
View File
@@ -1,6 +1,9 @@
const crypto = require('crypto');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {exec} = require('pkg');
const {execFileSync} = require('child_process');
const esbuild = require('esbuild');
const {version: packageVersion} = require('../package.json');
function normalizeBuildVersion(value) {
@@ -8,15 +11,83 @@ function normalizeBuildVersion(value) {
return version.startsWith('v') ? version : `v${version}`;
}
async function download(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
}
// SEA blobs must be injected into a node binary of the same version, and non-Windows hosts need a Windows base binary.
async function getWindowsNodeBinary(tempDir) {
if (process.platform === 'win32') {
return process.execPath;
}
const baseUrl = `https://nodejs.org/dist/${process.version}`;
const [binary, shasums] = await Promise.all([
download(`${baseUrl}/win-x64/node.exe`),
download(`${baseUrl}/SHASUMS256.txt`),
]);
const expected = shasums.toString('utf8').split('\n')
.map(line => line.trim().split(/\s+/))
.find(([, file]) => file === 'win-x64/node.exe');
const actual = crypto.createHash('sha256').update(binary).digest('hex');
if (!expected || expected[0] !== actual) {
throw new Error('Checksum verification failed for downloaded win-x64/node.exe');
}
const binaryPath = path.join(tempDir, 'node.exe');
fs.writeFileSync(binaryPath, binary);
return binaryPath;
}
async function main() {
const distDir = path.resolve(__dirname, '..', 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, {recursive: true});
}
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsa-sea-'));
const bundlePath = path.join(tempDir, 'index.cjs');
const seaBlobPath = path.join(tempDir, 'sea-prep.blob');
const seaConfigPath = path.join(tempDir, 'sea-config.json');
const buildVersion = normalizeBuildVersion(process.env.BUILD_VERSION);
const outputFile = path.join(distDir, `gSheets-Rate-Assistant-${buildVersion}.exe`);
await exec(['.', '--targets', 'latest-win-x64', '--output', outputFile]);
await esbuild.build({
entryPoints: [path.resolve(__dirname, '..', 'index.js')],
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node26',
outfile: bundlePath,
logLevel: 'info',
});
const executable = await getWindowsNodeBinary(tempDir);
fs.writeFileSync(seaConfigPath, JSON.stringify({
main: bundlePath,
executable,
output: outputFile,
mainFormat: 'commonjs',
disableExperimentalSEAWarning: true,
useCodeCache: false,
useSnapshot: false,
}, null, 2));
execFileSync(process.execPath, ['--build-sea', seaConfigPath], {stdio: 'inherit'});
const header = Buffer.alloc(2);
const fd = fs.openSync(outputFile, 'r');
fs.readSync(fd, header, 0, 2, 0);
fs.closeSync(fd);
if (header.toString('latin1') !== 'MZ') {
throw new Error(`${outputFile} is not a Windows PE executable`);
}
}
main().catch(error => {