Skip to content

Commit 64f804d

Browse files
committed
fix(ci): make npm publish idempotent per version
1 parent 94e1666 commit 64f804d

1 file changed

Lines changed: 46 additions & 1 deletion

File tree

.github/workflows/npm-publish.yml

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
tags:
88
- "v*"
99

10+
concurrency:
11+
group: npm-publish-${{ github.event.release.tag_name || github.ref_name }}
12+
cancel-in-progress: false
13+
1014
env:
1115
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
1216
NOTE_CONNECTION_SBOM_ATTESTATION_KEY_ID: ${{ secrets.SBOM_SIGNING_KEY_ID }}
@@ -40,19 +44,48 @@ jobs:
4044
registry-url: "https://registry.npmjs.org"
4145
cache: "npm"
4246

47+
- name: Resolve package metadata
48+
id: package_meta
49+
shell: bash
50+
run: |
51+
PACKAGE_NAME="$(node -p "require('./package.json').name")"
52+
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
53+
echo "name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
54+
echo "version=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
55+
echo "Package: ${PACKAGE_NAME}@${PACKAGE_VERSION}"
56+
57+
- name: Check npm version existence (idempotent publish guard)
58+
id: npm_guard
59+
shell: bash
60+
run: |
61+
PACKAGE_NAME="${{ steps.package_meta.outputs.name }}"
62+
PACKAGE_VERSION="${{ steps.package_meta.outputs.version }}"
63+
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
64+
echo "already_published=true" >> "$GITHUB_OUTPUT"
65+
echo "Skipping publish because ${PACKAGE_NAME}@${PACKAGE_VERSION} already exists on npm."
66+
else
67+
echo "already_published=false" >> "$GITHUB_OUTPUT"
68+
echo "Version ${PACKAGE_NAME}@${PACKAGE_VERSION} does not exist yet. Continue publish workflow."
69+
fi
70+
4371
- name: Install dependencies
72+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
4473
run: npm ci
4574

4675
- name: Build
76+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
4777
run: npm run build
4878

4979
- name: Generate release SBOM
80+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
5081
run: npm run generate:sbom
5182

5283
- name: Verify SBOM policy gate
84+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
5385
run: npm run verify:sbom -- --strict 1
5486

5587
- name: Validate SBOM signing key pair configuration
88+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
5689
shell: bash
5790
run: |
5891
KEY_ID="$(printf '%s' "${NOTE_CONNECTION_SBOM_ATTESTATION_KEY_ID}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
@@ -81,14 +114,15 @@ jobs:
81114
fi
82115
83116
- name: Generate SBOM attestation
117+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
84118
env:
85119
NOTE_CONNECTION_SBOM_ATTESTATION_ALLOW_UNSIGNED: ${{ env.NOTE_CONNECTION_SBOM_SIGNING_PRIVATE_KEY_PEM == '' }}
86120
NOTE_CONNECTION_SBOM_ATTESTATION_ENABLE_TRANSPARENCY_LOG: ${{ env.NOTE_CONNECTION_SBOM_SIGNING_PRIVATE_KEY_PEM != '' }}
87121
NOTE_CONNECTION_SBOM_ATTESTATION_TRANSPARENCY_LOG_PATH: "build/sbom/attestation-transparency-log.jsonl"
88122
run: npm run generate:sbom:attestation
89123

90124
- name: Materialize SBOM signing keyring policy (optional)
91-
if: ${{ env.NOTE_CONNECTION_SBOM_SIGNING_KEYRING_JSON != '' }}
125+
if: ${{ steps.npm_guard.outputs.already_published != 'true' && env.NOTE_CONNECTION_SBOM_SIGNING_KEYRING_JSON != '' }}
92126
shell: bash
93127
env:
94128
SBOM_SIGNING_KEYRING_JSON: ${{ env.NOTE_CONNECTION_SBOM_SIGNING_KEYRING_JSON }}
@@ -97,6 +131,7 @@ jobs:
97131
printf '%s' "${SBOM_SIGNING_KEYRING_JSON}" > build/sbom/signing-keyring.json
98132
99133
- name: Verify SBOM attestation policy gate
134+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
100135
env:
101136
NOTE_CONNECTION_REQUIRE_SBOM_ATTESTATION_SIGNATURE: ${{ env.NOTE_CONNECTION_SBOM_SIGNING_PUBLIC_KEY_PEM != '' }}
102137
NOTE_CONNECTION_SBOM_ATTESTATION_REQUIRE_SIGNED_KEY_ID: ${{ env.NOTE_CONNECTION_SBOM_SIGNING_PUBLIC_KEY_PEM != '' }}
@@ -119,20 +154,30 @@ jobs:
119154
run: npm run verify:sbom:attestation -- --strict 1 --allow-missing 0
120155

121156
- name: Enforce strict PathBridge inbound schema gate
157+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
122158
run: npm run verify:pathbridge:strict
123159

124160
- name: Enforce strict wasm parity gates
161+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
125162
run: npm run test:wasm:parity:gates
126163

127164
- name: Verify sidecar signing gate contract
165+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
128166
run: npm run verify:sidecar:signatures -- --contract-only
129167

130168
- name: Run tests
169+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
131170
# Release CI is intentionally serialized here because the parallel Jest
132171
# worker pool can trigger broken stdout pipes in spawned Node CLIs.
133172
run: npx jest --runInBand
134173

135174
- name: Publish to npm
175+
if: ${{ steps.npm_guard.outputs.already_published != 'true' }}
136176
run: npm publish
137177
env:
138178
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
179+
180+
- name: Skip publish summary
181+
if: ${{ steps.npm_guard.outputs.already_published == 'true' }}
182+
run: |
183+
echo "npm publish skipped: ${{ steps.package_meta.outputs.name }}@${{ steps.package_meta.outputs.version }} already exists."

0 commit comments

Comments
 (0)