From b0623eab729fdb131b256f73cd667e29d6078c11 Mon Sep 17 00:00:00 2001 From: Lef Date: Wed, 22 Apr 2026 11:52:25 +0200 Subject: [PATCH 1/5] ci: bump actions to v5 (node 24 runtime) --- .github/workflows/cicd.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a95d4ac..1f650e6 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -30,7 +30,7 @@ jobs: github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: lfs: true # On PR events, check out the PR head (not the merge commit) so the @@ -55,7 +55,7 @@ jobs: echo "BUCKET=${BUCKET}" >> $GITHUB_OUTPUT - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -169,7 +169,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v5 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From f74cba34d2cf859f64c8e65fd487f8ec08cd55d9 Mon Sep 17 00:00:00 2001 From: Lef Date: Wed, 22 Apr 2026 12:07:27 +0200 Subject: [PATCH 2/5] ci: log cloudflare api responses; fail step on success=false --- .github/workflows/cicd.yml | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 1f650e6..831c6d9 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -108,33 +108,44 @@ jobs: FQDN="${{ steps.names.outputs.BRANCH }}.${{ steps.zone.outputs.NAME }}" CONTENT="${{ steps.names.outputs.BUCKET }}.s3-website.${{ secrets.AWS_REGION }}.amazonaws.com" - EXISTING=$(curl --fail-with-body -sS -X GET \ + EXISTING_RESP=$(curl --fail-with-body -sS -X GET \ "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records?type=CNAME&name=${FQDN}" \ -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ - -H "Content-Type: application/json" | jq -r '.result[0].id // empty') + -H "Content-Type: application/json") + EXISTING=$(echo "$EXISTING_RESP" | jq -r '.result[0].id // empty') + echo "GET response (id=${EXISTING:-none}):" + echo "$EXISTING_RESP" | jq -c '{success, errors, messages, count: (.result | length)}' PAYLOAD=$(jq -n \ --arg name "${{ steps.names.outputs.BRANCH }}" \ --arg content "$CONTENT" \ '{type:"CNAME", name:$name, content:$content, ttl:1, proxied:true}') + echo "Payload: $PAYLOAD" if [ -n "$EXISTING" ]; then echo "Updating existing CNAME (id=$EXISTING) for ${FQDN}" - curl --fail-with-body -sS -X PUT \ + RESP=$(curl --fail-with-body -sS -X PUT \ "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records/$EXISTING" \ -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ -H "Content-Type: application/json" \ - --data "$PAYLOAD" > /dev/null + --data "$PAYLOAD") else echo "Creating new CNAME for ${FQDN}" - curl --fail-with-body -sS -X POST \ + RESP=$(curl --fail-with-body -sS -X POST \ "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records" \ -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ -H "Content-Type: application/json" \ - --data "$PAYLOAD" > /dev/null + --data "$PAYLOAD") + fi + + echo "Write response:" + echo "$RESP" | jq . + if ! echo "$RESP" | jq -e '.success == true' > /dev/null; then + echo "::error::Cloudflare DNS write returned success=false" + exit 1 fi # Scope the purge to just the hosts this deploy actually touched so we @@ -151,12 +162,17 @@ jobs: '[$fqdn]') fi echo "Purging hosts: $HOSTS" - curl --fail-with-body -sS -X POST \ + RESP=$(curl --fail-with-body -sS -X POST \ "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/purge_cache" \ -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ -H "Content-Type: application/json" \ - --data "{\"hosts\": $HOSTS}" > /dev/null + --data "{\"hosts\": $HOSTS}") + echo "$RESP" | jq -c '{success, errors}' + if ! echo "$RESP" | jq -e '.success == true' > /dev/null; then + echo "::error::Cloudflare purge returned success=false" + exit 1 + fi cleanup: # Runs when a PR is closed (merged or not). Tears down the preview bucket From 1413128abe62e59be1972258e2259ab043ad9fdc Mon Sep 17 00:00:00 2001 From: Lef Date: Wed, 22 Apr 2026 12:10:27 +0200 Subject: [PATCH 3/5] ci: follow POST with PUT so new proxied cnames actually publish to dns --- .github/workflows/cicd.yml | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 831c6d9..70b8089 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -123,28 +123,39 @@ jobs: '{type:"CNAME", name:$name, content:$content, ttl:1, proxied:true}') echo "Payload: $PAYLOAD" - if [ -n "$EXISTING" ]; then - echo "Updating existing CNAME (id=$EXISTING) for ${FQDN}" - RESP=$(curl --fail-with-body -sS -X PUT \ - "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records/$EXISTING" \ - -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ - -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ - -H "Content-Type: application/json" \ - --data "$PAYLOAD") - else + if [ -z "$EXISTING" ]; then echo "Creating new CNAME for ${FQDN}" - RESP=$(curl --fail-with-body -sS -X POST \ + POST_RESP=$(curl --fail-with-body -sS -X POST \ "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records" \ -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ -H "Content-Type: application/json" \ --data "$PAYLOAD") + if ! echo "$POST_RESP" | jq -e '.success == true' > /dev/null; then + echo "::error::Cloudflare DNS POST returned success=false" + echo "$POST_RESP" | jq . + exit 1 + fi + EXISTING=$(echo "$POST_RESP" | jq -r '.result.id') + echo "Created record id=$EXISTING" fi - echo "Write response:" - echo "$RESP" | jq . + # Always finish with a PUT against the record id. For a pre-existing + # record this is just the normal update. For a just-POSTed record + # this works around a Cloudflare quirk where proxied CNAMEs created + # by POST can linger in the control plane without being published to + # the public DNS layer until a subsequent modification nudges them. + echo "Updating CNAME (id=$EXISTING) for ${FQDN}" + RESP=$(curl --fail-with-body -sS -X PUT \ + "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records/$EXISTING" \ + -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ + -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ + -H "Content-Type: application/json" \ + --data "$PAYLOAD") + echo "PUT response:" + echo "$RESP" | jq -c '{success, errors, messages}' if ! echo "$RESP" | jq -e '.success == true' > /dev/null; then - echo "::error::Cloudflare DNS write returned success=false" + echo "::error::Cloudflare DNS PUT returned success=false" exit 1 fi From c637791e29c034b115c9e89931e539c0d58b7587 Mon Sep 17 00:00:00 2001 From: Lef Date: Wed, 22 Apr 2026 12:18:26 +0200 Subject: [PATCH 4/5] ci: apply success=true checks to cleanup dns calls; normalise log summary shape --- .github/workflows/cicd.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 70b8089..e089087 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -179,7 +179,7 @@ jobs: -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ -H "Content-Type: application/json" \ --data "{\"hosts\": $HOSTS}") - echo "$RESP" | jq -c '{success, errors}' + echo "$RESP" | jq -c '{success, errors, messages}' if ! echo "$RESP" | jq -e '.success == true' > /dev/null; then echo "::error::Cloudflare purge returned success=false" exit 1 @@ -240,19 +240,31 @@ jobs: - name: Delete preview Cloudflare DNS record run: | FQDN="${{ steps.names.outputs.BRANCH }}.${{ steps.zone.outputs.NAME }}" - RECORD_ID=$(curl --fail-with-body -sS -X GET \ + + LOOKUP_RESP=$(curl --fail-with-body -sS -X GET \ "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records?type=CNAME&name=${FQDN}" \ -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ - -H "Content-Type: application/json" | jq -r '.result[0].id // empty') + -H "Content-Type: application/json") + echo "$LOOKUP_RESP" | jq -c '{success, errors, messages}' + if ! echo "$LOOKUP_RESP" | jq -e '.success == true' > /dev/null; then + echo "::error::Cloudflare DNS lookup returned success=false" + exit 1 + fi + RECORD_ID=$(echo "$LOOKUP_RESP" | jq -r '.result[0].id // empty') if [ -n "$RECORD_ID" ]; then echo "Deleting CNAME ${FQDN} (id=$RECORD_ID)" - curl --fail-with-body -sS -X DELETE \ + DEL_RESP=$(curl --fail-with-body -sS -X DELETE \ "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/dns_records/$RECORD_ID" \ -H "X-Auth-Email: ${{ secrets.EMAIL }}" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_DNS_SECRET_API_TOKEN }}" \ - -H "Content-Type: application/json" > /dev/null + -H "Content-Type: application/json") + echo "$DEL_RESP" | jq -c '{success, errors, messages}' + if ! echo "$DEL_RESP" | jq -e '.success == true' > /dev/null; then + echo "::error::Cloudflare DNS DELETE returned success=false" + exit 1 + fi else echo "No CNAME for ${FQDN}, skipping." fi From 0143066433154539f820c0f658c9e534cc4aa5bb Mon Sep 17 00:00:00 2001 From: Lef Date: Wed, 22 Apr 2026 12:20:57 +0200 Subject: [PATCH 5/5] =?UTF-8?q?ci:=20bump=20aws-actions/configure-aws-cred?= =?UTF-8?q?entials=20v5=20=E2=86=92=20v6=20(actual=20node=2024)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cicd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index e089087..b1dee4c 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -55,7 +55,7 @@ jobs: echo "BUCKET=${BUCKET}" >> $GITHUB_OUTPUT - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -196,7 +196,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}