Skip to content

refactor: return neverthrow results instead of hand-rolled ok/err unions - #416

Merged
Vivswan merged 2 commits into
mainfrom
wt/lib-swaps-results
Sep 23, 2026
Merged

Vivswan merged 2 commits into
mainfrom
wt/lib-swaps-results

Conversation

@Vivswan

@Vivswan Vivswan commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #414 (base branch wt/lib-swaps); merge that one first.

What this changes

Three hand-rolled { ok: true, ... } | { ok: false, ... } unions become the neverthrow Result the rest of src/ already returns. No message changes.

site                                  before (hand-rolled)                                                     after (neverthrow)
engine/secret-refs.ts                 SecretRefCheck = { ok: true; ref } | { ok: false; error }               Result<SecretRef, string>
engine/secret-refs.ts                 SecretRefsResolution = { ok: true; values; mask } | { ok: false; errors } Result<ResolvedSecretRefs, string[]>
github/secret-scan.ts                 { ok: true; payload; traced; carriesSecret } | { ok: false; reason? }     Result<SafePayload, string | undefined>
sections/deploy_keys/schema.ts        PublicKeyParse = { ok: true; algorithm; material } | { ok: false; reason } Result<PublicKeyMaterial, string>

consumer line                         before                                                                   after
engine/validate.ts                    if (!checked.ok) problems.push(`${key}: ${checked.error}`)               if (checked.isErr()) problems.push(`${key}: ${checked.error}`)
engine/orchestrate.ts                 resolutionErrors.set(key, resolution.errors)                             resolutionErrors.set(key, resolution.error)
github/api.ts                         secretScan.reason ?? "its payload is not plain JSON data ..."             secretScan.error ?? "its payload is not plain JSON data ..."
sections/deploy_keys/index.ts         return parsed.material                                                   return parsed.value.material

How

  • Every consumer reads isErr(), value, and error; the two BUG: throws (a minted snapshot reference the file would refuse, deploy key material that passed the shape but not the planner) keep their text.
  • Result<SafePayload, string | undefined>, not a dedicated error type: undefined is exactly the branch that had no reason (a non-JSON primitive, or a hostile object throwing during the reflective read), and the two callers in api.ts word its fallback differently ("its payload" vs "its variables"), so the prose stays theirs.
  • Types in exported signatures are exported (SecretRef, ResolvedSecretRefs, SafePayload, PublicKeyMaterial); bun run build:lib bundles the declarations clean.
  • Tests assert the whole outcome (toEqual(ok({ values, mask }))) where they used to read one field after an ok check.

Proof

  • Gates: typecheck, knip, lint, lint:arch green.
  • Unit tests: bun test test/engine test/github test/sections -> 1911 pass, 0 fail (the message pins in secret-refs.test.ts, secrets.test.ts, deploy_keys.test.ts, and api.test.ts are unchanged in text).
  • e2e: bun run test:e2e --sections deploy_keys,actions_secrets -> 18/18.
  • Declarations: bun run build:lib completes; no exported signature uses a private name.
Technical details

Line accounting (git diff --numstat wt/lib-swaps...HEAD, 11 files, +117/-132):

 4   4  src/engine/orchestrate.ts
26  33  src/engine/secret-refs.ts
 1   1  src/engine/secrets.ts
 1   1  src/engine/validate.ts
10  10  src/github/api.ts
20  14  src/github/secret-scan.ts
 6   6  src/sections/deploy_keys/index.ts
13  11  src/sections/deploy_keys/schema.ts
13  21  test/engine/secret-refs.test.ts
 4   4  test/engine/secrets.test.ts
19  27  test/sections/deploy_keys/deploy_keys.test.ts

Reviewer note, deliberately left as is:

  • src/github/api.ts ClientAnswer tri-state ({ data } | { failed } | ...) at 55 sites: a design-level swap, the owner's call.
  • The secret-scan.ts header now says "transport-free" where it said "dependency-free": the module imports neverthrow, and the guarantee it states is independence from the transport.
  • Everything the base PR refactor: replace hand-rolled helpers with the library calls that already ship #414 left (@actions/core in the CLI by recorded decision, matchesGlob semantics, the two src/ regex-escape copies, the tsconfig lib bump) is unchanged here.

Codex rubber-duck: one round, no findings.

BEGIN_COMMIT_OVERRIDE
refactor: return neverthrow results instead of hand-rolled ok/err unions

