diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c6f8d67 --- /dev/null +++ b/.env.example @@ -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 + diff --git a/.gitignore b/.gitignore index 03a8611..12e58ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ dist/ .web-extension-id node_modules/ - +.env +# Ignore all release artifacts except updates.json (tracked) +releases/* +!releases/updates.json diff --git a/README.md b/README.md index 844c145..70ce090 100644 --- a/README.md +++ b/README.md @@ -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//` +- 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. diff --git a/manifest.json b/manifest.json index 56a7c64..65e3ac6 100644 --- a/manifest.json +++ b/manifest.json @@ -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" diff --git a/package.json b/package.json index ba5aef0..31b05a7 100644 --- a/package.json +++ b/package.json @@ -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" } } - diff --git a/releases/updates.json b/releases/updates.json new file mode 100644 index 0000000..d3ec19c --- /dev/null +++ b/releases/updates.json @@ -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" } } + } + ] + } + } +} + diff --git a/scripts/release-sign.js b/scripts/release-sign.js new file mode 100644 index 0000000..c082541 --- /dev/null +++ b/scripts/release-sign.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/* + * Signs an unlisted release via AMO using web-ext and outputs artifacts to releases// + * 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/ + 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); +} + diff --git a/scripts/sync-version.js b/scripts/sync-version.js new file mode 100644 index 0000000..767caf3 --- /dev/null +++ b/scripts/sync-version.js @@ -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); +} +