init and the check run as npx sluiceway, from a second bundle published by trusted publishing
Decision record 0094
Amends 0065 (a person runs init from the action’s bundle, with
INPUT_MODE=init). Built as slice 5.30, for issue 218.Amended by 0116 (slice 5.53): the command line also has commands that talk to the hosted app with a personal token, over HTTPS to the app alone and never to GitHub, and is released as standalone binaries too. init and the check still make no network call, and the arguments are still the only way in.
Amended by slice 5.32: the probe of npm’s exchange endpoint said yes on the first release, 0.28.0 (run 35884043091), and npm still refused the publish with
403 Forbidden - OIDC permission denied for this action, which failed the release run. The exchange proves only that some trusted publisher matches the workflow, not that it may runnpm publish. The probe is gone: the publish itself is the question, and a refusal that means npm does not trust the workflow yet is a warning.
Record 0065 had a person start init with one command line: clone the release tag into a temporary directory and run dist/index.js with INPUT_MODE=init. It kept one thing to release. The owner read that line on 2026-09-23 and asked why it is not simply npx. Nobody pastes that line with a straight face: it clones a repo and sets a variable that exists for the Actions runner. The npm names sluiceway and @sluiceway/sluiceway were already reserved as 0.0.1 placeholders, and docs/later.md listed “init as an npm package or a command of its own”. Issue 218 asked for it.
Decision
- Two commands run outside a runner, and only two.
sluiceway init [--force] [path]writes the workflow and, when there is none, a firstsluiceway.yaml, as record 0065 says.sluiceway check [path]is the check of record 0042 over the files atpath: the stacks it finds, what root module discovery found and left out, whethersluiceway.yamlis valid and what the workflow files lack. Both need no credential, no token, no tool and no network.pathis the current directory when left out. Only init needs the top of a git repo; the check needs a directory.--help,-h,--versionand-Vdo what they say. scan,resolve,apply,settleandautoare refused, never half run. The command line gives one sentence: that the mode needs the run’s identity and the workflow token, so it runs only in the workflow, with the workflow page of the docs site. It reads no file first.- Arguments, never the environment. The command line reads
process.argvand nothing ofINPUT_*orGITHUB_*. A laptop that hasINPUT_MODEor a token in its environment gets the same answer as one that has none. - Exit codes. 0 when the command did what it says. 1 when it ran and failed: init stopped at a workflow that is there, the config is not valid, discovery failed, no directory, or Node older than 22. 2 for a command line that was not understood, and for a refused mode. The check fails only where its job would be red (0042): a warning is still 0.
- A second entry point,
src/cli.ts, built todist/cli.js, besidedist/index.js. The action’s entry,action.ymlanddist/index.jsstay where they are, and the action still runs init withmode: init. Two reasons against one bundle with two doors. First, a bundle that holds the GitHub port, the process runner and the notification sender can only refuse at run time what the command line must never do. A bundle built from its own entry cannot reach them at all, and a test walks its imports to prove it, the same proof records 0042 and 0065 give. Second, the package ships one file of 1.1 MB, without@actions/*or@octokit/*, wheredist/index.jsis 2.5 MB. The cost is a second output underdist/, which the dist check covers like the first, and a second build command in the samebuildscript.src/cli/is glue likesrc/modes/: the boundary rule keepscore/,adapters/andrender/from importing it. --forcewrites.github/workflows/deploy-dashboard.ymlagain, and nothing else. Without it init never overwrites a file, as 0065 said. With it, that one file is replaced when it is there, and init saysReplacedin place ofWrote. Any other workflow that runs Sluiceway still stops init, with or without--force, because two workflows would scan and write the same dashboard. Asluiceway.yamlthat is there is still kept, and.github/scripts/export-env.shis still left alone when it is there: both are the person’s to change.- init’s words name the command line. The message that stops it at a workflow says
Run npx sluiceway check to see what it lacks, and addsor npx sluiceway init --forcewhen that file was all that stopped it. The last line of what is left for a person saysrun npx sluiceway check. The files init writes do not change by a byte, and a test compares what the action’s door and the command line write for the three example projects. - One package,
sluiceway. It is the name a person types.@sluiceway/sluicewaystays a held name and is never published: a second package with the same bundle doubles the release and the trusted publisher setup, splits the download count, and leaves two names to keep in step. It keeps a look-alike out of the scope. - The version is the action’s.
package.jsonis the action’s, release-please bumps it, and the package is published from the release tag. Sosluiceway@0.28.0is the code ofv0.28.0, andnpx sluiceway@0.28.0 initwrites the workflow 0.28.0 writes. The command line reads its version at run time from thepackage.jsonone directory above its bundle, as the action does (build plan, section 3), because release-please cannot rebuilddist/. The workflow init writes keepsuses: sluiceway/sluiceway@v0, as it did:@v0is what the docs recommend until 1.0.0, and the rules it writes to are those of the release it came from. - The published package.json names no dependencies. The bundle holds every package it runs, so the publish step deletes
dependencies,devDependenciesandscriptsfrom the release checkout’spackage.jsonbeforenpm publish, and npx installs the bundle alone.filesisdist/cli.js, and npm addspackage.json,README.mdandLICENSE.enginessays Node 22 or newer, and the command line says the same in words before it reads a file. The repo’s ownpackage.jsonkeeps its dependencies, because they are the approved runtime list of the build plan, section 5, and what the bundles are built from. It is no longer markedprivate, which npm needs to publish. - Trusted publishing, no token. npm’s trusted publishing with OIDC (docs.npmjs.com/trusted-publishers, generally available since 2025-07-31): the package on npmjs.com trusts one workflow file of one repository, and a publish from it gets a publish token that lives for that publish alone. A new job
npmin.github/workflows/release.ymlpublishes after release-please created a release. It hascontents: readandid-token: writeand nothing else, so the OIDC token is never in a job that can write to the repo, runs onubuntu-latestbecause npm accepts no self-hosted runner, checks out the release tag, and installs npm 11.20.0, since npm 11.5.1 is the first that publishes with OIDC. npm then adds provenance by itself, because the repository and the package are public. It was chosen over an automation token because a token is a long-lived secret that can publish from anywhere it leaks to, needs rotating and a person to create and store it, while trusted publishing has no secret to leak and ties every version to the run that built it, which matters for a tool that sits in a deploy path. npm matches the workflow by file name, so the publish is a job ofrelease.yml, the file the owner named on npmjs.com, and not a workflow file of its own. - A warning, never a red run, while npm does not trust the workflow (amended by slice 5.32). The job checks out the release tag and runs
scripts/ci/publish-npm.sh, which decides from npm’s own answer to the publish. A bundle that does not run, or prints another version thanpackage.json, fails before anything is published. A version already on npm (npm view, or npm’s refusal to publish over a version) is said in the log and the job stays green, which is what a re-run of a release does. A publish npm refuses withENEEDAUTH,E401,E403orE404is a warning on the run and green: npm does not trust the workflow yet, and the log says which fields to set on npmjs.com (organization or user, repository, workflow filename, an empty environment name, and allowed actions that includenpm publish), that publishing access must let a trusted publisher publish, the same setup as one command for npm 11.15 or newer (npm trust github <package> --repo <owner>/<repo> --file <workflow> --allow-publish, withnpm trust listto see what is there), and, forE404, that npm adds a trusted publisher only to a package that exists. Any other failure of the publish fails the job. The release, its tags andv0are done in the job before, whatever this one says.
Considered
- One bundle with two doors,
dist/index.jslooking atprocess.argvwhen noINPUT_*is set. One file less, but the action’s entry would change, and the command line could reach the GitHub port. - An automation token in a repository secret,
NPM_TOKEN. The first brief of the slice. Replaced by trusted publishing, for the reasons above. - A repository variable that turns the publish on. A second thing for the owner to set, and it can say yes while npm says no. Asking npm is the answer that cannot drift.
continue-on-erroron the publish. It would also hide a publish that npm refused for a real reason once the setup exists.- Asking npm’s exchange endpoint first, as slice 5.30 built it. It answered yes while the publish was refused (slice 5.32), so a green probe proved nothing the publish did not.
- Moving the dependencies to
devDependencies. It would make the published file right with no step, and blur the approved runtime list that the build plan asks the owner about. - Writing
@v0.28.0into the workflow a pinned package writes. It would change the files init writes, and@v0stays the recommendation.
Consequences
- The line
initas an npm package leavesdocs/later.md, and sodocs/roadmap.md. - The docs say
npx sluiceway initandnpx sluiceway checkwhere they said the clone line:docs/init.md, the README’s Get started,docs/reference.mdanddocs/workflow.md. The docs site follows at the release its pin moves to. - Until the owner adds the trusted publisher on npmjs.com, every release carries a warning that the package was not published and what to set, and npm keeps 0.0.1.
- Building this slice showed that the check job never handed the check
explainDiscovery, so a real run of the check left out the root module lines record 0092 promised. The check job and the command line now hand the check the same files-only part of the adapters.