Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/scripts/label-release-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Marks the open release-please PR with the channel it will publish to.
#
# Every cycle ships twice - an odd-minor pre-release first, then the even-minor
# release (see VERSIONING.md). Which one a Release PR is, is decided by the
# version it carries, so this says it on the PR instead of making the reviewer
# work out the minor's parity.
#
# Labels rather than renames on purpose: release-please parses the Release PR
# *title* to recover the version when the PR merges ("must be able to parse out
# the component and version from the pull request"), so editing the title risks
# breaking the release. Labels and comments are not parsed.
#
# Requires GH_TOKEN and REPO in the environment. Safe to run repeatedly.
set -euo pipefail

pr=$(gh pr list --repo "$REPO" --state open --limit 50 \
--json number,headRefName,title \
--jq '[.[] | select(.headRefName | startswith("release-please"))][0] // empty')

if [ -z "$pr" ]; then
echo "No open Release PR; nothing to label."
exit 0
fi

number=$(echo "$pr" | jq -r .number)
version=$(echo "$pr" | jq -r .title | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)

if [ -z "$version" ]; then
echo "::warning::Could not read a version out of Release PR #$number; skipping the channel label."
exit 0
fi

minor=$(echo "$version" | cut -d. -f2)
if [ $((minor % 2)) -eq 1 ]; then
keep="pre-release"
drop="release"
colour="fbca04"
note="🚧 **Pre-release \`$version\`** (odd minor). Merging this publishes to both registries with \`--pre-release\`, so only users who opted into pre-releases get it. The matching **release** PR is opened automatically once this one has shipped."
else
keep="release"
drop="pre-release"
colour="0e8a16"
note="🚀 **Release \`$version\`** (even minor). Merging this publishes the stable version to both registries."
fi

echo "Release PR #$number carries $version -> $keep"

# --force makes label creation idempotent across runs.
gh label create "$keep" --repo "$REPO" --color "$colour" \
--description "Release channel, see VERSIONING.md" --force

gh pr edit "$number" --repo "$REPO" --add-label "$keep"
# The PR can carry the other label from an earlier version proposal; removing a
# label that isn't there is an error, so this is allowed to fail.
gh pr edit "$number" --repo "$REPO" --remove-label "$drop" 2>/dev/null || true

# Only say it once - release-please amends this PR on every push to main.
if gh pr view "$number" --repo "$REPO" --json comments --jq '.comments[].body' \
| grep -qF "\`$version\`"; then
echo "Channel comment for $version already present."
else
gh pr comment "$number" --repo "$REPO" --body "$note"
fi
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
name: CI

# Least privilege for the GITHUB_TOKEN: this workflow only reads the repo.
# Without it the token inherits the repository default (read-write) and
# actions/checkout leaves it in .git/config for every later step to use.
permissions:
contents: read

on:
push:
branches:
- main
- develop
pull_request:
# Fallback for release-please.yml. The Release PR does fire `pull_request`,
# but GitHub can park that run at `action_required` when the repository
# requires approval for workflows from outside/first-time contributors - the
# PR's author is github-actions[bot], which trips that policy. release-please.yml
# approves the parked run (the normal path, and it puts the check on the PR
# where it belongs); if that approval is refused it dispatches this workflow
# instead, since workflow_dispatch is never gated.
workflow_dispatch:

jobs:
test:
Expand Down
150 changes: 150 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Builds the .vsix once and publishes that exact artifact to both registries:
# the Visual Studio Marketplace (vsce) and the Open VSX Registry (ovsx). Open
# VSX is what Cursor, VSCodium, Windsurf, Gitpod and Theia install from - see
# issue #18.
name: publish

on:
# Called by release-please.yml right after it creates a release. This is the
# normal path: a release created with GITHUB_TOKEN does NOT emit a usable
# `release` event ("Events triggered by the GITHUB_TOKEN will not create a
# new workflow run"), so the release workflow has to invoke this one itself.
workflow_call:
inputs:
tag:
description: Tag to publish.
required: true
type: string
publish:
description: Publish to the registries, or only build the package.
required: false
type: boolean
default: true
# Still useful for releases published by a human in the GitHub UI, which do
# emit the event.
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Tag or ref to (re)publish (e.g. v1.3.0, or main). Must exist on the remote.
required: true
type: string
publish:
description: Actually publish to the registries (vs build-only smoke test)?
type: boolean
default: true

