feat(P25): deploy outputs (SSM + PR comment) + error reporting via Lambda + stage comments

---ci---
project: acdl
phase: 25
milestone: v1.7
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-07-22 20:08:30 +00:00
parent 07c0349131
commit 4fe794c7a4
13 changed files with 910 additions and 16 deletions
+102 -9
View File
@@ -5,8 +5,9 @@ communication, D-051). Accepts { consumerRepo, contractId, contract,
environment, action } and writes contracts to DynamoDB table acdl-contracts
(PK consumerRepo, SK contractId#submittedAt).
The report_error action (D-055) is prepared as a stub in this phase; the
GitHub issue creation is implemented in Phase 25.
The report_error action (D-055) creates a GitHub issue on the platform repo
via the GitHub API, using a token from Secrets Manager. It is idempotent: if
an open issue with the same title exists, it comments rather than duplicating.
Cross-account: the Lambda's Function URL uses IAM auth; the consumer's
deploy role (granted during onboarding) invokes it via SigV4-signed
@@ -71,17 +72,109 @@ def _submit_contract(payload):
def _report_error(payload):
# Phase 25 implements the GitHub issue creation.
# This stub validates the payload and returns a prepared status.
"""Create a GitHub issue on the platform repo for a deploy failure (D-055).
Uses the GitHub token from Secrets Manager. Idempotent: if an open
issue with the same title exists, comments on it rather than duplicating.
"""
import urllib.request
required = ["consumerRepo", "contractId", "error"]
for field in required:
if field not in payload:
raise ValueError(f"report_error requires '{field}'")
return {
"status": "error_report_prepared",
"contractId": payload["contractId"],
"action": "report_error",
}
consumer_repo = payload["consumerRepo"]
contract_id = payload["contractId"]
error = payload.get("error", "unknown error")
run_url = payload.get("runUrl", "")
stack_trace = payload.get("stackTrace", "")[:2000] # truncate
# Get the GitHub token from Secrets Manager
secrets = _get_secrets_client()
try:
secret_response = secrets.get_secret_value(SecretId=GITHUB_TOKEN_SECRET_ID)
github_token = secret_response["SecretString"]
except Exception as e:
raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}")
owner, repo = PLATFORM_REPO.split("/")
title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
# Check for an existing open issue with the same title (idempotency)
search_url = (
f"https://api.github.com/search/issues?q=repo:{owner}/{repo}"
f"+is:issue+is:open+in:title+%22{contract_id}%22"
)
req = urllib.request.Request(search_url)
req.add_header("Authorization", f"token {github_token}")
req.add_header("Accept", "application/vnd.github+json")
try:
with urllib.request.urlopen(req, timeout=10) as resp:
search_result = json.loads(resp.read())
existing = search_result.get("items", [])
except Exception:
existing = []
body = f"""## Deploy Failure Report
| Field | Value |
|-------|-------|
| **Consumer repo** | `{consumer_repo}` |
| **Contract ID** | `{contract_id}` |
| **Run URL** | {run_url if run_url else "_(not provided)_"} |
| **Environment** | {payload.get('environment', 'unknown')} |
## Error
```
{error}
```
## Stack Trace
```
{stack_trace}
```
_This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
"""
if existing:
# Comment on the existing issue
issue_number = existing[0]["number"]
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments"
data = json.dumps({"body": body}).encode()
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Authorization", f"token {github_token}")
req.add_header("Accept", "application/vnd.github+json")
urllib.request.urlopen(req, timeout=10)
return {
"status": "commented_on_existing",
"issueNumber": issue_number,
"contractId": contract_id,
"action": "report_error",
}
else:
# Create a new issue
url = f"https://api.github.com/repos/{owner}/{repo}/issues"
data = json.dumps({
"title": title,
"body": body,
"labels": ["platform-alert", "auto-generated"],
}).encode()
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Authorization", f"token {github_token}")
req.add_header("Accept", "application/vnd.github+json")
resp = urllib.request.urlopen(req, timeout=10)
issue = json.loads(resp.read())
return {
"status": "issue_created",
"issueNumber": issue["number"],
"issueUrl": issue["html_url"],
"contractId": contract_id,
"action": "report_error",
}
def lambda_handler(event, context):