validateSecretRef returns Result<SecretRef, string> and resolveSecretRefs returns Result<ResolvedSecretRefs, string[]>, replacing the SecretRefCheck and SecretRefsResolution unions.
redactSecretPayloadSafe returns Result<SafePayload, string | undefined>, the undefined error being the case that had no reason field: a non-JSON primitive or a hostile throw.
parsePublicKey and parseStoredKey return Result<PublicKeyMaterial, string>, replacing the PublicKeyParse union.
Every consumer reads isErr(), value, and error; every message is unchanged.
END_COMMIT_OVERRIDE

@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

File size check

0 over a hard cap (fails), 30 warning(s).

File Size Tier Cap
.github/scripts/release-pipeline.ts:6 156 chars warn 150
.github/scripts/release-pipeline.ts:453 151 chars warn 150
.github/scripts/release-pipeline.ts:1247 153 chars warn 150
.github/scripts/release-pipeline.ts:1 34 comment lines (header) warn 25
.github/scripts/release-pipeline.ts:449 14 comment lines warn 10
.github/workflows/post-green.yml:29 153 chars warn 150
.github/workflows/update-release-pr.yml:109 14 comment lines warn 10
docs/upgrading/v2-to-v3.md 1276 lines warn 1040
src/engine/layers.ts:217 157 chars warn 150
src/flows/settings-write.ts:142 159 chars warn 150
src/flows/settings-write.ts:30 11 comment lines warn 10
src/flows/snapshot.ts:189 13 comment lines warn 10
src/github/secret-scan.ts:43 11 comment lines warn 10
src/schema.ts:186 153 chars warn 150
src/schema.ts:197 176 chars warn 150
src/sections/contract/errors.ts:14 14 comment lines warn 10
src/sections/contract/module.ts:963 185 chars warn 150
src/sections/contract/module.ts:627 12 comment lines warn 10
src/sections/contract/module.ts:897 12 comment lines warn 10
src/sections/secret_scanning_custom_patterns/compilable-form.ts:382 161 chars warn 150
src/sections/shared/roles.ts:43 13 comment lines warn 10
src/types.ts:16 156 chars warn 150
test/docs/guides.test.ts:451 155 chars warn 150
test/e2e/generators.ts 2633 lines warn 2560
test/e2e/generators.ts:1703 166 chars warn 150
test/e2e/generators.ts:1857 161 chars warn 150
test/e2e/generators.ts:1666 12 comment lines warn 10
test/engine/execute.test.ts:617 152 chars warn 150
test/flows/merge-parity.test.ts:63 164 chars warn 150
test/scripts/auto-fix-allowlist.test.ts:8 12 comment lines warn 10

Split the file, wrap the line, shorten or exempt the comment, or list the path in .file-size-allow.local with a # reason.

5 managed file(s) skipped; repo-platform owns them.

…eady ship

zod issue paths render through z.core.toDotPath at the four sites that each spelled their own join, so a key with a non-word character reads as ["my-key"] instead of .my-key.
The secret scanning pattern comparison uses isDeepStrictEqual from node:util instead of JSON.stringify equality.
The two set-equality helpers use Set.prototype.isSubsetOf, and the tsconfig lib moves from ES2022 to ES2025 for its types while the target stays ES2022.
The REST query string is built by URLSearchParams, which writes a space as + where encodeURIComponent wrote %20; GitHub decodes both.
Central-mode file names go through extname and parse from node:path instead of a YAML extension regex.
valueAt has one home in the shared list-section module, the two environments modules share one unreconcilable message builder, and the generator scripts call RegExp.escape instead of a local escapeRe.
validateSecretRef returns Result<SecretRef, string> and resolveSecretRefs returns Result<ResolvedSecretRefs, string[]>, replacing the SecretRefCheck and SecretRefsResolution unions.
redactSecretPayloadSafe returns Result<SafePayload, string | undefined>, the undefined error being the case that had no reason field: a non-JSON primitive or a hostile throw.
parsePublicKey and parseStoredKey return Result<PublicKeyMaterial, string>, replacing the PublicKeyParse union.
Every consumer reads isErr(), value, and error; every message is unchanged.
@Vivswan
Vivswan force-pushed the wt/lib-swaps-results branch from 239231c to b8020b6 Compare September 22, 2026 09:53
@Vivswan Vivswan added the merge-when-green Owner approved: merge once every gate is green label Sep 22, 2026
Base automatically changed from wt/lib-swaps to main September 23, 2026 00:48
@Vivswan
Vivswan merged commit 2493e7d into main Sep 23, 2026
34 checks passed
@Vivswan
Vivswan deleted the wt/lib-swaps-results branch September 23, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-when-green Owner approved: merge once every gate is green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant