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
96 changes: 71 additions & 25 deletions .github/workflows/publish-extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,74 @@ name: Publish Extension

on:
push:
tags:
- 'v*'
branches:
- main
workflow_dispatch:

concurrency:
group: publish-extension-${{ github.ref }}
group: publish-extension-main
cancel-in-progress: false

permissions:
contents: read

jobs:
detect_release:
name: detect release
runs-on: ubuntu-latest
outputs:
should_publish: ${{ steps.release.outputs.should_publish }}
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Detect unpublished package version
id: release
shell: bash
run: |
set -euo pipefail

package_version=$(node -p "require('./package.json').version")
release_tag="v${package_version}"

if [[ ! "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Package version $package_version is not a supported release version."
exit 1
fi

echo "version=$package_version" >> "$GITHUB_OUTPUT"
echo "tag=$release_tag" >> "$GITHUB_OUTPUT"

if git show-ref --verify --quiet "refs/tags/$release_tag"; then
echo "Version $package_version is already tagged; skipping Marketplace publish."
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi

escaped_version=${package_version//./\\.}
if ! grep -Eq "^## ${escaped_version} - [0-9]{4}-[0-9]{2}-[0-9]{2}$" CHANGELOG.md; then
echo "CHANGELOG.md does not contain a dated $package_version release heading."
exit 1
fi

echo "Version $package_version has not been published yet."
echo "should_publish=true" >> "$GITHUB_OUTPUT"

publish:
name: publish
needs: detect_release
if: needs.detect_release.outputs.should_publish == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
env:
DSLFORGE_TELEMETRY_PROXY_ENDPOINT: ${{ vars.DSLFORGE_TELEMETRY_PROXY_ENDPOINT }}
RELEASE_TAG: ${{ needs.detect_release.outputs.tag }}
RELEASE_VERSION: ${{ needs.detect_release.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
Expand All @@ -33,25 +85,6 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Verify tag matches package version
run: |
package_version=$(node -p "require('./package.json').version")
tag_version="${GITHUB_REF_NAME#v}"

if [ "$package_version" != "$tag_version" ]; then
echo "Tag version $tag_version does not match package.json version $package_version."
exit 1
fi

- name: Verify tag commit is on main
run: |
git fetch origin main

if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Release tags must point to a commit that is already on origin/main."
exit 1
fi

- name: Run format check
run: npm run format:check

Expand All @@ -65,16 +98,29 @@ jobs:
run: npm exec -- vsce ls

- name: Package extension
run: npm exec -- vsce package --out "dslforge-${GITHUB_REF_NAME#v}.vsix"
run: npm exec -- vsce package --out "dslforge-${RELEASE_VERSION}.vsix"

- name: Upload extension package
uses: actions/upload-artifact@v7
with:
name: dslforge-${{ github.ref_name }}
name: dslforge-${{ needs.detect_release.outputs.tag }}
path: dslforge-*.vsix
if-no-files-found: error

- name: Create release tag
id: release_tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag --annotate "$RELEASE_TAG" "$GITHUB_SHA" --message "Release $RELEASE_TAG"
git push origin "refs/tags/$RELEASE_TAG"

- name: Publish extension
id: marketplace_publish
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
run: npm exec -- vsce publish --packagePath "dslforge-${GITHUB_REF_NAME#v}.vsix" -p "$VSCE_PAT"
run: npm exec -- vsce publish --packagePath "dslforge-${RELEASE_VERSION}.vsix" -p "$VSCE_PAT"

- name: Remove release tag after publish failure
if: always() && steps.release_tag.outcome == 'success' && steps.marketplace_publish.outcome != 'success'
run: git push origin --delete "$RELEASE_TAG"
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.

## 0.3.2 - 2026-10-01

- Hardened the tag-based release workflow with GitHub Actions v7, a pinned `@vscode/vsce` toolchain, package-content verification, and archived VSIX artifacts.
- Automated Marketplace publishing for untagged package versions promoted to `main`, with release-tag rollback on publish failure, GitHub Actions v7, a pinned `@vscode/vsce` toolchain, package-content verification, and archived VSIX artifacts.
- Added CI packaging checks so pull requests verify the same extension payload used by releases.
- Added telemetry policy and proxy regression coverage for opt-out behavior, endpoint selection, sensitive-field filtering, payload validation, sink failures, and rate limiting.
- Fixed client telemetry filtering so allowlisted aggregate fields such as output kind, context file counts, and apply target counts are preserved without allowing workspace content or paths.
Expand Down
11 changes: 6 additions & 5 deletions docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Completed in this audit:
- reviewed AI preview apply logic now has dedicated fixture coverage for file-target parsing, bundle selection, and conflict detection
- telemetry client policy and proxy validation now have automated regression coverage
- pull requests now verify the extension package payload with pinned `@vscode/vsce`
- tagged releases now archive the generated VSIX before publishing
- untagged package versions promoted to `main` now publish automatically and archive the generated VSIX
- `preview: true` remains intentional for `0.3.2`
- `dslforge-0.3.2.vsix` installs successfully in an isolated VS Code profile
- the `VSCE_PAT` secret and `DSLFORGE_TELEMETRY_PROXY_ENDPOINT` repository variable are configured
Expand All @@ -44,8 +44,8 @@ Completed in this audit:

- complete manual VS Code smoke for the Xtext fixtures and AI preview/apply path
- confirm publisher access and Marketplace login path
- review the exact release commit state before `vsce publish`
- publish intentionally with the confirmed publisher account
- review the exact release commit state before merging the version change to `main`
- confirm the automatic Marketplace publish and generated release tag succeed

## Deferred Or Optional For This Workstream

Expand Down Expand Up @@ -120,14 +120,15 @@ These items must be true before `vsce publish`:
- `package.json.bugs.url`
- `package.json.homepage`
- git `origin`
- Release tagging is intentional, but Marketplace publish now runs automatically from the `Publish Extension` GitHub Actions workflow when a `v*` tag is pushed
- Marketplace publish runs automatically when `main` contains a package version without a matching `v*` tag
- the workflow validates the dated changelog heading, packages and publishes the extension, then keeps the generated tag only when Marketplace publish succeeds

## Publisher Checklist

- Confirm Marketplace publisher exists for `emfpdlzj`
- Confirm you can authenticate with the publisher account
- Confirm the pinned `@vscode/vsce` version installs through `npm ci`
- Confirm the `VSCE_PAT` GitHub Actions secret exists before pushing a release tag
- Confirm the `VSCE_PAT` GitHub Actions secret exists before merging a new package version to `main`
- Prefer an Entra ID-based publishing path for longer-term automation planning
- If doing a one-off manual publish, confirm the current official authentication path you will use before release day

Expand Down
Loading