Skip to content

Merge pull request #69 from SecureToolsProject/infra/h3-web-utilities… #2

Merge pull request #69 from SecureToolsProject/infra/h3-web-utilities…

Merge pull request #69 from SecureToolsProject/infra/h3-web-utilities… #2

name: Deploy Cloudflare bridge
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
deployments: write
concurrency:
group: web-utilities-cloudflare-bridge
cancel-in-progress: false
jobs:
deploy:
name: Deploy Web Utilities bridge
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
# actions/checkout v6.0.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
- name: Run static tool validation
run: node tests/run-all.mjs
- name: Validate bridge prerequisites
shell: bash
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
set -euo pipefail
missing=()
[[ -n "$CLOUDFLARE_API_TOKEN" ]] || missing+=(CLOUDFLARE_API_TOKEN)
[[ -n "$CLOUDFLARE_ACCOUNT_ID" ]] || missing+=(CLOUDFLARE_ACCOUNT_ID)
if (( ${#missing[@]} > 0 )); then
printf '::error::Missing required GitHub Actions secret: %s\n' "${missing[@]}"
exit 1
fi
[[ "$(<CNAME)" == "securetools.app" ]] || {
echo "::error file=CNAME::Existing GitHub Pages custom domain changed"
exit 1
}
- name: Prepare isolated bridge artifact
shell: bash
env:
BRIDGE_DIRECTORY: ${{ runner.temp }}/secure-tools-web-bridge
run: |
set -euo pipefail
mkdir -p "$BRIDGE_DIRECTORY"
cp -R \
404.html \
index.html \
about \
assets \
css \
js \
privacy \
robots.txt \
sitemap.xml \
tools \
"$BRIDGE_DIRECTORY/"
printf '/*\n X-Robots-Tag: noindex, nofollow\n' > "$BRIDGE_DIRECTORY/_headers"
[[ ! -e "$BRIDGE_DIRECTORY/CNAME" ]]
[[ ! -e "$BRIDGE_DIRECTORY/_redirects" ]]
[[ ! -e "$BRIDGE_DIRECTORY/_worker.js" ]]
[[ ! -d "$BRIDGE_DIRECTORY/functions" ]]
[[ "$(find "$BRIDGE_DIRECTORY" -name index.html -type f | wc -l)" -eq 19 ]]
- name: Validate Cloudflare Pages project isolation
shell: bash
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
set -euo pipefail
project="$(curl --fail --silent --show-error \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/secure-tools-web-bridge" \
--header "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}")"
jq --raw-output '
.result as $project |
"Pages project state: name=\($project.name), production_branch=\($project.production_branch), subdomain=\($project.subdomain), custom_domain_count=\($project.domains // [] | map(select(. != $project.subdomain)) | length), source=\(if $project.source == null then "direct-upload" else $project.source.type end), web_analytics=\(if ($project.build_config.web_analytics_tag // "") == "" and ($project.build_config.web_analytics_token // "") == "" then "disabled" else "enabled" end)"
' <<< "$project"
jq --exit-status '
.result as $project |
.success == true and
$project.name == "secure-tools-web-bridge" and
$project.production_branch == "main" and
$project.subdomain == "secure-tools-web-bridge.pages.dev" and
($project.domains // [] | map(select(. != $project.subdomain)) | length) == 0 and
$project.source == null and
($project.build_config.web_analytics_tag // "") == "" and
($project.build_config.web_analytics_token // "") == ""
' <<< "$project" > /dev/null || {
echo "::error::Cloudflare Pages project is missing or violates the H3.2 isolation contract"
exit 1
}
echo "Validated Direct Upload project secure-tools-web-bridge: production branch main, stable pages.dev subdomain only, zero custom domains, no Git integration, no Web Analytics."
- name: Deploy to Cloudflare Pages
id: deploy
# cloudflare/wrangler-action v4.0.0
uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
command: pages deploy "${{ runner.temp }}/secure-tools-web-bridge" --project-name=secure-tools-web-bridge --branch=main --commit-hash=${{ github.sha }}
- name: Validate deployed bridge
shell: bash
env:
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
run: |
set -euo pipefail
[[ "$DEPLOYMENT_URL" == https://*.pages.dev ]] || {
echo "::error::Wrangler did not return a Pages deployment URL"
exit 1
}
DEPLOYMENT_URL="${DEPLOYMENT_URL%/}"
routes=(
/
/privacy/
/about/
/tools/pdf/
/tools/pdf/images-to-pdf/
/tools/pdf/merge/
/tools/pdf/split/
/tools/pdf/organize/
/tools/pdf/to-images/
/tools/pdf/metadata/
/tools/image/
/tools/image/converter/
/tools/image/resize/
/tools/image/compress/
/tools/image/metadata/
/tools/privacy/
/tools/scan/
/tools/media/
/tools/image-to-pdf/
)
for route in "${routes[@]}"; do
status="$(curl --silent --show-error --output /dev/null --max-redirs 0 --write-out '%{http_code}' "${DEPLOYMENT_URL}${route}")"
[[ "$status" == 200 ]] || {
echo "::error::Bridge route ${route} returned HTTP ${status}"
exit 1
}
done
for asset in \
/css/base.css \
/css/components.css \
/css/pages.css \
/js/theme-bootstrap.js \
/js/main.js \
/assets/icons/favicon.ico \
/assets/vendor/pdf-lib/pdf-lib.min.js; do
curl --fail --silent --show-error --output /dev/null "${DEPLOYMENT_URL}${asset}"
done
headers="$(curl --fail --silent --show-error --head "${DEPLOYMENT_URL}/" | tr -d '\r')"
grep -Eiq '^x-robots-tag: *noindex, *nofollow$' <<< "$headers" || {
echo "::error::Bridge root is missing X-Robots-Tag: noindex, nofollow"
exit 1
}