CI / extension-version (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
Build images / build-ml (push) Successful in 4s
Build images / build-agent (push) Successful in 5s
Build images / build-web (push) Successful in 4s
CI / frontend-build (push) Successful in 18s
extension / lint (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m52s
Step 2 took the build consequence away from a `v*` tag — `main` has already built and published the commit by the time anyone tags it, and rebuilding would re-push `:c-<sha>`, which rule 145 forbids even when the source matches. That left the tag with nothing to do at all. This is the job it has instead. Step 6 put the derived version in the Settings footer, so an operator can say WHICH build they are running; this says what is in it that was not in the one they ran last month. Both halves of one question (note #3127 §5). The previous release is found by walking ANCESTRY, not by sorting a list. That is load-bearing here specifically: rule 148 moved the tag shape from `v26.05.22.0` to `v2026.08.28.2208`, and lexicographically `v2026...` sorts BEFORE `v26...` — the third character is `0` against `6`. A sorted implementation would reach back past every new-shape tag to the newest old-shape one and publish months of commits as "changes since", looking entirely correct while doing it. `git describe --exclude` is immune to the shape change, and reachability is the more honest question anyway. The publisher GETs and PATCHes rather than POSTing and recovering the id from a 409 — note #3127 §6.7, which is ThoughtSync #2182's bug. A `v*` tag is created once so the conflict path is rare, but "rare" is how that one survived to be found somewhere else. Cross-checks are reported on the release, not enforced. The tag is already pushed by the time this runs, so failing would leave the operator with a tag, no release, and a red lane to explain it — while the release is still the useful object. It says so at the top when the tag names a version the web image does not report, or when the commit is not on `main` and the `:c-` rollback refs it lists were never published. Nothing runs on a schedule and nothing auto-tags on merge. Release tags are bookmarks (note #3127 §0); FC went twelve weeks without one and nothing was wrong. Also here: - `scripts/` joins the ruff lane. release_notes.py runs only on a tag push, so a syntax error there would otherwise surface at the one moment nobody wants to be debugging a workflow. - version.spec.js reads the workflow directory instead of listing three files by hand. Its own comment says the assertion should survive consumers coming and going; the hardcoded list was the part that could not, and release.yml would have joined the directory without joining the check. Tests build a synthetic history spanning the tag-shape change rather than leaning on this repo's tags, so the span assertion holds whether or not a checkout brought the tags along — a span test that quietly skips is worse than one that fails.
187 lines
9.1 KiB
JavaScript
187 lines
9.1 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import { readdirSync, readFileSync } from 'node:fs'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { fileURLToPath } from 'node:url'
|
|
import path from 'node:path'
|
|
|
|
const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
const read = (name) => JSON.parse(readFileSync(path.join(EXT_DIR, name), 'utf8'))
|
|
const readText = (...seg) => readFileSync(path.join(EXT_DIR, ...seg), 'utf8')
|
|
|
|
// Only the git-free subcommands are exercised here: `version`/`patch` shell out
|
|
// to git, and the extension lane runs on node:24-bookworm-slim which may not
|
|
// ship it. Those two are covered where git is guaranteed — ci.yml and build.yml
|
|
// run on ci-python.
|
|
const packaging = (cmd) =>
|
|
execFileSync('sh', [path.join(EXT_DIR, 'scripts', 'packaging.sh'), cmd], {
|
|
cwd: EXT_DIR,
|
|
encoding: 'utf8'
|
|
})
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
|
|
describe('packaging.sh — the single definition of what ships', () => {
|
|
it('emits an ignore list and a pathspec that agree on the tracked files', () => {
|
|
const ignore = packaging('ignore')
|
|
const pathspec = packaging('pathspec').map((e) => e.replace(':(exclude)extension/', ''))
|
|
|
|
// Every git-excluded path must also be hidden from web-ext. The reverse is
|
|
// not required: node_modules and web-ext-artifacts are build output git
|
|
// never tracks, so they appear only in the ignore list.
|
|
for (const entry of pathspec) {
|
|
expect(ignore, `pathspec has "${entry}" but --ignore-files does not`).toContain(entry)
|
|
}
|
|
expect(pathspec.length).toBeGreaterThan(0)
|
|
expect(ignore).toContain('node_modules')
|
|
})
|
|
|
|
it('emits glob patterns literally, never expanded against the working tree', () => {
|
|
// The script iterates its lists with deliberate word-splitting, so it must
|
|
// run with pathname expansion off. Without that, invoking it from a cwd
|
|
// where test/ exists (exactly how ci.yml and vitest call it) expands
|
|
// `test/**` into the individual spec files, and the pathspec silently stops
|
|
// covering anything added later.
|
|
const pathspec = packaging('pathspec')
|
|
expect(pathspec).toContain(':(exclude)extension/test/**')
|
|
expect(pathspec.some((e) => e.includes('.spec.js'))).toBe(false)
|
|
expect(pathspec.some((e) => e.includes('helpers'))).toBe(false)
|
|
|
|
const ignore = packaging('ignore')
|
|
expect(ignore).toContain('test/**')
|
|
expect(ignore).toContain('scripts/**')
|
|
expect(ignore.some((e) => e.includes('.spec.js'))).toBe(false)
|
|
})
|
|
|
|
it('lets packaging.sh move the version, though it never ships in the XPI', () => {
|
|
// The two lists answer different questions and this is the one place they
|
|
// disagree. scripts/ is ignored by web-ext — it is repo tooling, not addon
|
|
// code — but packaging.sh DECIDES the version string, and build.yml stamps
|
|
// that string into the manifest.json that does ship. So changing how the
|
|
// version is computed changes the shipped bytes.
|
|
//
|
|
// Excluding it from the pathspec was invisible while every push rebuilt the
|
|
// web image. Milestone 313 step 4 made that rebuild conditional on the
|
|
// derived revision moving, and the omission turned into a silent failure:
|
|
// a new version means sign-extension misses its ext-<version> cache and
|
|
// signs, while build-web sees an unmoved revision, reuses the published
|
|
// image and ships the OLD XPI. An orphaned signature, and an instance
|
|
// serving code the registry calls current.
|
|
const pathspec = packaging('pathspec')
|
|
expect(
|
|
pathspec.some((e) => e.startsWith(':(exclude)extension/scripts')),
|
|
'the pathspec excludes scripts/, so a change to how the version is '
|
|
+ 'derived would not move the version it derives',
|
|
).toBe(false)
|
|
|
|
// ...and it is still kept out of the package itself. Both must hold: the
|
|
// tempting "fix" for either half is to make the two lists one again.
|
|
expect(packaging('ignore')).toContain('scripts')
|
|
})
|
|
|
|
it('keeps its own scripts and specs out of the XPI', () => {
|
|
// Both are repo infrastructure. web-ext packages everything not ignored, so
|
|
// omitting either would ship dev tooling to users -- and `test/**` in
|
|
// particular only survives because callers `set -f` before substituting it.
|
|
const ignore = packaging('ignore')
|
|
expect(ignore).toContain('vitest.config.js')
|
|
// Both forms per directory. The glob covers the contents; the bare name
|
|
// covers the directory ENTRY, which web-ext writes separately — with only
|
|
// the glob, the XPI carries an empty `test/` and `scripts/`.
|
|
for (const dir of ['test', 'scripts']) {
|
|
expect(ignore, `${dir} contents`).toContain(`${dir}/**`)
|
|
expect(ignore, `${dir} directory entry`).toContain(dir)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('consumers delegate rather than keeping their own copy', () => {
|
|
// These assertions are the actual anti-regression value: it is easy for a
|
|
// future edit to "simplify" by inlining a literal list again, which silently
|
|
// reintroduces the drift that issue #2397 was about.
|
|
it('package.json derives --ignore-files from the script', () => {
|
|
for (const [name, script] of Object.entries(read('package.json').scripts)) {
|
|
if (!script.includes('--ignore-files')) continue
|
|
expect(script, `${name} should call packaging.sh`).toContain('scripts/packaging.sh ignore')
|
|
expect(script, `${name} must set -f before the substitution`).toMatch(/set -f;/)
|
|
}
|
|
})
|
|
|
|
// Read from disk rather than listed by hand. The point of this assertion is
|
|
// that it survives consumers coming and going, and a hardcoded list is the
|
|
// one part of it that cannot — release.yml (milestone 318 step 7) would have
|
|
// joined the directory without joining the check.
|
|
const WORKFLOWS = readdirSync(path.join(EXT_DIR, '..', '.forgejo', 'workflows')).filter((f) =>
|
|
f.endsWith('.yml')
|
|
)
|
|
|
|
it('no workflow hardcodes the packaged-file set', () => {
|
|
// ci.yml used to substitute `packaging.sh pathspec` directly, for the
|
|
// manual-bump guard that milestone 271 step 5 retired. Nothing inlines the
|
|
// set today, and nothing should start to: a literal :(exclude)extension/...
|
|
// in a workflow means someone bypassed the shared definition, which is
|
|
// exactly the drift #2397 was about.
|
|
expect(
|
|
WORKFLOWS.length,
|
|
'no workflows found — the glob is not looking where it thinks'
|
|
).toBeGreaterThan(2)
|
|
for (const wf of WORKFLOWS) {
|
|
const text = readText('..', '.forgejo', 'workflows', wf)
|
|
expect(text, `${wf} inlines an :(exclude) literal`).not.toMatch(/:\(exclude\)extension\//)
|
|
}
|
|
})
|
|
|
|
it('build.yml takes the shipped version from the script, not from the repo', () => {
|
|
// The version is DERIVED from commit time (#3092, milestone 271 step 4).
|
|
// Going back to reading the committed value is not a style regression, it
|
|
// is the bug: a hand-set version makes dev and main sign the same number
|
|
// for different code, and the ext-<version> cache then serves one channel
|
|
// the other's XPI.
|
|
const build = readText('..', '.forgejo', 'workflows', 'build.yml')
|
|
expect(build).toContain('packaging.sh version')
|
|
expect(build, 'build.yml re-reads the committed version instead of deriving it')
|
|
.not.toMatch(/grep[^\n]*'"version"'[^\n]*package\.json/)
|
|
})
|
|
})
|
|
|
|
describe('extension version', () => {
|
|
const majorMinor = (v) => v.split('.').slice(0, 2).join('.')
|
|
|
|
it('keeps the hand-set MAJOR.MINOR in lockstep across both files', () => {
|
|
// Narrowed from full-string equality at milestone 271 step 5. Since step 4
|
|
// the patch component is derived from commit time and stamped into both
|
|
// files at build time, so the committed patch numbers are inert — nothing
|
|
// reads them and they are not what ships. Asserting on them would fail for
|
|
// a difference that changes nothing.
|
|
//
|
|
// MAJOR.MINOR is the opposite: still hand-set, still shipped, and
|
|
// packaging.sh reads it from manifest.json ALONE. Let the two diverge and
|
|
// the extension ships a version package.json disagrees with, with no other
|
|
// signal.
|
|
expect(majorMinor(read('manifest.json').version))
|
|
.toBe(majorMinor(read('package.json').version))
|
|
})
|
|
|
|
it('uses a plain dotted numeric version AMO will accept', () => {
|
|
// The committed value seeds MAJOR.MINOR, so it still has to parse even
|
|
// though its patch component never ships. ci.yml asserts the same shape on
|
|
// the DERIVED value, which is the one AMO actually sees.
|
|
expect(read('package.json').version).toMatch(/^\d+(\.\d+)*$/)
|
|
})
|
|
|
|
it('declares manifest v3', () => {
|
|
expect(read('manifest.json').manifest_version).toBe(3)
|
|
})
|
|
|
|
it('lists every background script that exists, in dependency order', () => {
|
|
// url.js must load BEFORE api.js: api.js calls normalizeApiUrl at
|
|
// init()-time, and these are classic scripts sharing one scope, so a
|
|
// reordering here is a runtime ReferenceError with no build-time signal.
|
|
const scripts = read('manifest.json').background.scripts
|
|
for (const rel of scripts) {
|
|
expect(() => readFileSync(path.join(EXT_DIR, rel)), `missing ${rel}`).not.toThrow()
|
|
}
|
|
expect(scripts.indexOf('lib/url.js')).toBeLessThan(scripts.indexOf('lib/api.js'))
|
|
})
|
|
})
|