diff --git a/.changeset/manifest-version-automation.md b/.changeset/manifest-version-automation.md new file mode 100644 index 0000000..a47a563 --- /dev/null +++ b/.changeset/manifest-version-automation.md @@ -0,0 +1,7 @@ +--- +'@wolfcola/devtools-extension': patch +--- + +Automate manifest version stamping: append CI build number as 4th version +segment so every Chrome Web Store and Firefox Add-ons upload has a unique, +always-increasing version. diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml index 2dbb58b..24a4b9c 100644 --- a/.github/workflows/publish-extension.yml +++ b/.github/workflows/publish-extension.yml @@ -21,6 +21,8 @@ jobs: - name: Build all packages run: pnpm build + env: + BUILD_NUMBER: ${{ github.run_number }} - name: Zip extension working-directory: packages/devtools-extension @@ -52,10 +54,14 @@ jobs: - name: Build all packages run: pnpm build + env: + BUILD_NUMBER: ${{ github.run_number }} - name: Build Firefox extension working-directory: packages/devtools-extension - run: node build.mjs --target=firefox + run: node --experimental-strip-types build.mjs --target=firefox + env: + BUILD_NUMBER: ${{ github.run_number }} - name: Zip extension working-directory: packages/devtools-extension diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b16a709..971a8d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,9 @@ jobs: - name: Build run: pnpm build + env: + BUILD_NUMBER: ${{ github.run_number }} + SNAPSHOT: 'true' - name: Publish npm snapshots run: pnpm changeset publish --tag snapshot @@ -64,7 +67,10 @@ jobs: - name: Build Firefox extension if: inputs.extension working-directory: packages/devtools-extension - run: node build.mjs --target=firefox + run: node --experimental-strip-types build.mjs --target=firefox + env: + BUILD_NUMBER: ${{ github.run_number }} + SNAPSHOT: 'true' - name: Zip Firefox extension if: inputs.extension @@ -106,14 +112,17 @@ jobs: - name: Build run: pnpm build + env: + BUILD_NUMBER: ${{ github.run_number }} - name: Create release PR or publish uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0 with: publish: pnpm release version: pnpm run version - title: "chore: version packages" - commit: "chore: version packages" + title: 'chore: version packages' + commit: 'chore: version packages' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: true + BUILD_NUMBER: ${{ github.run_number }} diff --git a/packages/devtools-extension/build.mjs b/packages/devtools-extension/build.mjs index 73c89dc..bc3007f 100644 --- a/packages/devtools-extension/build.mjs +++ b/packages/devtools-extension/build.mjs @@ -1,6 +1,7 @@ import { execFileSync } from 'node:child_process'; import { cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; +import { stampVersion } from './src/stamp-version.ts'; const target = process.argv.includes('--target=firefox') ? 'firefox' : 'chrome'; const cwd = import.meta.dirname; @@ -67,6 +68,16 @@ cpSync(`${uiDir}/dist/panel.html`, 'dist/panel/panel.html'); // Manifest const manifest = JSON.parse(readFileSync('manifest.json', 'utf8')); + +// Stamp version: append build number as 4th segment for Chrome Web Store +const buildNumber = parseInt(process.env.BUILD_NUMBER || '0', 10); +const isSnapshot = process.env.SNAPSHOT === 'true'; +const stamped = stampVersion(manifest.version, buildNumber, isSnapshot); +manifest.version = stamped.version; +if (stamped.version_name) { + manifest.version_name = stamped.version_name; +} + if (target === 'firefox') { manifest.background = { scripts: ['background/service-worker.js'], type: 'module' }; manifest.browser_specific_settings = { diff --git a/packages/devtools-extension/package.json b/packages/devtools-extension/package.json index ce53639..0adeb71 100644 --- a/packages/devtools-extension/package.json +++ b/packages/devtools-extension/package.json @@ -11,8 +11,8 @@ "license": "MIT", "type": "module", "scripts": { - "build": "node build.mjs", - "build:firefox": "node build.mjs --target=firefox", + "build": "node --experimental-strip-types build.mjs", + "build:firefox": "node --experimental-strip-types build.mjs --target=firefox", "lint": "eslint .", "test": "vitest run" }, diff --git a/packages/devtools-extension/src/stamp-version.test.ts b/packages/devtools-extension/src/stamp-version.test.ts new file mode 100644 index 0000000..f95e731 --- /dev/null +++ b/packages/devtools-extension/src/stamp-version.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; +import { stampVersion } from './stamp-version.js'; + +describe('stampVersion', () => { + it('appends build number 0 for local dev', () => { + const result = stampVersion('0.1.0', 0, false); + expect(result).toEqual({ version: '0.1.0.0' }); + }); + + it('appends build number and snapshot version_name', () => { + const result = stampVersion('0.1.0', 23, true); + expect(result).toEqual({ + version: '0.1.0.23', + version_name: '0.1.0-snapshot.23', + }); + }); + + it('appends build number and release version_name', () => { + const result = stampVersion('0.2.0', 24, false); + expect(result).toEqual({ + version: '0.2.0.24', + version_name: '0.2.0', + }); + }); + + it('handles max valid build number (65535)', () => { + const result = stampVersion('1.0.0', 65535, false); + expect(result).toEqual({ + version: '1.0.0.65535', + version_name: '1.0.0', + }); + }); + + it('throws when build number exceeds 65535', () => { + expect(() => stampVersion('1.0.0', 65536, false)).toThrow( + 'BUILD_NUMBER 65536 exceeds Chrome max of 65535', + ); + }); + + it('throws for negative build number', () => { + expect(() => stampVersion('1.0.0', -1, false)).toThrow('BUILD_NUMBER -1 must be >= 0'); + }); + + it('does not set version_name when build number is 0', () => { + const result = stampVersion('0.1.0', 0, false); + expect(result).not.toHaveProperty('version_name'); + }); + + it('does not set version_name when build number is 0 even with snapshot flag', () => { + const result = stampVersion('0.1.0', 0, true); + expect(result).not.toHaveProperty('version_name'); + }); + + it('throws for NaN build number', () => { + expect(() => stampVersion('1.0.0', NaN, false)).toThrow( + 'BUILD_NUMBER must be an integer, got NaN', + ); + }); +}); diff --git a/packages/devtools-extension/src/stamp-version.ts b/packages/devtools-extension/src/stamp-version.ts new file mode 100644 index 0000000..61c76a7 --- /dev/null +++ b/packages/devtools-extension/src/stamp-version.ts @@ -0,0 +1,26 @@ +export const stampVersion = ( + baseVersion: string, + buildNumber: number, + isSnapshot: boolean, +): { version: string; version_name?: string } => { + if (!Number.isInteger(buildNumber)) { + throw new Error(`BUILD_NUMBER must be an integer, got ${buildNumber}`); + } + if (buildNumber < 0) { + throw new Error(`BUILD_NUMBER ${buildNumber} must be >= 0`); + } + if (buildNumber > 65535) { + throw new Error(`BUILD_NUMBER ${buildNumber} exceeds Chrome max of 65535`); + } + + const version = `${baseVersion}.${buildNumber}`; + + if (buildNumber === 0) { + return { version }; + } + + return { + version, + version_name: isSnapshot ? `${baseVersion}-snapshot.${buildNumber}` : baseVersion, + }; +};