From 19c87c1734a54a28d90430431c68d306d5352f23 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:10:34 +0000 Subject: [PATCH 01/10] ci: make registry publishing parallel and retry-safe --- .github/workflows/marketplace-pre-release.yml | 30 +++++++++++- .github/workflows/release.yml | 47 +++++++++++++++---- CHANGELOG.md | 4 ++ 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml index 8d9e4e559..193f388ec 100644 --- a/.github/workflows/marketplace-pre-release.yml +++ b/.github/workflows/marketplace-pre-release.yml @@ -39,7 +39,8 @@ jobs: with: prerelease-date: ${{ needs.prepare.outputs.date }} - publish: + publish-vs-marketplace: + environment: vs-marketplace needs: [prepare, verify] if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest @@ -67,8 +68,33 @@ jobs: env: VSCE_PAT: ${{ secrets.VSCE_TOKEN }} run: pnpm exec vsce publish --pre-release --skip-duplicate --packagePath artifacts/vscode-R.vsix + + publish-open-vsx: + environment: open-vsx + needs: [prepare, verify] + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - name: Download verified pre-release + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: vscode-R.vsix + path: artifacts/ + - name: Recheck publication eligibility + id: eligibility + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node .github/scripts/marketplace-pre-release-history.js - name: Publish to Open VSX Registry if: steps.eligibility.outputs.publish == 'true' env: OVSX_PAT: ${{ secrets.OPEN_VSX_TOKEN }} - run: pnpm exec ovsx publish --skip-duplicate artifacts/vscode-R.vsix + run: pnpm exec ovsx publish --skip-duplicate --packagePath artifacts/vscode-R.vsix diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d14182b7e..dc1ed79ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,8 @@ on: tags: - "v*" -env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} +permissions: + contents: read jobs: check-version: @@ -32,6 +32,8 @@ jobs: release: name: Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} needs: verify runs-on: ubuntu-latest permissions: @@ -48,18 +50,21 @@ jobs: run: | VERSION=${GITHUB_REF#refs/tags/v} mv artifacts/vscode-R.vsix "artifacts/r-$VERSION.vsix" + if gh release view "v$VERSION" >/dev/null 2>&1; then + echo "Release v$VERSION already exists; skipping creation." + exit 0 + fi gh release create "v$VERSION" "artifacts/r-$VERSION.vsix" \ --title "v$VERSION" \ --notes "Release v$VERSION" \ --generate-notes - publish: - name: Publish + publish-vs-marketplace: + name: Publish to Visual Studio Marketplace + environment: vs-marketplace timeout-minutes: 30 runs-on: ubuntu-latest - needs: - - verify - - release + needs: verify steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 @@ -74,6 +79,30 @@ jobs: name: vscode-R.vsix path: artifacts/ - name: Publish to Visual Studio Marketplace - run: pnpm exec vsce publish -p ${{ secrets.VSCE_TOKEN }} --packagePath artifacts/*.vsix + env: + VSCE_PAT: ${{ secrets.VSCE_TOKEN }} + run: pnpm exec vsce publish --skip-duplicate --packagePath artifacts/*.vsix + + publish-open-vsx: + name: Publish to Open VSX Registry + environment: open-vsx + timeout-minutes: 30 + runs-on: ubuntu-latest + needs: verify + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - name: Download artifacts + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: vscode-R.vsix + path: artifacts/ - name: Publish to Open VSX Registry - run: pnpm exec ovsx publish -p ${{ secrets.OPEN_VSX_TOKEN }} --packagePath artifacts/*.vsix + env: + OVSX_PAT: ${{ secrets.OPEN_VSX_TOKEN }} + run: pnpm exec ovsx publish --skip-duplicate --packagePath artifacts/*.vsix diff --git a/CHANGELOG.md b/CHANGELOG.md index f199fc4b1..9035e8375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Internals and development builds + +- Made release publishing retry-safe by publishing to the VS Code Marketplace and Open VSX in independent jobs and skipping existing releases and extension versions. + ### Compatibility and upgrade notes - Raised the minimum supported VS Code version to **1.101.0** (Node.js 22). (#1850) From 1d67e8570c483d5dc14bdd1f2f74e367b8d25dc3 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:12:09 +0000 Subject: [PATCH 02/10] ci: recognize parallel registry publication history --- .../marketplace-pre-release-history.js | 27 ++++---- .../marketplace-pre-release-history.test.js | 64 +++++++++++++------ .github/workflows/marketplace-pre-release.yml | 8 ++- CHANGELOG.md | 4 -- 4 files changed, 63 insertions(+), 40 deletions(-) diff --git a/.github/scripts/marketplace-pre-release-history.js b/.github/scripts/marketplace-pre-release-history.js index c4dbbda0c..0645dec73 100644 --- a/.github/scripts/marketplace-pre-release-history.js +++ b/.github/scripts/marketplace-pre-release-history.js @@ -2,7 +2,8 @@ const { execFileSync } = require('node:child_process'); const { appendFileSync } = require('node:fs'); const WORKFLOW = 'marketplace-pre-release.yml'; -const PUBLISH_JOB = 'publish'; +// Include the former combined job when inspecting existing Actions history. +const PUBLISH_JOBS = ['publish', 'publish-vs-marketplace', 'publish-open-vsx']; const REGISTRY_STEPS = [ 'Publish to Visual Studio Marketplace', 'Publish to Open VSX Registry', @@ -111,20 +112,20 @@ function createGhApi(execFile = execFileSync) { }; } -function didAttemptPublication(job) { - if (job.name !== PUBLISH_JOB || !Array.isArray(job.steps)) { - return false; - } - return job.steps.some((step) => REGISTRY_STEPS.includes(step.name) +function registrySteps(jobs) { + return jobs.filter((job) => PUBLISH_JOBS.includes(job.name)) + .flatMap((job) => Array.isArray(job.steps) ? job.steps : []); +} + +function didAttemptPublication(jobs) { + return registrySteps(jobs).some((step) => REGISTRY_STEPS.includes(step.name) && (step.status === 'in_progress' || step.status === 'completed') && step.conclusion !== 'skipped'); } -function completedBothRegistries(job) { - if (job.name !== PUBLISH_JOB || !Array.isArray(job.steps)) { - return false; - } - return REGISTRY_STEPS.every((name) => job.steps.some((step) => +function completedBothRegistries(jobs) { + const steps = registrySteps(jobs); + return REGISTRY_STEPS.every((name) => steps.some((step) => step.name === name && step.status === 'completed' && step.conclusion === 'success')); } @@ -170,7 +171,7 @@ async function decidePublication({ api, repository, runId, sha }) { const isCurrentRun = String(run.id) === String(runId); if (!isCurrentRun && utcDate(run) >= runDate) { const jobs = await jobsFor(run); - if (jobs.some(didAttemptPublication)) { + if (didAttemptPublication(jobs)) { dateReserved = true; break; } @@ -192,7 +193,7 @@ async function decidePublication({ api, repository, runId, sha }) { } for (let index = end - 1; index >= start; index -= 1) { const jobs = await jobsFor(runs[index]); - if (jobs.some(completedBothRegistries)) { + if (completedBothRegistries(jobs)) { return runs[start].head_sha === sha ? { publish: false, date: runDate, reason: 'unchanged' } : { publish: true, date: runDate, reason: 'new-sha' }; diff --git a/.github/scripts/marketplace-pre-release-history.test.js b/.github/scripts/marketplace-pre-release-history.test.js index 561e07139..1451a7bff 100644 --- a/.github/scripts/marketplace-pre-release-history.test.js +++ b/.github/scripts/marketplace-pre-release-history.test.js @@ -2,7 +2,7 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const { readFileSync } = require('node:fs'); const { join } = require('node:path'); -const { createGhApi, decidePublication, pagesToItems, completePagesToItems } = require('./marketplace-pre-release-history'); +const { createGhApi, decidePublication, pagesToItems, completePagesToItems, didAttemptPublication, completedBothRegistries } = require('./marketplace-pre-release-history'); const marketplace = 'Publish to Visual Studio Marketplace'; const openVsx = 'Publish to Open VSX Registry'; @@ -335,25 +335,47 @@ test('run pagination fails closed on empty, inconsistent, or duplicate pages', ( : { total_count: 101, workflow_runs: [item(100)] }).listRuns('owner/repo'), /duplicate workflow runs/); }); -test('workflow rechecks eligibility in the publishing job and gates both registries', () => { - const workflowPath = join(__dirname, '../workflows/marketplace-pre-release.yml'); - const workflow = readFileSync(workflowPath, 'utf8'); - const prepareStart = workflow.indexOf(' prepare:'); - const publishStart = workflow.indexOf('\n publish:\n needs:'); - const prepare = workflow.slice(prepareStart, publishStart); - const helperIndex = prepare.indexOf('.github/scripts/marketplace-pre-release-history.js'); - assert.ok(helperIndex >= 0); - assert.ok(prepare.slice(0, helperIndex).includes('uses: actions/checkout@')); - assert.match(prepare.slice(0, helperIndex), /uses: actions\/setup-node@[\s\S]*?node-version: 24/); - const publish = workflow.slice(publishStart); - const eligibilityIndex = publish.indexOf('id: eligibility'); - assert.ok(eligibilityIndex >= 0); - assert.match(publish.slice(eligibilityIndex), /run: node \.github\/scripts\/marketplace-pre-release-history\.js/); - for (const name of [marketplace, openVsx]) { - const index = publish.indexOf(`- name: ${name}`); - assert.ok(index > eligibilityIndex, `${name} must run after the eligibility check`); - const nextStep = publish.indexOf('\n - name:', index + 1); - const step = publish.slice(index, nextStep < 0 ? undefined : nextStep); - assert.match(step, /if: steps\.eligibility\.outputs\.publish == 'true'/); +test('each split publishing job rechecks eligibility and gates its registry', () => { + const workflow = readFileSync(join(__dirname, '../workflows/marketplace-pre-release.yml'), 'utf8'); + for (const [job, name] of [['publish-vs-marketplace', marketplace], ['publish-open-vsx', openVsx]]) { + const start = workflow.indexOf(`\n ${job}:`); + assert.ok(start >= 0); + const end = workflow.indexOf('\n publish-', start + 1); + const body = workflow.slice(start, end < 0 ? undefined : end); + const eligibility = body.indexOf('id: eligibility'); + assert.ok(eligibility >= 0); + assert.ok(body.indexOf('.github/scripts/marketplace-pre-release-history.js', eligibility) > eligibility); + const publication = body.indexOf(`- name: ${name}`); + assert.ok(publication > eligibility); + assert.ok(body.slice(publication).includes("if: steps.eligibility.outputs.publish == 'true'")); } }); + +function splitJobs(options) { + return publishJob(options).steps.map((step, index) => ({ + name: ['publish-vs-marketplace', 'publish-open-vsx'][index], steps: [step], + })); +} + +for (const [format, jobsFor] of [['split jobs', splitJobs], ['old combined job', (options) => [publishJob(options)]]]) { + test(`${format}: both registries succeeded suppress unchanged SHA`, async () => { + const prior = run(800, '2026-10-05', 'sha-a'); + const current = run(801, '2026-10-06', 'sha-a'); + const jobs = jobsFor(); + assert.equal(didAttemptPublication(jobs), true); + assert.equal(completedBothRegistries(jobs), true); + assert.deepEqual(await decide(current, [prior, current], new Map([['800', jobs]])), { + publish: false, date: '2026-10-06', reason: 'unchanged', + }); + }); + test(`${format}: partial publication reserves date without completing publication`, async () => { + const prior = run(810, '2026-10-05', 'sha-a'); + const current = run(811, '2026-10-05', 'sha-a'); + const jobs = jobsFor({ openVsxConclusion: 'failure' }); + assert.equal(didAttemptPublication(jobs), true); + assert.equal(completedBothRegistries(jobs), false); + assert.deepEqual(await decide(current, [prior, current], new Map([['810', jobs]])), { + publish: false, date: '2026-10-05', reason: 'date-reserved', + }); + }); +} diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml index 193f388ec..9811d5e08 100644 --- a/.github/workflows/marketplace-pre-release.yml +++ b/.github/workflows/marketplace-pre-release.yml @@ -41,7 +41,9 @@ jobs: publish-vs-marketplace: environment: vs-marketplace - needs: [prepare, verify] + needs: + - prepare + - verify if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 30 @@ -71,7 +73,9 @@ jobs: publish-open-vsx: environment: open-vsx - needs: [prepare, verify] + needs: + - prepare + - verify if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9035e8375..f199fc4b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,6 @@ ## Unreleased -### Internals and development builds - -- Made release publishing retry-safe by publishing to the VS Code Marketplace and Open VSX in independent jobs and skipping existing releases and extension versions. - ### Compatibility and upgrade notes - Raised the minimum supported VS Code version to **1.101.0** (Node.js 22). (#1850) From 2db3919a273387c838d26eac7e726b2220da8d54 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:14:58 +0000 Subject: [PATCH 03/10] ci: defer publish environments until admin setup --- .github/workflows/marketplace-pre-release.yml | 6 ++++-- .github/workflows/release.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml index 9811d5e08..602c552e4 100644 --- a/.github/workflows/marketplace-pre-release.yml +++ b/.github/workflows/marketplace-pre-release.yml @@ -40,7 +40,8 @@ jobs: prerelease-date: ${{ needs.prepare.outputs.date }} publish-vs-marketplace: - environment: vs-marketplace + # TODO: Uncomment after a repository admin creates the vs-marketplace environment (for trusted publishing) and moves VSCE_TOKEN into it. + # environment: vs-marketplace needs: - prepare - verify @@ -72,7 +73,8 @@ jobs: run: pnpm exec vsce publish --pre-release --skip-duplicate --packagePath artifacts/vscode-R.vsix publish-open-vsx: - environment: open-vsx + # TODO: Uncomment after a repository admin creates the open-vsx environment (for trusted publishing) and moves OPEN_VSX_TOKEN into it. + # environment: open-vsx needs: - prepare - verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dc1ed79ca..f4f7023ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,8 @@ jobs: publish-vs-marketplace: name: Publish to Visual Studio Marketplace - environment: vs-marketplace + # TODO: Uncomment after a repository admin creates the vs-marketplace environment (for trusted publishing) and moves VSCE_TOKEN into it. + # environment: vs-marketplace timeout-minutes: 30 runs-on: ubuntu-latest needs: verify @@ -85,7 +86,8 @@ jobs: publish-open-vsx: name: Publish to Open VSX Registry - environment: open-vsx + # TODO: Uncomment after a repository admin creates the open-vsx environment (for trusted publishing) and moves OPEN_VSX_TOKEN into it. + # environment: open-vsx timeout-minutes: 30 runs-on: ubuntu-latest needs: verify From 988ab97e6917b53b1fd627cb14a586e4d4abab8d Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:16:17 +0000 Subject: [PATCH 04/10] ci: clarify TODO for trusted publishing environments --- .github/workflows/marketplace-pre-release.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml index 602c552e4..837157b8c 100644 --- a/.github/workflows/marketplace-pre-release.yml +++ b/.github/workflows/marketplace-pre-release.yml @@ -40,7 +40,7 @@ jobs: prerelease-date: ${{ needs.prepare.outputs.date }} publish-vs-marketplace: - # TODO: Uncomment after a repository admin creates the vs-marketplace environment (for trusted publishing) and moves VSCE_TOKEN into it. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the vs-marketplace environment first. # environment: vs-marketplace needs: - prepare @@ -73,7 +73,7 @@ jobs: run: pnpm exec vsce publish --pre-release --skip-duplicate --packagePath artifacts/vscode-R.vsix publish-open-vsx: - # TODO: Uncomment after a repository admin creates the open-vsx environment (for trusted publishing) and moves OPEN_VSX_TOKEN into it. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the open-vsx environment first. # environment: open-vsx needs: - prepare diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4f7023ca..faea8cf0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: publish-vs-marketplace: name: Publish to Visual Studio Marketplace - # TODO: Uncomment after a repository admin creates the vs-marketplace environment (for trusted publishing) and moves VSCE_TOKEN into it. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the vs-marketplace environment first. # environment: vs-marketplace timeout-minutes: 30 runs-on: ubuntu-latest @@ -86,7 +86,7 @@ jobs: publish-open-vsx: name: Publish to Open VSX Registry - # TODO: Uncomment after a repository admin creates the open-vsx environment (for trusted publishing) and moves OPEN_VSX_TOKEN into it. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the open-vsx environment first. # environment: open-vsx timeout-minutes: 30 runs-on: ubuntu-latest From 86d0a645ea7031ea96669311af5c7d315553f301 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:30:39 +0000 Subject: [PATCH 05/10] ci: create GitHub Release after both registries and repair missing assets --- .../marketplace-pre-release-history.test.js | 13 +++++++++++++ .github/workflows/release.yml | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/scripts/marketplace-pre-release-history.test.js b/.github/scripts/marketplace-pre-release-history.test.js index 1451a7bff..19e711d92 100644 --- a/.github/scripts/marketplace-pre-release-history.test.js +++ b/.github/scripts/marketplace-pre-release-history.test.js @@ -357,6 +357,19 @@ function splitJobs(options) { })); } +test('split jobs: re-running only the failed registry job completes the publication', async () => { + const prior = run(820, '2026-10-05', 'sha-a'); + const current = run(821, '2026-10-06', 'sha-a'); + const firstAttempt = splitJobs({ openVsxConclusion: 'failure' }); + const retriedOpenVsx = { ...splitJobs()[1], run_attempt: 2 }; + const jobs = [...firstAttempt, retriedOpenVsx]; + assert.equal(completedBothRegistries(firstAttempt), false); + assert.equal(completedBothRegistries(jobs), true); + assert.deepEqual(await decide(current, [prior, current], new Map([['820', jobs]])), { + publish: false, date: '2026-10-06', reason: 'unchanged', + }); +}); + for (const [format, jobsFor] of [['split jobs', splitJobs], ['old combined job', (options) => [publishJob(options)]]]) { test(`${format}: both registries succeeded suppress unchanged SHA`, async () => { const prior = run(800, '2026-10-05', 'sha-a'); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index faea8cf0c..5a4008ef6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,10 @@ jobs: name: Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - needs: verify + # Create the GitHub Release only after both registries have the version. + needs: + - publish-vs-marketplace + - publish-open-vsx runs-on: ubuntu-latest permissions: contents: write @@ -51,13 +54,15 @@ jobs: VERSION=${GITHUB_REF#refs/tags/v} mv artifacts/vscode-R.vsix "artifacts/r-$VERSION.vsix" if gh release view "v$VERSION" >/dev/null 2>&1; then - echo "Release v$VERSION already exists; skipping creation." - exit 0 + # A previous attempt may have created the release without the asset. + echo "Release v$VERSION already exists; uploading the VSIX." + gh release upload "v$VERSION" "artifacts/r-$VERSION.vsix" --clobber + else + gh release create "v$VERSION" "artifacts/r-$VERSION.vsix" \ + --title "v$VERSION" \ + --notes "Release v$VERSION" \ + --generate-notes fi - gh release create "v$VERSION" "artifacts/r-$VERSION.vsix" \ - --title "v$VERSION" \ - --notes "Release v$VERSION" \ - --generate-notes publish-vs-marketplace: name: Publish to Visual Studio Marketplace From bbab8f477f3f65d5b0b1762d2e4e4c062a659d2e Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:41:16 +0000 Subject: [PATCH 06/10] ci: keep GitHub Release independent of registry publishing --- .github/workflows/release.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a4008ef6..d743c09fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,10 +34,9 @@ jobs: name: Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Create the GitHub Release only after both registries have the version. - needs: - - publish-vs-marketplace - - publish-open-vsx + # Independent of the registry jobs so the VSIX stays available as a + # fallback when a registry is down. + needs: verify runs-on: ubuntu-latest permissions: contents: write From e38a1897b191f0fa19e19695d41379d8d1c7b578 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:46:48 +0000 Subject: [PATCH 07/10] ci: note trusted publishing constraints in environment TODOs --- .github/workflows/marketplace-pre-release.yml | 10 ++++++++-- .github/workflows/release.yml | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml index 837157b8c..427cddddb 100644 --- a/.github/workflows/marketplace-pre-release.yml +++ b/.github/workflows/marketplace-pre-release.yml @@ -40,7 +40,9 @@ jobs: prerelease-date: ${{ needs.prepare.outputs.date }} publish-vs-marketplace: - # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the vs-marketplace environment first. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token). + # A repository admin must create the vs-marketplace environment, and each + # publishing workflow file needs its own Marketplace trusted publishing policy. # environment: vs-marketplace needs: - prepare @@ -73,7 +75,11 @@ jobs: run: pnpm exec vsce publish --pre-release --skip-duplicate --packagePath artifacts/vscode-R.vsix publish-open-vsx: - # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the open-vsx environment first. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token). + # A repository admin must create the open-vsx environment. Open VSX may allow + # only one trusted publisher per extension, so stable and pre-release + # publishing may first need to be consolidated into a single workflow file + # (a shared reusable workflow may not be enough, since the caller differs). # environment: open-vsx needs: - prepare diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d743c09fc..5bed764e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,9 @@ jobs: publish-vs-marketplace: name: Publish to Visual Studio Marketplace - # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the vs-marketplace environment first. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token). + # A repository admin must create the vs-marketplace environment, and each + # publishing workflow file needs its own Marketplace trusted publishing policy. # environment: vs-marketplace timeout-minutes: 30 runs-on: ubuntu-latest @@ -90,7 +92,11 @@ jobs: publish-open-vsx: name: Publish to Open VSX Registry - # TODO: Uncomment when switching to trusted publishing (OIDC, no token); a repository admin must create the open-vsx environment first. + # TODO: Uncomment when switching to trusted publishing (OIDC, no token). + # A repository admin must create the open-vsx environment. Open VSX may allow + # only one trusted publisher per extension, so stable and pre-release + # publishing may first need to be consolidated into a single workflow file + # (a shared reusable workflow may not be enough, since the caller differs). # environment: open-vsx timeout-minutes: 30 runs-on: ubuntu-latest From e4979205fafae2c1f9886bdc61ab3def06a4657d Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 14:48:00 +0000 Subject: [PATCH 08/10] ci: state the Open VSX single trusted publisher limit --- .github/workflows/marketplace-pre-release.yml | 9 +++++---- .github/workflows/release.yml | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml index 427cddddb..5c9394fb0 100644 --- a/.github/workflows/marketplace-pre-release.yml +++ b/.github/workflows/marketplace-pre-release.yml @@ -76,10 +76,11 @@ jobs: publish-open-vsx: # TODO: Uncomment when switching to trusted publishing (OIDC, no token). - # A repository admin must create the open-vsx environment. Open VSX may allow - # only one trusted publisher per extension, so stable and pre-release - # publishing may first need to be consolidated into a single workflow file - # (a shared reusable workflow may not be enough, since the caller differs). + # A repository admin must create the open-vsx environment. Open VSX allows + # only one trusted publisher (bound to one workflow file) per extension, so + # stable and pre-release publishing must first be consolidated into a single + # workflow file (a shared reusable workflow may not be enough, since the + # caller differs). # environment: open-vsx needs: - prepare diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bed764e1..f636c8651 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,10 +93,11 @@ jobs: publish-open-vsx: name: Publish to Open VSX Registry # TODO: Uncomment when switching to trusted publishing (OIDC, no token). - # A repository admin must create the open-vsx environment. Open VSX may allow - # only one trusted publisher per extension, so stable and pre-release - # publishing may first need to be consolidated into a single workflow file - # (a shared reusable workflow may not be enough, since the caller differs). + # A repository admin must create the open-vsx environment. Open VSX allows + # only one trusted publisher (bound to one workflow file) per extension, so + # stable and pre-release publishing must first be consolidated into a single + # workflow file (a shared reusable workflow may not be enough, since the + # caller differs). # environment: open-vsx timeout-minutes: 30 runs-on: ubuntu-latest From 0dd3855e0fe04a20d7a1d27661861fa2e3f61ab4 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 22:23:46 +0000 Subject: [PATCH 09/10] ci: publish a GitHub Release draft left by an interrupted attempt gh release create uploads assets to a draft and publishes it afterwards, so an interrupted attempt can leave a draft that gh release view still finds. After uploading the VSIX, publish the release if it is a draft. --- .github/workflows/release.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f636c8651..3714677b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,10 +52,16 @@ jobs: run: | VERSION=${GITHUB_REF#refs/tags/v} mv artifacts/vscode-R.vsix "artifacts/r-$VERSION.vsix" - if gh release view "v$VERSION" >/dev/null 2>&1; then + if IS_DRAFT=$(gh release view "v$VERSION" --json isDraft --jq .isDraft 2>/dev/null); then # A previous attempt may have created the release without the asset. echo "Release v$VERSION already exists; uploading the VSIX." gh release upload "v$VERSION" "artifacts/r-$VERSION.vsix" --clobber + # gh release create uploads assets to a draft before publishing it, + # so an interrupted attempt can leave the release as a draft. + if [ "$IS_DRAFT" = "true" ]; then + echo "Release v$VERSION is a draft; publishing it." + gh release edit "v$VERSION" --draft=false + fi else gh release create "v$VERSION" "artifacts/r-$VERSION.vsix" \ --title "v$VERSION" \ From cb6463531fa61318127464967c7bb749cd9d8a93 Mon Sep 17 00:00:00 2001 From: eitsupi Date: Sun, 8 Nov 2026 22:33:33 +0000 Subject: [PATCH 10/10] ci: keep an existing release VSIX instead of replacing it gh release upload --clobber deletes the existing asset before uploading, so a failed upload on a rerun could remove a download that was already available. Upload only when the release has no completed VSIX. --- .github/workflows/release.yml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3714677b4..93bd37ca3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,19 +51,29 @@ jobs: - name: Create release run: | VERSION=${GITHUB_REF#refs/tags/v} - mv artifacts/vscode-R.vsix "artifacts/r-$VERSION.vsix" - if IS_DRAFT=$(gh release view "v$VERSION" --json isDraft --jq .isDraft 2>/dev/null); then + ASSET="r-$VERSION.vsix" + mv artifacts/vscode-R.vsix "artifacts/$ASSET" + if RELEASE=$(gh release view "v$VERSION" --json isDraft,assets 2>/dev/null); then # A previous attempt may have created the release without the asset. - echo "Release v$VERSION already exists; uploading the VSIX." - gh release upload "v$VERSION" "artifacts/r-$VERSION.vsix" --clobber + # Keep a completed asset: --clobber deletes it before uploading, so + # a failed upload would remove the existing download. + if jq -e --arg name "$ASSET" \ + '.assets | any(.name == $name and .state == "uploaded")' \ + <<<"$RELEASE" >/dev/null; then + echo "Release v$VERSION already has $ASSET; skipping the upload." + else + echo "Release v$VERSION already exists; uploading $ASSET." + # --clobber replaces an incomplete asset with the same name. + gh release upload "v$VERSION" "artifacts/$ASSET" --clobber + fi # gh release create uploads assets to a draft before publishing it, # so an interrupted attempt can leave the release as a draft. - if [ "$IS_DRAFT" = "true" ]; then + if [ "$(jq -r .isDraft <<<"$RELEASE")" = "true" ]; then echo "Release v$VERSION is a draft; publishing it." gh release edit "v$VERSION" --draft=false fi else - gh release create "v$VERSION" "artifacts/r-$VERSION.vsix" \ + gh release create "v$VERSION" "artifacts/$ASSET" \ --title "v$VERSION" \ --notes "Release v$VERSION" \ --generate-notes