Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8452a1d46 |
@@ -3,6 +3,9 @@ on: [push]
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
# Bounds script/cibuild, a cold-cache build included, so a hang frees
|
||||
# the shared runner. README.md "In CI" has the measured times.
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
# actions/checkout v4.2.2, 2026-02-22
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
@@ -35,6 +35,10 @@ on: [push]
|
||||
jobs:
|
||||
e2e-chrome:
|
||||
runs-on: ubuntu-latest
|
||||
# Bounds the image build, a cold cache included, and both Chrome
|
||||
# programs, so a hung browser frees the shared runner. README.md
|
||||
# "In CI" has the measured times.
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
# actions/checkout v4.2.2, 2026-02-22
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -42,6 +46,10 @@ jobs:
|
||||
|
||||
e2e-firefox:
|
||||
runs-on: ubuntu-latest
|
||||
# Bounds the image build, a cold cache included, and both Firefox
|
||||
# programs, so a hung browser frees the shared runner. README.md
|
||||
# "In CI" has the measured times.
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
# actions/checkout v4.2.2, 2026-02-22
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
@@ -107,14 +107,6 @@ vendor-blocklist:
|
||||
clean:
|
||||
@rm -rf dist/ release/
|
||||
|
||||
# Run the build make build runs, without the checks that follow it, then run it
|
||||
# again after every change to a file under src/, manifest/ or icons/, until
|
||||
# interrupted. A failed build is reported, may leave dist/ partly written, and
|
||||
# watching carries on. It writes no build receipt, so nothing can verify what it
|
||||
# leaves in dist/: anything handed on comes from make build. A release build
|
||||
# unless AUTISTMASK_DEBUG=1 is exported. A change anywhere else, package.json
|
||||
# and build.js included, starts no build, and a directory created while it runs
|
||||
# is not watched; restart it after either.
|
||||
dev:
|
||||
@echo "Building in watch mode..."
|
||||
@yarn run build --watch 2>&1
|
||||
|
||||
@@ -306,15 +306,7 @@ The Makefile shims to those. It also carries a few targets that have no
|
||||
debug build, and keeping its `dist/` on failure (see
|
||||
[Debug Builds](#debug-builds))
|
||||
- `make clean` — remove `dist/` and `release/`
|
||||
- `make dev` — run the build `make build` runs, without the checks that follow
|
||||
it, then run it again after every change to a file under `src/`, `manifest/`
|
||||
or `icons/`, until interrupted. A failed build is reported, may leave `dist/`
|
||||
partly written, and watching carries on. It writes no build receipt, so
|
||||
nothing can verify what it leaves in `dist/` (see
|
||||
[Build Receipts](#build-receipts)): anything handed on comes from
|
||||
`make build`. A release build unless `AUTISTMASK_DEBUG=1` is exported. A
|
||||
change anywhere else, `package.json` and `build.js` included, starts no build,
|
||||
and a directory created while it runs is not watched; restart it after either
|
||||
- `make dev` — build in watch mode
|
||||
|
||||
## End-to-End Tests
|
||||
|
||||
@@ -651,12 +643,20 @@ Nothing in either job can pass vacuously. There is no `continue-on-error` and no
|
||||
build fails, and when the browser fails to start; the Chrome harness aborts the
|
||||
suite outright if its network interception is not in effect.
|
||||
|
||||
Measured on this repo's runner: `e2e-chrome` about 1m55s cold, almost all of it
|
||||
the one-time pull of the pinned ~800MB Playwright layer, and well under a minute
|
||||
once that layer is cached. `e2e-firefox` about 1m05s cold, and it caches its
|
||||
Firefox and geckodriver downloads the same way. The `e2e-chrome` figures predate
|
||||
the two cases that wait for a receipt to end in error, which add about two
|
||||
minutes of real waiting.
|
||||
Measured on this repo's runner in the green runs of early October 2026, from a
|
||||
warm docker cache to a cold one: `check` 49s to 3m37s, `e2e-chrome` 1m44s to
|
||||
4m48s, and `e2e-firefox` 31s to 4m07s. A cold cache adds three to four minutes
|
||||
to each job, spent rebuilding its image: reinstalling dependencies and, for
|
||||
`e2e-firefox`, installing Firefox, geckodriver and their system libraries. Those
|
||||
`e2e-chrome` runs predate the cases that wait in real time for a receipt to end
|
||||
in error. `make test-e2e` now takes 3m51s locally with its image cached, so a
|
||||
cold `e2e-chrome` run comes to about seven minutes.
|
||||
|
||||
Every job has a `timeout-minutes` cap, so a hung build or browser ends the job
|
||||
instead of holding the shared runner: `check` 10 minutes, `e2e-firefox` 15 and
|
||||
`e2e-chrome` 20, each over two and a half times the job's slowest cold run. A
|
||||
job that reaches its cap has hung; read it as a hang, not as a slow run to
|
||||
retry.
|
||||
|
||||
### Element id guard (part of `make check`)
|
||||
|
||||
|
||||
@@ -45,16 +45,11 @@ but the review is broader than any of them.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-05: `make dev` watches
|
||||
([#332](https://git.eeqj.de/sneak/AutistMask/issues/332)). It used to pass
|
||||
`--watch` to a `build.js` that read no arguments, so it built once and exited.
|
||||
`build.js --watch` now builds, then builds again after every change to a file
|
||||
under `src/`, `manifest/` or `icons/`; any other argument fails. It watches
|
||||
each directory rather than using Node's recursive watch, which on Linux stops
|
||||
seeing a file an editor saves by renaming a new copy over it. It writes no
|
||||
build receipt, so nothing can verify what it builds; `make build` remains the
|
||||
way to produce a `dist/` to hand on. `tests/buildWatch.test.js` covers the
|
||||
watch loop against a temp directory.
|
||||
- 2026-10-05: Every CI job has a `timeout-minutes` cap
|
||||
([#294](https://git.eeqj.de/sneak/AutistMask/issues/294)): `check` 10 minutes,
|
||||
`e2e-firefox` 15 and `e2e-chrome` 20, each over two and a half times the job's
|
||||
slowest cold-cache run. A hung build or browser now ends its job instead of
|
||||
holding the shared runner for hours.
|
||||
|
||||
- 2026-10-05: `PROXY_METHODS` in `src/background/index.js` no longer lists
|
||||
`eth_chainId` and `net_version`
|
||||
|
||||
@@ -15,15 +15,6 @@ const DIST_CHROME = path.join(DIST, "chrome");
|
||||
const DIST_FIREFOX = path.join(DIST, "firefox");
|
||||
const SRC = path.join(__dirname, "src");
|
||||
|
||||
// What `make dev` watches: the directories whose files build() bundles,
|
||||
// compiles or copies. A change anywhere else, package.json and build.js
|
||||
// included, starts no build; restart make dev after one.
|
||||
const WATCHED_DIRS = [
|
||||
SRC,
|
||||
path.join(__dirname, "manifest"),
|
||||
path.join(__dirname, "icons"),
|
||||
];
|
||||
|
||||
// The module whose compiled DEBUG state script/verify-build asserts. Which
|
||||
// bundles contain it is derived from esbuild's own dependency graph rather
|
||||
// than from a hardcoded list, so it tracks the bundle layout instead of
|
||||
@@ -450,10 +441,6 @@ function getBuildInfo() {
|
||||
async function build() {
|
||||
console.log("Building AutistMask extension...");
|
||||
|
||||
// Under make dev this runs once per rebuild in the same process, and each
|
||||
// build accounts only for what it emits itself.
|
||||
emittedFiles.length = 0;
|
||||
|
||||
const receiptPath = receiptTarget();
|
||||
if (!receiptPath) {
|
||||
console.warn(
|
||||
@@ -610,94 +597,27 @@ async function build() {
|
||||
console.log("Build complete: dist/chrome/ and dist/firefox/");
|
||||
}
|
||||
|
||||
// make dev: build, then build again after every change under `dirs`, until
|
||||
// interrupted. A failed build is reported and watching carries on, so a
|
||||
// half-finished edit does not end it. A change that arrives while a build is
|
||||
// running is not lost: it causes one more build as soon as that one finishes.
|
||||
// A directory created after this starts is not watched until a restart.
|
||||
//
|
||||
// make dev sets no AUTISTMASK_BUILD_RECEIPT, so these builds write no receipt
|
||||
// and nothing can verify what they leave in dist/.
|
||||
//
|
||||
// `rebuild` is build() everywhere but tests/buildWatch.test.js, which also
|
||||
// closes the returned watchers.
|
||||
function watch(dirs, rebuild) {
|
||||
let building = false;
|
||||
let changedAgain = false;
|
||||
|
||||
async function run() {
|
||||
if (building) {
|
||||
changedAgain = true;
|
||||
return;
|
||||
}
|
||||
building = true;
|
||||
do {
|
||||
// Let the rest of one save's events arrive first, so that one
|
||||
// save is one build.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
changedAgain = false;
|
||||
try {
|
||||
await rebuild();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
}
|
||||
} while (changedAgain);
|
||||
building = false;
|
||||
console.log("Watching for changes (Ctrl-C to stop)...");
|
||||
}
|
||||
|
||||
// One watcher per directory rather than fs.watch's recursive option: on
|
||||
// Linux, Node's recursive watch watches each file, and stops seeing one
|
||||
// that an editor saves by renaming a new copy over it. A directory's
|
||||
// watcher reports every change to the files in it, however they are saved.
|
||||
const subdirectories = dirs.flatMap((dir) =>
|
||||
fs
|
||||
.readdirSync(dir, { recursive: true, withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(entry.parentPath, entry.name)),
|
||||
);
|
||||
const watchers = [...dirs, ...subdirectories].map((dir) =>
|
||||
fs.watch(dir, run),
|
||||
);
|
||||
run();
|
||||
return watchers;
|
||||
}
|
||||
|
||||
// Run only as a program. Required as a module — which is how
|
||||
// tests/buildForbiddenInputs.test.js and tests/buildWatch.test.js reach the
|
||||
// functions below — this file builds nothing and writes nothing.
|
||||
// tests/buildForbiddenInputs.test.js reaches the checks below — this file
|
||||
// builds nothing and writes nothing.
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
// An argument this file does not know fails rather than being ignored.
|
||||
if (args.length > 1 || (args.length === 1 && args[0] !== "--watch")) {
|
||||
console.error("usage: node build.js [--watch]");
|
||||
process.exit(2);
|
||||
}
|
||||
if (args[0] === "--watch") {
|
||||
watch(WATCHED_DIRS, build);
|
||||
} else {
|
||||
build().catch((err) => {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
build().catch((err) => {
|
||||
console.error(
|
||||
`Build failed: ${err && err.message ? err.message : err}`,
|
||||
);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Exported for tests only: watch() for tests/buildWatch.test.js, the rest for
|
||||
// tests/buildForbiddenInputs.test.js. The prohibition those enforce is the
|
||||
// guarantee behind https://git.eeqj.de/sneak/AutistMask/issues/324, and
|
||||
// `make check` does not run `make build` — so they are unit tested against
|
||||
// synthetic metafiles rather than being exercised only by CI, where "it ran"
|
||||
// is not "it works".
|
||||
// Exported for tests/buildForbiddenInputs.test.js only. The prohibition these
|
||||
// three functions enforce is the guarantee behind
|
||||
// https://git.eeqj.de/sneak/AutistMask/issues/324, and `make check` does not
|
||||
// run `make build` — so they are unit tested against synthetic metafiles
|
||||
// rather than being exercised only by CI, where "it ran" is not "it works".
|
||||
module.exports = {
|
||||
importChain,
|
||||
newForbiddenRecord,
|
||||
recordBundledInputs,
|
||||
assertNoForbiddenInputs,
|
||||
assertForbiddenTableCovered,
|
||||
watch,
|
||||
};
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
// `make dev` runs `node build.js --watch`
|
||||
// (https://git.eeqj.de/sneak/AutistMask/issues/332). These drive build.js's
|
||||
// watch() over a temp directory with a stand-in for build(), so nothing here
|
||||
// builds or writes dist/.
|
||||
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const { watch } = require("../build");
|
||||
|
||||
let dir;
|
||||
let watchers = [];
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), "autistmask-watch-"));
|
||||
fs.mkdirSync(path.join(dir, "nested"));
|
||||
jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const watcher of watchers) watcher.close();
|
||||
watchers = [];
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Wait until `condition()` holds; fail the test if it has not within 3s.
|
||||
async function until(condition) {
|
||||
const deadline = Date.now() + 3000;
|
||||
while (!condition()) {
|
||||
if (Date.now() > deadline) throw new Error("timed out waiting");
|
||||
await sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the way many editors do: write a new copy, then rename it over the
|
||||
// original, so the file at that path is a different one afterwards.
|
||||
function saveByRename(file, contents) {
|
||||
fs.writeFileSync(`${file}.tmp`, contents);
|
||||
fs.renameSync(`${file}.tmp`, file);
|
||||
}
|
||||
|
||||
test("builds at start, then once per change to a file in a subdirectory", async () => {
|
||||
const file = path.join(dir, "nested", "a.js");
|
||||
fs.writeFileSync(file, "1");
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
});
|
||||
await until(() => builds === 1);
|
||||
|
||||
fs.writeFileSync(file, "2");
|
||||
await until(() => builds === 2);
|
||||
await sleep(300);
|
||||
expect(builds).toBe(2);
|
||||
});
|
||||
|
||||
test("keeps seeing a file that is saved by renaming a new copy over it", async () => {
|
||||
const file = path.join(dir, "nested", "a.js");
|
||||
fs.writeFileSync(file, "1");
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
});
|
||||
await until(() => builds === 1);
|
||||
|
||||
saveByRename(file, "2");
|
||||
await until(() => builds === 2);
|
||||
saveByRename(file, "3");
|
||||
await until(() => builds === 3);
|
||||
});
|
||||
|
||||
test("a failed build is reported and watching carries on", async () => {
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
if (builds === 1) throw new Error("unexpected token");
|
||||
});
|
||||
await until(() => console.error.mock.calls.length === 1);
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
"Build failed: unexpected token",
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(dir, "a.js"), "1");
|
||||
await until(() => builds === 2);
|
||||
});
|
||||
|
||||
test("a change made while a build runs causes one more build after it", async () => {
|
||||
let builds = 0;
|
||||
watchers = watch([dir], async () => {
|
||||
builds++;
|
||||
if (builds === 1) {
|
||||
fs.writeFileSync(path.join(dir, "a.js"), "1");
|
||||
await sleep(200);
|
||||
}
|
||||
});
|
||||
await until(() => builds === 2);
|
||||
await sleep(300);
|
||||
expect(builds).toBe(2);
|
||||
});
|
||||
Reference in New Issue
Block a user