Shared tooling for the Dustin Edwards repositories, public so private repositories can use it.
default.json: the Renovate policy. Each repository'srenovate.jsonextends it; each setting's reason is in its description..github/workflows/: reusable workflows, each called, never run on its own. A caller pins a full 40-character devkit sha, andscripts/check-callers.mjs(run by the caller on its own workflow files) fails on a branch, a tag orsecrets: inherit.examples/holds the shape of each caller.ai-author.yml: refuses an AI attribution trailer on new commits, and on a pull request a commit authored or committed as Claude (devkit-check-trailers). It installs devkit from the tag in the caller's package.json, or from thedevkitinput.pr-footer.yml: removes the "Generated by Claude Code" footer from pull request descriptions. The script is inline, so a caller needs only the sha.no-bloat.yml: the Knip and jscpd ratchet (devkit-no-bloat). Public repositories call it on every pull request; private ones weekly.worker-size.yml: the Worker bundle size budget (devkit-worker-size).worker-deploy.yml: deploy, verify and roll back a Cloudflare Worker. See "The shared deploy".code-map.yml: the code map for the explorer at capsid.dustinedwards.info. See "The code map".bump-consumers.yml: a package repository calls it on a tag push; it starts a Renovate run in every repository whosepackage.jsondepends on that package. It needs theRENOVATE_TRIGGER_TOKENsecret described in its header.
testing/andbin/: the package (@dustinedwards/devkit): shared test helpers and shared checks. See below.test/: CI runs these (npm test).
Bins in the package, so a check runs the same locally (npx <bin>) as in the workflow that calls it. The workflow runs the bin from the caller's own install, so the caller lists @dustinedwards/devkit in its devDependencies at a tag.
devkit-no-bloat: Knip and jscpd, each count compared with the baseline in the repository's.no-bloat.json. A count above its baseline fails (exit 1, an::error::line each); a count at or below passes, and one that went down is named, with the way to lower the baseline:npx devkit-no-bloat --write-baseline, then commit.no-bloat.json. A deliberate raise is the same command, in a reviewed change. A tool that cannot run fails, naming its error, and so does a config that is not JSON. Tools run fromnode_modules/.binwhen installed, otherwise through npx at the config'stools(defaultknip@6,jscpd@4). The config's fields are in the header ofbin/no-bloat.mjs.devkit-worker-size: runswrangler deploy --dry-runand fails when its "Total Upload" (raw or gzip, the size Cloudflare counts) is over the budget in the repository's.worker-size.json:{ "raw_kib": 1700, "gzip_kib": 400 }, with optionalargsfor the dry run andwrangler(npx spec when wrangler is not installed, defaultwrangler@4). Cloudflare's upload limits are far higher; the budget is about startup, which must finish inside 1 s. No budget file, a failed dry run or no Total Upload line fails. The workflow'sprepareinput gives the dry run a config (cp wrangler.jsonc.example wrangler.jsonc, or a build) andworking-directorynames where it lives. No secret is needed.devkit-check-trailers: with no arguments it reads the range from the GitHub event (EVENT,PR_BASE,PR_HEAD,PUSH_BEFORE,PUSH_AFTER, set byai-author.yml); locally,npx devkit-check-trailers <base> <head> [--authors]. Exit 1 names each commit and how to fix it; exit 2 means it could not read the commits, which is a failure, not a pass.
Per-repository measurements (capsomer's page weight, dustinedwards-info's route weight, capsid's dashboard size budget) stay in the repository.
.github/workflows/worker-deploy.yml deploys a Cloudflare Worker, checks it live, and rolls it back when the check refuses it. It is capsid's deploy and live gate jobs and scripts/rollback-guard.mjs, so every Worker site gets what capsid has. A caller (examples/worker-deploy-caller.yml) calls it on a push to its default branch, after its own checks, pinned to a full devkit sha, with a concurrency group that never cancels a deploy in flight.
Two jobs:
deploychecks out, installs, runs the caller's pre-deploy guard, runswrangler deploywith the wrangler version pinned in the file, and publishes the run attempt it shipped in. Never on a pull request.live gatepolls the health URL (40 reads, 3 seconds apart) until its JSONshais this run's commit, then runs the verify command. Exit 0 passes. Exit 1 is a refusal, and only a refusal runs "Roll back the deploy this run shipped": the rollback guard rolls back only when what is live is this run's commit (or /health is unreadable and the deploy ran in this same attempt), runswrangler rollback, and reads /health until the sha moves. Exit 3, or a /health that never answered, is "could not run": the job fails and nothing rolls back.
capsid's watcher reads the job name live gate, the step name, and the step's three ::error:: sentences ("rolled back: was ...", "not rolling back. ...", "the rollback did not take effect: ...") to write deploy_outcomes, and the step summary carries rollback_from= and rollback_to=. test/worker-deploy-workflow.test.mjs fails on a change to any of them.
Inputs:
Secrets, passed by name (secrets: inherit is refused by check-callers.mjs): CLOUDFLARE_API_TOKEN, a token that can deploy and roll back the Worker (Workers Scripts: Edit), and CLOUDFLARE_ACCOUNT_ID. Both are required, and a missing one fails the deploy loudly rather than skipping it.
The limits. Cloudflare's rollback swaps the Worker's code and bindings only. It is refused when D1 or KV resources changed between the two versions, and it never undoes a D1 migration (Cloudflare Docs, "Rollbacks"). A deploy that ran a migration is not made safe by this workflow: keep migrations a separate, human step, and check them in pre-deploy. Data recovery is D1 Time Travel, by hand (wrangler d1 time-travel restore). The rollback goes to the previous version, whatever that is; the guard is what stops a rerun from moving production onto the refused commit.
The guard is inlined into the workflow so a caller's one pinned sha fixes both. After an edit to scripts/rollback-guard.mjs, run node scripts/inline-rollback-guard.mjs; the test fails while the two differ.
.github/workflows/code-map.yml runs on a push to a public repository's default branch and replaces code-map.json on that repository's orphan code-map branch. Capsid reads it there and the explorer draws it. The script, scripts/code-map.mjs, is inline in the workflow, so a caller pins one sha and needs no release (examples/code-map-caller.yml).
It reads the code through the caller's own TypeScript, so there is no new dependency: imports are resolved with the caller's compiler options, and cyclomatic complexity is counted on the syntax tree (one, plus each if, loop, case, catch, ?:, &&, || and ?? in a function's own body). Coverage is the istanbul JSON Vitest writes (--coverage.reporter=json) or the lcov file of node --test --experimental-test-coverage --test-reporter=lcov, and a Stryker mutation.json adds mutants and a mutation score; both are mapped onto the innermost function holding each statement or mutant. CRAP is CC² × (1 − coverage)³ + CC. A number nobody measured is null, never 0.
Mutation runs only when mutation-command is given, which a caller does on a weekly schedule: that run's Stryker reports (mutation-file, a glob for one report per chunk) are kept as mutation.json on the code-map branch beside the map. Every other run reads that file back and, from the source each report holds, carries a function's numbers forward only while its lines are unchanged; a function that changed or is new is stale: true with no numbers, and the map names the run as mutated: {sha, date}.
Inputs: coverage-command (empty for no coverage), coverage-file, mutation-command, mutation-artifacts (for a caller whose Stryker run is a matrix of earlier jobs: the pattern of the artifacts holding the reports, downloaded from that run into mutation-artifacts/<artifact>/), mutation-file, tsconfig (a repository without one is read as plain JavaScript modules), include (path prefixes to map, separated by spaces, default src/; . for every folder), exclude (prefixes to leave out), install (must install typescript; a JavaScript repository can add it with npm i --no-save typescript), npm-cache (false for a repository with no package-lock.json) and node-version-file. Locally: node scripts/code-map.mjs --out code-map.json [--coverage coverage/coverage-final.json] from the repository's root.
Test code that more than one repository needs, written once here. Installed by tag:
"@dustinedwards/devkit": "github:DrDustinEdwards/devkit#v0.7.0"
Only testing/, bin/ and this README are in the package. No dependencies and no test framework: they run in node and in workerd.
-
@dustinedwards/devkit/github:stubGitHub({ owner, repo, branch, files }), a fake of the GitHub contents and Git Data API at the outbound fetch. An unknown host or route throws, blob shas are real, a tree applies only when the ref moves,failNextplants 500s andhistoryplants the commit listing.install: falsehands back the fake asfetchinstead of installing it globally.gitBlobShaandversionOfcome with it. -
@dustinedwards/devkit/network:refuseNetwork({ hint }), the global fetch that throws naming the URL, andinstallFetch, the swap under it and under the GitHub fake. Each returns its restore. -
@dustinedwards/devkit/d1:resetDb(db, { keep }), which empties every tablesqlite_masterlists in one batch, so a table a migration adds is cleared without anyone adding it to a list. A full-text table is cleared through itself and its shadow tables are left to it; foreign keys are checked at commit, so table order does not matter;d1_migrations, SQLite's and D1's own tables are never touched. It returns the tables it cleared. -
@dustinedwards/devkit/access:await stubAccess({ issuer, audience, email, authorizationUrl }), a fake of Cloudflare Access for SaaS for one sign-in. The token endpoint answers an RS256 ID token signed by a key made per stub, carrying the issuer, the audience, the email and the nonce from the authorization URL, and the JWKS serves its public half, so the code under test verifies it as it verifies Access's.claimsadds to every token,token(extra)signs one directly (also what a test sends asCf-Access-Jwt-Assertion, withjwksUrlset to the team's/cdn-cgi/access/certs), andtokenRequestsholds what the token endpoint received. An unknown URL throws unlessfallbackanswers it.
An entry point is added when a repository adopts it. A release is a tag; a consumer moves to it in its own pull request.
