@plainconceptsplatform/workflows 0.4.33 → 0.5.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 +88 -88
- package/loops/actions/agent-output.cjs +17 -17
- package/loops/actions/audit-close/action.yml +24 -0
- package/loops/actions/classify-route/action.yml +4 -3
- package/loops/actions/classify-route/classify-route.sh +69 -29
- package/loops/actions/load-issue-context/action.yml +2 -0
- package/loops/actions/update-changelog/action.yml +113 -0
- package/loops/actions/validate-merge-gate-output/action.yml +40 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +34 -0
- package/loops/actions/validate-refine-output/validate-refine-output.sh +16 -3
- package/loops/actions/validate-review-output/action.yml +35 -0
- package/loops/actions/validate-review-output/validate-review-output.sh +30 -0
- package/loops/actions/validate-triage-output/action.yml +36 -0
- package/loops/actions/validate-triage-output/validate-triage-output.sh +36 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +121 -36
- package/loops/scripts/compile-agent-workflows.mjs +195 -6
- package/loops/templates/agentics/agentics-checks.yml +86 -86
- package/loops/templates/agentics/agentics-maintenance.yml +121 -121
- package/loops/templates/ci/app-ci-dotnet-next.yml +268 -171
- package/loops/templates/ci/app-ci-node-monorepo.yml +208 -178
- 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 +49 -47
- package/loops/templates/opencode/opencode.ci.json.md +46 -41
- package/loops/templates/release/github-release.yml +30 -30
- package/loops/workflows/agent-apply-review.md +465 -373
- package/loops/workflows/agent-audit.md +213 -197
- package/loops/workflows/agent-implement.md +538 -374
- package/loops/workflows/agent-merge-gate.md +730 -485
- package/loops/workflows/agent-refine.md +609 -379
- package/loops/workflows/agent-release.md +258 -0
- package/loops/workflows/agent-triage.md +447 -0
- package/loops/workflows/authorize-bot-work.yml +85 -81
- package/loops/workflows/shared/opencode-ci.md +206 -197
- package/loops/workflows/shared/platform-defaults.md +19 -16
- package/loops/workflows/work-router.yml +862 -551
- package/package.json +42 -42
- package/dist/catalog-installation.d.ts +0 -30
- package/dist/catalog-installation.js +0 -352
- package/dist/catalog-installation.test.d.ts +0 -1
- package/dist/catalog-installation.test.js +0 -448
- package/dist/catalog-listing.d.ts +0 -13
- package/dist/catalog-listing.js +0 -70
- package/dist/catalog-listing.test.d.ts +0 -1
- package/dist/catalog-listing.test.js +0 -150
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -218
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -273
- package/dist/repository-inspection.d.ts +0 -23
- package/dist/repository-inspection.js +0 -113
- package/dist/repository-inspection.test.d.ts +0 -1
- package/dist/repository-inspection.test.js +0 -77
- package/dist/repository-state.d.ts +0 -18
- package/dist/repository-state.js +0 -77
- package/dist/repository-state.test.d.ts +0 -1
- package/dist/repository-state.test.js +0 -96
- package/dist/route-processing.d.ts +0 -6
- package/dist/route-processing.js +0 -150
- package/dist/route-processing.test.d.ts +0 -1
- package/dist/route-processing.test.js +0 -342
- package/dist/stack-defaults.d.ts +0 -11
- package/dist/stack-defaults.js +0 -104
- package/dist/stack-defaults.test.d.ts +0 -1
- package/dist/stack-defaults.test.js +0 -266
- package/dist/tui.d.ts +0 -25
- package/dist/tui.js +0 -287
- package/dist/tui.test.d.ts +0 -1
- package/dist/tui.test.js +0 -246
- package/dist/workflow-catalog.d.ts +0 -25
- package/dist/workflow-catalog.js +0 -57
- package/dist/workflow-catalog.test.d.ts +0 -1
- package/dist/workflow-catalog.test.js +0 -29
- package/loops/workflows/agent-direct.md +0 -363
- package/loops/workflows/agent-propose.md +0 -342
package/README.md
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
# Workflows CLI
|
|
2
|
-
|
|
3
|
-
Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
|
|
4
|
-
|
|
5
|
-
## Quick start
|
|
6
|
-
|
|
7
|
-
The primary entrypoint is the interactive TUI. Run it with no arguments:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npx @plainconceptsplatform/workflows
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting routes installs only those route workers plus mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix). Selecting only templates installs just those templates.
|
|
14
|
-
|
|
15
|
-
## Install
|
|
16
|
-
|
|
17
|
-
Before installing workflows, install and configure [`PlainConceptsPlatform/
|
|
18
|
-
|
|
19
|
-
For non-interactive use (advanced):
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npx @plainconceptsplatform/workflows@latest init
|
|
23
|
-
npx @plainconceptsplatform/workflows@latest add
|
|
24
|
-
npx @plainconceptsplatform/workflows@latest update
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
For a project-local development dependency:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
pnpm add -D @plainconceptsplatform/workflows
|
|
31
|
-
pnpm exec workflows # launch interactive TUI
|
|
32
|
-
pnpm exec workflows init
|
|
33
|
-
pnpm exec workflows add
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
`init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
|
|
37
|
-
|
|
38
|
-
`add` installs mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix) when called with no route arguments. Pass route names as positional arguments to install specific route workers alongside the mandatory files:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
workflows add # mandatory files only, no worker .md files
|
|
42
|
-
workflows add implement refine direct # those route workers plus mandatory files
|
|
43
|
-
workflows add --template agentics-checks # named template only (no mandatory files)
|
|
44
|
-
workflows add refine --template agentics-checks --force # routes + template + mandatory, overwriting conflicts
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Route names: refine, implement, direct, apply-review, merge-gate, audit, propose. Unknown arguments produce an error.
|
|
48
|
-
|
|
49
|
-
Use `workflows update --force` to force-overwrite managed files that differ from the package source.
|
|
50
|
-
|
|
51
|
-
Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`, `app-ci-node-monorepo`, and `github-release`. CI templates are stack-specific copies, not a combined template. `github-release` publishes generated release notes when a `v*` tag is pushed. Edit their top-level `env:` values for repository paths, package names, and commands.
|
|
52
|
-
|
|
53
|
-
## List and search
|
|
54
|
-
|
|
55
|
-
List all available workflows, routes, and templates with install status:
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
npx --yes --package @plainconceptsplatform/workflows@latest workflows list
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Each entry is marked `[x]` when the corresponding `.github/workflows/agent-*.md` file already exists in the current directory, or `[ ]` when it is not yet installed.
|
|
62
|
-
|
|
63
|
-
Search by name or description:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Manual installation
|
|
70
|
-
|
|
71
|
-
The package includes `loops/`, a copyable equivalent of `.github/`:
|
|
72
|
-
|
|
73
|
-
- `loops/actions/` maps to `.github/actions/`
|
|
74
|
-
- `loops/workflows/` maps to `.github/workflows/`
|
|
75
|
-
- `loops/scripts/` maps to `scripts/`
|
|
76
|
-
|
|
77
|
-
Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
|
|
78
|
-
top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
|
|
79
|
-
|
|
80
|
-
## Compile
|
|
81
|
-
|
|
82
|
-
Consumer repositories generate and commit `*.lock.yml` files:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
node scripts/compile-agent-workflows.mjs --force
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Do not commit generated locks to this package source repository.
|
|
1
|
+
# Workflows CLI
|
|
2
|
+
|
|
3
|
+
Install and update shared GitHub Agentic Workflows for Plain Concepts Platform repositories.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
The primary entrypoint is the interactive TUI. Run it with no arguments:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @plainconceptsplatform/workflows
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The TUI lists all routes and templates with install status. Arrow keys navigate, space toggles, Enter installs. Selecting routes installs only those route workers plus mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix). Selecting only templates installs just those templates.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Before installing workflows, install and configure [`PlainConceptsPlatform/agent-harness`](https://github.com/PlainConceptsPlatform/agent-harness) in the consumer repository. Loop workers invoke the skills and commands it provides. Verify the required skills and commands are available before compiling workflows.
|
|
18
|
+
|
|
19
|
+
For non-interactive use (advanced):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @plainconceptsplatform/workflows@latest init
|
|
23
|
+
npx @plainconceptsplatform/workflows@latest add
|
|
24
|
+
npx @plainconceptsplatform/workflows@latest update
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For a project-local development dependency:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add -D @plainconceptsplatform/workflows
|
|
31
|
+
pnpm exec workflows # launch interactive TUI
|
|
32
|
+
pnpm exec workflows init
|
|
33
|
+
pnpm exec workflows add
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`init` inspects the repository and reports its stack and visibility. It does not create or manage repository configuration or a manifest.
|
|
37
|
+
|
|
38
|
+
`add` installs mandatory files (opencode.ci.json, compile script, shared imports, actions, router, classifier, route matrix) when called with no route arguments. Pass route names as positional arguments to install specific route workers alongside the mandatory files:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
workflows add # mandatory files only, no worker .md files
|
|
42
|
+
workflows add implement refine direct # those route workers plus mandatory files
|
|
43
|
+
workflows add --template agentics-checks # named template only (no mandatory files)
|
|
44
|
+
workflows add refine --template agentics-checks --force # routes + template + mandatory, overwriting conflicts
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Route names: refine, implement, direct, apply-review, merge-gate, audit, propose. Unknown arguments produce an error.
|
|
48
|
+
|
|
49
|
+
Use `workflows update --force` to force-overwrite managed files that differ from the package source.
|
|
50
|
+
|
|
51
|
+
Install optional standalone templates with `add --template`. Available templates are `agentics-checks`, `agentics-maintenance`, `app-ci-dotnet-next`, `app-ci-node-monorepo`, and `github-release`. CI templates are stack-specific copies, not a combined template. `github-release` publishes generated release notes when a `v*` tag is pushed. Edit their top-level `env:` values for repository paths, package names, and commands.
|
|
52
|
+
|
|
53
|
+
## List and search
|
|
54
|
+
|
|
55
|
+
List all available workflows, routes, and templates with install status:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows list
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each entry is marked `[x]` when the corresponding `.github/workflows/agent-*.md` file already exists in the current directory, or `[ ]` when it is not yet installed.
|
|
62
|
+
|
|
63
|
+
Search by name or description:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx --yes --package @plainconceptsplatform/workflows@latest workflows search "ci"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Manual installation
|
|
70
|
+
|
|
71
|
+
The package includes `loops/`, a copyable equivalent of `.github/`:
|
|
72
|
+
|
|
73
|
+
- `loops/actions/` maps to `.github/actions/`
|
|
74
|
+
- `loops/workflows/` maps to `.github/workflows/`
|
|
75
|
+
- `loops/scripts/` maps to `scripts/`
|
|
76
|
+
|
|
77
|
+
Copy these files manually if you do not use the CLI. Each worker is self-contained. Edit its
|
|
78
|
+
top-level `env:` defaults directly for consumer-specific endpoint, model, labels, paths, and checks.
|
|
79
|
+
|
|
80
|
+
## Compile
|
|
81
|
+
|
|
82
|
+
Consumer repositories generate and commit `*.lock.yml` files:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
node scripts/compile-agent-workflows.mjs --force
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Do not commit generated locks to this package source repository.
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// Managed by @plainconceptsplatform/workflows. Source: loops/actions/agent-output.cjs. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
|
|
4
|
-
// Every apply-agent-* action reads the same artifact the same way: a missing file means the
|
|
5
|
-
// agent produced nothing, which is a normal outcome rather than a failure.
|
|
6
|
-
function readAgentItems(outputFile, type) {
|
|
7
|
-
if (!outputFile || !fs.existsSync(outputFile)) {
|
|
8
|
-
return [];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const parsed = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
|
|
12
|
-
const items = Array.isArray(parsed.items) ? parsed.items : [];
|
|
13
|
-
|
|
14
|
-
return type ? items.filter((item) => item.type === type) : items;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
module.exports = { readAgentItems };
|
|
1
|
+
// Managed by @plainconceptsplatform/workflows. Source: loops/actions/agent-output.cjs. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
|
|
4
|
+
// Every apply-agent-* action reads the same artifact the same way: a missing file means the
|
|
5
|
+
// agent produced nothing, which is a normal outcome rather than a failure.
|
|
6
|
+
function readAgentItems(outputFile, type) {
|
|
7
|
+
if (!outputFile || !fs.existsSync(outputFile)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const parsed = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
|
|
12
|
+
const items = Array.isArray(parsed.items) ? parsed.items : [];
|
|
13
|
+
|
|
14
|
+
return type ? items.filter((item) => item.type === type) : items;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { readAgentItems };
|
|
@@ -81,6 +81,30 @@ runs:
|
|
|
81
81
|
? 'This report references no issues.'
|
|
82
82
|
: `All ${closedRefs} referenced issue(s) are closed.`;
|
|
83
83
|
|
|
84
|
+
// Do not close if there are open PRs that close this issue.
|
|
85
|
+
// The audit report may have spawned implement workers whose PRs
|
|
86
|
+
// are still under review — closing the report prematurely is misleading.
|
|
87
|
+
const openPRs = await github.paginate(github.rest.pulls.list, {
|
|
88
|
+
...context.repo,
|
|
89
|
+
state: 'open',
|
|
90
|
+
per_page: 100,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const closesThisReport = openPRs.some((pr) => {
|
|
94
|
+
const body = pr.body ?? '';
|
|
95
|
+
const patterns = [
|
|
96
|
+
new RegExp(`(?:^|[^A-Za-z0-9_/])#(?:closes?|fixes?|resolves?|closed?)\\s+#${report.number}\\b`, 'i'),
|
|
97
|
+
new RegExp(`(?:^|[^A-Za-z0-9_/])#${report.number}\\b`),
|
|
98
|
+
];
|
|
99
|
+
return patterns.some((p) => p.test(body));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (closesThisReport) {
|
|
103
|
+
core.info(`#${report.number}: kept open, has open PR(s) that close it.`);
|
|
104
|
+
keptOpen += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
await github.rest.issues.createComment({
|
|
85
109
|
...context.repo,
|
|
86
110
|
issue_number: report.number,
|
|
@@ -27,9 +27,9 @@ outputs:
|
|
|
27
27
|
refine-mode:
|
|
28
28
|
description: first or rerefine.
|
|
29
29
|
value: ${{ steps.classify.outputs.refine-mode }}
|
|
30
|
-
|
|
31
|
-
description: first or
|
|
32
|
-
value: ${{ steps.classify.outputs.
|
|
30
|
+
triage-mode:
|
|
31
|
+
description: first or retriage.
|
|
32
|
+
value: ${{ steps.classify.outputs.triage-mode }}
|
|
33
33
|
trigger-kind:
|
|
34
34
|
description: scheduled or manual.
|
|
35
35
|
value: ${{ steps.classify.outputs.trigger-kind }}
|
|
@@ -44,6 +44,7 @@ runs:
|
|
|
44
44
|
ACTION: ${{ github.event.action }}
|
|
45
45
|
LABEL: ${{ github.event.label.name }}
|
|
46
46
|
ISSUE_LABELS: ${{ toJSON(github.event.issue.labels.*.name) }}
|
|
47
|
+
ISSUE_STATE: ${{ github.event.issue.state }}
|
|
47
48
|
EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
48
49
|
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
49
50
|
COMMENT_ON_PR: ${{ github.event.issue.pull_request != '' }}
|
|
@@ -11,11 +11,7 @@ set -euo pipefail
|
|
|
11
11
|
readonly AUDIT_CRON="17 1 * * 1"
|
|
12
12
|
readonly AUDIT_CLOSE_CRON="43 3 * * *"
|
|
13
13
|
readonly CLEANUP_ARTIFACTS_CRON="0 6 * * *"
|
|
14
|
-
readonly RECONCILE_BOT_PR_RUNS_CRON="
|
|
15
|
-
# Daily, but it proposes far less often than daily: the worker holds one open
|
|
16
|
-
# proposal at a time and skips while that slot is filled. The cron is a heartbeat,
|
|
17
|
-
# the queue is the pacing.
|
|
18
|
-
readonly PROPOSE_CRON="29 7 * * *"
|
|
14
|
+
readonly RECONCILE_BOT_PR_RUNS_CRON="17 * * * *"
|
|
19
15
|
|
|
20
16
|
has_label() {
|
|
21
17
|
jq -e --arg name "$1" 'index($name)' >/dev/null 2>&1 <<<"${ISSUE_LABELS:-[]}"
|
|
@@ -28,11 +24,28 @@ is_issue_number() {
|
|
|
28
24
|
classify_route() {
|
|
29
25
|
local route="none" error=""
|
|
30
26
|
local issue_number="" pr_number="" ci_conclusion="" ci_run_id=""
|
|
31
|
-
local refine_mode=""
|
|
27
|
+
local refine_mode="" triage_mode="" trigger_kind=""
|
|
32
28
|
|
|
33
29
|
case "${EVENT:-}" in
|
|
34
30
|
issues)
|
|
35
|
-
|
|
31
|
+
# A closed issue is finished work, but label edits on one still arrive as events.
|
|
32
|
+
if [ "${ISSUE_STATE:-}" = "closed" ]; then
|
|
33
|
+
error="issue is closed"
|
|
34
|
+
elif [ "${ACTION:-}" = "opened" ]; then
|
|
35
|
+
# Issue opened with a work label → skip triage.
|
|
36
|
+
# The label event will trigger authorize-bot-work → bot-working → the
|
|
37
|
+
# correct worker. Triage would only interfere.
|
|
38
|
+
if has_label refine || has_label implement; then
|
|
39
|
+
error="issue opened with a work label; triage skipped"
|
|
40
|
+
else
|
|
41
|
+
# Issue opened by an outside collaborator → triage. The authorize job
|
|
42
|
+
# gates the caller on is_outside_collaborator; this routes unconditionally
|
|
43
|
+
# so write+ openers classify to triage but the caller job skips them.
|
|
44
|
+
route="triage"
|
|
45
|
+
triage_mode="first"
|
|
46
|
+
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
47
|
+
fi
|
|
48
|
+
elif [ "${ACTION:-}" = "labeled" ]; then
|
|
36
49
|
case "${LABEL:-}" in
|
|
37
50
|
bot-working)
|
|
38
51
|
# Bot adds bot-working → route based on which work label is present
|
|
@@ -46,15 +59,25 @@ classify_route() {
|
|
|
46
59
|
route="refine"
|
|
47
60
|
refine_mode="first"
|
|
48
61
|
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
49
|
-
elif has_label direct; then
|
|
50
|
-
route="direct"
|
|
51
|
-
direct_mode="first"
|
|
52
|
-
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
53
62
|
else
|
|
54
|
-
error="bot-working added but no work label
|
|
63
|
+
error="bot-working added but no work label found"
|
|
64
|
+
fi
|
|
65
|
+
;;
|
|
66
|
+
triage)
|
|
67
|
+
# A maintainer can explicitly re-run triage by adding this label. The
|
|
68
|
+
# triage worker adds it after claiming the issue, so bot label events
|
|
69
|
+
# and an existing claim must not start a second worker.
|
|
70
|
+
if [ "${ACTOR:-}" != "" ] && echo "${ACTOR:-}" | grep -q '\[bot\]$'; then
|
|
71
|
+
error="bot-added triage label does not re-trigger triage"
|
|
72
|
+
elif has_label bot-working; then
|
|
73
|
+
error="issue already has bot-working label; triage already in progress"
|
|
74
|
+
else
|
|
75
|
+
route="triage"
|
|
76
|
+
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
77
|
+
triage_mode="first"
|
|
55
78
|
fi
|
|
56
79
|
;;
|
|
57
|
-
refine | implement
|
|
80
|
+
refine | implement)
|
|
58
81
|
# If the actor is a bot (e.g. refine→implement transition), route directly.
|
|
59
82
|
# If the actor is a human, authorize-bot-work.yml will add bot-working which triggers the workflow.
|
|
60
83
|
if [ "${ACTOR:-}" != "" ] && echo "${ACTOR:-}" | grep -q '\[bot\]$'; then
|
|
@@ -62,13 +85,11 @@ classify_route() {
|
|
|
62
85
|
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
63
86
|
if [ "${LABEL:-}" = "refine" ]; then
|
|
64
87
|
refine_mode="first"
|
|
65
|
-
elif [ "${LABEL:-}" = "direct" ]; then
|
|
66
|
-
direct_mode="first"
|
|
67
88
|
fi
|
|
68
89
|
elif has_label bot-working; then
|
|
69
90
|
# Already has bot-working - the workflow is already running or queued.
|
|
70
91
|
# Don't re-trigger.
|
|
71
|
-
error="issue already has bot-working label; implement/refine
|
|
92
|
+
error="issue already has bot-working label; implement/refine already in progress"
|
|
72
93
|
else
|
|
73
94
|
error="waiting for bot to add bot-working label"
|
|
74
95
|
fi
|
|
@@ -81,16 +102,20 @@ classify_route() {
|
|
|
81
102
|
if [ "${COMMENT_ON_PR:-false}" = "true" ]; then
|
|
82
103
|
route="apply-review"
|
|
83
104
|
pr_number="${EVENT_ISSUE_NUMBER:-}"
|
|
105
|
+
elif [ "${ISSUE_STATE:-}" = "closed" ]; then
|
|
106
|
+
# A closing comment on a refine-labelled issue used to start a full re-refine, which
|
|
107
|
+
# held a runner for 35 minutes and filed split children under an already-shut parent.
|
|
108
|
+
error="issue is closed"
|
|
84
109
|
elif [ "${COMMENT_SENDER_TYPE:-}" = "Bot" ]; then
|
|
85
110
|
error="comment authored by a bot"
|
|
111
|
+
elif has_label triage; then
|
|
112
|
+
route="triage"
|
|
113
|
+
triage_mode="retriage"
|
|
114
|
+
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
86
115
|
elif has_label implement; then
|
|
87
116
|
error="issue has implement label; comments do not re-trigger implement"
|
|
88
|
-
elif has_label direct; then
|
|
89
|
-
route="direct"
|
|
90
|
-
direct_mode="continue"
|
|
91
|
-
issue_number="${EVENT_ISSUE_NUMBER:-}"
|
|
92
117
|
elif ! has_label refine; then
|
|
93
|
-
error="issue does not carry the refine
|
|
118
|
+
error="issue does not carry the refine label"
|
|
94
119
|
else
|
|
95
120
|
route="refine"
|
|
96
121
|
refine_mode="rerefine"
|
|
@@ -118,10 +143,15 @@ classify_route() {
|
|
|
118
143
|
;;
|
|
119
144
|
|
|
120
145
|
workflow_run)
|
|
121
|
-
|
|
146
|
+
# Only a FAILED CI run on an attached pull request auto-dispatches the gate. A green
|
|
147
|
+
# run reaches merge through app-ci's dispatch-merge-gate and the reconcile belt, so
|
|
148
|
+
# routing success here would double-fire the gate for every passing pull request.
|
|
149
|
+
if [ "${RUN_CONCLUSION:-}" != "failure" ]; then
|
|
150
|
+
error="CI concluded '${RUN_CONCLUSION:-}'; the gate auto-triggers only on failure"
|
|
151
|
+
elif is_issue_number "${RUN_PR_NUMBER:-}"; then
|
|
122
152
|
route="merge-gate"
|
|
123
153
|
pr_number="${RUN_PR_NUMBER}"
|
|
124
|
-
ci_conclusion="
|
|
154
|
+
ci_conclusion="failure"
|
|
125
155
|
ci_run_id="${RUN_ID:-}"
|
|
126
156
|
else
|
|
127
157
|
error="CI run has no attached pull request"
|
|
@@ -142,19 +172,26 @@ classify_route() {
|
|
|
142
172
|
workflow_dispatch)
|
|
143
173
|
trigger_kind="manual"
|
|
144
174
|
case "${OPERATION:-}" in
|
|
145
|
-
refine | implement
|
|
175
|
+
refine | implement)
|
|
146
176
|
if is_issue_number "${INPUT_ISSUE_NUMBER:-}"; then
|
|
147
177
|
route="${OPERATION}"
|
|
148
178
|
issue_number="${INPUT_ISSUE_NUMBER}"
|
|
149
179
|
if [ "$OPERATION" = "refine" ]; then
|
|
150
180
|
refine_mode="${INPUT_MODE:-first}"
|
|
151
|
-
elif [ "$OPERATION" = "direct" ]; then
|
|
152
|
-
direct_mode="${INPUT_MODE:-first}"
|
|
153
181
|
fi
|
|
154
182
|
else
|
|
155
183
|
error="operation '${OPERATION}' needs a positive issue-number, got '${INPUT_ISSUE_NUMBER:-}'"
|
|
156
184
|
fi
|
|
157
185
|
;;
|
|
186
|
+
triage)
|
|
187
|
+
if is_issue_number "${INPUT_ISSUE_NUMBER:-}"; then
|
|
188
|
+
route="triage"
|
|
189
|
+
issue_number="${INPUT_ISSUE_NUMBER}"
|
|
190
|
+
triage_mode="${INPUT_MODE:-first}"
|
|
191
|
+
else
|
|
192
|
+
error="operation 'triage' needs a positive issue-number, got '${INPUT_ISSUE_NUMBER:-}'"
|
|
193
|
+
fi
|
|
194
|
+
;;
|
|
158
195
|
apply-review)
|
|
159
196
|
if is_issue_number "${INPUT_PR_NUMBER:-}"; then
|
|
160
197
|
route="apply-review"
|
|
@@ -173,13 +210,16 @@ classify_route() {
|
|
|
173
210
|
error="operation 'merge-gate' needs a positive pr-number, got '${INPUT_PR_NUMBER:-}'"
|
|
174
211
|
fi
|
|
175
212
|
;;
|
|
176
|
-
audit
|
|
213
|
+
audit)
|
|
177
214
|
route="${OPERATION}"
|
|
178
215
|
trigger_kind="${INPUT_TRIGGER_KIND:-manual}"
|
|
179
216
|
;;
|
|
180
|
-
|
|
217
|
+
audit-close | cleanup-artifacts | reconcile-bot-pr-runs | validate)
|
|
181
218
|
route="${OPERATION}"
|
|
182
219
|
;;
|
|
220
|
+
release)
|
|
221
|
+
route="release"
|
|
222
|
+
;;
|
|
183
223
|
*)
|
|
184
224
|
error="unknown operation '${OPERATION:-}'"
|
|
185
225
|
;;
|
|
@@ -198,7 +238,7 @@ pr-number=${pr_number}
|
|
|
198
238
|
ci-conclusion=${ci_conclusion}
|
|
199
239
|
ci-run-id=${ci_run_id}
|
|
200
240
|
refine-mode=${refine_mode}
|
|
201
|
-
|
|
241
|
+
triage-mode=${triage_mode}
|
|
202
242
|
trigger-kind=${trigger_kind}
|
|
203
243
|
error=${error}
|
|
204
244
|
EOF
|
|
@@ -34,6 +34,8 @@ runs:
|
|
|
34
34
|
number: issue.data.number,
|
|
35
35
|
title: issue.data.title,
|
|
36
36
|
body: issue.data.body,
|
|
37
|
+
author: issue.data.user.login,
|
|
38
|
+
authorAssociation: issue.data.author_association,
|
|
37
39
|
labels: issue.data.labels.map(({ name }) => name),
|
|
38
40
|
comments: comments.map(({ user, author_association, body }) => ({
|
|
39
41
|
author: user.login,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/update-changelog/action.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Update changelog
|
|
3
|
+
description: Prepend a changelog entry for a landed issue and push it to the default branch.
|
|
4
|
+
|
|
5
|
+
inputs:
|
|
6
|
+
token:
|
|
7
|
+
description: Token with contents write on the repository.
|
|
8
|
+
required: true
|
|
9
|
+
issue-number:
|
|
10
|
+
description: Issue the landed commit implements.
|
|
11
|
+
required: true
|
|
12
|
+
commit-sha:
|
|
13
|
+
description: Commit the work landed as. Its subject becomes the summary.
|
|
14
|
+
required: true
|
|
15
|
+
changelog-path:
|
|
16
|
+
description: Path to changelog.json.
|
|
17
|
+
required: false
|
|
18
|
+
default: apps/web/src/shared/data/changelog.json
|
|
19
|
+
max-entries:
|
|
20
|
+
description: Newest entries to keep.
|
|
21
|
+
required: false
|
|
22
|
+
default: "20"
|
|
23
|
+
|
|
24
|
+
runs:
|
|
25
|
+
using: composite
|
|
26
|
+
steps:
|
|
27
|
+
- name: Prepend the entry and push
|
|
28
|
+
shell: bash
|
|
29
|
+
env:
|
|
30
|
+
GH_TOKEN: ${{ inputs.token }}
|
|
31
|
+
REPO: ${{ github.repository }}
|
|
32
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
33
|
+
COMMIT_SHA: ${{ inputs.commit-sha }}
|
|
34
|
+
CHANGELOG_PATH: ${{ inputs.changelog-path }}
|
|
35
|
+
MAX_ENTRIES: ${{ inputs.max-entries }}
|
|
36
|
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
37
|
+
run: |
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
|
|
40
|
+
# This used to be part of the implement prompt. It is deterministic work, and leaving it
|
|
41
|
+
# to the agent made every implement touch the same file: two runs whose branches were cut
|
|
42
|
+
# before the other merged conflicted here, produced correct code, and still failed to open
|
|
43
|
+
# a pull request. Doing it after the merge means exactly one writer, always against a
|
|
44
|
+
# current default branch.
|
|
45
|
+
|
|
46
|
+
host="${GITHUB_SERVER_URL#https://}"
|
|
47
|
+
git config user.name "github-actions[bot]"
|
|
48
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
49
|
+
git remote set-url origin "https://x-access-token:${GH_TOKEN}@${host}/${GITHUB_REPOSITORY}.git"
|
|
50
|
+
|
|
51
|
+
git fetch --quiet origin "$DEFAULT_BRANCH"
|
|
52
|
+
git checkout --quiet -B "$DEFAULT_BRANCH" "origin/$DEFAULT_BRANCH"
|
|
53
|
+
|
|
54
|
+
if [ ! -f "$CHANGELOG_PATH" ]; then
|
|
55
|
+
echo "::notice::$CHANGELOG_PATH does not exist; nothing to update."
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Idempotent: a re-run, a retry, or a second call for the same commit must not add a
|
|
60
|
+
# duplicate row.
|
|
61
|
+
short_sha="${COMMIT_SHA:0:7}"
|
|
62
|
+
if jq -e --arg c "$short_sha" '.changes[]? | select(.commit == $c)' "$CHANGELOG_PATH" >/dev/null 2>&1; then
|
|
63
|
+
echo "::notice::$short_sha is already in the changelog."
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
title=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json title --jq '.title')
|
|
68
|
+
|
|
69
|
+
# The commit subject carries what actually changed. Strip the conventional-commit prefix
|
|
70
|
+
# and the trailing issue reference so the summary reads as prose rather than as a commit.
|
|
71
|
+
subject=$(git log -1 --format=%s "$COMMIT_SHA")
|
|
72
|
+
summary=$(printf '%s' "$subject" \
|
|
73
|
+
| sed -E 's/^[a-z]+(\([^)]*\))?!?: *//' \
|
|
74
|
+
| sed -E 's/ *\(#[0-9]+\) *$//')
|
|
75
|
+
[ -n "$summary" ] || summary="$title"
|
|
76
|
+
# Capitalise, because a commit subject conventionally starts lowercase and this is read
|
|
77
|
+
# by people rather than by tooling.
|
|
78
|
+
summary="$(printf '%s' "${summary:0:1}" | tr '[:lower:]' '[:upper:]')${summary:1}"
|
|
79
|
+
|
|
80
|
+
timestamp=$(git log -1 --format=%cI "$COMMIT_SHA")
|
|
81
|
+
|
|
82
|
+
tmp=$(mktemp)
|
|
83
|
+
jq --argjson n "$MAX_ENTRIES" \
|
|
84
|
+
--arg ts "$timestamp" \
|
|
85
|
+
--argjson issue "$ISSUE_NUMBER" \
|
|
86
|
+
--arg title "$title" \
|
|
87
|
+
--arg summary "$summary" \
|
|
88
|
+
--arg commit "$short_sha" \
|
|
89
|
+
'.changes = ([{timestamp: $ts, issue: $issue, title: $title, summary: $summary, commit: $commit}] + (.changes // []))[:$n]' \
|
|
90
|
+
"$CHANGELOG_PATH" > "$tmp"
|
|
91
|
+
mv "$tmp" "$CHANGELOG_PATH"
|
|
92
|
+
|
|
93
|
+
if git diff --quiet -- "$CHANGELOG_PATH"; then
|
|
94
|
+
echo "::notice::changelog unchanged."
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
git add -- "$CHANGELOG_PATH"
|
|
99
|
+
git commit --quiet -m "docs(changelog): record #${ISSUE_NUMBER}"
|
|
100
|
+
|
|
101
|
+
# The default branch may have moved while this ran. Rebase and retry rather than fail:
|
|
102
|
+
# only this action writes the file, so a rebase here cannot produce a content conflict.
|
|
103
|
+
for attempt in 1 2 3; do
|
|
104
|
+
if git push --quiet origin "$DEFAULT_BRANCH"; then
|
|
105
|
+
echo "Recorded #${ISSUE_NUMBER} at ${short_sha} in the changelog."
|
|
106
|
+
exit 0
|
|
107
|
+
fi
|
|
108
|
+
echo "::notice::push rejected, rebasing (attempt ${attempt})"
|
|
109
|
+
git fetch --quiet origin "$DEFAULT_BRANCH"
|
|
110
|
+
git rebase --quiet "origin/$DEFAULT_BRANCH" || { git rebase --abort || true; break; }
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
echo "::warning::Could not push the changelog entry for #${ISSUE_NUMBER}."
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-merge-gate-output/action.yml. Update with workflows update --force; consumer edits may be overwritten.
|
|
2
|
+
name: Validate merge-gate output
|
|
3
|
+
description: Verify agent_output.json contains a usable merge-gate verdict before any state changes.
|
|
4
|
+
inputs:
|
|
5
|
+
output-file:
|
|
6
|
+
description: Path to agent_output.json.
|
|
7
|
+
required: true
|
|
8
|
+
issue-number:
|
|
9
|
+
description: Issue number that the merge-gate outcome must target.
|
|
10
|
+
required: true
|
|
11
|
+
ci-conclusion:
|
|
12
|
+
description: CI conclusion supplied to the merge gate.
|
|
13
|
+
required: true
|
|
14
|
+
outputs:
|
|
15
|
+
valid:
|
|
16
|
+
description: Whether the output contains a usable merge-gate comment with a verdict.
|
|
17
|
+
value: ${{ steps.validate.outputs.valid }}
|
|
18
|
+
outcome:
|
|
19
|
+
description: "Deterministic outcome: merge, review, remediated, or invalid."
|
|
20
|
+
value: ${{ steps.validate.outputs.outcome }}
|
|
21
|
+
runs:
|
|
22
|
+
using: composite
|
|
23
|
+
steps:
|
|
24
|
+
- name: Validate merge-gate outcome
|
|
25
|
+
id: validate
|
|
26
|
+
shell: bash
|
|
27
|
+
env:
|
|
28
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
29
|
+
CI_CONCLUSION: ${{ inputs.ci-conclusion }}
|
|
30
|
+
OUTPUT_FILE: ${{ inputs.output-file }}
|
|
31
|
+
run: |
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
|
|
34
|
+
outcome="$(bash "${{ github.action_path }}/validate-merge-gate-output.sh" "$OUTPUT_FILE" "$ISSUE_NUMBER" "$CI_CONCLUSION")"
|
|
35
|
+
echo "outcome=$outcome" >> "$GITHUB_OUTPUT"
|
|
36
|
+
echo "valid=$([ "$outcome" != 'invalid' ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
|
|
37
|
+
|
|
38
|
+
if [ "$outcome" = 'invalid' ]; then
|
|
39
|
+
echo "::warning::Agent output has no usable merge-gate outcome. It will not be applied."
|
|
40
|
+
fi
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
+
# Print the deterministic merge-gate outcome: merge, review, remediated, or invalid.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
output_file="$1"
|
|
8
|
+
issue_number="$2"
|
|
9
|
+
ci_conclusion="$3"
|
|
10
|
+
|
|
11
|
+
if [ ! -f "$output_file" ] || ! jq -e '.items | arrays' "$output_file" >/dev/null 2>&1; then
|
|
12
|
+
echo invalid
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# The agent emits exactly one comment on the source issue. Its verdict tells the
|
|
17
|
+
# workflow which App-token state transition to perform.
|
|
18
|
+
jq -r --arg issue "$issue_number" --arg conclusion "$ci_conclusion" '
|
|
19
|
+
.items as $items
|
|
20
|
+
| ($items
|
|
21
|
+
| [.[] | select(.type == "add_comment" and (.item_number | tostring) == $issue and (.body | type == "string"))]
|
|
22
|
+
| map(.body |
|
|
23
|
+
if test("\\*\\*Verdict:\\*\\*\\s*(merge|review|remediated)"; "i") then
|
|
24
|
+
capture("\\*\\*Verdict:\\*\\*\\s*(?<v>merge|review|remediated)"; "i").v | ascii_downcase
|
|
25
|
+
else empty end
|
|
26
|
+
)
|
|
27
|
+
| .[0] // "invalid") as $outcome
|
|
28
|
+
| ([$items[] | select(.type == "push_to_pull_request_branch")] | length) as $pushes
|
|
29
|
+
| if $outcome == "merge" and $conclusion == "success" and $pushes == 0 then "merge"
|
|
30
|
+
elif $outcome == "remediated" and $conclusion == "failure" and $pushes == 1 then "remediated"
|
|
31
|
+
elif $outcome == "review" and $pushes == 0 then "review"
|
|
32
|
+
else "invalid"
|
|
33
|
+
end
|
|
34
|
+
' "$output_file"
|