feat(P04): implement issue-to-contract.yml L3B trigger (T-4.3)

---ci---
phase: 4
milestone: v1.0
status: execute
persona: backend-engineer
task: T-4.3
requirements:
  covered: [REQ-12]
---/ci---

Wave 1, task T-4.3. Replaces the Phase 01 issue-to-contract.yml skeleton
with the real L3B trigger (D-030). On issue opened, the workflow checks out
the acdl repo at milestone/v1.0-initial (branch-pin rule) to obtain
l3b_agent_stub.py, parses the Issue body into contract.yaml, commits it to
a new branch contract/<issue-number> on acdl-contracts via the Gitea
file-contents API using the new_branch field (single POST creates the
branch + file), comments on and closes the Issue, then dispatches the
pipeline workflow on acdl via POST .../actions/workflows/pipeline.yml/
dispatches with inputs contract-ref=contract/<issue-number>
(cross-repo trigger per D-014). The `on:` key is quoted as "on": so
PyYAML parses it as a string key.
This commit is contained in:
Jon Chery
2026-07-21 13:39:06 +00:00
parent 1cb8d21315
commit 2f0062c24d
@@ -1,18 +1,35 @@
# ACDL issue-to-contract workflow (Phase 01 skeleton, reference copy).
#
# This file is the source-of-truth copy kept in the `acdl` repo under
# contracts-repo/.gitea/workflows/. Phase 04 will push it to the actual
# `acdl-contracts` repo under .gitea/workflows/ and implement the real
# step bodies.
# ACDL issue-to-contract workflow (Phase 04 implementation).
#
# Trigger: a new Issue is opened in acdl-contracts. The workflow runs
# l3b_agent_stub.py to map the Issue body to a contract.yaml, commits the
# contract to a new branch, closes the Issue, and triggers the main
# pipeline in the `acdl` repo via the workflow_dispatch API (D-014; Gitea
# Actions does not support repository_dispatch).
# l3b_agent_stub.py (checked out from the `acdl` repo, pinned to
# @milestone/v1.0-initial) to map the Issue body to a contract.yaml, commits
# the contract to a new branch `contract/<issue-number>` on acdl-contracts
# via the Gitea file-contents API, closes the Issue with a comment, and
# dispatches the main pipeline in the `acdl` repo via the workflow_dispatch
# API (D-014; Gitea Actions does not support repository_dispatch).
#
# Cross-repo trigger (D-014):
# The final step POSTs to
# /api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches
# with body {"ref": "milestone/v1.0-initial",
# "inputs": {"contract-ref": "contract/<issue-number>"}}.
#
# Branch-pin rule (ARCHITECTURE.md):
# The `acdl` repo's default branch is `milestone/v1.0-initial`, so the
# checkout step pins `ref: milestone/v1.0-initial`. The pipeline dispatch
# also pins `ref: milestone/v1.0-initial` (the workflow file lives on
# that branch). The new `contract/<n>` branch is created on acdl-contracts
# (whose default branch is `main`, per D-015).
#
# File-contents POST with `new_branch` (D-030):
# The POST to /repos/.../contents/contract.yaml includes
# `new_branch: contract/<n>`, which tells Gitea to create the file on a
# NEW branch off the current head of `branch: main` instead of committing
# directly to main. This avoids a separate branch-create + commit round
# trip.
name: issue-to-contract
on:
"on":
issues:
types: [opened]
@@ -20,21 +37,104 @@ jobs:
parse-and-trigger:
runs-on: ubuntu-latest
steps:
# Phase 04 will implement:
# 1. checkout acdl-contracts (so l3b_agent_stub.py is available).
# 2. run: python3 scripts/l3b_agent_stub.py "${{ gitea.event.issue.body }}" > contract.yaml
# 3. parse the generated contract; commit it to a new branch
# (e.g. contract/<issue-number>).
# 4. push the branch.
# 5. close the Issue with a comment linking to the pipeline run.
# 6. trigger the main pipeline:
# curl -X POST \
# -H "Authorization: token ${GITEA_TOKEN}" \
# https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/workflows/<id>/dispatches \
# -d '{"ref":"milestone/v1.0-initial","inputs":{"contract-ref":"<branch>"}}'
- name: "Issue-trigger placeholder"
- name: "Checkout acdl (pinned to milestone/v1.0-initial for l3b_agent_stub.py)"
uses: actions/checkout@v4
with:
repository: continuous-intelligence/acdl
ref: milestone/v1.0-initial
token: ${{ secrets.GITEA_TOKEN }}
- name: "Parse Issue body into contract.yaml"
run: |
echo "issue-to-contract placeholder (Phase 01 skeleton)"
echo "Issue body: ${{ gitea.event.issue.body }}"
echo "Phase 04 will run l3b_agent_stub.py, commit contract.yaml, close issue, dispatch pipeline"
exit 0
python3 scripts/l3b_agent_stub.py "${{ gitea.event.issue.body }}" -o contract.yaml
echo "--- generated contract.yaml ---"
cat contract.yaml
- name: "Commit contract.yaml to new branch contract/${{ gitea.event.issue.number }} on acdl-contracts"
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
STACK=$(python3 -c 'import yaml; print(yaml.safe_load(open("contract.yaml"))["stack"])')
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
BRANCH="contract/${ISSUE_NUMBER}"
HOST="https://git.cloudinit.dev"
API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/contents/contract.yaml"
B64=$(base64 -w 0 contract.yaml)
BODY=$(python3 -c "
import json
print(json.dumps({
'content': '${B64}',
'message': 'l3b: contract for issue #${ISSUE_NUMBER}',
'branch': 'main',
'new_branch': '${BRANCH}'
}))
")
STATUS=$(curl -sS -o /tmp/contract_post.json -w "%{http_code}" \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$BODY" \
"$API")
echo "POST contract.yaml -> HTTP ${STATUS}"
cat /tmp/contract_post.json || true
case "$STATUS" in
201) echo "contract.yaml committed on branch ${BRANCH}" ;;
*) echo "ERROR: file-contents POST failed (HTTP ${STATUS})" >&2; exit 1 ;;
esac
echo "STACK=${STACK}" >> "$GITHUB_ENV"
echo "BRANCH=${BRANCH}" >> "$GITHUB_ENV"
- name: "Comment on Issue + close it"
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
HOST="https://git.cloudinit.dev"
ISSUES_API="${HOST}/api/v1/repos/continuous-intelligence/acdl-contracts/issues/${ISSUE_NUMBER}"
COMMENT_BODY=$(python3 -c "
import json
print(json.dumps({'body': 'Generated contract.yaml for stack \`' + '${STACK}' + '\` on branch \`' + '${BRANCH}' + '\`. Pipeline dispatched.'}))
")
curl -sS -o /tmp/comment.json -w "comment HTTP %{http_code}\n" \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$COMMENT_BODY" \
"${ISSUES_API}/comments"
CLOSE_BODY='{"state":"closed"}'
curl -sS -o /tmp/close.json -w "close HTTP %{http_code}\n" \
-X PATCH \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$CLOSE_BODY" \
"${ISSUES_API}"
- name: "Dispatch the pipeline on acdl (contract-ref = contract/${{ gitea.event.issue.number }})"
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
ISSUE_NUMBER="${{ gitea.event.issue.number }}"
HOST="https://git.cloudinit.dev"
DISPATCH_URL="${HOST}/api/v1/repos/continuous-intelligence/acdl/actions/workflows/pipeline.yml/dispatches"
BODY=$(python3 -c "
import json
print(json.dumps({
'ref': 'milestone/v1.0-initial',
'inputs': {'contract-ref': 'contract/${ISSUE_NUMBER}'}
}))
")
STATUS=$(curl -sS -o /tmp/dispatch.json -w "%{http_code}" \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$BODY" \
"$DISPATCH_URL")
echo "pipeline dispatch -> HTTP ${STATUS}"
cat /tmp/dispatch.json || true
case "$STATUS" in
201|202|204) echo "pipeline dispatched (contract-ref=contract/${ISSUE_NUMBER})" ;;
*) echo "ERROR: pipeline dispatch failed (HTTP ${STATUS})" >&2; exit 1 ;;
esac