Repository navigation
Expand file tree
/
Copy pathaction.yml
More file actions
261 lines (248 loc) · 12.9 KB
/
Copy pathaction.yml
File metadata and controls
261 lines (248 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
name: github-actions-annotations-reporter
description: Turn workflow annotations into dedup-aware GitHub Issues — severity-labeled, won't-fix-aware, auto-closing when noise stops.
author: Yannic Labonte
branding:
icon: alert-triangle
color: yellow
inputs:
version:
description: 'npm tag or version of github-actions-annotations-reporter to run via `npx`. Defaults to `1`, matching the action major; override to pin tighter or to float across majors.'
required: false
default: '1'
github-token:
description: 'Token used for GitHub API auth. Defaults to the workflow-provided `github.token` when left empty.'
required: false
default: ''
workflows:
description: 'Whitespace-separated globs of workflow names or paths to include (e.g. "ci.yml release.*"). Individual globs may not contain whitespace — match names that do with a glob (e.g. "My*CI").'
required: false
default: ''
reject:
description: 'Whitespace-separated globs of workflows to exclude. Same whitespace caveat as `workflows`.'
required: false
default: ''
branch:
description: 'Branch whose latest run is scanned per workflow. Empty (the default) → the repo default branch.'
required: false
default: ''
min-severity:
description: 'Minimum annotation severity to file: notice, warning, or error.'
required: false
default: 'notice'
management-label:
description: 'Label applied to every issue managed by this action.'
required: false
default: 'automation/annotation-reporter'
max-issues:
description: 'Cap on the number of issue create/update/reopen writes per run. Auto-close is not capped.'
required: false
default: '25'
wontfix-labels:
description: 'Comma-separated labels on closed issues that should be treated as "won''t fix" suppressions.'
required: false
default: 'wontfix'
wontfix-respect-state-reason:
description: 'Treat `state_reason: not_planned` as a suppression signal on closed issues.'
required: false
default: 'true'
wontfix-comment-pattern:
description: 'Regex matched against the closing comment of a closed issue. Empty disables.'
required: false
default: ''
auto-close:
description: 'Auto-close managed issues whose annotation has vanished, subject to the guards below.'
required: false
default: 'true'
auto-close-after-days:
description: 'Minimum days since the annotation was last seen before auto-closing.'
required: false
default: '7'
auto-close-after-misses:
description: 'Minimum consecutive scans the annotation must be absent for before auto-closing.'
required: false
default: '3'
auto-close-require-success:
description: "Only consider auto-close when the workflow's latest run succeeded. Strongly recommended."
required: false
default: 'true'
dry-run:
description: 'Log what would change without creating, updating, or closing any issues.'
required: false
default: 'false'
fail-on-new:
description: 'Exit non-zero if any new issues were created on this run.'
required: false
default: 'false'
list-annotations:
description: 'Include every found annotation (full detail) in the JSON report under `annotations[]`.'
required: false
default: 'false'
outputs:
new-issues:
description: 'Number of issues newly created on this run.'
value: ${{ steps.run.outputs.new-issues }}
updated-issues:
description: 'Number of existing open issues touched on this run.'
value: ${{ steps.run.outputs.updated-issues }}
reopened-issues:
description: 'Number of previously-closed issues reopened on this run.'
value: ${{ steps.run.outputs.reopened-issues }}
suppressed:
description: 'Annotations skipped because a prior closed issue matched a "won''t fix" signal.'
value: ${{ steps.run.outputs.suppressed }}
auto-closed:
description: 'Managed open issues auto-closed because their annotation has vanished.'
value: ${{ steps.run.outputs.auto-closed }}
auto-close-held:
description: 'Managed open issues whose annotation has vanished but that did not meet the auto-close threshold yet (miss counter + age window).'
value: ${{ steps.run.outputs.auto-close-held }}
skipped:
description: 'Create / update / reopen actions that were dropped because `max-issues` was already exhausted on this run.'
value: ${{ steps.run.outputs.skipped }}
total-annotations:
description: 'Total deduped annotations observed after severity filtering.'
value: ${{ steps.run.outputs.total-annotations }}
json:
description: 'Path to the JSON report file the action produced.'
value: ${{ steps.run.outputs.json }}
runs:
using: composite
steps:
- id: run
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.github-token != '' && inputs.github-token || github.token }}
GHAAR_VERSION: ${{ inputs.version }}
GHAAR_WORKFLOWS: ${{ inputs.workflows }}
GHAAR_REJECT: ${{ inputs.reject }}
GHAAR_BRANCH: ${{ inputs.branch }}
GHAAR_MIN_SEVERITY: ${{ inputs.min-severity }}
GHAAR_MANAGEMENT_LABEL: ${{ inputs.management-label }}
GHAAR_MAX_ISSUES: ${{ inputs.max-issues }}
GHAAR_WONTFIX_LABELS: ${{ inputs.wontfix-labels }}
GHAAR_WONTFIX_RESPECT_STATE_REASON: ${{ inputs.wontfix-respect-state-reason }}
GHAAR_WONTFIX_COMMENT_PATTERN: ${{ inputs.wontfix-comment-pattern }}
GHAAR_AUTO_CLOSE: ${{ inputs.auto-close }}
GHAAR_AUTO_CLOSE_AFTER_DAYS: ${{ inputs.auto-close-after-days }}
GHAAR_AUTO_CLOSE_AFTER_MISSES: ${{ inputs.auto-close-after-misses }}
GHAAR_AUTO_CLOSE_REQUIRE_SUCCESS: ${{ inputs.auto-close-require-success }}
GHAAR_DRY_RUN: ${{ inputs.dry-run }}
GHAAR_FAIL_ON_NEW: ${{ inputs.fail-on-new }}
GHAAR_LIST_ANNOTATIONS: ${{ inputs.list-annotations }}
GHAAR_REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Validate `version` against a tight allowlist BEFORE it ever reaches `npx`.
# The package spec `github-actions-annotations-reporter@<version>` is parsed by
# npm, and npm accepts more than just semver/dist-tags — tarball URLs, git URLs,
# file paths, and alias syntax all resolve to arbitrary code that overrides the
# package name. If a caller ever wires this input from attacker-controllable data
# (issue body, PR title, etc.), the action would happily execute that code.
if [[ ! "$GHAAR_VERSION" =~ ^[A-Za-z0-9][A-Za-z0-9._+-]*$ ]]; then
echo "::error::Invalid 'version' input '${GHAAR_VERSION}'. Expected a dist-tag (e.g. 'latest', '1') or a semver (e.g. '1.2.3', '1.2.3-rc.1'); refusing to pass it to npx."
exit 1
fi
# Inputs reach the CLI through env vars set above (GHAAR_*), never inline via the
# template expression syntax, so glob/regex/label values cannot be shell-parsed.
# Each value is then pushed onto an args array so npx receives them as separate
# argv entries — no shell re-parsing.
args=(report --json --repo "$GHAAR_REPO")
# Word-split filters / rejections without invoking pathname expansion.
if [[ -n "$GHAAR_WORKFLOWS" ]]; then
read -r -a extra <<<"$GHAAR_WORKFLOWS"
if (( ${#extra[@]} > 0 )); then
args+=(--workflows "${extra[@]}")
fi
fi
if [[ -n "$GHAAR_REJECT" ]]; then
read -r -a extra <<<"$GHAAR_REJECT"
if (( ${#extra[@]} > 0 )); then
args+=(--reject "${extra[@]}")
fi
fi
[[ -n "$GHAAR_BRANCH" ]] && args+=(--branch "$GHAAR_BRANCH")
[[ -n "$GHAAR_MIN_SEVERITY" ]] && args+=(--min-severity "$GHAAR_MIN_SEVERITY")
[[ -n "$GHAAR_MANAGEMENT_LABEL" ]] && args+=(--management-label "$GHAAR_MANAGEMENT_LABEL")
[[ -n "$GHAAR_MAX_ISSUES" ]] && args+=(--max-issues "$GHAAR_MAX_ISSUES")
if [[ -n "$GHAAR_WONTFIX_LABELS" ]]; then
# Split comma-separated labels, trim leading/trailing whitespace
# only (preserving internal spaces — GitHub allows multi-word
# labels), drop empties, pass as repeated values.
IFS=',' read -r -a labels <<<"$GHAAR_WONTFIX_LABELS"
cleaned=()
for l in "${labels[@]}"; do
l_trim="$(echo "$l" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
[[ -n "$l_trim" ]] && cleaned+=("$l_trim")
done
if (( ${#cleaned[@]} > 0 )); then
args+=(--wontfix-labels "${cleaned[@]}")
fi
fi
[[ "$GHAAR_WONTFIX_RESPECT_STATE_REASON" == "false" ]] && args+=(--no-wontfix-respect-state-reason)
[[ -n "$GHAAR_WONTFIX_COMMENT_PATTERN" ]] && args+=(--wontfix-comment-pattern "$GHAAR_WONTFIX_COMMENT_PATTERN")
[[ "$GHAAR_AUTO_CLOSE" == "false" ]] && args+=(--no-auto-close)
[[ -n "$GHAAR_AUTO_CLOSE_AFTER_DAYS" ]] && args+=(--auto-close-after-days "$GHAAR_AUTO_CLOSE_AFTER_DAYS")
[[ -n "$GHAAR_AUTO_CLOSE_AFTER_MISSES" ]] && args+=(--auto-close-after-misses "$GHAAR_AUTO_CLOSE_AFTER_MISSES")
[[ "$GHAAR_AUTO_CLOSE_REQUIRE_SUCCESS" == "false" ]] && args+=(--no-auto-close-require-success)
[[ "$GHAAR_DRY_RUN" == "true" ]] && args+=(--dry-run)
[[ "$GHAAR_FAIL_ON_NEW" == "true" ]] && args+=(--fail-on-new)
[[ "$GHAAR_LIST_ANNOTATIONS" == "true" ]] && args+=(--list-annotations)
# Unique per-invocation report path so multiple uses in one job don't clobber each
# other's `json` output.
report=$(mktemp "$RUNNER_TEMP/ghaar.XXXXXXXX.json")
args+=(--json-out "$report")
# Install the package to a fresh prefix and invoke its bin directly,
# rather than relying on `npx -p PKG BIN` or `npm exec --package=PKG`.
# On `ubuntu-latest` runners (npm 10.x in the ubuntu-24.04 image),
# both heuristic-based forms were observed to skip the install step
# and fall through to `sh -c "ghaar …"` → `command not found` →
# exit 127. Explicit `npm install --prefix` + direct invocation of
# `node_modules/.bin/ghaar` bypasses every bin-resolution code path
# and works identically on npm 10 and 11.
#
# `mktemp -d` gives a unique per-invocation prefix so multiple uses
# of this action in one job don't collide on the same node_modules.
# `--no-save` and the throwaway prefix mean nothing leaks back to
# the workflow's checkout.
#
# Both the install AND the CLI run under suspended `errexit`: if
# the install fails (network, missing version, registry down), the
# action must still emit its `json` / counter outputs (set to the
# safe defaults below) instead of dying half-way and leaving
# downstream steps with no values. The `&&` short-circuit means we
# only invoke the CLI when the install succeeded; `cli_exit` then
# captures whichever step failed (install non-zero → cli_exit > 0,
# bin invocation non-zero → cli_exit > 0).
ghaar_install=$(mktemp -d "$RUNNER_TEMP/ghaar-install.XXXXXXXX")
set +e
npm install --silent --no-save --no-audit --no-fund --prefix "$ghaar_install" \
"github-actions-annotations-reporter@$GHAAR_VERSION" \
&& "$ghaar_install/node_modules/.bin/ghaar" "${args[@]}"
cli_exit=$?
set -e
# The CLI itself sets GitHub outputs via @actions/core when GITHUB_ACTIONS=true.
# But we additionally surface the report path and key counters here, derived from
# the JSON, so callers can always rely on shell-level outputs even if @actions/core
# ever fails to set them (e.g. when the CLI errors after the pipeline runs but
# before emit). `// 0` handles missing fields; `2>/dev/null || echo 0` handles a
# broken/missing report.
new_issues=$(jq '.summary.newIssues // 0' "$report" 2>/dev/null || echo 0)
updated_issues=$(jq '.summary.updatedIssues // 0' "$report" 2>/dev/null || echo 0)
reopened_issues=$(jq '.summary.reopenedIssues // 0' "$report" 2>/dev/null || echo 0)
suppressed=$(jq '.summary.suppressed // 0' "$report" 2>/dev/null || echo 0)
auto_closed=$(jq '.summary.autoClosed // 0' "$report" 2>/dev/null || echo 0)
auto_close_held=$(jq '.summary.autoCloseHeld // 0' "$report" 2>/dev/null || echo 0)
skipped=$(jq '.summary.skipped // 0' "$report" 2>/dev/null || echo 0)
total_annotations=$(jq '.summary.totalAnnotations // 0' "$report" 2>/dev/null || echo 0)
{
echo "json=$report"
echo "new-issues=$new_issues"
echo "updated-issues=$updated_issues"
echo "reopened-issues=$reopened_issues"
echo "suppressed=$suppressed"
echo "auto-closed=$auto_closed"
echo "auto-close-held=$auto_close_held"
echo "skipped=$skipped"
echo "total-annotations=$total_annotations"
} >> "$GITHUB_OUTPUT"
exit "$cli_exit"