mirror of
https://github.com/skoelle/moonweb-site.git
synced 2026-09-17 17:00:25 +00:00
Add Cloudflare redirect rules script and GitHub Action
- scripts/cloudflare/setup-redirects.sh: CLI tool to manage redirects - scripts/cloudflare/redirect-rules.json: rule definitions - .github/workflows/cloudflare-redirects.yml: manual trigger workflow - Supports: apply, status, dry-run, delete - Requires secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
name: Setup Cloudflare Redirects
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: "Action to perform"
|
||||||
|
required: true
|
||||||
|
default: "apply"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- apply
|
||||||
|
- status
|
||||||
|
- dry-run
|
||||||
|
- delete
|
||||||
|
|
||||||
|
env:
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
redirects:
|
||||||
|
name: Cloudflare Redirects
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Validate secrets
|
||||||
|
run: |
|
||||||
|
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
|
||||||
|
echo "Error: CLOUDFLARE_API_TOKEN secret is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$CLOUDFLARE_ZONE_ID" ]; then
|
||||||
|
echo "Error: CLOUDFLARE_ZONE_ID secret is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run ${{ inputs.action }}
|
||||||
|
run: |
|
||||||
|
case "${{ inputs.action }}" in
|
||||||
|
status)
|
||||||
|
./scripts/cloudflare/setup-redirects.sh --status
|
||||||
|
;;
|
||||||
|
dry-run)
|
||||||
|
./scripts/cloudflare/setup-redirects.sh --dry-run
|
||||||
|
;;
|
||||||
|
delete)
|
||||||
|
./scripts/cloudflare/setup-redirects.sh --delete
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
./scripts/cloudflare/setup-redirects.sh
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Required repo secrets:
|
||||||
|
# CLOUDFLARE_API_TOKEN — API token with Dynamic Redirects Write permission
|
||||||
|
# CLOUDFLARE_ZONE_ID — Zone ID for moonweb.org (found in Cloudflare dashboard)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"expression": "http.host eq \"hub.moonweb.org\"",
|
||||||
|
"description": "hub.moonweb.org -> www.moonweb.org",
|
||||||
|
"action": "redirect",
|
||||||
|
"action_parameters": {
|
||||||
|
"from_value": {
|
||||||
|
"target_url": {
|
||||||
|
"expression": "concat(\"https://www.moonweb.org\", http.request.uri.path)"
|
||||||
|
},
|
||||||
|
"status_code": 301,
|
||||||
|
"preserve_query_string": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "http.host eq \"infra.moonweb.org\"",
|
||||||
|
"description": "infra.moonweb.org -> www.moonweb.org/infra/",
|
||||||
|
"action": "redirect",
|
||||||
|
"action_parameters": {
|
||||||
|
"from_value": {
|
||||||
|
"target_url": {
|
||||||
|
"expression": "concat(\"https://www.moonweb.org/infra\", http.request.uri.path)"
|
||||||
|
},
|
||||||
|
"status_code": 301,
|
||||||
|
"preserve_query_string": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "http.host eq \"smarthome.moonweb.org\"",
|
||||||
|
"description": "smarthome.moonweb.org -> www.moonweb.org/smarthome/",
|
||||||
|
"action": "redirect",
|
||||||
|
"action_parameters": {
|
||||||
|
"from_value": {
|
||||||
|
"target_url": {
|
||||||
|
"expression": "concat(\"https://www.moonweb.org/smarthome\", http.request.uri.path)"
|
||||||
|
},
|
||||||
|
"status_code": 301,
|
||||||
|
"preserve_query_string": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "http.host eq \"code.moonweb.org\"",
|
||||||
|
"description": "code.moonweb.org -> www.moonweb.org/code/",
|
||||||
|
"action": "redirect",
|
||||||
|
"action_parameters": {
|
||||||
|
"from_value": {
|
||||||
|
"target_url": {
|
||||||
|
"expression": "concat(\"https://www.moonweb.org/code\", http.request.uri.path)"
|
||||||
|
},
|
||||||
|
"status_code": 301,
|
||||||
|
"preserve_query_string": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "http.host eq \"retro.moonweb.org\"",
|
||||||
|
"description": "retro.moonweb.org -> www.moonweb.org/retro/",
|
||||||
|
"action": "redirect",
|
||||||
|
"action_parameters": {
|
||||||
|
"from_value": {
|
||||||
|
"target_url": {
|
||||||
|
"expression": "concat(\"https://www.moonweb.org/retro\", http.request.uri.path)"
|
||||||
|
},
|
||||||
|
"status_code": 301,
|
||||||
|
"preserve_query_string": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
Executable
+172
@@ -0,0 +1,172 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Setup Cloudflare redirect rules for moonweb.org
|
||||||
|
# Redirects old subdomains to www.moonweb.org subdirectories
|
||||||
|
#
|
||||||
|
# Required env vars:
|
||||||
|
# CLOUDFLARE_API_TOKEN - API token with Dynamic Redirects Write permission
|
||||||
|
# CLOUDFLARE_ZONE_ID - Zone ID for moonweb.org
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./setup-redirects.sh # Apply redirects
|
||||||
|
# ./setup-redirects.sh --dry-run # Show what would be created
|
||||||
|
# ./setup-redirects.sh --status # Show current redirect rules
|
||||||
|
# ./setup-redirects.sh --delete # Delete all redirect rules
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
RULES_FILE="$SCRIPT_DIR/redirect-rules.json"
|
||||||
|
API="https://api.cloudflare.com/client/v4"
|
||||||
|
PHASE="http_request_dynamic_redirect"
|
||||||
|
RULESET_NAME="moonweb-subdomain-redirects"
|
||||||
|
|
||||||
|
DRY_RUN=false
|
||||||
|
STATUS=false
|
||||||
|
DELETE=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case $arg in
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--status) STATUS=true ;;
|
||||||
|
--delete) DELETE=true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "${CLOUDFLARE_API_TOKEN:-}" ] || [ -z "${CLOUDFLARE_ZONE_ID:-}" ]; then
|
||||||
|
echo "Error: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cf_api() {
|
||||||
|
local method=$1
|
||||||
|
local url=$2
|
||||||
|
shift 2
|
||||||
|
curl -s -X "$method" "$url" \
|
||||||
|
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get existing ruleset ID for this phase
|
||||||
|
get_ruleset_id() {
|
||||||
|
cf_api GET "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets" | \
|
||||||
|
python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for r in data.get('result', []):
|
||||||
|
if r.get('phase') == '$PHASE':
|
||||||
|
print(r['id'])
|
||||||
|
sys.exit(0)
|
||||||
|
print('')
|
||||||
|
" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get rules in a ruleset
|
||||||
|
get_rules() {
|
||||||
|
local ruleset_id=$1
|
||||||
|
cf_api GET "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets/$ruleset_id" | \
|
||||||
|
python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for r in data.get('result', {}).get('rules', []):
|
||||||
|
desc = r.get('description', 'unnamed')
|
||||||
|
expr = r.get('expression', '')
|
||||||
|
print(f' {desc}')
|
||||||
|
print(f' expression: {expr}')
|
||||||
|
" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Show current status
|
||||||
|
if [ "$STATUS" = true ]; then
|
||||||
|
echo "=== Current redirect rules ==="
|
||||||
|
ruleset_id=$(get_ruleset_id)
|
||||||
|
if [ -z "$ruleset_id" ]; then
|
||||||
|
echo "No redirect ruleset found for phase $PHASE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Ruleset ID: $ruleset_id"
|
||||||
|
get_rules "$ruleset_id"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete all redirect rules
|
||||||
|
if [ "$DELETE" = true ]; then
|
||||||
|
echo "=== Deleting redirect rules ==="
|
||||||
|
ruleset_id=$(get_ruleset_id)
|
||||||
|
if [ -z "$ruleset_id" ]; then
|
||||||
|
echo "No redirect ruleset found, nothing to delete"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Deleting ruleset $ruleset_id..."
|
||||||
|
RESULT=$(cf_api DELETE "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets/$ruleset_id")
|
||||||
|
echo "$RESULT" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
if data.get('success'):
|
||||||
|
print('Deleted successfully')
|
||||||
|
else:
|
||||||
|
for e in data.get('errors', []):
|
||||||
|
print(f\"Error: {e.get('message', '')}\")
|
||||||
|
" 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build payload from JSON file
|
||||||
|
PAYLOAD=$(python3 -c "
|
||||||
|
import json
|
||||||
|
with open('$RULES_FILE') as f:
|
||||||
|
rules = json.load(f)
|
||||||
|
payload = {
|
||||||
|
'name': '$RULESET_NAME',
|
||||||
|
'kind': 'zone',
|
||||||
|
'phase': '$PHASE',
|
||||||
|
'rules': rules
|
||||||
|
}
|
||||||
|
print(json.dumps(payload))
|
||||||
|
")
|
||||||
|
|
||||||
|
RULE_COUNT=$(echo "$PAYLOAD" | python3 -c "import sys, json; print(len(json.load(sys.stdin)['rules']))" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = true ]; then
|
||||||
|
echo "=== DRY RUN ==="
|
||||||
|
echo "Would create/update ruleset '$RULESET_NAME' with $RULE_COUNT rules:"
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('$RULES_FILE') as f:
|
||||||
|
rules = json.load(f)
|
||||||
|
for r in rules:
|
||||||
|
print(f\" - {r['description']}\")
|
||||||
|
" 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if ruleset already exists
|
||||||
|
echo "=== Checking existing ruleset ==="
|
||||||
|
EXISTING_ID=$(get_ruleset_id)
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_ID" ]; then
|
||||||
|
echo "Updating existing ruleset $EXISTING_ID..."
|
||||||
|
RESULT=$(cf_api PUT "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets/$EXISTING_ID" \
|
||||||
|
-d "$PAYLOAD")
|
||||||
|
else
|
||||||
|
echo "Creating new ruleset..."
|
||||||
|
RESULT=$(cf_api POST "$API/zones/$CLOUDFLARE_ZONE_ID/rulesets" \
|
||||||
|
-d "$PAYLOAD")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check result
|
||||||
|
echo "$RESULT" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
if data.get('success'):
|
||||||
|
ruleset = data['result']
|
||||||
|
print(f\"Success! Ruleset: {ruleset['name']} (ID: {ruleset['id']})\")
|
||||||
|
print(f\"Rules: {len(ruleset.get('rules', []))}\")
|
||||||
|
for r in ruleset.get('rules', []):
|
||||||
|
print(f\" - {r.get('description', 'unnamed')}\")
|
||||||
|
else:
|
||||||
|
print('Error:')
|
||||||
|
for e in data.get('errors', []):
|
||||||
|
print(f\" {e.get('code', '')}: {e.get('message', '')}\")
|
||||||
|
sys.exit(1)
|
||||||
|
" 2>/dev/null
|
||||||
Reference in New Issue
Block a user