@plainconceptsplatform/workflows 1.2.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/route-processing.js +1 -0
- package/loops/actions/add-issue-labels/action.yml +50 -50
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/apply-agent-bundle/action.yml +24 -24
- package/loops/actions/apply-agent-comments/action.yml +42 -42
- package/loops/actions/apply-agent-labels/action.yml +55 -55
- package/loops/actions/apply-agent-output/action.yml +108 -108
- package/loops/actions/assess-blast-radius/action.yml +47 -8
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +27 -5
- package/loops/actions/assess-blast-radius/migration-is-additive.sh +38 -0
- package/loops/actions/classify-route/action.yml +2 -1
- package/loops/actions/classify-route/classify-route.sh +7 -1
- package/loops/actions/cleanup-artifacts/action.yml +91 -91
- package/loops/actions/close-agent-issues/action.yml +43 -43
- package/loops/actions/create-agent-issues/action.yml +52 -52
- package/loops/actions/create-issue-comment/action.yml +29 -29
- package/loops/actions/download-agent-output/action.yml +53 -53
- package/loops/actions/link-pr-to-issue/action.yml +40 -40
- package/loops/actions/list-open-issues/action.yml +33 -33
- package/loops/actions/load-issue-context/action.yml +45 -45
- package/loops/actions/merge-agent-pr/action.yml +49 -49
- package/loops/actions/push-agent-branch/action.yml +45 -45
- package/loops/actions/remove-issue-labels/action.yml +37 -37
- package/loops/actions/update-agent-issues/action.yml +58 -58
- package/loops/actions/validate-merge-gate-output/action.yml +6 -1
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +9 -2
- package/loops/actions/validate-review-output/action.yml +35 -35
- package/loops/actions/validate-triage-output/action.yml +36 -36
- package/loops/actions/verify-composite-actions/action.yml +9 -9
- package/loops/actions/verify-refine-output/action.yml +9 -9
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +149 -29
- package/loops/docs/diagrams.md +26 -11
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +5 -0
- package/loops/workflows/agent-implement.md +6 -0
- package/loops/workflows/agent-merge-gate.md +100 -3
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/evidence.yml +79 -4
- package/loops/workflows/shared/platform-defaults.md +23 -23
- package/loops/workflows/work-router.yml +97 -21
- package/package.json +1 -1
|
@@ -35,6 +35,10 @@ inputs:
|
|
|
35
35
|
description: Lines changed at or above which the radius is MEDIUM.
|
|
36
36
|
required: false
|
|
37
37
|
default: '200'
|
|
38
|
+
ignore-paths:
|
|
39
|
+
description: Extended regular expression. Matching paths count toward neither size nor owner and sensitive paths; protected paths still match them.
|
|
40
|
+
required: false
|
|
41
|
+
default: ''
|
|
38
42
|
outputs:
|
|
39
43
|
level:
|
|
40
44
|
description: "low, medium or high."
|
|
@@ -64,11 +68,17 @@ outputs:
|
|
|
64
68
|
description: CODEOWNERS entries covering the changed paths, space separated. Empty when the repository has no CODEOWNERS.
|
|
65
69
|
value: ${{ steps.assess.outputs.required_owners }}
|
|
66
70
|
files_changed:
|
|
67
|
-
description: Number of files
|
|
71
|
+
description: Number of files that count toward size, ignored paths excluded.
|
|
68
72
|
value: ${{ steps.assess.outputs.files_changed }}
|
|
69
73
|
lines_changed:
|
|
70
|
-
description: Additions plus deletions.
|
|
74
|
+
description: Additions plus deletions of the files that count toward size.
|
|
71
75
|
value: ${{ steps.assess.outputs.lines_changed }}
|
|
76
|
+
files_total:
|
|
77
|
+
description: Number of files in the whole diff.
|
|
78
|
+
value: ${{ steps.assess.outputs.files_total }}
|
|
79
|
+
lines_total:
|
|
80
|
+
description: Additions plus deletions of the whole diff.
|
|
81
|
+
value: ${{ steps.assess.outputs.lines_total }}
|
|
72
82
|
runs:
|
|
73
83
|
using: composite
|
|
74
84
|
steps:
|
|
@@ -86,22 +96,51 @@ runs:
|
|
|
86
96
|
HIGH_LINES: ${{ inputs.high-lines }}
|
|
87
97
|
MEDIUM_FILES: ${{ inputs.medium-files }}
|
|
88
98
|
MEDIUM_LINES: ${{ inputs.medium-lines }}
|
|
99
|
+
IGNORE_PATHS: ${{ inputs.ignore-paths }}
|
|
89
100
|
run: |
|
|
90
101
|
set -euo pipefail
|
|
91
102
|
# One API call. The job this replaced already paid for it to grep PROTECTED_PATHS, and
|
|
92
103
|
# the additions and deletions come back in the same payload, so the diff shape costs
|
|
93
104
|
# nothing extra.
|
|
94
105
|
gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files?per_page=100" \
|
|
95
|
-
--jq '[.[] | {filename, additions, deletions}]' > /tmp/blast-files.json
|
|
106
|
+
--jq '[.[] | {filename, status, additions, deletions, patch}]' | jq -s 'add // []' > /tmp/blast-files.json
|
|
107
|
+
|
|
108
|
+
all_files=$(jq -r '.[].filename' /tmp/blast-files.json)
|
|
109
|
+
files_total=$(jq -r 'length' /tmp/blast-files.json)
|
|
110
|
+
lines_total=$(jq -r '[.[] | .additions + .deletions] | add // 0' /tmp/blast-files.json)
|
|
111
|
+
|
|
112
|
+
# Tests, docs, specs, translations and generated code change no runtime behaviour, so they
|
|
113
|
+
# do not make a diff bigger. They stay in ALL_PATHS, so a protected path is never ignored.
|
|
114
|
+
if [ -n "$IGNORE_PATHS" ]; then
|
|
115
|
+
jq -c --arg re "$IGNORE_PATHS" '[.[] | select(.filename | test($re) | not)]' /tmp/blast-files.json > /tmp/blast-counted.json
|
|
116
|
+
else
|
|
117
|
+
cp /tmp/blast-files.json /tmp/blast-counted.json
|
|
118
|
+
fi
|
|
119
|
+
files=$(jq -r '.[].filename' /tmp/blast-counted.json)
|
|
120
|
+
files_changed=$(jq -r 'length' /tmp/blast-counted.json)
|
|
121
|
+
lines_changed=$(jq -r '[.[] | .additions + .deletions] | add // 0' /tmp/blast-counted.json)
|
|
96
122
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
# A new EF Core migration whose Up() only adds schema. An edited one never qualifies:
|
|
124
|
+
# changing a migration that may already have run is exactly what needs its owner.
|
|
125
|
+
additive=""
|
|
126
|
+
while IFS= read -r candidate; do
|
|
127
|
+
[ -n "$candidate" ] || continue
|
|
128
|
+
if jq -r --arg f "$candidate" '.[] | select(.filename == $f) | .patch // ""' /tmp/blast-counted.json |
|
|
129
|
+
bash "${{ github.action_path }}/migration-is-additive.sh" >/dev/null; then
|
|
130
|
+
additive="${additive}${additive:+$'\n'}${candidate}"
|
|
131
|
+
fi
|
|
132
|
+
done < <(jq -r '.[] | select(.status == "added") | .filename
|
|
133
|
+
| select(test("(^|/)[Mm]igrations/[^/]+[.]cs$"))
|
|
134
|
+
| select(test("([.]Designer|ModelSnapshot)[.]cs$") | not)' /tmp/blast-counted.json)
|
|
100
135
|
|
|
101
|
-
|
|
136
|
+
ALL_PATHS="$all_files" ADDITIVE_MIGRATIONS="$additive" \
|
|
137
|
+
IGNORED_FILES=$((files_total - files_changed)) IGNORED_LINES=$((lines_total - lines_changed)) \
|
|
138
|
+
bash "${{ github.action_path }}/assess-blast-radius.sh" \
|
|
102
139
|
"$files_changed" "$lines_changed" <<<"$files" > /tmp/blast-out.env
|
|
103
140
|
|
|
104
141
|
cat /tmp/blast-out.env >> "$GITHUB_OUTPUT"
|
|
142
|
+
echo "files_total=$files_total" >> "$GITHUB_OUTPUT"
|
|
143
|
+
echo "lines_total=$lines_total" >> "$GITHUB_OUTPUT"
|
|
105
144
|
|
|
106
145
|
# CODEOWNERS refines who is asked; it never decides whether anyone is. A repository
|
|
107
146
|
# without the file still gets an owner disposition from OWNER_PATHS, and the comment
|
|
@@ -139,7 +178,7 @@ runs:
|
|
|
139
178
|
esac
|
|
140
179
|
# An unanchored CODEOWNERS pattern matches at any depth, an anchored one only at the root.
|
|
141
180
|
if [ "$anchored" -eq 1 ]; then regex="^${regex}"; else regex="(^|/)${regex}"; fi
|
|
142
|
-
if printf '%s\n' "$
|
|
181
|
+
if printf '%s\n' "$all_files" | grep -qE "$regex"; then
|
|
143
182
|
owners="$owners $rest"
|
|
144
183
|
fi
|
|
145
184
|
done < "$candidate"
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/assess-blast-radius/assess-blast-radius.sh. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
# Classify a pull request's blast radius from its changed paths and diff shape.
|
|
4
4
|
#
|
|
5
|
-
# Reads the changed paths on stdin, one per line. Prints
|
|
5
|
+
# Reads the changed paths that count toward the diff's size on stdin, one per line. Prints
|
|
6
|
+
# GITHUB_OUTPUT lines on stdout. ALL_PATHS, when set, is every changed path, ignored ones included:
|
|
7
|
+
# protected paths are matched against it, so ignoring a path never unprotects it.
|
|
6
8
|
# Deliberately model-free: the point of this file is that the most consequential input to the
|
|
7
9
|
# merge decision is computed from facts a shell can check, before any agent reads the diff.
|
|
8
10
|
#
|
|
@@ -21,8 +23,13 @@ HIGH_FILES="${HIGH_FILES:-20}"
|
|
|
21
23
|
HIGH_LINES="${HIGH_LINES:-800}"
|
|
22
24
|
MEDIUM_FILES="${MEDIUM_FILES:-5}"
|
|
23
25
|
MEDIUM_LINES="${MEDIUM_LINES:-200}"
|
|
26
|
+
# Added migrations the caller proved only add schema (migration-is-additive.sh), one per line.
|
|
27
|
+
ADDITIVE_MIGRATIONS="${ADDITIVE_MIGRATIONS:-}"
|
|
28
|
+
IGNORED_FILES="${IGNORED_FILES:-0}"
|
|
29
|
+
IGNORED_LINES="${IGNORED_LINES:-0}"
|
|
24
30
|
|
|
25
31
|
files="$(cat)"
|
|
32
|
+
all_paths="${ALL_PATHS:-$files}"
|
|
26
33
|
|
|
27
34
|
# An empty regex matches every line in grep -E, which would mark every pull request protected.
|
|
28
35
|
# A list nobody configured must match nothing, not everything.
|
|
@@ -34,9 +41,9 @@ files="$(cat)"
|
|
|
34
41
|
# before any of them is used: a `match` failure cannot report itself, because `match` runs
|
|
35
42
|
# inside a command substitution and anything it sets dies with the subshell.
|
|
36
43
|
match() {
|
|
37
|
-
local pattern="$1"
|
|
44
|
+
local pattern="$1" paths="${2-$files}"
|
|
38
45
|
[ -n "$pattern" ] || return 0
|
|
39
|
-
printf '%s\n' "$
|
|
46
|
+
printf '%s\n' "$paths" | grep -E "$pattern" || true
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
# Compile each configured pattern against nothing. grep exits 2 when the regex is bad, which is
|
|
@@ -50,14 +57,22 @@ for name in PROTECTED_PATHS OWNER_PATHS SENSITIVE_PATHS; do
|
|
|
50
57
|
fi
|
|
51
58
|
done
|
|
52
59
|
|
|
53
|
-
protected_hits="$(match "$PROTECTED_PATHS")"
|
|
60
|
+
protected_hits="$(match "$PROTECTED_PATHS" "$all_paths")"
|
|
54
61
|
owner_hits="$(match "$OWNER_PATHS")"
|
|
55
62
|
sensitive_hits="$(match "$SENSITIVE_PATHS")"
|
|
56
63
|
|
|
64
|
+
# An additive migration loses no data, so it asks for a second look (medium) rather than its
|
|
65
|
+
# owner. Every other owner path still forces high.
|
|
66
|
+
additive_hits=""
|
|
67
|
+
if [ -n "$ADDITIVE_MIGRATIONS" ] && [ -n "$owner_hits" ]; then
|
|
68
|
+
additive_hits="$(printf '%s\n' "$owner_hits" | grep -Fxf <(printf '%s\n' "$ADDITIVE_MIGRATIONS") || true)"
|
|
69
|
+
owner_hits="$(printf '%s\n' "$owner_hits" | grep -vFxf <(printf '%s\n' "$ADDITIVE_MIGRATIONS") || true)"
|
|
70
|
+
fi
|
|
71
|
+
|
|
57
72
|
# A threshold that is not a number turns its comparison into a shell error that `set -e` does
|
|
58
73
|
# not catch, because the test is an `if` condition: `HIGH_FILES=twenty` printed a diagnostic to
|
|
59
74
|
# stderr and quietly produced `level=low` for a diff of any size.
|
|
60
|
-
for threshold in HIGH_FILES HIGH_LINES MEDIUM_FILES MEDIUM_LINES files_changed lines_changed; do
|
|
75
|
+
for threshold in HIGH_FILES HIGH_LINES MEDIUM_FILES MEDIUM_LINES files_changed lines_changed IGNORED_FILES IGNORED_LINES; do
|
|
61
76
|
case "${!threshold}" in
|
|
62
77
|
""|*[!0-9]*)
|
|
63
78
|
echo "assess-blast-radius: ${threshold} must be a whole number, got '${!threshold}'" >&2
|
|
@@ -92,6 +107,10 @@ if [ "$lines_changed" -ge "$HIGH_LINES" ]; then
|
|
|
92
107
|
fi
|
|
93
108
|
|
|
94
109
|
if [ "$level" != high ]; then
|
|
110
|
+
if [ -n "$additive_hits" ]; then
|
|
111
|
+
level=medium
|
|
112
|
+
add_signal "additive migrations, no owner needed ($(printf '%s\n' "$additive_hits" | tr '\n' ' ' | sed 's/ $//'))"
|
|
113
|
+
fi
|
|
95
114
|
if [ -n "$sensitive_hits" ]; then
|
|
96
115
|
level=medium
|
|
97
116
|
add_signal "sensitive paths touched ($(printf '%s\n' "$sensitive_hits" | tr '\n' ' ' | sed 's/ $//'))"
|
|
@@ -107,6 +126,9 @@ if [ "$level" != high ]; then
|
|
|
107
126
|
fi
|
|
108
127
|
|
|
109
128
|
[ -n "$signals" ] || signals="no blast-radius signal fired: $files_changed files, $lines_changed lines, no configured path matched"
|
|
129
|
+
if [ "$IGNORED_FILES" -gt 0 ]; then
|
|
130
|
+
add_signal "not counted toward size: $IGNORED_FILES files, $IGNORED_LINES lines (tests, docs, specs, translations, generated code)"
|
|
131
|
+
fi
|
|
110
132
|
|
|
111
133
|
# A fixed heredoc delimiter is a fail-open here. Every multi-line value below is built from
|
|
112
134
|
# paths the pull request chose, so a path that is exactly the delimiter closes its block early
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/assess-blast-radius/migration-is-additive.sh. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
+
# Decide whether a newly added EF Core migration only adds schema. Reads the file's diff patch on
|
|
4
|
+
# stdin; exits 0 and prints "additive" when it does, exits 1 and prints the reason when it does not.
|
|
5
|
+
#
|
|
6
|
+
# Only Up() is read: Down() of an additive migration is all drops by construction. Anything that
|
|
7
|
+
# can lose or rewrite data, or a structure this cannot follow, is not additive.
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
awk '
|
|
12
|
+
/^\+/ { line = substr($0, 2) }
|
|
13
|
+
!/^\+/ { next }
|
|
14
|
+
line ~ /void[ \t]+Up[ \t]*\(/ { in_up = 1; seen_up = 1; next }
|
|
15
|
+
line ~ /void[ \t]+Down[ \t]*\(/ { in_up = 0; next }
|
|
16
|
+
!in_up { next }
|
|
17
|
+
line ~ /\.(Drop[A-Za-z]*|Rename[A-Za-z]*|Alter[A-Za-z]*|Sql|DeleteData|UpdateData|RestartSequence)[ \t]*[(<]/ {
|
|
18
|
+
match(line, /\.[A-Za-z]+[ \t]*[(<]/)
|
|
19
|
+
reason = "Up() calls " substr(line, RSTART + 1, RLENGTH - 1)
|
|
20
|
+
exit
|
|
21
|
+
}
|
|
22
|
+
line ~ /\.AddColumn[ \t]*</ { in_column = 1; column = "" }
|
|
23
|
+
in_column {
|
|
24
|
+
column = column " " line
|
|
25
|
+
if (line ~ /\);/) {
|
|
26
|
+
in_column = 0
|
|
27
|
+
if (column ~ /nullable:[ \t]*false/ && column !~ /defaultValue/) {
|
|
28
|
+
reason = "Up() adds a required column with no default"
|
|
29
|
+
exit
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
END {
|
|
34
|
+
if (reason == "" && !seen_up) reason = "no Up() method found in the added lines"
|
|
35
|
+
if (reason != "") { print reason; exit 1 }
|
|
36
|
+
print "additive"
|
|
37
|
+
}
|
|
38
|
+
'
|
|
@@ -56,6 +56,7 @@ runs:
|
|
|
56
56
|
PR_MERGED: ${{ github.event.pull_request.merged }}
|
|
57
57
|
COMMENT_ON_PR: ${{ github.event.issue.pull_request != '' }}
|
|
58
58
|
COMMENT_SENDER_TYPE: ${{ github.event.comment.user.type }}
|
|
59
|
+
REVIEW_STATE: ${{ github.event.review.state }}
|
|
59
60
|
ACTOR: ${{ github.actor }}
|
|
60
61
|
RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
|
|
61
62
|
RUN_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
|
|
@@ -84,7 +85,7 @@ runs:
|
|
|
84
85
|
|
|
85
86
|
- name: Resolve the issue the pull request closes
|
|
86
87
|
id: linked-issue
|
|
87
|
-
if:
|
|
88
|
+
if: contains(fromJson('["merge-gate","apply-review","merge-approved"]'), steps.classify.outputs.route)
|
|
88
89
|
shell: bash
|
|
89
90
|
env:
|
|
90
91
|
GH_TOKEN: ${{ inputs.token }}
|
|
@@ -135,7 +135,13 @@ classify_route() {
|
|
|
135
135
|
;;
|
|
136
136
|
|
|
137
137
|
pull_request_review_comment | pull_request_review)
|
|
138
|
-
|
|
138
|
+
# An approval is a decision, not feedback: the router merges the pull request when the
|
|
139
|
+
# gate had handed it to a person (merge-approved checks the rest).
|
|
140
|
+
if [ "${EVENT:-}" = "pull_request_review" ] && [ "${REVIEW_STATE:-}" = "approved" ]; then
|
|
141
|
+
route="merge-approved"
|
|
142
|
+
else
|
|
143
|
+
route="apply-review"
|
|
144
|
+
fi
|
|
139
145
|
pr_number="${EVENT_PR_NUMBER:-}"
|
|
140
146
|
;;
|
|
141
147
|
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/cleanup-artifacts/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Clean up artifacts
|
|
3
|
-
description: Delete workflow artifacts older than the configured retention period.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token with actions write access.
|
|
7
|
-
required: true
|
|
8
|
-
artifact-retention-days:
|
|
9
|
-
description: Retention period in days. Invalid or empty values use 3.
|
|
10
|
-
required: false
|
|
11
|
-
default: ''
|
|
12
|
-
runs:
|
|
13
|
-
using: composite
|
|
14
|
-
steps:
|
|
15
|
-
- name: Delete expired artifacts
|
|
16
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
17
|
-
env:
|
|
18
|
-
ARTIFACT_RETENTION_DAYS: ${{ inputs.artifact-retention-days }}
|
|
19
|
-
with:
|
|
20
|
-
github-token: ${{ inputs.token }}
|
|
21
|
-
script: |
|
|
22
|
-
const parsedDays = Number.parseInt(process.env.ARTIFACT_RETENTION_DAYS, 10);
|
|
23
|
-
const days = Number.isInteger(parsedDays) && parsedDays > 0 ? parsedDays : 3;
|
|
24
|
-
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
25
|
-
|
|
26
|
-
core.info(`Retention: ${days} days (cutoff ${cutoff.toISOString()})`);
|
|
27
|
-
|
|
28
|
-
const artifacts = await github.paginate(github.rest.actions.listArtifactsForRepo, {
|
|
29
|
-
...context.repo,
|
|
30
|
-
per_page: 100,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const expired = artifacts.filter((artifact) => new Date(artifact.created_at) < cutoff);
|
|
34
|
-
|
|
35
|
-
let deleted = 0;
|
|
36
|
-
const failed = [];
|
|
37
|
-
|
|
38
|
-
for (const artifact of expired) {
|
|
39
|
-
// GitHub answers a delete with a 5xx now and then. Three tries, because the
|
|
40
|
-
// alternative is a red daily run over one artifact that tomorrow's run would
|
|
41
|
-
// have removed anyway.
|
|
42
|
-
let lastError;
|
|
43
|
-
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
44
|
-
try {
|
|
45
|
-
await github.rest.actions.deleteArtifact({
|
|
46
|
-
...context.repo,
|
|
47
|
-
artifact_id: artifact.id,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
core.info(`Deleted ${artifact.name} (created ${artifact.created_at})`);
|
|
51
|
-
deleted += 1;
|
|
52
|
-
lastError = undefined;
|
|
53
|
-
break;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
lastError = error;
|
|
56
|
-
if (attempt < 3) await new Promise((wait) => setTimeout(wait, attempt * 2000));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (lastError) {
|
|
61
|
-
core.warning(
|
|
62
|
-
`Could not delete ${artifact.name} (${artifact.id}) after 3 tries: ${lastError.message}`,
|
|
63
|
-
);
|
|
64
|
-
failed.push({ name: artifact.name, status: lastError.status });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
await core.summary
|
|
69
|
-
.addHeading('Artifact cleanup', 2)
|
|
70
|
-
.addList([
|
|
71
|
-
`Retention: ${days} days (cutoff ${cutoff.toISOString()})`,
|
|
72
|
-
`Deleted: ${deleted}`,
|
|
73
|
-
`Failed to delete: ${failed.length}`,
|
|
74
|
-
`Within retention: ${artifacts.length - expired.length}`,
|
|
75
|
-
])
|
|
76
|
-
.write();
|
|
77
|
-
|
|
78
|
-
// Deleting nothing at all while expired artifacts exist is a permission or
|
|
79
|
-
// configuration problem, and has to be loud. One stubborn artifact among many is
|
|
80
|
-
// the API having a bad minute: tomorrow's run picks it up, and failing here only
|
|
81
|
-
// teaches people to ignore a red cleanup. A 4xx is never transient, so it fails
|
|
82
|
-
// whatever else succeeded.
|
|
83
|
-
const names = failed.map((entry) => entry.name).join(', ');
|
|
84
|
-
const structural = failed.some((entry) => entry.status && entry.status < 500);
|
|
85
|
-
if (failed.length > 0 && (deleted === 0 || structural)) {
|
|
86
|
-
core.setFailed(`${failed.length} artifact(s) could not be deleted: ${names}`);
|
|
87
|
-
} else if (failed.length > 0) {
|
|
88
|
-
core.warning(
|
|
89
|
-
`${failed.length} artifact(s) survived this run and will be retried tomorrow: ${names}`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/cleanup-artifacts/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Clean up artifacts
|
|
3
|
+
description: Delete workflow artifacts older than the configured retention period.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token with actions write access.
|
|
7
|
+
required: true
|
|
8
|
+
artifact-retention-days:
|
|
9
|
+
description: Retention period in days. Invalid or empty values use 3.
|
|
10
|
+
required: false
|
|
11
|
+
default: ''
|
|
12
|
+
runs:
|
|
13
|
+
using: composite
|
|
14
|
+
steps:
|
|
15
|
+
- name: Delete expired artifacts
|
|
16
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
17
|
+
env:
|
|
18
|
+
ARTIFACT_RETENTION_DAYS: ${{ inputs.artifact-retention-days }}
|
|
19
|
+
with:
|
|
20
|
+
github-token: ${{ inputs.token }}
|
|
21
|
+
script: |
|
|
22
|
+
const parsedDays = Number.parseInt(process.env.ARTIFACT_RETENTION_DAYS, 10);
|
|
23
|
+
const days = Number.isInteger(parsedDays) && parsedDays > 0 ? parsedDays : 3;
|
|
24
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
25
|
+
|
|
26
|
+
core.info(`Retention: ${days} days (cutoff ${cutoff.toISOString()})`);
|
|
27
|
+
|
|
28
|
+
const artifacts = await github.paginate(github.rest.actions.listArtifactsForRepo, {
|
|
29
|
+
...context.repo,
|
|
30
|
+
per_page: 100,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const expired = artifacts.filter((artifact) => new Date(artifact.created_at) < cutoff);
|
|
34
|
+
|
|
35
|
+
let deleted = 0;
|
|
36
|
+
const failed = [];
|
|
37
|
+
|
|
38
|
+
for (const artifact of expired) {
|
|
39
|
+
// GitHub answers a delete with a 5xx now and then. Three tries, because the
|
|
40
|
+
// alternative is a red daily run over one artifact that tomorrow's run would
|
|
41
|
+
// have removed anyway.
|
|
42
|
+
let lastError;
|
|
43
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
44
|
+
try {
|
|
45
|
+
await github.rest.actions.deleteArtifact({
|
|
46
|
+
...context.repo,
|
|
47
|
+
artifact_id: artifact.id,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
core.info(`Deleted ${artifact.name} (created ${artifact.created_at})`);
|
|
51
|
+
deleted += 1;
|
|
52
|
+
lastError = undefined;
|
|
53
|
+
break;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
lastError = error;
|
|
56
|
+
if (attempt < 3) await new Promise((wait) => setTimeout(wait, attempt * 2000));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (lastError) {
|
|
61
|
+
core.warning(
|
|
62
|
+
`Could not delete ${artifact.name} (${artifact.id}) after 3 tries: ${lastError.message}`,
|
|
63
|
+
);
|
|
64
|
+
failed.push({ name: artifact.name, status: lastError.status });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await core.summary
|
|
69
|
+
.addHeading('Artifact cleanup', 2)
|
|
70
|
+
.addList([
|
|
71
|
+
`Retention: ${days} days (cutoff ${cutoff.toISOString()})`,
|
|
72
|
+
`Deleted: ${deleted}`,
|
|
73
|
+
`Failed to delete: ${failed.length}`,
|
|
74
|
+
`Within retention: ${artifacts.length - expired.length}`,
|
|
75
|
+
])
|
|
76
|
+
.write();
|
|
77
|
+
|
|
78
|
+
// Deleting nothing at all while expired artifacts exist is a permission or
|
|
79
|
+
// configuration problem, and has to be loud. One stubborn artifact among many is
|
|
80
|
+
// the API having a bad minute: tomorrow's run picks it up, and failing here only
|
|
81
|
+
// teaches people to ignore a red cleanup. A 4xx is never transient, so it fails
|
|
82
|
+
// whatever else succeeded.
|
|
83
|
+
const names = failed.map((entry) => entry.name).join(', ');
|
|
84
|
+
const structural = failed.some((entry) => entry.status && entry.status < 500);
|
|
85
|
+
if (failed.length > 0 && (deleted === 0 || structural)) {
|
|
86
|
+
core.setFailed(`${failed.length} artifact(s) could not be deleted: ${names}`);
|
|
87
|
+
} else if (failed.length > 0) {
|
|
88
|
+
core.warning(
|
|
89
|
+
`${failed.length} artifact(s) survived this run and will be retried tomorrow: ${names}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/close-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Close agent issues
|
|
3
|
-
description: Close every close_issue item from agent_output.json.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
token:
|
|
9
|
-
description: GitHub token with issues:write.
|
|
10
|
-
required: true
|
|
11
|
-
runs:
|
|
12
|
-
using: composite
|
|
13
|
-
steps:
|
|
14
|
-
- name: Close issues
|
|
15
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
16
|
-
env:
|
|
17
|
-
AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
|
|
18
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
19
|
-
with:
|
|
20
|
-
github-token: ${{ inputs.token }}
|
|
21
|
-
script: |
|
|
22
|
-
const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
|
|
23
|
-
const items = readAgentItems(process.env.OUTPUT_FILE, 'close_issue');
|
|
24
|
-
|
|
25
|
-
let closed = 0;
|
|
26
|
-
|
|
27
|
-
for (const item of items) {
|
|
28
|
-
if (!item.item_number) {
|
|
29
|
-
core.warning(`close_issue item has no item_number, skipping: ${JSON.stringify(item)}`);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
await github.rest.issues.update({
|
|
34
|
-
...context.repo,
|
|
35
|
-
issue_number: Number(item.item_number),
|
|
36
|
-
state: 'closed',
|
|
37
|
-
state_reason: item.state_reason === 'not_planned' ? 'not_planned' : 'completed',
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
closed += 1;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
core.info(`Closed ${closed} issue(s).`);
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/close-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Close agent issues
|
|
3
|
+
description: Close every close_issue item from agent_output.json.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
token:
|
|
9
|
+
description: GitHub token with issues:write.
|
|
10
|
+
required: true
|
|
11
|
+
runs:
|
|
12
|
+
using: composite
|
|
13
|
+
steps:
|
|
14
|
+
- name: Close issues
|
|
15
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
16
|
+
env:
|
|
17
|
+
AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
|
|
18
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
19
|
+
with:
|
|
20
|
+
github-token: ${{ inputs.token }}
|
|
21
|
+
script: |
|
|
22
|
+
const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
|
|
23
|
+
const items = readAgentItems(process.env.OUTPUT_FILE, 'close_issue');
|
|
24
|
+
|
|
25
|
+
let closed = 0;
|
|
26
|
+
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
if (!item.item_number) {
|
|
29
|
+
core.warning(`close_issue item has no item_number, skipping: ${JSON.stringify(item)}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await github.rest.issues.update({
|
|
34
|
+
...context.repo,
|
|
35
|
+
issue_number: Number(item.item_number),
|
|
36
|
+
state: 'closed',
|
|
37
|
+
state_reason: item.state_reason === 'not_planned' ? 'not_planned' : 'completed',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
closed += 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
core.info(`Closed ${closed} issue(s).`);
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/create-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Create agent issues
|
|
3
|
-
description: Create every create_issue item from agent_output.json.
|
|
4
|
-
inputs:
|
|
5
|
-
output-file:
|
|
6
|
-
description: Path to agent_output.json.
|
|
7
|
-
required: true
|
|
8
|
-
token:
|
|
9
|
-
description: GitHub token with issues:write.
|
|
10
|
-
required: true
|
|
11
|
-
outputs:
|
|
12
|
-
created-count:
|
|
13
|
-
description: Number of issues created.
|
|
14
|
-
value: ${{ steps.create.outputs.created-count }}
|
|
15
|
-
first-issue-number:
|
|
16
|
-
description: Issue number of the first issue created, or empty.
|
|
17
|
-
value: ${{ steps.create.outputs.first-issue-number }}
|
|
18
|
-
runs:
|
|
19
|
-
using: composite
|
|
20
|
-
steps:
|
|
21
|
-
- name: Create issues
|
|
22
|
-
id: create
|
|
23
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
24
|
-
env:
|
|
25
|
-
AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
|
|
26
|
-
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
27
|
-
with:
|
|
28
|
-
github-token: ${{ inputs.token }}
|
|
29
|
-
script: |
|
|
30
|
-
const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
|
|
31
|
-
const items = readAgentItems(process.env.OUTPUT_FILE, 'create_issue');
|
|
32
|
-
|
|
33
|
-
const created = [];
|
|
34
|
-
|
|
35
|
-
for (const item of items) {
|
|
36
|
-
if (!item.title) {
|
|
37
|
-
core.warning(`create_issue item has no title, skipping: ${JSON.stringify(item)}`);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const { data } = await github.rest.issues.create({
|
|
42
|
-
...context.repo,
|
|
43
|
-
title: item.title,
|
|
44
|
-
body: item.body ?? '',
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
created.push(data.number);
|
|
48
|
-
core.info(`Created #${data.number}: ${item.title}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
core.setOutput('created-count', String(created.length));
|
|
52
|
-
core.setOutput('first-issue-number', created.length > 0 ? String(created[0]) : '');
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/create-agent-issues/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Create agent issues
|
|
3
|
+
description: Create every create_issue item from agent_output.json.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
token:
|
|
9
|
+
description: GitHub token with issues:write.
|
|
10
|
+
required: true
|
|
11
|
+
outputs:
|
|
12
|
+
created-count:
|
|
13
|
+
description: Number of issues created.
|
|
14
|
+
value: ${{ steps.create.outputs.created-count }}
|
|
15
|
+
first-issue-number:
|
|
16
|
+
description: Issue number of the first issue created, or empty.
|
|
17
|
+
value: ${{ steps.create.outputs.first-issue-number }}
|
|
18
|
+
runs:
|
|
19
|
+
using: composite
|
|
20
|
+
steps:
|
|
21
|
+
- name: Create issues
|
|
22
|
+
id: create
|
|
23
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
24
|
+
env:
|
|
25
|
+
AGENT_OUTPUT_LIB: ${{ github.action_path }}/../agent-output.cjs
|
|
26
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
27
|
+
with:
|
|
28
|
+
github-token: ${{ inputs.token }}
|
|
29
|
+
script: |
|
|
30
|
+
const { readAgentItems } = require(process.env.AGENT_OUTPUT_LIB);
|
|
31
|
+
const items = readAgentItems(process.env.OUTPUT_FILE, 'create_issue');
|
|
32
|
+
|
|
33
|
+
const created = [];
|
|
34
|
+
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
if (!item.title) {
|
|
37
|
+
core.warning(`create_issue item has no title, skipping: ${JSON.stringify(item)}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { data } = await github.rest.issues.create({
|
|
42
|
+
...context.repo,
|
|
43
|
+
title: item.title,
|
|
44
|
+
body: item.body ?? '',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
created.push(data.number);
|
|
48
|
+
core.info(`Created #${data.number}: ${item.title}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
core.setOutput('created-count', String(created.length));
|
|
52
|
+
core.setOutput('first-issue-number', created.length > 0 ? String(created[0]) : '');
|