From 9ff4016eabcaaa2d52be2959cea0a3b4bd3af490 Mon Sep 17 00:00:00 2001 From: Mehdi Date: Fri, 13 Feb 2026 00:08:54 +0000 Subject: [PATCH 1/5] ci: track actions freshness via draft PR --- .github/workflows/actions-freshness.yml | 8 +- scripts/audit-actions-freshness.js | 275 ++++++++++++++++++------ 2 files changed, 218 insertions(+), 65 deletions(-) diff --git a/.github/workflows/actions-freshness.yml b/.github/workflows/actions-freshness.yml index 1acab39..9493f63 100644 --- a/.github/workflows/actions-freshness.yml +++ b/.github/workflows/actions-freshness.yml @@ -18,8 +18,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 permissions: - contents: read - issues: write + contents: write + pull-requests: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd @@ -31,14 +31,14 @@ jobs: with: node-version: '20' - - name: Audit pinned action references and manage tracking issue + - name: Audit pinned action references and manage tracking pull request env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | node scripts/audit-actions-freshness.js \ --report actions-freshness-report.md \ --json actions-freshness-report.json \ - --manage-issue + --manage-pr - name: Upload freshness report artifact if: always() diff --git a/scripts/audit-actions-freshness.js b/scripts/audit-actions-freshness.js index 0276960..4a57bc9 100644 --- a/scripts/audit-actions-freshness.js +++ b/scripts/audit-actions-freshness.js @@ -11,8 +11,10 @@ const { const DEFAULT_REPORT_PATH = 'actions-freshness-report.md'; const DEFAULT_JSON_PATH = 'actions-freshness-report.json'; const DEFAULT_WORKFLOW_DIRECTORY = path.join('.github', 'workflows'); -const DEFAULT_TRACKING_ISSUE_TITLE = 'CI: refresh pinned GitHub Actions SHAs'; -const TRACKING_ISSUE_MARKER = ''; +const DEFAULT_TRACKING_PULL_REQUEST_TITLE = 'CI: refresh pinned GitHub Actions SHAs'; +const DEFAULT_TRACKING_PULL_REQUEST_BRANCH = 'automation/actions-freshness-tracker'; +const DEFAULT_TRACKING_PULL_REQUEST_FILE_PATH = '.github/actions-freshness-tracker.md'; +const TRACKING_PULL_REQUEST_MARKER = ''; function toPosixPath(filePath) { return filePath.split(path.sep).join('/'); @@ -24,8 +26,10 @@ function parseArguments(argv) { jsonPath: DEFAULT_JSON_PATH, workflowDirectory: DEFAULT_WORKFLOW_DIRECTORY, failOnStale: false, - manageIssue: false, - issueTitle: DEFAULT_TRACKING_ISSUE_TITLE, + managePullRequest: false, + pullRequestTitle: DEFAULT_TRACKING_PULL_REQUEST_TITLE, + pullRequestBranch: DEFAULT_TRACKING_PULL_REQUEST_BRANCH, + pullRequestFilePath: DEFAULT_TRACKING_PULL_REQUEST_FILE_PATH, }; for (let index = 0; index < argv.length; index += 1) { @@ -49,8 +53,8 @@ function parseArguments(argv) { continue; } - if (argument === '--issue-title') { - options.issueTitle = argv[index + 1]; + if (argument === '--issue-title' || argument === '--pr-title') { + options.pullRequestTitle = argv[index + 1]; index += 1; continue; } @@ -60,8 +64,20 @@ function parseArguments(argv) { continue; } - if (argument === '--manage-issue') { - options.manageIssue = true; + if (argument === '--pr-branch') { + options.pullRequestBranch = argv[index + 1]; + index += 1; + continue; + } + + if (argument === '--pr-file-path') { + options.pullRequestFilePath = argv[index + 1]; + index += 1; + continue; + } + + if (argument === '--manage-issue' || argument === '--manage-pr') { + options.managePullRequest = true; continue; } @@ -80,8 +96,16 @@ function parseArguments(argv) { throw new Error('The --workflow-dir option requires a value.'); } - if (!options.issueTitle) { - throw new Error('The --issue-title option requires a value.'); + if (!options.pullRequestTitle) { + throw new Error('The --pr-title option requires a value.'); + } + + if (!options.pullRequestBranch) { + throw new Error('The --pr-branch option requires a value.'); + } + + if (!options.pullRequestFilePath) { + throw new Error('The --pr-file-path option requires a value.'); } return options; @@ -348,112 +372,237 @@ function buildJsonReport({ }; } -async function findTrackingIssue({ owner, repository, token, issueTitle }) { - for (let page = 1; page <= 5; page += 1) { - const issues = await githubRequest({ - endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/issues?state=open&per_page=100&page=${page}`, +function toGitHubRefPath(value) { + return value + .split('/') + .filter((segment) => segment.length > 0) + .map((segment) => encodeURIComponent(segment)) + .join('/'); +} + +function toGitHubContentPath(filePath) { + return filePath + .split('/') + .filter((segment) => segment.length > 0) + .map((segment) => encodeURIComponent(segment)) + .join('/'); +} + +async function getRepositoryMetadata({ owner, repository, token }) { + return githubRequest({ + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}`, + token, + }); +} + +async function ensureTrackingPullRequestBranch({ + owner, + repository, + token, + defaultBranch, + trackingBranch, +}) { + const defaultBranchRef = await githubRequest({ + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/git/ref/heads/${toGitHubRefPath(defaultBranch)}`, + token, + }); + const defaultBranchSha = defaultBranchRef && defaultBranchRef.object ? defaultBranchRef.object.sha : ''; + if (!defaultBranchSha) { + throw new Error(`Could not resolve latest commit on ${defaultBranch}.`); + } + + try { + await githubRequest({ + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/git/ref/heads/${toGitHubRefPath(trackingBranch)}`, token, }); - - if (!Array.isArray(issues) || issues.length === 0) { - return null; + } catch (error) { + if (error.status !== 404) { + throw error; } - const trackingIssue = issues.find( - (issue) => - !issue.pull_request && - issue.title === issueTitle && - typeof issue.body === 'string' && - issue.body.includes(TRACKING_ISSUE_MARKER) - ); + await githubRequest({ + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/git/refs`, + token, + method: 'POST', + body: { + ref: `refs/heads/${trackingBranch}`, + sha: defaultBranchSha, + }, + }); + console.log(`[actions-freshness] Created branch ${trackingBranch}`); + } +} - if (trackingIssue) { - return trackingIssue; - } +async function upsertTrackingPullRequestFile({ + owner, + repository, + token, + trackingBranch, + filePath, + content, +}) { + const endpoint = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${toGitHubContentPath(filePath)}`; + let existingSha = ''; - if (issues.length < 100) { - break; + try { + const existingFile = await githubRequest({ + endpoint: `${endpoint}?ref=${encodeURIComponent(trackingBranch)}`, + token, + }); + if (existingFile && typeof existingFile === 'object' && existingFile.sha) { + existingSha = existingFile.sha; + } + } catch (error) { + if (error.status !== 404) { + throw error; } } - return null; -} + const payload = { + message: 'chore(ci): refresh actions freshness tracker', + content: Buffer.from(content, 'utf8').toString('base64'), + branch: trackingBranch, + }; + + if (existingSha) { + payload.sha = existingSha; + } -async function closeTrackingIssue({ owner, repository, issueNumber, token }) { await githubRequest({ - endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/issues/${issueNumber}/comments`, + endpoint, token, - method: 'POST', - body: { - body: 'Automated actions freshness audit is clean. Closing this tracker.', - }, + method: 'PUT', + body: payload, }); +} +async function findTrackingPullRequest({ owner, repository, token, trackingBranch }) { + const pullRequests = await githubRequest({ + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/pulls?state=open&head=${encodeURIComponent(`${owner}:${trackingBranch}`)}&per_page=10`, + token, + }); + + if (!Array.isArray(pullRequests) || pullRequests.length === 0) { + return null; + } + + return ( + pullRequests.find( + (pullRequest) => + typeof pullRequest.body === 'string' && + pullRequest.body.includes(TRACKING_PULL_REQUEST_MARKER) + ) || pullRequests[0] + ); +} + +async function closeTrackingPullRequest({ owner, repository, pullRequestNumber, token }) { await githubRequest({ - endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/issues/${issueNumber}`, + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/pulls/${pullRequestNumber}`, token, method: 'PATCH', - body: { state: 'closed' }, + body: { + state: 'closed', + }, }); } -async function upsertTrackingIssue({ +async function upsertTrackingPullRequest({ token, owner, repository, - issueTitle, + pullRequestTitle, + pullRequestBranch, + pullRequestFilePath, staleCount, resolutionErrorCount, reportMarkdown, }) { - const trackingIssue = await findTrackingIssue({ + const trackingPullRequest = await findTrackingPullRequest({ owner, repository, token, - issueTitle, + trackingBranch: pullRequestBranch, }); - const body = `${TRACKING_ISSUE_MARKER}\n\n${reportMarkdown}`; + const body = `${TRACKING_PULL_REQUEST_MARKER}\n\n${reportMarkdown}`; const hasFindings = staleCount > 0 || resolutionErrorCount > 0; if (!hasFindings) { - if (!trackingIssue) { + if (!trackingPullRequest) { return; } - await closeTrackingIssue({ + await closeTrackingPullRequest({ owner, repository, - issueNumber: trackingIssue.number, + pullRequestNumber: trackingPullRequest.number, token, }); - console.log(`[actions-freshness] Closed issue #${trackingIssue.number}`); + console.log(`[actions-freshness] Closed pull request #${trackingPullRequest.number}`); return; } - if (trackingIssue) { + const repositoryMetadata = await getRepositoryMetadata({ owner, repository, token }); + const defaultBranch = repositoryMetadata && repositoryMetadata.default_branch; + if (!defaultBranch) { + throw new Error('Could not resolve repository default branch for tracker pull request.'); + } + + await ensureTrackingPullRequestBranch({ + owner, + repository, + token, + defaultBranch, + trackingBranch: pullRequestBranch, + }); + + const trackingFileContent = [ + TRACKING_PULL_REQUEST_MARKER, + '', + '# Actions Freshness Tracker', + '', + `Last updated: ${new Date().toISOString()}`, + '', + reportMarkdown, + '', + ].join('\n'); + + await upsertTrackingPullRequestFile({ + owner, + repository, + token, + trackingBranch: pullRequestBranch, + filePath: pullRequestFilePath, + content: trackingFileContent, + }); + + if (trackingPullRequest) { await githubRequest({ - endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/issues/${trackingIssue.number}`, + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/pulls/${trackingPullRequest.number}`, token, method: 'PATCH', body: { - title: issueTitle, + title: pullRequestTitle, body, }, }); - console.log(`[actions-freshness] Updated issue #${trackingIssue.number}`); + console.log(`[actions-freshness] Updated pull request #${trackingPullRequest.number}`); return; } - const createdIssue = await githubRequest({ - endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/issues`, + const createdPullRequest = await githubRequest({ + endpoint: `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/pulls`, token, method: 'POST', body: { - title: issueTitle, + title: pullRequestTitle, + head: pullRequestBranch, + base: defaultBranch, body, + draft: true, }, }); - console.log(`[actions-freshness] Created issue #${createdIssue.number}`); + console.log(`[actions-freshness] Created draft pull request #${createdPullRequest.number}`); } function writeReportFiles({ reportPath, jsonPath, markdownReport, jsonReport }) { @@ -514,23 +663,27 @@ async function run() { console.log(`[actions-freshness] json: ${fileOutput.jsonAbsolutePath}`); console.log(`[actions-freshness] stale pinned references: ${jsonReport.staleCount}`); - if (options.manageIssue) { + if (options.managePullRequest) { if (!token) { - throw new Error('Issue management requested, but GITHUB_TOKEN/GH_TOKEN is not set.'); + throw new Error( + 'Tracker pull request management requested, but GITHUB_TOKEN/GH_TOKEN is not set.' + ); } const repositoryMetadata = parseRepositoryFromEnvironment(); if (!repositoryMetadata) { throw new Error( - 'Issue management requested, but GITHUB_REPOSITORY is not set to owner/repository.' + 'Tracker pull request management requested, but GITHUB_REPOSITORY is not set to owner/repository.' ); } - await upsertTrackingIssue({ + await upsertTrackingPullRequest({ token, owner: repositoryMetadata.owner, repository: repositoryMetadata.repository, - issueTitle: options.issueTitle, + pullRequestTitle: options.pullRequestTitle, + pullRequestBranch: options.pullRequestBranch, + pullRequestFilePath: options.pullRequestFilePath, staleCount: jsonReport.staleCount, resolutionErrorCount: jsonReport.resolutionErrors.length, reportMarkdown: markdownReport, From 2d61b7b8dc93bf567448317643dd3de6c5701920 Mon Sep 17 00:00:00 2001 From: Mehdi Date: Fri, 13 Feb 2026 00:10:06 +0000 Subject: [PATCH 2/5] ci: do not fail freshness audit when PR creation is blocked --- scripts/audit-actions-freshness.js | 54 ++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/scripts/audit-actions-freshness.js b/scripts/audit-actions-freshness.js index 4a57bc9..a86b672 100644 --- a/scripts/audit-actions-freshness.js +++ b/scripts/audit-actions-freshness.js @@ -625,6 +625,27 @@ function writeStepSummary(markdownReport) { fs.appendFileSync(summaryPath, `${markdownReport}\n`); } +function appendStepSummaryWarning(warningMessage) { + const summaryPath = process.env.GITHUB_STEP_SUMMARY; + + if (!summaryPath) { + return; + } + + fs.appendFileSync(summaryPath, `\n> [!WARNING]\n> ${warningMessage}\n`); +} + +function isPullRequestTrackerPermissionError(error) { + if (!error || typeof error.message !== 'string') { + return false; + } + + return ( + error.status === 403 && + error.message.includes('GitHub Actions is not permitted to create or approve pull requests') + ); +} + async function run() { const options = parseArguments(process.argv.slice(2)); const workflows = readWorkflowFiles(options.workflowDirectory); @@ -677,17 +698,28 @@ async function run() { ); } - await upsertTrackingPullRequest({ - token, - owner: repositoryMetadata.owner, - repository: repositoryMetadata.repository, - pullRequestTitle: options.pullRequestTitle, - pullRequestBranch: options.pullRequestBranch, - pullRequestFilePath: options.pullRequestFilePath, - staleCount: jsonReport.staleCount, - resolutionErrorCount: jsonReport.resolutionErrors.length, - reportMarkdown: markdownReport, - }); + try { + await upsertTrackingPullRequest({ + token, + owner: repositoryMetadata.owner, + repository: repositoryMetadata.repository, + pullRequestTitle: options.pullRequestTitle, + pullRequestBranch: options.pullRequestBranch, + pullRequestFilePath: options.pullRequestFilePath, + staleCount: jsonReport.staleCount, + resolutionErrorCount: jsonReport.resolutionErrors.length, + reportMarkdown: markdownReport, + }); + } catch (error) { + if (!isPullRequestTrackerPermissionError(error)) { + throw error; + } + + const warningMessage = + 'Could not manage actions freshness tracker PR because GitHub Actions is not allowed to create pull requests. Enable this in repository Settings > Actions > General > Workflow permissions.'; + console.warn(`[actions-freshness] ${warningMessage}`); + appendStepSummaryWarning(warningMessage); + } } if (options.failOnStale && jsonReport.staleCount > 0) { From 03de2a33d261c4cc9f5c54240f623337757d6342 Mon Sep 17 00:00:00 2001 From: Mehdi Date: Fri, 13 Feb 2026 00:33:13 +0000 Subject: [PATCH 3/5] ci: add multi-model opencode matrix and strengthen review prompts --- .github/workflows/claude-review-manual.yml | 33 ++++-- .github/workflows/opencode-review-manual.yml | 108 +++++++++++++++---- 2 files changed, 112 insertions(+), 29 deletions(-) diff --git a/.github/workflows/claude-review-manual.yml b/.github/workflows/claude-review-manual.yml index 8f1dda7..70b7858 100644 --- a/.github/workflows/claude-review-manual.yml +++ b/.github/workflows/claude-review-manual.yml @@ -87,14 +87,14 @@ jobs: run: | set -euo pipefail - claude_token="$(az keyvault secret show --vault-name "${AZURE_KEY_VAULT_NAME}" --name "${CLAUDE_SECRET_NAME}" --query value -o tsv)" - if [ -z "${claude_token}" ]; then + claude_token="az keyvault secret show --vault-name "${AZURE_KEY_VAULT_NAME}" --name "${CLAUDE_SECRET_NAME}" --query value -o tsv)" + if [ -z "${claude_token}}" ]; then echo "Failed to read Claude token from Azure Key Vault secret '${CLAUDE_SECRET_NAME}'." exit 1 fi - echo "::add-mask::${claude_token}" - echo "claude_code_oauth_token=${claude_token}" >> "${GITHUB_OUTPUT}" + echo "::add-mask::${claude_token}}" + echo "claude_code_oauth_token=${claude_token}}" >> "${GITHUB_OUTPUT}" - name: Resolve pull request metadata id: pr @@ -166,15 +166,26 @@ jobs: PR BODY: ${{ steps.pr.outputs.body }} - Review this pull request and leave exactly one consolidated `gh pr comment` with: - - code quality and best practices issues - - potential bugs or regressions - - performance concerns - - security concerns - - missing or weak test coverage + You are performing a strict pull request review. Review only changed files in this PR. + Post exactly one consolidated `gh pr comment`. + Required comment format: + - first line: `Model used: anthropics/claude-code-action` + - section `### Executive Summary` + - section `### Findings` + - section `### Test Coverage Gaps` + - section `### Final Verdict` + + Findings requirements: + - prioritize by severity: Critical, High, Medium, Low + - include concrete evidence with `path:line` references from the diff + - classify each finding as one of: Bug, Security, Performance, Testing, Maintainability + - explain why it matters and provide a concrete fix + - avoid speculative findings without evidence + + If no material issues exist, state exactly `No material issues found` and list any residual risks. Review only; do not modify files, push commits, or open additional PRs. - Keep comments factual and specific to the changed code. + Keep comments factual, specific, and action-oriented. - name: Warn when Claude review fails (non-blocking) if: ${{ always() && (inputs.force_review == 'true' || fromJSON(steps.pr.outputs.changed_files) >= 5 || fromJSON(steps.pr.outputs.total_changes) >= 20) && steps.claude_review.outcome == 'failure' }} diff --git a/.github/workflows/opencode-review-manual.yml b/.github/workflows/opencode-review-manual.yml index 3d3410b..c81b9ec 100644 --- a/.github/workflows/opencode-review-manual.yml +++ b/.github/workflows/opencode-review-manual.yml @@ -8,10 +8,25 @@ on: required: true type: string model: - description: OpenCode model in provider/model format (defaults to GLM, supports any provider/model) - required: true + description: Single OpenCode model in provider/model format (used when models is empty) + required: false default: zai-coding-plan/glm-4.7 type: string + models: + description: Optional comma or newline separated model list (overrides model) + required: false + default: "" + type: string + max_parallel: + description: Maximum parallel model reviews + required: true + default: "1" + type: choice + options: + - "1" + - "2" + - "3" + - "4" force_review: description: Run review even when the PR is below default size thresholds required: true @@ -29,10 +44,62 @@ concurrency: cancel-in-progress: true jobs: + prepare-model-matrix: + name: Prepare OpenCode model matrix + runs-on: ubuntu-latest + outputs: + models_json: ${{ steps.models.outputs.models_json }} + model_count: ${{ steps.models.outputs.model_count }} + steps: + - name: Normalize model input list + id: models + env: + SINGLE_MODEL: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} + MULTI_MODELS: ${{ inputs.models }} + run: | + set -euo pipefail + + if [ -n "${MULTI_MODELS}" ]; then + source_models="${MULTI_MODELS}" + else + source_models="${SINGLE_MODEL}" + fi + + normalized_models="$( + printf '%s\n' "${source_models}" \ + | tr ',;' '\n' \ + | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ + | awk 'NF && !seen[$0]++' + )" + + if [ -z "${normalized_models}" ]; then + echo "No valid model entries were provided." + exit 1 + fi + + models_json="$(printf '%s\n' "${normalized_models}" | jq -R . | jq -cs .)" + model_count="$(printf '%s\n' "${normalized_models}" | awk 'NF' | wc -l | tr -d ' ')" + + echo "models_json=${models_json}" >> "${GITHUB_OUTPUT}" + echo "model_count=${model_count}" >> "${GITHUB_OUTPUT}" + + { + echo "### OpenCode Model Matrix" + echo "- Count: ${model_count}" + echo "- Models:" + printf ' - %s\n' ${normalized_models} + } >> "${GITHUB_STEP_SUMMARY}" + opencode-review: - name: OpenCode Manual Review + name: OpenCode Manual Review (${{ matrix.model }}) + needs: prepare-model-matrix runs-on: ubuntu-latest timeout-minutes: 15 + strategy: + fail-fast: false + max-parallel: ${{ fromJSON(inputs.max_parallel) }} + matrix: + model: ${{ fromJSON(needs.prepare-model-matrix.outputs.models_json) }} permissions: contents: read pull-requests: write @@ -218,38 +285,43 @@ jobs: MOCK_EVENT: ${{ steps.mock.outputs.event }} ZHIPU_API_KEY: ${{ steps.keyvault.outputs.zhipu_api_key }} with: - model: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} + model: ${{ matrix.model }} agent: plan prompt: | REPO: ${{ github.repository }} PR NUMBER: ${{ inputs.pr_number }} - MODEL: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} + MODEL: ${{ matrix.model }} PR TITLE: ${{ steps.pr.outputs.title }} PR BODY: ${{ steps.pr.outputs.body }} - Review this pull request and leave exactly one consolidated `gh pr comment` with: - - findings prioritized by severity: Critical, High, Medium, Low - - concrete bugs/regressions with file path evidence - - security concerns and exploitability/risk - - performance concerns with likely impact - - missing or weak test coverage and suggested tests + You are performing a strict pull request review. Review only changed files in this PR. + Post exactly one consolidated `gh pr comment` for this model execution. + + Required comment format: + - first line: `Model used: ` + - section `### Executive Summary` + - section `### Findings` + - section `### Test Coverage Gaps` + - section `### Final Verdict` - In the comment: - - start with `Model used: ` - - include a brief summary and then findings - - for each finding include why it matters and a concrete fix - - if no material issues, state `No material issues found` and list residual risks + Findings requirements: + - prioritize by severity: Critical, High, Medium, Low + - include concrete evidence with `path:line` references from the diff + - classify each finding as one of: Bug, Security, Performance, Testing, Maintainability + - explain why it matters and provide a concrete fix + - avoid speculative findings without evidence + If no material issues exist, state exactly `No material issues found` and list any residual risks. Review only; do not modify files, push commits, or open additional PRs. - Keep comments factual and specific to the changed code. + Keep comments factual, specific, and action-oriented. use_github_token: true - name: Warn when OpenCode review fails (non-blocking) if: ${{ always() && (inputs.force_review == 'true' || fromJSON(steps.pr.outputs.changed_files) >= 5 || fromJSON(steps.pr.outputs.total_changes) >= 20) && steps.opencode_review.outcome == 'failure' }} env: PR_NUMBER: ${{ inputs.pr_number }} - MODEL: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} + MODEL: ${{ matrix.model }} run: | echo "::warning::OpenCode manual review failed but is configured as non-blocking. Check the previous step logs." { From d434ce32c51f7a76bc361f9e93b4c8f39094d71d Mon Sep 17 00:00:00 2001 From: Mehdi Date: Fri, 13 Feb 2026 01:27:45 +0000 Subject: [PATCH 4/5] ci: keep freshness PR scoped to tracker automation only --- .github/workflows/claude-review-manual.yml | 33 ++---- .github/workflows/opencode-review-manual.yml | 108 ++++--------------- 2 files changed, 29 insertions(+), 112 deletions(-) diff --git a/.github/workflows/claude-review-manual.yml b/.github/workflows/claude-review-manual.yml index 70b7858..8f1dda7 100644 --- a/.github/workflows/claude-review-manual.yml +++ b/.github/workflows/claude-review-manual.yml @@ -87,14 +87,14 @@ jobs: run: | set -euo pipefail - claude_token="az keyvault secret show --vault-name "${AZURE_KEY_VAULT_NAME}" --name "${CLAUDE_SECRET_NAME}" --query value -o tsv)" - if [ -z "${claude_token}}" ]; then + claude_token="$(az keyvault secret show --vault-name "${AZURE_KEY_VAULT_NAME}" --name "${CLAUDE_SECRET_NAME}" --query value -o tsv)" + if [ -z "${claude_token}" ]; then echo "Failed to read Claude token from Azure Key Vault secret '${CLAUDE_SECRET_NAME}'." exit 1 fi - echo "::add-mask::${claude_token}}" - echo "claude_code_oauth_token=${claude_token}}" >> "${GITHUB_OUTPUT}" + echo "::add-mask::${claude_token}" + echo "claude_code_oauth_token=${claude_token}" >> "${GITHUB_OUTPUT}" - name: Resolve pull request metadata id: pr @@ -166,26 +166,15 @@ jobs: PR BODY: ${{ steps.pr.outputs.body }} - You are performing a strict pull request review. Review only changed files in this PR. - Post exactly one consolidated `gh pr comment`. + Review this pull request and leave exactly one consolidated `gh pr comment` with: + - code quality and best practices issues + - potential bugs or regressions + - performance concerns + - security concerns + - missing or weak test coverage - Required comment format: - - first line: `Model used: anthropics/claude-code-action` - - section `### Executive Summary` - - section `### Findings` - - section `### Test Coverage Gaps` - - section `### Final Verdict` - - Findings requirements: - - prioritize by severity: Critical, High, Medium, Low - - include concrete evidence with `path:line` references from the diff - - classify each finding as one of: Bug, Security, Performance, Testing, Maintainability - - explain why it matters and provide a concrete fix - - avoid speculative findings without evidence - - If no material issues exist, state exactly `No material issues found` and list any residual risks. Review only; do not modify files, push commits, or open additional PRs. - Keep comments factual, specific, and action-oriented. + Keep comments factual and specific to the changed code. - name: Warn when Claude review fails (non-blocking) if: ${{ always() && (inputs.force_review == 'true' || fromJSON(steps.pr.outputs.changed_files) >= 5 || fromJSON(steps.pr.outputs.total_changes) >= 20) && steps.claude_review.outcome == 'failure' }} diff --git a/.github/workflows/opencode-review-manual.yml b/.github/workflows/opencode-review-manual.yml index c81b9ec..3d3410b 100644 --- a/.github/workflows/opencode-review-manual.yml +++ b/.github/workflows/opencode-review-manual.yml @@ -8,25 +8,10 @@ on: required: true type: string model: - description: Single OpenCode model in provider/model format (used when models is empty) - required: false + description: OpenCode model in provider/model format (defaults to GLM, supports any provider/model) + required: true default: zai-coding-plan/glm-4.7 type: string - models: - description: Optional comma or newline separated model list (overrides model) - required: false - default: "" - type: string - max_parallel: - description: Maximum parallel model reviews - required: true - default: "1" - type: choice - options: - - "1" - - "2" - - "3" - - "4" force_review: description: Run review even when the PR is below default size thresholds required: true @@ -44,62 +29,10 @@ concurrency: cancel-in-progress: true jobs: - prepare-model-matrix: - name: Prepare OpenCode model matrix - runs-on: ubuntu-latest - outputs: - models_json: ${{ steps.models.outputs.models_json }} - model_count: ${{ steps.models.outputs.model_count }} - steps: - - name: Normalize model input list - id: models - env: - SINGLE_MODEL: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} - MULTI_MODELS: ${{ inputs.models }} - run: | - set -euo pipefail - - if [ -n "${MULTI_MODELS}" ]; then - source_models="${MULTI_MODELS}" - else - source_models="${SINGLE_MODEL}" - fi - - normalized_models="$( - printf '%s\n' "${source_models}" \ - | tr ',;' '\n' \ - | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \ - | awk 'NF && !seen[$0]++' - )" - - if [ -z "${normalized_models}" ]; then - echo "No valid model entries were provided." - exit 1 - fi - - models_json="$(printf '%s\n' "${normalized_models}" | jq -R . | jq -cs .)" - model_count="$(printf '%s\n' "${normalized_models}" | awk 'NF' | wc -l | tr -d ' ')" - - echo "models_json=${models_json}" >> "${GITHUB_OUTPUT}" - echo "model_count=${model_count}" >> "${GITHUB_OUTPUT}" - - { - echo "### OpenCode Model Matrix" - echo "- Count: ${model_count}" - echo "- Models:" - printf ' - %s\n' ${normalized_models} - } >> "${GITHUB_STEP_SUMMARY}" - opencode-review: - name: OpenCode Manual Review (${{ matrix.model }}) - needs: prepare-model-matrix + name: OpenCode Manual Review runs-on: ubuntu-latest timeout-minutes: 15 - strategy: - fail-fast: false - max-parallel: ${{ fromJSON(inputs.max_parallel) }} - matrix: - model: ${{ fromJSON(needs.prepare-model-matrix.outputs.models_json) }} permissions: contents: read pull-requests: write @@ -285,43 +218,38 @@ jobs: MOCK_EVENT: ${{ steps.mock.outputs.event }} ZHIPU_API_KEY: ${{ steps.keyvault.outputs.zhipu_api_key }} with: - model: ${{ matrix.model }} + model: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} agent: plan prompt: | REPO: ${{ github.repository }} PR NUMBER: ${{ inputs.pr_number }} - MODEL: ${{ matrix.model }} + MODEL: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} PR TITLE: ${{ steps.pr.outputs.title }} PR BODY: ${{ steps.pr.outputs.body }} - You are performing a strict pull request review. Review only changed files in this PR. - Post exactly one consolidated `gh pr comment` for this model execution. - - Required comment format: - - first line: `Model used: ` - - section `### Executive Summary` - - section `### Findings` - - section `### Test Coverage Gaps` - - section `### Final Verdict` + Review this pull request and leave exactly one consolidated `gh pr comment` with: + - findings prioritized by severity: Critical, High, Medium, Low + - concrete bugs/regressions with file path evidence + - security concerns and exploitability/risk + - performance concerns with likely impact + - missing or weak test coverage and suggested tests - Findings requirements: - - prioritize by severity: Critical, High, Medium, Low - - include concrete evidence with `path:line` references from the diff - - classify each finding as one of: Bug, Security, Performance, Testing, Maintainability - - explain why it matters and provide a concrete fix - - avoid speculative findings without evidence + In the comment: + - start with `Model used: ` + - include a brief summary and then findings + - for each finding include why it matters and a concrete fix + - if no material issues, state `No material issues found` and list residual risks - If no material issues exist, state exactly `No material issues found` and list any residual risks. Review only; do not modify files, push commits, or open additional PRs. - Keep comments factual, specific, and action-oriented. + Keep comments factual and specific to the changed code. use_github_token: true - name: Warn when OpenCode review fails (non-blocking) if: ${{ always() && (inputs.force_review == 'true' || fromJSON(steps.pr.outputs.changed_files) >= 5 || fromJSON(steps.pr.outputs.total_changes) >= 20) && steps.opencode_review.outcome == 'failure' }} env: PR_NUMBER: ${{ inputs.pr_number }} - MODEL: ${{ matrix.model }} + MODEL: ${{ inputs.model || 'zai-coding-plan/glm-4.7' }} run: | echo "::warning::OpenCode manual review failed but is configured as non-blocking. Check the previous step logs." { From 375f3bf71746d1d9147c9c43eefea5fbf1bb3a9d Mon Sep 17 00:00:00 2001 From: Mehdi Date: Fri, 13 Feb 2026 01:41:54 +0000 Subject: [PATCH 5/5] ci: harden tracker PR detection and alias error messaging --- scripts/audit-actions-freshness.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/audit-actions-freshness.js b/scripts/audit-actions-freshness.js index a86b672..d292bc8 100644 --- a/scripts/audit-actions-freshness.js +++ b/scripts/audit-actions-freshness.js @@ -97,7 +97,7 @@ function parseArguments(argv) { } if (!options.pullRequestTitle) { - throw new Error('The --pr-title option requires a value.'); + throw new Error('The --pr-title/--issue-title option requires a value.'); } if (!options.pullRequestBranch) { @@ -492,7 +492,7 @@ async function findTrackingPullRequest({ owner, repository, token, trackingBranc (pullRequest) => typeof pullRequest.body === 'string' && pullRequest.body.includes(TRACKING_PULL_REQUEST_MARKER) - ) || pullRequests[0] + ) || null ); }