@lead: Dev-agent runs can break later steps by touching the runtime .agents-workflow scripts checkout #34
@@ -42,6 +42,15 @@ jobs:
|
|||||||
# .gitea/workflows/scripts/) are NOT on disk yet. Check this repo out into a separate subdir
|
# .gitea/workflows/scripts/) are NOT on disk yet. Check this repo out into a separate subdir
|
||||||
# and run the scripts from $SCRIPTS. Pinned to @main to match the caller's
|
# and run the scripts from $SCRIPTS. Pinned to @main to match the caller's
|
||||||
# `uses: …/agent.yml@main`, so the scripts and the workflow always move together.
|
# `uses: …/agent.yml@main`, so the scripts and the workflow always move together.
|
||||||
|
#
|
||||||
|
# actions/checkout requires `path` to be inside the workspace, so this necessarily lands the
|
||||||
|
# clone at `.agents-workflow/` INSIDE the tree the dev agent later edits. That checkout dir is
|
||||||
|
# untracked and looks like a stray artifact: an agent that commits it as a gitlink or
|
||||||
|
# `rm -rf`s it as "leftover" would destroy the very scripts the post-agent steps run, breaking
|
||||||
|
# the run with exit 127 and stranding pushed work with no PR (issue #33). To make the run
|
||||||
|
# immune, the next step copies the scripts to a stable location OUTSIDE the workspace
|
||||||
|
# (${{ runner.temp }}) and every later step runs from $SCRIPTS there — so nothing the agent
|
||||||
|
# does to the working tree can break the run's own execution environment.
|
||||||
- name: Fetch shared agent scripts (this repo)
|
- name: Fetch shared agent scripts (this repo)
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -50,10 +59,24 @@ jobs:
|
|||||||
path: .agents-workflow
|
path: .agents-workflow
|
||||||
token: ${{ secrets.GITEA_TOKEN }}
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
|
# Copy the step scripts out of the workspace so the agent cannot break them (issue #33).
|
||||||
|
# $SCRIPTS points here for every subsequent step, NOT into the in-tree .agents-workflow/.
|
||||||
|
- name: Stage shared scripts outside the workspace
|
||||||
|
env:
|
||||||
|
SRC: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
||||||
|
DST: ${{ runner.temp }}/agents-scripts
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
rm -rf "$DST"
|
||||||
|
mkdir -p "$DST"
|
||||||
|
cp -a "$SRC"/. "$DST"/
|
||||||
|
chmod -R a+rx "$DST" || true
|
||||||
|
echo "staged $(ls -1 "$DST" | wc -l) scripts at $DST"
|
||||||
|
|
||||||
- name: Route agent + prepare branch
|
- name: Route agent + prepare branch
|
||||||
id: prep
|
id: prep
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
BODY: ${{ github.event.comment.body }} # event text via env, never inline in shell
|
BODY: ${{ github.event.comment.body }} # event text via env, never inline in shell
|
||||||
IBODY: ${{ github.event.issue.body }}
|
IBODY: ${{ github.event.issue.body }}
|
||||||
# Comment-vs-issue discriminator. Do NOT use github.event_name here: this is a REUSABLE
|
# Comment-vs-issue discriminator. Do NOT use github.event_name here: this is a REUSABLE
|
||||||
@@ -73,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install opencode + provider config (+ Playwright MCP for browser agents)
|
- name: Install opencode + provider config (+ Playwright MCP for browser agents)
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
|
OLLAMA_URL: ${{ secrets.OLLAMA_URL }}
|
||||||
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
|
OLLAMA_CLOUD_API_KEY: ${{ secrets.OLLAMA_CLOUD_API_KEY }}
|
||||||
NAME: ${{ steps.prep.outputs.name }}
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
@@ -90,7 +113,7 @@ jobs:
|
|||||||
# All three secrets are passed via env and never inlined into shell — this shared workflow
|
# All three secrets are passed via env and never inlined into shell — this shared workflow
|
||||||
# runs in repos that don't have them and must not fail there.
|
# runs in repos that don't have them and must not fail there.
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
SWARM_HOST: ${{ secrets.SWARM_HOST }}
|
SWARM_HOST: ${{ secrets.SWARM_HOST }}
|
||||||
SWARM_USER: ${{ secrets.SWARM_USER }}
|
SWARM_USER: ${{ secrets.SWARM_USER }}
|
||||||
SSH_PRIV_KEY: ${{ secrets.SSH_PRIV_KEY }}
|
SSH_PRIV_KEY: ${{ secrets.SSH_PRIV_KEY }}
|
||||||
@@ -104,14 +127,14 @@ jobs:
|
|||||||
# Only emitted when AGENT_TOKEN is actually present, so repos without it don't get a
|
# Only emitted when AGENT_TOKEN is actually present, so repos without it don't get a
|
||||||
# broken skill. The token is passed via env and never inlined into shell.
|
# broken skill. The token is passed via env and never inlined into shell.
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
||||||
run: bash "$SCRIPTS/skill-gitea-api.sh"
|
run: bash "$SCRIPTS/skill-gitea-api.sh"
|
||||||
|
|
||||||
- name: Inspect / fetch image attachments (download only for vision agents)
|
- name: Inspect / fetch image attachments (download only for vision agents)
|
||||||
id: imgs
|
id: imgs
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
GT: ${{ secrets.GITEA_TOKEN }}
|
||||||
NUM: ${{ github.event.issue.number }}
|
NUM: ${{ github.event.issue.number }}
|
||||||
VISION: ${{ steps.prep.outputs.vision }}
|
VISION: ${{ steps.prep.outputs.vision }}
|
||||||
@@ -119,7 +142,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Fetch the full issue thread (shared memory)
|
- name: Fetch the full issue thread (shared memory)
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
GT: ${{ secrets.GITEA_TOKEN }}
|
||||||
NUM: ${{ github.event.issue.number }}
|
NUM: ${{ github.event.issue.number }}
|
||||||
run: bash "$SCRIPTS/fetch-thread.sh"
|
run: bash "$SCRIPTS/fetch-thread.sh"
|
||||||
@@ -127,7 +150,7 @@ jobs:
|
|||||||
- name: Run agent
|
- name: Run agent
|
||||||
id: run
|
id: run
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
# AGENT_TOKEN powers the `gitea-api` skill (cross-repo issue/PR/Actions read+write).
|
# AGENT_TOKEN powers the `gitea-api` skill (cross-repo issue/PR/Actions read+write).
|
||||||
# It is already a required secret for the delegation step below; exposing it here too
|
# It is already a required secret for the delegation step below; exposing it here too
|
||||||
@@ -149,13 +172,13 @@ jobs:
|
|||||||
- name: Build activity log (tool calls + reasoning) from the event stream
|
- name: Build activity log (tool calls + reasoning) from the event stream
|
||||||
id: log
|
id: log
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
MODE: ${{ steps.prep.outputs.mode }}
|
MODE: ${{ steps.prep.outputs.mode }}
|
||||||
run: bash "$SCRIPTS/build-activity-log.sh"
|
run: bash "$SCRIPTS/build-activity-log.sh"
|
||||||
|
|
||||||
- name: Publish — PR (dev agents) or comment (pm), always reply in the issue
|
- name: Publish — PR (dev agents) or comment (pm), always reply in the issue
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ github.workspace }}/.agents-workflow/.gitea/workflows/scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
GT: ${{ secrets.GITEA_TOKEN }}
|
||||||
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
||||||
TOKEN_PM: ${{ secrets.TOKEN_PM }}
|
TOKEN_PM: ${{ secrets.TOKEN_PM }}
|
||||||
@@ -171,6 +194,27 @@ jobs:
|
|||||||
NEW: ${{ steps.prep.outputs.new }}
|
NEW: ${{ steps.prep.outputs.new }}
|
||||||
run: bash "$SCRIPTS/publish.sh"
|
run: bash "$SCRIPTS/publish.sh"
|
||||||
|
|
||||||
|
# Failure-safe: if any step above failed AFTER a dev agent already pushed commits, the normal
|
||||||
|
# Publish step never ran, so the work would be stranded on the branch with no PR (issue #33).
|
||||||
|
# This best-effort step opens a PR for the pushed branch so nothing is silently lost. Runs from
|
||||||
|
# $SCRIPTS (outside the workspace) so it works even if the tree was mangled by the agent.
|
||||||
|
- name: Rescue — open a PR for pushed work if the run failed
|
||||||
|
if: failure()
|
||||||
|
env:
|
||||||
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
|
GT: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
TOKEN_PM: ${{ secrets.TOKEN_PM }}
|
||||||
|
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
|
||||||
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
|
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
||||||
|
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
||||||
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
|
MODE: ${{ steps.prep.outputs.mode }}
|
||||||
|
NUM: ${{ github.event.issue.number }}
|
||||||
|
TITLE: ${{ github.event.issue.title }}
|
||||||
|
BRANCH: ${{ steps.prep.outputs.branch }}
|
||||||
|
run: bash "$SCRIPTS/rescue-pr.sh" || true
|
||||||
|
|
||||||
- name: Mark done with 🚀 (remove 👀)
|
- name: Mark done with 🚀 (remove 👀)
|
||||||
env:
|
env:
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
GT: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|||||||
@@ -98,6 +98,18 @@ if [ "$MODE" != "pr" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Scrub the runtime scripts checkout (.agents-workflow) from the tree so it never lands in a
|
||||||
|
# commit/PR and never confuses the git ops below (issue #33). The scripts we run live outside the
|
||||||
|
# workspace ($SCRIPTS -> runner.temp), so removing this in-tree copy is always safe. Handle every
|
||||||
|
# way an agent might have left it: untracked dir, tracked files, or a committed gitlink/submodule.
|
||||||
|
if git ls-files --error-unmatch .agents-workflow >/dev/null 2>&1 || \
|
||||||
|
[ -n "$(git ls-files .agents-workflow 2>/dev/null)" ]; then
|
||||||
|
git rm -r --cached --quiet --ignore-unmatch .agents-workflow 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
git config -f .gitmodules --remove-section submodule..agents-workflow 2>/dev/null || true
|
||||||
|
[ -s .gitmodules ] || rm -f .gitmodules 2>/dev/null || true
|
||||||
|
rm -rf .agents-workflow 2>/dev/null || true
|
||||||
|
|
||||||
# The agent may have committed on the starting branch AND/OR created extra
|
# The agent may have committed on the starting branch AND/OR created extra
|
||||||
# ai/issue-N-<slug> branches. Commit any leftover on the current branch, push it, then
|
# ai/issue-N-<slug> branches. Commit any leftover on the current branch, push it, then
|
||||||
# open a PR for EVERY ai/issue-N* branch that has commits beyond main.
|
# open a PR for EVERY ai/issue-N* branch that has commits beyond main.
|
||||||
|
|||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Failure-safe rescue: when a run FAILED after a dev agent already pushed commits, the normal
|
||||||
|
# Publish step never ran and the work would be stranded on the branch with no PR (issue #33).
|
||||||
|
# This opens a PR for the pushed branch so nothing is silently lost. It is strictly best-effort:
|
||||||
|
# every failure here is swallowed (the caller also appends `|| true`) so it can never itself break
|
||||||
|
# the run. Comment-only roles (pm/qa) push nothing, so they are skipped.
|
||||||
|
#
|
||||||
|
# Required env (provided by the workflow step):
|
||||||
|
# GT TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
|
||||||
|
# NAME MODE NUM TITLE BRANCH GITHUB_SERVER_URL GITHUB_REPOSITORY
|
||||||
|
set +e
|
||||||
|
|
||||||
|
# Only dev agents (mode=pr) ever push a branch to rescue.
|
||||||
|
[ "${MODE:-}" = "pr" ] || { echo "rescue: comment-mode agent, nothing to rescue"; exit 0; }
|
||||||
|
[ -n "${BRANCH:-}" ] || { echo "rescue: no branch known, skipping"; exit 0; }
|
||||||
|
|
||||||
|
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
|
||||||
|
case "$NAME" in
|
||||||
|
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
||||||
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; *) TOK="";;
|
||||||
|
esac
|
||||||
|
[ -z "$TOK" ] && TOK="$GT"
|
||||||
|
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
|
hdr=(-H "Authorization: token $TOK" -H "Content-Type: application/json")
|
||||||
|
|
||||||
|
git fetch -q origin 2>/dev/null || true
|
||||||
|
|
||||||
|
# Nothing to rescue unless the branch exists on the remote with commits beyond main.
|
||||||
|
ahead=$(git rev-list --count "origin/main..origin/$BRANCH" 2>/dev/null || echo 0)
|
||||||
|
if [ "${ahead:-0}" -eq 0 ]; then
|
||||||
|
echo "rescue: no pushed commits on origin/$BRANCH beyond main — nothing to rescue"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "rescue: origin/$BRANCH is $ahead commit(s) ahead of main — ensuring a PR exists"
|
||||||
|
|
||||||
|
# Idempotent: Gitea ignores ?head=, so match the head branch client-side.
|
||||||
|
resp=$(curl -sS "${hdr[@]}" "$API/pulls?state=open&limit=50" \
|
||||||
|
| jq -r --arg br "$BRANCH" 'if type=="array" then (map(select(.head.ref==$br)) | .[0] // empty) else empty end' 2>/dev/null)
|
||||||
|
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
||||||
|
if [ -z "$url" ]; then
|
||||||
|
body=$(printf 'The run failed before it could publish, but pushed work exists on this branch — opening a PR so it is not lost.\n\n---\nResolves #%s · 🤖 @%s (auto-rescued after a failed run)' "$NUM" "$NAME")
|
||||||
|
resp=$(curl -sS -X POST "${hdr[@]}" "$API/pulls" \
|
||||||
|
-d "$(jq -nc --arg t "@$NAME: $TITLE" --arg h "$BRANCH" --arg b "$body" \
|
||||||
|
'{title:$t, head:$h, base:"main", body:$b}')")
|
||||||
|
echo "rescue PR create ($BRANCH): $resp"
|
||||||
|
url=$(printf '%s' "$resp" | jq -r '.html_url // empty' 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$url" ]; then
|
||||||
|
curl -sS -X POST "${hdr[@]}" "$API/issues/$NUM/comments" \
|
||||||
|
-d "$(jq -nc --arg b "$(printf '🤖 **@%s** — ⚠️ the run failed, but your pushed work was not lost — a PR was opened for branch \`%s\`:\n- %s' "$NAME" "$BRANCH" "$url")" '{body:$b}')" \
|
||||||
|
-w '\nrescue comment -> HTTP %{http_code}\n' || true
|
||||||
|
else
|
||||||
|
echo "rescue: could not open/find a PR for $BRANCH"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user