Skip to content

Commit ddbf255

Browse files
authored
Merge pull request #30 from link-foundation/issue-29-7f70f0d87db9
fix(ci): detect npm publish failures, split releases per language, surface credential runbooks
2 parents 41decfd + d40bf6a commit ddbf255

20 files changed

Lines changed: 8762 additions & 59 deletions

.github/workflows/csharp.yml

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -240,14 +240,40 @@ jobs:
240240
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
241241
run: |
242242
if [ -z "$NUGET_API_KEY" ]; then
243-
echo "::error::NUGET_API_KEY is not set; cannot publish to NuGet."
244-
echo "Configure NUGET_API_KEY as a repository secret. See docs/case-studies/issue-25/README.md."
243+
echo "::error title=NUGET_API_KEY missing::Cannot publish to NuGet without an API key."
244+
echo ""
245+
echo "How to fix:"
246+
echo " 1. Generate a key at https://www.nuget.org/account/apikeys"
247+
echo " (scope: 'Push new packages and package versions', glob pattern: package id)."
248+
echo " 2. Add it to this repo at:"
249+
echo " Settings -> Secrets and variables -> Actions -> New repository secret"
250+
echo " Name: NUGET_API_KEY"
251+
echo " 3. Re-run this workflow."
252+
echo ""
253+
echo "See docs/case-studies/issue-29/README.md (credentials) and"
254+
echo " docs/case-studies/issue-25/README.md (publishing pipeline)."
255+
exit 1
256+
fi
257+
if ! OUT=$(dotnet nuget push artifacts/*.nupkg \
258+
--api-key "$NUGET_API_KEY" \
259+
--source https://api.nuget.org/v3/index.json \
260+
--skip-duplicate 2>&1); then
261+
echo "$OUT"
262+
if echo "$OUT" | grep -qiE 'unauthorized|forbidden|403|401|api ?key'; then
263+
echo "::error title=NuGet credentials rejected::NuGet rejected NUGET_API_KEY."
264+
echo ""
265+
echo "How to fix:"
266+
echo " 1. The key may have expired or been revoked. Rotate it at"
267+
echo " https://www.nuget.org/account/apikeys"
268+
echo " 2. Update the secret at:"
269+
echo " Settings -> Secrets and variables -> Actions -> NUGET_API_KEY"
270+
echo " 3. Verify the key's package glob includes this package id."
271+
echo ""
272+
echo "See docs/case-studies/issue-29/README.md."
273+
fi
245274
exit 1
246275
fi
247-
dotnet nuget push artifacts/*.nupkg \
248-
--api-key "$NUGET_API_KEY" \
249-
--source https://api.nuget.org/v3/index.json \
250-
--skip-duplicate
276+
echo "$OUT"
251277
252278
- name: Create GitHub Release
253279
if: steps.version_check.outputs.should_release == 'true'
@@ -313,14 +339,40 @@ jobs:
313339
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
314340
run: |
315341
if [ -z "$NUGET_API_KEY" ]; then
316-
echo "::error::NUGET_API_KEY is not set; cannot publish to NuGet."
317-
echo "Configure NUGET_API_KEY as a repository secret. See docs/case-studies/issue-25/README.md."
342+
echo "::error title=NUGET_API_KEY missing::Cannot publish to NuGet without an API key."
343+
echo ""
344+
echo "How to fix:"
345+
echo " 1. Generate a key at https://www.nuget.org/account/apikeys"
346+
echo " (scope: 'Push new packages and package versions', glob pattern: package id)."
347+
echo " 2. Add it to this repo at:"
348+
echo " Settings -> Secrets and variables -> Actions -> New repository secret"
349+
echo " Name: NUGET_API_KEY"
350+
echo " 3. Re-run this workflow."
351+
echo ""
352+
echo "See docs/case-studies/issue-29/README.md (credentials) and"
353+
echo " docs/case-studies/issue-25/README.md (publishing pipeline)."
354+
exit 1
355+
fi
356+
if ! OUT=$(dotnet nuget push artifacts/*.nupkg \
357+
--api-key "$NUGET_API_KEY" \
358+
--source https://api.nuget.org/v3/index.json \
359+
--skip-duplicate 2>&1); then
360+
echo "$OUT"
361+
if echo "$OUT" | grep -qiE 'unauthorized|forbidden|403|401|api ?key'; then
362+
echo "::error title=NuGet credentials rejected::NuGet rejected NUGET_API_KEY."
363+
echo ""
364+
echo "How to fix:"
365+
echo " 1. The key may have expired or been revoked. Rotate it at"
366+
echo " https://www.nuget.org/account/apikeys"
367+
echo " 2. Update the secret at:"
368+
echo " Settings -> Secrets and variables -> Actions -> NUGET_API_KEY"
369+
echo " 3. Verify the key's package glob includes this package id."
370+
echo ""
371+
echo "See docs/case-studies/issue-29/README.md."
372+
fi
318373
exit 1
319374
fi
320-
dotnet nuget push artifacts/*.nupkg \
321-
--api-key "$NUGET_API_KEY" \
322-
--source https://api.nuget.org/v3/index.json \
323-
--skip-duplicate
375+
echo "$OUT"
324376
325377
- name: Create GitHub Release
326378
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'

.github/workflows/js.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,14 +254,14 @@ jobs:
254254
env:
255255
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
256256
working-directory: ./js
257-
run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}"
257+
run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix "js-v"
258258

259259
- name: Format GitHub release notes
260260
if: steps.publish.outputs.published == 'true'
261261
env:
262262
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
263263
working-directory: ./js
264-
run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}"
264+
run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix "js-v"
265265

266266
# Manual Instant Release - triggered via workflow_dispatch with instant mode
267267
instant-release:
@@ -316,14 +316,14 @@ jobs:
316316
env:
317317
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
318318
working-directory: ./js
319-
run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}"
319+
run: node scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix "js-v"
320320

321321
- name: Format GitHub release notes
322322
if: steps.publish.outputs.published == 'true'
323323
env:
324324
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
325325
working-directory: ./js
326-
run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}"
326+
run: node scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix "js-v"
327327

328328
# Manual Changeset PR - creates a pull request with the changeset for review
329329
changeset-pr:

.github/workflows/python.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ jobs:
233233
path: python/dist/
234234

235235
- name: Publish to PyPI
236+
id: pypi_publish
236237
if: steps.version_check.outputs.should_release == 'true'
237238
# Uses PyPI's OIDC Trusted Publisher flow (id-token: write below). If you see "Trusted
238239
# Publisher … not configured" in the logs, configure it for this repo + workflow on PyPI.
@@ -243,6 +244,50 @@ jobs:
243244
verbose: true
244245
skip-existing: true
245246

247+
- name: Diagnose PyPI publish failure
248+
if: failure() && steps.pypi_publish.outcome == 'failure'
249+
run: |
250+
echo "::error title=PyPI publish failed::pypa/gh-action-pypi-publish exited with an error."
251+
echo ""
252+
echo "Most common causes & fixes:"
253+
echo " 1. 'Trusted publisher … not configured' / 'invalid-publisher':"
254+
echo " PyPI does not yet trust this repository+workflow. Configure it at:"
255+
echo " https://pypi.org/manage/project/${{ steps.version_check.outputs.package_name }}/settings/publishing/"
256+
echo " Match: owner=${{ github.repository_owner }}, repo=$(basename ${{ github.repository }}),"
257+
echo " workflow=python.yml, environment=(blank unless you set one)."
258+
echo ""
259+
echo " 2. Token-based auth in use but PYPI_API_TOKEN missing/expired:"
260+
echo " Generate at https://pypi.org/manage/account/token/ and store as a repo secret."
261+
echo ""
262+
echo " 3. id-token: write missing on the job (this workflow already has it)."
263+
echo ""
264+
echo " 4. Version already published: skip-existing is true so PyPI conflicts are tolerated;"
265+
echo " a hard failure here means something else is wrong."
266+
echo ""
267+
echo "Docs: https://docs.pypi.org/trusted-publishers/"
268+
echo " docs/case-studies/issue-29/README.md"
269+
exit 1
270+
271+
- name: Verify package on PyPI
272+
if: steps.version_check.outputs.should_release == 'true' && steps.pypi_publish.outcome == 'success'
273+
run: |
274+
PKG="${{ steps.version_check.outputs.package_name }}"
275+
VER="${{ steps.version_check.outputs.current_version }}"
276+
# PyPI's CDN can lag a few seconds behind a successful upload.
277+
for i in 1 2 3 4 5; do
278+
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PKG}/${VER}/json")
279+
echo "Attempt $i: PyPI HTTP status for ${PKG}@${VER}: ${STATUS}"
280+
if [ "$STATUS" = "200" ]; then
281+
echo "✅ Verified ${PKG}@${VER} is on PyPI"
282+
exit 0
283+
fi
284+
sleep 5
285+
done
286+
echo "::error title=PyPI verification failed::${PKG}@${VER} is not on PyPI after publish."
287+
echo "The publish step reported success but the registry does not see the version."
288+
echo "See docs/case-studies/issue-29/README.md for the runbook."
289+
exit 1
290+
246291
- name: Create GitHub Release
247292
if: steps.version_check.outputs.should_release == 'true'
248293
env:
@@ -315,7 +360,16 @@ jobs:
315360
working-directory: ./python
316361
run: twine check dist/*
317362

363+
- name: Read package name from pyproject.toml
364+
id: pkg
365+
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
366+
working-directory: ./python
367+
run: |
368+
PACKAGE_NAME=$(grep -Po '(?<=^name = ")[^"]*' pyproject.toml | head -1)
369+
echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
370+
318371
- name: Publish to PyPI
372+
id: pypi_publish
319373
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
320374
# See note above: relies on PyPI Trusted Publisher; see docs/case-studies/issue-25/README.md.
321375
uses: pypa/gh-action-pypi-publish@release/v1
@@ -324,6 +378,49 @@ jobs:
324378
verbose: true
325379
skip-existing: true
326380

381+
- name: Diagnose PyPI publish failure
382+
if: failure() && steps.pypi_publish.outcome == 'failure'
383+
run: |
384+
echo "::error title=PyPI publish failed::pypa/gh-action-pypi-publish exited with an error."
385+
echo ""
386+
echo "Most common causes & fixes:"
387+
echo " 1. 'Trusted publisher … not configured' / 'invalid-publisher':"
388+
echo " PyPI does not yet trust this repository+workflow. Configure it at:"
389+
echo " https://pypi.org/manage/project/${{ steps.pkg.outputs.package_name }}/settings/publishing/"
390+
echo " Match: owner=${{ github.repository_owner }}, repo=$(basename ${{ github.repository }}),"
391+
echo " workflow=python.yml, environment=(blank unless you set one)."
392+
echo ""
393+
echo " 2. Token-based auth in use but PYPI_API_TOKEN missing/expired:"
394+
echo " Generate at https://pypi.org/manage/account/token/ and store as a repo secret."
395+
echo ""
396+
echo " 3. id-token: write missing on the job (this workflow already has it)."
397+
echo ""
398+
echo " 4. Version already published: skip-existing is true so PyPI conflicts are tolerated;"
399+
echo " a hard failure here means something else is wrong."
400+
echo ""
401+
echo "Docs: https://docs.pypi.org/trusted-publishers/"
402+
echo " docs/case-studies/issue-29/README.md"
403+
exit 1
404+
405+
- name: Verify package on PyPI
406+
if: (steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true') && steps.pypi_publish.outcome == 'success'
407+
run: |
408+
PKG="${{ steps.pkg.outputs.package_name }}"
409+
VER="${{ steps.version.outputs.new_version }}"
410+
for i in 1 2 3 4 5; do
411+
STATUS=$(curl -sS -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PKG}/${VER}/json")
412+
echo "Attempt $i: PyPI HTTP status for ${PKG}@${VER}: ${STATUS}"
413+
if [ "$STATUS" = "200" ]; then
414+
echo "✅ Verified ${PKG}@${VER} is on PyPI"
415+
exit 0
416+
fi
417+
sleep 5
418+
done
419+
echo "::error title=PyPI verification failed::${PKG}@${VER} is not on PyPI after publish."
420+
echo "The publish step reported success but the registry does not see the version."
421+
echo "See docs/case-studies/issue-29/README.md for the runbook."
422+
exit 1
423+
327424
- name: Create GitHub Release
328425
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
329426
env:

.github/workflows/rust.yml

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,18 @@ jobs:
336336
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }}
337337
run: |
338338
if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
339-
echo "::error::Neither CARGO_REGISTRY_TOKEN nor CARGO_TOKEN is set; cannot publish to crates.io."
340-
echo "Configure one of these as a repository secret. See docs/case-studies/issue-25/README.md."
339+
echo "::error title=CARGO_REGISTRY_TOKEN missing::Cannot publish to crates.io without an API token."
340+
echo ""
341+
echo "How to fix:"
342+
echo " 1. Generate a token at https://crates.io/me (Account Settings -> API Tokens)."
343+
echo " Scope: 'publish-update' for this crate."
344+
echo " 2. Add it to this repo at:"
345+
echo " Settings -> Secrets and variables -> Actions -> New repository secret"
346+
echo " Name: CARGO_REGISTRY_TOKEN (CARGO_TOKEN is also accepted as a fallback)"
347+
echo " 3. Re-run this workflow."
348+
echo ""
349+
echo "See docs/case-studies/issue-29/README.md (credentials) and"
350+
echo " docs/case-studies/issue-25/README.md (publishing pipeline)."
341351
exit 1
342352
fi
343353
# `cargo publish` exits non-zero on retry if the version already exists; we tolerate that
@@ -349,6 +359,18 @@ jobs:
349359
echo "::warning::Version was published by another run between probe and publish; treating as success."
350360
exit 0
351361
fi
362+
if echo "$OUT" | grep -qiE 'unauthorized|forbidden|403|401|invalid token|token (expired|rejected)'; then
363+
echo "::error title=crates.io credentials rejected::crates.io rejected CARGO_REGISTRY_TOKEN."
364+
echo ""
365+
echo "How to fix:"
366+
echo " 1. The token may have expired or been revoked. Rotate it at"
367+
echo " https://crates.io/me (Account Settings -> API Tokens)"
368+
echo " 2. Update the secret at:"
369+
echo " Settings -> Secrets and variables -> Actions -> CARGO_REGISTRY_TOKEN"
370+
echo " 3. Verify the token's scope includes 'publish-update' for this crate."
371+
echo ""
372+
echo "See docs/case-studies/issue-29/README.md."
373+
fi
352374
exit 1
353375
fi
354376
echo "$OUT"
@@ -419,8 +441,18 @@ jobs:
419441
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }}
420442
run: |
421443
if [ -z "$CARGO_REGISTRY_TOKEN" ]; then
422-
echo "::error::Neither CARGO_REGISTRY_TOKEN nor CARGO_TOKEN is set; cannot publish to crates.io."
423-
echo "Configure one of these as a repository secret. See docs/case-studies/issue-25/README.md."
444+
echo "::error title=CARGO_REGISTRY_TOKEN missing::Cannot publish to crates.io without an API token."
445+
echo ""
446+
echo "How to fix:"
447+
echo " 1. Generate a token at https://crates.io/me (Account Settings -> API Tokens)."
448+
echo " Scope: 'publish-update' for this crate."
449+
echo " 2. Add it to this repo at:"
450+
echo " Settings -> Secrets and variables -> Actions -> New repository secret"
451+
echo " Name: CARGO_REGISTRY_TOKEN (CARGO_TOKEN is also accepted as a fallback)"
452+
echo " 3. Re-run this workflow."
453+
echo ""
454+
echo "See docs/case-studies/issue-29/README.md (credentials) and"
455+
echo " docs/case-studies/issue-25/README.md (publishing pipeline)."
424456
exit 1
425457
fi
426458
if ! OUT=$(cargo publish 2>&1); then
@@ -429,6 +461,18 @@ jobs:
429461
echo "::warning::Version is already on crates.io; treating manual re-run as success."
430462
exit 0
431463
fi
464+
if echo "$OUT" | grep -qiE 'unauthorized|forbidden|403|401|invalid token|token (expired|rejected)'; then
465+
echo "::error title=crates.io credentials rejected::crates.io rejected CARGO_REGISTRY_TOKEN."
466+
echo ""
467+
echo "How to fix:"
468+
echo " 1. The token may have expired or been revoked. Rotate it at"
469+
echo " https://crates.io/me (Account Settings -> API Tokens)"
470+
echo " 2. Update the secret at:"
471+
echo " Settings -> Secrets and variables -> Actions -> CARGO_REGISTRY_TOKEN"
472+
echo " 3. Verify the token's scope includes 'publish-update' for this crate."
473+
echo ""
474+
echo "See docs/case-studies/issue-29/README.md."
475+
fi
432476
exit 1
433477
fi
434478
echo "$OUT"

0 commit comments

Comments
 (0)