Builds the deploy mechanism only for #4. No deploy/preview/provision was run; a maintainer supplies credentials and pulls the trigger later.
Deliverable 1 — .github/workflows/deploy.yml (CD)
Manual workflow_dispatch-only workflow (never runs on push/PR):
Input:stack choice, default prod (also dev).
Guards: runs in the production GitHub Actions environment (its secrets + any required-reviewer/protection rules apply) and a step fails the run if not launched from main.
Steps: checkout → Go 1.26 + TinyGo 0.41.1 + pnpm/Node → pnpm install → apply the .ci/tinygo-net-roundtrip.patch (same as CI) → pnpm run build (Wasm Worker) → pulumi up --stack <stack> via pulumi/actions, which installs the Pulumi CLI and injects the freshly built ../build/worker.mjs through config-map as workerScriptPath.
Concurrency: one deploy per stack, never cancels an in-flight pulumi up.
Every action is pinned by commit SHA (pulumi/actions@8e5e406… # v7.0.0, plus the same SHAs CI uses). actionlint clean.
infra/deploy.go previously registered a bindingless WorkersScript, so the deployed Worker couldn't reach its bucket/secrets/vars. Now the WorkersScript carries the full runtime contract (matches wrangler.jsonc):
Binding (JS var)
Type
Target
REPORTS_BUCKET
r2_bucket
libremail-bug-reports
BUGREPORT_ENC_KEYRING
secrets_store_secret
secretsStoreId / bugreport-enc-keyring
ADMIN_TOKEN
secrets_store_secret
secretsStoreId / bugreport-admin-token
GITHUB_TOKEN
secrets_store_secret
secretsStoreId / github-token
OTEL_EXPORTER_OTLP_HEADERS
secrets_store_secret
secretsStoreId / otel-exporter-otlp-headers
GITHUB_REPO
plain_text
JMR-dev/LibreMail
OTEL_EXPORTER_OTLP_ENDPOINT
plain_text
"" (disables telemetry)
OTEL_SERVICE_NAME
plain_text
libremail-bug-report-ingest
Also:
New cloudflare.WorkersCronTrigger registering the two Friday UTC crons 0 22 * * 5 and 0 23 * * 5 (#13), bound to the Worker.
Real artifact as content: when workerScriptPath is set the Worker uploads it via ContentFile + a program-computed ContentSha256; otherwise the documented placeholder module body is used (keeps the program testable without a build, as #2 did). The CD workflow sets the path.
Kept account-specific values as documented config/placeholders; secretsStoreId is a new required config. The #6/#7 rate-limit ruleset insertion point remains reserved.
Secrets & config a maintainer MUST set before the first deploy
1. production environment secrets (Settings → Environments → production):
Secret
Purpose
PULUMI_ACCESS_TOKEN
Pulumi Cloud token (state backend). Self-managed backend → use the action's cloud-url input + PULUMI_CONFIG_PASSPHRASE instead.
3. Cloudflare Secrets Store — under secretsStoreId, store the four secret values: bugreport-enc-keyring, bugreport-admin-token, github-token, otel-exporter-otlp-headers. (This stack binds them by name; it does not create the values.)
No pulumi up/preview, no wrangler deploy, no gh workflow run, no cloud resources/secrets/environments created. The maintainer configures the above and triggers the deploy.
Builds the **deploy mechanism only** for #4. No deploy/preview/provision was run; a maintainer supplies credentials and pulls the trigger later.
## Deliverable 1 — `.github/workflows/deploy.yml` (CD)
Manual **`workflow_dispatch`-only** workflow (never runs on push/PR):
- **Input:** `stack` choice, default `prod` (also `dev`).
- **Guards:** runs in the **`production`** GitHub Actions environment (its secrets + any required-reviewer/protection rules apply) and a step **fails the run if not launched from `main`**.
- **Steps:** checkout → Go 1.26 + TinyGo 0.41.1 + pnpm/Node → `pnpm install` → **apply the `.ci/tinygo-net-roundtrip.patch`** (same as CI) → `pnpm run build` (Wasm Worker) → `pulumi up --stack <stack>` via `pulumi/actions`, which installs the Pulumi CLI and injects the freshly built `../build/worker.mjs` through `config-map` as `workerScriptPath`.
- **Concurrency:** one deploy per stack, never cancels an in-flight `pulumi up`.
- Every action is **pinned by commit SHA** (`pulumi/actions@8e5e406…` # v7.0.0, plus the same SHAs CI uses). **actionlint clean.**
## Deliverable 2 — infra binding-wiring fix (#9)
`infra/deploy.go` previously registered a bindingless `WorkersScript`, so the deployed Worker couldn't reach its bucket/secrets/vars. Now the `WorkersScript` carries the full runtime contract (matches `wrangler.jsonc`):
| Binding (JS var) | Type | Target |
| --- | --- | --- |
| `REPORTS_BUCKET` | `r2_bucket` | `libremail-bug-reports` |
| `BUGREPORT_ENC_KEYRING` | `secrets_store_secret` | `secretsStoreId` / `bugreport-enc-keyring` |
| `ADMIN_TOKEN` | `secrets_store_secret` | `secretsStoreId` / `bugreport-admin-token` |
| `GITHUB_TOKEN` | `secrets_store_secret` | `secretsStoreId` / `github-token` |
| `OTEL_EXPORTER_OTLP_HEADERS` | `secrets_store_secret` | `secretsStoreId` / `otel-exporter-otlp-headers` |
| `GITHUB_REPO` | `plain_text` | `JMR-dev/LibreMail` |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `plain_text` | `""` (disables telemetry) |
| `OTEL_SERVICE_NAME` | `plain_text` | `libremail-bug-report-ingest` |
Also:
- **New `cloudflare.WorkersCronTrigger`** registering the two Friday UTC crons `0 22 * * 5` and `0 23 * * 5` (#13), bound to the Worker.
- **Real artifact as content:** when `workerScriptPath` is set the Worker uploads it via `ContentFile` + a program-computed `ContentSha256`; otherwise the documented placeholder module body is used (keeps the program testable without a build, as #2 did). The CD workflow sets the path.
- Kept account-specific values as documented config/placeholders; **`secretsStoreId` is a new required config**. The #6/#7 rate-limit ruleset insertion point remains reserved.
## Secrets & config a maintainer MUST set before the first deploy
**1. `production` environment secrets** (Settings → Environments → production):
| Secret | Purpose |
| --- | --- |
| `PULUMI_ACCESS_TOKEN` | Pulumi Cloud token (state backend). Self-managed backend → use the action's `cloud-url` input + `PULUMI_CONFIG_PASSPHRASE` instead. |
| `CLOUDFLARE_API_TOKEN` | Cloudflare token scoped to Workers Scripts + R2 (+ Cron Triggers). |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account id. |
| `GOOGLE_CREDENTIALS` | GCP service-account JSON with Cloud DNS admin on the zone. |
**2. Stack config** in `infra/Pulumi.<stack>.yaml` — replace every `REPLACE_ME_*`: `cloudflareAccountId`, **`secretsStoreId`** (new), `dnsManagedZone`, `dnsRecordName`, `dnsRecordTarget`, `gcp:project`. (Optional overrides: `githubRepo`, `otel*`, `r2*`, `dns*`.)
**3. Cloudflare Secrets Store** — under `secretsStoreId`, store the four secret *values*: `bugreport-enc-keyring`, `bugreport-admin-token`, `github-token`, `otel-exporter-otlp-headers`. (This stack binds them by name; it does not create the values.)
**4. Pulumi stack** — `pulumi stack init prod` once (workflow runs `upsert: false`).
## How to trigger
Actions → **CD** → *Run workflow* → branch **`main`**, stack **`prod`**.
## Mock-test evidence (runs via `go test` in `infra/`, no CLI)
Extended the `WithMocks` suite; `cd infra && go build ./... && go vet ./... && go test ./...` is green:
```
--- PASS: TestWorkerScriptRegistered
--- PASS: TestWorkerScriptBindings (R2 + 4 Secrets Store + 3 plain vars, exact store/secret_name)
--- PASS: TestWorkerContentFromArtifact (ContentFile + computed ContentSha256; content unset)
--- PASS: TestR2BucketRegistered
--- PASS: TestDNSRecordRegistered
--- PASS: TestWorkerCronTriggers (0 22 * * 5, 0 23 * * 5, bound to the Worker)
--- PASS: TestManagedResourceCounts (1 each: WorkersScript, R2Bucket, WorkersCronTrigger, RecordSet)
--- PASS: TestMissingRequiredConfigIsAnError
ok github.com/JMR-dev/LibreMail-Bug-Report-Ingest/infra
```
`actionlint .github/workflows/deploy.yml` → clean.
## Not done (by design — this is the mechanism only)
No `pulumi up`/`preview`, no `wrangler deploy`, no `gh workflow run`, no cloud resources/secrets/environments created. The maintainer configures the above and triggers the deploy.
Closes #4
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Builds the deploy mechanism only for #4. No deploy/preview/provision was run; a maintainer supplies credentials and pulls the trigger later.
Deliverable 1 —
.github/workflows/deploy.yml(CD)Manual
workflow_dispatch-only workflow (never runs on push/PR):stackchoice, defaultprod(alsodev).productionGitHub Actions environment (its secrets + any required-reviewer/protection rules apply) and a step fails the run if not launched frommain.pnpm install→ apply the.ci/tinygo-net-roundtrip.patch(same as CI) →pnpm run build(Wasm Worker) →pulumi up --stack <stack>viapulumi/actions, which installs the Pulumi CLI and injects the freshly built../build/worker.mjsthroughconfig-mapasworkerScriptPath.pulumi up.pulumi/actions@8e5e406…# v7.0.0, plus the same SHAs CI uses). actionlint clean.Deliverable 2 — infra binding-wiring fix (#9)
infra/deploy.gopreviously registered a bindinglessWorkersScript, so the deployed Worker couldn't reach its bucket/secrets/vars. Now theWorkersScriptcarries the full runtime contract (matcheswrangler.jsonc):REPORTS_BUCKETr2_bucketlibremail-bug-reportsBUGREPORT_ENC_KEYRINGsecrets_store_secretsecretsStoreId/bugreport-enc-keyringADMIN_TOKENsecrets_store_secretsecretsStoreId/bugreport-admin-tokenGITHUB_TOKENsecrets_store_secretsecretsStoreId/github-tokenOTEL_EXPORTER_OTLP_HEADERSsecrets_store_secretsecretsStoreId/otel-exporter-otlp-headersGITHUB_REPOplain_textJMR-dev/LibreMailOTEL_EXPORTER_OTLP_ENDPOINTplain_text""(disables telemetry)OTEL_SERVICE_NAMEplain_textlibremail-bug-report-ingestAlso:
cloudflare.WorkersCronTriggerregistering the two Friday UTC crons0 22 * * 5and0 23 * * 5(#13), bound to the Worker.workerScriptPathis set the Worker uploads it viaContentFile+ a program-computedContentSha256; otherwise the documented placeholder module body is used (keeps the program testable without a build, as #2 did). The CD workflow sets the path.secretsStoreIdis a new required config. The #6/#7 rate-limit ruleset insertion point remains reserved.Secrets & config a maintainer MUST set before the first deploy
1.
productionenvironment secrets (Settings → Environments → production):PULUMI_ACCESS_TOKENcloud-urlinput +PULUMI_CONFIG_PASSPHRASEinstead.CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDGOOGLE_CREDENTIALS2. Stack config in
infra/Pulumi.<stack>.yaml— replace everyREPLACE_ME_*:cloudflareAccountId,secretsStoreId(new),dnsManagedZone,dnsRecordName,dnsRecordTarget,gcp:project. (Optional overrides:githubRepo,otel*,r2*,dns*.)3. Cloudflare Secrets Store — under
secretsStoreId, store the four secret values:bugreport-enc-keyring,bugreport-admin-token,github-token,otel-exporter-otlp-headers. (This stack binds them by name; it does not create the values.)4. Pulumi stack —
pulumi stack init prodonce (workflow runsupsert: false).How to trigger
Actions → CD → Run workflow → branch
main, stackprod.Mock-test evidence (runs via
go testininfra/, no CLI)Extended the
WithMockssuite;cd infra && go build ./... && go vet ./... && go test ./...is green:actionlint .github/workflows/deploy.yml→ clean.Not done (by design — this is the mechanism only)
No
pulumi up/preview, nowrangler deploy, nogh workflow run, no cloud resources/secrets/environments created. The maintainer configures the above and triggers the deploy.Closes #4