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/ dist/
.web-extension-id .web-extension-id
node_modules/ 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 ## Disclaimer
This project is not affiliated with archive.org or aria2. Use responsibly and respect site terms of service. 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", "name": "Archive.org Link Grabber",
"version": "0.1.0", "version": "0.1.0",
"description": "Filter and export archive.org /download links; copy or send to aria2 RPC.", "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": { "browser_action": {
"default_title": "Archive.org Link Grabber", "default_title": "Archive.org Link Grabber",
"default_popup": "src/popup/index.html" "default_popup": "src/popup/index.html"

View file

@ -8,7 +8,15 @@
"build": "web-ext build -o -a dist", "build": "web-ext build -o -a dist",
"lint": "echo 'Add ESLint config to enable' && exit 0", "lint": "echo 'Add ESLint config to enable' && exit 0",
"format": "echo 'Add Prettier 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);
}