permissions:
contents: write

concurrency:
group: extension-publish
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
OVSX_PAT: ${{ secrets.OVSX_PAT }}
# A `release` event carries no inputs and always means "publish".
SHOULD_PUBLISH: ${{ github.event_name == 'release' || inputs.publish }}
steps:
- uses: actions/checkout@v4
with:
# `inputs` covers both workflow_call and workflow_dispatch; on a
# `release` event it is empty and github.ref is the release tag.
ref: ${{ inputs.tag || github.ref }}

- uses: actions/setup-node@v4
with:
# Kept explicit rather than read from a file, because this workflow
# also checks out old tags to re-publish them. Keep in step with ci.yml.
node-version: 24
cache: npm

- name: Install dependencies
run: npm ci

# The version decides the channel - there is no input to get wrong.
# VERSIONING.md: odd minor (1.3.x) = pre-release, even minor (1.4.x) =
# release. Every cycle ships both, so this runs twice per feature: once
# for the odd pre-release, once for the even release.
- name: Determine release channel
id: channel
run: |
version=$(node -p "require('./package.json').version")
minor=$(echo "$version" | cut -d. -f2)
if [ $((minor % 2)) -eq 1 ]; then
prerelease=true
channel=pre-release
else
prerelease=false
channel=release
fi
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
echo "channel=$channel" >> "$GITHUB_OUTPUT"
echo "Version $version (minor $minor) -> $channel"

# vsce runs `vscode:prepublish` itself, which produces the minified
# dist/extension.js bundle that `main` points at. (.npmrc sets
# ignore-scripts=true, which disables npm pre/post hooks but not
# explicitly invoked `npm run` scripts.)
- name: Package extension
id: package
run: |
if [ "$PRERELEASE" = "true" ]; then
npm run package:preRelease
else
npm run package
fi
vsix=$(ls -1 ./*.vsix | head -n 1)
echo "vsix=$vsix" >> "$GITHUB_OUTPUT"
echo "Packaged $vsix"
env:
PRERELEASE: ${{ steps.channel.outputs.prerelease }}

- name: Publish to Visual Studio Marketplace
if: env.SHOULD_PUBLISH == 'true'
run: |
npx vsce publish --packagePath "$VSIX" ${{ steps.channel.outputs.prerelease == 'true' && '--pre-release' || '' }}
env:
VSIX: ${{ steps.package.outputs.vsix }}

# Skipped with a warning rather than failing while the Open VSX account
# setup is still pending (Eclipse publisher agreement + `Soulcode`
# namespace, see RELEASE.md). Once OVSX_PAT is set, this step starts
# publishing on the very next release with no further changes.
- name: Publish to Open VSX
if: env.SHOULD_PUBLISH == 'true'
run: |
if [ -z "$OVSX_PAT" ]; then
echo "::warning::OVSX_PAT is not set - skipping the Open VSX publish. Cursor/VSCodium users will not get this version. See RELEASE.md -> One-time setup."
exit 0
fi
npx ovsx publish "$VSIX" ${{ steps.channel.outputs.prerelease == 'true' && '--pre-release' || '' }}
env:
VSIX: ${{ steps.package.outputs.vsix }}

- name: Attach .vsix to the GitHub Release
# Runs for a release event, for the release-please chain (workflow_call
# inherits the caller's `push` event name), and for a manual re-publish.
# Skipped outright on a build-only smoke test (`publish: false`).
if: github.event_name == 'release' || github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish)
# A dispatch may name a ref that has no GitHub Release (e.g. `main`, or
# a tag whose release was never created), so check first and skip
# cleanly instead of failing the run. The automatic paths keep failing
# loudly: there the release is guaranteed to exist, and a missing one is
# a bug.
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ] && ! gh release view "$TAG" >/dev/null 2>&1; then
echo "No GitHub Release for '$TAG'; nothing to attach."
exit 0
fi
gh release upload "$TAG" "$VSIX" --clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
TAG: ${{ github.event.release.tag_name || inputs.tag }}
VSIX: ${{ steps.package.outputs.vsix }}
Loading
Loading