fix(ci): add GitHub Actions release workflows and fix visual test
- Add release.yml for manual npm publishing on GitHub Release - Add release-canary.yml for automated canary releases (every 20 commits) - Fix visual test template path (use homedir() instead of ~) - Update visual test baselines - Add sync-version.mjs script for version synchronization - Add sync-version npm script to package.json
147
.github/workflows/release-canary.yml
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
name: Release Canary
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.claude/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commit_threshold:
|
||||
description: 'Commits needed for canary release'
|
||||
required: false
|
||||
default: '20'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
COMMIT_THRESHOLD: ${{ github.event.inputs.commit_threshold || '20' }}
|
||||
|
||||
jobs:
|
||||
# 检查是否需要发布预览版
|
||||
check-release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_release: ${{ steps.check.outputs.should_release }}
|
||||
commit_count: ${{ steps.check.outputs.commit_count }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
|
||||
- name: Check commit count
|
||||
id: check
|
||||
run: |
|
||||
# Get last release tag (not canary)
|
||||
LAST_TAG=$(git tag -l 'v*' | grep -v canary | sort -V | tail -1 || echo "")
|
||||
|
||||
if [ -z "$LAST_TAG" ]; then
|
||||
# No release tags, count all commits
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
else
|
||||
# Count commits since last release
|
||||
COMMIT_COUNT=$(git rev-list --count ${LAST_TAG}..HEAD)
|
||||
fi
|
||||
|
||||
THRESHOLD=${{ env.COMMIT_THRESHOLD }}
|
||||
|
||||
echo "📊 Commits since last release: ${COMMIT_COUNT}"
|
||||
echo "🎯 Threshold: ${THRESHOLD}"
|
||||
echo "commit_count=${COMMIT_COUNT}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Release canary when threshold reached
|
||||
if [ "$COMMIT_COUNT" -ge "$THRESHOLD" ]; then
|
||||
echo "✅ Reached ${THRESHOLD} commits threshold, will release canary"
|
||||
echo "should_release=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
REMAINING=$((THRESHOLD - COMMIT_COUNT))
|
||||
echo "⏭️ ${REMAINING} more commits needed for next canary"
|
||||
echo "should_release=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# 发布预览版
|
||||
canary:
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_release == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
continue-on-error: false
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Generate canary version
|
||||
id: version
|
||||
run: |
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||
TIMESTAMP=$(git log -1 --format=%cd --date=format:%Y%m%d%H%M%S)
|
||||
|
||||
# Canary version: 6.3.54-canary.20260227123456.abc1234
|
||||
CANARY_VERSION="${CURRENT_VERSION}-canary.${TIMESTAMP}.${SHORT_SHA}"
|
||||
|
||||
echo "canary_version=${CANARY_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "📦 Canary version: ${CANARY_VERSION}"
|
||||
|
||||
npm version ${CANARY_VERSION} --no-git-tag-version --allow-same-version
|
||||
|
||||
- name: Run prepublish checks
|
||||
run: node ccw/scripts/prepublish-clean.mjs
|
||||
|
||||
- name: Publish canary to npm
|
||||
run: npm publish --tag canary --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "### 🚀 Canary Release Published" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version**: \`${{ steps.version.outputs.canary_version }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commits**: ${{ needs.check-release.outputs.commit_count }} since last release" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Install**: \`npm install claude-code-workflow@canary\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 跳过时的通知
|
||||
skip-notice:
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.should_release == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Skip notice
|
||||
run: |
|
||||
COMMIT_COUNT=${{ needs.check-release.outputs.commit_count }}
|
||||
THRESHOLD=${{ env.COMMIT_THRESHOLD }}
|
||||
REMAINING=$((THRESHOLD - COMMIT_COUNT))
|
||||
|
||||
echo "### ⏭️ Canary Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commits**: ${COMMIT_COUNT}/${THRESHOLD}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Remaining**: ${REMAINING} more commits needed" >> $GITHUB_STEP_SUMMARY
|
||||
79
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to release (e.g., v6.3.55)'
|
||||
required: true
|
||||
type: string
|
||||
skip_tests:
|
||||
description: 'Skip tests (not recommended)'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
fetch-depth: 100
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
if: github.event.inputs.skip_tests != 'true'
|
||||
run: npm test
|
||||
continue-on-error: false
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run prepublish checks
|
||||
run: node ccw/scripts/prepublish-clean.mjs
|
||||
|
||||
- name: Dry run publish
|
||||
run: npm publish --dry-run
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --provenance --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release Notes
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.inputs.tag }}
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
VERSION=${{ github.event.inputs.tag || github.ref_name }}
|
||||
echo "### 🚀 Release Published" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version**: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Registry**: npm (latest)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Install**: \`npm install claude-code-workflow@latest\`" >> $GITHUB_STEP_SUMMARY
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 93 KiB |
@@ -2,7 +2,7 @@ import { after, before, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createServer } from 'node:http';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { homedir, tmpdir } from 'node:os';
|
||||
import { basename, join, resolve, sep } from 'node:path';
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
@@ -150,7 +150,7 @@ function writePrototypeHtml(filePath: string, target: string, style: number, lay
|
||||
|
||||
describe('ui_generate_preview visual regression', () => {
|
||||
const prototypesDir = mkdtempSync(join(tmpdir(), 'ccw-ui-generate-preview-'));
|
||||
const templatePath = resolve(process.cwd(), '~/.ccw/workflows/_template-compare-matrix.html');
|
||||
const templatePath = resolve(homedir(), '.ccw/workflows/_template-compare-matrix.html');
|
||||
|
||||
let server: StaticServer | undefined;
|
||||
let browser: import('playwright').Browser | undefined;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"ccw/frontend"
|
||||
],
|
||||
"scripts": {
|
||||
"sync-version": "node scripts/sync-version.mjs",
|
||||
"build": "tsc -p ccw/tsconfig.json",
|
||||
"postbuild": "node -e \"const fs=require('fs');['ccw/bin/ccw.js','ccw/bin/ccw-mcp.js'].forEach(f=>{try{fs.chmodSync(f,0o755)}catch{}})\"",
|
||||
"start": "node ccw/bin/ccw.js",
|
||||
|
||||
115
scripts/sync-version.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 版本同步脚本
|
||||
* 用法:
|
||||
* node scripts/sync-version.mjs # 检查版本状态
|
||||
* node scripts/sync-version.mjs --sync # 同步到最新 npm 版本
|
||||
* node scripts/sync-version.mjs --tag # 创建对应 git tag
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = join(__dirname, '..');
|
||||
const packagePath = join(rootDir, 'package.json');
|
||||
|
||||
function run(cmd, silent = false) {
|
||||
try {
|
||||
return execSync(cmd, { cwd: rootDir, encoding: 'utf-8', stdio: silent ? 'pipe' : 'inherit' });
|
||||
} catch (e) {
|
||||
if (!silent) console.error(`Command failed: ${cmd}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function runSilent(cmd) {
|
||||
try {
|
||||
return execSync(cmd, { cwd: rootDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalVersion() {
|
||||
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
function getNpmVersion() {
|
||||
const result = runSilent('npm view claude-code-workflow version');
|
||||
return result;
|
||||
}
|
||||
|
||||
function getLatestTag() {
|
||||
const result = runSilent('git describe --tags --abbrev=0 2>/dev/null');
|
||||
return result ? result.replace(/^v/, '') : null;
|
||||
}
|
||||
|
||||
function setLocalVersion(version) {
|
||||
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
||||
pkg.version = version;
|
||||
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
console.log(`✅ Updated package.json to ${version}`);
|
||||
}
|
||||
|
||||
function createTag(version) {
|
||||
const tagName = `v${version}`;
|
||||
run(`git tag -a ${tagName} -m "Release ${tagName}"`);
|
||||
console.log(`✅ Created tag ${tagName}`);
|
||||
console.log('💡 Run `git push origin ${tagName}` to push the tag');
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const shouldSync = args.includes('--sync');
|
||||
const shouldTag = args.includes('--tag');
|
||||
|
||||
console.log('📦 Version Status\n');
|
||||
|
||||
const localVersion = getLocalVersion();
|
||||
const npmVersion = getNpmVersion();
|
||||
const tagVersion = getLatestTag();
|
||||
|
||||
console.log(` Local (package.json): ${localVersion}`);
|
||||
console.log(` NPM (latest): ${npmVersion || 'not published'}`);
|
||||
console.log(` GitHub Tag (latest): ${tagVersion || 'no tags'}`);
|
||||
console.log('');
|
||||
|
||||
const allVersions = [localVersion, npmVersion, tagVersion].filter(Boolean);
|
||||
const allMatch = allVersions.every(v => v === allVersions[0]);
|
||||
|
||||
if (allMatch) {
|
||||
console.log('✅ All versions are in sync!\n');
|
||||
} else {
|
||||
console.log('⚠️ Versions are out of sync!\n');
|
||||
|
||||
if (shouldSync && npmVersion) {
|
||||
console.log(`Syncing to npm version: ${npmVersion}`);
|
||||
setLocalVersion(npmVersion);
|
||||
} else if (!shouldSync) {
|
||||
console.log('💡 Run with --sync to sync local version to npm');
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldTag && localVersion) {
|
||||
const currentTag = `v${localVersion}`;
|
||||
const existingTags = runSilent('git tag -l ' + currentTag);
|
||||
|
||||
if (existingTags) {
|
||||
console.log(`⚠️ Tag ${currentTag} already exists`);
|
||||
} else {
|
||||
createTag(localVersion);
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldSync && !shouldTag && !allMatch) {
|
||||
console.log('Suggested actions:');
|
||||
if (npmVersion && localVersion !== npmVersion) {
|
||||
console.log(` node scripts/sync-version.mjs --sync # Sync to npm ${npmVersion}`);
|
||||
}
|
||||
if (tagVersion !== localVersion) {
|
||||
console.log(` node scripts/sync-version.mjs --tag # Create tag v${localVersion}`);
|
||||
}
|
||||
}
|
||||