|
| 1 | +# Implements [CLI-INSTALL-HOMEBREW], [CLI-INSTALL-SCOOP], [CLI-INSTALL-DOTNET-TOOL] |
1 | 2 | name: Release |
2 | 3 |
|
3 | 4 | # Tag-triggered Shipwright release. Implements [SWR-REL-WORKFLOW], [SWR-REL-GITHUB]. |
@@ -407,16 +408,139 @@ jobs: |
407 | 408 | - uses: actions/setup-node@v4 |
408 | 409 | with: |
409 | 410 | node-version: 22 |
| 411 | + # Install vsce ONCE at a pinned, known-replicated version. `npx @vscode/vsce` |
| 412 | + # re-resolves the package on every call, so a transient npm `latest`-tag replication |
| 413 | + # race (ETARGET) on a single iteration can abort the whole publish mid-loop and |
| 414 | + # strand some platforms. One install, one binary, reused for all targets. |
| 415 | + - name: Install vsce (pinned) |
| 416 | + run: npm install -g @vscode/vsce@3.9.1 |
410 | 417 | - name: Download all per-platform VSIXs |
411 | 418 | uses: actions/download-artifact@v4 |
412 | 419 | with: |
413 | 420 | path: vsix-artifacts |
414 | 421 | pattern: vsix-* |
415 | 422 | merge-multiple: true |
416 | | - - name: Publish all platforms in one atomic call |
417 | | - run: npx @vscode/vsce publish --packagePath $(find vsix-artifacts -name '*.vsix' | tr '\n' ' ') |
| 423 | + # One `vsce publish` per platform VSIX: vsce silently uses only the FIRST when several |
| 424 | + # are passed in a single call (the previous single-call step published only one |
| 425 | + # platform). Each --target VSIX MUST be published on its own. The publish is |
| 426 | + # IDEMPOTENT — a target whose (version, platform) is already on the Marketplace |
| 427 | + # ("already exists") counts as success, so a re-run after a partial publish completes |
| 428 | + # the remaining platforms instead of aborting on the first duplicate. Transient errors |
| 429 | + # retry up to 3x; one failed platform never blocks the others. |
| 430 | + # Salvaged from repo-standardization@319159f. Implements [SWR-VSIX-PUBLISH]. |
| 431 | + - name: Publish each platform VSIX |
418 | 432 | env: |
419 | 433 | VSCE_PAT: ${{ secrets.VSCE_PAT }} |
| 434 | + run: | |
| 435 | + set -uo pipefail |
| 436 | + shopt -s globstar nullglob |
| 437 | + flag="" |
| 438 | + if [[ "${GITHUB_REF_NAME}" == *-* ]]; then |
| 439 | + flag="--pre-release" |
| 440 | + echo "Prerelease tag ${GITHUB_REF_NAME}; publishing with --pre-release" |
| 441 | + fi |
| 442 | + publish_one() { |
| 443 | + local vsix="$1" attempt out rc |
| 444 | + for attempt in 1 2 3; do |
| 445 | + out="$(vsce publish ${flag} --packagePath "${vsix}" 2>&1)"; rc=$? |
| 446 | + echo "${out}" |
| 447 | + if [ "${rc}" -eq 0 ]; then return 0; fi |
| 448 | + if echo "${out}" | grep -qiE "already exists"; then |
| 449 | + echo "→ ${vsix} already on Marketplace; treating as published." |
| 450 | + return 0 |
| 451 | + fi |
| 452 | + echo "→ attempt ${attempt} failed (rc=${rc}); retrying in $((attempt*10))s..." |
| 453 | + sleep $((attempt*10)) |
| 454 | + done |
| 455 | + return 1 |
| 456 | + } |
| 457 | + published=0; failed=0 |
| 458 | + for vsix in vsix-artifacts/**/*.vsix; do |
| 459 | + echo "Publishing ${vsix}" |
| 460 | + if publish_one "${vsix}"; then |
| 461 | + published=$((published + 1)) |
| 462 | + else |
| 463 | + echo "::error::Failed to publish ${vsix} after retries" |
| 464 | + failed=$((failed + 1)) |
| 465 | + fi |
| 466 | + done |
| 467 | + if [ "${published}" -eq 0 ]; then |
| 468 | + echo "::error::No VSIX artifacts found to publish" |
| 469 | + exit 1 |
| 470 | + fi |
| 471 | + echo "Published/confirmed ${published} VSIX(es); ${failed} failed." |
| 472 | + [ "${failed}" -eq 0 ] |
| 473 | +
|
| 474 | + # ── Publish per-platform VSIXs to the Open VSX Registry ──────── [SWR-VSIX-PUBLISH] |
| 475 | + # Open VSX serves the VS Code FORKS — Cursor, Windsurf, VSCodium, Gitpod, Eclipse |
| 476 | + # Theia, Antigravity — none of which can reach the Microsoft Marketplace. Fully |
| 477 | + # independent of publish-marketplace: the Open VSX push must not be gated on the MS |
| 478 | + # Marketplace publish, and vice versa. The GitHub Release + Homebrew + Scoop ship |
| 479 | + # regardless, so a missing OVSX token can NEVER block the native-binary release — |
| 480 | + # only the Open VSX publish waits. Implements [SWR-SEC-OIDC-PUBLISH] (per-channel). |
| 481 | + publish-openvsx: |
| 482 | + name: Publish to Open VSX Registry |
| 483 | + needs: [validate-tag, package-vsix] |
| 484 | + if: ${{ !cancelled() && needs.package-vsix.result != 'skipped' }} |
| 485 | + runs-on: ubuntu-latest |
| 486 | + timeout-minutes: 10 |
| 487 | + # Least privilege: this job only downloads same-run artifacts and pushes to an |
| 488 | + # external registry, so it drops the inherited top-level `contents: write` to |
| 489 | + # read-only. Open VSX has no OIDC/trusted-publishing path, so no `id-token` is |
| 490 | + # granted. Implements [SWR-SEC-TOKEN-PRIVILEGE]. |
| 491 | + permissions: |
| 492 | + contents: read |
| 493 | + actions: read |
| 494 | + steps: |
| 495 | + # Turn Open VSX's opaque auth failure (what you get from an empty/blank PAT) |
| 496 | + # into an actionable, operator-facing error before anything else runs. The |
| 497 | + # GitHub Release, Marketplace, Homebrew, and Scoop do NOT depend on this job, |
| 498 | + # so a missing token only stalls the Open VSX publish. |
| 499 | + - name: Require Open VSX credential |
| 500 | + env: |
| 501 | + OVSX_PAT: ${{ secrets.OPEN_VSX_PAT }} |
| 502 | + run: | |
| 503 | + set -euo pipefail |
| 504 | + if [ -z "${OVSX_PAT:-}" ]; then |
| 505 | + echo "::error title=OPEN_VSX_PAT secret is not set::Add an Open VSX access token as the repo (or Nimblesite org) secret OPEN_VSX_PAT, then re-run, to publish the per-platform VSIXs to Open VSX. The GitHub Release (with all VSIX + CLI assets), VS Code Marketplace, Homebrew, and Scoop already shipped independently. Create a token at https://open-vsx.org/user-settings/tokens and create the publisher namespace once with: npx ovsx create-namespace nimblesite -p \$OVSX_PAT" |
| 506 | + exit 1 |
| 507 | + fi |
| 508 | + echo "OPEN_VSX_PAT present — proceeding with Open VSX publish." |
| 509 | + - uses: actions/setup-node@v4 |
| 510 | + with: |
| 511 | + node-version: 22 |
| 512 | + - name: Download all per-platform VSIXs |
| 513 | + uses: actions/download-artifact@v4 |
| 514 | + with: |
| 515 | + path: vsix-artifacts |
| 516 | + pattern: vsix-* |
| 517 | + merge-multiple: true |
| 518 | + - name: Publish every per-platform VSIX to Open VSX |
| 519 | + # The token is exposed ONLY as an env var, never on the command line: ovsx |
| 520 | + # reads OVSX_PAT automatically when -p is omitted, keeping the secret out of |
| 521 | + # the process argv (where a `ps`/dump could read it). ovsx is version-pinned — |
| 522 | + # a floating `npx ovsx` would fetch and run the latest release at publish time, |
| 523 | + # inside the very job that holds the token (a supply-chain risk). Bump it |
| 524 | + # deliberately. Implements [SWR-SEC-FROZEN-INSTALL]. |
| 525 | + env: |
| 526 | + OVSX_PAT: ${{ secrets.OPEN_VSX_PAT }} |
| 527 | + run: | |
| 528 | + set -euo pipefail |
| 529 | + shopt -s nullglob |
| 530 | + # One publish per platform-specific VSIX: the target is baked into each |
| 531 | + # VSIX, so no --target flag is needed, but each must be pushed separately — |
| 532 | + # a single glob into one call would publish only the first. |
| 533 | + published=0 |
| 534 | + for vsix in vsix-artifacts/*.vsix; do |
| 535 | + echo "Publishing $vsix to Open VSX" |
| 536 | + npx --yes ovsx@1.0.0 publish --packagePath "$vsix" |
| 537 | + published=$((published + 1)) |
| 538 | + done |
| 539 | + if [ "$published" -eq 0 ]; then |
| 540 | + echo "::error::No VSIX artifacts found to publish to Open VSX" |
| 541 | + exit 1 |
| 542 | + fi |
| 543 | + echo "Published $published VSIX(es) to Open VSX" |
420 | 544 |
|
421 | 545 | # ── SECONDARY, best-effort dotnet-tool NuGet package ────────────────────────── |
422 | 546 | # continue-on-error + NOT a dependency of release / marketplace / brew / scoop, so a |
|
0 commit comments