name: Dry Run on: pull_request: paths: - "state/*.json" jobs: detect: runs-on: ubuntu-latest outputs: envs: ${{ steps.changed.outputs.envs }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Detect changed environments id: changed run: | FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} -- 'state/*.json') ENVS=$(python3 -c " import os, json files = '''$FILES'''.strip().split('\n') envs = [os.path.basename(f).replace('.json','') for f in files if f.strip()] print(json.dumps(envs)) ") echo "envs=$ENVS" >> "$GITHUB_OUTPUT" echo "Changed environments: $ENVS" dry-run: needs: detect runs-on: ubuntu-latest if: needs.detect.outputs.envs != '[]' strategy: matrix: env: ${{ fromJson(needs.detect.outputs.envs) }} steps: - uses: actions/checkout@v4 - name: Resolve environment secrets id: env run: | ENV_UPPER=$(echo "${{ matrix.env }}" | tr '[:lower:]-' '[:upper:]_') echo "token_key=${ENV_UPPER}_RECONCILER_TOKEN" >> "$GITHUB_OUTPUT" echo "url_key=${ENV_UPPER}_RECONCILER_URL" >> "$GITHUB_OUTPUT" - name: Run dry-run reconcile id: plan env: RECONCILER_TOKEN: ${{ secrets[steps.env.outputs.token_key] }} RECONCILER_URL: ${{ secrets[steps.env.outputs.url_key] }} run: | if [ -z "$RECONCILER_URL" ] || [ -z "$RECONCILER_TOKEN" ]; then echo "No secrets configured for environment '${{ matrix.env }}' — skipping" echo "response={}" >> "$GITHUB_OUTPUT" exit 0 fi RESPONSE=$(curl -sf \ -X POST \ -H "Authorization: Bearer ${RECONCILER_TOKEN}" \ -H "Content-Type: application/json" \ -d @state/${{ matrix.env }}.json \ "${RECONCILER_URL}/reconcile?dry_run=true") echo "response<> "$GITHUB_OUTPUT" echo "$RESPONSE" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" - name: Format and post PR comment if: steps.plan.outputs.response != '{}' env: GIT_TOKEN: ${{ secrets.GIT_TOKEN }} GIT_URL: ${{ secrets.GIT_URL }} RESPONSE: ${{ steps.plan.outputs.response }} ENV_NAME: ${{ matrix.env }} REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | python3 <<'SCRIPT' import json, os, urllib.request, urllib.parse data = json.loads(os.environ["RESPONSE"]) ops = data.get("operations", []) summary = data.get("summary", {}) env = os.environ["ENV_NAME"] lines = [f"## Reconciliation Plan: `{env}`\n"] if not ops: lines.append("No changes detected.\n") else: lines.append("| Operation | Name |") lines.append("|-----------|------|") for op in ops: lines.append(f"| `{op['type']}` | {op['name']} |") lines.append("") s = summary lines.append(f"**Summary:** {s.get('created',0)} create, {s.get('updated',0)} update, {s.get('deleted',0)} delete") comment = "\n".join(lines) url = f"{os.environ['GIT_URL']}/api/v1/repos/{os.environ['REPO']}/issues/{os.environ['PR_NUMBER']}/comments" body = json.dumps({"body": comment}).encode() req = urllib.request.Request(url, data=body, method="POST", headers={ "Authorization": f"token {os.environ['GIT_TOKEN']}", "Content-Type": "application/json", }) urllib.request.urlopen(req) print(f"Posted comment to PR #{os.environ['PR_NUMBER']}") SCRIPT