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
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
pull_request:
branches: [main]

# Read-only token; nothing here needs more.
permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Test, lint, package
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup pnpm
uses: pnpm/action-setup@v6
# Reads the pinned version from packageManager: pnpm@10.14.0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Test
run: pnpm test

- name: Lint (src only)
run: pnpm exec eslint src/

# Package smoke test: exercises icon-svg-to-png (sharp + resvg native),
# the marketplace README rewrite, and vsce packaging on every PR.
# Runs the build internally; no separate build step needed.
- name: Package
run: pnpm run package

- name: Locate vsix
id: vsix
run: |
VSIX="$(ls openspec-workflow-*.vsix 2>/dev/null | head -n1 || true)"
if [ -z "$VSIX" ]; then
echo "::error::No openspec-workflow-<version>.vsix produced by packaging step"
exit 1
fi
echo "path=$VSIX" >> "$GITHUB_OUTPUT"

- name: Upload vsix artifact
uses: actions/upload-artifact@v6
with:
name: vsix-${{ github.sha }}
path: ${{ steps.vsix.outputs.path }}
if-no-files-found: error
retention-days: 14
168 changes: 168 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
name: Release

on:
push:
branches: [main]
tags:
- 'v*'

# gh release create needs contents: write; the default GITHUB_TOKEN
# permission is read-only, so this must be explicit.
permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
name: Test, package, publish GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup pnpm
uses: pnpm/action-setup@v6
# Reads the pinned version from packageManager: pnpm@10.14.0

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Test
run: pnpm test

# ── Resolve what we're releasing ────────────────────────────────────
# Tag push v* → stable release; tag must match package.json version.
# Push to main via PR → rolling release "<version>-<PR number>" (e.g. v0.2.2-123),
# marked prerelease so stable tags keep the "latest" pointer.
# Direct push to main → tests only; no release is created.
- name: Resolve release metadata
id: meta
env:
GH_TOKEN: ${{ github.token }}
HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: |
set -euo pipefail
BASE_VERSION="$(node -p "require('./package.json').version")"

if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
if [ "v${BASE_VERSION}" != "${GITHUB_REF_NAME}" ]; then
echo "::error::Tag '${GITHUB_REF_NAME}' does not match package.json version '${BASE_VERSION}' (expected 'v${BASE_VERSION}')."
echo "::error::Bump package.json version (and CHANGELOG.md), commit, then delete and re-push the tag:"
echo "::error:: git tag -d ${GITHUB_REF_NAME} && git push origin :refs/tags/${GITHUB_REF_NAME}"
exit 1
fi
VERSION="${BASE_VERSION}"
TAG="${GITHUB_REF_NAME}"
if [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+- ]]; then PRERELEASE=true; else PRERELEASE=false; fi
else
# Push to main: find the PR behind this merge commit.
PR_NUMBER="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" --jq '.[0].number' 2>/dev/null || true)"
if [ -z "${PR_NUMBER}" ]; then
# Fallback: parse the merge-commit subject ("Merge pull request #N from ...").
PR_NUMBER="$(printf '%s' "${HEAD_COMMIT_MESSAGE}" | sed -n 's/^Merge pull request #\([0-9][0-9]*\) from .*/\1/p' | head -n1)"
fi
if [ -z "${PR_NUMBER}" ]; then
echo "No PR associated with ${GITHUB_SHA}; ran validation only, skipping release."
echo "version=" >> "$GITHUB_OUTPUT"
exit 0
fi
VERSION="${BASE_VERSION}-${PR_NUMBER}"
TAG="v${VERSION}"
PRERELEASE=true
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
echo "Merging PR #${PR_NUMBER} → rolling release ${TAG}"
# Patch the ephemeral checkout so the vsix carries the PR-suffixed version.
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.version=process.argv[1];fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');" "${VERSION}"
fi

echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT"

- name: Package
if: steps.meta.outputs.version != ''
run: pnpm run package

