Skip to content

feat: publish releases to npm and PyPI through trusted publishing, when asked - #76

Merged
vannt-dev merged 2 commits into
mainfrom
feat/publish-module
Oct 20, 2026
Merged

vannt-dev merged 2 commits into
mainfrom
feat/publish-module

Conversation

@vannt-dev

@vannt-dev vannt-dev commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

What changes for users

  • New opt-in switch modules.publish (off by default, in no preset). It adds jobs.publish to the
    generated release.yml. The job runs when release_created == 'true':
    • node → npm: Node.js 24 (npm ≥ 11.5.1), the stack's install command (corepack for pnpm/yarn),
      then npm publish --access public;
    • python → PyPI: python -m build (build 1.6.1), then pypa/gh-action-pypi-publish.
  • Trusted publishing only: permissions: { contents: read, id-token: write }, and no token or secret
    anywhere. npm adds provenance by itself for a public repository. --provenance is left out on
    purpose, because npm refuses it for a private repository.
  • The job is inline in release.yml, not in a reusable workflow. PyPI does not accept reusable
    workflows as trusted publishers, and npm checks the caller's filename, so both registries name
    release.yml.
  • Not published, with a note from init/update/check: a released stack other than node/python, a
    stack in a folder (release-please prefixes the outputs with the folder name and the reusable release
    workflow does not pass those on, so this is documented as not supported), or modules.release off.
    GitLab writes nothing.
  • init prints the trusted publisher entry to add. The docs (docs/ci-and-releases.md#publishing-to-npm-and-pypi)
    cover the npmjs.com and pypi.org setup (exact owner/repo/release.yml, empty environment, allowing
    npm publish, publishing once by hand first on npm) and the 404/403 trap when the entry does not
    match exactly.
  • README, init -i and the playground (with the "needs repokeeper 0.11.0" note) list the switch.
  • The standard stays 1.4.4: output without the switch does not change.

Pinned actions and Dependabot (second commit)

The publish job is the first generated output with third-party actions in a key repokeeper manages
(checkout, setup-node or setup-python, pypi-publish). They are SHA-pinned to the same commits as the
reusable workflows here (ACTION_PINS in version.ts, held together by a test). Until now, a Dependabot
bump of such a pin would have counted as drift.

Choice: option (a), a newer version of the same action is not drift. I chose it because option (b)
had nothing to follow: no generated output had a third-party action before, and check compared every
managed YAML key by its exact content. The change is in decide() (src/sync/decide.ts):

  • A step of a managed YAML key whose uses: names the same action at another commit or tag reads as
    unchanged. check passes and update keeps the newer version. YAML comments such as
    # v7.2.0 were never compared.
  • While the key is still exactly what repokeeper wrote, update moves the pin as before.
  • Still drift: a different action, any other change to the key, a local action (./…), a Docker image,
    and the ref a job calls a reusable workflow at (uses: …/.github/workflows/…@v0), which repokeeper
    pins on purpose.
  • Applies to YAML keys only, not to whole files.
  • So dependabot.yml no longer ignores the publish job's actions. The ignore would have stopped
    Dependabot from bumping them in the repository's own workflows too.

Known gaps:

  • Module switched off after a bump. If Dependabot has bumped a pin and the module is then
    switched off, update does not see the key as repokeeper's own. It reports it as edited and keeps
    it (orphan-edited) instead of deleting it. The lock stores only a hash of what repokeeper wrote,
    so it can't tell a version-only change from other edits there. Fixing that would change the lock
    format, which I left alone.
  • New standard that also changes the job. If a later standard changes the job in other ways too
    (say, a new step) after Dependabot bumped a pin, update reports a conflict, as it does today for
    any edited key. It is resolved with --accept.

This PR is rebased onto main after #74 and #75. Module and question order everywhere: cache_cleanup,
automerge, publish.

Verification

  • npm run typecheck, biome check ., npm test (524 passed), npm run build: all pass.
  • New test/sync-action-versions.test.ts, written before the change:
    • decide-level cases (bumped commit or tag → unchanged; untouched → write; another action, another
      edit, reusable-workflow ref, local/docker → still drift);
    • an end-to-end run: init with publish on, then the setup-node pin moved to another commit with a
      # v7.2.0 comment → check exits 0 and update keeps it; changing the run: line → check
      exits 1.
  • node dist/cli.js init on a sample node repo and a sample python repo with cache_cleanup, automerge
    and publish all on. dependabot.yml has no action ignores, and actionlint 1.7.12 reports nothing.
  • No real publish was run: that needs a trusted publisher entry and a release.

…en asked

modules.publish, off unless asked for and in no preset, adds a publish job to
release.yml. When the release job creates a release it publishes a node package
to npm (Node.js 24, the stack's install command, npm publish --access public) or
builds a python package and publishes it to PyPI with pypa/gh-action-pypi-publish.
The job has id-token: write and stores no token; npm adds provenance by itself on
a public repository. It is a job of release.yml, not a reusable workflow: PyPI
takes no reusable workflow as a trusted publisher, and npm checks the caller.

The actions are pinned to the commits the reusable workflows use, and Dependabot
is told to leave them alone so a bump does not show up as drift. A released stack
other than node or python, one in a folder, or release switched off writes
nothing and says why. init names the trusted publisher entry to add. The docs
explain the setup on npmjs.com and pypi.org and the 404/403 that npm answers
when the entry does not match the repository and release.yml exactly.
A step of a managed YAML key that uses the same action at another commit or tag,
as a Dependabot pull request leaves it, now reads as unchanged: check passes and
update keeps the newer version. While the key is still what repokeeper wrote,
update moves the pin as before. Any other change still counts, and so does the
ref a job calls a reusable workflow at, a local action or a Docker image.

So the publish job no longer makes dependabot.yml ignore its actions, which also
stopped Dependabot from bumping them in the repository's own workflows.
@vannt-dev
vannt-dev force-pushed the feat/publish-module branch from e2b5499 to 3987e52 Compare October 20, 2026 15:46
@vannt-dev
vannt-dev merged commit a50597c into main Oct 20, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant