@plainconceptsplatform/workflows 1.8.3 → 1.9.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/README.md +1 -1
- package/dist/catalog-installation.js +2 -2
- package/dist/route-processing.js +4 -1
- package/dist/worker-env.d.ts +1 -1
- package/dist/worker-env.js +6 -2
- package/dist/workflow-catalog.d.ts +3 -1
- package/dist/workflow-catalog.js +6 -0
- package/loops/actions/audit-close/action.yml +13 -6
- package/loops/actions/classify-route/classify-route.sh +3 -1
- package/loops/actions/collect-doc-drift/action.yml +29 -0
- package/loops/actions/collect-doc-drift/collect-doc-drift.mjs +129 -0
- package/loops/actions/land-pull-request/action.yml +10 -0
- package/loops/actions/land-pull-request/land-pull-request.sh +14 -0
- package/loops/actions/report-workflow-errors/action.yml +1 -1
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +49 -5
- package/loops/docs/diagrams.md +45 -2
- package/loops/templates/agentics/agentics-app-errors.yml +2 -1
- package/loops/templates/agentics/agentics-error-report.yml +2 -1
- package/loops/workflows/agent-docs-audit.md +266 -0
- package/loops/workflows/authorize-bot-work.yml +107 -105
- package/loops/workflows/release-tag.yml +2 -0
- package/loops/workflows/work-router.yml +49 -2
- package/package.json +1 -1
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-docs-audit.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
+
env:
|
|
4
|
+
# The model this worker's own session runs on. See agent-audit.md for why it names one.
|
|
5
|
+
AGENT_LEAD_MODEL: "forge/glm-5-2"
|
|
6
|
+
# Every harness tier on one model the CI's Forge account is granted; see agent-audit.md.
|
|
7
|
+
AGENT_TIER_MODEL: "plainconcepts/glm-5-2"
|
|
8
|
+
AGENT_FAST_MODEL: "plainconcepts/glm-5-2"
|
|
9
|
+
REPO_RULES: "Read-only documentation audit. Report only stale documentation you can prove with evidence from both the document and the code. Do not modify files, commit, push, or run write operations."
|
|
10
|
+
# What this repository counts as its entry documents, and what the scan leaves out. Archives,
|
|
11
|
+
# parked material and generated documents belong in the exclusions: a broken link in a record
|
|
12
|
+
# of the past is not drift.
|
|
13
|
+
DOCS_AUDIT_ENTRY_DOCS: "README.md,ARCHITECTURE.md,AGENTS.md,CLAUDE.md,CONTRIBUTING.md,docs/README.md"
|
|
14
|
+
DOCS_AUDIT_EXCLUDE: ".github/workflows/,.agents/,.claude/,.opencode/,node_modules/"
|
|
15
|
+
DOCS_AUDIT_FOCUS: "Components, modules or bounded contexts the documents list against the ones that exist. Counts that no longer hold. Infrastructure, hosting or tooling that was retired but is still described. Workflows, scripts and commands that no longer exist. Decisions superseded in practice that still read as current. Two documents stating the same fact differently."
|
|
16
|
+
# The Platform documentation standard, read before the agent starts. A repository that keeps
|
|
17
|
+
# its own may point this elsewhere. Empty skips the structure check.
|
|
18
|
+
DOCS_STANDARD_URL: "https://raw.githubusercontent.com/PlainConceptsPlatform/foundations/main/ai/DOCUMENTATION.md"
|
|
19
|
+
# No commit in this many days means nothing can have drifted since the last run, so a scheduled
|
|
20
|
+
# run stops before the model starts. 17 is the longest gap between runs on days 1 and 15.
|
|
21
|
+
DOCS_AUDIT_QUIET_DAYS: "17"
|
|
22
|
+
GIT_AUTHOR_NAME: "github-actions[bot]"
|
|
23
|
+
GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
24
|
+
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
25
|
+
GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
26
|
+
description: |
|
|
27
|
+
Read-only documentation audit. Checks that the repository's documentation still matches its
|
|
28
|
+
code and the Platform documentation standard. When it does not, files a single issue of
|
|
29
|
+
findings with evidence from both sides, labelled `docs-audit` + `refine`, so Refine sizes it
|
|
30
|
+
and splits it into one issue per finding. When the documentation is current, files nothing.
|
|
31
|
+
|
|
32
|
+
Router-only worker: triggered exclusively via workflow_call from work-router.yml.
|
|
33
|
+
Contract input: trigger-kind(scheduled|manual).
|
|
34
|
+
The router owns the fortnightly schedule and manual dispatch.
|
|
35
|
+
|
|
36
|
+
The `skip-if-match` below is backpressure: one unactioned report is enough, because a second
|
|
37
|
+
one would mostly restate the first.
|
|
38
|
+
|
|
39
|
+
name: "Agent: Docs Audit"
|
|
40
|
+
|
|
41
|
+
# Shared: network policy only. This workflow owns its Safe Outputs and OpenCode configuration.
|
|
42
|
+
# permissions, engine, model and runs-on cannot be shared , see shared/platform-defaults.md.
|
|
43
|
+
imports:
|
|
44
|
+
- github/gh-aw/.github/workflows/shared/opencode.md@v0.87.5
|
|
45
|
+
- shared/platform-defaults.md
|
|
46
|
+
- shared/opencode-ci.md
|
|
47
|
+
|
|
48
|
+
on:
|
|
49
|
+
workflow_call:
|
|
50
|
+
inputs:
|
|
51
|
+
trigger-kind:
|
|
52
|
+
description: "Docs audit trigger: scheduled or manual"
|
|
53
|
+
required: false
|
|
54
|
+
type: string
|
|
55
|
+
default: manual
|
|
56
|
+
|
|
57
|
+
# Rung 1. `-label:stale-audit` keeps this from becoming a stop: audit-close labels a report
|
|
58
|
+
# nobody actioned, which frees the slot and lists it in the "Needs a human" digest. Without it
|
|
59
|
+
# one abandoned report would disable this worker for good, with a green run every fortnight.
|
|
60
|
+
skip-if-match:
|
|
61
|
+
query: "is:issue is:open label:docs-audit -label:stale-audit"
|
|
62
|
+
max: 1
|
|
63
|
+
|
|
64
|
+
# gh-aw folds the top-level `if:` into activation without depending on the job it reads, so
|
|
65
|
+
# the reference would resolve to '' and the agent would never run.
|
|
66
|
+
needs: [recent_commits]
|
|
67
|
+
|
|
68
|
+
if: needs.recent_commits.outputs.changed == 'true'
|
|
69
|
+
|
|
70
|
+
runs-on: agents-arc
|
|
71
|
+
runs-on-slim: agents-arc
|
|
72
|
+
|
|
73
|
+
secrets:
|
|
74
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
75
|
+
|
|
76
|
+
engine:
|
|
77
|
+
id: opencode
|
|
78
|
+
version: "1.2.14"
|
|
79
|
+
env:
|
|
80
|
+
OPENAI_BASE_URL: https://forge.plainconcepts.com/v1
|
|
81
|
+
|
|
82
|
+
model: openai/glm-5-2
|
|
83
|
+
|
|
84
|
+
max-turns: 150
|
|
85
|
+
max-turn-cache-misses: 1500
|
|
86
|
+
max-ai-credits: 2500
|
|
87
|
+
|
|
88
|
+
permissions: read-all
|
|
89
|
+
|
|
90
|
+
steps:
|
|
91
|
+
- name: List what is already tracked
|
|
92
|
+
uses: ./.github/actions/list-open-issues
|
|
93
|
+
with:
|
|
94
|
+
token: ${{ github.token }}
|
|
95
|
+
repo: ${{ github.repository }}
|
|
96
|
+
- name: Collect documentation drift
|
|
97
|
+
uses: ./.github/actions/collect-doc-drift
|
|
98
|
+
with:
|
|
99
|
+
entry-docs: ${{ env.DOCS_AUDIT_ENTRY_DOCS }}
|
|
100
|
+
exclude: ${{ env.DOCS_AUDIT_EXCLUDE }}
|
|
101
|
+
# Fetched here rather than by the agent, whose network is closed. A standard that cannot be
|
|
102
|
+
# read costs the structure check, not the run: the prompt says to report it.
|
|
103
|
+
- name: Fetch the documentation standard
|
|
104
|
+
env:
|
|
105
|
+
STANDARD_PATH: /tmp/gh-aw/agent/documentation-standard.md
|
|
106
|
+
run: |
|
|
107
|
+
set -uo pipefail
|
|
108
|
+
mkdir -p "$(dirname "$STANDARD_PATH")"
|
|
109
|
+
if [ -n "$DOCS_STANDARD_URL" ] && curl -fsSL --max-time 30 "$DOCS_STANDARD_URL" -o "$STANDARD_PATH"; then
|
|
110
|
+
echo "Read the documentation standard from $DOCS_STANDARD_URL"
|
|
111
|
+
else
|
|
112
|
+
rm -f "$STANDARD_PATH"
|
|
113
|
+
echo "::warning::No documentation standard could be read from '${DOCS_STANDARD_URL}'. The audit runs without its structure check."
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
jobs:
|
|
117
|
+
# The cheap exit. A repository with no commits since the last run cannot have drifted, and a
|
|
118
|
+
# full agent run to say so would hold half of a two-machine fleet. A dispatch always runs: a
|
|
119
|
+
# person asking for an audit has a reason.
|
|
120
|
+
recent_commits:
|
|
121
|
+
runs-on: agents-arc
|
|
122
|
+
timeout-minutes: 5
|
|
123
|
+
permissions:
|
|
124
|
+
contents: read
|
|
125
|
+
outputs:
|
|
126
|
+
changed: ${{ steps.commits.outputs.changed }}
|
|
127
|
+
steps:
|
|
128
|
+
- name: Count recent commits on the default branch
|
|
129
|
+
id: commits
|
|
130
|
+
env:
|
|
131
|
+
GH_TOKEN: ${{ github.token }}
|
|
132
|
+
REPO: ${{ github.repository }}
|
|
133
|
+
TRIGGER_KIND: ${{ inputs.trigger-kind }}
|
|
134
|
+
QUIET_DAYS: ${{ env.DOCS_AUDIT_QUIET_DAYS }}
|
|
135
|
+
run: |
|
|
136
|
+
set -uo pipefail
|
|
137
|
+
if [ "$TRIGGER_KIND" != "scheduled" ]; then
|
|
138
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
139
|
+
exit 0
|
|
140
|
+
fi
|
|
141
|
+
since="$(date -u -d "${QUIET_DAYS} days ago" +%Y-%m-%dT%H:%M:%SZ)"
|
|
142
|
+
# An unanswered API call must not read as "nothing changed": that is a green run that
|
|
143
|
+
# audited nothing. Run the audit instead.
|
|
144
|
+
if ! commits="$(gh api "repos/${REPO}/commits?since=${since}&per_page=1" --jq 'length')"; then
|
|
145
|
+
echo "::warning::Could not list recent commits; running the audit anyway."
|
|
146
|
+
commits=1
|
|
147
|
+
fi
|
|
148
|
+
if [ "$commits" = "0" ]; then
|
|
149
|
+
echo "No commits since ${since}: nothing can have drifted, so the agent does not run."
|
|
150
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
151
|
+
else
|
|
152
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
conclude:
|
|
156
|
+
needs: [activation, agent, safe_outputs]
|
|
157
|
+
if: >
|
|
158
|
+
needs.agent.result == 'success' &&
|
|
159
|
+
needs.safe_outputs.result == 'success' &&
|
|
160
|
+
needs.safe_outputs.outputs.process_safe_outputs_processed_count != '0'
|
|
161
|
+
runs-on: agents-arc
|
|
162
|
+
permissions:
|
|
163
|
+
contents: read
|
|
164
|
+
issues: write
|
|
165
|
+
steps:
|
|
166
|
+
- name: Checkout workflow actions
|
|
167
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
168
|
+
with:
|
|
169
|
+
persist-credentials: false
|
|
170
|
+
- name: Create bot token
|
|
171
|
+
id: app-token
|
|
172
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
173
|
+
with:
|
|
174
|
+
client-id: ${{ secrets.BOT_APP_ID }}
|
|
175
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
176
|
+
# The safe_outputs job already created the issue. Only the labels are left, and they are
|
|
177
|
+
# applied with the App token so that `refine` reaches the router as a trusted event.
|
|
178
|
+
- name: Apply labels to the report
|
|
179
|
+
if: needs.safe_outputs.outputs.created_issue_number != ''
|
|
180
|
+
uses: ./.github/actions/add-issue-labels
|
|
181
|
+
with:
|
|
182
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
183
|
+
issue-number: ${{ needs.safe_outputs.outputs.created_issue_number }}
|
|
184
|
+
labels: |
|
|
185
|
+
docs-audit
|
|
186
|
+
refine
|
|
187
|
+
|
|
188
|
+
# An agent that ended without a report or a noop would otherwise be a green run that audited
|
|
189
|
+
# nothing; see the same job in agent-audit.md.
|
|
190
|
+
empty_run:
|
|
191
|
+
needs: [agent, safe_outputs]
|
|
192
|
+
if: >
|
|
193
|
+
needs.agent.result == 'success' &&
|
|
194
|
+
needs.safe_outputs.outputs.process_safe_outputs_processed_count == '0'
|
|
195
|
+
runs-on: agents-arc
|
|
196
|
+
permissions:
|
|
197
|
+
contents: read
|
|
198
|
+
steps:
|
|
199
|
+
- name: Report a docs audit that produced no outcome
|
|
200
|
+
env:
|
|
201
|
+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
202
|
+
run: |
|
|
203
|
+
set -euo pipefail
|
|
204
|
+
echo "::error::The docs audit agent finished without emitting a report or a noop. Nothing was filed and no reason was given, so this run is a failure rather than a clean audit. ${RUN_URL}"
|
|
205
|
+
exit 1
|
|
206
|
+
|
|
207
|
+
safe-outputs:
|
|
208
|
+
# A failed run is already a red run. An issue per failure buries the real backlog.
|
|
209
|
+
report-failure-as-issue: false
|
|
210
|
+
threat-detection: false
|
|
211
|
+
create-issue:
|
|
212
|
+
max: 1
|
|
213
|
+
|
|
214
|
+
# Reads documents and the code they describe, and writes one issue. Unobserved so far: audit, the
|
|
215
|
+
# nearest worker, takes about 45 minutes over a whole repository, and this one reads less.
|
|
216
|
+
timeout-minutes: 60
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
1. This is a read-only documentation audit. Do not modify any file, do not commit, and do not
|
|
220
|
+
push. Adhere to ${{ env.REPO_RULES }}.
|
|
221
|
+
|
|
222
|
+
2. Read the facts already collected for you, so you do not re-derive them:
|
|
223
|
+
- `/tmp/gh-aw/agent/doc-drift.json`: `brokenLinks` and `missingPaths`, each with its file and
|
|
224
|
+
line, plus `missingEntryDocuments`. These are established; do not check them again.
|
|
225
|
+
- `/tmp/gh-aw/agent/documentation-standard.md`: the Platform documentation standard. If this
|
|
226
|
+
file does not exist, skip step 4 and say so in one line of the report.
|
|
227
|
+
- `/tmp/gh-aw/agent/open-issues.json`: every open issue with its title and labels.
|
|
228
|
+
|
|
229
|
+
3. Read the entry documents in `doc-drift.json`, then compare what they state with the code.
|
|
230
|
+
Look for: ${{ env.DOCS_AUDIT_FOCUS }}
|
|
231
|
+
|
|
232
|
+
Every finding needs evidence on both sides: the document's `file:line` and what it says, and
|
|
233
|
+
the `file:line` or directory that shows otherwise. A claim you cannot back with both is not a
|
|
234
|
+
finding. Prefer what misleads a reader acting on it over what is merely imprecise, and do not
|
|
235
|
+
report style, tone or missing polish.
|
|
236
|
+
|
|
237
|
+
4. Compare the repository's documentation structure with the standard. Report a required
|
|
238
|
+
section that is missing, or that exists and is empty. A section the standard marks as not
|
|
239
|
+
applicable to this kind of repository is not a finding.
|
|
240
|
+
|
|
241
|
+
5. Group the broken links and missing paths from step 2 into findings by document or by cause,
|
|
242
|
+
such as one renamed directory, rather than one finding per link. State their counts as they
|
|
243
|
+
appear in `doc-drift.json`.
|
|
244
|
+
|
|
245
|
+
6. Discard any finding already tracked in `open-issues.json`. Exact-title duplicates are rejected
|
|
246
|
+
for you; your job is the ones worded differently that mean the same thing.
|
|
247
|
+
|
|
248
|
+
Before filing, call `memory_smart_search` for prior documentation audits of this repository
|
|
249
|
+
and drop any finding that was reported and consciously not acted on. After you have decided
|
|
250
|
+
what to file, call `memory_save` once with the date and, for each finding, its file, a
|
|
251
|
+
one-line description and its disposition. Keep it short; it is read by the next audit.
|
|
252
|
+
|
|
253
|
+
7. If no finding is left, call `noop` and stop. Filing nothing is the right outcome when the
|
|
254
|
+
documentation is current.
|
|
255
|
+
|
|
256
|
+
8. Call `create_issue` **once** with title "Docs audit: <date>". Do NOT specify labels, the
|
|
257
|
+
conclude job applies them. List every finding, the most misleading first, each with:
|
|
258
|
+
- the document's `file:line` and the stale statement;
|
|
259
|
+
- the evidence from the code, with its `file:line` or path;
|
|
260
|
+
- what the document should say, or the section that has to be added.
|
|
261
|
+
|
|
262
|
+
The issue goes to Refine, which sizes it and splits it into one issue per finding, so write
|
|
263
|
+
each finding to survive that: self-contained, naming its own files, never "as above".
|
|
264
|
+
|
|
265
|
+
Refer to people by role, never by name, and include no personal contact details, even when
|
|
266
|
+
a document you are quoting contains them.
|
|
@@ -1,105 +1,107 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/authorize-bot-work.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
# Human adds implement/refine/direct/feature label → validates permission → bot adds bot-working
|
|
3
|
-
# This ensures the bot is the actor for all agentic workflows.
|
|
4
|
-
#
|
|
5
|
-
# IMPORTANT: Only triggers for HUMAN actors. When the bot transitions refine→implement,
|
|
6
|
-
# it adds bot-working itself, so authorize-bot-work must not fire again.
|
|
7
|
-
name: "Authorize Bot Work"
|
|
8
|
-
|
|
9
|
-
run-name: "Authorizing: ${{ github.event.issue.title }} (#${{ github.event.issue.number }})"
|
|
10
|
-
|
|
11
|
-
on:
|
|
12
|
-
issues:
|
|
13
|
-
types: [labeled]
|
|
14
|
-
|
|
15
|
-
permissions:
|
|
16
|
-
contents: read
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
authorize:
|
|
20
|
-
# Only trigger for work labels from HUMANS (not bots), and only if another bot run does not
|
|
21
|
-
# already own the issue.
|
|
22
|
-
#
|
|
23
|
-
# `review` is deliberately NOT excluded. It used to be, and that made the label a one-way
|
|
24
|
-
# door: the classifier refuses to route while `review` is set, so a person adding `refine`
|
|
25
|
-
# to a parked issue got no run, no comment and no error anywhere. Triage's own
|
|
26
|
-
# needs-maintainer verdict tells the maintainer to add `refine`, which could not work.
|
|
27
|
-
#
|
|
28
|
-
# A person adding a work label IS the human review the label was waiting for, so this
|
|
29
|
-
# workflow clears it below before handing the issue to the bot. The classifier's own guard
|
|
30
|
-
# stays as it is: it exists to stop the *bot* re-triggering itself, and by the time
|
|
31
|
-
# bot-working is added `review` is already gone.
|
|
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
|
-
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/authorize-bot-work.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
# Human adds implement/refine/direct/feature label → validates permission → bot adds bot-working
|
|
3
|
+
# This ensures the bot is the actor for all agentic workflows.
|
|
4
|
+
#
|
|
5
|
+
# IMPORTANT: Only triggers for HUMAN actors. When the bot transitions refine→implement,
|
|
6
|
+
# it adds bot-working itself, so authorize-bot-work must not fire again.
|
|
7
|
+
name: "Authorize Bot Work"
|
|
8
|
+
|
|
9
|
+
run-name: "Authorizing: ${{ github.event.issue.title }} (#${{ github.event.issue.number }})"
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
issues:
|
|
13
|
+
types: [labeled]
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
authorize:
|
|
20
|
+
# Only trigger for work labels from HUMANS (not bots), and only if another bot run does not
|
|
21
|
+
# already own the issue.
|
|
22
|
+
#
|
|
23
|
+
# `review` is deliberately NOT excluded. It used to be, and that made the label a one-way
|
|
24
|
+
# door: the classifier refuses to route while `review` is set, so a person adding `refine`
|
|
25
|
+
# to a parked issue got no run, no comment and no error anywhere. Triage's own
|
|
26
|
+
# needs-maintainer verdict tells the maintainer to add `refine`, which could not work.
|
|
27
|
+
#
|
|
28
|
+
# A person adding a work label IS the human review the label was waiting for, so this
|
|
29
|
+
# workflow clears it below before handing the issue to the bot. The classifier's own guard
|
|
30
|
+
# stays as it is: it exists to stop the *bot* re-triggering itself, and by the time
|
|
31
|
+
# bot-working is added `review` is already gone.
|
|
32
|
+
# AGENTIC_WORKFLOWS_DISABLED, a repository variable with any value, turns the automation off.
|
|
33
|
+
if: >
|
|
34
|
+
vars.AGENTIC_WORKFLOWS_DISABLED == '' &&
|
|
35
|
+
(github.event.label.name == 'implement' ||
|
|
36
|
+
github.event.label.name == 'refine') &&
|
|
37
|
+
!contains(github.event.issue.labels.*.name, 'bot-working') &&
|
|
38
|
+
!endsWith(github.actor, '[bot]')
|
|
39
|
+
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
40
|
+
timeout-minutes: 5
|
|
41
|
+
concurrency:
|
|
42
|
+
group: authorize-${{ github.event.issue.number }}
|
|
43
|
+
cancel-in-progress: false
|
|
44
|
+
permissions:
|
|
45
|
+
contents: read
|
|
46
|
+
issues: write
|
|
47
|
+
steps:
|
|
48
|
+
- name: Check actor permission
|
|
49
|
+
id: check
|
|
50
|
+
env:
|
|
51
|
+
GH_TOKEN: ${{ github.token }}
|
|
52
|
+
ACTOR: ${{ github.actor }}
|
|
53
|
+
run: |
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
|
|
56
|
+
permission=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$ACTOR/permission" \
|
|
57
|
+
--jq '.permission' 2>/dev/null || echo "none")
|
|
58
|
+
|
|
59
|
+
case "$permission" in
|
|
60
|
+
admin | maintain | write)
|
|
61
|
+
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
|
62
|
+
echo "::notice::$ACTOR has $permission permission - authorized"
|
|
63
|
+
;;
|
|
64
|
+
*)
|
|
65
|
+
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
|
66
|
+
echo "::warning::$ACTOR has '$permission' permission - not authorized"
|
|
67
|
+
;;
|
|
68
|
+
esac
|
|
69
|
+
|
|
70
|
+
- name: Checkout for App token action
|
|
71
|
+
if: steps.check.outputs.authorized == 'true'
|
|
72
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
73
|
+
with:
|
|
74
|
+
persist-credentials: false
|
|
75
|
+
|
|
76
|
+
- name: Generate App token
|
|
77
|
+
if: steps.check.outputs.authorized == 'true'
|
|
78
|
+
id: app-token
|
|
79
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
80
|
+
with:
|
|
81
|
+
app-id: ${{ secrets.BOT_APP_ID }}
|
|
82
|
+
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
83
|
+
|
|
84
|
+
- name: Bot takes the issue over
|
|
85
|
+
if: steps.check.outputs.authorized == 'true'
|
|
86
|
+
env:
|
|
87
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
88
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
89
|
+
LABEL: ${{ github.event.label.name }}
|
|
90
|
+
run: |
|
|
91
|
+
set -euo pipefail
|
|
92
|
+
|
|
93
|
+
# Clear review FIRST. A person adding a work label is the human review the label was
|
|
94
|
+
# waiting for, and the classifier refuses to route an issue that still carries it, so
|
|
95
|
+
# removing it here is what makes the hand-off work at all. It also has to happen
|
|
96
|
+
# before bot-working, because that is the event the classifier reads: adding
|
|
97
|
+
# bot-working first would raise an event whose payload still shows review.
|
|
98
|
+
# --remove-label on an absent label is a no-op, so this is safe on a fresh issue.
|
|
99
|
+
# `stalled` goes with it: a person taking the issue on restarts the work, so the
|
|
100
|
+
# marker the janitor retries on must not survive into the new run.
|
|
101
|
+
gh issue edit "$ISSUE_NUMBER" --remove-label "review" --remove-label "stalled" 2>/dev/null ||
|
|
102
|
+
echo "::notice::no review or stalled label to clear on #$ISSUE_NUMBER"
|
|
103
|
+
|
|
104
|
+
echo "Adding bot-working label to issue #$ISSUE_NUMBER (triggered by $LABEL)"
|
|
105
|
+
gh issue edit "$ISSUE_NUMBER" --add-label "bot-working"
|
|
106
|
+
|
|
107
|
+
echo "::notice::bot-working label added - bot will now own the $LABEL workflow"
|
|
@@ -27,7 +27,9 @@ concurrency:
|
|
|
27
27
|
jobs:
|
|
28
28
|
tag:
|
|
29
29
|
name: Tag and publish the release
|
|
30
|
+
# AGENTIC_WORKFLOWS_DISABLED, a repository variable with any value, turns the automation off.
|
|
30
31
|
if: >-
|
|
32
|
+
vars.AGENTIC_WORKFLOWS_DISABLED == '' &&
|
|
31
33
|
startsWith(github.event.head_commit.message, 'chore(release): v')
|
|
32
34
|
runs-on: agents-arc
|
|
33
35
|
timeout-minutes: 10
|
|
@@ -83,6 +83,9 @@ run-name: >-
|
|
|
83
83
|
&& inputs.operation == 'audit'
|
|
84
84
|
&& format('Agent: Audit - manual{0}', '')
|
|
85
85
|
|| github.event_name == 'workflow_dispatch'
|
|
86
|
+
&& inputs.operation == 'docs-audit'
|
|
87
|
+
&& format('Agent: Docs Audit - manual{0}', '')
|
|
88
|
+
|| github.event_name == 'workflow_dispatch'
|
|
86
89
|
&& inputs.operation == 'release'
|
|
87
90
|
&& format('Agent: Release{0}', '')
|
|
88
91
|
|| github.event_name == 'workflow_dispatch'
|
|
@@ -91,6 +94,9 @@ run-name: >-
|
|
|
91
94
|
&& github.event.schedule == '17 1 * * 1'
|
|
92
95
|
&& format('Agent: Audit - weekly{0}', '')
|
|
93
96
|
|| github.event_name == 'schedule'
|
|
97
|
+
&& github.event.schedule == '47 2 1,15 * *'
|
|
98
|
+
&& format('Agent: Docs Audit - fortnightly{0}', '')
|
|
99
|
+
|| github.event_name == 'schedule'
|
|
94
100
|
&& github.event.schedule == '43 3 * * *'
|
|
95
101
|
&& format('Agentics: Audit Close - daily{0}', '')
|
|
96
102
|
|| github.event_name == 'schedule'
|
|
@@ -125,6 +131,9 @@ env:
|
|
|
125
131
|
# first and the two of them consume the whole fleet. Pick a free slot per repository: see
|
|
126
132
|
# "Scheduling across repositories" in the package README.
|
|
127
133
|
AUDIT_CRON: "17 1 * * 1"
|
|
134
|
+
# The same idea for the documentation audit, if it is installed: see the same section of the
|
|
135
|
+
# package README. Two days a month, so a slot is a pair of days rather than a weekday.
|
|
136
|
+
DOCS_AUDIT_CRON: "47 2 1,15 * *"
|
|
128
137
|
# The bot identities whose label changes and pull requests this router trusts. Space
|
|
129
138
|
# separated, and every login the same account can present: GitHub reports an App as
|
|
130
139
|
# `name[bot]` on some endpoints and `app/name` on others, so both spellings belong here.
|
|
@@ -221,6 +230,7 @@ on:
|
|
|
221
230
|
# Cron to route mapping lives in .github/actions/classify-route/classify-route.sh.
|
|
222
231
|
schedule:
|
|
223
232
|
- cron: "17 1 * * 1" # audit slot, mirrored from env.AUDIT_CRON by the installer
|
|
233
|
+
- cron: "47 2 1,15 * *" # docs-audit slot, mirrored from env.DOCS_AUDIT_CRON by the installer
|
|
224
234
|
- cron: "43 3 * * *"
|
|
225
235
|
- cron: "0 6 * * *"
|
|
226
236
|
- cron: "17 * * * *"
|
|
@@ -241,6 +251,7 @@ on:
|
|
|
241
251
|
- merge-train
|
|
242
252
|
- evidence
|
|
243
253
|
- audit
|
|
254
|
+
- docs-audit
|
|
244
255
|
- audit-close
|
|
245
256
|
- cleanup-artifacts
|
|
246
257
|
- reconcile-bot-pr-runs
|
|
@@ -307,6 +318,10 @@ permissions:
|
|
|
307
318
|
jobs:
|
|
308
319
|
# Public repositories must check repository permission before an event can start a model.
|
|
309
320
|
authorize:
|
|
321
|
+
# Off switch: a repository variable AGENTIC_WORKFLOWS_DISABLED, with any value, stops every
|
|
322
|
+
# route. Every other job needs this one or classify, so nothing runs. For a repository that has
|
|
323
|
+
# the workflows installed but is not ready for them.
|
|
324
|
+
if: vars.AGENTIC_WORKFLOWS_DISABLED == ''
|
|
310
325
|
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
311
326
|
timeout-minutes: 5
|
|
312
327
|
permissions:
|
|
@@ -371,6 +386,8 @@ jobs:
|
|
|
371
386
|
esac
|
|
372
387
|
|
|
373
388
|
classify:
|
|
389
|
+
# The same off switch as authorize; jobs that need only classify stop here.
|
|
390
|
+
if: vars.AGENTIC_WORKFLOWS_DISABLED == ''
|
|
374
391
|
runs-on: [agents-aw, "affinity-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}"]
|
|
375
392
|
timeout-minutes: 5
|
|
376
393
|
permissions:
|
|
@@ -678,7 +695,12 @@ jobs:
|
|
|
678
695
|
gh issue view "$feature" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
679
696
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' || true
|
|
680
697
|
} | sort -u ); do
|
|
681
|
-
|
|
698
|
+
# A feature's own children are not what the feature waits on. Refine's tracker lists them
|
|
699
|
+
# in the feature body as "#N ... Blocked by #M", and reading those lines as the feature's
|
|
700
|
+
# blockers held every child behind its siblings, and the first one behind itself.
|
|
701
|
+
info=$(gh issue view "$b" --repo "$REPO" --json state,body 2>/dev/null) || continue
|
|
702
|
+
printf '%s' "$info" | jq -e '(.state // "" | ascii_downcase) == "open"' >/dev/null || continue
|
|
703
|
+
printf '%s' "$info" | jq -r '.body // ""' | grep -qF "<!-- split-parent: ${feature} -->" && continue
|
|
682
704
|
waiting="$waiting #$b"
|
|
683
705
|
done
|
|
684
706
|
fi
|
|
@@ -735,6 +757,7 @@ jobs:
|
|
|
735
757
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --remove-label bot-working >/dev/null 2>&1 || true
|
|
736
758
|
marker='<!-- stack-held -->'
|
|
737
759
|
if ! gh api "repos/$REPO/issues/$ISSUE_NUMBER/comments?per_page=100" --jq '.[].body' 2>/dev/null | grep -qF "$marker"; then
|
|
760
|
+
# shellcheck disable=SC2016
|
|
738
761
|
printf '%s\nHeld until%s have landed on the default branch: this feature builds on them. It keeps `implement` and is released when they merge.\n' "$marker" "$waiting" > /tmp/held.md
|
|
739
762
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body-file /tmp/held.md >/dev/null || true
|
|
740
763
|
fi
|
|
@@ -1142,6 +1165,24 @@ jobs:
|
|
|
1142
1165
|
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
1143
1166
|
AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
|
|
1144
1167
|
|
|
1168
|
+
call-docs-audit:
|
|
1169
|
+
needs: classify
|
|
1170
|
+
if: needs.classify.outputs.route == 'docs-audit'
|
|
1171
|
+
uses: ./.github/workflows/agent-docs-audit.lock.yml
|
|
1172
|
+
concurrency:
|
|
1173
|
+
group: docs-audit
|
|
1174
|
+
cancel-in-progress: false
|
|
1175
|
+
permissions: write-all
|
|
1176
|
+
with:
|
|
1177
|
+
trigger-kind: ${{ needs.classify.outputs.trigger-kind }}
|
|
1178
|
+
secrets:
|
|
1179
|
+
OPENAI_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
1180
|
+
CODEX_API_KEY: ${{ secrets.FORGE_API_KEY }}
|
|
1181
|
+
COPILOT_GITHUB_TOKEN: ${{ secrets.FORGE_API_KEY }}
|
|
1182
|
+
BOT_APP_ID: ${{ secrets.BOT_APP_ID }}
|
|
1183
|
+
BOT_PRIVATE_KEY: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
1184
|
+
AGENTMEMORY_SECRET: ${{ secrets.AGENTMEMORY_SECRET }}
|
|
1185
|
+
|
|
1145
1186
|
call-release:
|
|
1146
1187
|
needs: [classify, authorize]
|
|
1147
1188
|
if: needs.classify.outputs.route == 'release' && needs.authorize.outputs.trusted == 'true'
|
|
@@ -1292,6 +1333,7 @@ jobs:
|
|
|
1292
1333
|
# each cut from the same branch, and every landing left the others conflicting. The
|
|
1293
1334
|
# next landing releases the next child. A held child with no `Blocked by` of its
|
|
1294
1335
|
# own (held because a sibling was in flight) is free once it carries the hold marker.
|
|
1336
|
+
# shellcheck disable=SC2086
|
|
1295
1337
|
for child in $(printf '%s\n' $children | sort -n); do
|
|
1296
1338
|
[ "$child" = "$closed_issue" ] && continue
|
|
1297
1339
|
meta=$(gh issue view "$child" --repo "$REPO" --json state,labels,body 2>/dev/null || echo '{}')
|
|
@@ -1418,7 +1460,11 @@ jobs:
|
|
|
1418
1460
|
gh api "repos/$REPO/issues/$1/dependencies/blocked_by" --jq '.[] | select(.state == "open") | .number' 2>/dev/null || true
|
|
1419
1461
|
for b in $(gh issue view "$1" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
1420
1462
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true); do
|
|
1421
|
-
|
|
1463
|
+
# The feature's own children, listed in its tracker with their own "Blocked by", are not blockers.
|
|
1464
|
+
info=$(gh issue view "$b" --repo "$REPO" --json state,body 2>/dev/null) || continue
|
|
1465
|
+
printf '%s' "$info" | jq -e '(.state // "" | ascii_downcase) == "open"' >/dev/null || continue
|
|
1466
|
+
printf '%s' "$info" | jq -r '.body // ""' | grep -qF "<!-- split-parent: $1 -->" && continue
|
|
1467
|
+
echo "$b"
|
|
1422
1468
|
done
|
|
1423
1469
|
} | grep -vx "$parent" | sort -u || true
|
|
1424
1470
|
}
|
|
@@ -1470,6 +1516,7 @@ jobs:
|
|
|
1470
1516
|
fi
|
|
1471
1517
|
# One child per feature, the lowest-numbered free one; each landing on the feature branch
|
|
1472
1518
|
# releases the next (land-stack-child), so siblings never build side by side.
|
|
1519
|
+
# shellcheck disable=SC2086
|
|
1473
1520
|
for child in $(printf '%s\n' ${next_children[$feature]} | sort -n); do
|
|
1474
1521
|
own=$(gh issue view "$child" --repo "$REPO" --json body --jq '.body // ""' 2>/dev/null \
|
|
1475
1522
|
| grep -oiE 'blocked by #[0-9]+' | grep -oE '[0-9]+' | sort -u || true)
|