Skip to content
Merged
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
250 changes: 250 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
name: Release

# Triggered by pushing a tag like v1.2.3. One tag drives all three artifacts (IDEA
# plugin, standalone debug agent, VS Code extension), but each is built and
# published only if ITS OWN changelog has a "## [1.2.3]" section -- an artifact with
# nothing new since its last release is skipped, and its last published version
# stays current everywhere it's distributed. Before tagging, patch the relevant
# changelog(s) by hand: move "[Unreleased]" entries under a "## [1.2.3] -
# YYYY-MM-DD" heading and commit that. See docs/RELEASING.md for the full runbook
# (per-artifact changelogs, one-time Marketplace/signing/token setup).

on:
push:
tags:
- "v*"

jobs:
version:
name: Derive the release version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Derive the version from the tag
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

plugin:
name: Build the IDEA plugin
needs: version
runs-on: ubuntu-latest
outputs:
released: ${{ steps.changelog.outputs.released }}
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle

- name: Extract this version's changelog section
id: changelog
run: |
if ./gradlew :plugin:getChangelog --no-unreleased --no-header --console=plain -q \
--project-version=${{ needs.version.outputs.version }} > notes.md; then
echo "released=true" >> "$GITHUB_OUTPUT"
else
echo "released=false" >> "$GITHUB_OUTPUT"
fi

- name: Build the plugin distribution
if: steps.changelog.outputs.released == 'true'
run: ./gradlew :plugin:buildPlugin -Pversion=${{ needs.version.outputs.version }} --stacktrace

