From ff752e8e67fa011a5f72a125740c8321a5084cd2 Mon Sep 17 00:00:00 2001 From: madhur310 Date: Mon, 26 Oct 2026 10:51:58 -0700 Subject: [PATCH 1/5] fix(release): migrate Open VSX publishing to OIDC trusted publishing - W-24666922 openvsx-publish-release-vsix.yml and vscode-promote-prerelease.yml both ran `ovsx publish` against a PAT (-p $OVSX_PAT / $IDEE_OVSX_PAT). Switches both to `ovsx publish --trusted-publishing`, which exchanges this job's GitHub Actions OIDC ID token for a short-lived publish token instead -- requires `id-token: write`, which both workflows now grant. Open VSX's trusted-publisher config binds one identity per extension to the exact workflow file that requests the OIDC token (its job_workflow_ref). vscode-promote-prerelease.yml's `ovsx` leg previously ran its own `ovsx publish` inline, inside the same matrix job as `vsce` -- a different file from openvsx-publish-release-vsix.yml, which the stable release path uses. That would mean two different job_workflow_ref values for the same extension's Open VSX publishes, which a single trusted-publisher entry can't match. Split the `ovsx` leg out of that matrix job (GitHub Actions won't let a `uses:`-based job share a matrix with per-leg different targets) into its own `publish-ovsx` job that delegates to openvsx-publish-release-vsix.yml via `uses:`, instead of duplicating the ovsx-publish logic. Every Open VSX publish for a given extension -- stable, stable hotfix, prerelease, prerelease/emergency hotfix -- now requests its OIDC token from that one file. openvsx-publish-release-vsix.yml gained `vsix-name-pattern` and `exclude-web-vsix` inputs (both optional, default-compatible) so the delegated call can reproduce vscode-promote-prerelease.yml's existing filtering for monorepo/multi-extension callers. vsce/Marketplace publishing (VSCE_PERSONAL_ACCESS_TOKEN) is untouched. --- .../openvsx-publish-release-vsix.yml | 32 ++++++-- .../workflows/vscode-promote-prerelease.yml | 81 +++++++++---------- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/.github/workflows/openvsx-publish-release-vsix.yml b/.github/workflows/openvsx-publish-release-vsix.yml index f366ff7..c98160b 100644 --- a/.github/workflows/openvsx-publish-release-vsix.yml +++ b/.github/workflows/openvsx-publish-release-vsix.yml @@ -12,10 +12,16 @@ on: required: false default: false type: boolean - secrets: - IDEE_OVSX_PAT: - description: Open VSX publishing token - required: true + vsix-name-pattern: + description: Glob of downloaded VSIX file names to publish + required: false + default: '*.vsix' + type: string + exclude-web-vsix: + description: Exclude *-web-* VSIXs from publishing + required: false + default: 'false' + type: string permissions: contents: read @@ -23,6 +29,11 @@ permissions: jobs: publish: runs-on: ubuntu-latest + permissions: + contents: read + # ovsx exchanges this job's OIDC ID token for a short-lived Open VSX publishing token + # (--trusted-publishing below) -- without this, there is no token to exchange. + id-token: write steps: - name: Download release VSIXs id: release @@ -33,8 +44,9 @@ jobs: - name: Publish VSIXs env: DRY_RUN: ${{ inputs.dry-run }} - IDEE_OVSX_PAT: ${{ secrets.IDEE_OVSX_PAT }} PRE_RELEASE: ${{ steps.release.outputs.pre-release }} + VSIX_PATTERN: ${{ inputs.vsix-name-pattern }} + EXCLUDE_WEB: ${{ inputs.exclude-web-vsix }} run: | # Open VSX can hang the same way as vsce; retry per VSIX. # --skip-duplicate makes a retry a no-op if the first attempt committed. @@ -54,7 +66,7 @@ jobs: } while IFS= read -r -d '' vsix; do - args=("$vsix" -p "$IDEE_OVSX_PAT" --skip-duplicate) + args=("$vsix" --trusted-publishing --skip-duplicate) pre_release_suffix='' if [ "$PRE_RELEASE" = true ]; then args+=(--pre-release) @@ -70,4 +82,10 @@ jobs: echo "::error::Failed to publish $vsix after 3 attempts" exit 1 fi - done < <(find extensions -type f -name '*.vsix' -print0) + done < <( + if [ "$EXCLUDE_WEB" = "true" ]; then + find extensions -type f -name "$VSIX_PATTERN" ! -iname '*-web-*' -print0 + else + find extensions -type f -name "$VSIX_PATTERN" -print0 + fi + ) diff --git a/.github/workflows/vscode-promote-prerelease.yml b/.github/workflows/vscode-promote-prerelease.yml index 3a35377..df5252e 100644 --- a/.github/workflows/vscode-promote-prerelease.yml +++ b/.github/workflows/vscode-promote-prerelease.yml @@ -36,7 +36,9 @@ name: Promote Nightly to Pre-release # Requirements - calling repository must have: # - Nightly tags matching *-nightly.* (e.g., v1.2.3-nightly.20260629) # - GitHub releases for each nightly tag with VSIX file(s) attached -# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN, IDEE_OVSX_PAT +# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN +# - Open VSX publishes via OIDC trusted publishing (--trusted-publishing) -- no secret needed, +# but the registry's trusted-publisher config must list this workflow file # # Workflow: downloads VSIX → publishes to both marketplaces → creates tracking tag @@ -116,18 +118,13 @@ permissions: contents: write packages: write actions: read + # ovsx exchanges this for a short-lived Open VSX publishing token (--trusted-publishing in the + # publish job below); vsce/Marketplace still uses VSCE_PERSONAL_ACCESS_TOKEN. + id-token: write jobs: - publish: + publish-vsce: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - registry: vsce - publish-tool: vsce - - registry: ovsx - publish-tool: ovsx steps: - name: Checkout uses: actions/checkout@v6 @@ -151,15 +148,13 @@ jobs: --dir ./vsix-artifacts \ --repo "${{ github.repository }}" - - name: Publish to ${{ matrix.registry }} + - name: Publish to Visual Studio Marketplace env: NIGHTLY_TAG: ${{ inputs.release-tag }} VSIX_PATTERN: ${{ inputs.vsix-name-pattern || '*.vsix' }} EXCLUDE_WEB: ${{ inputs.exclude-web-vsix || 'false' }} - PUBLISH_TOOL: ${{ matrix.publish-tool }} DRY_RUN: ${{ inputs.dry-run || 'false' }} VSCE_PERSONAL_ACCESS_TOKEN: ${{ secrets.VSCE_PERSONAL_ACCESS_TOKEN }} - OVSX_PAT: ${{ secrets.IDEE_OVSX_PAT }} run: | set -uo pipefail @@ -177,20 +172,13 @@ jobs: exit 1 fi - if [ "$PUBLISH_TOOL" = "ovsx" ]; then - MARKETPLACE_NAME="Open VSX Registry" - TOKEN="$OVSX_PAT" - else - MARKETPLACE_NAME="Visual Studio Marketplace" - TOKEN="$VSCE_PERSONAL_ACCESS_TOKEN" - fi - + TOKEN="$VSCE_PERSONAL_ACCESS_TOKEN" if [ "$DRY_RUN" != "true" ] && [ -z "$TOKEN" ]; then - echo "::error::Token for $PUBLISH_TOOL is not set" + echo "::error::Token for vsce is not set" exit 1 fi - echo "Publishing ${#VSIX_FILES[@]} VSIX(s) as pre-release to $MARKETPLACE_NAME:" + echo "Publishing ${#VSIX_FILES[@]} VSIX(s) as pre-release to Visual Studio Marketplace:" printf ' %s\n' "${VSIX_FILES[@]}" RUN_ID="${{ github.run_id }}" @@ -199,7 +187,7 @@ jobs: FAILED=0 # Marketplace gallery requests can sit until typed-rest-client's 180s - # socket timeout. vsce/ovsx have no timeout flag; retry per VSIX. + # socket timeout. vsce has no timeout flag; retry per VSIX. # --skip-duplicate makes a retry a no-op if the first attempt committed. publish_with_retry() { local attempt @@ -218,11 +206,11 @@ jobs: for VSIX in "${VSIX_FILES[@]}"; do NAME=$(basename "$VSIX") - echo "::group::$NAME → $MARKETPLACE_NAME" + echo "::group::$NAME → Visual Studio Marketplace" # Audit trail (mirrors the publish-vsix action's per-file record) HASH=$(sha256sum "$VSIX" 2>/dev/null | cut -d' ' -f1 || echo "unknown") - echo "[AUDIT] actor=$ACTOR repo=$REPO run_id=$RUN_ID tool=$PUBLISH_TOOL file=$NAME hash=$HASH pre_release=true dry_run=$DRY_RUN" + echo "[AUDIT] actor=$ACTOR repo=$REPO run_id=$RUN_ID tool=vsce file=$NAME hash=$HASH pre_release=true dry_run=$DRY_RUN" if [ "$DRY_RUN" = "true" ]; then echo "🔍 DRY RUN - would publish $NAME (pre-release)" @@ -230,32 +218,41 @@ jobs: continue fi - if [ "$PUBLISH_TOOL" = "vsce" ]; then - if VSCE_PAT="$TOKEN" publish_with_retry npx @vscode/vsce publish --packagePath "$VSIX" --skip-duplicate --pre-release; then - echo "✅ Published $NAME to $MARKETPLACE_NAME" - else - echo "::error::Failed to publish $NAME to $MARKETPLACE_NAME" - FAILED=1 - fi + if VSCE_PAT="$TOKEN" publish_with_retry npx @vscode/vsce publish --packagePath "$VSIX" --skip-duplicate --pre-release; then + echo "✅ Published $NAME to Visual Studio Marketplace" else - if publish_with_retry npx ovsx publish "$VSIX" -p "$TOKEN" --skip-duplicate --pre-release; then - echo "✅ Published $NAME to $MARKETPLACE_NAME" - else - echo "::error::Failed to publish $NAME to $MARKETPLACE_NAME" - FAILED=1 - fi + echo "::error::Failed to publish $NAME to Visual Studio Marketplace" + FAILED=1 fi echo "::endgroup::" done if [ "$FAILED" -ne 0 ]; then - echo "::error::One or more VSIX publishes failed for $PUBLISH_TOOL" + echo "::error::One or more VSIX publishes failed for vsce" exit 1 fi - echo "✅ All ${#VSIX_FILES[@]} VSIX(s) processed for $MARKETPLACE_NAME" + echo "✅ All ${#VSIX_FILES[@]} VSIX(s) processed for Visual Studio Marketplace" + + # ovsx publishing delegates to the shared workflow stable releases already use (rather than + # running its own `ovsx publish`), so every Open VSX publish for this extension -- prerelease + # and stable alike -- requests its OIDC token from the same job_workflow_ref. Open VSX's + # trusted-publisher config is one identity per extension, keyed on that exact file, so two + # different files here would mean only one of the two publish paths could ever be trusted. + publish-ovsx: + permissions: + contents: read + id-token: write + uses: salesforcecli/github-workflows/.github/workflows/openvsx-publish-release-vsix.yml@main + with: + release-tag: ${{ inputs.release-tag }} + vsix-name-pattern: ${{ inputs.vsix-name-pattern || '*.vsix' }} + exclude-web-vsix: ${{ inputs.exclude-web-vsix || 'false' }} + # This workflow's own dry-run input is a string ('true'/'false'); the callee's is boolean. + dry-run: ${{ (inputs.dry-run || 'false') == 'true' }} + secrets: inherit tag-promoted: - needs: publish + needs: [publish-vsce, publish-ovsx] runs-on: ubuntu-latest steps: - name: Checkout From 665cbd8ebfd908b8da076d6349306be1cab5f520 Mon Sep 17 00:00:00 2001 From: madhur310 Date: Mon, 26 Oct 2026 14:42:32 -0700 Subject: [PATCH 2/5] Revert: drop vscode-promote-prerelease.yml ovsx job-split - W-24666922 Open VSX's trusted-publisher check matches the OIDC token's workflow_ref claim (the top-level/triggering workflow), not job_workflow_ref (the reusable workflow actually executing a job) -- confirmed against eclipse-openvsx/openvsx's TrustedPublishingService/ GitHubTrustedPublishingProviderSupport source. Consolidating this reusable workflow's ovsx leg onto openvsx-publish-release-vsix.yml never closed that gap: salesforcedx-vscode's stable and prerelease flows are started by two different top-level workflow files regardless of what runs underneath them, and Open VSX only allows one trusted-publisher registration per extension (a hard server-side check). The real fix has to live in salesforcedx-vscode's own top-level workflows instead, so this reusable workflow goes back to vsce-only. Keeps the openvsx-publish-release-vsix.yml PAT -> --trusted-publishing migration, which the new salesforcedx-vscode-side design still delegates to. --- .../workflows/vscode-promote-prerelease.yml | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/.github/workflows/vscode-promote-prerelease.yml b/.github/workflows/vscode-promote-prerelease.yml index df5252e..3a35377 100644 --- a/.github/workflows/vscode-promote-prerelease.yml +++ b/.github/workflows/vscode-promote-prerelease.yml @@ -36,9 +36,7 @@ name: Promote Nightly to Pre-release # Requirements - calling repository must have: # - Nightly tags matching *-nightly.* (e.g., v1.2.3-nightly.20260629) # - GitHub releases for each nightly tag with VSIX file(s) attached -# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN -# - Open VSX publishes via OIDC trusted publishing (--trusted-publishing) -- no secret needed, -# but the registry's trusted-publisher config must list this workflow file +# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN, IDEE_OVSX_PAT # # Workflow: downloads VSIX → publishes to both marketplaces → creates tracking tag @@ -118,13 +116,18 @@ permissions: contents: write packages: write actions: read - # ovsx exchanges this for a short-lived Open VSX publishing token (--trusted-publishing in the - # publish job below); vsce/Marketplace still uses VSCE_PERSONAL_ACCESS_TOKEN. - id-token: write jobs: - publish-vsce: + publish: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - registry: vsce + publish-tool: vsce + - registry: ovsx + publish-tool: ovsx steps: - name: Checkout uses: actions/checkout@v6 @@ -148,13 +151,15 @@ jobs: --dir ./vsix-artifacts \ --repo "${{ github.repository }}" - - name: Publish to Visual Studio Marketplace + - name: Publish to ${{ matrix.registry }} env: NIGHTLY_TAG: ${{ inputs.release-tag }} VSIX_PATTERN: ${{ inputs.vsix-name-pattern || '*.vsix' }} EXCLUDE_WEB: ${{ inputs.exclude-web-vsix || 'false' }} + PUBLISH_TOOL: ${{ matrix.publish-tool }} DRY_RUN: ${{ inputs.dry-run || 'false' }} VSCE_PERSONAL_ACCESS_TOKEN: ${{ secrets.VSCE_PERSONAL_ACCESS_TOKEN }} + OVSX_PAT: ${{ secrets.IDEE_OVSX_PAT }} run: | set -uo pipefail @@ -172,13 +177,20 @@ jobs: exit 1 fi - TOKEN="$VSCE_PERSONAL_ACCESS_TOKEN" + if [ "$PUBLISH_TOOL" = "ovsx" ]; then + MARKETPLACE_NAME="Open VSX Registry" + TOKEN="$OVSX_PAT" + else + MARKETPLACE_NAME="Visual Studio Marketplace" + TOKEN="$VSCE_PERSONAL_ACCESS_TOKEN" + fi + if [ "$DRY_RUN" != "true" ] && [ -z "$TOKEN" ]; then - echo "::error::Token for vsce is not set" + echo "::error::Token for $PUBLISH_TOOL is not set" exit 1 fi - echo "Publishing ${#VSIX_FILES[@]} VSIX(s) as pre-release to Visual Studio Marketplace:" + echo "Publishing ${#VSIX_FILES[@]} VSIX(s) as pre-release to $MARKETPLACE_NAME:" printf ' %s\n' "${VSIX_FILES[@]}" RUN_ID="${{ github.run_id }}" @@ -187,7 +199,7 @@ jobs: FAILED=0 # Marketplace gallery requests can sit until typed-rest-client's 180s - # socket timeout. vsce has no timeout flag; retry per VSIX. + # socket timeout. vsce/ovsx have no timeout flag; retry per VSIX. # --skip-duplicate makes a retry a no-op if the first attempt committed. publish_with_retry() { local attempt @@ -206,11 +218,11 @@ jobs: for VSIX in "${VSIX_FILES[@]}"; do NAME=$(basename "$VSIX") - echo "::group::$NAME → Visual Studio Marketplace" + echo "::group::$NAME → $MARKETPLACE_NAME" # Audit trail (mirrors the publish-vsix action's per-file record) HASH=$(sha256sum "$VSIX" 2>/dev/null | cut -d' ' -f1 || echo "unknown") - echo "[AUDIT] actor=$ACTOR repo=$REPO run_id=$RUN_ID tool=vsce file=$NAME hash=$HASH pre_release=true dry_run=$DRY_RUN" + echo "[AUDIT] actor=$ACTOR repo=$REPO run_id=$RUN_ID tool=$PUBLISH_TOOL file=$NAME hash=$HASH pre_release=true dry_run=$DRY_RUN" if [ "$DRY_RUN" = "true" ]; then echo "🔍 DRY RUN - would publish $NAME (pre-release)" @@ -218,41 +230,32 @@ jobs: continue fi - if VSCE_PAT="$TOKEN" publish_with_retry npx @vscode/vsce publish --packagePath "$VSIX" --skip-duplicate --pre-release; then - echo "✅ Published $NAME to Visual Studio Marketplace" + if [ "$PUBLISH_TOOL" = "vsce" ]; then + if VSCE_PAT="$TOKEN" publish_with_retry npx @vscode/vsce publish --packagePath "$VSIX" --skip-duplicate --pre-release; then + echo "✅ Published $NAME to $MARKETPLACE_NAME" + else + echo "::error::Failed to publish $NAME to $MARKETPLACE_NAME" + FAILED=1 + fi else - echo "::error::Failed to publish $NAME to Visual Studio Marketplace" - FAILED=1 + if publish_with_retry npx ovsx publish "$VSIX" -p "$TOKEN" --skip-duplicate --pre-release; then + echo "✅ Published $NAME to $MARKETPLACE_NAME" + else + echo "::error::Failed to publish $NAME to $MARKETPLACE_NAME" + FAILED=1 + fi fi echo "::endgroup::" done if [ "$FAILED" -ne 0 ]; then - echo "::error::One or more VSIX publishes failed for vsce" + echo "::error::One or more VSIX publishes failed for $PUBLISH_TOOL" exit 1 fi - echo "✅ All ${#VSIX_FILES[@]} VSIX(s) processed for Visual Studio Marketplace" - - # ovsx publishing delegates to the shared workflow stable releases already use (rather than - # running its own `ovsx publish`), so every Open VSX publish for this extension -- prerelease - # and stable alike -- requests its OIDC token from the same job_workflow_ref. Open VSX's - # trusted-publisher config is one identity per extension, keyed on that exact file, so two - # different files here would mean only one of the two publish paths could ever be trusted. - publish-ovsx: - permissions: - contents: read - id-token: write - uses: salesforcecli/github-workflows/.github/workflows/openvsx-publish-release-vsix.yml@main - with: - release-tag: ${{ inputs.release-tag }} - vsix-name-pattern: ${{ inputs.vsix-name-pattern || '*.vsix' }} - exclude-web-vsix: ${{ inputs.exclude-web-vsix || 'false' }} - # This workflow's own dry-run input is a string ('true'/'false'); the callee's is boolean. - dry-run: ${{ (inputs.dry-run || 'false') == 'true' }} - secrets: inherit + echo "✅ All ${#VSIX_FILES[@]} VSIX(s) processed for $MARKETPLACE_NAME" tag-promoted: - needs: [publish-vsce, publish-ovsx] + needs: publish runs-on: ubuntu-latest steps: - name: Checkout From f9b21c1997439d07b1e74fedae5568b53fdc9118 Mon Sep 17 00:00:00 2001 From: madhur310 Date: Wed, 28 Oct 2026 15:35:06 -0700 Subject: [PATCH 3/5] fix(release): remove Open VSX publishing from shared promote workflows - W-24666922 Folds the removal work from the now-closed #200 into this PR, since both are the same migration and coordinating one github-workflows branch against each calling repo's own migration PR is simpler than two. Removes the `ovsx` matrix leg from vscode-promote-prerelease.yml and vscode-promote-stable.yml entirely (no toggle). Each of these reusable workflows' real callers (forcedotcom/salesforcedx-vscode, forcedotcom/apex-language-support) are migrating their Open VSX publishing onto a dedicated top-level workflow (publishOpenVSXTrusted.yml) that delegates to this PR's openvsx-publish-release-vsix.yml instead -- required, not optional, since Open VSX's trusted-publisher match keys off workflow_ref (the run's initiating workflow) and allows only one registration per extension. Also adds a `release-tag` output to vscode-promote-stable.yml (the stable tag it just created), so a caller can dispatch its own Open VSX publish for that exact tag afterward. --- .../workflows/vscode-promote-prerelease.yml | 68 +++++++------------ .github/workflows/vscode-promote-stable.yml | 49 +++++++++---- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/.github/workflows/vscode-promote-prerelease.yml b/.github/workflows/vscode-promote-prerelease.yml index 3a35377..66d0ec5 100644 --- a/.github/workflows/vscode-promote-prerelease.yml +++ b/.github/workflows/vscode-promote-prerelease.yml @@ -1,10 +1,20 @@ name: Promote Nightly to Pre-release -# Publishes a nightly build to pre-release on VS Code Marketplace and Open VSX. +# Publishes a nightly build to pre-release on the VS Code Marketplace. Open VSX is NOT published +# from here -- see the note below. # # NOTE: Callers should use vscode-find-nightly-candidate.yml and check-ci-status action # BEFORE calling this workflow to implement their own gate-check logic. # +# NOTE on Open VSX: Open VSX's trusted-publisher match keys off the OIDC token's workflow_ref +# claim (the run's initiating workflow), and allows only one trusted-publisher registration per +# extension. A caller whose stable release publishes Open VSX from a different top-level workflow +# file than this one would get two different workflow_ref values for the same extension, which +# can't both be registered. So Open VSX publishing is each caller's own responsibility -- publish +# it from whichever single top-level workflow file you also use for your stable release's Open VSX +# publish (dispatched via `gh workflow run`, not `uses:`, so it's a genuinely new, independently +# initiated run and not nested inside this one). +# # Usage: # jobs: # find-nightly: @@ -36,9 +46,9 @@ name: Promote Nightly to Pre-release # Requirements - calling repository must have: # - Nightly tags matching *-nightly.* (e.g., v1.2.3-nightly.20260629) # - GitHub releases for each nightly tag with VSIX file(s) attached -# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN, IDEE_OVSX_PAT +# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN # -# Workflow: downloads VSIX → publishes to both marketplaces → creates tracking tag +# Workflow: downloads VSIX → publishes to VS Code Marketplace → creates tracking tag on: workflow_call: @@ -120,14 +130,6 @@ permissions: jobs: publish: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - registry: vsce - publish-tool: vsce - - registry: ovsx - publish-tool: ovsx steps: - name: Checkout uses: actions/checkout@v6 @@ -151,15 +153,13 @@ jobs: --dir ./vsix-artifacts \ --repo "${{ github.repository }}" - - name: Publish to ${{ matrix.registry }} + - name: Publish to Visual Studio Marketplace env: NIGHTLY_TAG: ${{ inputs.release-tag }} VSIX_PATTERN: ${{ inputs.vsix-name-pattern || '*.vsix' }} EXCLUDE_WEB: ${{ inputs.exclude-web-vsix || 'false' }} - PUBLISH_TOOL: ${{ matrix.publish-tool }} DRY_RUN: ${{ inputs.dry-run || 'false' }} VSCE_PERSONAL_ACCESS_TOKEN: ${{ secrets.VSCE_PERSONAL_ACCESS_TOKEN }} - OVSX_PAT: ${{ secrets.IDEE_OVSX_PAT }} run: | set -uo pipefail @@ -177,20 +177,13 @@ jobs: exit 1 fi - if [ "$PUBLISH_TOOL" = "ovsx" ]; then - MARKETPLACE_NAME="Open VSX Registry" - TOKEN="$OVSX_PAT" - else - MARKETPLACE_NAME="Visual Studio Marketplace" - TOKEN="$VSCE_PERSONAL_ACCESS_TOKEN" - fi - + TOKEN="$VSCE_PERSONAL_ACCESS_TOKEN" if [ "$DRY_RUN" != "true" ] && [ -z "$TOKEN" ]; then - echo "::error::Token for $PUBLISH_TOOL is not set" + echo "::error::Token for vsce is not set" exit 1 fi - echo "Publishing ${#VSIX_FILES[@]} VSIX(s) as pre-release to $MARKETPLACE_NAME:" + echo "Publishing ${#VSIX_FILES[@]} VSIX(s) as pre-release to Visual Studio Marketplace:" printf ' %s\n' "${VSIX_FILES[@]}" RUN_ID="${{ github.run_id }}" @@ -199,7 +192,7 @@ jobs: FAILED=0 # Marketplace gallery requests can sit until typed-rest-client's 180s - # socket timeout. vsce/ovsx have no timeout flag; retry per VSIX. + # socket timeout. vsce has no timeout flag; retry per VSIX. # --skip-duplicate makes a retry a no-op if the first attempt committed. publish_with_retry() { local attempt @@ -218,11 +211,11 @@ jobs: for VSIX in "${VSIX_FILES[@]}"; do NAME=$(basename "$VSIX") - echo "::group::$NAME → $MARKETPLACE_NAME" + echo "::group::$NAME → Visual Studio Marketplace" # Audit trail (mirrors the publish-vsix action's per-file record) HASH=$(sha256sum "$VSIX" 2>/dev/null | cut -d' ' -f1 || echo "unknown") - echo "[AUDIT] actor=$ACTOR repo=$REPO run_id=$RUN_ID tool=$PUBLISH_TOOL file=$NAME hash=$HASH pre_release=true dry_run=$DRY_RUN" + echo "[AUDIT] actor=$ACTOR repo=$REPO run_id=$RUN_ID tool=vsce file=$NAME hash=$HASH pre_release=true dry_run=$DRY_RUN" if [ "$DRY_RUN" = "true" ]; then echo "🔍 DRY RUN - would publish $NAME (pre-release)" @@ -230,29 +223,20 @@ jobs: continue fi - if [ "$PUBLISH_TOOL" = "vsce" ]; then - if VSCE_PAT="$TOKEN" publish_with_retry npx @vscode/vsce publish --packagePath "$VSIX" --skip-duplicate --pre-release; then - echo "✅ Published $NAME to $MARKETPLACE_NAME" - else - echo "::error::Failed to publish $NAME to $MARKETPLACE_NAME" - FAILED=1 - fi + if VSCE_PAT="$TOKEN" publish_with_retry npx @vscode/vsce publish --packagePath "$VSIX" --skip-duplicate --pre-release; then + echo "✅ Published $NAME to Visual Studio Marketplace" else - if publish_with_retry npx ovsx publish "$VSIX" -p "$TOKEN" --skip-duplicate --pre-release; then - echo "✅ Published $NAME to $MARKETPLACE_NAME" - else - echo "::error::Failed to publish $NAME to $MARKETPLACE_NAME" - FAILED=1 - fi + echo "::error::Failed to publish $NAME to Visual Studio Marketplace" + FAILED=1 fi echo "::endgroup::" done if [ "$FAILED" -ne 0 ]; then - echo "::error::One or more VSIX publishes failed for $PUBLISH_TOOL" + echo "::error::One or more VSIX publishes failed for vsce" exit 1 fi - echo "✅ All ${#VSIX_FILES[@]} VSIX(s) processed for $MARKETPLACE_NAME" + echo "✅ All ${#VSIX_FILES[@]} VSIX(s) processed for Visual Studio Marketplace" tag-promoted: needs: publish diff --git a/.github/workflows/vscode-promote-stable.yml b/.github/workflows/vscode-promote-stable.yml index b141c95..76e1352 100644 --- a/.github/workflows/vscode-promote-stable.yml +++ b/.github/workflows/vscode-promote-stable.yml @@ -1,6 +1,17 @@ name: Promote Pre-release to Stable -# Reusable workflow for promoting a pre-release VS Code extension to stable. +# Reusable workflow for promoting a pre-release VS Code extension to stable. Publishes to the VS +# Code Marketplace only -- Open VSX is NOT published from here, see the note below. +# +# NOTE on Open VSX: Open VSX's trusted-publisher match keys off the OIDC token's workflow_ref +# claim (the run's initiating workflow), and allows only one trusted-publisher registration per +# extension. A caller whose prerelease publishes Open VSX from a different top-level workflow +# file than this one would get two different workflow_ref values for the same extension, which +# can't both be registered. So Open VSX publishing is each caller's own responsibility -- publish +# it from whichever single top-level workflow file you also use for your prerelease's Open VSX +# publish, using this workflow's `release-tag` output to know which tag to publish (dispatched via +# `gh workflow run`, not `uses:`, so it's a genuinely new, independently initiated run and not +# nested inside this one). # # Usage from consuming repository: # jobs: @@ -15,12 +26,20 @@ name: Promote Pre-release to Stable # dry-run: 'false' # secrets: inherit # +# publish-openvsx: +# needs: promote +# runs-on: ubuntu-latest +# env: +# GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} +# steps: +# - run: gh workflow run publishOpenVSXTrusted.yml --repo "${{ github.repository }}" -f release-tag="${{ needs.promote.outputs.release-tag }}" +# # Requirements - calling repository must have: # - marketplace-prerelease-* tracking tags # - Nightly tags matching v{version}-nightly.* pattern # - GitHub releases for each nightly tag with VSIX file(s) attached # - Passing CI checks on the nightly candidate commit -# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN, IDEE_OVSX_PAT +# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN # - The shared actions in this repository are resolved from the workflow branch. # - The package manager and lockfile inputs must match the caller repository. # @@ -28,7 +47,7 @@ name: Promote Pre-release to Stable # resolved from this repository so callers do not need duplicate composite actions. # # Workflow: finds latest pre-release → verifies quality gate → repackages VSIX -# → publishes to both marketplaces → creates GitHub release → tags stable +# → publishes to VS Code Marketplace → creates GitHub release → tags stable # → commits stable version back to main on: @@ -93,6 +112,10 @@ on: required: false default: 'npm ci' type: string + outputs: + release-tag: + description: 'The stable GitHub release tag this run created (e.g. apex-language-server-extension-v1.2.3). Use this to dispatch your own Open VSX trusted-publishing workflow afterward -- see the header comment above.' + value: ${{ jobs.create-github-release.outputs.release-tag }} workflow_dispatch: inputs: extension-name: @@ -336,16 +359,13 @@ jobs: name: stable-vsix path: ${{ steps.repack.outputs.vsix-path }} + # Open VSX is NOT published from here -- see this file's header for why (trusted-publisher + # match keys off workflow_ref, the run's initiating workflow, and allows only one registration + # per extension, so it has to live in whichever single top-level workflow file the caller also + # uses for its prerelease's Open VSX publish). publish: needs: [find-prerelease-candidate, repackage] runs-on: ubuntu-latest - strategy: - matrix: - include: - - registry: vsce - publish-tool: vsce - - registry: ovsx - publish-tool: ovsx steps: - name: Checkout uses: actions/checkout@v6 @@ -379,20 +399,21 @@ jobs: fi echo "VSIX_PATH=$VSIX_FILE" >> $GITHUB_ENV - - name: Publish to ${{ matrix.registry }} + - name: Publish to Visual Studio Marketplace uses: salesforcecli/github-workflows/.github/actions/publish-vsix@main with: vsix-path: ${{ env.VSIX_PATH }} - publish-tool: ${{ matrix.publish-tool }} + publish-tool: vsce pre-release: 'false' dry-run: ${{ inputs.dry-run || 'false' }} env: VSCE_PERSONAL_ACCESS_TOKEN: ${{ secrets.VSCE_PERSONAL_ACCESS_TOKEN }} - OVSX_PAT: ${{ secrets.IDEE_OVSX_PAT }} create-github-release: needs: [find-prerelease-candidate, repackage, publish] runs-on: ubuntu-latest + outputs: + release-tag: ${{ steps.release.outputs.release-tag }} steps: - name: Checkout uses: actions/checkout@v6 @@ -407,6 +428,7 @@ jobs: path: ./vsix-artifacts - name: Create GitHub release for stable version + id: release env: GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} STABLE_VERSION: ${{ needs.find-prerelease-candidate.outputs.stable-version }} @@ -427,6 +449,7 @@ jobs: # e.g., apex-language-server-extension-1.2.3.vsix -> apex-language-server-extension BASE_NAME=$(basename "$VSIX_FILE" .vsix | sed -E 's/-[0-9]+\.[0-9]+\.[0-9]+$//') RELEASE_TAG="${BASE_NAME}-v${STABLE_VERSION}" + echo "release-tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT" RELEASE_TITLE="${BASE_NAME} v${STABLE_VERSION} (stable)" RELEASE_NOTES="## Stable Release v${STABLE_VERSION} From 0a3e4294cfea29436e999a953bdc6841d63c8fe9 Mon Sep 17 00:00:00 2001 From: madhur310 Date: Thu, 29 Oct 2026 11:48:55 -0700 Subject: [PATCH 4/5] fix(release): redirect vscode-manual-publish.yml's Open VSX publish to the caller - W-24666922 Same migration, applied to the third apex-language-support entry point that can publish Open VSX (promote-prerelease.yml and promote-stable.yml already covered). vscode-manual-publish.yml is this repo's only real caller (confirmed via GitHub code search), so no toggle needed. The `publish` job's ovsx leg becomes a deliberate no-op step (not a removed step, and not a removed matrix entry) specifically so the job's own success/skip status -- and every downstream needs.publish.result == 'success' check in this file -- stays exactly as it was. The caller is expected to dispatch its own single top-level Open VSX workflow afterward, for the version-tag path only: adds a hard error in `prepare` for source-run-id + ovsx/all, since a branch-CI-run artifact has no GitHub Release for a tag-based trusted-publishing dispatch to target, and silently never publishing to Open VSX for that combination would be worse than failing loudly. --- .github/workflows/vscode-manual-publish.yml | 31 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/vscode-manual-publish.yml b/.github/workflows/vscode-manual-publish.yml index f81f1d4..a12029c 100644 --- a/.github/workflows/vscode-manual-publish.yml +++ b/.github/workflows/vscode-manual-publish.yml @@ -5,6 +5,16 @@ name: Manual Publish VS Code Extension # - A nightly GitHub Release (version-tag path — normal workflow) # - A branch CI build artifact (source-run-id path — out-of-band, bypass mandatory) # +# NOTE on Open VSX: this workflow does NOT publish Open VSX itself, even when registries is +# ovsx/all -- its `publish` job's ovsx leg is a deliberate no-op (see that job's comment). Open +# VSX's trusted-publisher match keys off the OIDC token's workflow_ref claim (the run's +# initiating workflow), and allows only one trusted-publisher registration per extension, so +# Open VSX publishing has to live in whichever single top-level workflow file the caller also +# uses for its other release channels. The caller is expected to dispatch that file itself (via +# `gh workflow run`, not `uses:`) after this workflow succeeds, for the version-tag path only -- +# source-run-id has no GitHub Release for a tag-based publish to target, so `prepare` hard-errors +# that combination instead of silently never publishing to Open VSX. +# # Usage from consuming repository: # jobs: # manual-publish: @@ -22,7 +32,7 @@ name: Manual Publish VS Code Extension # secrets: inherit # # Requirements - calling repository must have: -# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN, IDEE_OVSX_PAT +# - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN # - For CBWeb: MARKETPLACE_DEPLOY_TOKEN and MARKETPLACE_URL variable # - The package manager and lockfile inputs must match the caller repository. # - The shared actions in this repository are resolved from the workflow branch. @@ -317,6 +327,14 @@ jobs: echo "ERROR: source-run-id path requires skip-quality-checks=true (branch CI profiles differ from main)." exit 1 fi + # Open VSX publishing is tag-based (the caller dispatches publishOpenVSXTrusted.yml + # with a release tag -- see this workflow's header) and there is no GitHub Release, + # and therefore no tag, for a branch CI run artifact. Failing loudly here beats the + # alternative of silently never publishing to Open VSX for this combination. + if [ "$REGISTRIES" = "ovsx" ] || [ "$REGISTRIES" = "all" ]; then + echo "ERROR: source-run-id has no GitHub Release for Open VSX's tag-based trusted-publishing dispatch to target. Use registries=vsce for this path." + exit 1 + fi fi # Bypass requires confirmation (case-insensitive) @@ -794,7 +812,17 @@ jobs: fi echo "VSIX_PATH=$VSIX_FILE" >> $GITHUB_ENV + # Open VSX is NOT published here -- see this file's header. The caller dispatches + # publishOpenVSXTrusted.yml itself afterward instead, so this leg of the matrix (still + # present when registries is ovsx/all, so the job's own success/skip status -- and every + # downstream needs.publish.result == 'success' check -- stays exactly as it was) is a + # deliberate no-op rather than a removed step. + - name: Open VSX is published by the caller, not here + if: matrix.publish-tool == 'ovsx' + run: echo "Skipping -- the caller dispatches publishOpenVSXTrusted.yml for Open VSX. See this file's header comment." + - name: Publish to ${{ matrix.registry }} + if: matrix.publish-tool != 'ovsx' uses: salesforcecli/github-workflows/.github/actions/publish-vsix@main with: vsix-path: ${{ env.VSIX_PATH }} @@ -803,7 +831,6 @@ jobs: dry-run: ${{ inputs.dry-run || 'false' }} env: VSCE_PERSONAL_ACCESS_TOKEN: ${{ secrets.VSCE_PERSONAL_ACCESS_TOKEN }} - OVSX_PAT: ${{ secrets.IDEE_OVSX_PAT }} publish-to-cbweb-marketplace: name: Publish to CBWeb Internal Marketplace From 43df7abcc3072f7427aa57cab6f8ba90540e931f Mon Sep 17 00:00:00 2001 From: madhur310 Date: Thu, 5 Nov 2026 10:11:53 -0800 Subject: [PATCH 5/5] docs(release): centralize duplicated Open VSX trusted-publishing note in README - W-24666922 The same explanatory comment block was duplicated verbatim across vscode-promote-prerelease.yml, vscode-promote-stable.yml, and vscode-manual-publish.yml. Move it into a single README section and replace each with a short pointer. --- .github/workflows/vscode-manual-publish.yml | 12 ++------ .../workflows/vscode-promote-prerelease.yml | 12 ++------ .github/workflows/vscode-promote-stable.yml | 14 ++------- README.md | 30 +++++++++++++++++++ 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/.github/workflows/vscode-manual-publish.yml b/.github/workflows/vscode-manual-publish.yml index a12029c..b74b4cf 100644 --- a/.github/workflows/vscode-manual-publish.yml +++ b/.github/workflows/vscode-manual-publish.yml @@ -5,15 +5,9 @@ name: Manual Publish VS Code Extension # - A nightly GitHub Release (version-tag path — normal workflow) # - A branch CI build artifact (source-run-id path — out-of-band, bypass mandatory) # -# NOTE on Open VSX: this workflow does NOT publish Open VSX itself, even when registries is -# ovsx/all -- its `publish` job's ovsx leg is a deliberate no-op (see that job's comment). Open -# VSX's trusted-publisher match keys off the OIDC token's workflow_ref claim (the run's -# initiating workflow), and allows only one trusted-publisher registration per extension, so -# Open VSX publishing has to live in whichever single top-level workflow file the caller also -# uses for its other release channels. The caller is expected to dispatch that file itself (via -# `gh workflow run`, not `uses:`) after this workflow succeeds, for the version-tag path only -- -# source-run-id has no GitHub Release for a tag-based publish to target, so `prepare` hard-errors -# that combination instead of silently never publishing to Open VSX. +# This workflow does NOT publish Open VSX itself -- its `publish` job's ovsx leg is a +# deliberate no-op (see that job's comment). See "Open VSX Trusted Publishing" in this repo's +# README for why, and dispatch your own Open VSX publish after this workflow succeeds. # # Usage from consuming repository: # jobs: diff --git a/.github/workflows/vscode-promote-prerelease.yml b/.github/workflows/vscode-promote-prerelease.yml index 66d0ec5..d71fb1b 100644 --- a/.github/workflows/vscode-promote-prerelease.yml +++ b/.github/workflows/vscode-promote-prerelease.yml @@ -1,20 +1,12 @@ name: Promote Nightly to Pre-release # Publishes a nightly build to pre-release on the VS Code Marketplace. Open VSX is NOT published -# from here -- see the note below. +# from here. See "Open VSX Trusted Publishing" in this repo's README for why, and use this +# workflow's `release-tag` output to dispatch your own Open VSX publish afterward. # # NOTE: Callers should use vscode-find-nightly-candidate.yml and check-ci-status action # BEFORE calling this workflow to implement their own gate-check logic. # -# NOTE on Open VSX: Open VSX's trusted-publisher match keys off the OIDC token's workflow_ref -# claim (the run's initiating workflow), and allows only one trusted-publisher registration per -# extension. A caller whose stable release publishes Open VSX from a different top-level workflow -# file than this one would get two different workflow_ref values for the same extension, which -# can't both be registered. So Open VSX publishing is each caller's own responsibility -- publish -# it from whichever single top-level workflow file you also use for your stable release's Open VSX -# publish (dispatched via `gh workflow run`, not `uses:`, so it's a genuinely new, independently -# initiated run and not nested inside this one). -# # Usage: # jobs: # find-nightly: diff --git a/.github/workflows/vscode-promote-stable.yml b/.github/workflows/vscode-promote-stable.yml index 76e1352..0473db3 100644 --- a/.github/workflows/vscode-promote-stable.yml +++ b/.github/workflows/vscode-promote-stable.yml @@ -1,17 +1,9 @@ name: Promote Pre-release to Stable # Reusable workflow for promoting a pre-release VS Code extension to stable. Publishes to the VS -# Code Marketplace only -- Open VSX is NOT published from here, see the note below. -# -# NOTE on Open VSX: Open VSX's trusted-publisher match keys off the OIDC token's workflow_ref -# claim (the run's initiating workflow), and allows only one trusted-publisher registration per -# extension. A caller whose prerelease publishes Open VSX from a different top-level workflow -# file than this one would get two different workflow_ref values for the same extension, which -# can't both be registered. So Open VSX publishing is each caller's own responsibility -- publish -# it from whichever single top-level workflow file you also use for your prerelease's Open VSX -# publish, using this workflow's `release-tag` output to know which tag to publish (dispatched via -# `gh workflow run`, not `uses:`, so it's a genuinely new, independently initiated run and not -# nested inside this one). +# Code Marketplace only -- Open VSX is NOT published from here. See "Open VSX Trusted Publishing" +# in this repo's README for why, and use this workflow's `release-tag` output to dispatch your +# own Open VSX publish afterward. # # Usage from consuming repository: # jobs: diff --git a/README.md b/README.md index 296127e..6db4eaa 100644 --- a/README.md +++ b/README.md @@ -516,6 +516,36 @@ The VS Code workflows follow a modular design: 3. **Publishing** (`vscode-publish-extensions.yml`) - Complete release pipeline with version bumping and marketplace publishing 4. **Promotion** (`vscode-promote-prerelease.yml`, `vscode-promote-stable.yml`) - Promotes vetted builds between release channels +### Open VSX Trusted Publishing + +Open VSX's trusted-publisher match keys off the OIDC token's `workflow_ref` claim -- the run's +*initiating* workflow -- not `job_workflow_ref` (the reusable workflow actually executing a job, +which can be nested arbitrarily deep via `uses:`, even cross-repo, without changing +`workflow_ref`). Open VSX also allows exactly one trusted-publisher registration per extension, +enforced server-side, not just a UI limitation. + +Because of this, `vscode-promote-prerelease.yml`, `vscode-promote-stable.yml`, and +`vscode-manual-publish.yml` do **not** publish to Open VSX themselves, even when a caller asks +for `ovsx`/`all` registries -- publishing from inside a `uses:`-nested reusable workflow would +mint a different `workflow_ref` than whatever top-level workflow the caller already uses for its +other release channels, and an extension can't have two trusted-publisher registrations. + +Instead, each caller designates **one** top-level workflow file per extension for Open VSX +publishing, dispatched directly (e.g. via `gh workflow run`, never nested behind another `uses:` +call) so every Open VSX publish for that extension shares the same `workflow_ref`. That file +should delegate the actual publish step to [`openvsx-publish-release-vsix`](#openvsx-publish-release-vsix). The pattern: + +- `vscode-promote-prerelease.yml` takes the tag to promote as a `release-tag` input, so the + caller already has it and can dispatch its Open VSX workflow with the same value afterward. + `vscode-promote-stable.yml` creates the stable tag itself, so it exposes that tag back out + via a `release-tag` output instead. +- `vscode-manual-publish.yml`'s `publish` job keeps its `ovsx` matrix leg as a deliberate no-op + (so job status and every downstream `needs.publish.result == 'success'` check are unaffected) + and expects the caller to dispatch its Open VSX workflow itself, for the `version-tag` source + path only -- `source-run-id` has no GitHub Release for a tag-based trusted-publishing dispatch + to target, so `prepare` hard-errors that combination (`registries: ovsx` or `all`) instead of + silently never publishing. + ### Versioning Strategy These workflows use an **odd/even minor version convention** to distinguish release types: