Skill v1.0.1
Automated scan91/100+6 new
version: "1.0.1" name: actions-state-and-secrets description: Use when persisting values across a GitHub Action's pre/main/post phase boundary, handling a Redacted secret anywhere in @effected/github-actions, deciding whether a value belongs in ActionState or ActionOutputs, framing a blob with metadata for a cache or object store, reaping a detached child process, or rendering an Action's top-level failure. when_to_use: ActionState, Redacted secret, BlobEnvelope, Secret.forChildEnv, Secret.forRunnerFile, Secret.forSigning, Secret.adopt, DryRun, DetachedProcess, GITHUB_STATE, ChildEnv PATH prepend
Actions state and secrets
Everything in @effected/github-actions that crosses a phase boundary, or that must not leak. Each phase (pre/main/post) is a separate process; GitHub's own mechanism for carrying a value between them is a write-only file (GITHUB_STATE) whose entries the runner republishes to the next phase as STATE_<key> environment variables. That asymmetry — write through a file, read through the environment — is why ActionState is a service rather than a pair of helper functions.
For general Effect v4 service/layer shape, typed errors, Cause, and Scope, see effect-v4-services-layers, effect-v4-idioms, effect-v4-schema. This skill carries only the Actions-specific instance of those rules.
What you have
| Construct | Import | Reach for it when | |
|---|---|---|---|
ActionState.save / .get / .getOptional | import { ActionState } from "@effected/github-actions" | round-tripping an ordinary, non-secret value across pre/main/post | |
ActionState.saveSecret | same | persisting a value that came from a Redacted across the phase boundary | |
Secret.forChildEnv / .forRunnerFile / .forSigning / .adopt | import { Secret } from "@effected/github-actions" | the one module that turns a Redacted into a plaintext string, masking first | |
DryRun.guard | import { DryRun } from "@effected/github-actions" | running a mutation for real, or logging and returning a fallback in a rehearsal | |
DetachedProcess.reap | import { DetachedProcess } from "@effected/github-actions" | signalling a child process by a pid read back out of GITHUB_STATE | |
ChildEnv.prependPath | import { ChildEnv } from "@effected/github-actions" | safely extending a spawned child's PATH across platforms | |
BlobEnvelope | import { BlobEnvelope } from "@effected/github-actions" | framing bytes with caller-owned metadata before they reach a cache or blob store — full treatment in actions-cache-and-artifacts' references/blob-stores.md |
Standards
- Choose `ActionState` vs `ActionOutputs` mechanically, not stylistically.
ActionStatecrossespre → main → postof the same action and never persists past the run;ActionOutputscrosses one step to a later step (possibly a different action) viawith:outputs. A provisioned token or a detached child's pid is state; a value the workflow author wired intosteps.<id>.outputs.<name>is an output. - Design every state field's encoded form as plain JSON. Use
Schema.OptionFromNullOr, neverSchema.Option, for an optional field — the latter's encoded form is anOptioninstance, not a JSON primitive, and the failure lands one phase later than the mistake. - Call `saveSecret` for anything that came from a `Redacted`, ever; call plain `save` for everything else. Only
saveSecretmasks before persisting.GITHUB_STATEis plaintext by GitHub's protocol, so the mask coupled to the write is the only defense a persisted secret gets. - Let `Secret.ts` be the only place a `Redacted` becomes a string. When a genuine third need for a raw secret shows up, add a member to
Secretrather than granting an exception elsewhere — masking and declassifying are the same call by design. - Give `DryRun.guard` a real fallback, always. The fallback is required, not optional, so a rehearsal's return value is a design decision, not an afterthought.
- Validate a pid on the way *out* of `ActionState`, and again in `DetachedProcess.reap`. A pid that crosses a text-file boundary has already lost whatever type safety it had; guard it at both ends rather than trusting the second guard alone.
- Audit every ported error channel for whether it can actually fire. A channel wrapped around a body that can never throw is worse than no channel — delete the reason from the signature, or write a test that fires it.
Footguns
- An
Optionfield encoded withSchema.Optioninstead ofSchema.OptionFromNullOrreports success inmainand fails to decode inpost— the mistake and its failure land in different phases. Seereferences/cross-phase-state.md. DetachedProcess.reaptakes a plainnumber: an absent state key, a truncated file, or a bad parse all decode to0, andprocess.kill(0, …)signals the caller's entire process group. Seereferences/detached-processes.md.envpassed to a spawn call withoutextendEnv: truereplaces the child's whole environment, including thePATHa caller meant to extend. Seereferences/detached-processes.md.BlobEnvelope's wire format, five-reason error union, and why a legacy raw blob decodes as a clean miss rather than garbage live inactions-cache-and-artifacts, not here — don't re-derive the frame shape from this skill's description alone.- A `Schema.Redacted` field persisted as JSON round-trips to the literal string `<redacted>`. This is core Effect behavior, not a kit choice, and it is silent — the write succeeds, the read succeeds, and the value is garbage. Re-probed against beta.107 (
Schema.Redactedencode → an object whoseJSON.stringifyis"<redacted>";Schema.RedactedFromValueencode →"s3cret"):
``text Schema.Redacted(Schema.String) encode -> a Redacted object JSON.stringify of that -> "<redacted>" Schema.RedactedFromValue(Schema.String) encode -> "s3cret" (the real value) ``
Redacted's own toString/toJSON are what emit the sentinel, so anything that serializes a Redacted — JSON.stringify, a log line, a state field — gets <redacted> rather than the secret. That is the right default and the reason it exists. But it means Schema.Redacted is the wrong schema for a value you intend to read back: use `Schema.RedactedFromValue`, whose encoded form is the underlying value (and which takes disallowEncode when you want the write to fail loudly instead). In an Actions context that round trip is ActionState.saveSecret on the way in — it masks before persisting — and ActionState.get / getOptional on the way back out. One consumer lost real time to a token-theft theory before finding the sentinel was simply the encoder's output.
Additional resources
- references/cross-phase-state.md —
ActionState's full shape and round-trip mechanics, theActionState-vs-ActionOutputsdecision table in full,DryRun's safe-default contract, and the failure-channel discipline that decides demote-vs-die before aCauseever reachesAction.run. Load when: designing a state bundle, choosing between state and outputs, or auditing an error channel. - references/secrets.md —
Secret's four members in full, the v4 fact that keeps the seam small (HttpClientRequest.bearerTokenaccepting aRedacteddirectly), the structural scan that proves onlySecret.tsunwraps one, and@effected/commands'Redactionfor value-based scrubbing. Load when: adding a new place a secret needs to leaveRedacted, or reviewing a suspected leak. - references/detached-processes.md —
DetachedProcess.reap's bare-pid guard and its test discipline,ProcessId's validating constructor, andChildEnv's three PATH-prepending traps (extendEnv, Windows casing,.cmdshim shells). Load when: spawning or reaping a detached child process, or prepending to a child'sPATH.