@plainconceptsplatform/workflows 0.19.2 → 0.20.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/catalog-installation.js +12 -1
- package/dist/index.js +0 -0
- package/dist/stack-defaults.js +16 -16
- package/dist/worker-env.js +14 -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 +148 -0
- package/loops/actions/assess-blast-radius/assess-blast-radius.sh +138 -0
- package/loops/actions/classify-route/action.yml +100 -100
- 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/housekeeping/action.yml +55 -2
- 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 +62 -40
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +147 -31
- package/loops/actions/validate-refine-output/action.yml +48 -44
- package/loops/actions/validate-refine-output/validate-refine-output.sh +15 -4
- 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-refine-output/verify-refine-output.sh +6 -1
- package/loops/actions/verify-route-matrix/action.yml +9 -9
- package/loops/actions/verify-route-matrix/verify-gate-metrics.mjs +51 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +329 -29
- package/loops/scripts/compile-agent-workflows.mjs +331 -331
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +330 -330
- package/loops/templates/ci/app-ci-node-monorepo.yml +260 -260
- package/loops/templates/issues/bug_report.yml +109 -109
- package/loops/templates/issues/feature_request.yml +75 -75
- package/loops/templates/opencode/opencode.ci.json +55 -49
- package/loops/templates/opencode/opencode.ci.json.md +59 -49
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-merge-gate.md +367 -148
- package/loops/workflows/agent-refine.md +60 -16
- package/loops/workflows/authorize-bot-work.yml +105 -105
- package/loops/workflows/shared/opencode-ci.md +206 -206
- package/loops/workflows/shared/platform-defaults.md +19 -19
- package/package.json +12 -11
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/apply-agent-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
-
name: Apply agent output
|
|
3
|
-
description: Download the agent artifact and apply its safe outputs in one fixed order, so every worker concludes the same way.
|
|
4
|
-
inputs:
|
|
5
|
-
token:
|
|
6
|
-
description: GitHub token the writes are made with.
|
|
7
|
-
required: true
|
|
8
|
-
artifact-name:
|
|
9
|
-
description: >-
|
|
10
|
-
Name of the agent artifact. Callers pass the activation job's artifact_prefix output
|
|
11
|
-
followed by "agent"; gh-aw derives that prefix from the workflow's inputs, so it is
|
|
12
|
-
non-empty for every workflow_call worker and the bare name never resolves.
|
|
13
|
-
required: true
|
|
14
|
-
merge-pull-request:
|
|
15
|
-
description: Apply merge_pull_request items.
|
|
16
|
-
required: false
|
|
17
|
-
default: 'false'
|
|
18
|
-
push-to-branch:
|
|
19
|
-
description: Apply push_to_pull_request_branch items.
|
|
20
|
-
required: false
|
|
21
|
-
default: 'false'
|
|
22
|
-
close-issues:
|
|
23
|
-
description: Apply close_issue items.
|
|
24
|
-
required: false
|
|
25
|
-
default: 'false'
|
|
26
|
-
create-issues:
|
|
27
|
-
description: Apply create_issue items.
|
|
28
|
-
required: false
|
|
29
|
-
default: 'false'
|
|
30
|
-
update-issues:
|
|
31
|
-
description: Apply update_issue items.
|
|
32
|
-
required: false
|
|
33
|
-
default: 'false'
|
|
34
|
-
apply-labels:
|
|
35
|
-
description: Apply add_labels and remove_labels items.
|
|
36
|
-
required: false
|
|
37
|
-
default: 'true'
|
|
38
|
-
fallback-issue-number:
|
|
39
|
-
description: Issue an update_issue item targets when it names none.
|
|
40
|
-
required: false
|
|
41
|
-
default: ''
|
|
42
|
-
outputs:
|
|
43
|
-
item-count:
|
|
44
|
-
description: Number of items the agent produced.
|
|
45
|
-
value: ${{ steps.output.outputs.item-count }}
|
|
46
|
-
first-issue-number:
|
|
47
|
-
description: Issue number of the first issue created, or empty.
|
|
48
|
-
value: ${{ steps.create-issues.outputs.first-issue-number }}
|
|
49
|
-
runs:
|
|
50
|
-
using: composite
|
|
51
|
-
steps:
|
|
52
|
-
- name: Download agent output
|
|
53
|
-
id: output
|
|
54
|
-
uses: ./.github/actions/download-agent-output
|
|
55
|
-
with:
|
|
56
|
-
artifact-name: ${{ inputs.artifact-name }}
|
|
57
|
-
|
|
58
|
-
- name: Merge pull request
|
|
59
|
-
if: inputs.merge-pull-request == 'true' && steps.output.outputs.item-count != '0'
|
|
60
|
-
uses: ./.github/actions/merge-agent-pr
|
|
61
|
-
with:
|
|
62
|
-
output-file: ${{ steps.output.outputs.output-file }}
|
|
63
|
-
token: ${{ inputs.token }}
|
|
64
|
-
|
|
65
|
-
- name: Push to pull request branch
|
|
66
|
-
if: inputs.push-to-branch == 'true' && steps.output.outputs.item-count != '0'
|
|
67
|
-
uses: ./.github/actions/push-agent-branch
|
|
68
|
-
with:
|
|
69
|
-
output-file: ${{ steps.output.outputs.output-file }}
|
|
70
|
-
bundle-file: ${{ steps.output.outputs.bundle-file }}
|
|
71
|
-
token: ${{ inputs.token }}
|
|
72
|
-
|
|
73
|
-
- name: Close issues
|
|
74
|
-
if: inputs.close-issues == 'true' && steps.output.outputs.item-count != '0'
|
|
75
|
-
uses: ./.github/actions/close-agent-issues
|
|
76
|
-
with:
|
|
77
|
-
output-file: ${{ steps.output.outputs.output-file }}
|
|
78
|
-
token: ${{ inputs.token }}
|
|
79
|
-
|
|
80
|
-
- name: Update issues
|
|
81
|
-
if: inputs.update-issues == 'true' && steps.output.outputs.item-count != '0'
|
|
82
|
-
uses: ./.github/actions/update-agent-issues
|
|
83
|
-
with:
|
|
84
|
-
output-file: ${{ steps.output.outputs.output-file }}
|
|
85
|
-
token: ${{ inputs.token }}
|
|
86
|
-
fallback-issue-number: ${{ inputs.fallback-issue-number }}
|
|
87
|
-
|
|
88
|
-
- name: Create issues
|
|
89
|
-
id: create-issues
|
|
90
|
-
if: inputs.create-issues == 'true' && steps.output.outputs.item-count != '0'
|
|
91
|
-
uses: ./.github/actions/create-agent-issues
|
|
92
|
-
with:
|
|
93
|
-
output-file: ${{ steps.output.outputs.output-file }}
|
|
94
|
-
token: ${{ inputs.token }}
|
|
95
|
-
|
|
96
|
-
- name: Post agent comments
|
|
97
|
-
if: steps.output.outputs.item-count != '0'
|
|
98
|
-
uses: ./.github/actions/apply-agent-comments
|
|
99
|
-
with:
|
|
100
|
-
output-file: ${{ steps.output.outputs.output-file }}
|
|
101
|
-
token: ${{ inputs.token }}
|
|
102
|
-
|
|
103
|
-
- name: Apply agent label changes
|
|
104
|
-
if: inputs.apply-labels == 'true' && steps.output.outputs.item-count != '0'
|
|
105
|
-
uses: ./.github/actions/apply-agent-labels
|
|
106
|
-
with:
|
|
107
|
-
output-file: ${{ steps.output.outputs.output-file }}
|
|
108
|
-
token: ${{ inputs.token }}
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/apply-agent-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Apply agent output
|
|
3
|
+
description: Download the agent artifact and apply its safe outputs in one fixed order, so every worker concludes the same way.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token the writes are made with.
|
|
7
|
+
required: true
|
|
8
|
+
artifact-name:
|
|
9
|
+
description: >-
|
|
10
|
+
Name of the agent artifact. Callers pass the activation job's artifact_prefix output
|
|
11
|
+
followed by "agent"; gh-aw derives that prefix from the workflow's inputs, so it is
|
|
12
|
+
non-empty for every workflow_call worker and the bare name never resolves.
|
|
13
|
+
required: true
|
|
14
|
+
merge-pull-request:
|
|
15
|
+
description: Apply merge_pull_request items.
|
|
16
|
+
required: false
|
|
17
|
+
default: 'false'
|
|
18
|
+
push-to-branch:
|
|
19
|
+
description: Apply push_to_pull_request_branch items.
|
|
20
|
+
required: false
|
|
21
|
+
default: 'false'
|
|
22
|
+
close-issues:
|
|
23
|
+
description: Apply close_issue items.
|
|
24
|
+
required: false
|
|
25
|
+
default: 'false'
|
|
26
|
+
create-issues:
|
|
27
|
+
description: Apply create_issue items.
|
|
28
|
+
required: false
|
|
29
|
+
default: 'false'
|
|
30
|
+
update-issues:
|
|
31
|
+
description: Apply update_issue items.
|
|
32
|
+
required: false
|
|
33
|
+
default: 'false'
|
|
34
|
+
apply-labels:
|
|
35
|
+
description: Apply add_labels and remove_labels items.
|
|
36
|
+
required: false
|
|
37
|
+
default: 'true'
|
|
38
|
+
fallback-issue-number:
|
|
39
|
+
description: Issue an update_issue item targets when it names none.
|
|
40
|
+
required: false
|
|
41
|
+
default: ''
|
|
42
|
+
outputs:
|
|
43
|
+
item-count:
|
|
44
|
+
description: Number of items the agent produced.
|
|
45
|
+
value: ${{ steps.output.outputs.item-count }}
|
|
46
|
+
first-issue-number:
|
|
47
|
+
description: Issue number of the first issue created, or empty.
|
|
48
|
+
value: ${{ steps.create-issues.outputs.first-issue-number }}
|
|
49
|
+
runs:
|
|
50
|
+
using: composite
|
|
51
|
+
steps:
|
|
52
|
+
- name: Download agent output
|
|
53
|
+
id: output
|
|
54
|
+
uses: ./.github/actions/download-agent-output
|
|
55
|
+
with:
|
|
56
|
+
artifact-name: ${{ inputs.artifact-name }}
|
|
57
|
+
|
|
58
|
+
- name: Merge pull request
|
|
59
|
+
if: inputs.merge-pull-request == 'true' && steps.output.outputs.item-count != '0'
|
|
60
|
+
uses: ./.github/actions/merge-agent-pr
|
|
61
|
+
with:
|
|
62
|
+
output-file: ${{ steps.output.outputs.output-file }}
|
|
63
|
+
token: ${{ inputs.token }}
|
|
64
|
+
|
|
65
|
+
- name: Push to pull request branch
|
|
66
|
+
if: inputs.push-to-branch == 'true' && steps.output.outputs.item-count != '0'
|
|
67
|
+
uses: ./.github/actions/push-agent-branch
|
|
68
|
+
with:
|
|
69
|
+
output-file: ${{ steps.output.outputs.output-file }}
|
|
70
|
+
bundle-file: ${{ steps.output.outputs.bundle-file }}
|
|
71
|
+
token: ${{ inputs.token }}
|
|
72
|
+
|
|
73
|
+
- name: Close issues
|
|
74
|
+
if: inputs.close-issues == 'true' && steps.output.outputs.item-count != '0'
|
|
75
|
+
uses: ./.github/actions/close-agent-issues
|
|
76
|
+
with:
|
|
77
|
+
output-file: ${{ steps.output.outputs.output-file }}
|
|
78
|
+
token: ${{ inputs.token }}
|
|
79
|
+
|
|
80
|
+
- name: Update issues
|
|
81
|
+
if: inputs.update-issues == 'true' && steps.output.outputs.item-count != '0'
|
|
82
|
+
uses: ./.github/actions/update-agent-issues
|
|
83
|
+
with:
|
|
84
|
+
output-file: ${{ steps.output.outputs.output-file }}
|
|
85
|
+
token: ${{ inputs.token }}
|
|
86
|
+
fallback-issue-number: ${{ inputs.fallback-issue-number }}
|
|
87
|
+
|
|
88
|
+
- name: Create issues
|
|
89
|
+
id: create-issues
|
|
90
|
+
if: inputs.create-issues == 'true' && steps.output.outputs.item-count != '0'
|
|
91
|
+
uses: ./.github/actions/create-agent-issues
|
|
92
|
+
with:
|
|
93
|
+
output-file: ${{ steps.output.outputs.output-file }}
|
|
94
|
+
token: ${{ inputs.token }}
|
|
95
|
+
|
|
96
|
+
- name: Post agent comments
|
|
97
|
+
if: steps.output.outputs.item-count != '0'
|
|
98
|
+
uses: ./.github/actions/apply-agent-comments
|
|
99
|
+
with:
|
|
100
|
+
output-file: ${{ steps.output.outputs.output-file }}
|
|
101
|
+
token: ${{ inputs.token }}
|
|
102
|
+
|
|
103
|
+
- name: Apply agent label changes
|
|
104
|
+
if: inputs.apply-labels == 'true' && steps.output.outputs.item-count != '0'
|
|
105
|
+
uses: ./.github/actions/apply-agent-labels
|
|
106
|
+
with:
|
|
107
|
+
output-file: ${{ steps.output.outputs.output-file }}
|
|
108
|
+
token: ${{ inputs.token }}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/assess-blast-radius/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Assess blast radius
|
|
3
|
+
description: Classify a pull request's potential impact from its changed paths and diff shape, before any model reads it.
|
|
4
|
+
inputs:
|
|
5
|
+
token:
|
|
6
|
+
description: GitHub token with pull-requests read access.
|
|
7
|
+
required: true
|
|
8
|
+
pr-number:
|
|
9
|
+
description: Pull request number to assess.
|
|
10
|
+
required: true
|
|
11
|
+
protected-paths:
|
|
12
|
+
description: Extended regular expression. A match may never auto-merge and forces HIGH.
|
|
13
|
+
required: true
|
|
14
|
+
owner-paths:
|
|
15
|
+
description: Extended regular expression. A match forces HIGH and requires an owner.
|
|
16
|
+
required: false
|
|
17
|
+
default: ''
|
|
18
|
+
sensitive-paths:
|
|
19
|
+
description: Extended regular expression. A match raises the floor to MEDIUM.
|
|
20
|
+
required: false
|
|
21
|
+
default: ''
|
|
22
|
+
high-files:
|
|
23
|
+
description: Files changed at or above which the radius is HIGH.
|
|
24
|
+
required: false
|
|
25
|
+
default: '20'
|
|
26
|
+
high-lines:
|
|
27
|
+
description: Lines changed at or above which the radius is HIGH.
|
|
28
|
+
required: false
|
|
29
|
+
default: '800'
|
|
30
|
+
medium-files:
|
|
31
|
+
description: Files changed at or above which the radius is MEDIUM.
|
|
32
|
+
required: false
|
|
33
|
+
default: '5'
|
|
34
|
+
medium-lines:
|
|
35
|
+
description: Lines changed at or above which the radius is MEDIUM.
|
|
36
|
+
required: false
|
|
37
|
+
default: '200'
|
|
38
|
+
outputs:
|
|
39
|
+
level:
|
|
40
|
+
description: "low, medium or high."
|
|
41
|
+
value: ${{ steps.assess.outputs.level }}
|
|
42
|
+
signals:
|
|
43
|
+
description: One line per signal that fired, in the order the rules ran.
|
|
44
|
+
value: ${{ steps.assess.outputs.signals }}
|
|
45
|
+
requires_review:
|
|
46
|
+
description: Whether a protected path was touched. Kept for the jobs that already read it.
|
|
47
|
+
value: ${{ steps.assess.outputs.requires_review }}
|
|
48
|
+
files:
|
|
49
|
+
description: The protected paths that matched, one per line. Empty when none did.
|
|
50
|
+
value: ${{ steps.assess.outputs.files }}
|
|
51
|
+
owner_hits:
|
|
52
|
+
description: The owner paths that matched, one per line.
|
|
53
|
+
value: ${{ steps.assess.outputs.owner_hits }}
|
|
54
|
+
owner_hit:
|
|
55
|
+
description: Whether an owner path was touched. A scalar, so no caller has to compare a multi-line value to an empty string.
|
|
56
|
+
value: ${{ steps.assess.outputs.owner_hit }}
|
|
57
|
+
sensitive_hit:
|
|
58
|
+
description: Whether a sensitive path was touched.
|
|
59
|
+
value: ${{ steps.assess.outputs.sensitive_hit }}
|
|
60
|
+
sensitive_hits:
|
|
61
|
+
description: The sensitive paths that matched, one per line.
|
|
62
|
+
value: ${{ steps.assess.outputs.sensitive_hits }}
|
|
63
|
+
required_owners:
|
|
64
|
+
description: CODEOWNERS entries covering the changed paths, space separated. Empty when the repository has no CODEOWNERS.
|
|
65
|
+
value: ${{ steps.assess.outputs.required_owners }}
|
|
66
|
+
files_changed:
|
|
67
|
+
description: Number of files in the diff.
|
|
68
|
+
value: ${{ steps.assess.outputs.files_changed }}
|
|
69
|
+
lines_changed:
|
|
70
|
+
description: Additions plus deletions.
|
|
71
|
+
value: ${{ steps.assess.outputs.lines_changed }}
|
|
72
|
+
runs:
|
|
73
|
+
using: composite
|
|
74
|
+
steps:
|
|
75
|
+
- name: Assess blast radius
|
|
76
|
+
id: assess
|
|
77
|
+
shell: bash
|
|
78
|
+
env:
|
|
79
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
80
|
+
REPO: ${{ github.repository }}
|
|
81
|
+
PR_NUMBER: ${{ inputs.pr-number }}
|
|
82
|
+
PROTECTED_PATHS: ${{ inputs.protected-paths }}
|
|
83
|
+
OWNER_PATHS: ${{ inputs.owner-paths }}
|
|
84
|
+
SENSITIVE_PATHS: ${{ inputs.sensitive-paths }}
|
|
85
|
+
HIGH_FILES: ${{ inputs.high-files }}
|
|
86
|
+
HIGH_LINES: ${{ inputs.high-lines }}
|
|
87
|
+
MEDIUM_FILES: ${{ inputs.medium-files }}
|
|
88
|
+
MEDIUM_LINES: ${{ inputs.medium-lines }}
|
|
89
|
+
run: |
|
|
90
|
+
set -euo pipefail
|
|
91
|
+
# One API call. The job this replaced already paid for it to grep PROTECTED_PATHS, and
|
|
92
|
+
# the additions and deletions come back in the same payload, so the diff shape costs
|
|
93
|
+
# nothing extra.
|
|
94
|
+
gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files?per_page=100" \
|
|
95
|
+
--jq '[.[] | {filename, additions, deletions}]' > /tmp/blast-files.json
|
|
96
|
+
|
|
97
|
+
files=$(jq -r '.[].filename' /tmp/blast-files.json)
|
|
98
|
+
files_changed=$(jq -r 'length' /tmp/blast-files.json)
|
|
99
|
+
lines_changed=$(jq -r '[.[] | .additions + .deletions] | add // 0' /tmp/blast-files.json)
|
|
100
|
+
|
|
101
|
+
bash "${{ github.action_path }}/assess-blast-radius.sh" \
|
|
102
|
+
"$files_changed" "$lines_changed" <<<"$files" > /tmp/blast-out.env
|
|
103
|
+
|
|
104
|
+
cat /tmp/blast-out.env >> "$GITHUB_OUTPUT"
|
|
105
|
+
|
|
106
|
+
# CODEOWNERS refines who is asked; it never decides whether anyone is. A repository
|
|
107
|
+
# without the file still gets an owner disposition from OWNER_PATHS, and the comment
|
|
108
|
+
# names the paths instead of a team.
|
|
109
|
+
#
|
|
110
|
+
# The pattern is a gitignore-style glob, not a regex. Stripping a leading slash and a
|
|
111
|
+
# trailing `/**` and feeding the rest to grep -E was wrong in both directions: `apps/web/`
|
|
112
|
+
# matched `apps/webhooks/x.ts` because nothing anchored the segment end, `docs/*` matched
|
|
113
|
+
# `docsecret.md` because `*` applied to the slash, and `*.ts` became `^*.ts`, which GNU
|
|
114
|
+
# grep parses as a literal with the anchor dropped, so it matched `src/payments/Fee.cs`.
|
|
115
|
+
# The result was over-broad `required_owners`: teams asked to review changes they do not
|
|
116
|
+
# own. Translate the glob properly instead.
|
|
117
|
+
owners=""
|
|
118
|
+
glob_to_regex() {
|
|
119
|
+
printf '%s' "$1" | sed -e 's/[.[\$()|+^{}]/\\&/g' \
|
|
120
|
+
-e 's#\*\*/#\x01#g' -e 's#/\*\*#\x02#g' \
|
|
121
|
+
-e 's#\*#[^/]*#g' -e 's#?#[^/]#g' \
|
|
122
|
+
-e 's#\x01#(.*/)?#g' -e 's#\x02#(/.*)?#g'
|
|
123
|
+
}
|
|
124
|
+
for candidate in CODEOWNERS .github/CODEOWNERS docs/CODEOWNERS; do
|
|
125
|
+
[ -f "$candidate" ] || continue
|
|
126
|
+
# `|| [ -n "$pattern" ]` so a last line with no trailing newline is still read: plain
|
|
127
|
+
# `while read` returns 1 on EOF and drops it, which silently loses the final rule.
|
|
128
|
+
while read -r pattern rest || [ -n "$pattern" ]; do
|
|
129
|
+
case "$pattern" in ''|'#'*) continue ;; esac
|
|
130
|
+
[ -n "$rest" ] || continue
|
|
131
|
+
anchored=0
|
|
132
|
+
case "$pattern" in /*) anchored=1 ;; esac
|
|
133
|
+
glob="${pattern#/}"; glob="${glob%/}"
|
|
134
|
+
regex="$(glob_to_regex "$glob")"
|
|
135
|
+
# A directory rule owns everything beneath it; a file rule matches the path exactly.
|
|
136
|
+
case "$pattern" in
|
|
137
|
+
*/|*/\*\*) regex="${regex}(/.*)?$" ;;
|
|
138
|
+
*) regex="${regex}(/.*)?$" ;;
|
|
139
|
+
esac
|
|
140
|
+
# An unanchored CODEOWNERS pattern matches at any depth, an anchored one only at the root.
|
|
141
|
+
if [ "$anchored" -eq 1 ]; then regex="^${regex}"; else regex="(^|/)${regex}"; fi
|
|
142
|
+
if printf '%s\n' "$files" | grep -qE "$regex"; then
|
|
143
|
+
owners="$owners $rest"
|
|
144
|
+
fi
|
|
145
|
+
done < "$candidate"
|
|
146
|
+
break
|
|
147
|
+
done
|
|
148
|
+
echo "required_owners=$(echo "$owners" | tr -s ' ' | sed 's/^ //;s/ $//')" >> "$GITHUB_OUTPUT"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
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
|
+
# Classify a pull request's blast radius from its changed paths and diff shape.
|
|
4
|
+
#
|
|
5
|
+
# Reads the changed paths on stdin, one per line. Prints GITHUB_OUTPUT lines on stdout.
|
|
6
|
+
# Deliberately model-free: the point of this file is that the most consequential input to the
|
|
7
|
+
# merge decision is computed from facts a shell can check, before any agent reads the diff.
|
|
8
|
+
#
|
|
9
|
+
# Usage: assess-blast-radius.sh <files_changed> <lines_changed> < paths
|
|
10
|
+
# Regexes and thresholds arrive in the environment so the caller and the tests share one contract.
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
files_changed="${1:-0}"
|
|
15
|
+
lines_changed="${2:-0}"
|
|
16
|
+
|
|
17
|
+
PROTECTED_PATHS="${PROTECTED_PATHS:-}"
|
|
18
|
+
OWNER_PATHS="${OWNER_PATHS:-}"
|
|
19
|
+
SENSITIVE_PATHS="${SENSITIVE_PATHS:-}"
|
|
20
|
+
HIGH_FILES="${HIGH_FILES:-20}"
|
|
21
|
+
HIGH_LINES="${HIGH_LINES:-800}"
|
|
22
|
+
MEDIUM_FILES="${MEDIUM_FILES:-5}"
|
|
23
|
+
MEDIUM_LINES="${MEDIUM_LINES:-200}"
|
|
24
|
+
|
|
25
|
+
files="$(cat)"
|
|
26
|
+
|
|
27
|
+
# An empty regex matches every line in grep -E, which would mark every pull request protected.
|
|
28
|
+
# A list nobody configured must match nothing, not everything.
|
|
29
|
+
#
|
|
30
|
+
# The `|| true` that used to end this function swallowed grep exit 2 (a regex that does not
|
|
31
|
+
# compile) exactly as it swallowed exit 1 (no match), so a consumer typo in PROTECTED_PATHS
|
|
32
|
+
# reported "no protected paths touched" and merged. Consumers are told to edit all three of
|
|
33
|
+
# these, so the typo is the likely case. Each pattern is compiled once below, in this shell,
|
|
34
|
+
# before any of them is used: a `match` failure cannot report itself, because `match` runs
|
|
35
|
+
# inside a command substitution and anything it sets dies with the subshell.
|
|
36
|
+
match() {
|
|
37
|
+
local pattern="$1"
|
|
38
|
+
[ -n "$pattern" ] || return 0
|
|
39
|
+
printf '%s\n' "$files" | grep -E "$pattern" || true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Compile each configured pattern against nothing. grep exits 2 when the regex is bad, which is
|
|
43
|
+
# distinct from 1 for no match, and that is the whole signal.
|
|
44
|
+
for name in PROTECTED_PATHS OWNER_PATHS SENSITIVE_PATHS; do
|
|
45
|
+
pattern="${!name}"
|
|
46
|
+
[ -n "$pattern" ] || continue
|
|
47
|
+
if printf '' | grep -E "$pattern" >/dev/null 2>&1; then :; elif [ "$?" -gt 1 ]; then
|
|
48
|
+
echo "assess-blast-radius: ${name} is not a valid extended regular expression: ${pattern}" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
fi
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
protected_hits="$(match "$PROTECTED_PATHS")"
|
|
54
|
+
owner_hits="$(match "$OWNER_PATHS")"
|
|
55
|
+
sensitive_hits="$(match "$SENSITIVE_PATHS")"
|
|
56
|
+
|
|
57
|
+
# A threshold that is not a number turns its comparison into a shell error that `set -e` does
|
|
58
|
+
# not catch, because the test is an `if` condition: `HIGH_FILES=twenty` printed a diagnostic to
|
|
59
|
+
# 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
|
|
61
|
+
case "${!threshold}" in
|
|
62
|
+
""|*[!0-9]*)
|
|
63
|
+
echo "assess-blast-radius: ${threshold} must be a whole number, got '${!threshold}'" >&2
|
|
64
|
+
exit 2
|
|
65
|
+
;;
|
|
66
|
+
esac
|
|
67
|
+
done
|
|
68
|
+
|
|
69
|
+
signals=""
|
|
70
|
+
add_signal() { signals="${signals}${signals:+$'\n'}$1"; }
|
|
71
|
+
|
|
72
|
+
level=low
|
|
73
|
+
|
|
74
|
+
if [ -n "$protected_hits" ]; then
|
|
75
|
+
level=high
|
|
76
|
+
add_signal "protected paths touched ($(printf '%s\n' "$protected_hits" | tr '\n' ' ' | sed 's/ $//'))"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
if [ -n "$owner_hits" ]; then
|
|
80
|
+
level=high
|
|
81
|
+
add_signal "owner paths touched ($(printf '%s\n' "$owner_hits" | tr '\n' ' ' | sed 's/ $//'))"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [ "$files_changed" -ge "$HIGH_FILES" ]; then
|
|
85
|
+
level=high
|
|
86
|
+
add_signal "$files_changed files changed, at or above the high threshold of $HIGH_FILES"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [ "$lines_changed" -ge "$HIGH_LINES" ]; then
|
|
90
|
+
level=high
|
|
91
|
+
add_signal "$lines_changed lines changed, at or above the high threshold of $HIGH_LINES"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [ "$level" != high ]; then
|
|
95
|
+
if [ -n "$sensitive_hits" ]; then
|
|
96
|
+
level=medium
|
|
97
|
+
add_signal "sensitive paths touched ($(printf '%s\n' "$sensitive_hits" | tr '\n' ' ' | sed 's/ $//'))"
|
|
98
|
+
fi
|
|
99
|
+
if [ "$files_changed" -ge "$MEDIUM_FILES" ]; then
|
|
100
|
+
level=medium
|
|
101
|
+
add_signal "$files_changed files changed, at or above the medium threshold of $MEDIUM_FILES"
|
|
102
|
+
fi
|
|
103
|
+
if [ "$lines_changed" -ge "$MEDIUM_LINES" ]; then
|
|
104
|
+
level=medium
|
|
105
|
+
add_signal "$lines_changed lines changed, at or above the medium threshold of $MEDIUM_LINES"
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
[ -n "$signals" ] || signals="no blast-radius signal fired: $files_changed files, $lines_changed lines, no configured path matched"
|
|
110
|
+
|
|
111
|
+
# A fixed heredoc delimiter is a fail-open here. Every multi-line value below is built from
|
|
112
|
+
# paths the pull request chose, so a path that is exactly the delimiter closes its block early
|
|
113
|
+
# and every line after it is read as a new output -- including `level=low`, which is emitted
|
|
114
|
+
# above and would be overridden by the later value. The shipped regexes cannot match a bare
|
|
115
|
+
# delimiter, but a consumer who writes a loose one can, and the failure is silent and merges.
|
|
116
|
+
# A per-run delimiter removes the class rather than the instance, which is what GitHub
|
|
117
|
+
# documents for exactly this reason.
|
|
118
|
+
DELIM="BLASTEOF_$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
|
119
|
+
|
|
120
|
+
emit_block() {
|
|
121
|
+
local name="$1" value="$2"
|
|
122
|
+
printf '%s<<%s\n%s\n%s\n' "$name" "$DELIM" "$value" "$DELIM"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
echo "level=$level"
|
|
126
|
+
echo "files_changed=$files_changed"
|
|
127
|
+
echo "lines_changed=$lines_changed"
|
|
128
|
+
# Emitted as plain scalars, not left for the caller to derive by comparing a heredoc block to
|
|
129
|
+
# an empty string. Whether the runner renders an empty block as "" or as a newline is not
|
|
130
|
+
# something this repository can test off-runner, and a caller that guessed wrong would have sent
|
|
131
|
+
# every pull request to owner review. A shell that already knows the answer should just say it.
|
|
132
|
+
echo "requires_review=$([ -n "$protected_hits" ] && echo true || echo false)"
|
|
133
|
+
echo "owner_hit=$([ -n "$owner_hits" ] && echo true || echo false)"
|
|
134
|
+
echo "sensitive_hit=$([ -n "$sensitive_hits" ] && echo true || echo false)"
|
|
135
|
+
emit_block signals "$signals"
|
|
136
|
+
emit_block files "$protected_hits"
|
|
137
|
+
emit_block owner_hits "$owner_hits"
|
|
138
|
+
emit_block sensitive_hits "$sensitive_hits"
|