ソースを参照

CI: PR previews on Railway's own domains, no certificates per PR (#5936)

Each PR preview got two custom hosts, pr-<n>.unacms.app (NEO) and
api-pr-<n>.unacms.app (UNA), and so two new Let's Encrypt certificates.
Cert Spotter lists 153 for unacms.app in the 7 days to 2026-10-20 (up to
47 a day); after pr-5933's, Let's Encrypt refused more, and Railway reports
"rate limit reached", not retryable, for PR #5934's hosts.

Previews now use the Railway domains, neo-pr-<n>.up.railway.app and
una-pr-<n>.up.railway.app, which share Railway's wildcard certificate and
work as soon as they deploy. Nothing needed the unacms.app names: NEO
already reached its UNA through the Railway domain, and the session cookie
belongs to NEO's own host. UNA_CLIENT_URL (links in emails and pushes) and
NEO's APP_URL are now the NEO Railway URL, and the PR comment lists the
Railway URLs. Teardown still removes the unacms.app DNS records of previews
created before this change. Production (api.unacms.app) is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 8 時間 前
親
コミット
8f84074c12
3 ファイル変更、20 行追加、36 行削除
  1. 18 34
      .github/workflows/ci.yml
  2. 1 1
      AGENTS.md
  3. 1 1
      scripts/railway/seed.php

+ 18 - 34
.github/workflows/ci.yml

@@ -155,14 +155,7 @@ jobs:
       RAILWAY_BASE_ENVIRONMENT: ${{ secrets.RAILWAY_ENVIRONMENT }}
       RAILWAY_TEMPLATE_ENVIRONMENT: pr-template
       RAILWAY_PR_ENVIRONMENT: pr-${{ github.event.pull_request.number }}
-      # unacms.app hosts: UNA (service una) and the NEO web client (service neo), both on Railway
-      VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
-      VERCEL_TEAM: yasko
-      PREVIEW_API_HOST: api-pr-${{ github.event.pull_request.number }}.unacms.app
-      PREVIEW_CLIENT_HOST: pr-${{ github.event.pull_request.number }}.unacms.app
       UNA_PREVIEW_API_KEY: ${{ secrets.UNA_PREVIEW_API_KEY }}
-    outputs:
-      domains: ${{ steps.domains.outputs.ok }}
     steps:
     - uses: actions/checkout@v4
       with:
@@ -208,30 +201,27 @@ jobs:
             MYSQLUSER=una \
             MYSQLPASSWORD=una \
             UNA_DEMO_SEED=1 \
-            UNA_CLIENT_URL="https://$PREVIEW_CLIENT_HOST" \
             ${UNA_PREVIEW_API_KEY:+UNA_API_KEY="$UNA_PREVIEW_API_KEY"} \
             && break
           sleep 5
         done
+        # Previews live on their Railway domains (<service>-pr-<n>.up.railway.app), which share
+        # Railway's wildcard certificate. Custom pr-<n> / api-pr-<n> hosts on unacms.app needed two
+        # Let's Encrypt certificates per PR, and a busy week ran into Let's Encrypt's rate limit
+        # (PR #5934 got none). UNA_AUTO_HOSTNAME makes UNA build its URLs from its host.
         railway domain --service una --environment "$RAILWAY_PR_ENVIRONMENT" || true
         railway domain --service neo --environment "$RAILWAY_PR_ENVIRONMENT" || true
-
-        # NEO talks to this PR's UNA through its Railway domain, which has a certificate from
-        # the start (the api-pr host gets one minutes later). UNA_AUTO_HOSTNAME makes UNA build
-        # its URLs from that host.
-        una_host=$(railway domain list --service una --environment "$RAILWAY_PR_ENVIRONMENT" --json | jq -r '.domains[] | select(.type == "service") | .domain' | head -n1)
+        service_host() {
+          railway domain list --service "$1" --environment "$RAILWAY_PR_ENVIRONMENT" --json | jq -r '.domains[] | select(.type == "service") | .domain' | head -n1
+        }
+        una_host=$(service_host una)
+        neo_host=$(service_host neo)
         [ -n "$una_host" ] || { echo "No Railway domain for una" >&2; exit 1; }
-        scripts/railway/neo.sh vars "$RAILWAY_PR_ENVIRONMENT" "https://${una_host#https://}" "https://$PREVIEW_CLIENT_HOST"
-
-    - name: Preview domains on unacms.app
-      id: domains
-      if: env.VERCEL_TOKEN != ''
-      run: |
-        scripts/railway/domains.sh railway-domain "$PREVIEW_API_HOST" "$RAILWAY_PR_ENVIRONMENT" una
-        # Until 2026-10 the client host was a Vercel alias of the neo-ci build; drop it if present.
-        scripts/railway/domains.sh client-unalias "$PREVIEW_CLIENT_HOST"
-        scripts/railway/domains.sh railway-domain "$PREVIEW_CLIENT_HOST" "$RAILWAY_PR_ENVIRONMENT" neo
-        echo "ok=1" >> "$GITHUB_OUTPUT"
+        [ -n "$neo_host" ] || { echo "No Railway domain for neo" >&2; exit 1; }
+        una_url="https://${una_host#https://}"
+        neo_url="https://${neo_host#https://}"
+        railway variable set --service una --environment "$RAILWAY_PR_ENVIRONMENT" --skip-deploys UNA_CLIENT_URL="$neo_url"
+        scripts/railway/neo.sh vars "$RAILWAY_PR_ENVIRONMENT" "$una_url" "$neo_url"
 
   deploy:
     name: Deploy PR to Railway (una)
