mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-12 05:00:36 +08:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0e96688b8 | ||
|
|
260de7bfa7 | ||
|
|
6f03cfe12f | ||
|
|
1fcdfd1a7d | ||
|
|
428545a799 | ||
|
|
36a975153f | ||
|
|
728243d2c2 | ||
|
|
19f7357ecb |
@@ -8,13 +8,8 @@ permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository == 'Fission-AI/OpenSpec'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -29,22 +24,14 @@ jobs:
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@fission-ai'
|
||||
always-auth: true
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# Opens/updates the Version Packages PR; publishes when the Version PR merges
|
||||
# Opens/updates the Version Packages PR; no publishing here
|
||||
- name: Create/Update Version PR
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
title: 'chore(release): version packages'
|
||||
createGithubReleases: true
|
||||
# Use CI-specific release script: relies on version PR having been merged
|
||||
# so package.json already contains the bumped version.
|
||||
publish: pnpm run release:ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Publish to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: publish-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@fission-ai'
|
||||
always-auth: true
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
|
||||
- name: Ensure running from a tag
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
|
||||
echo "This workflow must run from a tag (got: $GITHUB_REF)";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
- name: Verify release tag matches package.json
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME#v}"
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
if [ "$TAG" != "$PKG_VERSION" ]; then
|
||||
echo "Tag v$TAG does not match package.json $PKG_VERSION"; exit 1
|
||||
fi
|
||||
|
||||
- name: Debug npm auth and context
|
||||
run: |
|
||||
test -n "$NODE_AUTH_TOKEN" || (echo "NODE_AUTH_TOKEN is missing" && exit 1)
|
||||
echo "NODE_AUTH_TOKEN present"
|
||||
npm --version
|
||||
pnpm --version
|
||||
node --version
|
||||
npm config get registry
|
||||
npm whoami
|
||||
npm ping
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
- name: Publish
|
||||
run: pnpm publish --access public --provenance --no-git-checks
|
||||
@@ -1,17 +1,5 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d070d08: Fix CLI version mismatch and add a release guard that validates the packed tarball prints the same version as package.json via `openspec --version`.
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- c29b06d: Add Windsurf support.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<a href="https://nodejs.org/"><img alt="node version" src="https://img.shields.io/node/v/@fission-ai/openspec?style=flat-square" /></a>
|
||||
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a>
|
||||
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg?style=flat-square" /></a>
|
||||
<a href="https://discord.gg/YctCnvvshC"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
|
||||
<a href="https://discord.gg/saTQQGQZ"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white&style=flat-square" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -23,7 +23,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/YctCnvvshC">OpenSpec Discord</a> for help and questions.
|
||||
Follow <a href="https://x.com/0xTab">@0xTab on X</a> for updates · Join the <a href="https://discord.gg/saTQQGQZ">OpenSpec Discord</a> for help and questions.
|
||||
</p>
|
||||
|
||||
# OpenSpec
|
||||
@@ -40,15 +40,6 @@ Key outcomes:
|
||||
- Shared visibility into what's proposed, active, or archived.
|
||||
- Works with the AI tools you already use: custom slash commands where supported, context rules everywhere else.
|
||||
|
||||
## How OpenSpec compares (at a glance)
|
||||
|
||||
- **Lightweight**: simple workflow, no API keys, minimal setup.
|
||||
- **Brownfield-first**: works great beyond 0→1. OpenSpec separates the source of truth from proposals: `openspec/specs/` (current truth) and `openspec/changes/` (proposed updates). This keeps diffs explicit and manageable across features.
|
||||
- **Change tracking**: proposals, tasks, and spec deltas live together; archiving merges the approved updates back into specs.
|
||||
- **Compared to spec-kit & Kiro**: those shine for brand-new features (0→1). OpenSpec also excels when modifying existing behavior (1→n), especially when updates span multiple specs.
|
||||
|
||||
See the full comparison in [How OpenSpec Compares](#how-openspec-compares).
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
@@ -305,9 +296,6 @@ Deltas are "patches" that show how specs change:
|
||||
|
||||
## How OpenSpec Compares
|
||||
|
||||
### vs. spec-kit
|
||||
OpenSpec’s two-folder model (`openspec/specs/` for the current truth, `openspec/changes/` for proposed updates) keeps state and diffs separate. This scales when you modify existing features or touch multiple specs. spec-kit is strong for greenfield/0→1 but provides less structure for cross-spec updates and evolving features.
|
||||
|
||||
### vs. Kiro.dev
|
||||
OpenSpec groups every change for a feature in one folder (`openspec/changes/feature-name/`), making it easy to track related specs, tasks, and designs together. Kiro spreads updates across multiple spec folders, which can make feature tracking harder.
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
## Why
|
||||
Today’s process requires maintainers to merge the Changesets PR, cut a tag, and draft the GitHub release by hand. npm publish then runs from our existing workflow after the GitHub release is published. The human-in-the-loop steps (versioning, tagging, release notes) slow us down and risk drift between npm, tags, and changelog.
|
||||
|
||||
## What Changes
|
||||
- Use the single `changesets/action` on pushes to `main` to either open/update the version PR or, when the release PR is merged, run our publish command automatically using repository secrets.
|
||||
- Add a `release` script that builds and runs `changeset publish` so the action handles version bumps, changelog commits, npm publish, and GitHub releases end-to-end.
|
||||
- Enable `createGithubReleases: true` so GitHub releases are created from the changeset data right after publishing.
|
||||
- Document the automated flow, required secrets, guardrails, and recovery steps (rollback, hotfixes).
|
||||
|
||||
## Two-Phase Rollout (Two PRs)
|
||||
1) Phase 1 — Dry run (no publish)
|
||||
- Update the existing `release-prepare.yml` to wire up `changesets/action` with `createGithubReleases: true` and a no-op `publish` command (e.g., `echo 'dry run'`).
|
||||
- Keep `.github/workflows/release-publish.yml` intact. This avoids any publish path changes while we verify that the version PR behavior and permissions are correct.
|
||||
- Add a repository guard (`if: github.repository == 'Fission-AI/OpenSpec'`) and a concurrency group for safety.
|
||||
|
||||
2) Phase 2 — Enable publish and consolidate
|
||||
- Add `"release": "pnpm run build && pnpm exec changeset publish"` to `package.json`.
|
||||
- Change `release-prepare.yml` to use `with: publish: pnpm run release` and `env: NPM_TOKEN: \\${{ secrets.NPM_TOKEN }}` plus the default `GITHUB_TOKEN`.
|
||||
- Remove `.github/workflows/release-publish.yml` to avoid double-publish. Publishing now happens when the version PR is merged.
|
||||
|
||||
## Guardrails
|
||||
- Concurrency: `concurrency: { group: release-\\${{ github.ref }}, cancel-in-progress: false }` on the workflow to serialize releases.
|
||||
- Repository/branch guard: run publish logic only on upstream `main` (`if: github.repository == 'Fission-AI/OpenSpec' && github.ref == 'refs/heads/main'`).
|
||||
- Permissions: ensure `contents: write` and `pull-requests: write` for opening/updating the version PR; `packages: read` optional.
|
||||
|
||||
## Rollback and Hotfixes
|
||||
- Rollback: revert the release PR merge (which reverts version bumps/changelog); if a tag or GitHub release was created, delete the tag and release; deprecate the npm version if necessary (`npm deprecate @fission-ai/openspec@x.y.z 'reason'`).
|
||||
- Hotfix (urgent, no pending changesets): create a changeset for the fix and merge the release PR; in emergencies, run a manual bump/publish but reconcile with Changesets by adding a follow-up changeset to align versions.
|
||||
|
||||
## Required Secrets
|
||||
- `NPM_TOKEN` with publish rights for the `@fission-ai` scope.
|
||||
- Default `GITHUB_TOKEN` (provided by GitHub) for opening/updating the version PR and creating GitHub releases.
|
||||
|
||||
## How the Maintainer Flow Changes
|
||||
| Step | Current process | Future process |
|
||||
| --- | --- | --- |
|
||||
| Prepare release | Merge changeset PR, then manually draft release notes and tags | Merge release PR; action updates versions and handles changelog automatically |
|
||||
| Publish npm package | Happens automatically after GitHub release | Happens automatically via `changeset publish` invoked by the action |
|
||||
| GitHub release | Draft manually and sync with changelog | Action creates GitHub releases from changeset data |
|
||||
| Docs/process | Follow manual tagging/release steps | Docs describe automated flow + recovery and hotfix paths |
|
||||
|
||||
## Impact
|
||||
- Automation: reuse `.github/workflows/release-prepare.yml` (phase 1: dry-run, phase 2: publish) and remove `.github/workflows/release-publish.yml` in phase 2.
|
||||
- Package metadata: add `release` script to `package.json`.
|
||||
- Docs: update README or `/docs` to show the automated flow, secrets, guardrails, and recovery steps.
|
||||
|
||||
## Acceptance Criteria
|
||||
- Phase 1: merges to `main` open/update a version PR; on merge, the action’s `publish` step is a no-op; no npm publish occurs; logs confirm intended behavior; GitHub releases creation is wired but inert due to no publish.
|
||||
- Phase 2: merges to `main` run `pnpm run release` from the action; npm package publishes successfully; GitHub release is created automatically; `.github/workflows/release-publish.yml` is removed; no duplicate publishes occur.
|
||||
@@ -1,12 +0,0 @@
|
||||
## 1. Release workflow automation
|
||||
- [ ] 1.1 Add a `.github/workflows/release.yml` that runs on pushes to `main`, sets up pnpm + Node 20, installs dependencies, and invokes `changesets/action@v1` with `publish: pnpm run release`.
|
||||
- [ ] 1.2 Configure the action with `createGithubReleases: true` and document required secrets (`NPM_TOKEN`, default `GITHUB_TOKEN`) plus recommended concurrency safeguards.
|
||||
- [ ] 1.3 Validate the workflow using `act` or a dry-run push to confirm the action opens release PRs when changesets exist and publishes when the release PR merge lands.
|
||||
|
||||
## 2. Package release script
|
||||
- [ ] 2.1 Add a `release` script to `package.json` that builds the project and runs `changeset publish` using pnpm.
|
||||
- [ ] 2.2 Ensure the script respects the existing `prepare`/`prepublishOnly` hooks to avoid duplicate builds and update documentation or scripts if adjustments are needed.
|
||||
|
||||
## 3. Documentation and recovery steps
|
||||
- [ ] 3.1 Update maintainer docs (e.g., README or `/docs`) with the end-to-end automated release flow, explicitly removing the manual tag/release steps that are no longer required and explaining how changesets drive the release PR.
|
||||
- [ ] 3.2 Document fallback steps for failed publishes (rerun workflow, manual publish) and the hotfix path when a release must be cut without pending changesets.
|
||||
+1
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.8.1",
|
||||
"version": "0.7.0",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
@@ -46,10 +46,6 @@
|
||||
"test:coverage": "vitest --coverage",
|
||||
"prepare": "pnpm run build",
|
||||
"prepublishOnly": "pnpm run build",
|
||||
"check:pack-version": "node scripts/pack-version-check.mjs",
|
||||
"release": "pnpm run release:ci",
|
||||
"release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
|
||||
"release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
|
||||
"changeset": "changeset"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Guard: Ensure the packed tarball's CLI `--version` matches package.json.
|
||||
//
|
||||
// Notes:
|
||||
// - We intentionally use `npm pack` (not pnpm) because `npm pack --json` is
|
||||
// consistently supported and returns the tarball metadata we need. The
|
||||
// project uses pnpm for install/publish, but this guard only needs to pack
|
||||
// locally and verify the installed CLI output.
|
||||
// - `npm pack` triggers the package's `prepare` script (build), and
|
||||
// `changeset publish` triggers `prepublishOnly` (also builds here). This
|
||||
// means an explicit build is not strictly necessary for the guard.
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
|
||||
function log(msg) {
|
||||
if (process.env.CI) return; // keep CI logs quiet by default
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function run(cmd, args, opts = {}) {
|
||||
return execFileSync(cmd, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], ...opts });
|
||||
}
|
||||
|
||||
function npmPack() {
|
||||
try {
|
||||
const jsonOut = run('npm', ['pack', '--json', '--silent']);
|
||||
const arr = JSON.parse(jsonOut);
|
||||
if (Array.isArray(arr) && arr.length > 0) {
|
||||
const last = arr[arr.length - 1];
|
||||
const file = (last && typeof last === 'object' && last.filename) || (typeof last === 'string' ? last : null);
|
||||
if (file) return String(file).trim();
|
||||
}
|
||||
// Unexpected JSON shape or empty array; fallback to plain output
|
||||
const out = run('npm', ['pack', '--silent']).trim();
|
||||
const lines = out.split(/\r?\n/);
|
||||
return lines[lines.length - 1].trim();
|
||||
} catch (e) {
|
||||
// Fallback for environments not supporting --json
|
||||
const out = run('npm', ['pack', '--silent']).trim();
|
||||
const lines = out.split(/\r?\n/);
|
||||
return lines[lines.length - 1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const pkg = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
||||
const expected = pkg.version;
|
||||
|
||||
let work;
|
||||
let tgzPath;
|
||||
|
||||
try {
|
||||
log(`Packing @fission-ai/openspec@${expected}...`);
|
||||
const filename = npmPack();
|
||||
tgzPath = path.resolve(filename);
|
||||
log(`Created: ${tgzPath}`);
|
||||
|
||||
work = mkdtempSync(path.join(tmpdir(), 'openspec-pack-check-'));
|
||||
log(`Temp dir: ${work}`);
|
||||
|
||||
// Make a tiny project
|
||||
writeFileSync(
|
||||
path.join(work, 'package.json'),
|
||||
JSON.stringify({ name: 'pack-check', private: true }, null, 2)
|
||||
);
|
||||
|
||||
// Try to avoid noisy output and speed up
|
||||
const env = {
|
||||
...process.env,
|
||||
npm_config_loglevel: 'silent',
|
||||
npm_config_audit: 'false',
|
||||
npm_config_fund: 'false',
|
||||
npm_config_progress: 'false',
|
||||
};
|
||||
|
||||
// Install the tarball
|
||||
run('npm', ['install', tgzPath, '--silent', '--no-audit', '--no-fund'], { cwd: work, env });
|
||||
|
||||
// Run the installed CLI via Node to avoid bin resolution/platform issues
|
||||
const binRel = path.join('node_modules', '@fission-ai', 'openspec', 'bin', 'openspec.js');
|
||||
const actual = run(process.execPath, [binRel, '--version'], { cwd: work }).trim();
|
||||
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`Packed CLI version mismatch: expected ${expected}, got ${actual}. ` +
|
||||
'Ensure the dist is built and the CLI reads version from package.json.'
|
||||
);
|
||||
}
|
||||
|
||||
log('Version check passed.');
|
||||
} finally {
|
||||
// Always attempt cleanup
|
||||
if (work) {
|
||||
try { rmSync(work, { recursive: true, force: true }); } catch {}
|
||||
}
|
||||
if (tgzPath) {
|
||||
try { rmSync(tgzPath, { force: true }); } catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
console.log('✅ pack-version-check: OK');
|
||||
} catch (err) {
|
||||
console.error(`❌ pack-version-check: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user