Skip to content

Commit 289e521

Browse files
authored
Add guarded DNS cutover for web.geolibre.app (#2058)
Prove Cloudflare DNS write access before replacing the Worker custom domain with the GitHub Pages CNAME.
1 parent da60e8e commit 289e521

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: Cut over web.geolibre.app DNS
2+
3+
# One-way migration helper for moving web.geolibre.app from the viewer Worker
4+
# custom domain to the dedicated GitHub Pages repository. It is deliberately
5+
# manual: the initial Pages snapshot must be built and verified before DNS is
6+
# changed. Re-running it is safe and restores the intended DNS state.
7+
8+
on:
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: web-dns-cutover
13+
cancel-in-progress: false
14+
15+
permissions:
16+
contents: read
17+
18+
jobs:
19+
cutover:
20+
name: Replace the Worker domain with a Pages CNAME
21+
if: github.repository == 'opengeos/GeoLibre'
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Verify DNS access and apply the cutover
25+
env:
26+
CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
27+
CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
28+
HOSTNAME: web.geolibre.app
29+
PAGES_HOSTNAME: opengeos.github.io
30+
run: |
31+
set -euo pipefail
32+
33+
if [ -z "${CF_API_TOKEN:-}" ] || [ -z "${CF_ACCOUNT_ID:-}" ]; then
34+
echo "::error::Cloudflare credentials are not configured."
35+
exit 1
36+
fi
37+
38+
cf_request() {
39+
local method=$1
40+
local path=$2
41+
local body=${3:-}
42+
if [ -n "$body" ]; then
43+
curl --fail-with-body --silent --show-error \
44+
--request "$method" \
45+
--header "Authorization: Bearer $CF_API_TOKEN" \
46+
--header "Content-Type: application/json" \
47+
--data "$body" \
48+
"https://api.cloudflare.com/client/v4/$path"
49+
else
50+
curl --fail-with-body --silent --show-error \
51+
--request "$method" \
52+
--header "Authorization: Bearer $CF_API_TOKEN" \
53+
--header "Content-Type: application/json" \
54+
"https://api.cloudflare.com/client/v4/$path"
55+
fi
56+
}
57+
58+
if ! zone_response=$(cf_request GET "zones?name=geolibre.app"); then
59+
echo "::error::The Cloudflare token cannot read the geolibre.app zone."
60+
exit 1
61+
fi
62+
zone_id=$(jq -r '.result[0].id // empty' <<< "$zone_response")
63+
if [ -z "$zone_id" ]; then
64+
echo "::error::The Cloudflare token cannot resolve the geolibre.app zone."
65+
exit 1
66+
fi
67+
68+
# Prove DNS *write* access before detaching the live Worker domain.
69+
# A token with Workers permissions but no DNS edit permission fails
70+
# here and leaves web.geolibre.app completely untouched.
71+
check_name="_geolibre-pages-cutover-check.geolibre.app"
72+
check_body=$(jq -nc \
73+
--arg name "$check_name" \
74+
--arg content "github-actions-$GITHUB_RUN_ID" \
75+
'{type:"TXT", name:$name, content:$content, ttl:60}')
76+
if ! check_response=$(cf_request POST "zones/$zone_id/dns_records" "$check_body"); then
77+
echo "::error::The Cloudflare token cannot create DNS records; the Worker domain was not changed."
78+
exit 1
79+
fi
80+
check_id=$(jq -r '.result.id // empty' <<< "$check_response")
81+
if [ -z "$check_id" ]; then
82+
echo "::error::The Cloudflare token cannot create DNS records; the Worker domain was not changed."
83+
exit 1
84+
fi
85+
cf_request DELETE "zones/$zone_id/dns_records/$check_id" >/dev/null
86+
echo "DNS write access verified."
87+
88+
domains=$(cf_request GET "accounts/$CF_ACCOUNT_ID/workers/domains?hostname=$HOSTNAME")
89+
domain_id=$(jq -r --arg hostname "$HOSTNAME" \
90+
'.result[] | select(.hostname == $hostname) | .id' <<< "$domains")
91+
if [ -n "$domain_id" ]; then
92+
cf_request DELETE "accounts/$CF_ACCOUNT_ID/workers/domains/$domain_id" >/dev/null
93+
echo "Detached $HOSTNAME from its Worker."
94+
else
95+
echo "$HOSTNAME is already detached from Workers."
96+
fi
97+
98+
# Worker custom domains own a managed DNS record. Wait for Cloudflare
99+
# to remove it before creating the replacement CNAME.
100+
records='[]'
101+
for _ in {1..30}; do
102+
records_response=$(cf_request GET "zones/$zone_id/dns_records?name=$HOSTNAME")
103+
records=$(jq '.result' <<< "$records_response")
104+
if [ "$(jq length <<< "$records")" -eq 0 ]; then
105+
break
106+
fi
107+
if [ "$(jq length <<< "$records")" -eq 1 ] && \
108+
[ "$(jq -r '.[0].type' <<< "$records")" = CNAME ] && \
109+
[ "$(jq -r '.[0].content' <<< "$records")" = "$PAGES_HOSTNAME" ]; then
110+
break
111+
fi
112+
sleep 2
113+
done
114+
115+
count=$(jq length <<< "$records")
116+
if [ "$count" -eq 0 ]; then
117+
cname_body=$(jq -nc \
118+
--arg name "$HOSTNAME" \
119+
--arg content "$PAGES_HOSTNAME" \
120+
'{type:"CNAME", name:$name, content:$content, ttl:1, proxied:false, comment:"GeoLibre web app on GitHub Pages"}')
121+
cf_request POST "zones/$zone_id/dns_records" "$cname_body" >/dev/null
122+
elif [ "$count" -eq 1 ] && \
123+
[ "$(jq -r '.[0].type' <<< "$records")" = CNAME ] && \
124+
[ "$(jq -r '.[0].content' <<< "$records")" = "$PAGES_HOSTNAME" ]; then
125+
record_id=$(jq -r '.[0].id' <<< "$records")
126+
cname_body=$(jq -nc \
127+
--arg name "$HOSTNAME" \
128+
--arg content "$PAGES_HOSTNAME" \
129+
'{type:"CNAME", name:$name, content:$content, ttl:1, proxied:false, comment:"GeoLibre web app on GitHub Pages"}')
130+
cf_request PUT "zones/$zone_id/dns_records/$record_id" "$cname_body" >/dev/null
131+
else
132+
echo "::error::Unexpected DNS records remain for $HOSTNAME; refusing to overwrite them."
133+
jq -r '.[] | [.type, .name, .content] | @tsv' <<< "$records"
134+
exit 1
135+
fi
136+
137+
echo "$HOSTNAME now points directly to $PAGES_HOSTNAME."

0 commit comments

Comments
 (0)