From f4ad01d8cb862d2b6f911a2a3e1f3f23353334c4 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 29 Apr 2026 16:58:03 +0200 Subject: [PATCH 1/5] fix(ci): release on shared pipeline changes --- scripts/release-scope.ts | 91 ++++++++++++++++++++++++------------- tests/release-scope.test.ts | 45 ++++++++++++++++++ 2 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 tests/release-scope.test.ts diff --git a/scripts/release-scope.ts b/scripts/release-scope.ts index 76ef78b434..0691c66cc5 100644 --- a/scripts/release-scope.ts +++ b/scripts/release-scope.ts @@ -1,12 +1,30 @@ import { execFileSync } from 'node:child_process' -type Component = 'capgo' | 'cli' -type ReleaseAs = 'patch' | 'minor' | 'major' - -const componentMatchers: Record = { +export type Component = 'capgo' | 'cli' +export type ReleaseAs = 'patch' | 'minor' | 'major' + +const sharedMatchers = [ + /^\.github\/workflows\/bump_version\.yml$/, + /^\.github\/workflows\/tests\.yml$/, + /^\.github\/scripts\//, + /^scripts\/release-scope\.ts$/, + /^scripts\/setup-bun\.sh$/, + /^scripts\/setup-bun\.ps1$/, + /^package\.json$/, + /^bun\.lock$/, + /^\.npmrc$/, + /^\.typos\.toml$/, + /^bunfig\.toml$/, + /^tsconfig\.json$/, + /^vitest\.config\.ts$/, + /^vitest\.config\.cloudflare\.ts$/, + /^vitest\.config\.cloudflare-plugin\.ts$/, +] + +export const componentMatchers: Record = { capgo: [ - /^package\.json$/, - /^bun\.lock$/, + ...sharedMatchers, + /^\.github\/workflows\/build_and_deploy\.yml$/, /^aliproxy\//, /^android\//, /^assets\//, @@ -35,7 +53,8 @@ const componentMatchers: Record = { /^wrangler\.jsonc$/, ], cli: [ - /^bun\.lock$/, + ...sharedMatchers, + /^\.github\/workflows\/publish_cli\.yml$/, /^cli\/src\//, /^cli\/skills\/[^/]+\/SKILL\.md$/, /^cli\/skills\/(?!.*\.(md|mdx)$)/, @@ -82,11 +101,11 @@ function getCommitMessage(commit: string): { subject: string, body: string } { } } -function matchesComponent(component: Component, files: string[]): boolean { +export function matchesComponent(component: Component, files: string[]): boolean { return files.some(file => componentMatchers[component].some(pattern => pattern.test(file))) } -function getSeverity(subject: string, body: string): number { +export function getSeverity(subject: string, body: string): number { const conventionalMatch = subject.match(/^([a-z]+)(\([^)]+\))?(!)?:/) const hasBreakingChange = body.includes('BREAKING CHANGE:') || conventionalMatch?.[3] === '!' @@ -101,7 +120,7 @@ function getSeverity(subject: string, body: string): number { return 1 } -function toReleaseAs(severity: number): ReleaseAs { +export function toReleaseAs(severity: number): ReleaseAs { if (severity >= 3) { return 'major' } @@ -113,31 +132,41 @@ function toReleaseAs(severity: number): ReleaseAs { return 'patch' } -const componentArg = process.argv[2] -const before = process.argv[3] ?? '' -const after = process.argv[4] ?? 'HEAD' - -if (componentArg !== 'capgo' && componentArg !== 'cli') { - console.error('Usage: bun scripts/release-scope.ts [before] [after]') - process.exit(1) -} +export function resolveReleaseScope(component: Component, before: string, after: string) { + const commits = getCommitShas(before, after) -const component = componentArg as Component -const commits = getCommitShas(before, after) + let shouldRelease = false + let highestSeverity = 0 -let shouldRelease = false -let highestSeverity = 0 + for (const commit of commits) { + const files = getChangedFiles(commit) + if (!matchesComponent(component, files)) { + continue + } -for (const commit of commits) { - const files = getChangedFiles(commit) - if (!matchesComponent(component, files)) { - continue + shouldRelease = true + const message = getCommitMessage(commit) + highestSeverity = Math.max(highestSeverity, getSeverity(message.subject, message.body)) } - shouldRelease = true - const message = getCommitMessage(commit) - highestSeverity = Math.max(highestSeverity, getSeverity(message.subject, message.body)) + return { + shouldRelease, + releaseAs: shouldRelease ? toReleaseAs(highestSeverity) : 'patch', + } } -console.log(`should_release=${shouldRelease}`) -console.log(`release_as=${shouldRelease ? toReleaseAs(highestSeverity) : 'patch'}`) +if (import.meta.main) { + const componentArg = process.argv[2] + const before = process.argv[3] ?? '' + const after = process.argv[4] ?? 'HEAD' + + if (componentArg !== 'capgo' && componentArg !== 'cli') { + console.error('Usage: bun scripts/release-scope.ts [before] [after]') + process.exit(1) + } + + const scope = resolveReleaseScope(componentArg, before, after) + + console.log(`should_release=${scope.shouldRelease}`) + console.log(`release_as=${scope.releaseAs}`) +} diff --git a/tests/release-scope.test.ts b/tests/release-scope.test.ts new file mode 100644 index 0000000000..0865439379 --- /dev/null +++ b/tests/release-scope.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest' +import { matchesComponent } from '../scripts/release-scope.ts' + +describe('release scope matching', () => { + it('treats shared release infrastructure as affecting both components', () => { + const files = [ + '.github/workflows/tests.yml', + '.github/workflows/bump_version.yml', + '.github/scripts/start-background-service.sh', + 'scripts/setup-bun.sh', + 'scripts/release-scope.ts', + ] + + expect(matchesComponent('capgo', files)).toBe(true) + expect(matchesComponent('cli', files)).toBe(true) + }) + + it('treats capgo deploy workflow changes as capgo-only releases', () => { + const files = ['.github/workflows/build_and_deploy.yml'] + + expect(matchesComponent('capgo', files)).toBe(true) + expect(matchesComponent('cli', files)).toBe(false) + }) + + it('treats cli publish workflow changes as cli-only releases', () => { + const files = ['.github/workflows/publish_cli.yml'] + + expect(matchesComponent('capgo', files)).toBe(false) + expect(matchesComponent('cli', files)).toBe(true) + }) + + it('keeps runtime code scoped to the matching component', () => { + expect(matchesComponent('capgo', ['src/pages/index.vue'])).toBe(true) + expect(matchesComponent('cli', ['src/pages/index.vue'])).toBe(false) + expect(matchesComponent('capgo', ['cli/src/index.ts'])).toBe(false) + expect(matchesComponent('cli', ['cli/src/index.ts'])).toBe(true) + }) + + it('does not release on unrelated changes', () => { + const files = ['README.md'] + + expect(matchesComponent('capgo', files)).toBe(false) + expect(matchesComponent('cli', files)).toBe(false) + }) +}) From 2221fcbf669eafd020dc4c9850ba7bec617ffd04 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 29 Apr 2026 17:00:41 +0200 Subject: [PATCH 2/5] fix(ci): test release scope changes in both workspaces --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ba52a6790a..e26a9f07a7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,6 +56,7 @@ jobs: shared: - '.github/workflows/**' - '.github/scripts/**' + - 'scripts/release-scope.ts' - 'scripts/setup-bun.sh' - 'scripts/setup-bun.ps1' - '.npmrc' From d1d2727f77b19634ed3cf6bf500eefca957655e3 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 29 Apr 2026 17:17:19 +0200 Subject: [PATCH 3/5] fix(ci): trigger discord on cli releases --- .github/workflows/publish_cli.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_cli.yml b/.github/workflows/publish_cli.yml index 7313ddf1ab..f67a898162 100644 --- a/.github/workflows/publish_cli.yml +++ b/.github/workflows/publish_cli.yml @@ -24,7 +24,7 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + uses: actions/checkout@v6 with: fetch-depth: 0 filter: blob:none @@ -36,7 +36,7 @@ jobs: run: bun run cli:build - name: Generate AI changelog id: changelog - uses: mistricky/ccc@d9358b3eca472c0f41a6c1732bf073c0c54b837e # v0.2.6 + uses: mistricky/ccc@v0.2.6 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ github.token }} @@ -53,7 +53,7 @@ jobs: run: bun publish --cwd cli --tag next --access public - name: Create GitHub release id: create_release - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + uses: softprops/action-gh-release@v2 with: body: | ## 🆕 Changelog @@ -64,5 +64,5 @@ jobs: 🔗 **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.changelog.outputs.from_tag }}...${{ steps.changelog.outputs.to_tag }} make_latest: false - token: ${{ github.token }} + token: "${{ secrets.PERSONAL_ACCESS_TOKEN }}" prerelease: ${{ contains(github.ref, '-alpha.') }} From 893090cb9047378d7a41fa2217ea2862f4e43457 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 29 Apr 2026 17:21:02 +0200 Subject: [PATCH 4/5] test(ci): run release scope specs concurrently --- tests/release-scope.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/release-scope.test.ts b/tests/release-scope.test.ts index 0865439379..0e99beeb66 100644 --- a/tests/release-scope.test.ts +++ b/tests/release-scope.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { matchesComponent } from '../scripts/release-scope.ts' describe('release scope matching', () => { - it('treats shared release infrastructure as affecting both components', () => { + it.concurrent('treats shared release infrastructure as affecting both components', () => { const files = [ '.github/workflows/tests.yml', '.github/workflows/bump_version.yml', @@ -15,28 +15,28 @@ describe('release scope matching', () => { expect(matchesComponent('cli', files)).toBe(true) }) - it('treats capgo deploy workflow changes as capgo-only releases', () => { + it.concurrent('treats capgo deploy workflow changes as capgo-only releases', () => { const files = ['.github/workflows/build_and_deploy.yml'] expect(matchesComponent('capgo', files)).toBe(true) expect(matchesComponent('cli', files)).toBe(false) }) - it('treats cli publish workflow changes as cli-only releases', () => { + it.concurrent('treats cli publish workflow changes as cli-only releases', () => { const files = ['.github/workflows/publish_cli.yml'] expect(matchesComponent('capgo', files)).toBe(false) expect(matchesComponent('cli', files)).toBe(true) }) - it('keeps runtime code scoped to the matching component', () => { + it.concurrent('keeps runtime code scoped to the matching component', () => { expect(matchesComponent('capgo', ['src/pages/index.vue'])).toBe(true) expect(matchesComponent('cli', ['src/pages/index.vue'])).toBe(false) expect(matchesComponent('capgo', ['cli/src/index.ts'])).toBe(false) expect(matchesComponent('cli', ['cli/src/index.ts'])).toBe(true) }) - it('does not release on unrelated changes', () => { + it.concurrent('does not release on unrelated changes', () => { const files = ['README.md'] expect(matchesComponent('capgo', files)).toBe(false) From 1dd299b68d809500e2c89198afa084fc7b51c7ad Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Wed, 29 Apr 2026 17:30:25 +0200 Subject: [PATCH 5/5] fix(ci): exclude workflow files from sonar --- .sonarcloud.properties | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 7cc614b4c2..c53d1982bf 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,7 +1,8 @@ # -# We pin workflow actions by commit SHA, which creates noisy GitHub Actions -# hotspot findings in Sonar. Disable that analysis for this repository. +# GitHub Actions workflow files create noisy security hotspot findings in +# Sonar. Keep workflow analysis disabled and exclude CI automation files from +# the general scan for this repository. sonar.githubactions.activate=false -sonar.exclusions=cli/**,scripts/**,tests/**,scriptable/**,.cursor/**,playwright/**,formkit.theme.ts,sql/**,supabase/seed.sql,supabase/tests/**,src/components/comp_def.ts,supabase/functions/_backend/utils/supabase.types.ts,src/types/supabase.types.ts -sonar.cpd.exclusions=cli/**,scripts/**,tests/**,scriptable/**,.cursor/**,playwright/**,formkit.theme.ts,sql/**,supabase/seed.sql,supabase/migrations/**,supabase/functions/_backend/utils/conversion.ts,supabase/tests/**,supabase/functions/_backend/plugins/updates_lite.ts,supabase/functions/_backend/utils/pg_lite.ts,supabase/functions/_backend/utils/supabase.types.ts,src/types/supabase.types.ts,src/components/comp_def.ts,src/services/conversion.ts +sonar.exclusions=.github/workflows/**,.github/scripts/**,cli/**,scripts/**,tests/**,scriptable/**,.cursor/**,playwright/**,formkit.theme.ts,sql/**,supabase/seed.sql,supabase/tests/**,src/components/comp_def.ts,supabase/functions/_backend/utils/supabase.types.ts,src/types/supabase.types.ts +sonar.cpd.exclusions=.github/workflows/**,.github/scripts/**,cli/**,scripts/**,tests/**,scriptable/**,.cursor/**,playwright/**,formkit.theme.ts,sql/**,supabase/seed.sql,supabase/migrations/**,supabase/functions/_backend/utils/conversion.ts,supabase/tests/**,supabase/functions/_backend/plugins/updates_lite.ts,supabase/functions/_backend/utils/pg_lite.ts,supabase/functions/_backend/utils/supabase.types.ts,src/types/supabase.types.ts,src/components/comp_def.ts,src/services/conversion.ts