-
Notifications
You must be signed in to change notification settings - Fork 119
feat(infra): deploy a second geographic instance from the stage string #908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Makisuo
wants to merge
1
commit into
main
Choose a base branch
from
worktree-eu-region
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| name: Deploy one production instance | ||
|
|
||
| # The body of a production deploy, for one geographic instance. `deploy-prd.yml` | ||
| # calls it once per instance — `us` on every green CI run, `eu` once the EU | ||
| # instance exists — so the two share one set of steps and differ only in the | ||
| # alchemy stage (`prd` / `prd-eu`), the GitHub environment, the Infisical | ||
| # environment (same variable names, that instance's values) and the AWS region. | ||
| # A caller passes the commit to deploy explicitly: inside a reusable workflow | ||
| # `github.sha` is still the default-branch head, not the commit CI ran against. | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| region: | ||
| description: Which instance to deploy, `us` or `eu`. | ||
| required: true | ||
| type: string | ||
| commit-sha: | ||
| description: The commit being deployed, stamped onto telemetry as `vcs.ref.head.revision`. | ||
| required: true | ||
| type: string | ||
|
|
||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 45 | ||
| # `production` holds the us instance's protection rules and variables; | ||
| # `production-eu` the EU instance's. The Infisical environment follows the | ||
| # same naming: `prod` and `prod-eu`. | ||
| environment: ${{ inputs.region == 'us' && 'production' || format('production-{0}', inputs.region) }} | ||
| env: | ||
| MAPLE_REGION: ${{ inputs.region }} | ||
| # `prd` for us, `prd-eu` for eu — the stage string carries the instance, | ||
| # and alchemy keys its state by it, so the two never share a plan. | ||
| MAPLE_STAGE: ${{ inputs.region == 'us' && 'prd' || format('prd-{0}', inputs.region) }} | ||
| INFISICAL_ENV_SLUG: ${{ inputs.region == 'us' && 'prod' || format('prod-{0}', inputs.region) }} | ||
| # Must match `resolveAwsRegion` for the instance; the stack refuses a mismatch. | ||
| AWS_REGION: ${{ inputs.region == 'us' && 'us-east-1' || 'eu-central-1' }} | ||
| API_HOST: ${{ inputs.region == 'us' && 'api.maple.dev' || format('api.{0}.maple.dev', inputs.region) }} | ||
| # Stamped onto deployed telemetry as `vcs.ref.head.revision` (server SDK | ||
| # reads COMMIT_SHA; web build reads VITE_COMMIT_SHA via Vite define). | ||
| COMMIT_SHA: ${{ inputs.commit-sha }} | ||
| VITE_COMMIT_SHA: ${{ inputs.commit-sha }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 | ||
| with: | ||
| ref: ${{ inputs.commit-sha }} | ||
|
|
||
| # Toolchain, Infisical secrets, AWS OIDC (after Infisical, so its | ||
| # credentials win), dependencies, and the ingest binary the caller's | ||
| # `ingest-binary` job compiled — one composite, one order. | ||
| - name: Deploy setup | ||
| id: setup | ||
| uses: ./.github/actions/deploy-setup | ||
| with: | ||
| infisical-env-slug: ${{ env.INFISICAL_ENV_SLUG }} | ||
| infisical-identity-id: ${{ secrets.INFISICAL_MACHINE_IDENTITY_ID }} | ||
| infisical-project-slug: ${{ vars.INFISICAL_PROJECT_SLUG }} | ||
| aws-role-arn: ${{ vars.AWS_DEPLOY_ROLE_ARN }} | ||
| aws-region: ${{ env.AWS_REGION }} | ||
|
|
||
| # NOTE: prod schema migrations are applied OUT OF BAND (manually, via | ||
| # `bun run migrate:prod` → `ps:apply-schema main`, against the direct 5432 | ||
| # port), NOT by this workflow — so a deploy never touches the prod database | ||
| # and needs no MAPLE_PG_URL/admin credential. The worker binds to the | ||
| # pre-configured Hyperdrive config for its instance (`MapleDb` in | ||
| # packages/infra). apply-schema installs default privileges granting | ||
| # PUBLIC before it migrates, so new and rebuilt tables are readable by | ||
| # every consumer — including the ingest gateway, which reaches Postgres | ||
| # through PSBouncer as a role that does NOT inherit `postgres`. | ||
|
|
||
| # alchemy's env-credential path (CI=true) otherwise discovers the account | ||
| # with an STS GetCallerIdentity issued while its own AWSEnvironment is | ||
| # still being built, and that call waits on the half-built environment | ||
| # for its endpoint resolver — a self-deadlock with no network I/O and no | ||
| # log line. That was the "AWS ingest deploy hang" (#378). With the id | ||
| # supplied, the lookup is skipped. Reproduced locally with CI=true and | ||
| # the id unset on alchemy 2.0.0-beta.64 through beta.74. | ||
| # | ||
| # One pass, no retry. A stage's first deploy used to fail here — its | ||
| # ACM certificates were created PENDING_VALIDATION and their 443 | ||
| # listeners refused them — and was recovered by a second step that | ||
| # published the validation CNAMEs with `scripts/acm-cert-validate.sh` | ||
| # and deployed again. The stack now publishes those records itself and | ||
| # waits for ISSUED (`@maple/infra/acm`), so a first deploy completes | ||
| # like any other and a failure here is a real failure. | ||
| - name: Deploy the ${{ inputs.region }} instance with Alchemy | ||
| id: deploy | ||
| run: bun run alchemy:deploy:prd | ||
| env: | ||
| AWS_ACCOUNT_ID: ${{ steps.setup.outputs.aws-account-id }} | ||
|
|
||
| # Alchemy isolates per-resource failures on purpose: a Worker that | ||
| # fails to upload never interrupts its siblings, so a deploy can | ||
| # leave production serving two commits at once. It did on | ||
| # 2026-09-07 — `api` was rejected with `ScriptStartupError` while | ||
| # `app`, `landing`, `alerting` and `ingest` all shipped, and prod | ||
| # ran a 6h-old api behind a current web until someone read the log. | ||
| # | ||
| # `always()`: when the deploy step fails this is exactly when the | ||
| # answer matters — it names which Worker is stale instead of | ||
| # leaving it in a 4000-line log. It also catches the quieter case | ||
| # the deploy cannot report at all, where alchemy succeeds but the | ||
| # script serving traffic is not the one we just uploaded. | ||
| # | ||
| # Liveness only. Every other prod Worker is covered by the | ||
| # "Prod revision skew" alert, which compares | ||
| # `vcs.ref.head.revision` across services from their own telemetry. | ||
| # Which services that is, and the fact that it assumes they always | ||
| # deploy together, is pinned in `PRD_LOCKSTEP_REVISION_SERVICES` | ||
| # (`packages/infra/src/env.ts`) — change what this workflow deploys | ||
| # and `env.test.ts` will tell you the rule needs editing too. | ||
| - name: Verify the deployed api serves this commit | ||
| if: ${{ always() && steps.deploy.outcome != 'skipped' }} | ||
| env: | ||
| EXPECTED: ${{ env.COMMIT_SHA }} | ||
| run: | | ||
| set -uo pipefail | ||
| # Cloudflare propagates a new script over a few seconds, so a | ||
| # single probe races the rollout rather than testing it. | ||
| for attempt in 1 2 3 4 5 6; do | ||
| served=$(curl -fsS --max-time 10 -D - -o /dev/null "https://$API_HOST/health" 2>/dev/null \ | ||
| | tr -d '\r' | awk 'tolower($1) == "x-maple-revision:" { print $2 }') | ||
| [ "$served" = "$EXPECTED" ] && break | ||
| echo "attempt $attempt: api serves '${served:-<none>}', expected '$EXPECTED'" | ||
| sleep 10 | ||
| done | ||
| if [ "$served" = "$EXPECTED" ]; then | ||
| echo "api ($API_HOST) is serving $EXPECTED" | ||
| exit 0 | ||
| fi | ||
| if [ -z "$served" ]; then | ||
| echo "::error::api /health on $API_HOST returned no x-maple-revision header. Either the Worker predates this check or it is not answering — check the deploy log for a resource that reported 'fail'." | ||
| else | ||
| echo "::error::PARTIAL DEPLOY — api on $API_HOST is serving $served but this run deployed $EXPECTED. The api Worker did not update; other Workers likely did. Find the resource that reported 'fail' in the deploy log above." | ||
| fi | ||
| exit 1 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: MapleTechLabs/maple
Length of output: 16870
Sensitive Data Exposure
Reachability: Internal
Exploitability: Difficult
CWE: CWE-522 — Insufficiently Protected Credentials
Disable credential persistence on checkout.
actions/checkoutpersists the GitHub token in.git/configby default. Later steps install dependencies and runbun run alchemy:deploy:prdin the same job. Repository-controlled or third-party code in these steps can read the persisted token. The deployment does not require Git authentication after checkout.🔒 Proposed hardening
with: ref: ${{ inputs.commit-sha }} + persist-credentials: false📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.30.0)
[warning] 45-52: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents