Skip to content
Go to console
Go to console

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 run npm 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 first sluiceway.yaml, as record 0065 says. sluiceway check [path] is the check of record 0042 over the files at path: the stacks it finds, what root module discovery found and left out, whether sluiceway.yaml is valid and what the workflow files lack. Both need no credential, no token, no tool and no network. path is the current directory when left out. Only init needs the top of a git repo; the check needs a directory. --help, -h, --version and -V do what they say.
  • scan, resolve, apply, settle and auto are 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.argv and nothing of INPUT_* or GITHUB_*. A laptop that has INPUT_MODE or 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 to dist/cli.js, beside dist/index.js. The action’s entry, action.yml and dist/index.js stay where they are, and the action still runs init with mode: 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/*, where dist/index.js is 2.5 MB. The cost is a second output under dist/, which the dist check covers like the first, and a second build command in the same build script. src/cli/ is glue like src/modes/: the boundary rule keeps core/, adapters/ and render/ from importing it.
  • --force writes .github/workflows/deploy-dashboard.yml again, 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 says Replaced in place of Wrote. Any other workflow that runs Sluiceway still stops init, with or without --force, because two workflows would scan and write the same dashboard. A sluiceway.yaml that is there is still kept, and .github/scripts/export-env.sh is 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 adds or npx sluiceway init --force when that file was all that stopped it. The last line of what is left for a person says run 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/sluiceway stays 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.json is the action’s, release-please bumps it, and the package is published from the release tag. So sluiceway@0.28.0 is the code of v0.28.0, and npx sluiceway@0.28.0 init writes the workflow 0.28.0 writes. The command line reads its version at run time from the package.json one directory above its bundle, as the action does (build plan, section 3), because release-please cannot rebuild dist/. The workflow init writes keeps uses: sluiceway/sluiceway@v0, as it did: @v0 is 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, devDependencies and scripts from the release checkout’s package.json before npm publish, and npx installs the bundle alone. files is dist/cli.js, and npm adds package.json, README.md and LICENSE. engines says Node 22 or newer, and the command line says the same in words before it reads a file. The repo’s own package.json keeps 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 marked private, 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 npm in .github/workflows/release.yml publishes after release-please created a release. It has contents: read and id-token: write and nothing else, so the OIDC token is never in a job that can write to the repo, runs on ubuntu-latest because 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 of release.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 than package.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 with ENEEDAUTH, E401, E403 or E404 is 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 include npm 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, with npm trust list to see what is there), and, for E404, 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 and v0 are done in the job before, whatever this one says.

Considered

  • One bundle with two doors, dist/index.js looking at process.argv when no INPUT_* 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-error on 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.0 into the workflow a pinned package writes. It would change the files init writes, and @v0 stays the recommendation.

Consequences

  • The line init as an npm package leaves docs/later.md, and so docs/roadmap.md.
  • The docs say npx sluiceway init and npx sluiceway check where they said the clone line: docs/init.md, the README’s Get started, docs/reference.md and docs/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.