From 22539af49ecaf517efc68bdf75d41cbce8d6e675 Mon Sep 17 00:00:00 2001 From: eitsupi <50911393+eitsupi@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:25:54 +0900 Subject: [PATCH 1/3] ci: publish daily Marketplace pre-releases with odd minor versions --- .github/workflows/main.yml | 16 +++++ .github/workflows/marketplace-pre-release.yml | 71 +++++++++++++++++++ .github/workflows/release.yml | 15 ++++ CONTRIBUTING.md | 28 ++++++++ README.md | 2 + scripts/extension-version.js | 58 +++++++++++++++ scripts/extension-version.test.js | 60 ++++++++++++++++ 7 files changed, 250 insertions(+) create mode 100644 .github/workflows/marketplace-pre-release.yml create mode 100644 scripts/extension-version.js create mode 100644 scripts/extension-version.test.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 26af18dac..43c2ec303 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,11 @@ on: branches: - main workflow_call: + inputs: + prerelease-date: + description: UTC date for the Marketplace pre-release (YYYY-MM-DD) + type: string + default: '' workflow_dispatch: concurrency: @@ -104,7 +109,18 @@ jobs: node-version: 24 cache: 'pnpm' - run: pnpm install --frozen-lockfile + - name: Test extension versioning + run: node --test scripts/extension-version.test.js + - name: Set Marketplace pre-release version + if: inputs.prerelease-date + env: + PRERELEASE_DATE: ${{ inputs.prerelease-date }} + run: node scripts/extension-version.js prerelease "$PRERELEASE_DATE" + - name: Package Marketplace pre-release + if: inputs.prerelease-date + run: pnpm exec vsce package --pre-release --no-dependencies -o $VSIX_FILE - name: Package extension + if: ${{ !inputs.prerelease-date }} run: pnpm exec vsce package --no-dependencies -o $VSIX_FILE - name: Verify packaged sess identity run: node .github/scripts/verify-sess-vsix.js diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml new file mode 100644 index 000000000..fea70014d --- /dev/null +++ b/.github/workflows/marketplace-pre-release.yml @@ -0,0 +1,71 @@ +name: marketplace-pre-release + +on: + schedule: + - cron: '23 3 * * *' + +concurrency: + group: marketplace-pre-release + cancel-in-progress: false + +permissions: + contents: read + actions: read + +jobs: + prepare: + if: github.repository == 'REditorSupport/vscode-R' + runs-on: ubuntu-latest + outputs: + publish: ${{ steps.previous.outputs.publish }} + date: ${{ steps.previous.outputs.date }} + steps: + - name: Check for changes since the last successful daily run + id: previous + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # A retry retains its original UTC date and therefore its version. + RUN_DATE=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq '.created_at[:10]') + PREVIOUS=$(gh api --method GET "repos/$GITHUB_REPOSITORY/actions/workflows/marketplace-pre-release.yml/runs" \ + -f event=schedule -f status=success -f branch=main -f per_page=100 \ + --jq "[.workflow_runs[] | select(.id != $GITHUB_RUN_ID)] | sort_by(.created_at) | last // empty") + if [ -n "$PREVIOUS" ]; then + PREVIOUS_SHA=$(jq -r '.head_sha' <<< "$PREVIOUS") + PREVIOUS_DATE=$(jq -r '.created_at[:10]' <<< "$PREVIOUS") + if [ "$PREVIOUS_SHA" = "$GITHUB_SHA" ] || [[ "$PREVIOUS_DATE" > "$RUN_DATE" ]] || [ "$PREVIOUS_DATE" = "$RUN_DATE" ]; then + echo 'No new daily pre-release to publish.' + exit 0 + fi + fi + echo "date=$RUN_DATE" >> "$GITHUB_OUTPUT" + echo 'publish=true' >> "$GITHUB_OUTPUT" + + verify: + needs: prepare + if: needs.prepare.outputs.publish == 'true' + uses: ./.github/workflows/main.yml + with: + prerelease-date: ${{ needs.prepare.outputs.date }} + + publish: + needs: [prepare, verify] + 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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - name: Download verified pre-release + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: vscode-R.vsix + path: artifacts/ + - name: Publish to Visual Studio Marketplace + env: + VSCE_PAT: ${{ secrets.VSCE_TOKEN }} + run: pnpm exec vsce publish --pre-release --skip-duplicate --packagePath artifacts/vscode-R.vsix diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2fe41f45f..14ee88ec7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,22 @@ env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: + check-version: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + - name: Check stable version and release tag + env: + RELEASE_TAG: ${{ github.ref_name }} + run: node scripts/extension-version.js check-stable "$RELEASE_TAG" + verify: + needs: check-version uses: ./.github/workflows/main.yml release: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64fa66c86..824abe464 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,3 +112,31 @@ Open . The harn The [analysis fixtures](src/test/examples/README.md) document public/research examples, reference plots, widget checks, and large-table allocation measurements. Keep repeatable procedures there; record dated results and environment-specific limitations in the PR discussion. For changes to persistence or supervision, also exercise the intended Remote SSH host: create managed arf sessions, adopt an existing terminal arf, create distinct objects, and submit jobs that stream text and plots. Close VS Code and disconnect SSH during execution, then reconnect. Verify the same R PIDs and objects, retained output, session isolation, usable plots, and no duplicate evaluation. Check that the adopted terminal remains usable and that stop/restart targets only the selected session. Local process tests do not establish server-specific logout, systemd, or network behavior. + +## Release versioning + +Stable releases use an even minor version (for example, `3.0.2` or `3.2.0`). +The release workflow rejects odd minors, non-numeric versions, and tags that do +not match `package.json`. Stable patch releases remain independent of daily +pre-releases. + +The Marketplace pre-release workflow runs daily at 03:23 UTC. It skips commits +already covered by a successful daily run, runs the existing build, lint and +all-OS tests, and publishes only the verified pre-release VSIX. The GitHub +`latest` development VSIX continues to update on every verified push. + +`scripts/extension-version.js prerelease YYYY-MM-DD` rewrites `package.json` +only in the packaging checkout. It uses the next odd minor after an even minor, +or keeps an explicitly selected odd minor, with a UTC `YYYYMMDD` patch: +`3.0.1` → `3.1.20261006`; after releasing `3.2.0`, builds use `3.3.YYYYMMDD`. +Keep the base version in `package.json`; do not commit generated daily versions. +For development toward `4.0.0`, maintainers can explicitly set the base version +to `3.999.0`, then change it to `4.0.0` for the stable release. + +The workflow reuses `VSCE_TOKEN` and publishes only to the VS Code Marketplace. +Retries retain the original run's UTC date and tolerate an already-published +version. A retry older than a successful daily run is skipped. No Git tags or +version-bump commits are created by daily publishing. + +Run `node --test scripts/extension-version.test.js` to test version generation +and stable-release validation without installing dependencies. diff --git a/README.md b/README.md index 1e2168650..b60c4563e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ for other changes and upgrade notes. code --install-extension REditorSupport.r ``` + To receive daily development updates, choose **Switch to Pre-Release Version** on the R extension's page in VS Code. New pre-releases are published to the Marketplace when `main` changes and verification passes. Choose **Switch to Release Version** to return to stable releases. When reporting problems, include the extension version and reproduction steps. + Alternatively, you can test the latest development version by installing from GitHub:
diff --git a/scripts/extension-version.js b/scripts/extension-version.js new file mode 100644 index 000000000..cfeadd4ba --- /dev/null +++ b/scripts/extension-version.js @@ -0,0 +1,58 @@ +const fs = require('node:fs'); +const path = require('node:path'); + +function parseVersion(version) { + if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) { + throw new Error(`Expected a numeric major.minor.patch version, got ${version}`); + } + const parts = version.split('.').map(Number); + if (parts.some(part => !Number.isSafeInteger(part) || part > 0xffffffff)) { + throw new Error(`Version component exceeds uint32: ${version}`); + } + return parts; +} + +function prereleaseVersion(version, date) { + const [major, minor] = parseVersion(version); + if (!/^\d{4}-\d{2}-\d{2}$/.test(date) + || new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) { + throw new Error(`Expected a UTC date in YYYY-MM-DD format, got ${date}`); + } + // Keep an explicitly selected odd series (e.g. 3.999 before 4.0). + const result = `${major}.${minor % 2 === 0 ? minor + 1 : minor}.${date.replaceAll('-', '')}`; + parseVersion(result); + return result; +} + +function checkStableVersion(version, tag) { + const [, minor] = parseVersion(version); + if (minor % 2 !== 0) { + throw new Error('Stable releases must use an even minor version'); + } + if (tag !== `v${version}`) { + throw new Error(`Release tag ${tag} does not match package.json version ${version}`); + } +} + +function rewriteVersion(manifestPath, date) { + const source = fs.readFileSync(manifestPath, 'utf8'); + const manifest = JSON.parse(source); + const version = prereleaseVersion(manifest.version, date); + // Change only the top-level version, preserving the manifest's formatting. + manifest.version = version; + fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + return version; +} + +module.exports = { parseVersion, prereleaseVersion, checkStableVersion, rewriteVersion }; +if (require.main === module) { + const [command, value] = process.argv.slice(2); + const manifestPath = path.join(__dirname, '..', 'package.json'); + if (command === 'prerelease') { + console.log(rewriteVersion(manifestPath, value)); + } else if (command === 'check-stable') { + checkStableVersion(JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version, value); + } else { + throw new Error('Usage: node scripts/extension-version.js '); + } +} diff --git a/scripts/extension-version.test.js b/scripts/extension-version.test.js new file mode 100644 index 000000000..0c382301f --- /dev/null +++ b/scripts/extension-version.test.js @@ -0,0 +1,60 @@ +const assert = require('node:assert/strict'); +const { test } = require('node:test'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { prereleaseVersion, checkStableVersion, rewriteVersion } = require('./extension-version'); + +test('daily versions are independent of stable patch releases', () => { + for (const version of ['3.0.1', '3.0.2', '3.0.99']) { + assert.equal(prereleaseVersion(version, '2026-10-06'), '3.1.20261006'); + } + assert.equal(prereleaseVersion('3.2.0', '2026-10-07'), '3.3.20261007'); + assert.equal(prereleaseVersion('3.999.0', '2026-10-07'), '3.999.20261007'); + assert.equal(prereleaseVersion('4.0.0', '2026-10-08'), '4.1.20261008'); +}); + +test('versions increase across days, years, stable releases and majors', () => { + const versions = ['3.0.2', prereleaseVersion('3.0.2', '2026-12-31'), + prereleaseVersion('3.0.2', '2027-01-01'), '3.2.0', + prereleaseVersion('3.999.0', '2027-01-02'), '4.0.0']; + const compare = (a, b) => { + const aa = a.split('.').map(Number); + const bb = b.split('.').map(Number); + return aa[0] - bb[0] || aa[1] - bb[1] || aa[2] - bb[2]; + }; + assert.deepEqual([...versions].sort(compare), versions); +}); + +test('stable releases require even minors and matching tags', () => { + checkStableVersion('3.0.2', 'v3.0.2'); + checkStableVersion('3.2.0', 'v3.2.0'); + checkStableVersion('4.0.0', 'v4.0.0'); + for (const [version, tag] of [['3.1.0', 'v3.1.0'], ['3.0.2', 'v3.0.1'], + ['3.0.2-rc.1', 'v3.0.2-rc.1'], ['3.00.2', 'v3.00.2']]) { + assert.throws(() => checkStableVersion(version, tag)); + } +}); + +test('invalid dates and non-Marketplace versions fail closed', () => { + for (const date of ['2026-02-30', '2026-13-01', '20261006', '']) { + assert.throws(() => prereleaseVersion('3.0.1', date)); + } + for (const version of ['3.0.1-rc.0', '3.0', '3.0.4294967296']) { + assert.throws(() => prereleaseVersion(version, '2026-10-06')); + } +}); + +test('rewrite changes only the version and is repeatable for retries', () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'extension-version-')); + try { + const file = path.join(directory, 'package.json'); + const original = { name: 'r', version: '3.0.1', contributes: { commands: [] } }; + fs.writeFileSync(file, `${JSON.stringify(original, null, 2)}\n`); + assert.equal(rewriteVersion(file, '2026-10-06'), '3.1.20261006'); + assert.equal(rewriteVersion(file, '2026-10-06'), '3.1.20261006'); + assert.deepEqual(JSON.parse(fs.readFileSync(file, 'utf8')), { ...original, version: '3.1.20261006' }); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); From 399d3498872ee218815100472b790810815d28a9 Mon Sep 17 00:00:00 2001 From: eitsupi <50911393+eitsupi@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:35:07 +0900 Subject: [PATCH 2/3] ci: move extension version scripts into .github/scripts --- {scripts => .github/scripts}/extension-version.js | 4 ++-- {scripts => .github/scripts}/extension-version.test.js | 0 .github/workflows/main.yml | 4 ++-- .github/workflows/release.yml | 2 +- CONTRIBUTING.md | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename {scripts => .github/scripts}/extension-version.js (91%) rename {scripts => .github/scripts}/extension-version.test.js (100%) diff --git a/scripts/extension-version.js b/.github/scripts/extension-version.js similarity index 91% rename from scripts/extension-version.js rename to .github/scripts/extension-version.js index cfeadd4ba..1d3723885 100644 --- a/scripts/extension-version.js +++ b/.github/scripts/extension-version.js @@ -47,12 +47,12 @@ function rewriteVersion(manifestPath, date) { module.exports = { parseVersion, prereleaseVersion, checkStableVersion, rewriteVersion }; if (require.main === module) { const [command, value] = process.argv.slice(2); - const manifestPath = path.join(__dirname, '..', 'package.json'); + const manifestPath = path.join(__dirname, '..', '..', 'package.json'); if (command === 'prerelease') { console.log(rewriteVersion(manifestPath, value)); } else if (command === 'check-stable') { checkStableVersion(JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version, value); } else { - throw new Error('Usage: node scripts/extension-version.js '); + throw new Error('Usage: node .github/scripts/extension-version.js '); } } diff --git a/scripts/extension-version.test.js b/.github/scripts/extension-version.test.js similarity index 100% rename from scripts/extension-version.test.js rename to .github/scripts/extension-version.test.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 43c2ec303..d4673978c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -110,12 +110,12 @@ jobs: cache: 'pnpm' - run: pnpm install --frozen-lockfile - name: Test extension versioning - run: node --test scripts/extension-version.test.js + run: node --test .github/scripts/extension-version.test.js - name: Set Marketplace pre-release version if: inputs.prerelease-date env: PRERELEASE_DATE: ${{ inputs.prerelease-date }} - run: node scripts/extension-version.js prerelease "$PRERELEASE_DATE" + run: node .github/scripts/extension-version.js prerelease "$PRERELEASE_DATE" - name: Package Marketplace pre-release if: inputs.prerelease-date run: pnpm exec vsce package --pre-release --no-dependencies -o $VSIX_FILE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 14ee88ec7..12a4142c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,7 +24,7 @@ jobs: - name: Check stable version and release tag env: RELEASE_TAG: ${{ github.ref_name }} - run: node scripts/extension-version.js check-stable "$RELEASE_TAG" + run: node .github/scripts/extension-version.js check-stable "$RELEASE_TAG" verify: needs: check-version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 824abe464..25472fb10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,7 +125,7 @@ already covered by a successful daily run, runs the existing build, lint and all-OS tests, and publishes only the verified pre-release VSIX. The GitHub `latest` development VSIX continues to update on every verified push. -`scripts/extension-version.js prerelease YYYY-MM-DD` rewrites `package.json` +`.github/scripts/extension-version.js prerelease YYYY-MM-DD` rewrites `package.json` only in the packaging checkout. It uses the next odd minor after an even minor, or keeps an explicitly selected odd minor, with a UTC `YYYYMMDD` patch: `3.0.1` → `3.1.20261006`; after releasing `3.2.0`, builds use `3.3.YYYYMMDD`. @@ -138,5 +138,5 @@ Retries retain the original run's UTC date and tolerate an already-published version. A retry older than a successful daily run is skipped. No Git tags or version-bump commits are created by daily publishing. -Run `node --test scripts/extension-version.test.js` to test version generation +Run `node --test .github/scripts/extension-version.test.js` to test version generation and stable-release validation without installing dependencies. From c00f3ccfa6e6ff7fc6b8419cf944c131675cc26b Mon Sep 17 00:00:00 2001 From: eitsupi <50911393+eitsupi@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:42:04 +0900 Subject: [PATCH 3/3] ci: also publish daily pre-releases to Open VSX --- .github/workflows/marketplace-pre-release.yml | 4 ++++ CONTRIBUTING.md | 7 +++++-- README.md | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/marketplace-pre-release.yml b/.github/workflows/marketplace-pre-release.yml index fea70014d..a7246da57 100644 --- a/.github/workflows/marketplace-pre-release.yml +++ b/.github/workflows/marketplace-pre-release.yml @@ -69,3 +69,7 @@ jobs: env: VSCE_PAT: ${{ secrets.VSCE_TOKEN }} run: pnpm exec vsce publish --pre-release --skip-duplicate --packagePath artifacts/vscode-R.vsix + - name: Publish to Open VSX Registry + env: + OVSX_PAT: ${{ secrets.OPEN_VSX_TOKEN }} + run: pnpm exec ovsx publish --skip-duplicate artifacts/vscode-R.vsix diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25472fb10..f843d6b12 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,9 +133,12 @@ Keep the base version in `package.json`; do not commit generated daily versions. For development toward `4.0.0`, maintainers can explicitly set the base version to `3.999.0`, then change it to `4.0.0` for the stable release. -The workflow reuses `VSCE_TOKEN` and publishes only to the VS Code Marketplace. +The workflow reuses `VSCE_TOKEN` and `OPEN_VSX_TOKEN` to publish the same VSIX +to the VS Code Marketplace and Open VSX Registry. Both publications must succeed +for the daily run to count as successful. Retries retain the original run's UTC date and tolerate an already-published -version. A retry older than a successful daily run is skipped. No Git tags or +version in either registry, allowing a partially failed publication to recover. +A retry older than a successful daily run is skipped. No Git tags or version-bump commits are created by daily publishing. Run `node --test .github/scripts/extension-version.test.js` to test version generation diff --git a/README.md b/README.md index b60c4563e..38f309978 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ for other changes and upgrade notes. code --install-extension REditorSupport.r ``` - To receive daily development updates, choose **Switch to Pre-Release Version** on the R extension's page in VS Code. New pre-releases are published to the Marketplace when `main` changes and verification passes. Choose **Switch to Release Version** to return to stable releases. When reporting problems, include the extension version and reproduction steps. + To receive daily development updates, choose **Switch to Pre-Release Version** on the R extension's page in VS Code. New pre-releases are published to the VS Code Marketplace and Open VSX Registry when `main` changes and verification passes. Choose **Switch to Release Version** to return to stable releases. When reporting problems, include the extension version and reproduction steps. Alternatively, you can test the latest development version by installing from GitHub: