feat: local release workflow (signing + version sync); track updates.json; add .env.example

This commit is contained in:
Jordan Wages 2025-08-22 02:30:27 -05:00
commit 1c1f51a8b9
8 changed files with 158 additions and 3 deletions

5
.env.example Normal file
View file

@ -0,0 +1,5 @@
# Firefox AMO API credentials for web-ext signing
# Obtain from https://addons.mozilla.org/en-US/developers/addon/api/key/
AMO_JWT_ISSUER=your-amo-jwt-issuer
AMO_JWT_SECRET=your-amo-jwt-secret

5
.gitignore vendored
View file

@ -1,4 +1,7 @@
dist/
.web-extension-id
node_modules/
.env
# Ignore all release artifacts except updates.json (tracked)
releases/*
!releases/updates.json

View file

@ -130,3 +130,20 @@ Issues and PRs are welcome. If proposing new filters or aria2 options, please in
## Disclaimer
This project is not affiliated with archive.org or aria2. Use responsibly and respect site terms of service.
## Release Workflow
- Stable ID: set a permanent add-on ID in `manifest.json` at `applications.gecko.id` (replace the `@example` value with your ID). If you self-host updates, set `applications.gecko.update_url` to your `updates.json` URL.
- Prepare (version bump + sync):
- Patch: `npm run release:prepare:patch`
- Minor: `npm run release:prepare:minor`
- Major: `npm run release:prepare:major`
- Lint (Firefox): `npm run lint:fx`
- Dev ZIP: `npm run build:dev` → output in `dist/`
- Sign (unlisted):
- Set environment secrets locally (do not commit): `AMO_JWT_ISSUER=... AMO_JWT_SECRET=...`
- Run: `npm run release:sign`
- Artifacts land in `releases/<version>/`
- Self-hosted updates: copy `releases/updates.example.json` to `releases/updates.json` and update with the new version and `update_link` pointing to your hosted `.xpi`.
Notes: Keep AMO secrets local. CI is optional. You can tag releases with `git tag vX.Y.Z` and push tags if desired.

View file

@ -3,6 +3,12 @@
"name": "Archive.org Link Grabber",
"version": "0.1.0",
"description": "Filter and export archive.org /download links; copy or send to aria2 RPC.",
"applications": {
"gecko": {
"id": "archive-org-link-grabber@example",
"update_url": "https://example.com/updates.json"
}
},
"browser_action": {
"default_title": "Archive.org Link Grabber",
"default_popup": "src/popup/index.html"

View file

@ -8,7 +8,15 @@
"build": "web-ext build -o -a dist",
"lint": "echo 'Add ESLint config to enable' && exit 0",
"format": "echo 'Add Prettier config to enable' && exit 0",
"test": "echo 'No tests yet' && exit 0"
"test": "echo 'No tests yet' && exit 0",
"lint:fx": "web-ext lint --source-dir .",
"build:dev": "web-ext build -o -a dist",
"release:prepare:patch": "npm version patch && node scripts/sync-version.js",
"release:prepare:minor": "npm version minor && node scripts/sync-version.js",
"release:prepare:major": "npm version major && node scripts/sync-version.js",
"release:sign": "node scripts/release-sign.js"
},
"devDependencies": {
"web-ext": "^8.3.0"
}
}

14
releases/updates.json Normal file
View file

@ -0,0 +1,14 @@
{
"addons": {
"archive-org-link-grabber@example": {
"updates": [
{
"version": "0.1.0",
"update_link": "https://example.com/releases/0.1.0/archive-org-link-grabber-0.1.0.xpi",
"applications": { "gecko": { "strict_min_version": "91.0" } }
}
]
}
}
}

63
scripts/release-sign.js Normal file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env node
/*
* Signs an unlisted release via AMO using web-ext and outputs artifacts to releases/<version>/
* Requires env: AMO_JWT_ISSUER, AMO_JWT_SECRET
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const root = path.join(__dirname, '..');
const pkg = require(path.join(root, 'package.json'));
const manifest = require(path.join(root, 'manifest.json'));
const issuer = process.env.AMO_JWT_ISSUER;
const secret = process.env.AMO_JWT_SECRET;
if (!issuer || !secret) {
console.error('Missing AMO credentials. Set AMO_JWT_ISSUER and AMO_JWT_SECRET.');
process.exit(1);
}
const addonId = manifest?.applications?.gecko?.id;
if (!addonId || addonId.includes('example')) {
console.error('Invalid add-on id. Set applications.gecko.id in manifest.json to your stable ID.');
process.exit(1);
}
const version = pkg.version;
if (!version) {
console.error('package.json is missing version');
process.exit(1);
}
const outDir = path.join(root, 'releases', version);
fs.mkdirSync(outDir, { recursive: true });
function run(cmd) {
console.log(`> ${cmd}`);
execSync(cmd, { stdio: 'inherit' });
}
try {
// Lint before signing
run('npx --yes web-ext lint --source-dir .');
// Sign (unlisted) and place artifacts in releases/<version>
const signCmd = [
'npx --yes web-ext sign',
'--channel=unlisted',
`--api-key="${issuer}"`,
`--api-secret="${secret}"`,
`--id="${addonId}"`,
`--artifacts-dir "${outDir}"`,
].join(' ');
run(signCmd);
console.log('\nSigned artifacts written to:', outDir);
console.log('Next: host .xpi and update updates.json (if self-hosting updates).');
} catch (err) {
console.error('Release signing failed:', err.message);
process.exit(1);
}

39
scripts/sync-version.js Normal file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env node
// Syncs manifest.json version with package.json version
const fs = require('fs');
const path = require('path');
const pkgPath = path.join(__dirname, '..', 'package.json');
const manifestPath = path.join(__dirname, '..', 'manifest.json');
function readJson(p) {
return JSON.parse(fs.readFileSync(p, 'utf8'));
}
function writeJson(p, obj) {
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
}
try {
const pkg = readJson(pkgPath);
const manifest = readJson(manifestPath);
const nextVersion = pkg.version;
if (!nextVersion) {
console.error('package.json is missing version');
process.exit(1);
}
if (manifest.version === nextVersion) {
console.log(`manifest.json already at version ${nextVersion}`);
process.exit(0);
}
manifest.version = nextVersion;
writeJson(manifestPath, manifest);
console.log(`Updated manifest.json version to ${nextVersion}`);
} catch (err) {
console.error('Failed to sync versions:', err.message);
process.exit(1);
}