- name: Upload the build
if: steps.changelog.outputs.released == 'true'
uses: actions/upload-artifact@v4
with:
name: plugin
path: |
plugin/build/distributions/*.zip
notes.md
retention-days: 1

agent:
name: Build the debug agent
needs: version
runs-on: ubuntu-latest
outputs:
released: ${{ steps.changelog.outputs.released }}
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle

- name: Extract this version's changelog section
id: changelog
run: |
if bash scripts/changelog-section.sh agent/CHANGELOG.md ${{ needs.version.outputs.version }} > notes.md; then
echo "released=true" >> "$GITHUB_OUTPUT"
else
echo "released=false" >> "$GITHUB_OUTPUT"
fi

- name: Build the agent jar
if: steps.changelog.outputs.released == 'true'
run: ./gradlew :agent:instrument:shadowJar -Pversion=${{ needs.version.outputs.version }} --stacktrace

- name: Upload the build
if: steps.changelog.outputs.released == 'true'
uses: actions/upload-artifact@v4
with:
name: agent
path: |
agent/instrument/build/libs/*.jar
notes.md
retention-days: 1

vscode:
name: Build the VS Code extension
needs: version
runs-on: ubuntu-latest
outputs:
released: ${{ steps.changelog.outputs.released }}
defaults:
run:
working-directory: editors/vscode
steps:
- uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: editors/vscode/package-lock.json

- name: Extract this version's changelog section
id: changelog
run: |
if bash ../../scripts/changelog-section.sh CHANGELOG.md ${{ needs.version.outputs.version }} > ../../notes.md; then
echo "released=true" >> "$GITHUB_OUTPUT"
else
echo "released=false" >> "$GITHUB_OUTPUT"
fi

- name: Install dependencies
if: steps.changelog.outputs.released == 'true'
run: npm ci

- name: Package the extension
if: steps.changelog.outputs.released == 'true'
run: |
npx @vscode/vsce package --no-git-tag-version --no-update-package-json \
${{ needs.version.outputs.version }} -o bsh-debug-${{ needs.version.outputs.version }}.vsix

- name: Upload the build
if: steps.changelog.outputs.released == 'true'
uses: actions/upload-artifact@v4
with:
name: vscode
path: |
editors/vscode/*.vsix
notes.md
retention-days: 1

github-release:
name: Publish the GitHub Release
needs: [version, plugin, agent, vscode]
if: needs.plugin.outputs.released == 'true' || needs.agent.outputs.released == 'true' || needs.vscode.outputs.released == 'true'
runs-on: ubuntu-latest
steps:
- name: Download the plugin build
if: needs.plugin.outputs.released == 'true'
uses: actions/download-artifact@v4
with:
name: plugin
path: dl/plugin

- name: Download the agent build
if: needs.agent.outputs.released == 'true'
uses: actions/download-artifact@v4
with:
name: agent
path: dl/agent

- name: Download the VS Code extension build
if: needs.vscode.outputs.released == 'true'
uses: actions/download-artifact@v4
with:
name: vscode
path: dl/vscode

- name: Assemble the release body
run: |
{
if [ -f dl/plugin/notes.md ]; then echo "## IDEA plugin v${{ needs.version.outputs.version }}"; echo; cat dl/plugin/notes.md; echo; fi
if [ -f dl/agent/notes.md ]; then echo "## Debug agent v${{ needs.version.outputs.version }}"; echo; cat dl/agent/notes.md; echo; fi
if [ -f dl/vscode/notes.md ]; then echo "## VS Code extension v${{ needs.version.outputs.version }}"; echo; cat dl/vscode/notes.md; echo; fi
} > body.md

- name: Create the GitHub Release
uses: softprops/action-gh-release@v2
with:
name: v${{ needs.version.outputs.version }}
body_path: body.md
files: |
dl/plugin/*.zip
dl/agent/*.jar
dl/vscode/*.vsix

publish-jetbrains-marketplace:
name: Publish the plugin to the JetBrains Marketplace
needs: [version, plugin, github-release]
if: needs.plugin.outputs.released == 'true'
runs-on: ubuntu-latest
# Requires a GitHub Environment named "jetbrains-marketplace" with required
# reviewers configured (Settings -> Environments), so every Marketplace publish
# needs a manual approval click. See docs/RELEASING.md.
environment: jetbrains-marketplace
steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: gradle

- name: Publish the plugin
env:
JETBRAINS_MARKETPLACE_TOKEN: ${{ secrets.JETBRAINS_MARKETPLACE_TOKEN }}
CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }}
PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }}
run: ./gradlew :plugin:publishPlugin -Pversion=${{ needs.version.outputs.version }} --stacktrace

publish-vscode-marketplace:
name: Publish the extension to the VS Code Marketplace
needs: [version, vscode, github-release]
if: needs.vscode.outputs.released == 'true'
runs-on: ubuntu-latest
# Requires a GitHub Environment named "vscode-marketplace" with required
# reviewers configured (Settings -> Environments) -- the same protection as
# the JetBrains publish job, for the same reason: a bad Marketplace publish
# isn't as easily undone as deleting a GitHub Release. See docs/RELEASING.md.
environment: vscode-marketplace
steps:
- name: Download the VS Code extension build
uses: actions/download-artifact@v4
with:
name: vscode
path: dl/vscode

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Publish the extension
run: npx --yes @vscode/vsce publish --packagePath dl/vscode/*.vsix -p "${{ secrets.VSCODE_MARKETPLACE_TOKEN }}"
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ coordinates and publishes just as well.
[`editors/eclipse/`](editors/eclipse/README.md) cover the same transport for those editors.
- [`docs/BEANSHELL-DEFECTS.md`](docs/BEANSHELL-DEFECTS.md) — upstream bugs in
BeanShell 2.0b6 that a debugger runs into.
- [`docs/RELEASING.md`](docs/RELEASING.md) — cutting a release: the changelog step,
tagging, and the one-time Marketplace/signing/secrets setup.

**Two different things are called "the agent".** `agent/` is the ASM-instrumenting
JVM agent (`cz.loplex.bsh.*`), the default mechanism. `plugin/…/debug/agent/BshDebugAgent.java`
Expand Down
10 changes: 10 additions & 0 deletions agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!-- Keep a Changelog guide -> https://keepachangelog.com -->

# Debug agent Changelog

Covers the standalone debug agent (`bsh-debug-agent-X.Y.Z.jar`, built from
`:agent:instrument` with `:agent:hook` shaded in) as distributed on its own for the
VS Code, Neovim and Eclipse DAP transports. The copy of this jar bundled inside the
IDEA plugin has its own release cycle -- see [`../plugin/CHANGELOG.md`](../plugin/CHANGELOG.md).

## [Unreleased]
117 changes: 117 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Releasing

One tag drives three independently-gated artifacts, all via
[`.github/workflows/release.yml`](../.github/workflows/release.yml):

| Artifact | Changelog gate | Built by | Published to |
|---|---|---|---|
| IDEA plugin | `plugin/CHANGELOG.md` | `:plugin:buildPlugin` | GitHub Release + JetBrains Marketplace |
| Debug agent | `agent/CHANGELOG.md` | `:agent:instrument:shadowJar` | GitHub Release only |
| VS Code extension | `editors/vscode/CHANGELOG.md` | `vsce package` | GitHub Release + VS Code Marketplace |

Pushing `vX.Y.Z` runs all three build jobs, but **each one only builds and publishes
if its own changelog has a `## [X.Y.Z]` section.** An artifact with nothing new this
round is skipped entirely — no build, no upload, nothing added to the release — and
its last-published version stays current wherever it's distributed. This is why
there's no single "the project version" anymore: `gradle.properties`' `version` and
`editors/vscode/package.json`'s `"version"` are local dev defaults only. Every
workflow build passes `-Pversion=X.Y.Z` (Gradle) or an explicit version argument
(`vsce package`), both of which override the committed file — so what's committed
there never has to match the tag, and is free to sit at a `-SNAPSHOT`-style dev
placeholder between releases.

Everything under "One-time setup" is done once by whoever holds the two Marketplace
vendor accounts and the signing key. After that, releasing is just "Cutting a
release" below.

## Cutting a release

1. For each artifact you want to release this round, move its changelog's entries
from `## [Unreleased]` to a new `## [X.Y.Z] - YYYY-MM-DD` heading, and commit
that. Leave an artifact's changelog with an empty (or absent) `[X.Y.Z]` section to
skip it this round — its job then does nothing and its current published version
stands.

The plugin's check (`:plugin:getChangelog --no-unreleased --project-version=X.Y.Z`)
and the agent/VS Code check (`scripts/changelog-section.sh <file> X.Y.Z`) both key
off the exact version string, so a typo or a missing heading just means that
artifact quietly gets skipped rather than failing the whole workflow.

2. Tag and push:

```bash
git tag vX.Y.Z
git push origin vX.Y.Z
```

3. Watch the **Release** workflow in the Actions tab. `publish-jetbrains-marketplace`
and `publish-vscode-marketplace` each pause for manual
approval — approve whichever ran, once you're happy with the GitHub Release that
just went out. A skipped artifact's publish job doesn't run at all, so there's
nothing to approve for it.

## One-time setup

### 1. JetBrains Marketplace: token + first upload

- Generate a Marketplace API token at
https://plugins.jetbrains.com/author/me/tokens and add it as the
`JETBRAINS_MARKETPLACE_TOKEN` repository secret (Settings -> Secrets and
variables -> Actions).
- **The very first version of a brand-new plugin must be uploaded by hand** through
the Marketplace web UI (New plugin -> upload the ZIP from
`plugin/build/distributions/`) and pass JetBrains' manual review. `publishPlugin`
only works for versions *after* that first approval — run a tag through the
pipeline for real only once this is done.

### 2. Plugin signing

JetBrains recommends every Marketplace plugin be signed, so its origin is verifiable.
Generate a self-signed certificate locally (never paste the private key into a chat
tool or commit it anywhere):

```bash
openssl genpkey -aes256 -algorithm RSA -out private_encrypted.pem -pkeyopt rsa_keygen_bits:4096
openssl req -key private_encrypted.pem -new -x509 -days 3650 -out chain.crt -subj "/CN=loplex"
```

`openssl req` will prompt for the passphrase chosen in the first command. Add three
repository secrets:

- `PRIVATE_KEY` — contents of `private_encrypted.pem` (stays password-protected)
- `CERTIFICATE_CHAIN` — contents of `chain.crt`
- `PRIVATE_KEY_PASSWORD` — the passphrase from the first command

Then delete both files locally — only the secrets need to persist.

### 3. VS Code Marketplace: publisher + token

- Create the `loplex` publisher at https://marketplace.visualstudio.com/manage (needs
an Azure DevOps organization) if it doesn't exist yet.
- Generate an Azure DevOps Personal Access Token scoped to **Marketplace ->
Manage**, and add it as the `VSCODE_MARKETPLACE_TOKEN` repository secret. See
https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token
for the exact steps.
- Unlike JetBrains, `vsce publish` works for a brand-new extension on the first try —
there's no separate manual-review gate to clear first.

### 4. Approval gates

Two GitHub Environments (Settings -> Environments), each with yourself as a required
reviewer, so every Marketplace publish needs an explicit approval click in the
Actions UI before it goes out:

- `jetbrains-marketplace` — targeted by `publish-jetbrains-marketplace`
- `vscode-marketplace` — targeted by `publish-vscode-marketplace` (VS Code)

Neither the `github-release` job nor the debug agent's build has this gate — that
side is easily undone (delete the release), while a bad Marketplace publish is not.

## The standalone debug agent

`bsh-debug-agent-X.Y.Z.jar` (`:agent:instrument:shadowJar`, with `:agent:hook` shaded
in) is only ever attached as a GitHub Release asset — there's no separate registry
it's published to today. VS Code, Neovim and Eclipse users who don't want the IDEA
plugin download it straight from the Release page. If that ever needs to change (e.g.
Maven Central), that's a materially bigger one-time setup (GPG signing, POM
metadata, group ID verification) than anything above.
5 changes: 5 additions & 0 deletions editors/vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- Keep a Changelog guide -> https://keepachangelog.com -->

# BeanShell Debug (VS Code extension) Changelog

## [Unreleased]
Binary file added editors/vscode/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading