Skip to content

feat(ci): automate the learn-cli deploy pipeline — merge deploys prod, dispatch previews#14

Merged
OriNachum merged 15 commits into
mainfrom
feat/deploy-worker-pipeline
Jul 11, 2026
Merged

feat(ci): automate the learn-cli deploy pipeline — merge deploys prod, dispatch previews#14
OriNachum merged 15 commits into
mainfrom
feat/deploy-worker-pipeline

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

Automate the learn-cli deploy pipeline

Turns the Worker go-live from a hand-run, non-reproducible wrangler runbook
(that auto-mode blocks the agent from running) into a CI pipeline: merging to
main deploys the learn-api Worker + applies the D1 schema
, and a branch
workflow_dispatch previews the same jobs against a non-production target
so
the pipeline is proven green before the merge path is trusted.

Specced with devague /think, planned with /spec-to-plan, built via
/assign-to-workforce5 tasks, 3 waves, one worktree per task, every merge
TDD-gated
(585 tests green before and after each). The spec + plan are committed
under docs/specs/ and docs/plans/.

What ships

The Astro site already auto-deploys on merge (deploy-site.yml). This adds the
sibling deploy-worker.yml so the Worker + schema stop being manual:

Merge to main (production) Branch workflow_dispatch (preview)
Config top-level wrangler.toml (KV+D1, /learn/* route) new [env.preview]learn-api-preview, no route
Schema d1 execute learn-ledger --remote --file schema.sql applied to a separate preview D1 (learn-ledger-preview)
Deploy wrangler deploy (promotes to prod) wrangler versions upload --env preview (no promote)
Prod impact this is the deploy none — version, route, D1 all untouched

Every production / promote / prod-D1 step is guarded by github.ref == 'refs/heads/main'; the preview path is gated to the complement, so a branch
dispatch can never hit prod.

Key decisions (from the converged spec)

  • Separate preview D1 via [env.preview] — a plain versions upload shares
    the top-level Worker's D1 binding, so a Cloudflare preview environment is how
    "no-promote preview" and "separate preview D1" compose.
  • CI-synced Worker secretsSESSION_SECRET, GITHUB_CLIENT_ID,
    GITHUB_CLIENT_SECRET, INFERENCE_TOKEN, VOICE_TOKEN_SECRET are applied via
    printf '%s' "${{ secrets.X }}" | wrangler secret put X (stdin — never a CLI
    arg, never logged; masked by GitHub). Credential-less forks skip with a notice.
  • Out of scope — the SAM voice bridge (infra/) stays a manual sam deploy;
    no D1 migrations framework (idempotent schema.sql only); the tutoring flip
    (INFERENCE_URL) stays committed config.

Operator prerequisites (before the pipeline runs green)

This PR ships the mechanism; a branch dispatch will fail red until:

  1. Provision the preview D1wrangler d1 create learn-ledger-preview, paste
    its id into the [env.preview] placeholder in wrangler.toml (and the preview
    KV). Production bindings are unchanged.
  2. Create the GitHub Actions secretsCLOUDFLARE_API_TOKEN,
    CLOUDFLARE_ACCOUNT_ID, plus the five Worker secrets above.

Both are documented in workers/learn-api/README.md (deploy section rewritten:
CI is the routine path; local wrangler deploy is a break-glass fallback). The
preview-env OAuth-callback caveat and the post-merge verify (LIVE launch gate vs
BASELINE-2026-07-11.md, plus run-twice idempotency) are documented there too.

Test evidence

  • tests/test_deploy_pipeline_invariants.py — 13 hermetic guard tests
    (proven non-vacuous by mutation): prod steps gated to main, no sam/infra
    reference (robust word-boundary check), deploys wrangler.toml not
    wrangler.signedout.toml, no secret as a CLI arg, all five secrets synced on
    both paths, schema.sql fully idempotent, preview uses non-promoting
    versions upload.
  • Full suite: 585 passed, 12 skipped (launch-gate e2e hermetic default);
    coverage 94.91% (floor 60%).
  • black / isort / flake8 / bandit clean; markdownlint 0; rubric
    teken cli doctor --strict PASS. Version bumped 0.6.00.7.0.

Follow-ups (not this PR)

  • Operator provisions the preview D1 + sets the Actions secrets, then a branch
    dispatch verifies the pipeline end-to-end.

  • Voice bridge (infra/) SAM deploy remains manual.

  • learn-cli (Claude)

OriNachum and others added 13 commits July 11, 2026 19:38
…spec-to-plan)

Converged frame + buildable plan for merge-to-main deploys of the whole
/learn stack (site + learn-api Worker + D1 schema) with a branch
workflow_dispatch preview path (versions upload --env preview, separate
preview D1, CI-synced secrets). 5 tasks / 3 waves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QpvZY8K67Q9uGA6keFhTz
…iew D1, no route)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QpvZY8K67Q9uGA6keFhTz
Exported spec/plan quote claim text verbatim, which can contain a bare URL
(e.g. LIVE_ORIGIN=https://agentculture.org). Consistent with the existing
MD033: false relaxation — don't force rewording generated text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QpvZY8K67Q9uGA6keFhTz
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QpvZY8K67Q9uGA6keFhTz
@OriNachum

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Jul 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Automate learn-api Worker deploys: prod on main, preview on dispatch

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Add deploy-worker GitHub Actions workflow for prod deploys and safe preview uploads.
• Introduce Wrangler preview environment with isolated KV/D1 bindings and no routes.
• Add invariant tests and docs to prevent prod leaks and document operator setup.
Diagram

graph TD
S["GitHub Secrets"] --> W["GitHub Actions: deploy-worker"]
W --> R["wrangler (npx)"] --> WP["Worker (prod)"]
R --> D1P[("D1 prod")]
R --> WV["Worker (preview env)"]
R --> D1V[("D1 preview")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Protect prod via GitHub Environments
  • ➕ Adds an explicit approval gate and audit trail for production deploys
  • ➕ Can centralize prod-only secrets in the environment instead of repo-level secrets
  • ➖ More setup and operational ceremony; may slow down routine merges
  • ➖ Still needs ref/trigger discipline for preview vs prod behaviors
2. Use a reusable workflow or combine with deploy-site.yml
  • ➕ Deduplicates common steps (checkout/setup-node/skip notices)
  • ➕ Could provide a single “deploy /learn stack” entrypoint
  • ➖ Couples site and Worker deploy cadence; reduces independent triggering/path filtering
  • ➖ Harder to reason about safety boundaries if the workflow grows
3. Use cloudflare/wrangler-action instead of npx wrangler
  • ➕ Standardizes Wrangler setup/auth and may reduce boilerplate
  • ➕ Action-level updates can encapsulate best practices
  • ➖ Adds a third-party action dependency and implicit behavior
  • ➖ Less transparent than explicit CLI invocations for boundary-auditing tests

Recommendation: The PR’s current approach (explicit npx wrangler@4 commands plus hard github.ref gating and invariant tests) is the most auditable and easiest to mechanically validate. Consider adding a GitHub Environment for production later if you want an approval gate, but keep the current ref-based mutual exclusion and non-promoting preview upload.

Files changed (14) +1462 / -30

Tests (1) +343 / -0
test_deploy_pipeline_invariants.pyAdd hermetic tests enforcing deploy-worker safety boundaries +343/-0

Add hermetic tests enforcing deploy-worker safety boundaries

• Adds text/regex-based tests that assert prod steps are main-gated, preview uses non-promoting uploads, secrets are piped via stdin, schema.sql remains idempotent, and the workflow never references SAM/infra.

tests/test_deploy_pipeline_invariants.py

Documentation (6) +892 / -27
learn-cli-ships-a-one-command-free-deployment-pipe.jsonAdd exported devague frame for deploy-worker automation +369/-0

Add exported devague frame for deploy-worker automation

• Introduces the structured frame capturing before/after, requirements, decisions, and success signals for CI-based Worker deploys and preview runs.

.devague/frames/learn-cli-ships-a-one-command-free-deployment-pipe.json

learn-cli-ships-a-one-command-free-deployment-pipe.jsonAdd exported devague plan detailing tasks for the deploy pipeline +254/-0

Add exported devague plan detailing tasks for the deploy pipeline

• Adds the task breakdown and acceptance criteria for preview env config, workflow creation, invariant tests, docs, and version bump.

.devague/plans/learn-cli-ships-a-one-command-free-deployment-pipe.json

CHANGELOG.mdDocument 0.7.0 release for automated Worker deploy pipeline +8/-0

Document 0.7.0 release for automated Worker deploy pipeline

• Adds a 0.7.0 entry describing the new deploy-worker workflow, preview verification path, and CI-synced secrets.

CHANGELOG.md

2026-07-11-learn-cli-ships-a-one-command-free-deployment-pipe.mdAdd build plan markdown for deploy-worker automation +52/-0

Add build plan markdown for deploy-worker automation

• Adds the human-readable plan document enumerating tasks, acceptance criteria, and dependencies for the CI deploy pipeline work.

docs/plans/2026-07-11-learn-cli-ships-a-one-command-free-deployment-pipe.md

2026-07-11-learn-cli-ships-a-one-command-free-deployment-pipe.mdAdd spec markdown for CI-based Worker deploys and previews +77/-0

Add spec markdown for CI-based Worker deploys and previews

• Introduces the spec covering goals, requirements, boundaries (no infra/SAM), and the prod-vs-preview design using Cloudflare preview environments.

docs/specs/2026-07-11-learn-cli-ships-a-one-command-free-deployment-pipe.md

README.mdRewrite deploy docs around the new CI pipeline and operator prerequisites +132/-27

Rewrite deploy docs around the new CI pipeline and operator prerequisites

• Replaces the routine deploy path with CI (prod on main, preview via workflow_dispatch), documents one-time preview D1/KV provisioning and required secrets, and adds post-merge verification + break-glass manual steps.

workers/learn-api/README.md

Other (7) +227 / -3
currentPoint devague tracker at the new deploy-pipeline frame +1/-1

Point devague tracker at the new deploy-pipeline frame

• Updates the current devague slug to the deployment-pipeline work item so the tooling references the new spec/plan.

.devague/current

current_planPoint devague tracker at the new deploy-pipeline plan +1/-1

Point devague tracker at the new deploy-pipeline plan

• Updates the current devague plan slug to the deployment-pipeline plan so the tooling follows the new workstream.

.devague/current_plan

deploy-worker.ymlAdd CI workflow to deploy prod Worker on main and upload previews on dispatch +172/-0

Add CI workflow to deploy prod Worker on main and upload previews on dispatch

• Creates a dedicated Worker pipeline that applies D1 schema, syncs secrets via stdin, and deploys with 'wrangler deploy' only on main. Non-main refs run against preview D1 and upload a non-promoted version via 'wrangler versions upload --env preview', with Cloudflare secrets gating and job summaries.

.github/workflows/deploy-worker.yml

.markdownlint-cli2.yamlAllow bare URLs in plans markdownlint config +3/-0

Allow bare URLs in plans markdownlint config

• Disables MD034 to permit verbatim URL quoting in plan documents (e.g., LIVE_ORIGIN=...).

docs/plans/.markdownlint-cli2.yaml

.markdownlint-cli2.yamlAllow bare URLs in specs markdownlint config +3/-0

Allow bare URLs in specs markdownlint config

• Disables MD034 to permit bare URLs in spec text where required for clarity and reproducibility.

docs/specs/.markdownlint-cli2.yaml

pyproject.tomlBump learn-cli version to 0.7.0 +1/-1

Bump learn-cli version to 0.7.0

• Updates the project version to satisfy the repo’s version-bump gate and reflect the new deployment feature.

pyproject.toml

wrangler.tomlAdd [env.preview] Worker environment with isolated KV/D1 and no routes +46/-0

Add [env.preview] Worker environment with isolated KV/D1 and no routes

• Introduces a preview environment named learn-api-preview that mirrors required vars while binding to separate preview KV and D1 placeholders, explicitly omitting routes so preview runs cannot affect production.

workers/learn-api/wrangler.toml

@qodo-code-review

qodo-code-review Bot commented Jul 11, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 9 rules

Grey Divider


Action required

1. Secrets synced without validation ✓ Resolved 🐞 Bug ⛨ Security
Description
.github/workflows/deploy-worker.yml pushes Worker secrets via wrangler secret put without first
verifying the GitHub secrets are present/non-empty, so a misconfigured repo can end up with
missing/blank runtime secrets or a failed secret-sync step. The Worker code hard-requires these
secrets for core flows (OAuth login/callback, session issuance, voice tokens), so a bad sync can
break production behavior.
Code

.github/workflows/deploy-worker.yml[R96-141]

+      - name: "[prod] Sync Worker secrets"
+        if: >-
+          env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
+          github.ref == 'refs/heads/main'
+        working-directory: workers/learn-api
+        run: |
+          printf '%s' "${{ secrets.SESSION_SECRET }}" | npx wrangler@4 secret put SESSION_SECRET
+          printf '%s' "${{ secrets.GITHUB_CLIENT_ID }}" | npx wrangler@4 secret put GITHUB_CLIENT_ID
+          printf '%s' "${{ secrets.GITHUB_CLIENT_SECRET }}" | npx wrangler@4 secret put GITHUB_CLIENT_SECRET
+          printf '%s' "${{ secrets.INFERENCE_TOKEN }}" | npx wrangler@4 secret put INFERENCE_TOKEN
+          printf '%s' "${{ secrets.VOICE_TOKEN_SECRET }}" | npx wrangler@4 secret put VOICE_TOKEN_SECRET
+
+      # The only step in this workflow allowed to touch production: promotes
+      # the top-level (production) wrangler.toml config — never
+      # wrangler.signedout.toml, never an --env flag.
+      - name: "[prod] wrangler deploy"
+        if: >-
+          env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
+          github.ref == 'refs/heads/main'
+        working-directory: workers/learn-api
+        run: npx wrangler@4 deploy
+
+      # -----------------------------------------------------------------
+      # PREVIEW path — workflow_dispatch from any branch OTHER than main.
+      # Targets the [env.preview] config only; never deploys/promotes to
+      # production.
+      # -----------------------------------------------------------------
+
+      - name: "[preview] Apply D1 schema (learn-ledger-preview)"
+        if: >-
+          env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
+          github.ref != 'refs/heads/main'
+        working-directory: workers/learn-api
+        run: npx wrangler@4 d1 execute learn-ledger-preview --remote --file schema.sql
+
+      - name: "[preview] Sync Worker secrets"
+        if: >-
+          env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
+          github.ref != 'refs/heads/main'
+        working-directory: workers/learn-api
+        run: |
+          printf '%s' "${{ secrets.SESSION_SECRET }}" | npx wrangler@4 secret put SESSION_SECRET --env preview
+          printf '%s' "${{ secrets.GITHUB_CLIENT_ID }}" | npx wrangler@4 secret put GITHUB_CLIENT_ID --env preview
+          printf '%s' "${{ secrets.GITHUB_CLIENT_SECRET }}" | npx wrangler@4 secret put GITHUB_CLIENT_SECRET --env preview
+          printf '%s' "${{ secrets.INFERENCE_TOKEN }}" | npx wrangler@4 secret put INFERENCE_TOKEN --env preview
+          printf '%s' "${{ secrets.VOICE_TOKEN_SECRET }}" | npx wrangler@4 secret put VOICE_TOKEN_SECRET --env preview
Evidence
The workflow unconditionally pipes secrets.* values into wrangler secret put (no
presence/non-empty checks). The Worker implementation throws/errors when these secrets are missing,
so an invalid secret sync can break live auth/session/voice functionality.

.github/workflows/deploy-worker.yml[96-151]
workers/learn-api/src/index.js[218-231]
workers/learn-api/src/session.js[39-55]
workers/learn-api/src/voice.js[70-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The deploy workflow syncs Worker secrets to Cloudflare using values from GitHub Actions secrets, but it does not validate that those secrets exist and are non-empty before calling `wrangler secret put`. If any required secret is missing/misconfigured in GitHub, the workflow may misconfigure the Worker secrets (or fail partway through secret sync), and the deployed Worker will then fail at runtime.

## Issue Context
The Worker runtime explicitly requires these secrets for critical routes (OAuth, session/token issuance). CI should guard this at deploy time with a clear, early failure rather than relying on post-deploy runtime failures.

## Fix Focus Areas
- .github/workflows/deploy-worker.yml[85-151]

## What to change
- Add an explicit “verify required secrets present” step (for both prod and preview paths) before any `wrangler secret put` lines.
- In that step, check each of: `SESSION_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `INFERENCE_TOKEN`, `VOICE_TOKEN_SECRET` for non-empty values, and `exit 1` with an actionable error if any are missing.
- Keep the existing Cloudflare-token skip behavior, but once Cloudflare credentials are present, do not proceed to secret sync unless all required app secrets are present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Dispatch can deploy prod 🐞 Bug ☼ Reliability
Description
Because workflow_dispatch is enabled and the production steps are gated only by `github.ref ==
'refs/heads/main', manually running the workflow on main` will execute the production schema
apply, secret sync, and wrangler deploy. This contradicts the “dispatch previews” safety
expectation and makes accidental production mutation much easier than intended.
Code

.github/workflows/deploy-worker.yml[R42-116]

+on:
+  push:
+    branches: [main]
+    paths:
+      - 'workers/learn-api/**'
+      - '.github/workflows/deploy-worker.yml'
+  workflow_dispatch: {}
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+    concurrency:
+      group: worker-deploy-${{ github.ref }}
+      cancel-in-progress: true
+    # Mapped here so later steps can gate with `if: env.CLOUDFLARE_API_TOKEN
+    # != ''` — secrets.* isn't usable directly in an `if:` conditional.
+    env:
+      CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+      CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+    steps:
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+
+      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+        with:
+          node-version: '20'
+
+      - name: Skip notice (Cloudflare secrets not configured)
+        if: env.CLOUDFLARE_API_TOKEN == '' || env.CLOUDFLARE_ACCOUNT_ID == ''
+        run: |
+          echo "::notice::CLOUDFLARE_API_TOKEN and/or CLOUDFLARE_ACCOUNT_ID are not configured on this repo yet — skipping the learn-api Worker deploy (both the production and preview paths). This validates the workflow parses and runs cleanly up to that point."
+          {
+            echo '### Deploy skipped'
+            echo ''
+            echo 'The Cloudflare secrets (`CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID`) are not both set on this repo yet, so the Worker deploy/upload steps were skipped.'
+          } >> "$GITHUB_STEP_SUMMARY"
+
+      # -----------------------------------------------------------------
+      # PRODUCTION path — push to main, or a workflow_dispatch run whose
+      # ref is main. Never runs on any other branch.
+      # -----------------------------------------------------------------
+
+      - name: "[prod] Apply D1 schema (learn-ledger)"
+        if: >-
+          env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
+          github.ref == 'refs/heads/main'
+        working-directory: workers/learn-api
+        run: npx wrangler@4 d1 execute learn-ledger --remote --file schema.sql
+
+      # Secrets are synced BEFORE the deploy below so the deployed Worker
+      # version has them from the moment it goes live. Each value is piped
+      # over stdin — never a CLI argument — so it never lands in a process
+      # list or shell history.
+      - name: "[prod] Sync Worker secrets"
+        if: >-
+          env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
+          github.ref == 'refs/heads/main'
+        working-directory: workers/learn-api
+        run: |
+          printf '%s' "${{ secrets.SESSION_SECRET }}" | npx wrangler@4 secret put SESSION_SECRET
+          printf '%s' "${{ secrets.GITHUB_CLIENT_ID }}" | npx wrangler@4 secret put GITHUB_CLIENT_ID
+          printf '%s' "${{ secrets.GITHUB_CLIENT_SECRET }}" | npx wrangler@4 secret put GITHUB_CLIENT_SECRET
+          printf '%s' "${{ secrets.INFERENCE_TOKEN }}" | npx wrangler@4 secret put INFERENCE_TOKEN
+          printf '%s' "${{ secrets.VOICE_TOKEN_SECRET }}" | npx wrangler@4 secret put VOICE_TOKEN_SECRET
+
+      # The only step in this workflow allowed to touch production: promotes
+      # the top-level (production) wrangler.toml config — never
+      # wrangler.signedout.toml, never an --env flag.
+      - name: "[prod] wrangler deploy"
+        if: >-
+          env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
+          github.ref == 'refs/heads/main'
+        working-directory: workers/learn-api
+        run: npx wrangler@4 deploy
Evidence
The workflow is configured to allow manual dispatch, and production steps check only `github.ref ==
'refs/heads/main'`, so a dispatch run on main satisfies the production condition and will run prod
mutations.

.github/workflows/deploy-worker.yml[42-49]
.github/workflows/deploy-worker.yml[85-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`workflow_dispatch` is enabled for the deploy-worker workflow, and production actions are guarded only by `github.ref == 'refs/heads/main'`. As a result, a workflow_dispatch run targeting the default branch (`main`) will execute production steps (remote schema apply + prod secret sync + promote deploy).

## Issue Context
The PR description and workflow comments emphasize that branch `workflow_dispatch` runs are meant to be preview verification and should not touch production. The current gating makes it easy to accidentally run a production deploy from the UI.

## Fix Focus Areas
- .github/workflows/deploy-worker.yml[42-151]

## What to change
- Restrict production steps to pushes to main by adding `github.event_name == 'push'` (in addition to `github.ref == 'refs/heads/main'`).
- Restrict preview steps to `github.event_name == 'workflow_dispatch'` (and optionally keep `github.ref != 'refs/heads/main'` if you want to forbid dispatch-on-main entirely).
- Alternatively (if you do want manual prod deploy), add an explicit workflow_dispatch input like `target: preview|prod` and require `target == 'prod'` plus ref==main for prod steps.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread .github/workflows/deploy-worker.yml Outdated
Comment on lines +42 to +116
on:
push:
branches: [main]
paths:
- 'workers/learn-api/**'
- '.github/workflows/deploy-worker.yml'
workflow_dispatch: {}

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: worker-deploy-${{ github.ref }}
cancel-in-progress: true
# Mapped here so later steps can gate with `if: env.CLOUDFLARE_API_TOKEN
# != ''` — secrets.* isn't usable directly in an `if:` conditional.
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'

- name: Skip notice (Cloudflare secrets not configured)
if: env.CLOUDFLARE_API_TOKEN == '' || env.CLOUDFLARE_ACCOUNT_ID == ''
run: |
echo "::notice::CLOUDFLARE_API_TOKEN and/or CLOUDFLARE_ACCOUNT_ID are not configured on this repo yet — skipping the learn-api Worker deploy (both the production and preview paths). This validates the workflow parses and runs cleanly up to that point."
{
echo '### Deploy skipped'
echo ''
echo 'The Cloudflare secrets (`CLOUDFLARE_API_TOKEN` + `CLOUDFLARE_ACCOUNT_ID`) are not both set on this repo yet, so the Worker deploy/upload steps were skipped.'
} >> "$GITHUB_STEP_SUMMARY"

# -----------------------------------------------------------------
# PRODUCTION path — push to main, or a workflow_dispatch run whose
# ref is main. Never runs on any other branch.
# -----------------------------------------------------------------

- name: "[prod] Apply D1 schema (learn-ledger)"
if: >-
env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
github.ref == 'refs/heads/main'
working-directory: workers/learn-api
run: npx wrangler@4 d1 execute learn-ledger --remote --file schema.sql

# Secrets are synced BEFORE the deploy below so the deployed Worker
# version has them from the moment it goes live. Each value is piped
# over stdin — never a CLI argument — so it never lands in a process
# list or shell history.
- name: "[prod] Sync Worker secrets"
if: >-
env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
github.ref == 'refs/heads/main'
working-directory: workers/learn-api
run: |
printf '%s' "${{ secrets.SESSION_SECRET }}" | npx wrangler@4 secret put SESSION_SECRET
printf '%s' "${{ secrets.GITHUB_CLIENT_ID }}" | npx wrangler@4 secret put GITHUB_CLIENT_ID
printf '%s' "${{ secrets.GITHUB_CLIENT_SECRET }}" | npx wrangler@4 secret put GITHUB_CLIENT_SECRET
printf '%s' "${{ secrets.INFERENCE_TOKEN }}" | npx wrangler@4 secret put INFERENCE_TOKEN
printf '%s' "${{ secrets.VOICE_TOKEN_SECRET }}" | npx wrangler@4 secret put VOICE_TOKEN_SECRET

# The only step in this workflow allowed to touch production: promotes
# the top-level (production) wrangler.toml config — never
# wrangler.signedout.toml, never an --env flag.
- name: "[prod] wrangler deploy"
if: >-
env.CLOUDFLARE_API_TOKEN != '' && env.CLOUDFLARE_ACCOUNT_ID != '' &&
github.ref == 'refs/heads/main'
working-directory: workers/learn-api
run: npx wrangler@4 deploy

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Dispatch can deploy prod 🐞 Bug ☼ Reliability

Because workflow_dispatch is enabled and the production steps are gated only by `github.ref ==
'refs/heads/main', manually running the workflow on main` will execute the production schema
apply, secret sync, and wrangler deploy. This contradicts the “dispatch previews” safety
expectation and makes accidental production mutation much easier than intended.
Agent Prompt
## Issue description
`workflow_dispatch` is enabled for the deploy-worker workflow, and production actions are guarded only by `github.ref == 'refs/heads/main'`. As a result, a workflow_dispatch run targeting the default branch (`main`) will execute production steps (remote schema apply + prod secret sync + promote deploy).

## Issue Context
The PR description and workflow comments emphasize that branch `workflow_dispatch` runs are meant to be preview verification and should not touch production. The current gating makes it easy to accidentally run a production deploy from the UI.

## Fix Focus Areas
- .github/workflows/deploy-worker.yml[42-151]

## What to change
- Restrict production steps to pushes to main by adding `github.event_name == 'push'` (in addition to `github.ref == 'refs/heads/main'`).
- Restrict preview steps to `github.event_name == 'workflow_dispatch'` (and optionally keep `github.ref != 'refs/heads/main'` if you want to forbid dispatch-on-main entirely).
- Alternatively (if you do want manual prod deploy), add an explicit workflow_dispatch input like `target: preview|prod` and require `target == 'prod'` plus ref==main for prod steps.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Each 'wrangler secret put' is now guarded by a non-empty check on its
Actions secret. An unset/empty secret is skipped (leaving the deployed
value unchanged) instead of overwriting a live prod secret with '' — so
the first merge can't wipe pre-existing prod secrets, and the currently-
unset optional secrets (INFERENCE_TOKEN, VOICE_TOKEN_SECRET, while
tutoring/voice are off) don't clobber or fail. Adds a guard-assertion
test to lock the invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QpvZY8K67Q9uGA6keFhTz
docs/hosting.md documented what's hosted but not how it deploys. Adds a
durable, findable reference for the two deploy workflows (site + worker),
the merge=prod / dispatch=preview split, and the load-bearing safety
properties, cross-linking the README runbook (step-by-step) and the spec
(rationale).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QpvZY8K67Q9uGA6keFhTz
@sonarqubecloud

Copy link
Copy Markdown

@OriNachum OriNachum merged commit c5b93a2 into main Jul 11, 2026
8 checks passed
@OriNachum OriNachum deleted the feat/deploy-worker-pipeline branch July 11, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant