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
5 changes: 5 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ inputs:
description: Node.js version to install via actions/setup-node. Reads .nvmrc when omitted.
required: false
default: ""
setup-node:
description: Set to "false" when the job installs Node.js itself, as the npm publish job does.
required: false
default: "true"

runs:
using: composite
steps:
- name: Setup Node.js
if: ${{ inputs.setup-node != 'false' }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ inputs.node-version }}
Expand Down
78 changes: 57 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
# and CHANGELOG.md entry are generated from Conventional Commit messages since the last
# release (feat -> minor, fix -> patch, `!`/`BREAKING CHANGE:` -> major). Nothing is
# published yet. A maintainer reviews and merges that PR: that merge is confirmation #1.
# 2. Merging the release PR makes release-please tag the release commit and create a GitHub
# Release, which triggers this same workflow again. This time `release_created` is `true`,
# so the `publish-npm` job builds, validates and STAGES the package on npm
# (`npm stage publish --provenance`). A staged version is not installable until a
# 2. Merging the release PR makes release-please tag the release commit and create a draft
# GitHub Release, which triggers this same workflow again. This time `release_created` is
# `true`: the `release-assets` job attaches the tarball, its signed provenance and the SBOM to
# the draft and publishes it, and the `publish-npm` job builds, validates and STAGES the
# package on npm (`npm stage publish --provenance`). A staged version is not installable until a
# maintainer approves it with 2FA, on npmjs.com (package -> Staged versions) or with
# `npm stage approve <stage-id>`. That approval is confirmation #2 (npm's
# "proof-of-presence"), and the trusted publisher is configured for staged publishing only,
Expand Down Expand Up @@ -88,10 +89,19 @@ jobs:
ref: ${{ needs.release-please.outputs.tag_name }}
persist-credentials: false

# setup-node runs in the workflow itself, not in the composite action, with the npm registry
# URL npm's trusted publishing guide uses: OpenSSF Scorecard (Packaging) only recognizes an
# npm publishing job by this step next to the publish command.
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Setup
uses: ./.github/actions/setup
with:
node-version: 24
setup-node: "false"

- name: Install dependencies
run: npm ci
Expand Down Expand Up @@ -157,14 +167,18 @@ jobs:
- name: Publish to JSR
run: deno publish

sbom:
name: Record the SBOM of the release
release-assets:
name: Attach the signed package to the release
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
permissions:
contents: read
contents: write
id-token: write
attestations: write
env:
TAG: ${{ needs.release-please.outputs.tag_name }}

steps:
- name: Checkout the release tag
Expand All @@ -175,18 +189,40 @@ jobs:

- name: Setup
uses: ./.github/actions/setup
with:
node-version: 24

- name: Generate the CycloneDX SBOM of the published package
# The package has no runtime dependencies, so the SBOM describes the package itself;
# --package-lock-only reads the lockfile instead of installing anything.
run: npm sbom --sbom-format cyclonedx --omit dev --package-lock-only > brazilian-utils.cdx.json
- name: Install dependencies
run: npm ci

- name: Run build
run: npm run build

# Releases are immutable in this repository, so an asset cannot be added after release-please
# creates one (HTTP 422 on 2.4.0); the SBOM is kept as an artifact of the release run instead,
# next to the provenance statement npm records for the package.
- name: Keep the SBOM as an artifact of the release run
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- name: Pack the package as npm publishes it
id: pack
# The SBOM goes in first because `files` in package.json ships it inside the tarball.
run: |
npm sbom --sbom-format cyclonedx --omit dev --package-lock-only > brazilian-utils.cdx.json
echo "tarball=$(npm pack --silent)" >> "$GITHUB_OUTPUT"

- name: Attest the build provenance of the tarball
id: attest
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
name: sbom-${{ needs.release-please.outputs.tag_name }}
path: brazilian-utils.cdx.json
if-no-files-found: error
subject-path: ${{ steps.pack.outputs.tarball }}

