name: Reconcile on: push: branches: - main paths: - "state/*.json" jobs: detect: runs-on: ubuntu-latest outputs: envs: ${{ steps.changed.outputs.envs }} steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - name: Detect changed environments id: changed run: | FILES=$(git diff --name-only HEAD~1 HEAD -- '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" reconcile: needs: detect runs-on: ubuntu-latest if: needs.detect.outputs.envs != '[]' steps: - uses: actions/checkout@v4 - name: Reconcile each changed environment env: ENVS: ${{ needs.detect.outputs.envs }} TEST_RECONCILER_TOKEN: ${{ secrets.TEST_RECONCILER_TOKEN }} TEST_RECONCILER_URL: ${{ secrets.TEST_RECONCILER_URL }} DEV_RECONCILER_TOKEN: ${{ secrets.DEV_RECONCILER_TOKEN }} DEV_RECONCILER_URL: ${{ secrets.DEV_RECONCILER_URL }} PROD_RECONCILER_TOKEN: ${{ secrets.PROD_RECONCILER_TOKEN }} PROD_RECONCILER_URL: ${{ secrets.PROD_RECONCILER_URL }} run: | python3 <<'SCRIPT' import json, os, urllib.request, sys envs = json.loads(os.environ["ENVS"]) failed = [] for env in envs: key = env.upper().replace("-", "_") token = os.environ.get(f"{key}_RECONCILER_TOKEN", "") url = os.environ.get(f"{key}_RECONCILER_URL", "") if not token or not url: print(f"No secrets for '{env}' — skipping") continue # Sync events first try: req = urllib.request.Request( f"{url}/sync-events", method="POST", headers={"Authorization": f"Bearer {token}"}, ) urllib.request.urlopen(req) print(f"[{env}] Synced events") except Exception as e: print(f"[{env}] Sync events failed (non-fatal): {e}") # Apply reconcile with open(f"state/{env}.json", "rb") as f: state_data = f.read() req = urllib.request.Request( f"{url}/reconcile", data=state_data, method="POST", headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) try: resp = urllib.request.urlopen(req) data = json.loads(resp.read()) except Exception as e: print(f"[{env}] Reconcile FAILED: {e}") failed.append(env) continue status = data.get("status", "ok") if status == "error": print(f"[{env}] Reconcile returned error:") print(json.dumps(data, indent=2)) failed.append(env) continue summary = data.get("summary", {}) print(f"[{env}] Reconcile OK: " f"{summary.get('created',0)} created, " f"{summary.get('updated',0)} updated, " f"{summary.get('deleted',0)} deleted") # Log created keys (names only, not values) keys = data.get("created_keys", {}) if keys: print(f"[{env}] Created setup keys: {list(keys.keys())}") if failed: print(f"\nFailed environments: {failed}") sys.exit(1) SCRIPT