Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions alchemy.run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
resolveMapleDomains,
} from "@maple/infra/cloudflare"
import * as Acm from "@maple/infra/acm"
import { optionalPlain, plainWithDefault } from "@maple/infra/env"
import { optionalPlain, plainWithDefault, secretIsSet } from "@maple/infra/env"
import * as Portless from "@maple/alchemy-portless"
import { DEV_PROCESS_APPS, selectedDevApps, type DevApp } from "@maple/infra/dev-urls"
import Alerting from "./apps/alerting/src/worker.ts"
Expand Down Expand Up @@ -217,7 +217,20 @@ export default Alchemy.Stack(
// Object, and the api binds it as `SANDBOX`. Yielded first so the binding
// sees a Worker this deploy created rather than stored state, and only on
// the stages that run it — see `stageDeploysSandbox`.
const sandbox = stageDeploysSandbox(stage) ? yield* MapleSandbox : undefined
//
// The token is checked here rather than required inside the Worker's own
// props. Both refuse to deploy a sandbox that would answer 401 to every
// call, but a `requiredSecret` in the props fails the whole `alchemy deploy`
// at config load, before any resource is touched — so a token nobody had
// provisioned yet stopped every Worker in the stage from updating. Deciding
// it here keeps the blast radius at the feature: no token, no sandbox, and
// the api logs that it is unavailable.
const sandboxTokenSet = yield* secretIsSet("SANDBOX_INTERNAL_SERVICE_TOKEN")
if (stageDeploysSandbox(stage) && !sandboxTokenSet)
yield* Effect.logWarning(
"skipping the repository sandbox: SANDBOX_INTERNAL_SERVICE_TOKEN is not set for this stage",
)
const sandbox = stageDeploysSandbox(stage) && sandboxTokenSet ? yield* MapleSandbox : undefined
const api = yield* sandbox === undefined
? MapleApi
: Effect.provideService(MapleApi, SandboxWorker, sandbox)
Expand Down
24 changes: 24 additions & 0 deletions packages/infra/src/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
ingestKeyCryptoEnv,
optionalPlain,
optionalSecret,
requireSecretEntry,
secretIsSet,
planetScaleOAuthEnv,
plainWithDefault,
PRD_LOCKSTEP_REVISION_SERVICES,
Expand Down Expand Up @@ -124,6 +126,28 @@ describe("primitives", () => {
})
})

describe("secretIsSet", () => {
// Gates whether a resource is declared at all, so it must agree with
// `requiredSecret` on what counts as present: a blank value is not.
it("is false when the key is missing or blank, true only for a real value", () => {
expect(run(secretIsSet("SANDBOX_INTERNAL_SERVICE_TOKEN"), {})).toBe(false)
expect(
run(secretIsSet("SANDBOX_INTERNAL_SERVICE_TOKEN"), { SANDBOX_INTERNAL_SERVICE_TOKEN: "" }),
).toBe(false)
expect(
run(secretIsSet("SANDBOX_INTERNAL_SERVICE_TOKEN"), { SANDBOX_INTERNAL_SERVICE_TOKEN: " " }),
).toBe(false)
expect(
run(secretIsSet("SANDBOX_INTERNAL_SERVICE_TOKEN"), { SANDBOX_INTERNAL_SERVICE_TOKEN: "tok" }),
).toBe(true)
})

it("never fails, so a missing secret cannot abort the deploy before any resource is touched", () => {
expect(runExit(secretIsSet("SANDBOX_INTERNAL_SERVICE_TOKEN"), {})._tag).toBe("Success")
expect(runExit(requireSecretEntry("SANDBOX_INTERNAL_SERVICE_TOKEN"), {})._tag).toBe("Failure")
})
})

describe("selfObservabilityEnv", () => {
const base = { MAPLE_OTEL_INGEST_KEY: "maple_ak_test" }

Expand Down
12 changes: 12 additions & 0 deletions packages/infra/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ export const optionalPlain = (key: string, fallback?: string): Config.Config<Pla
export const optionalSecret = (key: string): Config.Config<SecretEnv> =>
trimmedOption(key).pipe(Config.map((value) => entry(key, Option.map(value, Redacted.make))))

/**
* Whether a secret is set, without reading it.
*
* For deciding at the stack level whether a resource that *requires* a secret
* should be declared at all. `requiredSecret` inside a resource's props is the
* right shape once the resource exists, but it fails the whole `alchemy deploy`
* before any resource is touched — so a secret nobody provisioned takes down
* every Worker in the stage rather than the one feature that needed it.
*/
export const secretIsSet = (key: string): Config.Config<boolean> =>
trimmedOption(key).pipe(Config.map(Option.isSome))

/** The first present-and-non-blank of `keys`, else `fallback`. For build vars with a `VITE_` twin. */
export const plainFrom = (keys: ReadonlyArray<string>, fallback: string): Config.Config<string> =>
Config.all(keys.map(trimmedOption)).pipe(
Expand Down