|
| 1 | +# Release |
| 2 | + |
| 3 | +How a new version of `@socketregistry/packageurl-js` gets cut and |
| 4 | +shipped to npm with provenance. Read this before running a release |
| 5 | +or changing anything under `.github/workflows/provenance.yml`, |
| 6 | +`scripts/publish.mts`, or `scripts/bump.mts`. |
| 7 | + |
| 8 | +## Who this is for |
| 9 | + |
| 10 | +Maintainers cutting a release and contributors curious about what |
| 11 | +happens after a PR lands on main. You do not run `npm publish` |
| 12 | +directly in this repo — the workflow does it on your behalf. |
| 13 | + |
| 14 | +## The shape of a release |
| 15 | + |
| 16 | +Three things happen, in order: |
| 17 | + |
| 18 | +``` |
| 19 | + ┌───────────────────────────────────────────────────────────────┐ |
| 20 | + │ 1. bump version — local or via CI │ |
| 21 | + │ • pnpm bump <patch|minor|major|prerelease> │ |
| 22 | + │ • updates package.json + CHANGELOG │ |
| 23 | + │ • commits the bump │ |
| 24 | + └───────────────────┬──────────────────────────────────────────┘ |
| 25 | + │ |
| 26 | + ▼ |
| 27 | + ┌───────────────────────────────────────────────────────────────┐ |
| 28 | + │ 2. trigger the Publish workflow — manual dispatch │ |
| 29 | + │ GitHub → Actions → 📦 Publish → Run workflow │ |
| 30 | + │ Inputs: │ |
| 31 | + │ • dist-tag (latest | next | beta | canary | …) │ |
| 32 | + │ • debug (0 | 1) │ |
| 33 | + └───────────────────┬──────────────────────────────────────────┘ |
| 34 | + │ |
| 35 | + ▼ |
| 36 | + ┌───────────────────────────────────────────────────────────────┐ |
| 37 | + │ 3. the workflow (calling the fleet's shared provenance.yml): │ |
| 38 | + │ ci:validate → pnpm check + build + coverage │ |
| 39 | + │ release:ci → npm publish --provenance │ |
| 40 | + │ Socket.dev → post-publish malware audit │ |
| 41 | + │ Sigstore → attestation published │ |
| 42 | + └───────────────────────────────────────────────────────────────┘ |
| 43 | +``` |
| 44 | + |
| 45 | +Nothing publishes without passing all checks. The `prepublishOnly` |
| 46 | +hook in `package.json` throws on local `npm publish` attempts so |
| 47 | +you cannot accidentally ship from a laptop. |
| 48 | + |
| 49 | +## The workflow file |
| 50 | + |
| 51 | +`.github/workflows/provenance.yml` is a thin delegation to the |
| 52 | +fleet-shared workflow at |
| 53 | +`SocketDev/socket-registry/.github/workflows/provenance.yml`. The |
| 54 | +shared workflow does the heavy lifting; this file just wires up |
| 55 | +repo-specific inputs: |
| 56 | + |
| 57 | +```yaml |
| 58 | +with: |
| 59 | + debug: ${{ inputs.debug }} |
| 60 | + dist-tag: ${{ inputs.dist-tag }} |
| 61 | + package-name: '@socketregistry/packageurl-js' |
| 62 | + publish-script: 'release:ci' |
| 63 | + setup-script: 'ci:validate' |
| 64 | + use-trusted-publishing: true |
| 65 | +secrets: |
| 66 | + SOCKET_API_KEY: ${{ secrets.SOCKET_API_TOKEN }} |
| 67 | +``` |
| 68 | +
|
| 69 | +Key choices: |
| 70 | +
|
| 71 | +- **`use-trusted-publishing: true`** — npm recognizes this |
| 72 | + repository as an OIDC-trusted publisher. No long-lived `NPM_TOKEN` |
| 73 | + secret is stored anywhere; the GitHub Actions runner presents an |
| 74 | + OIDC token, npm verifies it, and the publish proceeds. A leaked |
| 75 | + token in the repo history cannot be used to publish. |
| 76 | +- **`setup-script: 'ci:validate'`** — runs `pnpm check` in a clean |
| 77 | + env before the publish starts. Publication aborts if any check |
| 78 | + fails. |
| 79 | +- **`publish-script: 'release:ci'`** — the npm script the workflow |
| 80 | + invokes to actually publish. Wraps `npm publish --provenance`. |
| 81 | + |
| 82 | +### The dist-tag input |
| 83 | + |
| 84 | +| Tag | Use | |
| 85 | +|---|---| |
| 86 | +| `latest` | Default. Production release for most consumers. | |
| 87 | +| `next` | Release candidate before cutting `latest`. | |
| 88 | +| `beta` | Pre-release on a feature branch. | |
| 89 | +| `canary` | Experimental / daily / short-lived. | |
| 90 | +| `backport` | Patches to older major lines. | |
| 91 | + |
| 92 | +npm shows the `latest` tag by default; other tags require |
| 93 | +`npm install <pkg>@<tag>`. Picking the wrong tag (e.g. `latest` for |
| 94 | +a breaking pre-release) breaks every downstream consumer that did |
| 95 | +not pin. Triple-check the dist-tag on the dispatch form. |
| 96 | + |
| 97 | +## Version bump commands |
| 98 | + |
| 99 | +```bash |
| 100 | +# Patch release (bug fixes) |
| 101 | +pnpm bump patch |
| 102 | +
|
| 103 | +# Minor release (backward-compatible features) |
| 104 | +pnpm bump minor |
| 105 | +
|
| 106 | +# Major release (breaking changes) |
| 107 | +pnpm bump major |
| 108 | +
|
| 109 | +# Pre-release |
| 110 | +pnpm bump prerelease --preid=beta |
| 111 | +``` |
| 112 | + |
| 113 | +`pnpm bump` runs `scripts/bump.mts`. It: |
| 114 | + |
| 115 | +1. Reads the current version from `package.json`. |
| 116 | +2. Computes the next version per semver rules. |
| 117 | +3. Updates `package.json`. |
| 118 | +4. Updates `CHANGELOG.md` (prepending a section). |
| 119 | +5. Commits the change with a message |
| 120 | + `chore(release): v<new-version>`. |
| 121 | +6. Does **not** tag — tagging happens as part of the workflow. |
| 122 | + |
| 123 | +You review and push the bump commit like any other change. The |
| 124 | +Publish workflow is a manual dispatch after the bump is on main. |
| 125 | + |
| 126 | +## Provenance — what it means on the npm side |
| 127 | + |
| 128 | +Every release ships with a **provenance attestation**: an Sigstore- |
| 129 | +signed statement that says "this tarball was built from commit |
| 130 | +`<sha>` of `<repo>` by `<workflow>` at `<time>`." Consumers can |
| 131 | +verify via: |
| 132 | + |
| 133 | +```bash |
| 134 | +npm install @socketregistry/packageurl-js |
| 135 | +npm audit signatures |
| 136 | +``` |
| 137 | + |
| 138 | +or |
| 139 | + |
| 140 | +```bash |
| 141 | +# Inspect the attestation on npm |
| 142 | +npm view @socketregistry/packageurl-js dist.provenance |
| 143 | +``` |
| 144 | + |
| 145 | +A release without a valid attestation either (a) was cut before the |
| 146 | +fleet adopted provenance, or (b) was published out-of-band and |
| 147 | +should be treated with suspicion. The `provenance.yml` path is the |
| 148 | +**only** supported release path for new versions. |
| 149 | + |
| 150 | +## The Socket.dev post-publish audit |
| 151 | + |
| 152 | +The shared provenance workflow runs a Socket.dev scan on the |
| 153 | +published tarball immediately after `npm publish`. If Socket flags |
| 154 | +malware or a critical issue in the published artifact, the workflow |
| 155 | +fails loudly — you'll get a notification, and the release row in |
| 156 | +GitHub Actions goes red. npm keeps the version (publish is |
| 157 | +irrevocable) but the audit trail captures the alert. |
| 158 | + |
| 159 | +This is belt-and-suspenders: Socket checks the dependency closure |
| 160 | +pre-publish via `ci:validate`, but the post-publish audit catches |
| 161 | +anything that slipped in between "CI passed" and "tarball on |
| 162 | +registry" (re-bundle side-effects, mis-configured files, etc.). |
| 163 | + |
| 164 | +## What lands in the published tarball |
| 165 | + |
| 166 | +``` |
| 167 | +@socketregistry/packageurl-js-<version>.tgz |
| 168 | +├── package.json |
| 169 | +├── CHANGELOG.md |
| 170 | +├── dist/ |
| 171 | +│ ├── index.js + index.d.ts |
| 172 | +│ ├── package-url.js + .d.ts |
| 173 | +│ ├── … (every compiled module) |
| 174 | +│ ├── purl-types/<ecosystem>.js + .d.ts |
| 175 | +│ └── (no sourcemaps — we don't ship them) |
| 176 | +└── data/npm/ |
| 177 | + ├── builtin-names.json |
| 178 | + └── legacy-names.json |
| 179 | +``` |
| 180 | +
|
| 181 | +Controlled by `package.json`'s `files` field: |
| 182 | +
|
| 183 | +```json |
| 184 | +"files": [ |
| 185 | + "dist/**/*", |
| 186 | + "data/**/*.json", |
| 187 | + "CHANGELOG.md" |
| 188 | +] |
| 189 | +``` |
| 190 | + |
| 191 | +Things explicitly NOT shipped: |
| 192 | + |
| 193 | +- `src/` — TypeScript sources. |
| 194 | +- `test/` — test files. |
| 195 | +- `scripts/`, `.config/`, `.github/`, `.claude/` — tooling. |
| 196 | +- `docs/` — rendered as the tour site, not shipped to npm. |
| 197 | +- Any sourcemap or tsbuildinfo. |
| 198 | + |
| 199 | +A CI check (`scripts/ci-validate.mts`) asserts the packed tarball |
| 200 | +size stays under a sanity threshold so an accidental `dist/` |
| 201 | +explosion or a misconfigured `files` field fails loudly. |
| 202 | + |
| 203 | +## Common release scenarios |
| 204 | + |
| 205 | +### Cutting a normal patch release |
| 206 | + |
| 207 | +1. PRs with bug fixes merge to main. |
| 208 | +2. `pnpm bump patch` on a branch; commit + PR + merge. |
| 209 | +3. In GitHub, dispatch the Publish workflow with `dist-tag: latest`. |
| 210 | +4. Wait for green. Verify the new version on npmjs.com. |
| 211 | + |
| 212 | +### Cutting a pre-release for testing |
| 213 | + |
| 214 | +1. `pnpm bump prerelease --preid=beta` on a branch; commit + PR + |
| 215 | + merge to main (or to a release branch, if you're working off one). |
| 216 | +2. Dispatch Publish with `dist-tag: beta`. |
| 217 | +3. Consumers test with `npm install @socketregistry/packageurl-js@beta`. |
| 218 | +4. When satisfied, `pnpm bump <appropriate-level>` to drop the |
| 219 | + pre-release tag and dispatch again with `dist-tag: latest`. |
| 220 | + |
| 221 | +### Patching an older major |
| 222 | + |
| 223 | +1. Checkout the last-known-good commit on the older major |
| 224 | + (`v1.x.x`). |
| 225 | +2. `git checkout -b backport-fix-xyz`. |
| 226 | +3. Apply the fix, test, PR into a `release/1.x` branch. |
| 227 | +4. `pnpm bump patch` on that branch. |
| 228 | +5. Dispatch Publish with `dist-tag: backport` (or a specific tag |
| 229 | + like `1.x-latest`) to avoid bumping the `latest` tag. |
| 230 | + |
| 231 | +### Emergency revert |
| 232 | + |
| 233 | +npm does not let you delete a published version after 72 hours. For |
| 234 | +emergencies: |
| 235 | + |
| 236 | +1. Publish a patch release that reverts the bad change. |
| 237 | +2. Use `npm deprecate @socketregistry/packageurl-js@<bad-version> |
| 238 | + "<reason>"` to mark the bad version deprecated (visible in |
| 239 | + installs, but not removed). |
| 240 | +3. Announce in the repo's CHANGELOG and any Socket channels. |
| 241 | + |
| 242 | +Do not use `npm unpublish`; it is a last resort and has messy |
| 243 | +consequences (integrity mismatches, dependency confusion risks). |
| 244 | + |
| 245 | +## Hazards |
| 246 | + |
| 247 | +- **Dispatching with the wrong `dist-tag`.** Promoting a |
| 248 | + pre-release to `latest` by accident breaks every un-pinned |
| 249 | + consumer. Read the tag twice before clicking Run. |
| 250 | +- **Publishing from a non-clean tree.** The workflow runs |
| 251 | + `ci:validate`, but if a bump commit bundles unrelated changes, |
| 252 | + those ship too. Keep bump commits single-purpose. |
| 253 | +- **`release:ci` vs `publish:ci` script name drift.** The workflow |
| 254 | + calls `release:ci`. Confirm `package.json` has that script name; |
| 255 | + otherwise the workflow falls over with "script not found." (If |
| 256 | + renamed, update either this file and the workflow, or the |
| 257 | + script, in the same PR.) |
| 258 | +- **Trusted publisher misconfig.** If the npm-side trusted-publisher |
| 259 | + config for this repo is removed or the workflow file is renamed, |
| 260 | + publishes will fail with a 403. Check npm's trusted publisher |
| 261 | + settings first. |
| 262 | +- **OIDC token lifetime.** The workflow's OIDC-to-npm exchange |
| 263 | + happens in a narrow window. If a retry lands outside it, re- |
| 264 | + dispatch rather than trying to edit tokens. |
| 265 | + |
| 266 | +## Checklist before dispatching a release |
| 267 | + |
| 268 | +- [ ] Main is green on CI. |
| 269 | +- [ ] Bump commit is on main and builds + tests pass locally. |
| 270 | +- [ ] `CHANGELOG.md` describes every user-visible change in the |
| 271 | + bumped version. |
| 272 | +- [ ] `dist-tag` chosen is correct for the kind of release |
| 273 | + (latest / next / beta / canary / backport). |
| 274 | +- [ ] If this is a breaking major, the migration notes are in the |
| 275 | + CHANGELOG. |
| 276 | +- [ ] No uncommitted changes to `package.json`, `pnpm-lock.yaml`, |
| 277 | + or `data/`. |
| 278 | + |
| 279 | +## Further reading |
| 280 | + |
| 281 | +- [`docs/contributing.md`](./contributing.md) — the pre-PR |
| 282 | + workflow that must land cleanly before any release is possible. |
| 283 | +- [`docs/hardening.md`](./hardening.md) — the hostile-input |
| 284 | + posture that makes Socket.dev post-publish scans meaningful. |
| 285 | +- [`scripts/publish.mts`](../scripts/publish.mts) — the local |
| 286 | + publish script (mainly for CI; `prepublishOnly` blocks local |
| 287 | + runs). |
| 288 | +- [`scripts/bump.mts`](../scripts/bump.mts) — version-bumping |
| 289 | + logic. |
| 290 | +- [`.github/workflows/provenance.yml`](../.github/workflows/provenance.yml) |
| 291 | + — the wrapper workflow. |
| 292 | +- [npm docs: provenance](https://docs.npmjs.com/generating-provenance-statements) |
| 293 | + — upstream documentation on the attestation format. |
| 294 | +- [sigstore.dev](https://www.sigstore.dev/) — the signing / |
| 295 | + transparency service. |
0 commit comments