# ACDL issue-to-contract workflow (Phase 04 implementation). # # Trigger: a new Issue is opened in acdl-contracts. The workflow runs # 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/` 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/"}}. # # 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/` 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/`, 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": issues: types: [opened] jobs: parse-and-trigger: runs-on: ubuntu-latest steps: - 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" env: ISSUE_BODY: ${{ gitea.event.issue.body }} run: | # Pass the Issue body via an env var to avoid shell injection from # arbitrary Issue text. l3b_agent_stub.py reads argv[1]; we pass # the env var quoted so no metacharacter interpretation happens. python3 scripts/l3b_agent_stub.py "$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