From 20f9f5e74984194d8531475424edf8ec35bc2582 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 14 Sep 2026 21:38:54 +0200 Subject: [PATCH 1/2] feat: migrate from App Engine to Firebase Hosting --- .firebaserc | 5 ++ .github/workflows/deploy.yml | 23 ++++----- .github/workflows/firebase-preview.yml | 35 +++++++++++++ app.yaml | 68 -------------------------- firebase.json | 53 ++++++++++++++++++++ package.json | 5 +- tools/scripts/run_and_test_website.bat | 2 +- tools/scripts/run_and_test_website.sh | 12 ++--- 8 files changed, 115 insertions(+), 88 deletions(-) create mode 100644 .firebaserc create mode 100644 .github/workflows/firebase-preview.yml delete mode 100644 app.yaml create mode 100644 firebase.json diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..0a320928 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "httparchive" + } +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fb31d381..083ac605 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ ## Custom HTTP Archive GitHub action ## ###################################### # -# Deploys the website to Google App Engine (production) on every push to main. +# Deploys the website to Firebase Hosting (production live channel) on every push to main. # Requires a GCP service account key stored as the GCP_SA_KEY repository secret. # name: Deploy to Production @@ -12,9 +12,13 @@ on: branches: - main +permissions: + checks: write + contents: read + jobs: deploy: - name: Build and Deploy to App Engine + name: Build and Deploy to Firebase Hosting (Production) runs-on: ubuntu-latest if: github.repository == 'HTTPArchive/httparchive.org' steps: @@ -32,13 +36,10 @@ jobs: - name: Build website run: npm run build - - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v3 + - name: Deploy to Firebase Hosting (Live) + uses: FirebaseExtended/action-hosting-deploy@v0 with: - credentials_json: ${{ secrets.GCP_SA_KEY }} - - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v3 - - - name: Deploy to App Engine - run: echo "Y" | gcloud app deploy app.yaml --project httparchive + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.GCP_SA_KEY }}' + channelId: live + projectId: httparchive diff --git a/.github/workflows/firebase-preview.yml b/.github/workflows/firebase-preview.yml new file mode 100644 index 00000000..87c7be5f --- /dev/null +++ b/.github/workflows/firebase-preview.yml @@ -0,0 +1,35 @@ +name: Deploy to Firebase Hosting Preview + +on: + pull_request: + +permissions: + checks: write + contents: read + pull-requests: write + +jobs: + build_and_preview: + if: github.repository == 'HTTPArchive/httparchive.org' + runs-on: ubuntu-latest + steps: + - name: Checkout branch + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '24' + + - name: Install dependencies + run: npm ci || npm install + + - name: Build website + run: npm run build + + - name: Deploy to Firebase Hosting Preview Channel + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.GCP_SA_KEY }}' + projectId: httparchive diff --git a/app.yaml b/app.yaml deleted file mode 100644 index 55760020..00000000 --- a/app.yaml +++ /dev/null @@ -1,68 +0,0 @@ -runtime: nodejs24 - -instance_class: F1 - -# Global error handler for 404 pages (serves dist/404.html) -error_handlers: - - file: dist/404.html - -handlers: - # 1. Long-term caching for static fonts (/static/fonts/**) - - url: /static/fonts/(.*\.(woff|woff2))$ - static_files: dist/static/fonts/\1 - upload: dist/static/fonts/.*\.(woff|woff2)$ - secure: always - expiration: 365d - http_headers: - Cache-Control: "public, max-age=31536000, immutable" - X-Frame-Options: SAMEORIGIN - X-Content-Type-Options: nosniff - Referrer-Policy: strict-origin-when-cross-origin - Strict-Transport-Security: max-age=31556926; includeSubDomains - - # 2. Direct static assets pass-through (JS, CSS, images, json, xml, txt, etc.) - - url: /(.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|json|xml|txt))$ - static_files: dist/\1 - upload: dist/(.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|json|xml|txt))$ - secure: always - http_headers: - X-Frame-Options: SAMEORIGIN - X-Content-Type-Options: nosniff - Referrer-Policy: strict-origin-when-cross-origin - Strict-Transport-Security: max-age=31556926; includeSubDomains - - # 3. Root homepage - - url: / - static_files: dist/index.html - upload: dist/index.html - secure: always - http_headers: - X-Frame-Options: SAMEORIGIN - X-Content-Type-Options: nosniff - Referrer-Policy: strict-origin-when-cross-origin - Strict-Transport-Security: max-age=31556926; includeSubDomains - Content-Security-Policy: >- - default-src 'self' https:; - script-src 'self' https: 'unsafe-inline'; - style-src 'self' https: 'unsafe-inline'; - img-src 'self' https: data:; - font-src 'self' https: data:; - connect-src 'self' https:; - - # 4. Clean URLs routing (e.g. /about -> dist/about/index.html) - - url: /(.*) - static_files: dist/\1/index.html - upload: dist/(.*)/index.html - secure: always - http_headers: - X-Frame-Options: SAMEORIGIN - X-Content-Type-Options: nosniff - Referrer-Policy: strict-origin-when-cross-origin - Strict-Transport-Security: max-age=31556926; includeSubDomains - Content-Security-Policy: >- - default-src 'self' https:; - script-src 'self' https: 'unsafe-inline'; - style-src 'self' https: 'unsafe-inline'; - img-src 'self' https: data:; - font-src 'self' https: data:; - connect-src 'self' https:; diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..8f8fc009 --- /dev/null +++ b/firebase.json @@ -0,0 +1,53 @@ +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "cleanUrls": true, + "trailingSlash": false, + "headers": [ + { + "source": "**", + "headers": [ + { + "key": "X-Frame-Options", + "value": "SAMEORIGIN" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "Referrer-Policy", + "value": "strict-origin-when-cross-origin" + }, + { + "key": "Strict-Transport-Security", + "value": "max-age=31556926; includeSubDomains" + }, + { + "key": "Content-Security-Policy", + "value": "default-src 'self' https:; script-src 'self' https: 'unsafe-inline'; style-src 'self' https: 'unsafe-inline'; img-src 'self' https: data:; font-src 'self' https: data:; connect-src 'self' https:;" + } + ] + }, + { + "source": "/static/fonts/**", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=31536000, immutable" + } + ] + } + ] + }, + "emulators": { + "hosting": { + "port": 8080 + } + } +} diff --git a/package.json b/package.json index f3ae1ae9..4bacb631 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "build": "npm run timestamps && npm run generate && npm run astro:build", "astro:build": "astro build", "astro:dev": "astro dev", - "start": "astro dev", - "deploy": "npm run build && gcloud app deploy app.yaml", + "start": "npm run build && npx -y firebase-tools@latest emulators:start --only hosting", + "stage": "npm run build && npx -y firebase-tools@latest hosting:channel:deploy staging", + "deploy": "npm run build && npx -y firebase-tools@latest deploy --only hosting", "lint": "run-script-os", "lint:darwin:linux": "docker container run -it --rm --platform linux/amd64 -e SHELL=/bin/bash -v \"$PWD\":/app -w /app --entrypoint=./tools/scripts/run_linter_locally.sh ghcr.io/super-linter/super-linter:slim-v8", "lint:win32": "docker container run --rm -e SHELL=/bin/bash -v \"%cd%\":/app -w /app/ --entrypoint=./tools/scripts/run_linter_locally.sh ghcr.io/super-linter/super-linter:slim-v8", diff --git a/tools/scripts/run_and_test_website.bat b/tools/scripts/run_and_test_website.bat index a4064776..6aa0e12a 100755 --- a/tools/scripts/run_and_test_website.bat +++ b/tools/scripts/run_and_test_website.bat @@ -19,7 +19,7 @@ echo "Building website" call npm run build echo "Starting website" -start npx -y serve dist -p 8080 +start npx -y firebase-tools@latest emulators:start --only hosting rem # Sleep for 5 seconds to make sure server is up timeout /t 5 /nobreak rem # Use sleep as well in case running in GitBash where above command fails diff --git a/tools/scripts/run_and_test_website.sh b/tools/scripts/run_and_test_website.sh index 792cfa6e..fbcf51e4 100755 --- a/tools/scripts/run_and_test_website.sh +++ b/tools/scripts/run_and_test_website.sh @@ -41,10 +41,10 @@ while getopts "h?d" opt; do done shift "$((OPTIND-1))" # Discard the options and sentinel -- -# Kill existing static server if running -if pgrep -f "serve.*dist" > /dev/null; then - echo "Killing existing server to run a fresh version" - pkill -f "serve.*dist" || true +# Kill existing hosting emulator if running +if pgrep -f "firebase-tools.*hosting" > /dev/null; then + echo "Killing existing emulator to run a fresh version" + pkill -f "firebase-tools.*hosting" || true fi echo "Installing node modules" @@ -53,8 +53,8 @@ npm install --legacy-peer-deps echo "Building website" npm run build -echo "Starting static server on port 8080 for tests" -npx -y serve dist -p 8080 & +echo "Starting Firebase Hosting emulator in background mode for tests" +npx -y firebase-tools@latest emulators:start --only hosting & SERVER_PID=$! # Ensure the background server is terminated on exit unless KEEP_SERVER_RUNNING is true From 7fa9785d769852fdf43a2fe812323e0af229c706 Mon Sep 17 00:00:00 2001 From: Max Ostapenko <1611259+max-ostapenko@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:30:55 +0200 Subject: [PATCH 2/2] fix(ci): use node 24 and increase emulator startup timeout in test workflow --- .github/workflows/test-website.yml | 2 +- tools/scripts/run_and_test_website.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-website.yml b/.github/workflows/test-website.yml index 09c5975e..ecbc94d4 100644 --- a/.github/workflows/test-website.yml +++ b/.github/workflows/test-website.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node.js for use with actions uses: actions/setup-node@v7 with: - node-version: '26' + node-version: '24' - name: Run the website env: diff --git a/tools/scripts/run_and_test_website.sh b/tools/scripts/run_and_test_website.sh index fbcf51e4..1bc2f381 100755 --- a/tools/scripts/run_and_test_website.sh +++ b/tools/scripts/run_and_test_website.sh @@ -65,7 +65,7 @@ else fi echo "Waiting for server to start..." -timeout=15 +timeout=45 while ! curl -s http://127.0.0.1:8080 > /dev/null; do sleep 1 timeout=$((timeout-1))