@plainconceptsplatform/workflows 0.5.1 → 0.6.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/action-validation.test.d.ts +1 -0
- package/dist/action-validation.test.js +87 -0
- package/dist/catalog-installation.d.ts +30 -0
- package/dist/catalog-installation.js +422 -0
- package/dist/catalog-installation.test.d.ts +1 -0
- package/dist/catalog-installation.test.js +485 -0
- package/dist/catalog-listing.d.ts +13 -0
- package/dist/catalog-listing.js +70 -0
- package/dist/catalog-listing.test.d.ts +1 -0
- package/dist/catalog-listing.test.js +150 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +218 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +273 -0
- package/dist/repository-inspection.d.ts +23 -0
- package/dist/repository-inspection.js +113 -0
- package/dist/repository-inspection.test.d.ts +1 -0
- package/dist/repository-inspection.test.js +77 -0
- package/dist/route-processing.d.ts +6 -0
- package/dist/route-processing.js +149 -0
- package/dist/route-processing.test.d.ts +1 -0
- package/dist/route-processing.test.js +283 -0
- package/dist/stack-defaults.d.ts +11 -0
- package/dist/stack-defaults.js +104 -0
- package/dist/stack-defaults.test.d.ts +1 -0
- package/dist/stack-defaults.test.js +266 -0
- package/dist/tui.d.ts +25 -0
- package/dist/tui.js +287 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +249 -0
- package/dist/workflow-catalog.d.ts +25 -0
- package/dist/workflow-catalog.js +55 -0
- package/dist/workflow-catalog.test.d.ts +1 -0
- package/dist/workflow-catalog.test.js +29 -0
- package/loops/actions/add-issue-labels/action.yml +2 -2
- package/loops/actions/apply-agent-bundle/apply-bundle.sh +14 -1
- package/loops/actions/classify-route/action.yml +7 -0
- package/loops/actions/classify-route/classify-route.sh +20 -5
- package/loops/actions/cleanup-artifacts/action.yml +38 -12
- package/loops/actions/identify-gate-subject/action.yml +3 -1
- package/loops/actions/remove-issue-labels/action.yml +4 -2
- package/loops/actions/validate-refine-output/validate-refine-output.sh +18 -2
- package/loops/actions/verify-refine-output/verify-refine-output.sh +20 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +300 -16
- package/loops/scripts/compile-agent-workflows.mjs +96 -1
- package/loops/scripts/merge-changelog.mjs +76 -0
- package/loops/templates/agentics/actionlint.yaml +13 -0
- package/loops/templates/agentics/agentics-checks.yml +203 -86
- package/loops/templates/ci/app-ci-dotnet-next.yml +64 -2
- package/loops/templates/ci/app-ci-node-monorepo.yml +52 -0
- package/loops/templates/opencode/opencode.ci.json +2 -2
- package/loops/templates/opencode/opencode.ci.json.md +4 -1
- package/loops/workflows/agent-apply-review.md +5 -1
- package/loops/workflows/agent-implement.md +105 -3
- package/loops/workflows/agent-merge-gate.md +139 -25
- package/loops/workflows/agent-refine.md +26 -2
- package/loops/workflows/work-router.yml +227 -51
- package/package.json +1 -1
|
@@ -1,86 +1,203 @@
|
|
|
1
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
-
name: Agentics checks
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
pull_request:
|
|
6
|
-
paths:
|
|
7
|
-
- .github/workflows/*.md
|
|
8
|
-
- .github/workflows/shared/*.md
|
|
9
|
-
- .github/workflows/*.yml
|
|
10
|
-
- .github/workflows/aw.json
|
|
11
|
-
- .github/actions/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/templates/agentics/agentics-checks.yml. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
2
|
+
name: Agentics checks
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
pull_request:
|
|
6
|
+
paths:
|
|
7
|
+
- .github/workflows/*.md
|
|
8
|
+
- .github/workflows/shared/*.md
|
|
9
|
+
- .github/workflows/*.yml
|
|
10
|
+
- .github/workflows/aw.json
|
|
11
|
+
- .github/actions/**
|
|
12
|
+
# A push to the default branch that bypassed review is exactly the case that needs the check:
|
|
13
|
+
# seven broken locks reached Pliny-Bot's main in twenty minutes with no pull request and no
|
|
14
|
+
# run of this workflow. A red run here is the detection.
|
|
15
|
+
push:
|
|
16
|
+
branches: [main]
|
|
17
|
+
paths:
|
|
18
|
+
- .github/workflows/*.md
|
|
19
|
+
- .github/workflows/shared/*.md
|
|
20
|
+
- .github/workflows/*.yml
|
|
21
|
+
- .github/workflows/aw.json
|
|
22
|
+
- .github/actions/**
|
|
23
|
+
|
|
24
|
+
env:
|
|
25
|
+
# Must match the version the locks were compiled with (first line of every .lock.yml) and the
|
|
26
|
+
# `github/gh-aw/...@vX` import pin in every worker. A different compiler regenerates every lock
|
|
27
|
+
# and the freshness diff measures the version, not the change under review.
|
|
28
|
+
GH_AW_VERSION: v0.87.5
|
|
29
|
+
ACTIONLINT_VERSION: 1.7.7
|
|
30
|
+
ACTIONLINT_SHA256: 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757
|
|
31
|
+
SHELLCHECK_VERSION: 0.10.0
|
|
32
|
+
SHELLCHECK_SHA256: 6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87
|
|
33
|
+
# The same script a developer runs. It wraps `gh aw compile --strict` and applies the
|
|
34
|
+
# repository's post-processing; a bare `gh aw compile` on this side would regenerate every lock
|
|
35
|
+
# without the transform, and the diff below could never be clean.
|
|
36
|
+
AGENTICS_COMPILE_COMMAND: node scripts/compile-agent-workflows.mjs
|
|
37
|
+
|
|
38
|
+
permissions:
|
|
39
|
+
contents: read
|
|
40
|
+
|
|
41
|
+
concurrency:
|
|
42
|
+
group: agentics-checks-${{ github.event.pull_request.number || github.ref }}
|
|
43
|
+
cancel-in-progress: true
|
|
44
|
+
|
|
45
|
+
jobs:
|
|
46
|
+
verify-lockfiles:
|
|
47
|
+
name: Verify generated agent lockfiles
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
timeout-minutes: 10
|
|
50
|
+
env:
|
|
51
|
+
# gh-aw resolves the `github/gh-aw/...` imports through the GitHub API on every compile,
|
|
52
|
+
# and an unauthenticated gh on a hosted runner refuses API calls.
|
|
53
|
+
GH_TOKEN: ${{ github.token }}
|
|
54
|
+
steps:
|
|
55
|
+
- name: Checkout repository
|
|
56
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
57
|
+
with:
|
|
58
|
+
persist-credentials: false
|
|
59
|
+
- name: Install gh-aw
|
|
60
|
+
shell: bash
|
|
61
|
+
env:
|
|
62
|
+
GH_TOKEN: ${{ github.token }}
|
|
63
|
+
run: |
|
|
64
|
+
set -euo pipefail
|
|
65
|
+
gh extension install github/gh-aw --pin "$GH_AW_VERSION"
|
|
66
|
+
gh aw --version
|
|
67
|
+
# The compile command is a node script. Hosted images have node; a setup step pins the
|
|
68
|
+
# major version so the script sees the same runtime everywhere.
|
|
69
|
+
- name: Set up Node
|
|
70
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
71
|
+
with:
|
|
72
|
+
node-version: 22
|
|
73
|
+
- name: Verify the locks and the workers agree on the gh-aw version
|
|
74
|
+
shell: bash
|
|
75
|
+
run: |
|
|
76
|
+
set -euo pipefail
|
|
77
|
+
# `gh aw --version` writes to stderr; without 2>&1 the grep saw nothing and set -e
|
|
78
|
+
# ended the step with no message at all.
|
|
79
|
+
want="$(gh aw --version 2>&1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -n 1 || true)"
|
|
80
|
+
[ "$want" = "$GH_AW_VERSION" ] || { echo "::error::gh aw reports '${want:-nothing}', this workflow pins $GH_AW_VERSION"; exit 1; }
|
|
81
|
+
status=0
|
|
82
|
+
for lock in .github/workflows/*.lock.yml; do
|
|
83
|
+
have="$(sed -n '1s/.*"compiler_version":"\([^"]*\)".*/\1/p' "$lock")"
|
|
84
|
+
if [ "$have" != "$want" ]; then
|
|
85
|
+
echo "::error::$lock was compiled by ${have:-an unknown version}; this job runs $want. Recompile with the pinned version, or bump GH_AW_VERSION and the worker import pins together."
|
|
86
|
+
status=1
|
|
87
|
+
fi
|
|
88
|
+
done
|
|
89
|
+
while read -r file pin; do
|
|
90
|
+
if [ "$pin" != "$want" ]; then
|
|
91
|
+
echo "::error::$file imports gh-aw shared files at $pin; this job runs $want."
|
|
92
|
+
status=1
|
|
93
|
+
fi
|
|
94
|
+
done < <(grep -HoE 'github/gh-aw/[^@]*@v[0-9]+\.[0-9]+\.[0-9]+' .github/workflows/*.md | sed -E 's/^([^:]*):.*@(v[0-9.]+)$/\1 \2/' | sort -u)
|
|
95
|
+
exit "$status"
|
|
96
|
+
- name: Compile agent workflows strictly
|
|
97
|
+
run: ${{ env.AGENTICS_COMPILE_COMMAND }}
|
|
98
|
+
- name: Verify generated lockfiles are current
|
|
99
|
+
shell: bash
|
|
100
|
+
run: |
|
|
101
|
+
set -euo pipefail
|
|
102
|
+
if git diff --exit-code -- .github/workflows/*.lock.yml; then
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
if git diff --exit-code -I '^[[:space:]]*GH_AW_INFO_MODEL_COSTS:' -- .github/workflows/*.lock.yml; then
|
|
106
|
+
echo "::warning::Only volatile GH_AW_INFO_MODEL_COSTS metadata changed."
|
|
107
|
+
exit 0
|
|
108
|
+
fi
|
|
109
|
+
echo "::error::Generated agent lockfiles are stale. Run '${AGENTICS_COMPILE_COMMAND}' and commit the resulting .lock.yml files."
|
|
110
|
+
git diff -- .github/workflows/*.lock.yml
|
|
111
|
+
exit 1
|
|
112
|
+
# gh aw compile leaves a lock alone when its hashes still match the source, so the script
|
|
113
|
+
# must recognise a lock it already processed. If a second run changes anything, the
|
|
114
|
+
# post-processing is not idempotent and every consumer's freshness check is measuring the
|
|
115
|
+
# transform instead of the drift.
|
|
116
|
+
- name: Verify a second compile changes nothing
|
|
117
|
+
shell: bash
|
|
118
|
+
run: |
|
|
119
|
+
set -euo pipefail
|
|
120
|
+
cp -R .github/workflows "$RUNNER_TEMP/first-pass"
|
|
121
|
+
${AGENTICS_COMPILE_COMMAND}
|
|
122
|
+
if ! diff -r -I '^[[:space:]]*GH_AW_INFO_MODEL_COSTS:' "$RUNNER_TEMP/first-pass" .github/workflows; then
|
|
123
|
+
echo "::error::Running '${AGENTICS_COMPILE_COMMAND}' twice produced different locks; the post-processing is not idempotent."
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
lint-workflows:
|
|
128
|
+
name: Lint authored workflows and shell
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
timeout-minutes: 10
|
|
131
|
+
steps:
|
|
132
|
+
- name: Checkout repository
|
|
133
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
134
|
+
with:
|
|
135
|
+
persist-credentials: false
|
|
136
|
+
- name: Install actionlint
|
|
137
|
+
shell: bash
|
|
138
|
+
run: |
|
|
139
|
+
set -euo pipefail
|
|
140
|
+
tarball="$RUNNER_TEMP/actionlint.tar.gz"
|
|
141
|
+
curl -fsSL -o "$tarball" "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
|
142
|
+
echo "${ACTIONLINT_SHA256} $tarball" | sha256sum --check --strict
|
|
143
|
+
mkdir -p "$RUNNER_TEMP/bin"
|
|
144
|
+
tar -xzf "$tarball" -C "$RUNNER_TEMP/bin" actionlint
|
|
145
|
+
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
|
146
|
+
# Pinned rather than taken from the image: a copy of this job that ran on a self-hosted
|
|
147
|
+
# runner died with "shellcheck: No such file or directory".
|
|
148
|
+
- name: Install shellcheck
|
|
149
|
+
shell: bash
|
|
150
|
+
run: |
|
|
151
|
+
set -euo pipefail
|
|
152
|
+
tarball="$RUNNER_TEMP/shellcheck.tar.xz"
|
|
153
|
+
curl -fsSL -o "$tarball" "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz"
|
|
154
|
+
echo "${SHELLCHECK_SHA256} $tarball" | sha256sum --check --strict
|
|
155
|
+
mkdir -p "$RUNNER_TEMP/bin"
|
|
156
|
+
tar -xJf "$tarball" -C "$RUNNER_TEMP/bin" --strip-components=1 "shellcheck-v${SHELLCHECK_VERSION}/shellcheck"
|
|
157
|
+
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
|
|
158
|
+
# actionlint 1.7.7 predates `concurrency.queue`, which GitHub accepts and the router uses;
|
|
159
|
+
# that one message is a known false positive and is ignored by its exact text so anything
|
|
160
|
+
# else under `concurrency` still fails. shellcheck style notes (SC2129 and friends) are
|
|
161
|
+
# opinions about consumer scripts, not defects; warnings and errors still fail the job.
|
|
162
|
+
- name: Lint authored workflows
|
|
163
|
+
shell: bash
|
|
164
|
+
env:
|
|
165
|
+
SHELLCHECK_OPTS: -S warning
|
|
166
|
+
run: |
|
|
167
|
+
set -euo pipefail
|
|
168
|
+
mapfile -t authored < <(git ls-files '.github/workflows/*.yml' | grep -vE '\.lock\.yml$|agentics-maintenance\.yml$')
|
|
169
|
+
if [ "${#authored[@]}" -gt 0 ]; then
|
|
170
|
+
actionlint -ignore 'unexpected key "queue" for "concurrency" section' "${authored[@]}"
|
|
171
|
+
fi
|
|
172
|
+
- name: Lint composite action shell
|
|
173
|
+
shell: bash
|
|
174
|
+
run: find .github/actions -name '*.sh' -print0 | xargs -0 -r shellcheck -x
|
|
175
|
+
# A password written into a workflow is a finding whatever it protects, and one sat in
|
|
176
|
+
# these files for weeks: a throwaway SQL container credential, repeated inline in six
|
|
177
|
+
# to nine places per repository, found by a policy scan rather than by us. Anything a
|
|
178
|
+
# job needs at container-start time belongs in an expression or a secret, and this is
|
|
179
|
+
# the check that says so before a reviewer has to.
|
|
180
|
+
- name: No written-down passwords in workflows
|
|
181
|
+
shell: bash
|
|
182
|
+
run: |
|
|
183
|
+
set -euo pipefail
|
|
184
|
+
mapfile -t authored < <(git ls-files '.github/workflows/*.yml' '.github/actions/**' 2>/dev/null || true)
|
|
185
|
+
[ "${#authored[@]}" -gt 0 ] || exit 0
|
|
186
|
+
# Two passes, because "is this a literal" is the hard half. The first finds every
|
|
187
|
+
# assignment to a password-ish name whose value is quoted; the second keeps only
|
|
188
|
+
# the lines with no dollar sign, so a value built from an expression or read from a
|
|
189
|
+
# shell variable passes and a written-down word does not. A literal that happens to
|
|
190
|
+
# contain a dollar sign slips through; erring that way beats a check people learn to
|
|
191
|
+
# route around.
|
|
192
|
+
# Two shapes: a password-ish name assigned a quoted value, and a command-line
|
|
193
|
+
# flag given one. The literal these repositories carried appeared as both.
|
|
194
|
+
hits=$(grep -nEi \
|
|
195
|
+
-e "(password|passwd|pwd)[\"']?[[:space:]]*[:=][[:space:]]*[\"'][^\"']" \
|
|
196
|
+
-e "(^|[[:space:]])(-P|--password)[[:space:]=]*[\"'][^\"']" \
|
|
197
|
+
"${authored[@]}" | grep -v '[$]' || true)
|
|
198
|
+
if [ -n "$hits" ]; then
|
|
199
|
+
printf '%s\n' "$hits"
|
|
200
|
+
echo "::error::A password is written into a workflow. Use a secret, or derive it per run from an expression."
|
|
201
|
+
exit 1
|
|
202
|
+
fi
|
|
203
|
+
echo "No written-down passwords."
|
|
@@ -33,7 +33,17 @@ env:
|
|
|
33
33
|
SEED_PROJECT: src/App.Seed
|
|
34
34
|
PRIMARY_DATABASE: app
|
|
35
35
|
DATA_LAYER_DATABASE: numa_dl
|
|
36
|
-
|
|
36
|
+
# Derived per run rather than written down. The container is ephemeral, bound to a random
|
|
37
|
+
# host port and reachable only from this job, so the value protects nothing, but a literal
|
|
38
|
+
# password in a config file is a policy finding wherever it sits, and this file is public.
|
|
39
|
+
# It has to be a workflow-level expression: service containers receive their environment
|
|
40
|
+
# before any step runs, so a value generated in a step would arrive too late.
|
|
41
|
+
#
|
|
42
|
+
# The same expression is repeated inside the service's `options` above rather than read
|
|
43
|
+
# from here, because the `env` context is not available in `jobs.<id>.services` — only
|
|
44
|
+
# inside that service's own `env:`. Written as ${{ env.SQL_SA_PASSWORD }} there, the health
|
|
45
|
+
# check probes with an empty password and the service never reports healthy.
|
|
46
|
+
SQL_SA_PASSWORD: "Aa1!${{ github.run_id }}"
|
|
37
47
|
WEB_PACKAGE: "@app/web"
|
|
38
48
|
NPM_REGISTRY_URL: "//npm.pkg.github.com/"
|
|
39
49
|
TRUFFLEHOG_IMAGE: "trufflesecurity/trufflehog:latest"
|
|
@@ -66,7 +76,7 @@ jobs:
|
|
|
66
76
|
# exit code 1". Steps read the assigned port from the job.services context.
|
|
67
77
|
- 1433
|
|
68
78
|
options: >-
|
|
69
|
-
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '
|
|
79
|
+
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'Aa1!${{ github.run_id }}' -Q 'SELECT 1' -C || exit 1"
|
|
70
80
|
--health-interval 10s --health-timeout 5s --health-retries 12 --health-start-period 30s
|
|
71
81
|
defaults:
|
|
72
82
|
run:
|
|
@@ -266,3 +276,55 @@ jobs:
|
|
|
266
276
|
with:
|
|
267
277
|
format: cyclonedx-json
|
|
268
278
|
artifact-name: ${{ env.SBOM_ARTIFACT_NAME }}
|
|
279
|
+
|
|
280
|
+
# A bot pull request is opened with GITHUB_TOKEN, and GitHub delivers no workflow_run event
|
|
281
|
+
# for the CI runs that follow (the run's actor is the bot). The router's CI-completion route
|
|
282
|
+
# never sees them, so this job hands the verdict over from inside CI. workflow_dispatch is
|
|
283
|
+
# one of the two events GITHUB_TOKEN may raise, so no App token is needed here, and the
|
|
284
|
+
# router treats a dispatch as trusted. Human pull requests are left to the workflow_run
|
|
285
|
+
# route, which does fire for them. The reconcile belt remains the hourly fallback.
|
|
286
|
+
#
|
|
287
|
+
# If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
|
|
288
|
+
# `actions: write`. A called workflow may not request a scope its caller withheld, and the
|
|
289
|
+
# whole run fails to start with zero jobs and no annotation when it does.
|
|
290
|
+
dispatch-merge-gate:
|
|
291
|
+
name: Dispatch merge gate
|
|
292
|
+
needs: [api, mutation-api, web, mutation-web]
|
|
293
|
+
if: >
|
|
294
|
+
always() &&
|
|
295
|
+
github.event_name == 'pull_request' &&
|
|
296
|
+
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
297
|
+
github.event.pull_request.draft == false &&
|
|
298
|
+
github.event.pull_request.user.type == 'Bot'
|
|
299
|
+
runs-on: agents-arc
|
|
300
|
+
timeout-minutes: 5
|
|
301
|
+
permissions:
|
|
302
|
+
contents: read
|
|
303
|
+
actions: write
|
|
304
|
+
steps:
|
|
305
|
+
- name: Dispatch the merge gate with this run's verdict
|
|
306
|
+
env:
|
|
307
|
+
GH_TOKEN: ${{ github.token }}
|
|
308
|
+
REPO: ${{ github.repository }}
|
|
309
|
+
REF: ${{ github.event.repository.default_branch }}
|
|
310
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
311
|
+
RUN_ID: ${{ github.run_id }}
|
|
312
|
+
RESULTS: ${{ join(needs.*.result, ' ') }}
|
|
313
|
+
run: |
|
|
314
|
+
set -euo pipefail
|
|
315
|
+
conclusion=success
|
|
316
|
+
for result in $RESULTS; do
|
|
317
|
+
case "$result" in
|
|
318
|
+
failure) conclusion=failure; break ;;
|
|
319
|
+
cancelled | timed_out) conclusion=cancelled ;;
|
|
320
|
+
esac
|
|
321
|
+
done
|
|
322
|
+
# A cancelled run was superseded by a newer push, whose own run dispatches.
|
|
323
|
+
if [ "$conclusion" = cancelled ]; then
|
|
324
|
+
echo "CI was cancelled; not dispatching the merge gate."
|
|
325
|
+
exit 0
|
|
326
|
+
fi
|
|
327
|
+
echo "Dispatching the merge gate for PR #$PR_NUMBER with CI $conclusion (run $RUN_ID)."
|
|
328
|
+
gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
|
|
329
|
+
-f operation=merge-gate -f pr-number="$PR_NUMBER" \
|
|
330
|
+
-f ci-conclusion="$conclusion" -f ci-run-id="$RUN_ID"
|
|
@@ -206,3 +206,55 @@ jobs:
|
|
|
206
206
|
with:
|
|
207
207
|
format: cyclonedx-json
|
|
208
208
|
artifact-name: ${{ env.SBOM_ARTIFACT_NAME }}
|
|
209
|
+
|
|
210
|
+
# A bot pull request is opened with GITHUB_TOKEN, and GitHub delivers no workflow_run event
|
|
211
|
+
# for the CI runs that follow (the run's actor is the bot). The router's CI-completion route
|
|
212
|
+
# never sees them, so this job hands the verdict over from inside CI. workflow_dispatch is
|
|
213
|
+
# one of the two events GITHUB_TOKEN may raise, so no App token is needed here, and the
|
|
214
|
+
# router treats a dispatch as trusted. Human pull requests are left to the workflow_run
|
|
215
|
+
# route, which does fire for them. The reconcile belt remains the hourly fallback.
|
|
216
|
+
#
|
|
217
|
+
# If a deploy pipeline calls this workflow with `uses:`, that caller must also grant
|
|
218
|
+
# `actions: write`. A called workflow may not request a scope its caller withheld, and the
|
|
219
|
+
# whole run fails to start with zero jobs and no annotation when it does.
|
|
220
|
+
dispatch-merge-gate:
|
|
221
|
+
name: Dispatch merge gate
|
|
222
|
+
needs: [lint, web, mutation-web, e2e, desktop, mobile]
|
|
223
|
+
if: >
|
|
224
|
+
always() &&
|
|
225
|
+
github.event_name == 'pull_request' &&
|
|
226
|
+
github.event.pull_request.head.repo.full_name == github.repository &&
|
|
227
|
+
github.event.pull_request.draft == false &&
|
|
228
|
+
github.event.pull_request.user.type == 'Bot'
|
|
229
|
+
runs-on: agents-arc
|
|
230
|
+
timeout-minutes: 5
|
|
231
|
+
permissions:
|
|
232
|
+
contents: read
|
|
233
|
+
actions: write
|
|
234
|
+
steps:
|
|
235
|
+
- name: Dispatch the merge gate with this run's verdict
|
|
236
|
+
env:
|
|
237
|
+
GH_TOKEN: ${{ github.token }}
|
|
238
|
+
REPO: ${{ github.repository }}
|
|
239
|
+
REF: ${{ github.event.repository.default_branch }}
|
|
240
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
241
|
+
RUN_ID: ${{ github.run_id }}
|
|
242
|
+
RESULTS: ${{ join(needs.*.result, ' ') }}
|
|
243
|
+
run: |
|
|
244
|
+
set -euo pipefail
|
|
245
|
+
conclusion=success
|
|
246
|
+
for result in $RESULTS; do
|
|
247
|
+
case "$result" in
|
|
248
|
+
failure) conclusion=failure; break ;;
|
|
249
|
+
cancelled | timed_out) conclusion=cancelled ;;
|
|
250
|
+
esac
|
|
251
|
+
done
|
|
252
|
+
# A cancelled run was superseded by a newer push, whose own run dispatches.
|
|
253
|
+
if [ "$conclusion" = cancelled ]; then
|
|
254
|
+
echo "CI was cancelled; not dispatching the merge gate."
|
|
255
|
+
exit 0
|
|
256
|
+
fi
|
|
257
|
+
echo "Dispatching the merge gate for PR #$PR_NUMBER with CI $conclusion (run $RUN_ID)."
|
|
258
|
+
gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
|
|
259
|
+
-f operation=merge-gate -f pr-number="$PR_NUMBER" \
|
|
260
|
+
-f ci-conclusion="$conclusion" -f ci-run-id="$RUN_ID"
|
|
@@ -17,7 +17,10 @@ repository root as `opencode.ci.json`.
|
|
|
17
17
|
### Provider
|
|
18
18
|
|
|
19
19
|
- The `forge` provider is OpenAI-compatible and reads its endpoint from
|
|
20
|
-
`
|
|
20
|
+
`OPENAI_BASE_URL` and its key from `OPENAI_API_KEY` at runtime. Those are the
|
|
21
|
+
two variables every worker's agent job sets (`engine.env` in the worker and
|
|
22
|
+
the `OPENAI_API_KEY` secret), so the file works with no consumer wiring. An
|
|
23
|
+
earlier template read `FORGE_API_URL`/`FORGE_API_KEY`, which nothing set.
|
|
21
24
|
- Each consumer configures its own router endpoint, credential, model IDs, and
|
|
22
25
|
capabilities. The workflow package does not assume a particular gateway
|
|
23
26
|
host, authentication system, or model vendor.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/agent-apply-review.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
3
|
env:
|
|
4
|
+
# Printed by the prompt as the verification block. Consumers set their own commands.
|
|
5
|
+
VERIFY_COMMANDS: ""
|
|
4
6
|
REPO_RULES: "Apply only actionable outstanding reviewer feedback to the selected bot pull request. Make minimal changes that address each comment. Preserve architecture and do not weaken tests. Run full verification after changes."
|
|
5
7
|
WORKING_LABEL: bot-working
|
|
6
8
|
REVIEW_LABEL: review
|
|
@@ -231,7 +233,9 @@ jobs:
|
|
|
231
233
|
with:
|
|
232
234
|
token: ${{ steps.app-token.outputs.token }}
|
|
233
235
|
issue-number: ${{ needs.subject.outputs.issue }}
|
|
234
|
-
labels:
|
|
236
|
+
labels: |
|
|
237
|
+
${{ env.WORKING_LABEL }}
|
|
238
|
+
${{ env.PR_PENDING_LABEL }}
|
|
235
239
|
incomplete:
|
|
236
240
|
needs: [subject, agent, safe_outputs, validate_output]
|
|
237
241
|
if: >
|
|
@@ -12,7 +12,20 @@ env:
|
|
|
12
12
|
GIT_COMMITTER_NAME: "github-actions[bot]"
|
|
13
13
|
GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com"
|
|
14
14
|
IMPLEMENT_MARKER: "<!-- agent-implement -->"
|
|
15
|
-
|
|
15
|
+
ATTEMPT_MARKER: "<!-- agent-implement-attempt -->"
|
|
16
|
+
# The model provider fails in bursts: the same model answers "not found" or 401 for a minute
|
|
17
|
+
# and works again immediately after, and a run that dies that way used to burn the issue and
|
|
18
|
+
# hand it to a human. Retry those, and give up on the fifth, which is an outage not a blip.
|
|
19
|
+
MAX_ATTEMPTS: "5"
|
|
20
|
+
PARK_AT_ATTEMPT: "4"
|
|
21
|
+
# Only a run that died before it could do any work is worth repeating. A provider failure
|
|
22
|
+
# kills the run in a couple of minutes with no answer; a run that worked for half an hour and
|
|
23
|
+
# then failed produced an answer that was wrong, and repeating it costs the whole fleet the
|
|
24
|
+
# same half hour to be wrong again. Observed: "Model not found" died in seconds, while a run
|
|
25
|
+
# whose own build failed to compile had spent 182 turns, and an out-of-memory kill came after
|
|
26
|
+
# a full verification suite.
|
|
27
|
+
RETRY_UNDER_MINUTES: "6"
|
|
28
|
+
INCOMPLETE_COMMENT: "Automated implementation ran and ended without an outcome. The issue is released and flagged for review: a run that got this far and still failed will fail the same way again."
|
|
16
29
|
ISSUE_CONTEXT_PATH: /tmp/gh-aw/agent/implementation-context.json
|
|
17
30
|
GH_AW_ALLOWED_BOTS: "platform-devbox[bot],github-actions[bot]"
|
|
18
31
|
description: |
|
|
@@ -42,6 +55,11 @@ on:
|
|
|
42
55
|
description: Issue number to implement.
|
|
43
56
|
required: true
|
|
44
57
|
type: string
|
|
58
|
+
attempts_so_far:
|
|
59
|
+
description: Failed implement runs already made for this issue. Parked when it reaches the cap.
|
|
60
|
+
required: false
|
|
61
|
+
type: string
|
|
62
|
+
default: '0'
|
|
45
63
|
jobs:
|
|
46
64
|
eligibility:
|
|
47
65
|
runs-on: agents-arc
|
|
@@ -202,6 +220,8 @@ jobs:
|
|
|
202
220
|
permissions:
|
|
203
221
|
contents: read
|
|
204
222
|
issues: write
|
|
223
|
+
# the retry re-enters through the router, which is a workflow_dispatch
|
|
224
|
+
actions: write
|
|
205
225
|
steps:
|
|
206
226
|
- name: Checkout workflow actions
|
|
207
227
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
@@ -213,19 +233,90 @@ jobs:
|
|
|
213
233
|
with:
|
|
214
234
|
client-id: ${{ secrets.BOT_APP_ID }}
|
|
215
235
|
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
236
|
+
- name: Decide whether this failure is worth repeating
|
|
237
|
+
id: decide
|
|
238
|
+
env:
|
|
239
|
+
GH_TOKEN: ${{ github.token }}
|
|
240
|
+
REPO: ${{ github.repository }}
|
|
241
|
+
RUN_ID: ${{ github.run_id }}
|
|
242
|
+
ATTEMPTS: ${{ inputs.attempts_so_far || '0' }}
|
|
243
|
+
PARK_AT: ${{ env.PARK_AT_ATTEMPT }}
|
|
244
|
+
UNDER_MINUTES: ${{ env.RETRY_UNDER_MINUTES }}
|
|
245
|
+
run: |
|
|
246
|
+
set -euo pipefail
|
|
247
|
+
# The agent job belongs to this same run: a called workflow shares the caller's run id.
|
|
248
|
+
read -r started finished <<<"$(gh api "repos/$REPO/actions/runs/$RUN_ID/jobs?per_page=100" \
|
|
249
|
+
--jq '[.jobs[] | select(.name | endswith("agent"))] | last // empty
|
|
250
|
+
| "\(.started_at // "") \(.completed_at // "")"')"
|
|
251
|
+
minutes=-1
|
|
252
|
+
if [ -n "${started:-}" ] && [ -n "${finished:-}" ]; then
|
|
253
|
+
minutes=$(( ( $(date -u -d "$finished" +%s) - $(date -u -d "$started" +%s) ) / 60 ))
|
|
254
|
+
fi
|
|
255
|
+
retry=false
|
|
256
|
+
# An unknown duration is treated as a long run: never retry on a guess.
|
|
257
|
+
if [ "$minutes" -ge 0 ] && [ "$minutes" -lt "$UNDER_MINUTES" ] && [ "$ATTEMPTS" -lt "$PARK_AT" ]; then
|
|
258
|
+
retry=true
|
|
259
|
+
fi
|
|
260
|
+
{
|
|
261
|
+
echo "retry=$retry"
|
|
262
|
+
echo "next=$((ATTEMPTS + 1))"
|
|
263
|
+
echo "minutes=$minutes"
|
|
264
|
+
} >> "$GITHUB_OUTPUT"
|
|
265
|
+
echo "agent job ran for ${minutes}m; attempts so far ${ATTEMPTS}; retry=${retry}"
|
|
266
|
+
# The attempt is recorded before any label moves, so a failure in the steps below leaves a
|
|
267
|
+
# run that can be counted rather than an issue released with nothing to show for it.
|
|
268
|
+
- name: Report the failed attempt
|
|
269
|
+
if: steps.decide.outputs.retry == 'true'
|
|
270
|
+
uses: ./.github/actions/create-issue-comment
|
|
271
|
+
with:
|
|
272
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
273
|
+
issue-number: ${{ inputs.issue-number }}
|
|
274
|
+
body: |
|
|
275
|
+
${{ env.ATTEMPT_MARKER }}
|
|
276
|
+
Attempt ${{ steps.decide.outputs.next }} of ${{ env.MAX_ATTEMPTS }} ended after ${{ steps.decide.outputs.minutes }} minutes, before the run could produce an answer. That is what a provider outage looks like, so this is being retried.
|
|
277
|
+
The issue keeps `implement`.
|
|
278
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
279
|
+
- name: Release the reservation for the retry
|
|
280
|
+
if: steps.decide.outputs.retry == 'true'
|
|
281
|
+
uses: ./.github/actions/remove-issue-labels
|
|
282
|
+
with:
|
|
283
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
284
|
+
issue-number: ${{ inputs.issue-number }}
|
|
285
|
+
labels: ${{ env.WORKING_LABEL }}
|
|
286
|
+
- name: Send the issue back through the router
|
|
287
|
+
if: steps.decide.outputs.retry == 'true'
|
|
288
|
+
env:
|
|
289
|
+
GH_TOKEN: ${{ github.token }}
|
|
290
|
+
REPO: ${{ github.repository }}
|
|
291
|
+
REF: ${{ github.event.repository.default_branch }}
|
|
292
|
+
ISSUE_NUMBER: ${{ inputs.issue-number }}
|
|
293
|
+
NEXT: ${{ steps.decide.outputs.next }}
|
|
294
|
+
run: |
|
|
295
|
+
set -euo pipefail
|
|
296
|
+
# The provider recovers in seconds, so pause before re-entering rather than dispatching
|
|
297
|
+
# back into the same outage. The router's own classify and authorize jobs add more.
|
|
298
|
+
sleep 30
|
|
299
|
+
gh workflow run work-router.yml --repo "$REPO" --ref "$REF" \
|
|
300
|
+
-f operation=implement -f issue-number="$ISSUE_NUMBER" -f attempts_so_far="$NEXT"
|
|
301
|
+
echo "Re-dispatched implement for #$ISSUE_NUMBER as attempt $NEXT."
|
|
216
302
|
- name: Release the selected issue
|
|
303
|
+
if: steps.decide.outputs.retry != 'true'
|
|
217
304
|
uses: ./.github/actions/remove-issue-labels
|
|
218
305
|
with:
|
|
219
306
|
token: ${{ steps.app-token.outputs.token }}
|
|
220
307
|
issue-number: ${{ inputs.issue-number }}
|
|
221
|
-
labels:
|
|
308
|
+
labels: |
|
|
309
|
+
${{ env.WORKING_LABEL }}
|
|
310
|
+
implement
|
|
222
311
|
- name: Flag for human review
|
|
312
|
+
if: steps.decide.outputs.retry != 'true'
|
|
223
313
|
uses: ./.github/actions/add-issue-labels
|
|
224
314
|
with:
|
|
225
315
|
token: ${{ steps.app-token.outputs.token }}
|
|
226
316
|
issue-number: ${{ inputs.issue-number }}
|
|
227
317
|
labels: ${{ env.REVIEW_LABEL }}
|
|
228
318
|
- name: Report missing implementation outcome
|
|
319
|
+
if: steps.decide.outputs.retry != 'true'
|
|
229
320
|
uses: ./.github/actions/create-issue-comment
|
|
230
321
|
with:
|
|
231
322
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -425,7 +516,8 @@ timeout-minutes: 90
|
|
|
425
516
|
expects you to use good judgment. If two approaches are equally valid, pick one and
|
|
426
517
|
proceed. You can always iterate based on PR feedback.
|
|
427
518
|
|
|
428
|
-
4. Verify before you conclude. From the
|
|
519
|
+
4. Verify before you conclude, running only what your change can affect. From the
|
|
520
|
+
repository root:
|
|
429
521
|
|
|
430
522
|
**Scoped verification.** This runner has limited memory, and a whole-repo lint or build
|
|
431
523
|
can be killed mid-run. Scope verification to the files you actually changed first, and
|
|
@@ -442,6 +534,10 @@ timeout-minutes: 90
|
|
|
442
534
|
${{ env.VERIFY_COMMANDS }}
|
|
443
535
|
```
|
|
444
536
|
|
|
537
|
+
Run only the parts your change can affect, and none of them for a change that touches
|
|
538
|
+
only documentation. A cold Release build takes minutes on a shared runner, and running
|
|
539
|
+
it for a change that never left the front end is time the run does not get back.
|
|
540
|
+
|
|
445
541
|
If a check fails, fix the cause and rerun. Do not weaken a test, lower a threshold, or skip
|
|
446
542
|
a check to make it pass. After all checks pass, run the project's lint fix command (e.g.
|
|
447
543
|
`pnpm lint:fix` or `pnpm exec biome check --write <changed-files>`) to auto-format the
|
|
@@ -475,6 +571,12 @@ timeout-minutes: 90
|
|
|
475
571
|
safeoutputs/create_pull_request(title="[bot] Fix X", body="Closes #${{ inputs.issue-number }}\n\n...", branch="fix/x")
|
|
476
572
|
```
|
|
477
573
|
|
|
574
|
+
Send it complete, first time. Each of these has an allowance of one call per run, and a
|
|
575
|
+
call that fails still spends it: a short payload sent to find out what the tool accepts
|
|
576
|
+
can come back a success, take the allowance with it, and leave the real call refused as
|
|
577
|
+
over the limit. That has happened, and the run ends having done all the work and
|
|
578
|
+
published none of it. Do not probe, and do not send a partial payload to test the shape.
|
|
579
|
+
|
|
478
580
|
Choose exactly one:
|
|
479
581
|
|
|
480
582
|
- **`safeoutputs/create_pull_request`** , propose a pull request against `main` with
|