- name: Verify vsix exists
if: steps.meta.outputs.version != ''
run: |
VSIX="openspec-workflow-${{ steps.meta.outputs.version }}.vsix"
if [ ! -f "$VSIX" ]; then
echo "::error::Expected ${VSIX} in the repository root after packaging:"
ls -l ./*.vsix 2>/dev/null || echo "(no .vsix files found)"
exit 1
fi
echo "VSIX_PATH=$VSIX" >> "$GITHUB_ENV"

- name: Extract release notes from CHANGELOG.md
if: steps.meta.outputs.version != '' && startsWith(github.ref, 'refs/tags/')
run: |
VERSION="${{ steps.meta.outputs.version }}"
# Literal index() match, not regex: no escaping pitfalls for dots,
# and "[0.2.2]" cannot prefix-match "[0.2.20]".
awk -v ver="[${VERSION}]" '
index($0, "## " ver) == 1 { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > release_notes.md
if [ ! -s release_notes.md ]; then
echo "::warning::No CHANGELOG.md section found for ${VERSION}; using fallback notes."
printf 'See the [CHANGELOG](https://github.com/MotusLabs/openspec-ext/blob/main/CHANGELOG.md) for details.\n' > release_notes.md
fi
echo '--- Extracted release notes ---'
cat release_notes.md

- name: Build release notes from PR
if: steps.meta.outputs.version != '' && !startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr view --repo "${GITHUB_REPOSITORY}" "${{ steps.meta.outputs.pr_number }}" \
--json title,body,url \
--jq '"## " + .title + "\n\n" + .body + "\n\n---\nPull request: " + .url' > release_notes.md
echo '--- Release notes ---'
cat release_notes.md

- name: Create GitHub Release and attach vsix
if: steps.meta.outputs.version != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
FLAGS=""
if [ "${{ steps.meta.outputs.prerelease }}" = "true" ]; then
FLAGS="--prerelease"
fi
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
gh release create "${{ steps.meta.outputs.tag }}" \
--repo "${GITHUB_REPOSITORY}" \
--title "OpenSpec (MotusLab) ${{ steps.meta.outputs.tag }}" \
--notes-file release_notes.md \
--verify-tag \
$FLAGS \
"${VSIX_PATH}"
else
gh release create "${{ steps.meta.outputs.tag }}" \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--title "OpenSpec (MotusLab) ${{ steps.meta.outputs.tag }} (PR #${{ steps.meta.outputs.pr_number }})" \
--notes-file release_notes.md \
$FLAGS \
"${VSIX_PATH}"
fi

- name: Job summary
if: steps.meta.outputs.version != ''
run: |
echo "Release: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${{ steps.meta.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
echo "Artifact: ${VSIX_PATH}" >> "$GITHUB_STEP_SUMMARY"
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ out
*.vsix
.vscode-test
.DS_Store
pnpm-lock.yaml
*.log
.env
build/
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
13 changes: 12 additions & 1 deletion .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ build/
.git/
.codex/
.codegraph/
.claude/
.cursor/
.agents/
.opencode/
.github/
articles/
scripts/
.gitignore
*.md
!README.md
!README.zh-CN.md
!THIRD_PARTY_NOTICES.md
README.md.bak
docs/*
docs/**
!docs/images/
!docs/images/**
docs/scripts/**
Expand All @@ -37,3 +45,6 @@ test/
test-extension.sh
.env*
.DS_Store
.nvmrc
Makefile
pnpm-lock.yaml
24 changes: 18 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- CI/CD via GitHub Actions: PR validation (test, lint, package smoke) and internal releases on GitHub Releases. Every PR merge to `main` publishes a prerelease `v<version>-<PR number>` build; pushing a `vX.Y.Z` tag publishes a stable release. Both attach the `.vsix` for internal installation.
- `pnpm-lock.yaml` is now committed; CI installs with `--frozen-lockfile`.
- `THIRD_PARTY_NOTICES.md` shipped in the `.vsix` attributing `@vscode/codicons` (CC-BY-4.0).

### Changed

- Extension identity for internal distribution: `publisher` is now `motuslabs` (extension ID `motuslabs.openspec-workflow`) and `displayName` is "OpenSpec (MotusLab)". Settings keys (`openspec.*`) are unchanged; uninstall older `randysss.openspec-workflow` builds.
- `repository.url` and documentation links now point at `MotusLabs/openspec-ext`.

## [0.2.2] - 2026-09-08

### Fixed
Expand Down Expand Up @@ -123,9 +134,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Workspace and bundled path resolution for extension resources.

[Unreleased]: https://github.com/RandyZ/openspec-ext/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/RandyZ/openspec-ext/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/RandyZ/openspec-ext/compare/v0.1.4...v0.2.0
[0.1.4]: https://github.com/RandyZ/openspec-ext/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/RandyZ/openspec-ext/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/RandyZ/openspec-ext/releases/tag/v0.1.2
[Unreleased]: https://github.com/MotusLabs/openspec-ext/compare/v0.2.2...HEAD
[0.2.2]: https://github.com/MotusLabs/openspec-ext/compare/v0.2.1...v0.2.2
[0.2.1]: https://github.com/MotusLabs/openspec-ext/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/MotusLabs/openspec-ext/compare/v0.1.4...v0.2.0
[0.1.4]: https://github.com/MotusLabs/openspec-ext/compare/v0.1.3...v0.1.4
[0.1.3]: https://github.com/MotusLabs/openspec-ext/compare/v0.1.2...v0.1.3
[0.1.2]: https://github.com/MotusLabs/openspec-ext/releases/tag/v0.1.2
15 changes: 8 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ All user-facing strings go through `t('key')` from `src/i18n/`. Locales are in `
## Non-obvious caveats

- Run `npx eslint src/` rather than `npx eslint .` — the project has pre-existing `no-undef` issues on Node/browser globals, and linting `dist/` or `esbuild.js` is not intended.
- `pnpm-lock.yaml` is not committed; `pnpm install` resolves fresh each time.
- esbuild may log a cosmetic warning about ignored build scripts; the binary still resolves correctly.
- `pnpm-lock.yaml` is committed; CI installs with `pnpm install --frozen-lockfile`. After changing dependencies, run `pnpm install` and commit the regenerated lockfile.
- esbuild may log a cosmetic warning about ignored build scripts; the binary still resolves correctly. This is pnpm 10 blocking dependency build scripts by default — the affected packages (esbuild, sharp, @resvg/resvg-js) ship prebuilt binaries via optionalDependencies and work without scripts.
- The package does not set `"type": "module"`, so `eslint.config.js` and `postcss.config.js` use ESM syntax and Node logs a cosmetic `MODULE_TYPELESS_PACKAGE_JSON` warning.
- `DataManager.initialize()` throws if the OpenSpec CLI is unavailable, which prevents extension activation. Unit tests bypass this by mocking CLI calls.
- The `openspec` workspace under `openspec/` in this repo is used for extension development and manual testing.
Expand All @@ -104,11 +104,12 @@ All user-facing strings go through `t('key')` from `src/i18n/`. Locales are in `

## Publishing

See `docs/PUBLISHING.md` for full details. Short version:
See `docs/PUBLISHING.md` for full details. The primary channel is the internal GitHub Release pipeline (`.github/workflows/`):

1. Bump `version` in `package.json` and commit.
2. `pnpm run package` produces a `.vsix`.
3. `pnpm run publish:marketplace` (requires `vsce login` or `VSCE_PAT`).
4. `OVSX_TOKEN=<token> pnpm run publish:openvsx` or `make publish-ovsx` (loads `.env` if present).
- Every PR merge to `main` automatically produces a prerelease `v<version>-<PR number>` (e.g. `v0.2.2-123`) with the `.vsix` attached.
- Stable releases: bump `version` in `package.json`, update `CHANGELOG.md`, commit, then push a `vX.Y.Z` tag — CI runs tests, packages, and creates the GitHub Release.
- Install internally by downloading the `.vsix` from Releases ("Install from VSIX…" or `code --install-extension`). The extension ID is `motuslabs.openspec-workflow`.

Public marketplaces (optional): `pnpm run publish:marketplace` (requires `vsce login` or `VSCE_PAT`), or `OVSX_TOKEN=<token> pnpm run publish:openvsx` / `make publish-ovsx` (loads `.env` if present).

Do not commit tokens or `.env`.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The detail view keeps artifacts, task progress, and workflow actions together. V

## Installation

- **From internal GitHub release** (MotusLab): download `openspec-workflow-<version>.vsix` from [GitHub Releases](https://github.com/MotusLabs/openspec-ext/releases) and install via **Extensions: Install from VSIX…**, or run `code --install-extension openspec-workflow-<version>.vsix`. Every PR merge to `main` publishes a new `v<version>-<PR number>` build. This is the internal extension ID `motuslabs.openspec-workflow`; uninstall any older `randysss.openspec-workflow` build first.
- **From marketplace**: Install **OpenSpec** from the [VS Code Marketplace](https://marketplace.visualstudio.com/) or [Open VSX](https://open-vsx.org/) (e.g. in Cursor).
- **Requirements**: [OpenSpec CLI](https://github.com/Fission-AI/OpenSpec#quick-start); a workspace that contains (or will contain) `openspec/config.yaml`. The extension activates when it finds an OpenSpec workspace.

Expand Down Expand Up @@ -74,9 +75,9 @@ A row opens only its detail view, not a new editor window. A Project member swit

### Complete user guide

- [English user guide](https://github.com/RandyZ/openspec-ext/blob/main/docs/USER_GUIDE.md)
- [Plugin interface](https://github.com/RandyZ/openspec-ext/blob/main/docs/USER_GUIDE.md#plugin-interface)
- [简体中文使用指南](https://github.com/RandyZ/openspec-ext/blob/main/docs/USER_GUIDE.zh-CN.md)
- [English user guide](https://github.com/MotusLabs/openspec-ext/blob/main/docs/USER_GUIDE.md)
- [Plugin interface](https://github.com/MotusLabs/openspec-ext/blob/main/docs/USER_GUIDE.md#plugin-interface)
- [简体中文使用指南](https://github.com/MotusLabs/openspec-ext/blob/main/docs/USER_GUIDE.zh-CN.md)

### Commands

Expand Down
21 changes: 21 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Third-party notices

This extension bundles third-party open-source software within `dist/`. Licenses of the bundled components:

| Component | Version(s) | License |
|---|---|---|
| React / ReactDOM | 19.x | MIT |
| Radix UI primitives | various | MIT |
| marked | 17.x | MIT |
| mermaid | 11.x | MIT |
| highlight.js | 11.x | BSD-3-Clause |
| yaml | 2.x | ISC |
| [@vscode/codicons](https://github.com/microsoft/vscode-codicons) | 0.0.x | CC-BY-4.0 |

## @vscode/codicons attribution

The Visual Studio Code Codicon icon set (`codicon.ttf` and related files) is © Microsoft Corporation and licensed under the [Creative Commons Attribution 4.0 International License (CC-BY-4.0)](https://creativecommons.org/licenses/by/4.0/legalcode). It is redistributed inside this extension as permitted by that license, with this notice providing the required attribution.

---

Exact versions are recorded in [`pnpm-lock.yaml`](pnpm-lock.yaml). Update this table when adding runtime dependencies that are bundled into `dist/`.
Loading
Loading