From c0fca2efdc563b6259aa08081156469a9930c4eb Mon Sep 17 00:00:00 2001 From: Duy /zuey/ Date: Mon, 22 Jun 2026 18:35:45 +0700 Subject: [PATCH] feat(release): add semantic release automation (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(release): add semantic release automation * fix(release): disable npm publishing * ci: remove obsolete conda workflow * ci: remove Claude review workflows --------- Co-authored-by: Mỹ Ngân Co-authored-by: Mỹ Ngân --- .github/workflows/claude-code-review.yml | 44 ------------- .github/workflows/claude.yml | 50 -------------- .github/workflows/python-package-conda.yml | 39 ----------- .github/workflows/release.yml | 64 ++++++++++++++++++ .releaserc.json | 58 ++++++++++++++++ README.md | 18 +++++ scripts/sync-release-version.mjs | 77 ++++++++++++++++++++++ 7 files changed, 217 insertions(+), 133 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml delete mode 100644 .github/workflows/claude.yml delete mode 100644 .github/workflows/python-package-conda.yml create mode 100644 .github/workflows/release.yml create mode 100644 .releaserc.json create mode 100755 scripts/sync-release-version.mjs diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index b5e8cfd..0000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize, ready_for_review, reopened] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - plugin_marketplaces: 'https://github.com/anthropics/claude-code.git' - plugins: 'code-review@claude-code-plugins' - prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}' - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml deleted file mode 100644 index d300267..0000000 --- a/.github/workflows/claude.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Claude Code - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - issues: - types: [opened, assigned] - pull_request_review: - types: [submitted] - -jobs: - claude: - if: | - (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || - (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || - (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - actions: read # Required for Claude to read CI results on PRs - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code - id: claude - uses: anthropics/claude-code-action@v1 - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # This is an optional setting that allows Claude to read CI results on PRs - additional_permissions: | - actions: read - - # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. - # prompt: 'Update the pull request description to include a summary of changes.' - - # Optional: Add claude_args to customize behavior and configuration - # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md - # or https://code.claude.com/docs/en/cli-reference for available options - # claude_args: '--allowed-tools Bash(gh pr:*)' - diff --git a/.github/workflows/python-package-conda.yml b/.github/workflows/python-package-conda.yml deleted file mode 100644 index 41c11cb..0000000 --- a/.github/workflows/python-package-conda.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Python Package using Conda - -on: - push: - paths-ignore: - - '**/*.md' - - 'docs/**' - - '.claude/**' - -jobs: - build-linux: - runs-on: ubuntu-latest - strategy: - max-parallel: 5 - - steps: - - uses: actions/checkout@v4 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: '3.10' - - name: Add conda to system path - run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH - - name: Install dependencies - run: | - conda env update --file environment.yml --name base - - name: Lint with flake8 - run: | - conda install flake8 - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - conda install pytest - pytest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ece6a84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release + +on: + push: + branches: + - main + - dev + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + id-token: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + name: Semantic release + runs-on: ubuntu-latest + if: github.repository == 'nextlevelbuilder/ui-ux-pro-max-skill' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: cli/package-lock.json + + - name: Install CLI dependencies + working-directory: cli + run: npm ci + + - name: Build CLI + working-directory: cli + run: bun run build + + - name: Run semantic-release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx --yes \ + -p semantic-release \ + -p @semantic-release/commit-analyzer \ + -p @semantic-release/release-notes-generator \ + -p @semantic-release/changelog \ + -p @semantic-release/exec \ + -p @semantic-release/git \ + -p @semantic-release/github \ + -p conventional-changelog-conventionalcommits \ + semantic-release diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..a90ac12 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,58 @@ +{ + "branches": [ + "main", + { + "name": "dev", + "channel": "beta", + "prerelease": "beta" + } + ], + "tagFormat": "v${version}", + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/exec", + { + "prepareCmd": "node scripts/sync-release-version.mjs ${nextRelease.version}" + } + ], + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md", + "skill.json", + ".claude-plugin/plugin.json", + ".claude-plugin/marketplace.json", + "cli/package.json", + "cli/package-lock.json" + ], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ], + [ + "@semantic-release/github", + { + "successComment": false, + "failTitle": "Release failed for ${branch.name}" + } + ] + ] +} diff --git a/README.md b/README.md index 8187813..6a1f1ad 100755 --- a/README.md +++ b/README.md @@ -508,6 +508,24 @@ gh pr create See [CLAUDE.md](CLAUDE.md) for detailed development guidelines. + +## Automated Releases + +This repository uses semantic-release with Conventional Commits to create GitHub releases automatically: + +- `dev` branch creates beta GitHub prereleases such as `2.6.0-beta.1`. +- `main` branch creates official stable GitHub releases such as `2.6.0`. + +Release notes and `CHANGELOG.md` are generated from Conventional Commit messages. Version numbers are synchronized across `skill.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `cli/package.json`, and `cli/package-lock.json` during release preparation. + +Use these commit types for correct version bumps: + +- `fix:` -> patch release +- `feat:` -> minor release +- `feat!:` or `BREAKING CHANGE:` -> major release + +The release workflow only needs the default `GITHUB_TOKEN`; it does not publish to npm. + ## Troubleshooting ### `uipro: unknown command 'uninstall'` or `unknown command 'update'` diff --git a/scripts/sync-release-version.mjs b/scripts/sync-release-version.mjs new file mode 100755 index 0000000..4014fa7 --- /dev/null +++ b/scripts/sync-release-version.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const version = process.argv[2]; +if (!version) { + console.error('Usage: node scripts/sync-release-version.mjs '); + process.exit(1); +} + +function readJson(path) { + return JSON.parse(readFileSync(resolve(path), 'utf8')); +} + +function replaceJsonVersion(path, updater) { + const fullPath = resolve(path); + const original = readFileSync(fullPath, 'utf8'); + const data = JSON.parse(original); + updater(data); + let next = original; + const replacements = []; + + function setTopLevelVersion(value) { + replacements.push([/(\"version\"\s*:\s*)\"[^\"]+\"/, `$1\"${value}\"`]); + } + + function setMetadataVersion(value) { + replacements.push([/(\"metadata\"\s*:\s*\{[\s\S]*?\"version\"\s*:\s*)\"[^\"]+\"/, `$1\"${value}\"`]); + } + + function setPluginVersions(value) { + replacements.push([/(\"plugins\"\s*:\s*\[[\s\S]*?\"version\"\s*:\s*)\"[^\"]+\"/, `$1\"${value}\"`]); + } + + if (path === '.claude-plugin/marketplace.json') { + setMetadataVersion(data.metadata?.version); + if (data.plugins?.[0]?.version) setPluginVersions(data.plugins[0].version); + } else { + setTopLevelVersion(data.version); + if (data.packages?.['']?.version) { + replacements.push([/(\"packages\"\s*:\s*\{\s*\"\"\s*:\s*\{[\s\S]*?\"version\"\s*:\s*)\"[^\"]+\"/, `$1\"${data.packages[''].version}\"`]); + } + } + + for (const [pattern, replacement] of replacements) next = next.replace(pattern, replacement); + writeFileSync(fullPath, next); +} + +replaceJsonVersion('skill.json', (data) => { + data.version = version; +}); + +replaceJsonVersion('.claude-plugin/plugin.json', (data) => { + data.version = version; +}); + +replaceJsonVersion('.claude-plugin/marketplace.json', (data) => { + if (data.metadata) data.metadata.version = version; + if (Array.isArray(data.plugins)) { + for (const item of data.plugins) item.version = version; + } +}); + +replaceJsonVersion('cli/package.json', (data) => { + data.version = version; +}); + +try { + replaceJsonVersion('cli/package-lock.json', (data) => { + data.version = version; + if (data.packages?.['']) data.packages[''].version = version; + }); +} catch (err) { + if (err.code !== 'ENOENT') throw err; +} + +console.log(`Synced release version ${version} across skill, plugin, marketplace, and CLI manifests.`);