feat(ci): add workflow_dispatch for AI review (#291)

* feat(ci): add AI code review workflow with Claude via CLIProxyAPI

- Self-hosted runner calls CLIProxyAPI at localhost:8317
- Triggers on PR open/update and /review comment
- Uses gemini-claude-opus-4-5-thinking for deep reviews
- Posts summary + inline comments via gh CLI
- Handles self-PR fallback to COMMENT mode

* chore(release): 7.15.0-dev.1 [skip ci]

* refactor(ci): use GitHub App for reviewer identity + new review format

- Posts as ccs-agy-reviewer[bot] via GitHub App token
- New review format: structured markdown with verdict, summary, issues table
- Single PR comment instead of inline comments
- Concise, focused on PR changes only

* chore(release): 7.15.0-dev.2 [skip ci]

* refactor(ci): switch to Claude Code CLI for reviews

- Use claude -p instead of custom TypeScript script
- Auto-install if not present on runner
- CLIProxyAPI via env vars (ANTHROPIC_BASE_URL, ANTHROPIC_MODEL)
- Allowed tools: Read, Glob, Grep
- Max 3 turns for file exploration

* chore(release): 7.15.0-dev.3 [skip ci]

* chore(release): 7.15.0-dev.4 [skip ci]

* feat(ci): add workflow_dispatch for manual review trigger

* chore(release): 7.15.0-dev.5 [skip ci]

* refactor(cliproxy): add faulty version range infrastructure (#289)

* refactor(cliproxy): add faulty version range infrastructure

- Add CLIPROXY_FAULTY_RANGE constant for marking known buggy versions
- Add isVersionFaulty() helper to version-checker.ts
- Update lifecycle.ts to warn users on faulty versions with upgrade suggestion
- Update health checks to distinguish faulty vs experimental versions
- Update stats routes to include faultyRange in API response
- Skip faulty versions when calculating latestStable

Infrastructure ready for future version promotions. Currently:
- v80 and below: stable
- v81+: marked as faulty (context cancellation bugs)

* chore: trigger review

* chore: re-trigger review

* chore: test PR trigger

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kai (Tam Nhu) Tran
2026-01-07 02:25:49 -08:00
committed by GitHub
co-authored by github-actions[bot] <github-actions[bot]@users.noreply.github.com>
parent 49c4d299c0
commit b6d65209cd
7 changed files with 97 additions and 22 deletions
+10 -1
View File
@@ -13,10 +13,16 @@ on:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
issue_comment: issue_comment:
types: [created] types: [created]
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to review'
required: true
type: string
# Cancel in-progress runs for same PR # Cancel in-progress runs for same PR
concurrency: concurrency:
group: ai-review-${{ github.event.pull_request.number || github.event.issue.number }} group: ai-review-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
@@ -30,6 +36,7 @@ jobs:
# - Comment event: only if it's a PR and contains /review # - Comment event: only if it's a PR and contains /review
if: > if: >
github.event_name == 'pull_request' || github.event_name == 'pull_request' ||
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && (github.event_name == 'issue_comment' &&
github.event.issue.pull_request && github.event.issue.pull_request &&
contains(github.event.comment.body, '/review')) contains(github.event.comment.body, '/review'))
@@ -52,6 +59,8 @@ jobs:
run: | run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "number=${{ github.event.inputs.pr_number }}" >> $GITHUB_OUTPUT
else else
echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
fi fi
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@kaitranntt/ccs", "name": "@kaitranntt/ccs",
"version": "7.15.0-dev.3", "version": "7.15.0-dev.5",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [ "keywords": [
"cli", "cli",
+23 -7
View File
@@ -5,12 +5,17 @@
import * as fs from 'fs'; import * as fs from 'fs';
import { BinaryManagerConfig } from '../types'; import { BinaryManagerConfig } from '../types';
import { checkForUpdates, fetchLatestVersion, isNewerVersion } from './version-checker'; import {
checkForUpdates,
fetchLatestVersion,
isNewerVersion,
isVersionFaulty,
} from './version-checker';
import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer'; import { downloadAndInstall, deleteBinary, getBinaryPath } from './installer';
import { info, warn } from '../../utils/ui'; import { info, warn } from '../../utils/ui';
import { isCliproxyRunning } from '../stats-fetcher'; import { isCliproxyRunning } from '../stats-fetcher';
import { CLIPROXY_DEFAULT_PORT } from '../config-generator'; import { CLIPROXY_DEFAULT_PORT } from '../config-generator';
import { CLIPROXY_MAX_STABLE_VERSION } from '../platform-detector'; import { CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_FAULTY_RANGE } from '../platform-detector';
/** Log helper */ /** Log helper */
function log(message: string, verbose: boolean): void { function log(message: string, verbose: boolean): void {
@@ -46,15 +51,26 @@ async function handleAutoUpdate(config: BinaryManagerConfig, verbose: boolean):
const currentVersion = updateResult.currentVersion; const currentVersion = updateResult.currentVersion;
const latestVersion = updateResult.latestVersion; const latestVersion = updateResult.latestVersion;
// Check if user is on known unstable version - inform but don't force downgrade // Check if user is on known faulty version - recommend upgrade
if (isAboveMaxStable(currentVersion)) { if (isVersionFaulty(currentVersion)) {
console.log( console.log(
warn( warn(
`CLIProxy Plus v${currentVersion} has known stability issues. ` + `CLIProxy Plus v${currentVersion} has known bugs (v${CLIPROXY_FAULTY_RANGE.min.replace(/-\d+$/, '')}-${CLIPROXY_FAULTY_RANGE.max.replace(/-\d+$/, '')}). ` +
`Stable version: v${CLIPROXY_MAX_STABLE_VERSION}` `Upgrade to v${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')} recommended.`
)
);
console.log(
info(
`Run "ccs cliproxy install ${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')}" to upgrade`
)
);
} else if (isAboveMaxStable(currentVersion)) {
// Version newer than max stable (experimental)
console.log(
warn(
`CLIProxy Plus v${currentVersion} is experimental (above stable v${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')})`
) )
); );
console.log(info('Run "ccs cliproxy install 80" to downgrade, or wait for upstream fix'));
} }
if (!updateResult.hasUpdate) return; if (!updateResult.hasUpdate) return;
+14 -3
View File
@@ -17,7 +17,7 @@ import {
GITHUB_API_ALL_RELEASES, GITHUB_API_ALL_RELEASES,
VersionListResult, VersionListResult,
} from './types'; } from './types';
import { CLIPROXY_MAX_STABLE_VERSION } from '../platform-detector'; import { CLIPROXY_MAX_STABLE_VERSION, CLIPROXY_FAULTY_RANGE } from '../platform-detector';
/** /**
* Compare semver versions (true if latest > current) * Compare semver versions (true if latest > current)
@@ -43,6 +43,17 @@ export function isNewerVersion(latest: string, current: string): boolean {
return false; // Equal versions return false; // Equal versions
} }
/**
* Check if version is within the faulty range (v81-85)
* @returns true if version has known critical bugs
*/
export function isVersionFaulty(version: string): boolean {
const { min, max } = CLIPROXY_FAULTY_RANGE;
const atOrAboveMin = !isNewerVersion(min, version); // version >= min
const atOrBelowMax = !isNewerVersion(version, max); // version <= max
return atOrAboveMin && atOrBelowMax;
}
/** /**
* Fetch latest version from GitHub API * Fetch latest version from GitHub API
*/ */
@@ -123,9 +134,9 @@ export async function fetchAllVersions(verbose = false): Promise<VersionListResu
const latest = versions[0] || ''; const latest = versions[0] || '';
// Find latest stable (not newer than max stable) // Find latest stable (not newer than max stable AND not in faulty range)
const latestStable = const latestStable =
versions.find((v) => !isNewerVersion(v, CLIPROXY_MAX_STABLE_VERSION)) || versions.find((v) => !isNewerVersion(v, CLIPROXY_MAX_STABLE_VERSION) && !isVersionFaulty(v)) ||
CLIPROXY_MAX_STABLE_VERSION; CLIPROXY_MAX_STABLE_VERSION;
const result: VersionListResult = { const result: VersionListResult = {
+7
View File
@@ -21,6 +21,13 @@ export const CLIPROXY_FALLBACK_VERSION = '6.6.40-0';
*/ */
export const CLIPROXY_MAX_STABLE_VERSION = '6.6.80-0'; export const CLIPROXY_MAX_STABLE_VERSION = '6.6.80-0';
/**
* Faulty version range - versions with known critical bugs
* v81+ have context cancellation bugs causing intermittent 500 errors
* When a stable version is found, update MAX_STABLE and set faulty range accordingly
*/
export const CLIPROXY_FAULTY_RANGE = { min: '6.6.81-0', max: '6.6.999-0' };
/** @deprecated Use CLIPROXY_FALLBACK_VERSION instead */ /** @deprecated Use CLIPROXY_FALLBACK_VERSION instead */
export const CLIPROXY_VERSION = CLIPROXY_FALLBACK_VERSION; export const CLIPROXY_VERSION = CLIPROXY_FALLBACK_VERSION;
+16 -3
View File
@@ -16,7 +16,7 @@ import {
import { getPortProcess, isCLIProxyProcess } from '../../utils/port-utils'; import { getPortProcess, isCLIProxyProcess } from '../../utils/port-utils';
import type { HealthCheck } from './types'; import type { HealthCheck } from './types';
import { CLIPROXY_MAX_STABLE_VERSION } from '../../cliproxy/platform-detector'; import { CLIPROXY_MAX_STABLE_VERSION } from '../../cliproxy/platform-detector';
import { isNewerVersion } from '../../cliproxy/binary/version-checker'; import { isNewerVersion, isVersionFaulty } from '../../cliproxy/binary/version-checker';
/** /**
* Check CLIProxy binary installation * Check CLIProxy binary installation
@@ -26,17 +26,30 @@ export function checkCliproxyBinary(): HealthCheck {
const version = getInstalledCliproxyVersion(); const version = getInstalledCliproxyVersion();
const binaryPath = getCLIProxyPath(); const binaryPath = getCLIProxyPath();
// Check if version is in faulty range (v81-85)
const isFaulty = isVersionFaulty(version);
// Check if version exceeds stable cap // Check if version exceeds stable cap
const isUnstable = isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION); const isUnstable = isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION);
if (isFaulty) {
return {
id: 'cliproxy-binary',
name: 'CLIProxy Binary',
status: 'warning',
message: `v${version} (faulty)`,
details: binaryPath,
fix: `Upgrade: ccs cliproxy install ${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')}`,
};
}
if (isUnstable) { if (isUnstable) {
return { return {
id: 'cliproxy-binary', id: 'cliproxy-binary',
name: 'CLIProxy Binary', name: 'CLIProxy Binary',
status: 'warning', status: 'warning',
message: `v${version} (unstable)`, message: `v${version} (experimental)`,
details: binaryPath, details: binaryPath,
fix: `Downgrade: ccs cliproxy install ${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')}`, fix: `Stable: ccs cliproxy install ${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')}`,
}; };
} }
+26 -7
View File
@@ -26,8 +26,15 @@ import {
getInstalledCliproxyVersion, getInstalledCliproxyVersion,
installCliproxyVersion, installCliproxyVersion,
} from '../../cliproxy/binary-manager'; } from '../../cliproxy/binary-manager';
import { fetchAllVersions, isNewerVersion } from '../../cliproxy/binary/version-checker'; import {
import { CLIPROXY_MAX_STABLE_VERSION } from '../../cliproxy/platform-detector'; fetchAllVersions,
isNewerVersion,
isVersionFaulty,
} from '../../cliproxy/binary/version-checker';
import {
CLIPROXY_MAX_STABLE_VERSION,
CLIPROXY_FAULTY_RANGE,
} from '../../cliproxy/platform-detector';
const router = Router(); const router = Router();
@@ -549,6 +556,7 @@ router.get('/versions', async (_req: Request, res: Response): Promise<void> => {
...result, ...result,
currentVersion, currentVersion,
maxStableVersion: CLIPROXY_MAX_STABLE_VERSION, maxStableVersion: CLIPROXY_MAX_STABLE_VERSION,
faultyRange: CLIPROXY_FAULTY_RANGE,
}); });
} catch (error) { } catch (error) {
res.status(500).json({ error: (error as Error).message }); res.status(500).json({ error: (error as Error).message });
@@ -575,14 +583,24 @@ router.post('/install', async (req: Request, res: Response): Promise<void> => {
return; return;
} }
// Check if version is unstable // Check if version is faulty (v81-85) or experimental (above max stable)
const isUnstable = isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION); const isFaulty = isVersionFaulty(version);
const isExperimental = isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION);
if (isUnstable && !force) { if (isFaulty && !force) {
res.json({ res.json({
success: false, success: false,
requiresConfirmation: true, requiresConfirmation: true,
message: `Version ${version} is unstable (above max stable ${CLIPROXY_MAX_STABLE_VERSION}). Set force=true to proceed.`, message: `Version ${version} has known bugs (v${CLIPROXY_FAULTY_RANGE.min.replace(/-\d+$/, '')}-${CLIPROXY_FAULTY_RANGE.max.replace(/-\d+$/, '')}). Set force=true to proceed.`,
});
return;
}
if (isExperimental && !force) {
res.json({
success: false,
requiresConfirmation: true,
message: `Version ${version} is experimental (above stable ${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')}). Set force=true to proceed.`,
}); });
return; return;
} }
@@ -599,7 +617,8 @@ router.post('/install', async (req: Request, res: Response): Promise<void> => {
res.json({ res.json({
success: true, success: true,
version, version,
isUnstable, isFaulty,
isExperimental,
message: `Successfully installed CLIProxy Plus v${version}`, message: `Successfully installed CLIProxy Plus v${version}`,
}); });
} catch (error) { } catch (error) {