Repository navigation
196 lines (183 loc) · 8.53 KB
/
Copy pathpublish.yml
File metadata and controls
196 lines (183 loc) · 8.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
name: Publish to npm
# Publishes @gsa-sam/sam-styles to the public npm registry.
#
# Trigger: a full GitHub Release is published (activity type `released`,
# which points at a version tag, e.g. v3.1.0). Manual dispatch is
# rehearsal-only and must run in dry-run mode.
#
# Why `released` and not `published`: `published` also fires for
# pre-releases (e.g. v3.1.0-beta.1). Because we publish with npm's default
# `latest` dist-tag, a pre-release firing `published` would land as
# `latest` and be picked up by every consumer by default — not what we
# want. `released` fires only for full, non-prerelease Releases, so it
# structurally prevents an accidental beta-as-latest publish. Revisit with
# explicit dist-tag handling if we ever want beta channels.
#
# Why key on Release (not a raw `push: tags`): publishing a Release is a
# deliberate, auditable act. It gives DevSecOps a clear HITL approval point
# and pairs with the `release` environment's required reviewers. A raw
# tag push is quiet and unauthenticated by comparison, so the live
# `npm publish` only happens on a published *Release*.
#
# Security model (locked-down access):
# - Runs only in the `release` GitHub Actions environment, which
# DevSecOps can gate with required reviewers / protection rules.
# - Uses npm Trusted Publishing (OIDC) — no long-lived token stored in
# the repo. The `id-token: write` permission mints a short-lived
# credential that npm trusts because this repo + workflow filename are
# registered as the package's Trusted Publisher.
# (npmjs.com → package → Settings → Trusted Publisher:
# Publisher = GitHub Actions
# Organization = GSA, Repository = sam-styles
# Workflow filename = publish.yml
# Environment name = release)
# - Quality gates (lint + tests + SCSS compile) must pass before publish.
# - Values that come from outside the workflow file (`vars.DRY_RUN`,
# `inputs.dry-run`, `github.event_name`) are passed into `run` steps
# through `env:` and read as quoted shell variables, never interpolated
# with `${{ }}` directly into a script body. `${{ }}` expressions are
# substituted into the generated script *text* before bash parses it,
# so a value containing shell metacharacters would execute as code —
# in the one job holding `id-token: write` for npm Trusted Publishing.
#
# HITL handoff to DevSecOps:
# Until the Trusted Publisher connection is live, Release-triggered runs
# publish with `npm publish --dry-run` only (DRY_RUN defaults to "true").
# DevSecOps:
# 1. Registers the Trusted Publisher on npmjs (values above).
# 2. Enables Release-triggered live publishing by setting the
# repo/environment variable DRY_RUN = false (Settings → Secrets and
# variables → Actions → Variables), or edits the default below.
# Manual workflow_dispatch runs are always rehearsal-only, regardless of
# DRY_RUN, and cannot perform a live publish.
#
# Note on workflow_dispatch rehearsals: a manual dry-run against an
# already-published version (e.g. the previously released 3.0.25) will verify the
# tarball and THEN fail with npm's "cannot publish over previously
# published version" error. That's expected — the packaging check still
# ran. Bump to an unpublished version to see a fully clean dry-run.
on:
release:
types: [released]
workflow_dispatch:
inputs:
dry-run:
description: "Run npm publish in --dry-run mode (no upload)"
type: boolean
default: true
# Least-privilege at the top level; the publish job opts into id-token.
permissions:
contents: read
jobs:
# Reuse the existing lint/build quality gates before publishing.
quality-gates:
uses: ./.github/workflows/build.yml
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: ".nvmrc"
- name: Install dependencies
run: npm ci
- name: Run tests (stylelint)
run: npm test
- name: SCSS compilation check
run: npm run compile:check
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
- name: Run Storybook smoke tests
run: npm run test:storybook
publish:
needs: [quality-gates, test]
runs-on: ubuntu-latest
environment: release
permissions:
contents: read
id-token: write # required for npm Trusted Publishing (OIDC)
steps:
- name: Check out repository code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: npm ci
# Trusted Publishing needs npm >= 11.5.1. Pin the floor rather than
# pulling an unpinned @latest on this security-sensitive job.
- name: Ensure npm supports Trusted Publishing
run: |
NPM_MIN="11.5.1"
CURRENT="$(npm --version)"
if [ "$(printf '%s\n' "$NPM_MIN" "$CURRENT" | sort -V | head -n1)" != "$NPM_MIN" ]; then
echo "npm $CURRENT < $NPM_MIN; upgrading"
npm install -g "npm@$NPM_MIN"
else
echo "npm $CURRENT satisfies >= $NPM_MIN"
fi
- name: Verify tag matches package.json version
if: github.event_name == 'release'
run: |
# Strip an optional leading "v" (v3.1.0 -> 3.1.0); other prefixes fail.
TAG="${GITHUB_REF_NAME#v}"
PKG="$(node -p "require('./package.json').version")"
echo "Release tag: $GITHUB_REF_NAME (normalized: $TAG) package.json: $PKG"
if [ "$TAG" != "$PKG" ]; then
echo "::error::Release tag ($GITHUB_REF_NAME) does not match package.json version ($PKG). Tags must be <version> or v<version>."
exit 1
fi
- name: Determine dry-run mode
id: mode
env:
# NEVER interpolate these with `${{ }}` inside the script body:
# expression interpolation is substituted into the script *text*
# before bash parses it, so a DRY_RUN value such as
# `true"; echo PWNED; #` would execute before the boolean
# validation below could reject it. This job holds
# `id-token: write` for npm Trusted Publishing, so the blast
# radius is publish credentials. Read them as quoted shell
# variables instead.
EVENT_NAME: ${{ github.event_name }}
DISPATCH_DRY_RUN: ${{ inputs.dry-run }}
VARS_DRY_RUN: ${{ vars.DRY_RUN }}
run: |
# workflow_dispatch is rehearsal-only. Only a published Release may
# perform a live publish, controlled by repo/env variable DRY_RUN.
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
DRY="$DISPATCH_DRY_RUN"
if [ "$DRY" != "true" ]; then
echo "::error::workflow_dispatch runs must use dry-run=true. Publish a GitHub Release to run a live publish."
exit 1
fi
else
DRY="$VARS_DRY_RUN"
fi
# Default to the safe mode when unset.
DRY="${DRY:-true}"
# Normalize case so True/FALSE/etc. behave predictably.
DRY="$(echo "$DRY" | tr '[:upper:]' '[:lower:]')"
# Fail loudly on anything that isn't an explicit boolean, so a
# release never silently no-ops (neither dry-run nor live).
if [ "$DRY" != "true" ] && [ "$DRY" != "false" ]; then
echo "::error::DRY_RUN must be 'true' or 'false' (got '$DRY')"
exit 1
fi
echo "dry-run=$DRY" >> "$GITHUB_OUTPUT"
echo "Publish dry-run mode: $DRY"
- name: Publish (dry-run)
if: steps.mode.outputs['dry-run'] == 'true'
run: npm publish --dry-run --access public
# Live publish via OIDC Trusted Publishing. No token env is set — npm
# mints a short-lived OIDC credential trusted because this repo +
# workflow are registered as the package's Trusted Publisher. This is
# the only supported publish path; there is no long-lived-token fallback.
- name: Publish (live, OIDC Trusted Publishing)
if: github.event_name == 'release' && steps.mode.outputs['dry-run'] == 'false'
run: npm publish --access public