@plainconceptsplatform/workflows 0.20.3 → 0.21.0
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/loops/actions/verify-route-matrix/verify-route-matrix.sh +145 -12
- package/loops/workflows/agent-implement.md +99 -6
- package/loops/workflows/agent-merge-gate.md +4 -2
- package/loops/workflows/shared/opencode-ci.md +206 -206
- package/loops/workflows/work-router.yml +2 -2
- package/package.json +1 -1
|
@@ -372,6 +372,52 @@ else
|
|
|
372
372
|
fi
|
|
373
373
|
if [ "$MIRROR_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
374
374
|
|
|
375
|
+
# Third-party action pins are the same shape of problem. Every `uses:` in a package-managed
|
|
376
|
+
# file is pinned to a full commit SHA, and one action must resolve to exactly one SHA across
|
|
377
|
+
# the whole package: the router, the workers, the shared mechanics and the composite actions
|
|
378
|
+
# are installed together, so two pins for one action mean two versions of it run in the same
|
|
379
|
+
# repository, and the older one is invisible until it breaks. That is how the merge-belt
|
|
380
|
+
# dispatchers ran create-github-app-token v2.0.6 for a release while every worker ran v3.2.0:
|
|
381
|
+
# nothing failed, nothing flagged it, and the action that receives the App's private key was
|
|
382
|
+
# the stale one. Templates are excluded: they are consumer-owned from installation and
|
|
383
|
+
# deliberately carry their own versions (the CI templates pin download-artifact v6 while the
|
|
384
|
+
# loops pin v8). A floating tag (`@v4`) is a missing pin and fails the same check.
|
|
385
|
+
PIN_OK=1
|
|
386
|
+
declare -A PIN_SEEN
|
|
387
|
+
PKG_ROOT="$(cd "${HERE}/../../.." && pwd)"
|
|
388
|
+
pin_scan() {
|
|
389
|
+
local file="$1" line action ref
|
|
390
|
+
# Resolve to an absolute path so the package-relative name in the failure reads like the
|
|
391
|
+
# source tree, not like this script's location.
|
|
392
|
+
file="$(cd "$(dirname "$file")" && pwd)/$(basename "$file")"
|
|
393
|
+
local shown="${file#"${PKG_ROOT}/"}"
|
|
394
|
+
# Anchored on the line start so a comment quoting a `uses:` cannot match, and tolerant of
|
|
395
|
+
# both spellings (`uses:` as a mapping key and `- uses:` as a list item). Local composite
|
|
396
|
+
# actions (`./.github/actions/...`) and docker refs carry no `@` and never match.
|
|
397
|
+
local pin_re='^[[:space:]]*-?[[:space:]]*uses:[[:space:]]+([A-Za-z0-9_.-]+/[A-Za-z0-9_./-]+)@([A-Za-z0-9._-]+)'
|
|
398
|
+
while IFS= read -r line; do
|
|
399
|
+
[[ "$line" =~ $pin_re ]] || continue
|
|
400
|
+
action="${BASH_REMATCH[1]}"
|
|
401
|
+
ref="${BASH_REMATCH[2]}"
|
|
402
|
+
if [ "${PIN_SEEN[$action]+set}" ] && [ "${PIN_SEEN[$action]}" != "$ref" ]; then
|
|
403
|
+
PIN_OK=0
|
|
404
|
+
echo "FAIL: ${shown} pins ${action}@${ref} but ${PIN_SEEN[$action]} is pinned elsewhere; one action, one pin" >&2
|
|
405
|
+
else
|
|
406
|
+
PIN_SEEN[$action]="$ref"
|
|
407
|
+
fi
|
|
408
|
+
if [ "${#ref}" -ne 40 ]; then
|
|
409
|
+
PIN_OK=0
|
|
410
|
+
echo "FAIL: ${shown} uses ${action}@${ref}; a tag is mutable, pin the full commit SHA" >&2
|
|
411
|
+
fi
|
|
412
|
+
done < "$file"
|
|
413
|
+
}
|
|
414
|
+
for pin_file in "$ROUTER_YML" "${WORKFLOWS_DIR}"/agent-*.md "${WORKFLOWS_DIR}"/shared/*.md \
|
|
415
|
+
"${WORKFLOWS_DIR}"/authorize-bot-work.yml "${HERE}/.."/*/action.yml; do
|
|
416
|
+
[ -f "$pin_file" ] || continue
|
|
417
|
+
pin_scan "$pin_file"
|
|
418
|
+
done
|
|
419
|
+
if [ "$PIN_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
420
|
+
|
|
375
421
|
# Run the belt's own jq, rather than reading it. The filter that picks which open pull requests
|
|
376
422
|
# the hourly reconcile job acts on was written as
|
|
377
423
|
# ((env.TRUSTED_BOTS | split(" ")) | index(.user.login) != null)
|
|
@@ -841,19 +887,16 @@ if worker_installed implement; then
|
|
|
841
887
|
PUSH_FALLBACK_OK=0
|
|
842
888
|
echo "FAIL: implement gates a no-pull-request path on code_push_failure_count, which is 0 when gh-aw files the patch as an issue" >&2
|
|
843
889
|
fi
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
#
|
|
851
|
-
|
|
852
|
-
# have been merged or duplicated.
|
|
853
|
-
if [ "$(count -cF 'env.PUSH_CONFLICT_COMMENT' "$IMPLEMENT_WORKER_MD")" -ne 1 ] ||
|
|
854
|
-
[ "$(count -cF 'env.NO_PULL_REQUEST_COMMENT' "$IMPLEMENT_WORKER_MD")" -ne 1 ]; then
|
|
890
|
+
grep -qF 'env.PUSH_CONFLICT_COMMENT' "$IMPLEMENT_WORKER_MD" || {
|
|
891
|
+
PUSH_FALLBACK_OK=0
|
|
892
|
+
echo "FAIL: implement no longer says env.PUSH_CONFLICT_COMMENT on any path" >&2
|
|
893
|
+
}
|
|
894
|
+
# Collapsing this back into whatever covers the other endings is the regression this guards: it
|
|
895
|
+
# is defined once in the env block and printed on exactly one path. The four messages it used to
|
|
896
|
+
# be paired with are asserted together under "Ways a run ends with nothing".
|
|
897
|
+
if [ "$(count -cF 'env.PUSH_CONFLICT_COMMENT' "$IMPLEMENT_WORKER_MD")" -ne 1 ]; then
|
|
855
898
|
PUSH_FALLBACK_OK=0
|
|
856
|
-
echo "FAIL: implement should print
|
|
899
|
+
echo "FAIL: implement should print the conflicted-push message on exactly one path" >&2
|
|
857
900
|
fi
|
|
858
901
|
# And the two paths must be mutually exclusive, or a conflicted push gets both comments.
|
|
859
902
|
if [ "$(count -cF "process_safe_outputs_items_succeeded != '0'" "$IMPLEMENT_WORKER_MD")" -lt 2 ] ||
|
|
@@ -1202,6 +1245,31 @@ echo "── Merge gate validator ───────────────
|
|
|
1202
1245
|
# reasoning alone, and the worker has not run in production since, so these fixtures are the only
|
|
1203
1246
|
# evidence the change is right. Executing the real script is the same technique that finally
|
|
1204
1247
|
# caught the belt's jq bug, which every reading assertion had walked past.
|
|
1248
|
+
# The gate comments on every bot pull request, so anything it writes there is written dozens of
|
|
1249
|
+
# times a week. A CODEOWNERS of `* @someone` matches every pull request, so the owners row was
|
|
1250
|
+
# naming four people on routine auto-merges and @-mentioning each of them, which notifies them.
|
|
1251
|
+
# The row belongs to the disposition that actually wants an owner, and the names go in a code
|
|
1252
|
+
# span so the comment reports who owns the area without pinging them: on owner-review the gate
|
|
1253
|
+
# requests the review properly, and that is the notification.
|
|
1254
|
+
if worker_installed merge-gate; then
|
|
1255
|
+
OWNERS_OK=1
|
|
1256
|
+
if ! grep -q "outcome == 'owner-review'" "$MERGE_GATE_WORKER_MD"; then
|
|
1257
|
+
OWNERS_OK=0
|
|
1258
|
+
echo "FAIL: the gate comment names required owners unconditionally; gate the row on owner-review" >&2
|
|
1259
|
+
fi
|
|
1260
|
+
# Every rendering of required_owners has to sit inside a code span.
|
|
1261
|
+
while IFS= read -r line; do
|
|
1262
|
+
case "$line" in
|
|
1263
|
+
*'`{0}`'*|*'`${{ needs.protected_changes.outputs.required_owners'*) continue ;;
|
|
1264
|
+
*)
|
|
1265
|
+
OWNERS_OK=0
|
|
1266
|
+
echo "FAIL: required_owners is rendered without a code span, so the gate @-mentions people: ${line# }" >&2
|
|
1267
|
+
;;
|
|
1268
|
+
esac
|
|
1269
|
+
done < <(grep -F 'required_owners' "$MERGE_GATE_WORKER_MD" | grep -vE '^\s*(#|[a-z_]+:)' | grep -F 'Required owners')
|
|
1270
|
+
if [ "$OWNERS_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1271
|
+
fi
|
|
1272
|
+
|
|
1205
1273
|
# Blast radius is the input the merge decision leans on hardest, and it is the one a reader
|
|
1206
1274
|
# cannot check by eye. These cases are the six real pull requests the redesign was measured
|
|
1207
1275
|
# against, reduced to their shape: the three that used to be parked for a person purely because
|
|
@@ -1872,6 +1940,71 @@ if worker_installed audit; then
|
|
|
1872
1940
|
if [ "$AUDIT_EMPTY_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
1873
1941
|
fi
|
|
1874
1942
|
|
|
1943
|
+
echo "── Ways a run ends with nothing ──────────────────────────────────────────"
|
|
1944
|
+
|
|
1945
|
+
# A run can end without a pull request in five ways and they need different answers. One sentence
|
|
1946
|
+
# covered all of them, and three issues in two days said "nothing landed": one agent gave up in a
|
|
1947
|
+
# minute, one did more work than the run that succeeded the same hour and never asked for a pull
|
|
1948
|
+
# request, one was told there was nothing to do. Nobody could act on any of them, and the retry
|
|
1949
|
+
# that followed was right for some and wasted on the rest.
|
|
1950
|
+
#
|
|
1951
|
+
# Asserted because collapsing two of these back together is a one-line edit that turns every run
|
|
1952
|
+
# green again while saying less.
|
|
1953
|
+
if worker_installed implement; then
|
|
1954
|
+
ENDINGS_OK=1
|
|
1955
|
+
ENDING_MESSAGES="PUSH_CONFLICT_COMMENT OUTPUT_NOT_APPLIED_COMMENT NOOP_COMMENT PATCH_WITHOUT_REQUEST_COMMENT NO_OUTPUT_COMMENT"
|
|
1956
|
+
|
|
1957
|
+
for message in $ENDING_MESSAGES; do
|
|
1958
|
+
# Defined once in the env block and printed on exactly one path. Two usages means two
|
|
1959
|
+
# branches now say the same thing, which is the collapse this guards.
|
|
1960
|
+
if [ "$(count -cF "env.${message}" "$IMPLEMENT_WORKER_MD")" -ne 1 ]; then
|
|
1961
|
+
ENDINGS_OK=0
|
|
1962
|
+
echo "FAIL: implement should print ${message} on exactly one path" >&2
|
|
1963
|
+
fi
|
|
1964
|
+
done
|
|
1965
|
+
|
|
1966
|
+
# The sentence that used to cover all five. Its text survives as NO_OUTPUT_COMMENT; the name
|
|
1967
|
+
# coming back means the branches were merged again.
|
|
1968
|
+
if [ "$(count -cF 'NO_PULL_REQUEST_COMMENT' "$IMPLEMENT_WORKER_MD")" -ne 0 ]; then
|
|
1969
|
+
ENDINGS_OK=0
|
|
1970
|
+
echo "FAIL: implement still carries NO_PULL_REQUEST_COMMENT, which said the same thing about five different endings" >&2
|
|
1971
|
+
fi
|
|
1972
|
+
|
|
1973
|
+
# What tells them apart. Both are the agent job's own outputs, and gh-aw gates on the first
|
|
1974
|
+
# itself, so losing them means guessing again.
|
|
1975
|
+
for signal in 'needs.agent.outputs.output_types' 'needs.agent.outputs.has_patch'; do
|
|
1976
|
+
if [ "$(count -cE "^ *if:.*${signal}" "$IMPLEMENT_WORKER_MD")" -eq 0 ]; then
|
|
1977
|
+
ENDINGS_OK=0
|
|
1978
|
+
echo "FAIL: implement no longer reads ${signal}, so it cannot tell its no-pull-request endings apart" >&2
|
|
1979
|
+
fi
|
|
1980
|
+
done
|
|
1981
|
+
|
|
1982
|
+
# `!x == 'true'` parses as `(!x) == 'true'` in a GitHub expression, which compares a boolean to
|
|
1983
|
+
# a string and is always false. Written wrong here first, and it would have made the
|
|
1984
|
+
# produced-nothing branch unreachable while every other branch still looked right.
|
|
1985
|
+
if [ "$(count -cF "!needs.agent.outputs.has_patch ==" "$IMPLEMENT_WORKER_MD")" -ne 0 ]; then
|
|
1986
|
+
ENDINGS_OK=0
|
|
1987
|
+
echo "FAIL: implement negates has_patch as \`!x == 'true'\`, which binds as \`(!x) == 'true'\` and never matches; use != 'true'" >&2
|
|
1988
|
+
fi
|
|
1989
|
+
|
|
1990
|
+
# A bot that reported nothing to do made a decision. The janitor retries a stalled issue, and
|
|
1991
|
+
# re-running it produces the same answer, so this one path must not be flagged.
|
|
1992
|
+
noop_block=$(awk "/name: Say there was nothing to do/{found=1} found{print} found && /View this workflow run/{exit}" "$IMPLEMENT_WORKER_MD")
|
|
1993
|
+
if [ -n "$noop_block" ] && printf '%s' "$noop_block" | grep -qF 'STALLED_LABEL'; then
|
|
1994
|
+
ENDINGS_OK=0
|
|
1995
|
+
echo "FAIL: implement flags a deliberate noop as stalled, so the janitor will retry a decision until it runs out of attempts" >&2
|
|
1996
|
+
fi
|
|
1997
|
+
|
|
1998
|
+
# The usage artifact carrying the token counts is uploaded by gh-aw's conclusion job, and that
|
|
1999
|
+
# job already depends on conclude. Naming it is a cycle and the worker stops compiling.
|
|
2000
|
+
if [ "$(count -cE "^ *needs:.*conclusion" "$IMPLEMENT_WORKER_MD")" -ne 0 ]; then
|
|
2001
|
+
ENDINGS_OK=0
|
|
2002
|
+
echo "FAIL: implement depends on the conclusion job, which depends on conclude; that cycle does not compile" >&2
|
|
2003
|
+
fi
|
|
2004
|
+
|
|
2005
|
+
if [ "$ENDINGS_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
|
|
2006
|
+
fi
|
|
2007
|
+
|
|
1875
2008
|
echo "── Runner pools ──────────────────────────────────────────────────────────"
|
|
1876
2009
|
|
|
1877
2010
|
# Where every job runs, stated once and asserted, because GitHub gives a wrong pool no error: a
|
|
@@ -16,7 +16,25 @@ env:
|
|
|
16
16
|
# re-running a decision produces the same decision. Created idempotently where it is applied.
|
|
17
17
|
STALLED_LABEL: stalled
|
|
18
18
|
PR_PENDING_LABEL: pr-pending
|
|
19
|
-
|
|
19
|
+
# Five ways a run ends with no pull request, and they are not the same thing. One sentence
|
|
20
|
+
# covered all of them, which is why three issues in two days said "nothing landed" and none of
|
|
21
|
+
# them could be acted on: one agent gave up in a minute, another did more work than the run that
|
|
22
|
+
# succeeded and never asked for a pull request, and a third was told to stop. The retry that
|
|
23
|
+
# follows is right for some and wasted on the rest. Each is one line: the compiler flattens a
|
|
24
|
+
# multi-line env value.
|
|
25
|
+
#
|
|
26
|
+
# The agent produced nothing at all. Measured on Pliny-Bot #191: the model was called, replied
|
|
27
|
+
# with almost nothing each time, and emitted no safe output.
|
|
28
|
+
NO_OUTPUT_COMMENT: "The implementation run produced nothing at all: no patch, and no request to open a pull request. That is a failed run rather than a decision, so the issue keeps `implement` and is flagged for a retry."
|
|
29
|
+
# The agent wrote the code and never asked for it to be published. Measured on Pliny-Bot #175,
|
|
30
|
+
# whose run produced more output than the run that succeeded the same hour.
|
|
31
|
+
PATCH_WITHOUT_REQUEST_COMMENT: "The implementation wrote code but never asked for a pull request, so nothing was published and the patch is only in the run. The work is not lost -- it is in the run's artifacts -- but it has to be asked for again. The issue keeps `implement` and is flagged for a retry."
|
|
32
|
+
# The agent asked, and the handler did not apply it. Different from a push that conflicted:
|
|
33
|
+
# there, gh-aw kept the patch as an issue and said so.
|
|
34
|
+
OUTPUT_NOT_APPLIED_COMMENT: "The implementation asked for a pull request and it was not applied, so the request exists in the run and nothing reached the repository. That is a failure on our side of the handoff rather than the agent's, and it is flagged for a retry."
|
|
35
|
+
# The agent was asked to do the work and reported there was none. A decision, not a failure:
|
|
36
|
+
# retrying it produces the same answer, so it is not flagged stalled and the janitor leaves it.
|
|
37
|
+
NOOP_COMMENT: "The implementation ran and reported there was nothing to do. That is an answer rather than a failure, so the issue is not queued for another attempt. If it is wrong, say what is missing and label it `implement` again."
|
|
20
38
|
# Said when the agent DID write the code and the push failed. gh-aw pushes through the GraphQL
|
|
21
39
|
# signed-commits API, which rebases onto the current parent, so a `main` that moved under a long
|
|
22
40
|
# run conflicts; gh-aw keeps the work by filing the patch as an issue rather than dropping it,
|
|
@@ -299,8 +317,83 @@ jobs:
|
|
|
299
317
|
${{ env.IMPLEMENT_MARKER }}
|
|
300
318
|
${{ env.PUSH_CONFLICT_COMMENT }}
|
|
301
319
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
302
|
-
|
|
303
|
-
|
|
320
|
+
# Which of the remaining four happened, from what the agent itself reported.
|
|
321
|
+
#
|
|
322
|
+
# `output_types` and `has_patch` are the agent job's own outputs, and gh-aw gates on the
|
|
323
|
+
# first of them itself, so they are read here rather than invented. They are also the only
|
|
324
|
+
# way to tell these apart from `conclude`: the usage artifact that carries the token counts
|
|
325
|
+
# is uploaded by `conclusion`, and `conclusion` already depends on this job, so naming it is
|
|
326
|
+
# a dependency cycle that does not compile.
|
|
327
|
+
#
|
|
328
|
+
# Ordered most specific first, and each condition excludes the ones above it, so exactly one
|
|
329
|
+
# of the four fires. Pliny-Bot #191 is the last of them and #175 is the one before it: both
|
|
330
|
+
# said "nothing landed" and neither could be acted on, while #175's run had produced more
|
|
331
|
+
# output than the run that succeeded in the same hour.
|
|
332
|
+
- name: Flag a request that was not applied
|
|
333
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && contains(needs.agent.outputs.output_types, 'create_pull_request')
|
|
334
|
+
uses: ./.github/actions/add-issue-labels
|
|
335
|
+
with:
|
|
336
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
337
|
+
issue-number: ${{ inputs.issue-number }}
|
|
338
|
+
labels: |-
|
|
339
|
+
${{ env.REVIEW_LABEL }}
|
|
340
|
+
${{ env.STALLED_LABEL }}
|
|
341
|
+
- name: Say the request was not applied
|
|
342
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && contains(needs.agent.outputs.output_types, 'create_pull_request')
|
|
343
|
+
uses: ./.github/actions/create-issue-comment
|
|
344
|
+
with:
|
|
345
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
346
|
+
issue-number: ${{ inputs.issue-number }}
|
|
347
|
+
body: |
|
|
348
|
+
${{ env.IMPLEMENT_MARKER }}
|
|
349
|
+
${{ env.OUTPUT_NOT_APPLIED_COMMENT }}
|
|
350
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
351
|
+
- name: Release an issue the agent found nothing to do on
|
|
352
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && !contains(needs.agent.outputs.output_types, 'create_pull_request') && contains(needs.agent.outputs.output_types, 'noop')
|
|
353
|
+
uses: ./.github/actions/add-issue-labels
|
|
354
|
+
with:
|
|
355
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
356
|
+
issue-number: ${{ inputs.issue-number }}
|
|
357
|
+
labels: |-
|
|
358
|
+
${{ env.REVIEW_LABEL }}
|
|
359
|
+
- name: Take the implement label off a decided issue
|
|
360
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && !contains(needs.agent.outputs.output_types, 'create_pull_request') && contains(needs.agent.outputs.output_types, 'noop')
|
|
361
|
+
uses: ./.github/actions/remove-issue-labels
|
|
362
|
+
with:
|
|
363
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
364
|
+
issue-number: ${{ inputs.issue-number }}
|
|
365
|
+
labels: ${{ env.IMPLEMENT_LABEL }}
|
|
366
|
+
- name: Say there was nothing to do
|
|
367
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && !contains(needs.agent.outputs.output_types, 'create_pull_request') && contains(needs.agent.outputs.output_types, 'noop')
|
|
368
|
+
uses: ./.github/actions/create-issue-comment
|
|
369
|
+
with:
|
|
370
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
371
|
+
issue-number: ${{ inputs.issue-number }}
|
|
372
|
+
body: |
|
|
373
|
+
${{ env.IMPLEMENT_MARKER }}
|
|
374
|
+
${{ env.NOOP_COMMENT }}
|
|
375
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
376
|
+
- name: Flag a patch nobody asked to publish
|
|
377
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch == 'true'
|
|
378
|
+
uses: ./.github/actions/add-issue-labels
|
|
379
|
+
with:
|
|
380
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
381
|
+
issue-number: ${{ inputs.issue-number }}
|
|
382
|
+
labels: |-
|
|
383
|
+
${{ env.REVIEW_LABEL }}
|
|
384
|
+
${{ env.STALLED_LABEL }}
|
|
385
|
+
- name: Say the patch was never asked for
|
|
386
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch == 'true'
|
|
387
|
+
uses: ./.github/actions/create-issue-comment
|
|
388
|
+
with:
|
|
389
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
390
|
+
issue-number: ${{ inputs.issue-number }}
|
|
391
|
+
body: |
|
|
392
|
+
${{ env.IMPLEMENT_MARKER }}
|
|
393
|
+
${{ env.PATCH_WITHOUT_REQUEST_COMMENT }}
|
|
394
|
+
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
395
|
+
- name: Flag a run that produced nothing
|
|
396
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch != 'true'
|
|
304
397
|
uses: ./.github/actions/add-issue-labels
|
|
305
398
|
with:
|
|
306
399
|
token: ${{ steps.app-token.outputs.token }}
|
|
@@ -308,15 +401,15 @@ jobs:
|
|
|
308
401
|
labels: |-
|
|
309
402
|
${{ env.REVIEW_LABEL }}
|
|
310
403
|
${{ env.STALLED_LABEL }}
|
|
311
|
-
- name: Say
|
|
312
|
-
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '')
|
|
404
|
+
- name: Say the run produced nothing
|
|
405
|
+
if: needs.safe_outputs.outputs.created_pr_number == '' && (needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '0' || needs.safe_outputs.outputs.process_safe_outputs_items_succeeded == '') && !contains(needs.agent.outputs.output_types, 'create_pull_request') && !contains(needs.agent.outputs.output_types, 'noop') && needs.agent.outputs.has_patch != 'true'
|
|
313
406
|
uses: ./.github/actions/create-issue-comment
|
|
314
407
|
with:
|
|
315
408
|
token: ${{ steps.app-token.outputs.token }}
|
|
316
409
|
issue-number: ${{ inputs.issue-number }}
|
|
317
410
|
body: |
|
|
318
411
|
${{ env.IMPLEMENT_MARKER }}
|
|
319
|
-
${{ env.
|
|
412
|
+
${{ env.NO_OUTPUT_COMMENT }}
|
|
320
413
|
[View this workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
321
414
|
incomplete:
|
|
322
415
|
needs: [agent, safe_outputs, eligibility]
|
|
@@ -285,7 +285,7 @@ jobs:
|
|
|
285
285
|
Protected files:
|
|
286
286
|
${{ needs.protected_changes.outputs.files }}
|
|
287
287
|
|
|
288
|
-
Required owners:
|
|
288
|
+
Required owners: `${{ needs.protected_changes.outputs.required_owners || 'none configured' }}`
|
|
289
289
|
|
|
290
290
|
**Verdict:** owner-review
|
|
291
291
|
|
|
@@ -439,7 +439,9 @@ jobs:
|
|
|
439
439
|
| Files / lines | ${{ needs.protected_changes.outputs.files_changed }} / ${{ needs.protected_changes.outputs.lines_changed }} |
|
|
440
440
|
| Protected paths | ${{ needs.protected_changes.outputs.requires_review == 'true' && 'yes' || 'no' }} |
|
|
441
441
|
| Owner paths | ${{ needs.protected_changes.outputs.owner_hit == 'true' && 'yes' || 'no' }} |
|
|
442
|
-
|
|
442
|
+
${{ needs.validate_output.outputs.outcome == 'owner-review'
|
|
443
|
+
&& format('| Required owners | `{0}` |', needs.protected_changes.outputs.required_owners != '' && needs.protected_changes.outputs.required_owners || 'none configured, see the matched paths above')
|
|
444
|
+
|| '' }}
|
|
443
445
|
|
|
444
446
|
Why this blast radius:
|
|
445
447
|
```
|
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
---
|
|
2
|
-
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/shared/opencode-ci.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
-
env:
|
|
4
|
-
AGENTMEMORY_VERSION: "0.9.28"
|
|
5
|
-
CODEGRAPH_VERSION: "1.6.0"
|
|
6
|
-
RTK_VERSION: "0.44.1"
|
|
7
|
-
RTK_SHA256: "986f29704469b3d1051e2474105c6c75ab8b73651068dcd61612c1fb3938ad95"
|
|
8
|
-
description: |
|
|
9
|
-
Shared CI setup for Platform agent workflows. Installs pinned tooling and merges
|
|
10
|
-
opencode.ci.json into opencode.jsonc so the CI agent gets its provider and model config.
|
|
11
|
-
|
|
12
|
-
Consumer-specific steps (NuGet, .NET restore, OpenSpec, etc.) should be added after the
|
|
13
|
-
shared baseline in the consumer copy. The merge step below is package-owned and required
|
|
14
|
-
for the agent to resolve its provider and its models.
|
|
15
|
-
|
|
16
|
-
# Consumer repositories should add stack-specific steps (NuGet cache, dotnet restore,
|
|
17
|
-
# OpenSpec, Playwright, etc.) after the shared baseline. The merge step at the end is
|
|
18
|
-
# package-owned and required for the agent to resolve its provider and models. Do not
|
|
19
|
-
# remove it.
|
|
20
|
-
pre-agent-steps:
|
|
21
|
-
- name: Create agent scratch directory
|
|
22
|
-
run: mkdir -p .opencode/.tmp
|
|
23
|
-
|
|
24
|
-
- name: Install the Docker Compose plugin
|
|
25
|
-
# The ARC runner image ships the docker CLI without the compose plugin, and awf drives its
|
|
26
|
-
# sandbox with `docker compose up`. User-space install; no root required.
|
|
27
|
-
run: |
|
|
28
|
-
set -euo pipefail
|
|
29
|
-
if docker compose version >/dev/null 2>&1; then
|
|
30
|
-
echo "compose already present"; exit 0
|
|
31
|
-
fi
|
|
32
|
-
mkdir -p "$HOME/.docker/cli-plugins"
|
|
33
|
-
curl -sSfL "https://github.com/docker/compose/releases/download/v2.39.2/docker-compose-linux-x86_64" -o "$HOME/.docker/cli-plugins/docker-compose"
|
|
34
|
-
chmod +x "$HOME/.docker/cli-plugins/docker-compose"
|
|
35
|
-
docker compose version
|
|
36
|
-
|
|
37
|
-
- name: Install ripgrep
|
|
38
|
-
run: |
|
|
39
|
-
set -euo pipefail
|
|
40
|
-
|
|
41
|
-
if command -v rg > /dev/null 2>&1; then
|
|
42
|
-
echo "ripgrep already installed: $(rg --version | head -1)"
|
|
43
|
-
exit 0
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
# ARC runner pods have no root, so ripgrep comes from its release tarball into a
|
|
47
|
-
# user-writable prefix instead of apt.
|
|
48
|
-
mkdir -p "$HOME/.local/bin"
|
|
49
|
-
curl -sSfL https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz | tar -xz --strip-components=1 -C "$HOME/.local/bin" --wildcards "*/rg"
|
|
50
|
-
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
51
|
-
rg --version
|
|
52
|
-
|
|
53
|
-
- name: Activate the pnpm version package.json pins
|
|
54
|
-
run: |
|
|
55
|
-
set -euo pipefail
|
|
56
|
-
corepack enable
|
|
57
|
-
corepack prepare --activate
|
|
58
|
-
pnpm --version
|
|
59
|
-
|
|
60
|
-
- name: Cache the pnpm store
|
|
61
|
-
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
62
|
-
with:
|
|
63
|
-
path: ~/.local/share/pnpm/store
|
|
64
|
-
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
65
|
-
restore-keys: pnpm-store-${{ runner.os }}-
|
|
66
|
-
|
|
67
|
-
- name: Install RTK
|
|
68
|
-
run: |
|
|
69
|
-
set -euo pipefail
|
|
70
|
-
|
|
71
|
-
if command -v rtk > /dev/null 2>&1 && rtk --version 2>/dev/null | grep -q "${RTK_VERSION}"; then
|
|
72
|
-
echo "RTK ${RTK_VERSION} already installed"
|
|
73
|
-
exit 0
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
tarball="$RUNNER_TEMP/rtk.tar.gz"
|
|
77
|
-
curl -fsSL -o "$tarball" \
|
|
78
|
-
"https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-x86_64-unknown-linux-musl.tar.gz"
|
|
79
|
-
echo "${RTK_SHA256} $tarball" | sha256sum --check --strict
|
|
80
|
-
|
|
81
|
-
tar -xzf "$tarball" -C "$RUNNER_TEMP"
|
|
82
|
-
mkdir -p "$HOME/.local/bin"
|
|
83
|
-
install -m 0755 "$RUNNER_TEMP/rtk" "$HOME/.local/bin/rtk"
|
|
84
|
-
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
85
|
-
|
|
86
|
-
rtk --version
|
|
87
|
-
rtk init -g --opencode --auto-patch
|
|
88
|
-
|
|
89
|
-
- name: Install agentmemory
|
|
90
|
-
run: |
|
|
91
|
-
set -euo pipefail
|
|
92
|
-
|
|
93
|
-
if command -v agentmemory > /dev/null 2>&1 && agentmemory --version 2>/dev/null | grep -q "${AGENTMEMORY_VERSION}"; then
|
|
94
|
-
echo "agentmemory ${AGENTMEMORY_VERSION} already installed"
|
|
95
|
-
exit 0
|
|
96
|
-
fi
|
|
97
|
-
|
|
98
|
-
npm install -g "@agentmemory/agentmemory@${AGENTMEMORY_VERSION}"
|
|
99
|
-
agentmemory --version
|
|
100
|
-
|
|
101
|
-
- name: Restore the CodeGraph index
|
|
102
|
-
# Ephemeral ARC runners start empty, so without this every run rebuilds the whole graph.
|
|
103
|
-
# actions/cache is repository-scoped by design, which keeps consumers isolated, and
|
|
104
|
-
# each run saves an immutable snapshot under its own key at job end (the action's built-in
|
|
105
|
-
# post step), so two concurrent runs never write the same entry: the newest snapshot simply
|
|
106
|
-
# wins the next restore. The index lives in .codegraph inside the checkout plus a small
|
|
107
|
-
# registry in ~/.codegraph.
|
|
108
|
-
uses: actions/cache@
|
|
109
|
-
with:
|
|
110
|
-
path: |
|
|
111
|
-
.codegraph
|
|
112
|
-
~/.codegraph
|
|
113
|
-
key: codegraph-${{ github.repository_id }}-${{ github.run_id }}
|
|
114
|
-
restore-keys: |
|
|
115
|
-
codegraph-${{ github.repository_id }}-
|
|
116
|
-
|
|
117
|
-
- name: Install codegraph and index the repository
|
|
118
|
-
continue-on-error: true
|
|
119
|
-
run: |
|
|
120
|
-
set -euo pipefail
|
|
121
|
-
|
|
122
|
-
if command -v codegraph > /dev/null 2>&1 && codegraph --version 2>/dev/null | grep -q "${CODEGRAPH_VERSION}"; then
|
|
123
|
-
echo "codegraph ${CODEGRAPH_VERSION} already installed"
|
|
124
|
-
else
|
|
125
|
-
npm install -g "@colbymchenry/codegraph@${CODEGRAPH_VERSION}"
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
codegraph init
|
|
129
|
-
|
|
130
|
-
- name: Install OpenSpec CLI
|
|
131
|
-
run: |
|
|
132
|
-
set -euo pipefail
|
|
133
|
-
|
|
134
|
-
if command -v openspec > /dev/null 2>&1 && openspec --version 2>/dev/null | grep -q "1.8.0"; then
|
|
135
|
-
echo "openspec 1.8.0 already installed"
|
|
136
|
-
exit 0
|
|
137
|
-
fi
|
|
138
|
-
|
|
139
|
-
npm install -g "@fission-ai/openspec@1.8.0"
|
|
140
|
-
openspec --version
|
|
141
|
-
|
|
142
|
-
# NOTE: playwright-cli and SQL Server startup steps are not part of the shared CI
|
|
143
|
-
# baseline. The agent path captures no visual evidence: it cannot reach a browser
|
|
144
|
-
# from inside the awf sandbox, and a screenshot with no baseline to compare against
|
|
145
|
-
# gated nothing. Run /ops-evidence locally when a change warrants screenshots. If
|
|
146
|
-
# you need playwright-cli or SQL Server inside the agent sandbox for other reasons,
|
|
147
|
-
# add them as consumer-specific steps after the shared baseline.
|
|
148
|
-
|
|
149
|
-
- name: Setup .NET
|
|
150
|
-
# Ephemeral VMs carry no SDK; the old host had it baked in, and GitHub-hosted images made
|
|
151
|
-
# the dependency invisible. setup-dotnet caches per run from the MS CDN.
|
|
152
|
-
uses: actions/setup-dotnet@v5
|
|
153
|
-
with:
|
|
154
|
-
dotnet-version: 10.0.x
|
|
155
|
-
env:
|
|
156
|
-
# the default /usr/share/dotnet needs root; the tool cache belongs to the runner user
|
|
157
|
-
DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet
|
|
158
|
-
|
|
159
|
-
- name: Cache NuGet packages
|
|
160
|
-
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
161
|
-
with:
|
|
162
|
-
path: ~/.nuget/packages
|
|
163
|
-
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.slnx', '**/Directory.Packages.props') }}
|
|
164
|
-
restore-keys: nuget-${{ runner.os }}-
|
|
165
|
-
|
|
166
|
-
- name: Install workspace dependencies
|
|
167
|
-
run: pnpm install --frozen-lockfile
|
|
168
|
-
|
|
169
|
-
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
170
|
-
run: |
|
|
171
|
-
set -euo pipefail
|
|
172
|
-
|
|
173
|
-
CONFIG=opencode.jsonc
|
|
174
|
-
FRAGMENT=opencode.ci.json
|
|
175
|
-
|
|
176
|
-
[ -f "$FRAGMENT" ] || { echo "::error::$FRAGMENT is missing from the checkout"; exit 1; }
|
|
177
|
-
|
|
178
|
-
# Pure JSON on purpose, not JSONC: jq cannot parse `//` comments, and a naive
|
|
179
|
-
# comment-stripper would corrupt the `http://` inside the provider's api URL.
|
|
180
|
-
jq -e . "$FRAGMENT" > /dev/null \
|
|
181
|
-
|| { echo "::error::$FRAGMENT is not valid JSON. Comments are not allowed in it."; exit 1; }
|
|
182
|
-
|
|
183
|
-
# Despite the .jsonc name, this file is committed in this repository and is read by
|
|
184
|
-
# jq below, so it must contain no comments. A single `//` line fails the merge with
|
|
185
|
-
# "Invalid numeric literal", which names neither the file nor the reason.
|
|
186
|
-
if [ -f "$CONFIG" ] && ! jq -e . "$CONFIG" > /dev/null 2>&1; then
|
|
187
|
-
echo "::error::$CONFIG is tracked and must be comment-free JSON: jq cannot parse it."
|
|
188
|
-
exit 1
|
|
189
|
-
fi
|
|
190
|
-
|
|
191
|
-
# opencode.jsonc is untracked in most repositories, so it usually does not exist here.
|
|
192
|
-
# Create it from the fragment when absent, merge when a checkout did provide one.
|
|
193
|
-
if [ -f "$CONFIG" ]; then
|
|
194
|
-
merged=$(jq -s '.[0] * .[1]' "$CONFIG" "$FRAGMENT")
|
|
195
|
-
else
|
|
196
|
-
merged=$(jq -S . "$FRAGMENT")
|
|
197
|
-
fi
|
|
198
|
-
|
|
199
|
-
printf '%s\n' "$merged" > "$CONFIG"
|
|
200
|
-
|
|
201
|
-
# gh-aw's own "Write OpenCode Config" step runs next and merges its base config with
|
|
202
|
-
# `$existing * $base`. Base wins on conflicting keys, but it defines neither `model`
|
|
203
|
-
# nor this provider, so both survive and `awf-proxy` is added alongside.
|
|
204
|
-
echo "Wrote $CONFIG from $FRAGMENT:"
|
|
205
|
-
jq -r '" model: \(.model // "unset")", " plugins: \(.plugin // [] | join(", "))", " providers: \(.provider // {} | keys | join(", "))"' "$CONFIG"
|
|
206
|
-
---
|
|
1
|
+
---
|
|
2
|
+
# Managed by @plainconceptsplatform/workflows. Source: loops/workflows/shared/opencode-ci.md. Update with `workflows update --force`; consumer edits may be overwritten.
|
|
3
|
+
env:
|
|
4
|
+
AGENTMEMORY_VERSION: "0.9.28"
|
|
5
|
+
CODEGRAPH_VERSION: "1.6.0"
|
|
6
|
+
RTK_VERSION: "0.44.1"
|
|
7
|
+
RTK_SHA256: "986f29704469b3d1051e2474105c6c75ab8b73651068dcd61612c1fb3938ad95"
|
|
8
|
+
description: |
|
|
9
|
+
Shared CI setup for Platform agent workflows. Installs pinned tooling and merges
|
|
10
|
+
opencode.ci.json into opencode.jsonc so the CI agent gets its provider and model config.
|
|
11
|
+
|
|
12
|
+
Consumer-specific steps (NuGet, .NET restore, OpenSpec, etc.) should be added after the
|
|
13
|
+
shared baseline in the consumer copy. The merge step below is package-owned and required
|
|
14
|
+
for the agent to resolve its provider and its models.
|
|
15
|
+
|
|
16
|
+
# Consumer repositories should add stack-specific steps (NuGet cache, dotnet restore,
|
|
17
|
+
# OpenSpec, Playwright, etc.) after the shared baseline. The merge step at the end is
|
|
18
|
+
# package-owned and required for the agent to resolve its provider and models. Do not
|
|
19
|
+
# remove it.
|
|
20
|
+
pre-agent-steps:
|
|
21
|
+
- name: Create agent scratch directory
|
|
22
|
+
run: mkdir -p .opencode/.tmp
|
|
23
|
+
|
|
24
|
+
- name: Install the Docker Compose plugin
|
|
25
|
+
# The ARC runner image ships the docker CLI without the compose plugin, and awf drives its
|
|
26
|
+
# sandbox with `docker compose up`. User-space install; no root required.
|
|
27
|
+
run: |
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
if docker compose version >/dev/null 2>&1; then
|
|
30
|
+
echo "compose already present"; exit 0
|
|
31
|
+
fi
|
|
32
|
+
mkdir -p "$HOME/.docker/cli-plugins"
|
|
33
|
+
curl -sSfL "https://github.com/docker/compose/releases/download/v2.39.2/docker-compose-linux-x86_64" -o "$HOME/.docker/cli-plugins/docker-compose"
|
|
34
|
+
chmod +x "$HOME/.docker/cli-plugins/docker-compose"
|
|
35
|
+
docker compose version
|
|
36
|
+
|
|
37
|
+
- name: Install ripgrep
|
|
38
|
+
run: |
|
|
39
|
+
set -euo pipefail
|
|
40
|
+
|
|
41
|
+
if command -v rg > /dev/null 2>&1; then
|
|
42
|
+
echo "ripgrep already installed: $(rg --version | head -1)"
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# ARC runner pods have no root, so ripgrep comes from its release tarball into a
|
|
47
|
+
# user-writable prefix instead of apt.
|
|
48
|
+
mkdir -p "$HOME/.local/bin"
|
|
49
|
+
curl -sSfL https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz | tar -xz --strip-components=1 -C "$HOME/.local/bin" --wildcards "*/rg"
|
|
50
|
+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
51
|
+
rg --version
|
|
52
|
+
|
|
53
|
+
- name: Activate the pnpm version package.json pins
|
|
54
|
+
run: |
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
corepack enable
|
|
57
|
+
corepack prepare --activate
|
|
58
|
+
pnpm --version
|
|
59
|
+
|
|
60
|
+
- name: Cache the pnpm store
|
|
61
|
+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
62
|
+
with:
|
|
63
|
+
path: ~/.local/share/pnpm/store
|
|
64
|
+
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
65
|
+
restore-keys: pnpm-store-${{ runner.os }}-
|
|
66
|
+
|
|
67
|
+
- name: Install RTK
|
|
68
|
+
run: |
|
|
69
|
+
set -euo pipefail
|
|
70
|
+
|
|
71
|
+
if command -v rtk > /dev/null 2>&1 && rtk --version 2>/dev/null | grep -q "${RTK_VERSION}"; then
|
|
72
|
+
echo "RTK ${RTK_VERSION} already installed"
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
tarball="$RUNNER_TEMP/rtk.tar.gz"
|
|
77
|
+
curl -fsSL -o "$tarball" \
|
|
78
|
+
"https://github.com/rtk-ai/rtk/releases/download/v${RTK_VERSION}/rtk-x86_64-unknown-linux-musl.tar.gz"
|
|
79
|
+
echo "${RTK_SHA256} $tarball" | sha256sum --check --strict
|
|
80
|
+
|
|
81
|
+
tar -xzf "$tarball" -C "$RUNNER_TEMP"
|
|
82
|
+
mkdir -p "$HOME/.local/bin"
|
|
83
|
+
install -m 0755 "$RUNNER_TEMP/rtk" "$HOME/.local/bin/rtk"
|
|
84
|
+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
85
|
+
|
|
86
|
+
rtk --version
|
|
87
|
+
rtk init -g --opencode --auto-patch
|
|
88
|
+
|
|
89
|
+
- name: Install agentmemory
|
|
90
|
+
run: |
|
|
91
|
+
set -euo pipefail
|
|
92
|
+
|
|
93
|
+
if command -v agentmemory > /dev/null 2>&1 && agentmemory --version 2>/dev/null | grep -q "${AGENTMEMORY_VERSION}"; then
|
|
94
|
+
echo "agentmemory ${AGENTMEMORY_VERSION} already installed"
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
npm install -g "@agentmemory/agentmemory@${AGENTMEMORY_VERSION}"
|
|
99
|
+
agentmemory --version
|
|
100
|
+
|
|
101
|
+
- name: Restore the CodeGraph index
|
|
102
|
+
# Ephemeral ARC runners start empty, so without this every run rebuilds the whole graph.
|
|
103
|
+
# actions/cache is repository-scoped by design, which keeps consumers isolated, and
|
|
104
|
+
# each run saves an immutable snapshot under its own key at job end (the action's built-in
|
|
105
|
+
# post step), so two concurrent runs never write the same entry: the newest snapshot simply
|
|
106
|
+
# wins the next restore. The index lives in .codegraph inside the checkout plus a small
|
|
107
|
+
# registry in ~/.codegraph.
|
|
108
|
+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
109
|
+
with:
|
|
110
|
+
path: |
|
|
111
|
+
.codegraph
|
|
112
|
+
~/.codegraph
|
|
113
|
+
key: codegraph-${{ github.repository_id }}-${{ github.run_id }}
|
|
114
|
+
restore-keys: |
|
|
115
|
+
codegraph-${{ github.repository_id }}-
|
|
116
|
+
|
|
117
|
+
- name: Install codegraph and index the repository
|
|
118
|
+
continue-on-error: true
|
|
119
|
+
run: |
|
|
120
|
+
set -euo pipefail
|
|
121
|
+
|
|
122
|
+
if command -v codegraph > /dev/null 2>&1 && codegraph --version 2>/dev/null | grep -q "${CODEGRAPH_VERSION}"; then
|
|
123
|
+
echo "codegraph ${CODEGRAPH_VERSION} already installed"
|
|
124
|
+
else
|
|
125
|
+
npm install -g "@colbymchenry/codegraph@${CODEGRAPH_VERSION}"
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
codegraph init
|
|
129
|
+
|
|
130
|
+
- name: Install OpenSpec CLI
|
|
131
|
+
run: |
|
|
132
|
+
set -euo pipefail
|
|
133
|
+
|
|
134
|
+
if command -v openspec > /dev/null 2>&1 && openspec --version 2>/dev/null | grep -q "1.8.0"; then
|
|
135
|
+
echo "openspec 1.8.0 already installed"
|
|
136
|
+
exit 0
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
npm install -g "@fission-ai/openspec@1.8.0"
|
|
140
|
+
openspec --version
|
|
141
|
+
|
|
142
|
+
# NOTE: playwright-cli and SQL Server startup steps are not part of the shared CI
|
|
143
|
+
# baseline. The agent path captures no visual evidence: it cannot reach a browser
|
|
144
|
+
# from inside the awf sandbox, and a screenshot with no baseline to compare against
|
|
145
|
+
# gated nothing. Run /ops-evidence locally when a change warrants screenshots. If
|
|
146
|
+
# you need playwright-cli or SQL Server inside the agent sandbox for other reasons,
|
|
147
|
+
# add them as consumer-specific steps after the shared baseline.
|
|
148
|
+
|
|
149
|
+
- name: Setup .NET
|
|
150
|
+
# Ephemeral VMs carry no SDK; the old host had it baked in, and GitHub-hosted images made
|
|
151
|
+
# the dependency invisible. setup-dotnet caches per run from the MS CDN.
|
|
152
|
+
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
|
|
153
|
+
with:
|
|
154
|
+
dotnet-version: 10.0.x
|
|
155
|
+
env:
|
|
156
|
+
# the default /usr/share/dotnet needs root; the tool cache belongs to the runner user
|
|
157
|
+
DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet
|
|
158
|
+
|
|
159
|
+
- name: Cache NuGet packages
|
|
160
|
+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
161
|
+
with:
|
|
162
|
+
path: ~/.nuget/packages
|
|
163
|
+
key: nuget-${{ runner.os }}-${{ hashFiles('**/*.slnx', '**/Directory.Packages.props') }}
|
|
164
|
+
restore-keys: nuget-${{ runner.os }}-
|
|
165
|
+
|
|
166
|
+
- name: Install workspace dependencies
|
|
167
|
+
run: pnpm install --frozen-lockfile
|
|
168
|
+
|
|
169
|
+
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
170
|
+
run: |
|
|
171
|
+
set -euo pipefail
|
|
172
|
+
|
|
173
|
+
CONFIG=opencode.jsonc
|
|
174
|
+
FRAGMENT=opencode.ci.json
|
|
175
|
+
|
|
176
|
+
[ -f "$FRAGMENT" ] || { echo "::error::$FRAGMENT is missing from the checkout"; exit 1; }
|
|
177
|
+
|
|
178
|
+
# Pure JSON on purpose, not JSONC: jq cannot parse `//` comments, and a naive
|
|
179
|
+
# comment-stripper would corrupt the `http://` inside the provider's api URL.
|
|
180
|
+
jq -e . "$FRAGMENT" > /dev/null \
|
|
181
|
+
|| { echo "::error::$FRAGMENT is not valid JSON. Comments are not allowed in it."; exit 1; }
|
|
182
|
+
|
|
183
|
+
# Despite the .jsonc name, this file is committed in this repository and is read by
|
|
184
|
+
# jq below, so it must contain no comments. A single `//` line fails the merge with
|
|
185
|
+
# "Invalid numeric literal", which names neither the file nor the reason.
|
|
186
|
+
if [ -f "$CONFIG" ] && ! jq -e . "$CONFIG" > /dev/null 2>&1; then
|
|
187
|
+
echo "::error::$CONFIG is tracked and must be comment-free JSON: jq cannot parse it."
|
|
188
|
+
exit 1
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# opencode.jsonc is untracked in most repositories, so it usually does not exist here.
|
|
192
|
+
# Create it from the fragment when absent, merge when a checkout did provide one.
|
|
193
|
+
if [ -f "$CONFIG" ]; then
|
|
194
|
+
merged=$(jq -s '.[0] * .[1]' "$CONFIG" "$FRAGMENT")
|
|
195
|
+
else
|
|
196
|
+
merged=$(jq -S . "$FRAGMENT")
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
printf '%s\n' "$merged" > "$CONFIG"
|
|
200
|
+
|
|
201
|
+
# gh-aw's own "Write OpenCode Config" step runs next and merges its base config with
|
|
202
|
+
# `$existing * $base`. Base wins on conflicting keys, but it defines neither `model`
|
|
203
|
+
# nor this provider, so both survive and `awf-proxy` is added alongside.
|
|
204
|
+
echo "Wrote $CONFIG from $FRAGMENT:"
|
|
205
|
+
jq -r '" model: \(.model // "unset")", " plugins: \(.plugin // [] | join(", "))", " providers: \(.provider // {} | keys | join(", "))"' "$CONFIG"
|
|
206
|
+
---
|
|
@@ -698,7 +698,7 @@ jobs:
|
|
|
698
698
|
persist-credentials: false
|
|
699
699
|
- name: Generate App token
|
|
700
700
|
id: app-token
|
|
701
|
-
uses: actions/create-github-app-token@
|
|
701
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
702
702
|
with:
|
|
703
703
|
app-id: ${{ secrets.BOT_APP_ID }}
|
|
704
704
|
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|
|
@@ -896,7 +896,7 @@ jobs:
|
|
|
896
896
|
persist-credentials: false
|
|
897
897
|
- name: Generate App token
|
|
898
898
|
id: app-token
|
|
899
|
-
uses: actions/create-github-app-token@
|
|
899
|
+
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
900
900
|
with:
|
|
901
901
|
app-id: ${{ secrets.BOT_APP_ID }}
|
|
902
902
|
private-key: ${{ secrets.BOT_PRIVATE_KEY }}
|