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+
1014env :
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 }}
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