Contributing
Set up the repository and follow its test, release, and safety rules.
Thanks for improving pixelkiln. The project is deliberately conservative around paid generation, file ownership, and provider state: a convenient failure must never widen a paid run, overwrite hand-edited art, or lose the identity of work that already cost money.
Development setup
Requires Node.js 22 or newer. The repository defaults to the latest Node.js 24
release through .nvmrc.
npm ci
npm run test:release
npm run typecheck
npm run test:docs
npm run test:security
npm test
npm run build
npm run test:package
npm run test:gallery
npm run test:godot
test:gallery serves a gallery over FakeProvider and drives it in headless
Chrome: the Generate and Regenerate buttons, the busy badge while a job runs,
the previous generation kept and restored for free, and zero console errors.
It needs a Chrome or Chromium binary (CHROME=/path/to/chrome when it is not
in a usual place) and skips without one outside CI.
The Next.js marketing/documentation site is isolated in website/:
npm ci --prefix website
npm run website:check
npm run website:lint
npm run website:build
npm run website:dev
The site reads canonical Markdown from docs/ and the root policy files at
build time. Do not create a second documentation copy under website/.
The home page's "Latest updates from our providers" section reads
website/app/lib/provider-updates.ts. When a provider ships a feature
PixelKiln can use, add an entry at the top of that list. The newest entry
shows its figures and code sample; earlier ones collapse to a short row. Show
only output from a real call, with the bill the docs record.
Website sprites are generated from website/art/pixelkiln.manifest.json.
Treat those requests as paid work: run plan and audit first, use an explicit
hard budget, and commit the manifest, lockfile, and reviewed outputs together.
Capture the Review UI at 1280×720 only after every asset
loads. Give a replacement capture a new public filename and update the page
reference so deployed image caches cannot retain the old version.
Run the source CLI without a global install:
npm run pixelkiln -- help
npm run pixelkiln -- plan --manifest examples/minimal/pixelkiln.manifest.json
Tests must not require a live provider account or API key. Use FakeProvider
for pipeline behavior and mocked HTTP responses for PixelLab or Retro Diffusion
wire contracts.
Change guidelines
- Start from
mainand keep one coherent change per pull request. - Use conventional commit subjects (
feat:,fix:,docs:,refactor:,test:,chore:). Semantic Release derives versions from them. - Scope website-only work as
chore(website):.website/is not in the packagefilesallowlist, so nothing under it can reach the published tarball. Semantic Release cannot see that, and afeat(website):subject cuts a minor release whose contents are byte-identical to the one before it. That happened once already: 0.2.0 is a favicon and an Open Graph image. Anywebsitescope is also refused a release byreleaseRulesin.releaserc.json, so a slip is caught rather than published, but the right subject keeps the changelog honest. - A change under
skills/pixelkiln/always releases, whatever the commit type says. That directory is what the publicgfargo/skillstap mirrors on every new release tag (see Releases), so adocs:-typed commit touching it — accurate framing for a human reader, since it reads as documentation — would otherwise cut no release at all under the default rules, and the tap would never see the change.analyzeCommitsruns a second plugin (scripts/release-plugins/skill-release-analyzer.mjs) alongside@semantic-release/commit-analyzerthat inspects each commit's actual changed files and forces at least a patch release whenever one touchesskills/pixelkiln/, regardless of type or scope. It only ever raises the release semantic-release would otherwise cut, never lowers or blocks one, so there is no commit-subject convention to remember here — just don't remove the plugin from.releaserc.json(test:releasefails if it is missing). - Add regression coverage for bug fixes and behavior coverage for new public options or exports.
- Update README/help text and focused docs in the same change as user-facing behavior.
- Keep README as the concise product landing page. Put durable reference and
workflow detail in
docs/, and link every guide fromdocs/README.md. - Run
npm run test:docsafter changing Markdown or the CLI command/flag surface. It checks local links, the docs index, README size, and CLI coverage. - Run
npm run test:securityafter changing provider source handling, lockfile fixtures, or benchmark state. It scans tracked JSON without printing a credential-bearing value. - Run the website lint and build checks when changing
website/or the canonical Markdown it renders. - Run the package smoke test when changing exports, build configuration, the
executable, package metadata, or the
filesallowlist.
Architectural boundaries
- Provider-specific URLs, authentication, response schemas, and quirks belong
below
Provider. - Planning stays offline. Provider
supports()andestimate()must perform no I/O and estimates must carry their cost unit. - Provider responses are untrusted input and need runtime validation before entering the lockfile.
- Temporary signed URLs must not settle in a committed lockfile. Persist a refreshable provider reference and keep storage credentials transient.
- Lockfile writes must remain atomic and resumable. Additive defaults should preserve valid v2 files when extending lock entries.
- Unknown flags and ambiguous output selection are errors. Silent widening is especially dangerous when a command can spend provider quota.
- Never overwrite a generated file whose current bytes differ from its recorded hash, and never make deletion an implicit side effect of recovery/triage.
- The CLI is a thin layer.
src/cli.tsis the bin entry,src/cli/main.tsmaps command names to modules undersrc/cli/commands/,src/cli/args.tsparses flags, andsrc/cli/project.tsopens a project the way every command needs it. A command module should read and print; anything it computes belongs insrc/pipeline/where the library exports it.
Pull requests
Describe the user-visible outcome, any compatibility impact, and the exact
checks run. Keep generated dist/ files out of commits; packaging builds them
from source. If hosted CI is unavailable, include the local Node version and
full check results in the PR description.
Security-sensitive findings should follow SECURITY.md, not a public issue with exploit details.
Releases
Merging to main is the release. Semantic Release derives the version from the
conventional commit subjects in the range, publishes to npm, tags the commit,
writes the GitHub release, prepends to CHANGELOG.md, and commits the changelog
and version back to main with [skip ci]. Contributors do not run anything.
There is no npm token. Publishing authenticates over OIDC trusted
publishing: the workflow grants id-token: write, npm exchanges that for a
short-lived credential, and the npm CLI performs the exchange itself during
npm publish. A side effect worth keeping is that every release carries a
signed provenance attestation linking the tarball to its source commit and
workflow run.
The release job is serialized so two quick merges cannot publish concurrently,
and it times out after 20 minutes rather than holding publishing permission
indefinitely. npm run test:release checks the repository-side trust contract:
the OIDC permission, GitHub-hosted runner, registry configuration, full checkout
history, absence of an npm publishing token, and the expected release plugins.
The public gfargo/skills tap watches PixelKiln releases on an hourly schedule.
When a new tag appears, it mirrors the tagged skills/pixelkiln/ directory,
bumps the games plugin, and publishes a games release. PixelKiln remains the
source of truth; the source repository needs no cross-repository write token.
Two conditions have to hold on the npm side, and neither lives in this repository:
- the package must exist on the registry, and
- it must have a Trusted Publisher entry naming this repository and the
release.ymlworkflow.
When one is missing, the token exchange reports `404 OIDC token exchange error
- package not found
, and Semantic Release then falls through to token auth and fails withEINVALIDNPMTOKEN`. That 404 does not necessarily mean the package is absent. It reads identically when the package is published but has no Trusted Publisher entry, which is the more likely cause once a release has ever succeeded. Check the entry before doubting the publish.
Because the exchange cannot authenticate against a package that does not exist
yet, 0.1.0 was published by hand to bootstrap that trust, and its changelog
section was written by hand for the same reason. Every release from 0.2.0
onward is automated.
A failed release opens an issue labelled semantic-release, which the next
successful run closes. That label must exist in the repository or the reporting
step itself fails with a validation error and hides the original failure.
See the website scoping rule and the skills/pixelkiln/ release-forcing
plugin under change guidelines for the two things that
change whether a release happens at all, independent of the conventional
commit type on its own.
