Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog 2.0.0](https://keepachangelog.com/en/2.

## [Unreleased]

### Changed

- **Breaking:** R-PUB-02 requires Maven Central credentials to become available only after a native forge publication gate. A USER_MANAGED Portal stop alone no longer passes because the same user token can publish through the Portal API.
- `oss-publish` configures one publication gate by default. It uses npm staged approval for one package, a forge gate for packages released together, and a forge gate with automatic Maven Central publication. It preserves existing extra gates until the maintainer accepts a migration.
- GitLab release examples use one protected blocking manual job by default. Deployment approval rules remain an optional extra gate because GitLab also requires a manual job start after approval.

### Fixed

- npm staged publication examples use `./package/*.tgz`, so npm reads the values as local tarball paths.
- PyPI and pub.dev examples use the `release` environment name consistently.

## [0.12.0] - 2026-08-04

### Changed
Expand Down
4 changes: 2 additions & 2 deletions skills/oss-audit/STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ Forges: both

A long-lived registry token in CI secrets is the single credential that turns any workflow compromise into a supply-chain compromise. Trusted publishing exchanges a short-lived OIDC token per run, so there is nothing to steal between releases.

Check: the publish job requests `id-token: write` and publishes through the registry's OIDC flow (npm trusted publishing, PyPI trusted publishers, RubyGems OIDC, crates.io trusted publishing). Where the registry's own publishing documentation names no OIDC flow at all, which is the case for Hex, a token scoped to the single package being published is below the bar and permitted, and the registry limitation is reported beside it. That is not the same as a registry documenting a flow this standard has not enumerated, which is unverified rather than absent: read the registry's documentation and report the rule unknown until the flow is checked, because the scoped-token fallback would otherwise pass a repository that could have used OIDC. Where the registry documents no credential narrower than the publishing account, which is the case for Maven Central, an account-scoped token is below the bar and permitted, reported beside the compensating controls that keep it below the bar rather than at it: an expiry set when the token is generated, revocation and replacement on compromise, namespace ownership verified against the account, and a publishing type that holds a validated deployment until a person releases it. Sonatype has announced namespace-scoped and artifact-scoped tokens without shipping them, so re-read the registry's token documentation before accepting this fallback rather than treating it as settled. A repository that publishes to no registry, shipping only built assets on a forge release, falls outside this rule rather than failing it.
Check: the publish job requests `id-token: write` and publishes through the registry's OIDC flow (npm trusted publishing, PyPI trusted publishers, RubyGems OIDC, crates.io trusted publishing). Where the registry's own publishing documentation names no OIDC flow at all, which is the case for Hex, a token scoped to the single package being published is below the bar and permitted, and the registry limitation is reported beside it. That is not the same as a registry documenting a flow this standard has not enumerated, which is unverified rather than absent: read the registry's documentation and report the rule unknown until the flow is checked, because the scoped-token fallback would otherwise pass a repository that could have used OIDC. Where the registry documents no credential narrower than the publishing account, which is the case for Maven Central, an account-scoped token is below the bar and permitted. Report the compensating controls that keep it below the bar rather than at it: an expiry set when the token is generated, revocation and replacement on compromise, namespace ownership verified against the account, and credentials available only to the forge job after its native publication gate passes. Sonatype has announced namespace-scoped and artifact-scoped tokens without shipping them, so re-read the registry's token documentation before accepting this fallback rather than treating it as settled. A repository that publishes to no registry, shipping only built assets on a forge release, falls outside this rule rather than failing it.

Fixed by: oss-publish
Forges: both
Expand All @@ -427,7 +427,7 @@ Forges: both

A registry publish cannot be undone. An approval gate is the last point where a compromised tag, a wrong version, or a bad artifact can be stopped.

Check: before public availability, a person other than an automation account must approve through a GitHub environment with required reviewers, a GitLab protected environment with a manual job and approval rules, or a registry proof-of-presence gate such as npm staged publishing with 2FA approval. Verify the configured gate through the forge or registry API. If the repository visibility or forge plan does not provide a native gate and the registry has no equivalent, report the rule as unmet rather than substituting an unverified approval action.
Check: before public availability, a person other than an automation account must approve through a GitHub environment with required reviewers, a GitLab protected environment with a blocking manual job or deployment approval rules, or a registry proof-of-presence gate such as npm staged publishing with 2FA approval. Verify the configured gate through the forge or registry API. If the repository visibility or forge plan does not provide a native gate and the registry has no equivalent, report the rule as unmet rather than substituting an unverified approval action.

Fixed by: oss-publish
Forges: both
Expand Down
17 changes: 12 additions & 5 deletions skills/oss-audit/rule-sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,18 @@
"https://github.com/hexpm/hexpm/blob/main/lib/hexpm_web/controllers/api/release_controller.ex",
"https://central.sonatype.org/publish/generate-portal-token/",
"https://central.sonatype.org/publish/publish-portal-api/",
"https://central.sonatype.org/publish/publish-portal-maven/",
"https://central.sonatype.org/register/namespace/",
"https://central.sonatype.org/news/20250326_ossrh_sunset/",
"https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments",
"https://docs.gitlab.com/ci/environments/protected_environments/",
"https://docs.gitlab.com/ci/jobs/job_control/",
"https://github.com/hexpm/hex/blob/main/lib/mix/tasks/hex.user.ex",
"https://github.com/hexpm/hex/blob/main/lib/hex/state.ex",
"https://github.com/hexpm/hexpm/blob/main/lib/hexpm_web/controllers/dashboard/key_controller.ex"
],
"verified": "2026-07-31",
"note": "All four registries confirm id-token: write and short-lived tokens replacing long-lived ones; the RubyGems guide is explicit that the permission belongs at job level, workflow level discouraged. crates.io is client-rendered and returns only a shell to a fetcher, so it has to be read in a browser, not by fetching; read that way it is complete and shipped. It documents both GitHub and GitLab, GitLab still in public beta, tokens expiring after 30 minutes, and an initial publish that still requires an API token because the crate must already exist. The GitHub path is the rust-lang/crates-io-auth-action@v1 action; the GitLab path is a hand-rolled curl exchange against /api/v1/trusted_publishing/tokens with an id_tokens block, which is worth knowing before a check assumes an action exists on both. Hex is the checked negative, and it is why the Check now separates a registry that offers no flow from one this project has not verified. Its publish page names exactly one CI path, a long-lived HEX_API_KEY in an environment variable, and the hex client changelog through v2.5.2-dev mentions no OIDC, trusted publishing, or attestation, though it does add an OAuth device flow in v2.4.0. The scoped-token fallback is reachable there, but through the hex.pm dashboard rather than through the client: the hexpm release controller still authorizes :publish on domains [{\"api\", \"write\"}, \"package\"] gated by package_owner, so a key scoped package:<name> publishes that one package and nothing else, and the Packages checkboxes on the /dashboard/keys create form are what issues one. Do not repeat the mix hex.user key generate command the hex.pm publish page gives: Hex 2.4.0 replaced password authentication with an OAuth device flow and removed that subcommand, lib/mix/tasks/hex.user.ex now declares @switches [] and dispatches whoami, auth, and deauth alone, and the publish page was not updated. Reported as hexpm/hexpm#1787 with the fix in #1788, and the correction was read from the client source on 2026-07-31 rather than from that page. HEX_API_KEY itself is unchanged and still read by the client as api_key in lib/hex/state.ex. hexpm/hexpm#1785, feat: trusted publisher, is open and would retire this whole clause for Hex. The three Hex sources were read on 2026-07-31; the four registry sources above were last read on the verified date. The account-scoped clause was added on 2026-07-31 for Maven Central, which is the second checked negative and a different one from Hex: Hex documents no OIDC flow but does scope a key to one package, while Sonatype documents neither. Its Central Portal authenticates with a user token carrying a display name and an expiry and nothing else, and the token-generation, API, namespace, and credential-management pages are all silent on scope, which is what makes the absence a reading rather than an inference. The compensating controls the clause names are Sonatype's own: an expiry set at generation, revocation and replacement documented as the compromise path, namespace ownership verified by DNS record or a forge-hosted repository, and USER_MANAGED as the default publishingType, which holds a validated deployment until a person releases it. The clause is written to expire: the OSSRH sunset roadmap of 2025-03-26 lists tokens 'scoped to specific namespaces, groupIds, or groupId and artifactId combinations' as planned, and two of that entry's three bullets have since shipped while the scope bullet has not. Re-read the token page before accepting the fallback, because a web search will report scoped tokens as available by restating that roadmap in the present tense."
"verified": "2026-08-07",
"note": "npm, PyPI, RubyGems, and crates.io document OIDC publishing with short-lived credentials. Hex documents no OIDC flow. Its dashboard can issue a key for one package, as confirmed in the hexpm release controller and key controller. Hex 2.4.0 removed mix hex.user key generate, although the publish page still names it. hexpm/hexpm#1785 would retire the Hex fallback if trusted publishing ships. Maven Central also documents no OIDC flow and no artifact-scoped or namespace-scoped token. Sonatype documents token expiry, revocation, namespace ownership, and autoPublish for CI. The Portal API uses the same user token for upload and for the publish endpoint. A USER_MANAGED deployment therefore provides an optional review stop, but it does not protect publication from CI that still holds that token. The compensating control requires the Central token and signing secret to become available only after a native forge gate. GitHub environment secrets become available after environment approval. GitLab protected environments can limit deployment variables and blocking manual jobs to authorized users. Sonatype's 2025 roadmap lists scoped tokens as planned. Re-read the token page before accepting this fallback."
},
"R-PUB-03": {
"sources": [
Expand All @@ -320,10 +324,13 @@
"R-PUB-04": {
"sources": [
"https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments",
"https://docs.gitlab.com/ci/environments/deployment_approvals/"
"https://docs.gitlab.com/ci/environments/deployment_approvals/",
"https://docs.gitlab.com/ci/environments/protected_environments/",
"https://docs.gitlab.com/ci/jobs/job_control/",
"https://docs.npmjs.com/staged-publishing/"
],
"verified": "2026-07-28",
"note": "GitHub confirms required reviewers gate the job and that 'Prevent self-review' is a separate opt-in setting, which is why the rule says a person other than the triggering automation. GitLab's deployment approvals invert that default in the project's favour: 'All jobs deploying to the environment are blocked and wait for approvals before running', and the person who started the pipeline cannot approve their own deployment unless an administrator enables 'Allow pipeline triggerer to approve deployment'. npm staged publishing has no source named."
"verified": "2026-08-07",
"note": "GitHub confirms that required reviewers gate a job and that Prevent self-review is a separate setting. GitLab supports two native gates. A protected environment can restrict who runs a blocking manual deployment job. Deployment approval rules block the deployment, but an approved job still needs a separate manual start. npm staged publishing keeps a package unavailable until a person approves its stage with 2FA. Each stage has its own stage ID and approval command, so a release flow with several packages needs several npm approvals. R-PUB-04 requires at least one verified gate. Extra gates remain valid."
},
"R-PUB-05": {
"sources": [
Expand Down
14 changes: 12 additions & 2 deletions skills/oss-publish/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ A registry's documentation is not proof that a command still exists. Before hand

When a registry's documentation and its client disagree, that is a defect in the registry, not a puzzle to work around. Name which two sources disagree and what you ran to establish it. Do not improvise a substitute credential path, do not extract an undocumented on-disk token, and do not pin around the version where it last worked. Offer to report it upstream, and write the issue with `oss-writing`.

Present policy choices instead of guessing at them, even when the repository already contains a release workflow or a stored token. Which cooldown to use, whether to keep an existing publish step, and how much of an existing workflow to carry over are decisions the user makes.
Configure one publication gate by default. Additional gates remain valid, but they add another human action to every release. In an established release flow, preserve every configured gate, report the human actions it requires, and recommend the default path. Change those gates only after the maintainer accepts the migration.

Present other policy choices instead of guessing at them, even when the repository already contains a release workflow or a stored token. Which cooldown to use, whether to keep an existing publish step, and how much of an existing workflow to carry over are decisions the user makes.

## The shape of a release workflow

Expand Down Expand Up @@ -56,6 +58,8 @@ Detect what the repository ships, not what it merely contains. A manifest presen

Collect, from the manifest and the repository, before changing anything: the package, gem, or crate name and version; the owner and repository, from the manifest's own repository or source metadata first, falling back to the git remote; repository visibility and forge plan, since approval protection is not available on every plan; whether the owner is an organization or a personal account; whether the package is already published, since an unpublished package may need a different first-release path; the existing tag format from `git tag --sort=-creatordate | head`, keeping whatever format is already in use; and any existing release workflow or pipeline, especially one referencing a stored registry token, which this skill's changes should remove.

Identify each release flow. One tag or release event can publish several packages when they version and ship together. Independently tagged packages are separate flows. Inventory every forge and registry approval each flow already requires before proposing a change.

Detect the forge the same way `oss-ci` does: look for `.github/workflows/` or `.gitlab-ci.yml`, check the git remote host, or ask directly if neither signal is present. If the user states the forge explicitly, trust that over any signal found in the repository.

Route to the matching reference file now, using the table at the end of this file. If what the repository ships matches none of the eleven rows there, say so plainly: name the ecosystem found and state that this skill has no reference file for it, rather than improvising a publishing flow for a registry nobody has read the documentation of.
Expand All @@ -74,7 +78,13 @@ Trigger the workflow on a version tag, matching the format found in Step 1. Befo

### Step 4: Gate on manual approval with two-factor authentication

Pin the publish job to a GitHub environment with required reviewers, or a GitLab protected environment with a manual job and approval rules, naming at least one approver who is not an automation account. Check availability before writing the workflow; the reference file for this registry names the plan each gate needs. Do not simulate a missing native gate with an unverified third-party approval action. Where the registry offers its own proof-of-presence gate, such as npm staged publishing with two-factor approval, use it as an additional gate and as the documented fallback when the forge plan has no native approval gate. For another registry on a plan without a native gate, report that R-PUB-04 cannot be satisfied until the repository visibility or plan changes.
Choose the last enforced publication gate that covers the complete release flow. A registry gate qualifies only when the CI identity cannot approve its own staged artifact. It must also approve every package in the flow with one human action. npm staged publishing meets both conditions for a flow containing one package.

Where a registry gate qualifies, use it as the default. Keep any forge environment binding that forms part of the publisher identity, but configure no required reviewer there. Where no registry gate qualifies, pin the publish job to a GitHub environment with required reviewers. On GitLab, use a protected environment with one blocking manual job. Set `when: manual` and `allow_failure: false`, and restrict deployment permission to at least one person who is not an automation account.

Check feature availability before writing the workflow. The ecosystem reference names the plan each gate needs. Do not simulate a missing native gate with an unverified third-party approval action. If the forge plan provides no native gate and the registry has no qualifying gate, report R-PUB-04 as unmet.

Additional gates still satisfy R-PUB-04. If a release flow already has more than one, report each human action and suggest the one-gate default. Do not remove or weaken an established gate until the maintainer accepts that change.

### Step 5: Verify provenance after the first release

Expand Down
Loading
Loading