72b359c9a9
Squash merge of phase/04-pipeline-and-approval-gates; pipeline.yml + issue-to-contract.yml + finalize_evidence.py; verify_phase04.sh green; 1 P0 fixed (shell injection).
145 lines
6.2 KiB
YAML
145 lines
6.2 KiB
YAML
# 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/<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":
|
|
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 |