fix: feat: gardener defers all repo actions to a manifest — review gate covers grooming decisions, not just docs (#572)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c9bf9fe528
commit
7ecf372e40
3 changed files with 237 additions and 86 deletions
|
|
@ -37,6 +37,9 @@ Set up the working environment for this gardener run.
|
||||||
3. Record the current HEAD SHA for AGENTS.md watermarks:
|
3. Record the current HEAD SHA for AGENTS.md watermarks:
|
||||||
HEAD_SHA=$(git rev-parse HEAD)
|
HEAD_SHA=$(git rev-parse HEAD)
|
||||||
echo "$HEAD_SHA" > /tmp/gardener-head-sha
|
echo "$HEAD_SHA" > /tmp/gardener-head-sha
|
||||||
|
|
||||||
|
4. Initialize the pending-actions manifest (JSONL, converted to JSON at commit time):
|
||||||
|
printf '' > "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -108,16 +111,10 @@ Sibling dependency rule (CRITICAL):
|
||||||
Read AGENTS.md and extract the AD table. For each backlog issue,
|
Read AGENTS.md and extract the AD table. For each backlog issue,
|
||||||
compare the issue title and body against each AD. If an issue
|
compare the issue title and body against each AD. If an issue
|
||||||
clearly violates an AD:
|
clearly violates an AD:
|
||||||
a. Post a comment explaining the violation:
|
a. Write a comment action to the manifest:
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
echo '{"action":"comment","issue":NNN,"body":"Closing: violates AD-NNN (<decision summary>). See AGENTS.md § Architecture Decisions."}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
-H "Content-Type: application/json" \
|
b. Write a close action to the manifest:
|
||||||
"$CODEBERG_API/issues/<number>/comments" \
|
echo '{"action":"close","issue":NNN,"reason":"violates AD-NNN"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
-d '{"body":"Closing: violates AD-NNN (<decision summary>). See AGENTS.md § Architecture Decisions."}'
|
|
||||||
b. Close the issue:
|
|
||||||
curl -sf -X PATCH -H "Authorization: token $CODEBERG_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"$CODEBERG_API/issues/<number>" \
|
|
||||||
-d '{"state":"closed"}'
|
|
||||||
c. Log to the result file:
|
c. Log to the result file:
|
||||||
echo "ACTION: closed #NNN — violates AD-NNN" >> "$RESULT_FILE"
|
echo "ACTION: closed #NNN — violates AD-NNN" >> "$RESULT_FILE"
|
||||||
|
|
||||||
|
|
@ -134,20 +131,13 @@ Sibling dependency rule (CRITICAL):
|
||||||
"## Affected files" section with at least one file path
|
"## Affected files" section with at least one file path
|
||||||
|
|
||||||
If either section is missing:
|
If either section is missing:
|
||||||
a. Look up the 'backlog' label ID:
|
a. Write a comment action to the manifest:
|
||||||
BACKLOG_LABEL_ID=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
echo '{"action":"comment","issue":NNN,"body":"This issue is missing required sections. Please use the issue templates at `.codeberg/ISSUE_TEMPLATE/` — needs: <missing items>."}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
"$CODEBERG_API/labels" | jq -r '.[] | select(.name == "backlog") | .id')
|
|
||||||
b. Post a comment listing what's missing:
|
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"$CODEBERG_API/issues/<number>/comments" \
|
|
||||||
-d '{"body":"This issue is missing required sections. Please use the issue templates at `.codeberg/ISSUE_TEMPLATE/` — needs: <missing items>."}'
|
|
||||||
Where <missing items> is a comma-separated list of what's absent
|
Where <missing items> is a comma-separated list of what's absent
|
||||||
(e.g. "acceptance criteria, affected files" or just "affected files").
|
(e.g. "acceptance criteria, affected files" or just "affected files").
|
||||||
c. Remove the 'backlog' label:
|
b. Write a remove_label action to the manifest:
|
||||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
echo '{"action":"remove_label","issue":NNN,"label":"backlog"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
"$CODEBERG_API/issues/<number>/labels/$BACKLOG_LABEL_ID"
|
c. Log to the result file:
|
||||||
d. Log to the result file:
|
|
||||||
echo "ACTION: stripped backlog from #NNN — missing: <missing items>" >> "$RESULT_FILE"
|
echo "ACTION: stripped backlog from #NNN — missing: <missing items>" >> "$RESULT_FILE"
|
||||||
|
|
||||||
Well-structured issues (both sections present) are left untouched —
|
Well-structured issues (both sections present) are left untouched —
|
||||||
|
|
@ -203,16 +193,11 @@ session, so changes there would be lost.
|
||||||
jq -r '[.group, (.issue | tostring)] | join("\\t")' "$DUST_FILE" | sort -u | cut -f1 | sort | uniq -c | sort -rn
|
jq -r '[.group, (.issue | tostring)] | join("\\t")' "$DUST_FILE" | sort -u | cut -f1 | sort | uniq -c | sort -rn
|
||||||
b. For each group with count >= 3:
|
b. For each group with count >= 3:
|
||||||
- Collect issue details and distinct issue numbers for the group
|
- Collect issue details and distinct issue numbers for the group
|
||||||
- Look up the backlog label ID:
|
- Write a create_issue action to the manifest:
|
||||||
BACKLOG_LABEL_ID=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
echo '{"action":"create_issue","title":"fix: bundled dust cleanup — GROUP","body":"...","labels":["backlog"]}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
"$CODEBERG_API/labels" | jq -r '.[] | select(.name == "backlog") | .id')
|
- Write comment + close actions for each source issue:
|
||||||
- Create a bundled backlog issue:
|
echo '{"action":"comment","issue":NNN,"body":"Bundled into dust cleanup issue for GROUP"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
echo '{"action":"close","issue":NNN,"reason":"bundled into dust cleanup for GROUP"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
-H "Content-Type: application/json" "$CODEBERG_API/issues" \
|
|
||||||
-d '{"title":"fix: bundled dust cleanup — GROUP","body":"...","labels":[LABEL_ID]}'
|
|
||||||
- Close each source issue with a cross-reference comment:
|
|
||||||
curl ... "$CODEBERG_API/issues/NNN/comments" -d '{"body":"Bundled into #NEW"}'
|
|
||||||
curl ... "$CODEBERG_API/issues/NNN" -d '{"state":"closed"}'
|
|
||||||
- Remove bundled items from dust.jsonl:
|
- Remove bundled items from dust.jsonl:
|
||||||
jq -c --arg g "GROUP" 'select(.group != $g)' "$DUST_FILE" > "${DUST_FILE}.tmp" && mv "${DUST_FILE}.tmp" "$DUST_FILE"
|
jq -c --arg g "GROUP" 'select(.group != $g)' "$DUST_FILE" > "${DUST_FILE}.tmp" && mv "${DUST_FILE}.tmp" "$DUST_FILE"
|
||||||
|
|
||||||
|
|
@ -233,57 +218,42 @@ description = """
|
||||||
Review all issues labeled 'blocked' and decide their fate.
|
Review all issues labeled 'blocked' and decide their fate.
|
||||||
(See issue #352 for the blocked label convention.)
|
(See issue #352 for the blocked label convention.)
|
||||||
|
|
||||||
1. Look up the 'blocked' label ID (Gitea needs integer IDs for label removal):
|
1. Fetch all blocked issues:
|
||||||
BLOCKED_LABEL_ID=$(curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
|
||||||
"$CODEBERG_API/labels" | jq -r '.[] | select(.name == "blocked") | .id')
|
|
||||||
If the lookup fails, skip label removal and just post comments.
|
|
||||||
|
|
||||||
2. Fetch all blocked issues:
|
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||||
"$CODEBERG_API/issues?state=open&type=issues&labels=blocked&limit=50"
|
"$CODEBERG_API/issues?state=open&type=issues&labels=blocked&limit=50"
|
||||||
|
|
||||||
3. For each blocked issue, read the full body and comments:
|
2. For each blocked issue, read the full body and comments:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<number>"
|
"$CODEBERG_API/issues/<number>"
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<number>/comments"
|
"$CODEBERG_API/issues/<number>/comments"
|
||||||
|
|
||||||
4. Check dependencies — extract issue numbers from ## Dependencies /
|
3. Check dependencies — extract issue numbers from ## Dependencies /
|
||||||
## Depends on / ## Blocked by sections. For each dependency:
|
## Depends on / ## Blocked by sections. For each dependency:
|
||||||
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
curl -sf -H "Authorization: token $CODEBERG_TOKEN" \
|
||||||
"$CODEBERG_API/issues/<dep_number>"
|
"$CODEBERG_API/issues/<dep_number>"
|
||||||
Check if the dependency is now closed.
|
Check if the dependency is now closed.
|
||||||
|
|
||||||
5. For each blocked issue, choose ONE action:
|
4. For each blocked issue, choose ONE action:
|
||||||
|
|
||||||
UNBLOCK — all dependencies are now closed or the blocking condition resolved:
|
UNBLOCK — all dependencies are now closed or the blocking condition resolved:
|
||||||
a. Remove the 'blocked' label (using ID from step 1):
|
a. Write a remove_label action to the manifest:
|
||||||
curl -sf -X DELETE -H "Authorization: token $CODEBERG_TOKEN" \
|
echo '{"action":"remove_label","issue":NNN,"label":"blocked"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
"$CODEBERG_API/issues/<number>/labels/$BLOCKED_LABEL_ID"
|
b. Write a comment action to the manifest:
|
||||||
b. Add context comment explaining what changed:
|
echo '{"action":"comment","issue":NNN,"body":"Unblocked: <explanation of what resolved the blocker>"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"$CODEBERG_API/issues/<number>/comments" \
|
|
||||||
-d '{"body":"Unblocked: <explanation of what resolved the blocker>"}'
|
|
||||||
|
|
||||||
NEEDS HUMAN — blocking condition is ambiguous, requires architectural
|
NEEDS HUMAN — blocking condition is ambiguous, requires architectural
|
||||||
decision, or involves external factors:
|
decision, or involves external factors:
|
||||||
a. Post a diagnostic comment explaining what you found and what
|
a. Write a comment action to the manifest:
|
||||||
decision is needed
|
echo '{"action":"comment","issue":NNN,"body":"<diagnostic: what you found and what decision is needed>"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
b. Leave the 'blocked' label in place
|
b. Leave the 'blocked' label in place
|
||||||
|
|
||||||
CLOSE — issue is stale (blocked 30+ days with no progress on blocker),
|
CLOSE — issue is stale (blocked 30+ days with no progress on blocker),
|
||||||
the blocker is wontfix, or the issue is no longer relevant:
|
the blocker is wontfix, or the issue is no longer relevant:
|
||||||
a. Post a comment explaining why:
|
a. Write a comment action to the manifest:
|
||||||
curl -sf -X POST -H "Authorization: token $CODEBERG_TOKEN" \
|
echo '{"action":"comment","issue":NNN,"body":"Closing: <reason — stale blocker, no longer relevant, etc.>"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
-H "Content-Type: application/json" \
|
b. Write a close action to the manifest:
|
||||||
"$CODEBERG_API/issues/<number>/comments" \
|
echo '{"action":"close","issue":NNN,"reason":"<stale blocker / no longer relevant / etc.>"}' >> "$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
-d '{"body":"Closing: <reason — stale blocker, no longer relevant, etc.>"}'
|
|
||||||
b. Close the issue:
|
|
||||||
curl -sf -X PATCH -H "Authorization: token $CODEBERG_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"$CODEBERG_API/issues/<number>" \
|
|
||||||
-d '{"state":"closed"}'
|
|
||||||
|
|
||||||
CRITICAL: If this step fails, log the failure and move on.
|
CRITICAL: If this step fails, log the failure and move on.
|
||||||
"""
|
"""
|
||||||
|
|
@ -421,49 +391,63 @@ needs = ["blocked-review"]
|
||||||
id = "commit-and-pr"
|
id = "commit-and-pr"
|
||||||
title = "One commit with all file changes, push, create PR, monitor to merge"
|
title = "One commit with all file changes, push, create PR, monitor to merge"
|
||||||
description = """
|
description = """
|
||||||
Collect all file changes from this run (AGENTS.md updates) into a single commit.
|
Collect all file changes from this run (AGENTS.md updates + pending-actions
|
||||||
API calls (issue creation, PR comments, closures) already happened during the
|
manifest) into a single commit. All repo mutation API calls (comments, closures,
|
||||||
run — only file changes need the PR.
|
label changes, issue creation) are deferred to the manifest — the orchestrator
|
||||||
|
executes them after the PR merges.
|
||||||
|
|
||||||
1. Check for staged or unstaged changes:
|
1. Convert the JSONL manifest to a JSON array:
|
||||||
cd "$PROJECT_REPO_ROOT"
|
cd "$PROJECT_REPO_ROOT"
|
||||||
|
JSONL_FILE="$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl"
|
||||||
|
JSON_FILE="$PROJECT_REPO_ROOT/gardener/pending-actions.json"
|
||||||
|
if [ -s "$JSONL_FILE" ]; then
|
||||||
|
jq -s '.' "$JSONL_FILE" > "$JSON_FILE"
|
||||||
|
else
|
||||||
|
echo '[]' > "$JSON_FILE"
|
||||||
|
fi
|
||||||
|
rm -f "$JSONL_FILE"
|
||||||
|
|
||||||
|
2. Check for staged or unstaged changes:
|
||||||
git status --porcelain
|
git status --porcelain
|
||||||
|
|
||||||
If there are no file changes, skip to step 3 — no commit, no PR needed.
|
If there are no file changes (no AGENTS.md updates AND manifest is empty []),
|
||||||
|
skip to step 4 — no commit, no PR needed.
|
||||||
|
|
||||||
2. If there are changes:
|
3. If there are changes:
|
||||||
a. Create a branch:
|
a. Create a branch:
|
||||||
BRANCH="chore/gardener-$(date -u +%Y%m%d-%H%M)"
|
BRANCH="chore/gardener-$(date -u +%Y%m%d-%H%M)"
|
||||||
git checkout -B "$BRANCH"
|
git checkout -B "$BRANCH"
|
||||||
b. Stage all modified AGENTS.md files:
|
b. Stage all modified AGENTS.md files:
|
||||||
find . -name "AGENTS.md" -not -path "./.git/*" -exec git add {} +
|
find . -name "AGENTS.md" -not -path "./.git/*" -exec git add {} +
|
||||||
c. Also stage any other files the gardener modified (if any):
|
c. Stage the pending-actions manifest:
|
||||||
|
git add gardener/pending-actions.json
|
||||||
|
d. Also stage any other files the gardener modified (if any):
|
||||||
git add -u
|
git add -u
|
||||||
d. Commit:
|
e. Commit:
|
||||||
git commit -m "chore: gardener housekeeping $(date -u +%Y-%m-%d)"
|
git commit -m "chore: gardener housekeeping $(date -u +%Y-%m-%d)"
|
||||||
e. Push:
|
f. Push:
|
||||||
git push -u origin "$BRANCH"
|
git push -u origin "$BRANCH"
|
||||||
f. Create a PR:
|
g. Create a PR:
|
||||||
PR_RESPONSE=$(curl -sf -X POST \
|
PR_RESPONSE=$(curl -sf -X POST \
|
||||||
-H "Authorization: token $CODEBERG_TOKEN" \
|
-H "Authorization: token $CODEBERG_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
"$CODEBERG_API/pulls" \
|
"$CODEBERG_API/pulls" \
|
||||||
-d '{"title":"chore: gardener housekeeping",
|
-d '{"title":"chore: gardener housekeeping",
|
||||||
"head":"'"$BRANCH"'","base":"'"$PRIMARY_BRANCH"'",
|
"head":"'"$BRANCH"'","base":"'"$PRIMARY_BRANCH"'",
|
||||||
"body":"Automated gardener housekeeping — AGENTS.md updates.\\n\\nReview-agent fast-tracks doc-only PRs."}')
|
"body":"Automated gardener housekeeping — AGENTS.md updates + pending actions manifest.\\n\\nReview `gardener/pending-actions.json` for proposed grooming actions (label changes, closures, comments). These execute after merge."}')
|
||||||
PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.number')
|
PR_NUMBER=$(echo "$PR_RESPONSE" | jq -r '.number')
|
||||||
g. Save PR number for orchestrator tracking:
|
h. Save PR number for orchestrator tracking:
|
||||||
echo "$PR_NUMBER" > /tmp/gardener-pr-${PROJECT_NAME}.txt
|
echo "$PR_NUMBER" > /tmp/gardener-pr-${PROJECT_NAME}.txt
|
||||||
h. Signal the orchestrator to monitor CI:
|
i. Signal the orchestrator to monitor CI:
|
||||||
echo "PHASE:awaiting_ci" > "$PHASE_FILE"
|
echo "PHASE:awaiting_ci" > "$PHASE_FILE"
|
||||||
i. STOP and WAIT. Do NOT return to the primary branch.
|
j. STOP and WAIT. Do NOT return to the primary branch.
|
||||||
The orchestrator polls CI, injects results and review feedback.
|
The orchestrator polls CI, injects results and review feedback.
|
||||||
When you receive injected CI or review feedback, follow its
|
When you receive injected CI or review feedback, follow its
|
||||||
instructions, then write PHASE:awaiting_ci and wait again.
|
instructions, then write PHASE:awaiting_ci and wait again.
|
||||||
|
|
||||||
3. If no file changes existed (step 1 found nothing):
|
4. If no file changes existed (step 2 found nothing):
|
||||||
echo "PHASE:done" > "$PHASE_FILE"
|
echo "PHASE:done" > "$PHASE_FILE"
|
||||||
|
|
||||||
4. If PR creation fails, log the error and write PHASE:failed.
|
5. If PR creation fails, log the error and write PHASE:failed.
|
||||||
"""
|
"""
|
||||||
needs = ["agents-update"]
|
needs = ["agents-update"]
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ runs directly from cron like the planner, predictor, and supervisor.
|
||||||
- `gardener/gardener-run.sh` — Cron wrapper + orchestrator: lock, memory guard,
|
- `gardener/gardener-run.sh` — Cron wrapper + orchestrator: lock, memory guard,
|
||||||
consumes escalation replies, sources disinto project config, creates tmux session,
|
consumes escalation replies, sources disinto project config, creates tmux session,
|
||||||
injects formula prompt, monitors phase file, handles crash recovery via
|
injects formula prompt, monitors phase file, handles crash recovery via
|
||||||
`run_formula_and_monitor`
|
`run_formula_and_monitor`, executes pending-actions manifest after PR merge
|
||||||
- `formulas/run-gardener.toml` — Execution spec: preflight, grooming, dust-bundling, blocked-review, agents-update, commit-and-pr
|
- `formulas/run-gardener.toml` — Execution spec: preflight, grooming, dust-bundling, blocked-review, agents-update, commit-and-pr
|
||||||
|
- `gardener/pending-actions.json` — Manifest of deferred repo actions (label changes,
|
||||||
|
closures, comments, issue creation). Written during grooming steps, committed to the
|
||||||
|
PR, reviewed alongside AGENTS.md changes, executed by gardener-run.sh after merge.
|
||||||
|
|
||||||
**Environment variables consumed**:
|
**Environment variables consumed**:
|
||||||
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
- `CODEBERG_TOKEN`, `CODEBERG_REPO`, `CODEBERG_API`, `PROJECT_NAME`, `PROJECT_REPO_ROOT`
|
||||||
|
|
@ -27,5 +30,7 @@ runs directly from cron like the planner, predictor, and supervisor.
|
||||||
|
|
||||||
**Lifecycle**: gardener-run.sh (cron 0,6,12,18) → lock + memory guard →
|
**Lifecycle**: gardener-run.sh (cron 0,6,12,18) → lock + memory guard →
|
||||||
consume escalation replies → load formula + context → create tmux session →
|
consume escalation replies → load formula + context → create tmux session →
|
||||||
Claude grooms backlog, bundles dust, reviews blocked issues, updates AGENTS.md,
|
Claude grooms backlog (writes proposed actions to manifest), bundles dust,
|
||||||
commits and creates PR → `PHASE:done`.
|
reviews blocked issues, updates AGENTS.md, commits manifest + docs to PR →
|
||||||
|
review-agent reviews all proposed actions → after merge, gardener-run.sh
|
||||||
|
executes manifest actions via API → `PHASE:done`.
|
||||||
|
|
|
||||||
|
|
@ -66,13 +66,24 @@ build_context_block AGENTS.md
|
||||||
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
|
SCRATCH_CONTEXT=$(read_scratch_context "$SCRATCH_FILE")
|
||||||
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
SCRATCH_INSTRUCTION=$(build_scratch_instruction "$SCRATCH_FILE")
|
||||||
|
|
||||||
# ── Build prompt (gardener needs extra API endpoints for issue management) ─
|
# ── Build prompt (manifest format reference for deferred actions) ─────────
|
||||||
GARDENER_API_EXTRA="
|
GARDENER_API_EXTRA="
|
||||||
Relabel: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PUT -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/labels' -d '{\"labels\":[LABEL_ID]}'
|
|
||||||
Comment: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X POST -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}/comments' -d '{\"body\":\"...\"}'
|
## Pending-actions manifest (REQUIRED)
|
||||||
Close: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"state\":\"closed\"}'
|
All repo mutations (comments, closures, label changes, issue creation) MUST be
|
||||||
Edit body: curl -sf -H \"Authorization: token \$CODEBERG_TOKEN\" -X PATCH -H 'Content-Type: application/json' '${CODEBERG_API}/issues/{number}' -d '{\"body\":\"new body\"}'
|
written to the JSONL manifest instead of calling APIs directly. Append one JSON
|
||||||
"
|
object per line to: \$PROJECT_REPO_ROOT/gardener/pending-actions.jsonl
|
||||||
|
|
||||||
|
Supported actions:
|
||||||
|
{\"action\":\"add_label\", \"issue\":NNN, \"label\":\"priority\"}
|
||||||
|
{\"action\":\"remove_label\", \"issue\":NNN, \"label\":\"backlog\"}
|
||||||
|
{\"action\":\"close\", \"issue\":NNN, \"reason\":\"already implemented\"}
|
||||||
|
{\"action\":\"comment\", \"issue\":NNN, \"body\":\"Relates to issue 1031\"}
|
||||||
|
{\"action\":\"create_issue\", \"title\":\"...\", \"body\":\"...\", \"labels\":[\"backlog\"]}
|
||||||
|
{\"action\":\"edit_body\", \"issue\":NNN, \"body\":\"new body\"}
|
||||||
|
|
||||||
|
The commit-and-pr step converts JSONL to JSON array. The orchestrator executes
|
||||||
|
actions after the PR merges. Do NOT call mutation APIs directly during the run."
|
||||||
build_prompt_footer "$GARDENER_API_EXTRA"
|
build_prompt_footer "$GARDENER_API_EXTRA"
|
||||||
|
|
||||||
# Extend phase protocol with merge-through instructions for compaction survival
|
# Extend phase protocol with merge-through instructions for compaction survival
|
||||||
|
|
@ -121,6 +132,154 @@ ${PROMPT_FOOTER}"
|
||||||
# Handles CI polling, review injection, merge, and cleanup after PR creation.
|
# Handles CI polling, review injection, merge, and cleanup after PR creation.
|
||||||
# Lighter than dev/phase-handler.sh — tailored for gardener doc-only PRs.
|
# Lighter than dev/phase-handler.sh — tailored for gardener doc-only PRs.
|
||||||
|
|
||||||
|
# ── Post-merge manifest execution ─────────────────────────────────────
|
||||||
|
# Reads gardener/pending-actions.json and executes each action via API.
|
||||||
|
# Failed actions are logged but do not block completion.
|
||||||
|
# shellcheck disable=SC2317 # called indirectly via _gardener_merge
|
||||||
|
_gardener_execute_manifest() {
|
||||||
|
local manifest_file="$PROJECT_REPO_ROOT/gardener/pending-actions.json"
|
||||||
|
if [ ! -f "$manifest_file" ]; then
|
||||||
|
log "manifest: no pending-actions.json — skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local count
|
||||||
|
count=$(jq 'length' "$manifest_file" 2>/dev/null || echo 0)
|
||||||
|
if [ "$count" -eq 0 ]; then
|
||||||
|
log "manifest: empty — skipping"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "manifest: executing ${count} actions"
|
||||||
|
|
||||||
|
local i=0
|
||||||
|
while [ "$i" -lt "$count" ]; do
|
||||||
|
local action issue
|
||||||
|
action=$(jq -r ".[$i].action" "$manifest_file")
|
||||||
|
issue=$(jq -r ".[$i].issue // empty" "$manifest_file")
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
add_label)
|
||||||
|
local label label_id
|
||||||
|
label=$(jq -r ".[$i].label" "$manifest_file")
|
||||||
|
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
|
||||||
|
'.[] | select(.name == $n) | .id') || true
|
||||||
|
if [ -n "$label_id" ]; then
|
||||||
|
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
"${CODEBERG_API}/issues/${issue}/labels" \
|
||||||
|
-d "{\"labels\":[${label_id}]}" >/dev/null 2>&1; then
|
||||||
|
log "manifest: add_label '${label}' to #${issue}"
|
||||||
|
else
|
||||||
|
log "manifest: FAILED add_label '${label}' to #${issue}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "manifest: FAILED add_label — label '${label}' not found"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
remove_label)
|
||||||
|
local label label_id
|
||||||
|
label=$(jq -r ".[$i].label" "$manifest_file")
|
||||||
|
label_id=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${CODEBERG_API}/labels" | jq -r --arg n "$label" \
|
||||||
|
'.[] | select(.name == $n) | .id') || true
|
||||||
|
if [ -n "$label_id" ]; then
|
||||||
|
if curl -sf -X DELETE -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${CODEBERG_API}/issues/${issue}/labels/${label_id}" >/dev/null 2>&1; then
|
||||||
|
log "manifest: remove_label '${label}' from #${issue}"
|
||||||
|
else
|
||||||
|
log "manifest: FAILED remove_label '${label}' from #${issue}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "manifest: FAILED remove_label — label '${label}' not found"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
close)
|
||||||
|
local reason
|
||||||
|
reason=$(jq -r ".[$i].reason // empty" "$manifest_file")
|
||||||
|
if curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
"${CODEBERG_API}/issues/${issue}" \
|
||||||
|
-d '{"state":"closed"}' >/dev/null 2>&1; then
|
||||||
|
log "manifest: closed #${issue} (${reason})"
|
||||||
|
else
|
||||||
|
log "manifest: FAILED close #${issue}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
comment)
|
||||||
|
local body escaped_body
|
||||||
|
body=$(jq -r ".[$i].body" "$manifest_file")
|
||||||
|
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
|
||||||
|
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
"${CODEBERG_API}/issues/${issue}/comments" \
|
||||||
|
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
||||||
|
log "manifest: commented on #${issue}"
|
||||||
|
else
|
||||||
|
log "manifest: FAILED comment on #${issue}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
create_issue)
|
||||||
|
local title body labels escaped_title escaped_body label_ids
|
||||||
|
title=$(jq -r ".[$i].title" "$manifest_file")
|
||||||
|
body=$(jq -r ".[$i].body" "$manifest_file")
|
||||||
|
labels=$(jq -r ".[$i].labels // [] | .[]" "$manifest_file")
|
||||||
|
escaped_title=$(printf '%s' "$title" | jq -Rs '.')
|
||||||
|
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
|
||||||
|
# Resolve label names to IDs
|
||||||
|
label_ids="[]"
|
||||||
|
if [ -n "$labels" ]; then
|
||||||
|
local all_labels ids_json=""
|
||||||
|
all_labels=$(curl -sf -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
"${CODEBERG_API}/labels") || true
|
||||||
|
while IFS= read -r lname; do
|
||||||
|
local lid
|
||||||
|
lid=$(echo "$all_labels" | jq -r --arg n "$lname" \
|
||||||
|
'.[] | select(.name == $n) | .id') || true
|
||||||
|
[ -n "$lid" ] && ids_json="${ids_json:+${ids_json},}${lid}"
|
||||||
|
done <<< "$labels"
|
||||||
|
[ -n "$ids_json" ] && label_ids="[${ids_json}]"
|
||||||
|
fi
|
||||||
|
if curl -sf -X POST -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
"${CODEBERG_API}/issues" \
|
||||||
|
-d "{\"title\":${escaped_title},\"body\":${escaped_body},\"labels\":${label_ids}}" >/dev/null 2>&1; then
|
||||||
|
log "manifest: created issue '${title}'"
|
||||||
|
else
|
||||||
|
log "manifest: FAILED create_issue '${title}'"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
edit_body)
|
||||||
|
local body escaped_body
|
||||||
|
body=$(jq -r ".[$i].body" "$manifest_file")
|
||||||
|
escaped_body=$(printf '%s' "$body" | jq -Rs '.')
|
||||||
|
if curl -sf -X PATCH -H "Authorization: token ${CODEBERG_TOKEN}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
"${CODEBERG_API}/issues/${issue}" \
|
||||||
|
-d "{\"body\":${escaped_body}}" >/dev/null 2>&1; then
|
||||||
|
log "manifest: edited body of #${issue}"
|
||||||
|
else
|
||||||
|
log "manifest: FAILED edit_body #${issue}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
log "manifest: unknown action '${action}' — skipping"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
log "manifest: execution complete (${count} actions processed)"
|
||||||
|
}
|
||||||
|
|
||||||
# shellcheck disable=SC2317 # called indirectly by monitor_phase_loop
|
# shellcheck disable=SC2317 # called indirectly by monitor_phase_loop
|
||||||
_gardener_merge() {
|
_gardener_merge() {
|
||||||
local merge_response merge_http_code
|
local merge_response merge_http_code
|
||||||
|
|
@ -133,6 +292,7 @@ _gardener_merge() {
|
||||||
|
|
||||||
if [ "$merge_http_code" = "200" ] || [ "$merge_http_code" = "204" ]; then
|
if [ "$merge_http_code" = "200" ] || [ "$merge_http_code" = "204" ]; then
|
||||||
log "gardener PR #${_GARDENER_PR} merged"
|
log "gardener PR #${_GARDENER_PR} merged"
|
||||||
|
_gardener_execute_manifest
|
||||||
printf 'PHASE:done\n' > "$PHASE_FILE"
|
printf 'PHASE:done\n' > "$PHASE_FILE"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -144,6 +304,7 @@ _gardener_merge() {
|
||||||
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
|
"${CODEBERG_API}/pulls/${_GARDENER_PR}" | jq -r '.merged // false') || true
|
||||||
if [ "$pr_merged" = "true" ]; then
|
if [ "$pr_merged" = "true" ]; then
|
||||||
log "gardener PR #${_GARDENER_PR} already merged"
|
log "gardener PR #${_GARDENER_PR} already merged"
|
||||||
|
_gardener_execute_manifest
|
||||||
printf 'PHASE:done\n' > "$PHASE_FILE"
|
printf 'PHASE:done\n' > "$PHASE_FILE"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -422,6 +583,7 @@ Then stop and wait."
|
||||||
|
|
||||||
if [ "$pr_merged" = "true" ]; then
|
if [ "$pr_merged" = "true" ]; then
|
||||||
log "gardener PR #${_GARDENER_PR} merged externally"
|
log "gardener PR #${_GARDENER_PR} merged externally"
|
||||||
|
_gardener_execute_manifest
|
||||||
printf 'PHASE:done\n' > "$PHASE_FILE"
|
printf 'PHASE:done\n' > "$PHASE_FILE"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue