diff --git a/.github/workflows/lists.yml b/.github/workflows/lists.yml index 892007044..705f57b4e 100644 --- a/.github/workflows/lists.yml +++ b/.github/workflows/lists.yml @@ -34,6 +34,11 @@ jobs: # in the build job because it must run even when the build cannot. - name: The repository's own metadata run: node scripts/checkRepoMeta.mjs + # Whether the Deno build can still reach its dependencies. Same reason as + # above: it must run even when the toolchain cannot be installed, and it + # is the check that would have caught the broken map years earlier. + - name: The Deno import map + run: node scripts/checkDenoMap.mjs # What phishing.pezkuwichain.io runs before it serves a new master: against a scratch # repository and web root, with these lists. - name: The host's publish checks diff --git a/.github/workflows/pr-any.yml b/.github/workflows/pr-any.yml index f59f09f3c..807336066 100644 --- a/.github/workflows/pr-any.yml +++ b/.github/workflows/pr-any.yml @@ -5,7 +5,10 @@ permissions: {} jobs: pr: - continue-on-error: true + # continue-on-error was inherited from upstream and meant these four steps + # could never turn a pull request red. Two of them were failing — build since + # 2026-09-13 and deno for far longer — and nobody saw it, because the job + # reported success either way. A check that cannot fail is not a check. strategy: matrix: step: ['lint', 'test', 'build', 'deno'] diff --git a/.gitignore b/.gitignore index e996b5602..c5122996d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ target/ tmp/ /mod.ts /import_map.json +/deno.lock .DS_Store .env.local .env.development.local diff --git a/import_map.in.json b/import_map.in.json new file mode 100644 index 000000000..5ac05cf0c --- /dev/null +++ b/import_map.in.json @@ -0,0 +1,7 @@ +{ + "imports": { + "https://deno.land/x/pezkuwi/util/mod.ts": "npm:@pezkuwi/util@^14.0.5", + "https://deno.land/x/pezkuwi/util-crypto/mod.ts": "npm:@pezkuwi/util-crypto@^14.0.5", + "https://deno.land/x/pezkuwi/x-fetch/mod.ts": "npm:@pezkuwi/x-fetch@^14.0.5" + } +} diff --git a/package.json b/package.json index daeb871ab..0256c7324 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "@types/node": "^22.10.5" }, "resolutions": { - "typescript": "^5.5.4", "axios": "^1.13.5", - "semver": "^7.5.2" + "semver": "^7.5.2", + "typescript": "^5.5.4" } } diff --git a/scripts/checkDenoMap.mjs b/scripts/checkDenoMap.mjs new file mode 100644 index 000000000..f2aff94d6 --- /dev/null +++ b/scripts/checkDenoMap.mjs @@ -0,0 +1,74 @@ +// Copyright 2020-2026 @pezkuwi/phishing authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Whether the Deno build can still reach its dependencies. +// +// The build rewrites every @pezkuwi/* import into a deno.land URL: +// @pezkuwi/util becomes https://deno.land/x/pezkuwi/util/mod.ts. Upstream can do +// that because deno.land/x/polkadot is published; our fork renamed the prefix to +// x/pezkuwi and never published there, so deno.land/x/pezkuwi answers 404 and +// `deno check` failed on three modules — for months, unnoticed, because the PR +// job carries continue-on-error and can never turn a pull request red. +// +// import_map.in.json is the tooling's own hook for this: pezkuwi-dev-deno-map +// merges its entries into the generated import_map.json. The three externals are +// mapped to npm: specifiers, which Deno resolves natively, so the Deno build uses +// the very packages we already publish. Nothing new has to be published, and +// claiming the deno.land/x/pezkuwi namespace stays a decision we can take later +// rather than one this build forces. +// +// What can rot: a dependency is bumped in packages/phishing/package.json and the +// map keeps pointing at the old range, so `deno check` type-checks a different +// version of @pezkuwi/util than the node build compiles against. That divergence +// would not fail anything — it would just quietly check the wrong code. +// +// node scripts/checkDenoMap.mjs + +import { existsSync, readFileSync } from 'node:fs'; + +const errors = []; + +function check (name, ok, detail) { + console.log((ok ? 'ok ' : 'FAIL ') + name + (ok ? '' : ` — got: ${JSON.stringify(detail)}`)); + + if (!ok) { + errors.push(name); + } +} + +check('the deno import map exists', existsSync('import_map.in.json')); + +if (errors.length) { + console.log(`FAILED: ${errors.length}`); + process.exit(1); +} + +const map = JSON.parse(readFileSync('import_map.in.json', 'utf8')).imports || {}; +const pkg = JSON.parse(readFileSync('packages/phishing/package.json', 'utf8')); +const deps = pkg.dependencies || {}; + +// Every @pezkuwi dependency the package pulls in has to be reachable from Deno, +// because the build turns all of them into deno.land URLs. +const external = Object.keys(deps).filter((d) => d.startsWith('@pezkuwi/')); + +check('the package has @pezkuwi dependencies to map', external.length > 0, external); + +for (const dep of external) { + const short = dep.replace('@pezkuwi/', ''); + const key = `https://deno.land/x/pezkuwi/${short}/mod.ts`; + const want = `npm:${dep}@${deps[dep]}`; + + check(`${dep}: the deno build has somewhere to resolve it`, !!map[key], { have: Object.keys(map), key }); + check(`${dep}: the deno build and the node build agree on the version`, map[key] === want, { map: map[key], want }); +} + +// And nothing in the map points at a package we no longer depend on, which would +// hide a removed dependency behind a mapping that still resolves. +for (const [key, value] of Object.entries(map)) { + const named = String(value).replace(/^npm:/, '').replace(/@[^@/]*$/, ''); + + check(`${key}: maps to a dependency we still declare`, external.includes(named), { named, value }); +} + +console.log(errors.length ? `FAILED: ${errors.length}` : 'all deno map checks passed'); +process.exit(errors.length ? 1 : 0);