From c6f4ae96144f756e4f22d94ee66212d09deed414 Mon Sep 17 00:00:00 2001 From: Prox Date: Fri, 6 Mar 2026 18:04:17 +0200 Subject: [PATCH] improved jobs --- .gitea/workflows/dry-run.yml | 130 ++++++++++++++++--------------- .gitea/workflows/reconcile.yml | 137 +++++++++++++++++---------------- 2 files changed, 142 insertions(+), 125 deletions(-) diff --git a/.gitea/workflows/dry-run.yml b/.gitea/workflows/dry-run.yml index e1a1a63..0b80444 100644 --- a/.gitea/workflows/dry-run.yml +++ b/.gitea/workflows/dry-run.yml @@ -32,77 +32,87 @@ jobs: 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 != '{}' + - name: Run dry-run for 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 }} 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 + import json, os, urllib.request - data = json.loads(os.environ["RESPONSE"]) - ops = data.get("operations", []) - summary = data.get("summary", {}) - env = os.environ["ENV_NAME"] + envs = json.loads(os.environ["ENVS"]) - 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") + for env in envs: + key = env.upper().replace("-", "_") + token = os.environ.get(f"{key}_RECONCILER_TOKEN", "") + url = os.environ.get(f"{key}_RECONCILER_URL", "") - 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']}") + if not token or not url: + print(f"No secrets for '{env}' — skipping") + continue + + # Call reconciler dry-run + with open(f"state/{env}.json", "rb") as f: + state_data = f.read() + + req = urllib.request.Request( + f"{url}/reconcile?dry_run=true", + 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"Reconciler call failed for '{env}': {e}") + continue + + # Format as markdown + ops = data.get("operations", []) + summary = data.get("summary", {}) + 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("") + lines.append( + f"**Summary:** {summary.get('created',0)} create, " + f"{summary.get('updated',0)} update, " + f"{summary.get('deleted',0)} delete" + ) + comment = "\n".join(lines) + print(comment) + + # Post PR comment + git_token = os.environ.get("GIT_TOKEN", "") + git_url = os.environ.get("GIT_URL", "") + if git_token and git_url: + api_url = f"{git_url}/api/v1/repos/{os.environ['REPO']}/issues/{os.environ['PR_NUMBER']}/comments" + body = json.dumps({"body": comment}).encode() + req = urllib.request.Request(api_url, data=body, method="POST", headers={ + "Authorization": f"token {git_token}", + "Content-Type": "application/json", + }) + urllib.request.urlopen(req) + print(f"Posted comment to PR #{os.environ['PR_NUMBER']}") SCRIPT diff --git a/.gitea/workflows/reconcile.yml b/.gitea/workflows/reconcile.yml index 3c3003a..046c305 100644 --- a/.gitea/workflows/reconcile.yml +++ b/.gitea/workflows/reconcile.yml @@ -34,78 +34,85 @@ jobs: 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" - echo "age_key=${ENV_UPPER}_AGE_PUBLIC_KEY" >> "$GITHUB_OUTPUT" - - - name: Sync events + - name: Reconcile each changed environment env: - RECONCILER_TOKEN: ${{ secrets[steps.env.outputs.token_key] }} - RECONCILER_URL: ${{ secrets[steps.env.outputs.url_key] }} + 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: | - if [ -z "$RECONCILER_URL" ] || [ -z "$RECONCILER_TOKEN" ]; then - echo "No secrets configured for environment '${{ matrix.env }}' — skipping" - exit 0 - fi - curl -sf \ - -X POST \ - -H "Authorization: Bearer ${RECONCILER_TOKEN}" \ - "${RECONCILER_URL}/sync-events" + python3 <<'SCRIPT' + import json, os, urllib.request, sys - - name: Pull latest (poller may have committed) - run: git pull --rebase + envs = json.loads(os.environ["ENVS"]) + failed = [] - - name: Apply reconcile - id: reconcile - env: - RECONCILER_TOKEN: ${{ secrets[steps.env.outputs.token_key] }} - RECONCILER_URL: ${{ secrets[steps.env.outputs.url_key] }} - run: | - RESPONSE=$(curl -sf \ - -X POST \ - -H "Authorization: Bearer ${RECONCILER_TOKEN}" \ - -H "Content-Type: application/json" \ - -d @state/${{ matrix.env }}.json \ - "${RECONCILER_URL}/reconcile") - echo "response<> "$GITHUB_OUTPUT" - echo "$RESPONSE" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" + for env in envs: + key = env.upper().replace("-", "_") + token = os.environ.get(f"{key}_RECONCILER_TOKEN", "") + url = os.environ.get(f"{key}_RECONCILER_URL", "") - STATUS=$(python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('status','ok'))" <<< "$RESPONSE") - if [ "$STATUS" = "error" ]; then - echo "Reconcile failed for ${{ matrix.env }}" - python3 -m json.tool <<< "$RESPONSE" - exit 1 - fi + if not token or not url: + print(f"No secrets for '{env}' — skipping") + continue - - name: Encrypt and upload setup keys - if: success() - env: - AGE_PUBLIC_KEY: ${{ secrets[steps.env.outputs.age_key] }} - RESPONSE: ${{ steps.reconcile.outputs.response }} - run: | - KEYS=$(python3 -c "import json,os; d=json.loads(os.environ['RESPONSE']); k=d.get('created_keys'); print(json.dumps(k) if k and k != {} else '')") - if [ -n "$KEYS" ] && [ -n "$AGE_PUBLIC_KEY" ]; then - echo "$KEYS" | age -r "$AGE_PUBLIC_KEY" -o setup-keys-${{ matrix.env }}.age - echo "Setup keys for ${{ matrix.env }} encrypted" - else - echo "No new keys created for ${{ matrix.env }}" - fi + # 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}") - - name: Upload artifact - if: success() - uses: actions/upload-artifact@v4 - with: - name: setup-keys-${{ matrix.env }} - path: setup-keys-${{ matrix.env }}.age - if-no-files-found: ignore + # 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