diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml new file mode 100644 index 00000000..51bdb371 --- /dev/null +++ b/.github/workflows/release-canary.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..220b0e42 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_initial.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_initial.png index c5204373..d98cb70b 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_initial.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_initial.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_page_card.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_page_card.png index cfcb7825..89a9de44 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_page_card.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_page_card.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_sync_scroll_off.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_sync_scroll_off.png index 07963aea..45e8c533 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_sync_scroll_off.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_sync_scroll_off.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_tab_comparison.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_tab_comparison.png index 5569218f..91681210 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_tab_comparison.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_tab_comparison.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_zoom_75.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_zoom_75.png index 8dd91dcc..60d3a07a 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_zoom_75.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_desktop_zoom_75.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_mobile_initial.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_mobile_initial.png index f4b21e0e..7852fd60 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_mobile_initial.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_mobile_initial.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_tablet_initial.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_tablet_initial.png index b19df19f..3dba4449 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_tablet_initial.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_compare_tablet_initial.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_desktop.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_desktop.png index 34c9c5ee..44250e6f 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_desktop.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_desktop.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_mobile.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_mobile.png index d79dc08b..0d783e06 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_mobile.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_mobile.png differ diff --git a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_tablet.png b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_tablet.png index 478d2c75..92c62b6d 100644 Binary files a/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_tablet.png and b/ccw/tests/visual/snapshots/baseline/ui-generate-preview_index_tablet.png differ diff --git a/ccw/tests/visual/ui-generate-preview.visual.test.ts b/ccw/tests/visual/ui-generate-preview.visual.test.ts index 21077df1..02039f20 100644 --- a/ccw/tests/visual/ui-generate-preview.visual.test.ts +++ b/ccw/tests/visual/ui-generate-preview.visual.test.ts @@ -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; diff --git a/package.json b/package.json index 75b8fa0b..175732e4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/sync-version.mjs b/scripts/sync-version.mjs new file mode 100644 index 00000000..e8dfe029 --- /dev/null +++ b/scripts/sync-version.mjs @@ -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}`); + } +}