# release-please creates the release as a draft (release-please-config.json) because releases
# are immutable in this repository: assets can only be added before the release is published.
# The Sigstore bundle can be checked with `gh attestation verify`; the .intoto.jsonl file holds
# the same signed SLSA provenance as a DSSE envelope, the form OpenSSF Scorecard
# (Signed-Releases) looks for.
- name: Attach the tarball, its provenance and the SBOM, then publish the release
env:
GH_TOKEN: ${{ github.token }}
TARBALL: ${{ steps.pack.outputs.tarball }}
BUNDLE: ${{ steps.attest.outputs.bundle-path }}
run: |
head -n 1 "$BUNDLE" | jq . > "$TARBALL.sigstore.json"
jq -c .dsseEnvelope "$BUNDLE" > "$TARBALL.intoto.jsonl"
gh release upload "$TAG" "$TARBALL" "$TARBALL.sigstore.json" "$TARBALL.intoto.jsonl" brazilian-utils.cdx.json --repo "$GITHUB_REPOSITORY"
gh release edit "$TAG" --draft=false --latest --repo "$GITHUB_REPOSITORY"
16 changes: 9 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,11 +385,12 @@ public signatures are pinned by the `describe("<name> types")` blocks in the tes
permissions, branch protection, code review, dependency updates, SAST) rather than the code,
publishes the score and uploads the findings to the Security tab.
- Every release ships `brazilian-utils.cdx.json` inside the package, a CycloneDX SBOM generated
with `npm sbom` from the release tag right before staging on npm, and keeps the same file as a
workflow artifact (`sbom-<tag>`); releases are immutable here, so the file cannot be attached to
the release itself. The package has no
runtime dependencies, so the document describes the package itself; it exists for consumers
whose supply-chain policy requires one.
with `npm sbom` from the release tag right before staging on npm, and attaches the same file to
the GitHub Release. The package has no runtime dependencies, so the document describes the
package itself; it exists for consumers whose supply-chain policy requires one.
- Releases are immutable here, so release-please creates each one as a draft: the
`release-assets` job attaches the packed tarball, its signed build provenance (a Sigstore bundle
and the same DSSE envelope as `.intoto.jsonl`) and the SBOM, then publishes the release.
- Commit messages are checked with commitlint on every pull request, since release-please derives
the version bump and the changelog from them.
- The URLs cited in the Markdown files and in the `@see` tags of the source are checked by hand
Expand Down Expand Up @@ -520,8 +521,9 @@ There are no local release commands to run.
hidden.
2. A maintainer reviews the release PR (version bump, changelog) and merges it. **Merging the
release PR is the first confirmation.** Nothing is published yet at this point.
3. Merging tags the release and publishes a GitHub Release, which triggers the `publish-npm` job in
`.github/workflows/release.yml`. That job builds and validates the package and **stages** it on
3. Merging tags the release and creates a draft GitHub Release, which triggers the
`release-assets` and `publish-npm` jobs in `.github/workflows/release.yml`. The first attaches
the signed tarball and the SBOM to the draft and publishes it; the second builds and validates the package and **stages** it on
npm with `npm stage publish --provenance` (npm Trusted Publishing/OIDC; no npm token is stored
in the repository). A staged version is not installable yet.
4. A maintainer approves the staged version with 2FA, on npmjs.com (package → Staged versions) or
Expand Down
10 changes: 7 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,13 @@ It verifies the registry signature and the provenance attestation of every insta
The attestation, with the commit and the workflow run that built the version, is also shown on the
["Provenance" panel of the version on npmjs.com](https://www.npmjs.com/package/@brazilian-utils/brazilian-utils?activeTab=versions).
A CycloneDX SBOM of each version ships inside the package as `brazilian-utils.cdx.json` (so it
sits in `node_modules/@brazilian-utils/brazilian-utils/` after install) and is also kept as the
`sbom-<tag>` artifact of its release run under
[Actions → Release](https://github.com/brazilian-utils/javascript/actions/workflows/release.yml).
sits in `node_modules/@brazilian-utils/brazilian-utils/` after install) and is also attached to
its [GitHub Release](https://github.com/brazilian-utils/javascript/releases), next to the packed
tarball and its signed build provenance. To check a tarball downloaded from the release:

```bash
gh attestation verify brazilian-utils-brazilian-utils-<version>.tgz --repo brazilian-utils/javascript
```

## Secrets and credentials

Expand Down
2 changes: 2 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"include-v-in-tag": false,
"include-component-in-tag": false,
"changelog-path": "CHANGELOG.md",
"draft": true,
"force-tag-creation": true,
"bump-minor-pre-major": false,
"changelog-sections": [
{ "type": "feat", "section": "Features" },
Expand Down
Loading