@@ -292,9 +282,6 @@ jobs:
       RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
       RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
       RAILWAY_PR_ENVIRONMENT: pr-${{ github.event.pull_request.number }}
-      PREVIEW_API_HOST: api-pr-${{ github.event.pull_request.number }}.unacms.app
-      PREVIEW_CLIENT_HOST: pr-${{ github.event.pull_request.number }}.unacms.app
-      DOMAINS_OK: ${{ needs.preview-env.outputs.domains }}
       UNA_RESULT: ${{ needs.deploy.result }}
       NEO_RESULT: ${{ needs.deploy-neo.result }}
       GH_TOKEN: ${{ github.token }}
@@ -319,13 +306,8 @@ jobs:
         state() { [ "$1" = success ] && echo "" || echo " (deploy: $1)"; }
 
         marker="<!-- railway-preview -->"
-        if [ "$DOMAINS_OK" = "1" ]; then
-          body=$(printf '%s\n**Preview**: https://%s (NEO web client%s, also %s)\n**API**: https://%s (UNA%s, also %s)\n\nRailway environment: `%s`. A new custom domain needs a few minutes for its certificate, and a fresh UNA install about 10.' \
-            "$marker" "$PREVIEW_CLIENT_HOST" "$(state "$NEO_RESULT")" "$neo_url" "$PREVIEW_API_HOST" "$(state "$UNA_RESULT")" "$una_url" "$RAILWAY_PR_ENVIRONMENT")
-        else
-          body=$(printf '%s\n**Preview**: %s (NEO web client%s)\n**API**: %s (UNA%s)\n\nRailway environment: `%s`' \
-            "$marker" "$neo_url" "$(state "$NEO_RESULT")" "$una_url" "$(state "$UNA_RESULT")" "$RAILWAY_PR_ENVIRONMENT")
-        fi
+        body=$(printf '%s\n**Preview**: %s (NEO web client%s)\n**API**: %s (UNA%s)\n\nRailway environment: `%s`. A fresh UNA install takes about 10 minutes.' \
+          "$marker" "$neo_url" "$(state "$NEO_RESULT")" "$una_url" "$(state "$UNA_RESULT")" "$RAILWAY_PR_ENVIRONMENT")
 
         comment_id=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \
           --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | contains(\"${marker}\"))) | .id" \
@@ -493,6 +475,8 @@ jobs:
           .github/actions
           scripts/railway
 
+    # Previews created before they moved to Railway domains (2026-10-20) still have DNS records
+    # on unacms.app; this step can go once those PRs are closed.
     - name: Remove preview domains
       if: env.VERCEL_TOKEN != ''
       continue-on-error: true

+ 1 - 1
AGENTS.md

@@ -90,6 +90,6 @@ The pull request description must include the database migration SQL when the ch
 - Branch from `master` and open the PR here. Start the title of a NEO-only PR with `Neo:`.
 - Never commit to unacms/neo. It is a mirror that `.github/workflows/sync-neo.yml` pushes on every push to `master`, and client projects fork it.
 - UNA CI doesn't check `neo/`. Run `yarn typecheck` and `yarn lint` in `neo/` before you push.
-- CI deploys both halves to the `una-ci` Railway project: service `una` (UNA) and service `neo` (the NEO web client, built from `neo/` with `scripts/railway/neo/`). A PR gets `pr-<n>.unacms.app` (NEO) and `api-pr-<n>.unacms.app` (UNA); master deploys production. See `.github/workflows/ci.yml` and `scripts/railway/`.
+- CI deploys both halves to the `una-ci` Railway project: service `una` (UNA) and service `neo` (the NEO web client, built from `neo/` with `scripts/railway/neo/`). A PR gets `neo-pr-<n>.up.railway.app` (NEO) and `una-pr-<n>.up.railway.app` (UNA), Railway's own domains, so previews need no certificates of their own; master deploys production (`api.unacms.app`). See `.github/workflows/ci.yml` and `scripts/railway/`.
 - A UNA service that NEO calls must return JSON for guests too; see [`neo/.agents/skills/una-api/SKILL.md`](neo/.agents/skills/una-api/SKILL.md).
 

+ 1 - 1
scripts/railway/seed.php

@@ -153,7 +153,7 @@ else
 $aPosts = [
     ['Hello from the demo site', "This site is a fresh UNA install deployed from the master branch. Everything here is sample content."],
     ['What NEO is', "NEO is the universal UNA client: one React Native codebase for web, iOS and Android, talking to UNA through its API."],
-    ['Preview environments', "Every pull request to UNA gets its own backend and client on unacms.app, created by CI and removed when the PR closes."],
+    ['Preview environments', "Every pull request to UNA gets its own backend and client on Railway, created by CI and removed when the PR closes."],
     ['Weekend reading', "A few articles about building communities that stay useful: keep threads short, answer quickly, and welcome new members by name."],
     ['Photo walk this Saturday', "Meeting at the old harbour at 10:00. Bring any camera; phones are fine. Coffee afterwards."],
     ['Tips for a good profile', "Add a picture, write two sentences about what you do, and follow a few groups. That's enough to get a useful feed."],