|
1 | | - |
2 | 1 | name: Sync Codacy Issues |
3 | 2 |
|
4 | 3 | on: |
|
9 | 8 | required: false |
10 | 9 | default: "false" |
11 | 10 | push: |
12 | | - # branches to consider in the event; optional, defaults to all |
13 | 11 | branches: |
14 | 12 | - master |
| 13 | + - main |
| 14 | + |
15 | 15 | jobs: |
16 | 16 | sync-codacy-issues: |
17 | 17 | runs-on: ubuntu-latest |
| 18 | + permissions: |
| 19 | + issues: write |
| 20 | + contents: read |
| 21 | + |
18 | 22 | steps: |
19 | 23 | - name: Checkout |
20 | 24 | uses: actions/checkout@v4 |
21 | 25 |
|
| 26 | + - name: Install jq and gh |
| 27 | + run: | |
| 28 | + sudo apt-get update -y |
| 29 | + sudo apt-get install -y jq gh |
| 30 | +
|
| 31 | + # 🔹 STEP 1: Fetch Codacy Issues |
22 | 32 | - name: Fetch Codacy Issues |
| 33 | + env: |
| 34 | + CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} |
23 | 35 | run: | |
24 | 36 | curl --request POST \ |
25 | 37 | --url "https://app.codacy.com/api/v3/analysis/organizations/gh/${{ github.repository_owner }}/repositories/${{ github.event.repository.name }}/issues/search" \ |
26 | | - --header "api-token: ${{ secrets.CODACY_API_TOKEN }}" \ |
| 38 | + --header "api-token: $CODACY_API_TOKEN" \ |
27 | 39 | --header "content-type: application/json" \ |
28 | 40 | --data '{"levels":["Error","Warning","High"]}' \ |
29 | 41 | --silent \ |
30 | 42 | --fail \ |
31 | | - -o issues.json |
32 | | -
|
33 | | - - name: Extract issues |
34 | | - run: jq '.data' issues.json > filtered_issues.json |
35 | | - |
36 | | - - name: Create GitHub Issues |
37 | | - uses: actions/github-script@v7 |
38 | | - with: |
39 | | - script: | |
40 | | - const fs = require('fs'); |
41 | | - const dryRun = "${{ github.event.inputs.dry_run }}" === "true"; |
42 | | -
|
43 | | - const rawIssues = JSON.parse(fs.readFileSync('filtered_issues.json', 'utf8')); |
44 | | -
|
45 | | - const grouped = {}; |
46 | | - for (const issue of rawIssues) { |
47 | | - grouped[issue.issueId] = grouped[issue.issueId] || []; |
48 | | - grouped[issue.issueId].push(issue); |
49 | | - } |
50 | | -
|
51 | | - const openIssues = await github.paginate( |
52 | | - github.rest.issues.listForRepo, |
53 | | - { |
54 | | - owner: context.repo.owner, |
55 | | - repo: context.repo.repo, |
56 | | - state: "open" |
57 | | - } |
58 | | - ); |
59 | | -
|
60 | | - console.log(`Fetched ${openIssues.length} open issues from GitHub`); |
61 | | -
|
62 | | - const existingIds = new Set(); |
63 | | - for (const ghIssue of openIssues) { |
64 | | - const matches = ghIssue.body?.match(/codacy-issue-([a-f0-9]+)/g); |
65 | | - if (matches) { |
66 | | - matches.forEach(m => existingIds.add(m.replace("codacy-issue-", ""))); |
67 | | - } |
68 | | - } |
69 | | -
|
70 | | - console.log(`Found ${existingIds.size} existing Codacy issues in GitHub`); |
71 | | -
|
72 | | - for (const [issueId, issues] of Object.entries(grouped)) { |
73 | | - if (existingIds.has(issueId)) { |
74 | | - console.log(`Skipping duplicate Codacy issueId ${issueId}`); |
75 | | - continue; |
76 | | - } |
77 | | -
|
78 | | - const key = `codacy-issue-${issueId}`; |
79 | | - const first = issues[0]; |
80 | | - const title = `[Codacy] ${first.patternInfo.severityLevel} issue(s) in ${first.filePath}`; |
81 | | -
|
82 | | - let body = `Codacy detected **${issues.length}** occurrence(s) of rule \`${first.patternInfo.id}\`:\n\n`; |
83 | | - for (const issue of issues) { |
84 | | - body += `- **${issue.patternInfo.severityLevel}** at \`${issue.filePath}:${issue.lineNumber}\` → ${issue.message}\n`; |
85 | | - } |
86 | | - body += `\nSee full details in [Codacy Report](https://app.codacy.com/gh/${context.repo.owner}/${context.repo.repo}/issues)\n\n`; |
87 | | - body += `Unique ID: \`${key}\``; |
88 | | -
|
89 | | - if (dryRun) { |
90 | | - console.log(`[DRY RUN] Would create issue: ${title}`); |
91 | | - } else { |
92 | | - await github.rest.issues.create({ |
93 | | - owner: context.repo.owner, |
94 | | - repo: context.repo.repo, |
95 | | - title, |
96 | | - body, |
97 | | - labels: ["codacy"] |
98 | | - }); |
99 | | - console.log(`✅ Created GitHub issue for Codacy issueId ${issueId}`); |
100 | | - await new Promise(resolve => setTimeout(resolve, 2000)); |
101 | | - } |
102 | | - } |
103 | | -
|
104 | | - - name: Close Resolved GitHub Issues |
105 | | - uses: actions/github-script@v7 |
106 | | - with: |
107 | | - script: | |
108 | | - const fs = require('fs'); |
109 | | - const dryRun = "${{ github.event.inputs.dry_run }}" === "true"; |
110 | | - const rawIssues = JSON.parse(fs.readFileSync('filtered_issues.json', 'utf8')); |
111 | | -
|
112 | | - // Build current Codacy set (only *active* issues, not ignored) |
113 | | - const currentCodacyIds = new Set( |
114 | | - rawIssues.filter(i => !i.ignored).map(i => i.issueId) |
115 | | - ); |
116 | | -
|
117 | | - // Build ignored Codacy set |
118 | | - const ignoredCodacyIds = new Set( |
119 | | - rawIssues.filter(i => i.ignored).map(i => i.issueId) |
120 | | - ); |
121 | | -
|
122 | | - // Fetch ALL GitHub issues with codacy label |
123 | | - const allIssues = await github.paginate( |
124 | | - github.rest.issues.listForRepo, |
125 | | - { |
126 | | - owner: context.repo.owner, |
127 | | - repo: context.repo.repo, |
128 | | - state: "all", |
129 | | - labels: ["codacy"] |
130 | | - } |
131 | | - ); |
| 43 | + -o codacy_issues.json |
132 | 44 |
|
133 | | - for (const ghIssue of allIssues) { |
134 | | - const matches = ghIssue.body?.match(/codacy-issue-([a-f0-9]+)/g); |
135 | | - if (!matches) continue; |
136 | | -
|
137 | | - for (const match of matches) { |
138 | | - const issueId = match.replace("codacy-issue-", ""); |
139 | | -
|
140 | | - // Close if not active OR explicitly ignored |
141 | | - if ((!currentCodacyIds.has(issueId) || ignoredCodacyIds.has(issueId)) |
142 | | - && ghIssue.state === "open") { |
143 | | - if (dryRun) { |
144 | | - console.log(`[DRY RUN] Would close issue #${ghIssue.number} (Codacy issueId ${issueId})`); |
145 | | - } else { |
146 | | - // Add comment before closing |
147 | | - const reason = ignoredCodacyIds.has(issueId) |
148 | | - ? "Auto closed because Codacy issue is marked as *ignored*" |
149 | | - : "Auto closed as not found in last analysis"; |
150 | | -
|
151 | | - await github.rest.issues.createComment({ |
152 | | - owner: context.repo.owner, |
153 | | - repo: context.repo.repo, |
154 | | - issue_number: ghIssue.number, |
155 | | - body: reason |
156 | | - }); |
157 | | -
|
158 | | - await github.rest.issues.update({ |
159 | | - owner: context.repo.owner, |
160 | | - repo: context.repo.repo, |
161 | | - issue_number: ghIssue.number, |
162 | | - state: "closed" |
163 | | - }); |
164 | | - console.log(`❌ Closed GitHub issue #${ghIssue.number} for Codacy issueId ${issueId}`); |
165 | | - } |
166 | | - } |
167 | | - } |
168 | | - } |
169 | | -
|
170 | | - - name: Close Duplicate Codacy Issues |
171 | | - uses: actions/github-script@v7 |
172 | | - with: |
173 | | - script: | |
174 | | - const dryRun = "${{ github.event.inputs.dry_run }}" === "true"; |
175 | | -
|
176 | | - // Fetch all issues with the codacy label (open + closed) |
177 | | - const allIssues = await github.paginate( |
178 | | - github.rest.issues.listForRepo, |
179 | | - { |
180 | | - owner: context.repo.owner, |
181 | | - repo: context.repo.repo, |
182 | | - state: "all", |
183 | | - labels: ["codacy"] |
184 | | - } |
185 | | - ); |
186 | | -
|
187 | | - const grouped = {}; |
188 | | -
|
189 | | - for (const issue of allIssues) { |
190 | | - const matches = issue.body?.match(/codacy-issue-([a-f0-9]+)/g); |
191 | | - if (!matches) continue; |
192 | | -
|
193 | | - for (const match of matches) { |
194 | | - const issueId = match.replace("codacy-issue-", ""); |
195 | | - if (!grouped[issueId]) grouped[issueId] = []; |
196 | | - grouped[issueId].push(issue); |
197 | | - } |
198 | | - } |
199 | | -
|
200 | | - for (const [issueId, issues] of Object.entries(grouped)) { |
201 | | - if (issues.length <= 1) continue; // No duplicates |
202 | | -
|
203 | | - // Sort by creation date descending (newest first) |
204 | | - issues.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); |
205 | | -
|
206 | | - const [latest, ...duplicates] = issues; |
207 | | - console.log(`Found ${issues.length} duplicates for Codacy issueId ${issueId}. Keeping #${latest.number}.`); |
208 | | -
|
209 | | - for (const dup of duplicates) { |
210 | | - if (dup.state === "closed") continue; |
211 | | -
|
212 | | - if (dryRun) { |
213 | | - console.log(`[DRY RUN] Would close duplicate issue #${dup.number} (Codacy issueId ${issueId})`); |
214 | | - } else { |
215 | | - await github.rest.issues.createComment({ |
216 | | - owner: context.repo.owner, |
217 | | - repo: context.repo.repo, |
218 | | - issue_number: dup.number, |
219 | | - body: `Auto-closing as duplicate of newer Codacy issue #${latest.number} for ID \`${issueId}\`.` |
220 | | - }); |
221 | | -
|
222 | | - await github.rest.issues.update({ |
223 | | - owner: context.repo.owner, |
224 | | - repo: context.repo.repo, |
225 | | - issue_number: dup.number, |
226 | | - state: "closed" |
227 | | - }); |
228 | | -
|
229 | | - console.log(`❌ Closed duplicate issue #${dup.number} (Codacy issueId ${issueId})`); |
230 | | - } |
231 | | - } |
232 | | - } |
| 45 | + # 🔹 STEP 2: Ensure standard labels exist with your colors |
| 46 | + - name: Ensure standard labels exist |
| 47 | + env: |
| 48 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 49 | + run: | |
| 50 | + declare -A LABELS=( |
| 51 | + ["task"]="Tasks and work items|#aefcde" |
| 52 | + ["refactor"]="Code cleanup and best practices|#eb69a2" |
| 53 | + ["bug"]="Bugs and error-prone code|#b60205" |
| 54 | + ["security"]="Security-related issues|#d93f0b" |
| 55 | + ["performance"]="Performance optimization issues|#e57504" |
| 56 | + ) |
| 57 | + for label in "${!LABELS[@]}"; do |
| 58 | + desc=${LABELS[$label]%%|*} |
| 59 | + color=${LABELS[$label]##*|} |
| 60 | + echo "Creating/updating label '$label'" |
| 61 | + gh label create "$label" --description "$desc" --color "$color" --repo ${{ github.repository }} --force |
| 62 | + done |
| 63 | +
|
| 64 | + # 🔹 STEP 3: Sync Codacy Issues (create/update) |
| 65 | + - name: Sync Codacy Issues |
| 66 | + env: |
| 67 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 68 | + run: | |
| 69 | + dry_run="${{ github.event.inputs.dry_run }}" |
| 70 | + current_issues=$(gh issue list --repo ${{ github.repository }} --state open --json number,title,body,labels) |
| 71 | + |
| 72 | + jq -c '.data[]' codacy_issues.json | while read -r issue; do |
| 73 | + id=$(echo "$issue" | jq -r '.issueId') |
| 74 | + file=$(echo "$issue" | jq -r '.filePath') |
| 75 | + line=$(echo "$issue" | jq -r '.lineNumber') |
| 76 | + message=$(echo "$issue" | jq -r '.message') |
| 77 | + line_text=$(echo "$issue" | jq -r '.lineText // empty') |
| 78 | + category=$(echo "$issue" | jq -r '.patternInfo.category') |
| 79 | + severity=$(echo "$issue" | jq -r '.patternInfo.severityLevel') |
| 80 | + sha=$(echo "$issue" | jq -r '.commitInfo.sha') |
| 81 | + |
| 82 | + title="[$category] $message" |
| 83 | + |
| 84 | + # Prepare the code snippet inline |
| 85 | + if [ -n "$line_text" ]; then |
| 86 | + # Replace literal \n from JSON and real newlines with space |
| 87 | + clean_line_text=$(echo "$line_text" | tr -d '\r' | tr '\n' ' ' | sed 's/\\n/ /g') |
| 88 | + code_snippet="\`\`\` |
| 89 | + $clean_line_text |
| 90 | + \`\`\`" |
| 91 | + else |
| 92 | + code_snippet="" |
| 93 | + fi |
| 94 | + |
| 95 | + # Build the full body with Codacy ID at the top |
| 96 | + body="**Codacy ID:** \`$id\` |
233 | 97 |
|
234 | | -
|
| 98 | + **File:** \`$file\` |
| 99 | + **Line:** $line |
| 100 | + **Rule:** $category ($severity) |
| 101 | + **Commit:** \`$sha\`" |
235 | 102 |
|
| 103 | + # Append code snippet if present |
| 104 | + if [ -n "$code_snippet" ]; then |
| 105 | + body+=" |
| 106 | + |
| 107 | + **Code Snippet:** |
| 108 | + $code_snippet" |
| 109 | + fi |
| 110 | + |
| 111 | + # Determine labels based on message/category |
| 112 | + labels=() |
| 113 | + if echo "$message" | grep -iq "todo"; then |
| 114 | + labels+=("task") |
| 115 | + elif echo "$category" | grep -Eiq "best[_ ]?practice|code[_ ]?style|complexity"; then |
| 116 | + labels+=("refactor") |
| 117 | + elif echo "$message" | grep -iq "security"; then |
| 118 | + labels+=("task" "security") |
| 119 | + elif echo "$category" | grep -iq "performance"; then |
| 120 | + labels+=("task" "performance") |
| 121 | + elif echo "$message" | grep -Eiq "error[_ ]?prone"; then |
| 122 | + labels+=("bug") |
| 123 | + fi |
| 124 | + |
| 125 | + label_string=$(IFS=,; echo "${labels[*]}") |
| 126 | + |
| 127 | + # Check if issue exists |
| 128 | + existing_number=$(echo "$current_issues" | jq -r --arg id "$id" '.[] | select(.body | contains($id)) | .number') |
| 129 | + |
| 130 | + if [ -n "$existing_number" ]; then |
| 131 | + echo "Updating existing issue #$existing_number ($id)" |
| 132 | + if [ "$dry_run" != "true" ]; then |
| 133 | + gh issue edit "$existing_number" \ |
| 134 | + --body "$body" \ |
| 135 | + --add-label "$label_string" \ |
| 136 | + --repo ${{ github.repository }} |
| 137 | + else |
| 138 | + echo "[DRY RUN] Would update issue #$existing_number with labels: $label_string" |
| 139 | + fi |
| 140 | + else |
| 141 | + echo "Creating new issue for Codacy ID $id" |
| 142 | + if [ "$dry_run" != "true" ]; then |
| 143 | + gh issue create \ |
| 144 | + --title "$title" \ |
| 145 | + --body "$body" \ |
| 146 | + --label "$label_string" \ |
| 147 | + --repo ${{ github.repository }} |
| 148 | + else |
| 149 | + echo "[DRY RUN] Would create issue '$title' with labels: $label_string" |
| 150 | + fi |
| 151 | + fi |
| 152 | + done |
| 153 | + |
| 154 | + |
| 155 | + # 🔹 STEP 4: Close resolved issues |
| 156 | + - name: Close resolved issues |
| 157 | + env: |
| 158 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 159 | + run: | |
| 160 | + current_ids=$(jq -r '.data[].issueId' codacy_issues.json) |
| 161 | + gh issue list --repo ${{ github.repository }} --state open --label codacy --json number,body | jq -c '.[]' | while read -r issue; do |
| 162 | + number=$(echo "$issue" | jq -r '.number') |
| 163 | + body=$(echo "$issue" | jq -r '.body') |
| 164 | + id=$(echo "$body" | grep -oE 'Codacy ID: [a-f0-9]+' | awk '{print $3}') |
| 165 | + if [ -n "$id" ] && ! echo "$current_ids" | grep -q "$id"; then |
| 166 | + echo "Closing stale Codacy issue #$number ($id)" |
| 167 | + if [ "$dry_run" != "true" ]; then |
| 168 | + gh issue close "$number" --comment "This issue no longer appears in Codacy reports and has been auto-closed." --repo ${{ github.repository }} |
| 169 | + else |
| 170 | + echo "[DRY RUN] Would close issue #$number" |
| 171 | + fi |
| 172 | + fi |
| 173 | + done |
0 commit comments