Skip to content

Restore Sparkle notarization credentials from Fleet-OPS Infisical - #257

Merged
jaywedgeworth22 merged 47 commits into
mainfrom
jet/fix-sparkle-infisical
Oct 21, 2026
Merged

jaywedgeworth22 merged 47 commits into
mainfrom
jet/fix-sparkle-infisical

Conversation

@jaywedgeworth22

@jaywedgeworth22 jaywedgeworth22 commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Summary

Restores Sparkle release notarization by sourcing the App Store Connect credentials from Fleet-OPS Infisical (prod /asc) instead of the stale repository ASC_* secrets, and adds a read-only notarization preflight plus credential-free contract tests.

What changed

.github/workflows/mac-release.yml

  • Removed ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8 from the job environment, so the workflow no longer reads secrets.ASC_*.  SPARKLE_ED_PRIVATE_KEY is now only supplied outside pull-request runs.
  • Added a Load ASC credentials (Fleet-OPS prod /asc) step that:
    • Installs the Infisical CLI via Homebrew if it is not already present.
    • Authenticates with INFISICAL_UNIVERSAL_AUTH_CLIENT_ID/_SECRET, falling back to INFISICAL_CLIENT_ID/_SECRET, and fails if neither is configured.
    • Fetches ASC_KEY_ID and ASC_ISSUER_ID, rejects empty or multiline values, masks them, and writes them to GITHUB_ENV.
    • Fetches ASC_KEY_P8 and pipes it to scripts/ios-stage-asc-key.sh, so only a mode-600 file path (ASC_KEY_PATH) crosses the step boundary.
  • Updated the publish gate to require ASC_KEY_PATH and to reject a missing or empty staged key file.
  • Added Verify notarization access before building, which runs only when publishing is possible.
  • Removed the previous Write the notarization key step.
  • The build/notarize step now reads NOTARY_KEY_PATH, NOTARY_KEY_ID, and NOTARY_ISSUER_ID from the loaded env.ASC_* values.
  • Cleanup now removes $ASC_KEY_PATH instead of a hardcoded temporary key.
  • Expanded path triggers to include assets/AppIcon.icns, scripts/check-notary-access.sh, scripts/mac-release-workflow.test.mjs, and scripts/ios-stage-asc-key.sh.
  • Added a credential-free contract test step (node --test scripts/mac-release-workflow.test.mjs) before the Swift CI wait.

scripts/check-notary-access.sh (new)

  • Read-only preflight that requires ASC_KEY_ID, ASC_ISSUER_ID, and a non-empty ASC_KEY_PATH.
  • Runs xcrun notarytool history and reports only a fixed failure category (authentication rejected, service access rejected, connectivity failure, key format/issuer failure, or unclassified) without printing Apple's raw response or credential values.
  • Cleans up its temporary diagnostic file and makes clear that successful authentication does not imply artifact acceptance.

scripts/mac-release-workflow.test.mjs (new)

  • Contract tests asserting the workflow never references secrets.ASC_*, uses the fleet loader, passes the staged key path through, cleans up the key, blocks PR notarization/publication, and orders the preflight before the release build.
  • Mocked tests for check-notary-access.sh covering success, sanitized failure categories, raw-error withholding, temporary-file cleanup, and failure when credentials are missing before Apple is invoked.

docs/AUTO-UPDATE.md

  • Updated the credential table to document Fleet-OPS Infisical prod /asc, the staging script, and the read-only notarization preflight.
  • Added a Diagnosing a stale Sparkle feed section explaining the release pipeline's required stages, the separation from TestFlight, the read-only preflight, and the monotonic build-number behavior.

Open review findings

  • Critical: A bare infisical secrets get call was added to a public workflow; it should be routed through scripts/infisical-secrets-safe.sh.
  • Critical: An Infisical project UUID is committed in the public workflow; it should be moved to a secret registered in the private ops inventory.
  • Medium: The task description was insufficient for business-logic validation.

Summary

Updates the failure diagnostics in the macOS release workflow so that missing Sparkle/notarization credential errors point to the Fleet-OPS prod /asc path rather than referencing the internal Infisical project ID variable.

Changes

  • Revised the error message emitted when a required secret is missing from Fleet-OPS prod /asc: it now instructs that the CI identity must have read access to Fleet-OPS prod /asc, dropping the reference to ${ASC_INFISICAL_PROJECT_ID}.
  • Applied the same wording change to the error raised when ASC key material is missing.

Impact

  • Error output for failed credential retrieval is now consistent and no longer exposes an internal Infisical project identifier.
  • Guidance to operators is simplified to the path-level access requirement, while the workflow's failure behavior (exiting with an error) is unchanged.

Summary

Routes the App Store Connect credentials used by the Sparkle macOS notarization release back to Fleet-OPS Infisical prod /asc.  The workflows now use a secret-backed Fleet-OPS project ID, the existing infisical-secrets composite action for the key ID and issuer ID, and a protected handoff for the multiline private key.

The same credential-loading flow is applied to both mac-release.yml and ios-ship.yml, with offline contract coverage and a manual release-credential verification path.

Functional changes

  • Replaced stale repository ASC credentials and the hard-coded Fleet-OPS project ID with INFISICAL_FLEET_OPS_PROJECT_ID.
  • Added scripts/infisical-fetch-asc-key.mjs to authenticate to Infisical, retrieve ASC_KEY_P8 from Fleet-OPS prod /asc, mask the token and key material, and stream the key into scripts/ios-stage-asc-key.sh.
  • Keeps the private key out of the environment: only the generated ASC_KEY_PATH is published, with the key stored in a protected directory and file.
  • Updated both the macOS release and iOS TestFlight workflows to validate that the key ID, issuer ID, and staged key file arrived before continuing.
  • Added a manual verify-release-credentials job that loads the release credentials, checks Apple notarization access with scripts/check-notary-access.sh, and removes the staged key without publishing.
  • Added a verify-only workflow-dispatch option that marks the release as non-publishing.
  • Kept pull-request runs credential-free while retaining contract tests for the release workflow.
  • Updated the iOS ship contract to remove the retired macOS TestFlight ship checks and include the new credential-handoff tests.
  • Updated Sparkle auto-update documentation so the EdDSA private key’s primary backup is stored in Fleet-OPS under /codecaps-sparkle-ed25519-key as CODECAPS_SPARKLE_ED25519, with temporary export files deleted afterward.

Validation

  • Added offline Node tests covering:
    • Correct Fleet-OPS project, prod environment, and /asc path.
    • Single-path secret retrieval.
    • Missing project ID and missing private-key failures.
    • Mode-0600 key files, mode-0700 staging directories, and absence of key material from the environment or logs.
  • Updated the shell handoff test to ensure workflows do not use the bare Infisical CLI, hard-code the Fleet-OPS project ID, or export the private key.
  • Updated macOS and iOS workflow contract tests to require consistent credential loaders and prohibit stale repository ASC secrets.
  • The iOS ship contract now runs:
    • node --test scripts/ios-ship-workflow.test.mjs scripts/infisical-fetch-asc-key.node-test.mjs
    • bash scripts/ios-fleet/test-asc-credentials.sh
    • node --test scripts/ios-fleet/test-asc-credentials.mjs

Outstanding automated-review findings

The listed review findings remain unresolved:

  • Critical: The new fetch script accesses Infisical directly rather than through the approved secret-retrieval path.
  • High: The login-token handling appears to send Bearer undefined during the vault request.
  • High: The verify-only release path can still perform a full build without running the intended notarization preflight.
  • High: The credential-verification job inherits contents: write rather than read-only repository permissions.
  • Medium: The test stub does not validate the Authorization header, so authenticated vault access is not exercised.
  • Low: The new contract test file is missing from a workflow path filter.

The review summary also reports two additional critical findings that could not be posted and are not included in the available list.


Summary

Restores Sparkle release notarization credentials by loading ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8 from Fleet-OPS Infisical (prod /asc) in the Mac Release and iOS Ship workflows.  This replaces direct vault access with the approved infisical-secrets action and addresses the revoked repository key that caused Apple notarization to return HTTP 401 and blocked Mac publishing.

Key changes

  • Both release workflows now retrieve all App Store Connect credentials through the Fleet-OPS Infisical composite action.
  • Added scripts/stage-asc-key.mjs to securely stage the multiline private key:
    • Passes the key through standard input rather than command-line arguments.
    • Writes it to a mode-600 file inside a mode-700 directory.
    • Publishes only ASC_KEY_PATH for downstream steps.
    • Clears ASC_KEY_P8 from the job environment after staging.
  • Removed workflow reliance on the direct Infisical fetch helper.
  • Added a verify-only dispatch option to the Mac Release workflow.  It fetches the credentials and asks Apple’s notarization service whether they remain valid, but does not build or publish a release.
  • Restricted the credential-verification job to contents: read so it cannot push commits, tags, or releases while holding the signing key.
  • Updated workflow path filters and contract tests so changes to the staging script and its tests trigger the relevant CI checks.
  • Added tests covering protected file permissions, environment cleanup, missing credentials, and prevention of key exposure through logs or command-line arguments.
  • Documented the Fleet-OPS credential source, the revoked-key incident, and the manual credential-verification workflow.

Validation

CI runs the Mac and iOS workflow contract tests, the new ASC key staging tests, and the existing App Store Connect credential checks.

Automated review follow-ups

  • Critical: The missing-secret error should explicitly identify ASC_KEY_P8.
  • Medium: A verify-only run still installs XcodeGen after completing the notarization preflight.
  • Medium: Documentation should clarify that the key exists in the action-provided environment until staging clears it; only the file path is passed to later steps.
  • Medium: Add coverage that invokes node scripts/stage-asc-key.mjs through its CLI entry point, matching how CI executes it.

Summary

Restores and verifies the Fleet-OPS/Infisical credential flow used by the Sparkle/macOS notarization release.  The App Store Connect signing key is retrieved from Fleet-OPS prod /asc, staged in a protected file, and exposed to later CI steps only through its file path.

Changes

  • Added regression coverage confirming that Infisical secret requests include the bearer token returned by the universal authentication login.
  • Improved missing-key diagnostics to identify the exact ASC_KEY_P8 secret and its Fleet-OPS prod /asc location.
  • Added CLI-level testing for the ASC key staging step to verify that:
    • The private key is written with mode 0600 permissions.
    • ASC_KEY_PATH is published to subsequent steps.
    • ASC_KEY_P8 is cleared from the environment after the path is published.
    • Key material is not exposed through stdout or stderr.
  • Updated the auto-update documentation to describe the complete secure handoff from the infisical-secrets action to the staging script and onward release steps.

Testing

Targeted Node test suites:

  • scripts/infisical-fetch-expand.node-test.mjs
  • scripts/stage-asc-key.node-test.mjs

The review identified that the new bearer-token test is not currently invoked by any CI workflow; wiring it into CI remains necessary for continuous regression protection.


Summary

  • Restores the Fleet-OPS Infisical-backed notarization credential flow used by the macOS Sparkle release.  The ASC identity is sourced from the production /asc path rather than stale repository copies, and notarization access is checked with read-only notarytool history before a publishing build.
  • Isolates verify-only dispatches from the release job.  Credential verification now runs in a dedicated read-only job without loading Developer ID, Sentry, and Sparkle release material, installing the build toolchain, producing a release bundle, or publishing.
  • Restricts publication to main.  Manual runs from other branches remain build-and-verify only and are stopped by the publication gate before release credentials are processed.
  • Enables CodeCaps APNs and verifies the production push profile during the iOS shipping workflow before App Store profiles are installed.  Changes to UsageAlertPreferences.swift now also trigger the iOS ship workflow.
  • Expands macOS release path filtering to cover all infisical-fetch*.mjs helpers.
  • Corrects the documented ASC private-key handoff: the key arrives in the step environment, is streamed into a mode-600 file by stage-asc-key.mjs, and is cleared from the environment in the same step, leaving only the file path for later steps.

CI Coverage

  • The iOS ship contract now runs scripts/infisical-fetch-expand.node-test.mjs alongside the existing workflow, ASC key-staging, and Fleet-OPS credential tests.
  • The macOS release contract now runs the Infisical expansion test and asserts verify-only isolation, main-only publishing, and build-toolchain gating.

Outstanding Review Notes

  • APNs provisioning unit tests are not currently executed by any CI workflow.
  • The macOS release gate retains an unreachable verify_only branch after the release job was isolated, while the contract test still treats that branch as live.

Summary

Restores the App Store Connect credentials used by Sparkle notarization and iOS TestFlight from Fleet-OPS Infisical (prod, /asc) without using the stale repository ASC_* secrets.

The iOS and macOS release workflows now pass their Infisical connection settings directly to scripts/stage-asc-key.mjs.  The script authenticates with Infisical, retrieves the ASC key, key ID, and issuer ID, and stages the private key in a protected file for subsequent signing and notarization steps.

Changes

  • Removed the infisical-secrets loading steps from ios-ship.yml and both credential-loading paths in mac-release.yml.
  • Updated stage-asc-key.mjs to:
    • Authenticate through Infisical Universal Auth.
    • Read ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8 from the configured secret path.
    • Fail with actionable access guidance when the vault connection settings, key, or identifiers are missing.
    • Stream the private key over stdin into the existing mode-600 staging script rather than placing it in the environment or command-line arguments.
    • Publish only ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_PATH to later workflow steps.
    • Ignore any ASC_KEY_P8 value supplied through the environment.
    • Register the key with the GitHub Actions log masker.
  • Updated workflow and staging-script tests to cover vault-only retrieval, protected file permissions, environment isolation, missing credentials, and the CLI execution path.
  • Updated the iOS TestFlight troubleshooting documentation to identify the staging step as the point where Fleet-OPS /asc access failures are reported.

Review Findings

The automated review identified unresolved concerns:

  • Critical: The macOS release credential path bypasses the required environment-first lookup.
  • Critical: The staging test codifies direct Infisical secret retrieval rather than the approved secret-loading path.
  • Critical: The log-mask registration command contains the raw ASC key, and the test requires that raw value to appear in the log.
  • High: Infisical secret responses are not validated through the expected Zod schema.
  • High: Unexpanded Infisical secret references are not rejected before the credentials are used.
  • Low: ASC_KEY_ID and ASC_ISSUER_ID are written to GITHUB_ENV without being masked.

Restore Sparkle notarization credentials from Fleet-OPS Infisical

Summary

  • Restores App Store Connect credentials for macOS Sparkle release and notarization jobs by loading ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8 from Fleet-OPS Infisical prod /asc.  The workflows continue to avoid stale repository signing secrets.
  • Runs the approved Infisical loader and ASC staging script in the same workflow step.  The private key is written to a mode-600 file, while later steps receive only ASC_KEY_PATH and the masked key and issuer identifiers.
  • Refactors ASC staging to resolve credentials from the process environment first, then the job environment file, and finally through the fleet-secrets get fallback.  Missing, malformed, and unresolved Infisical values are rejected before staging.
  • Removes the private key entry from the job environment file and process environment on staging success, failure, and interruption so key material is not propagated to subsequent steps or written to logs.
  • Applies the same credential-loading flow to the iOS TestFlight workflow.
  • Adds a best-effort standing TestFlight tester synchronization after successful uploads.  Tester addresses are masked, and configuration or synchronization failures produce warnings without failing an otherwise successful upload.
  • Updates workflow contracts, ASC staging tests, documentation, and APNs contract-test coverage.

Test coverage

  • node --test scripts/ios-ship-workflow.test.mjs scripts/stage-asc-key.node-test.mjs scripts/infisical-fetch-expand.node-test.mjs scripts/ios-enable-apns.test.mjs
  • bash scripts/ios-fleet/test-asc-credentials.sh
  • node --test scripts/ios-fleet/test-asc-credentials.mjs
  • node --test scripts/mac-release-workflow.test.mjs

Coverage includes environment-first credential resolution, removal of loader-written key entries, malformed and unresolved-value handling, staging and interruption failures, and verification that key material does not survive into later steps.

Known review follow-ups

  • A failure in infisical-fetch.mjs after it writes ASC_KEY_P8 but before staging begins can still leave the key in $GITHUB_ENV.
  • A malformed environment-file entry currently fails the credential lookup even when a valid process-environment value exists, which does not fully preserve the intended environment-first behavior.
  • Contract coverage still permits direct or composite-action credential retrieval and does not fully exercise the loader-to-staging sequence used in CI.

Summary

This PR restores access to the App Store Connect credentials required for Sparkle notarization from Fleet-OPS Infisical (prod, /asc) and aligns the iOS TestFlight workflow with the same credential flow.  The notarization and publication logic is unchanged; only credential installation, retrieval, and staging are updated.

Changes

  • Added scripts/install-release-secrets-tooling.sh to:
    • Install the Infisical CLI v0.44.4 for macOS with pinned SHA-256 verification.
    • Install the fleet-secrets reader from a pinned AI-Fleet-Coordinator commit.
    • Authenticate to Fleet-OPS with universal auth.
    • Verify that the reader can access the /asc secret folder.
    • Pass the minted session to the credential staging step through a mode-600 temporary shell file, which is removed after use.
  • Updated the Sparkle release, credential-check, and iOS TestFlight workflows to:
    • Use the new tooling and scripts/stage-asc-key.mjs together in one step.
    • Stop invoking the legacy infisical-fetch.mjs loader for ASC credentials.
    • Continue running the staging cleanup even when tooling installation fails, preventing key-bearing environment-file entries from surviving.
    • Return the failure status of the command that actually failed.
    • Continue excluding stale repository ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8 secrets.
  • Updated ASC credential staging to:
    • Resolve credentials from the process environment first, then the job environment file, and finally through fleet-secrets.
    • Associate malformed environment-file verdicts with the source that supplied the selected value.
    • Preserve a truncated multiline entry’s partial body so it can be rejected rather than silently treated as absent.
    • Remove the private key from the job environment file while retaining only its mode-600 file path and the masked key identifiers for later steps.
  • Updated workflow and staging tests to cover the new handoff, credential-source precedence, cleanup after a partial loader failure, and malformed environment entries.

Automated Review Findings

The review reports seven outstanding concerns in the current patch:

  • Critical: The resolved Infisical access token is written to the temporary shell file.
  • Critical: The new cleanup test remains coupled to the legacy raw Infisical loader through infisical-fetch.mjs.
  • Critical: Infisical loading occurs before environment credential resolution in the tested sequence and workflow, despite environment-first lookup inside the staging script.
  • High: scripts/install-release-secrets-tooling.sh is not included in the workflow path filters.
  • High: An empty truncated environment-file entry can fall through to the fleet reader without receiving the malformed fail-closed verdict.
  • Medium: The malformed-entry test verifies file bytes rather than proving which resolved value is passed to a later step.
  • Low: The previous PR description had a one-space sentence gap; this description uses explicit sections and line breaks.

Summary

  • Restores the macOS/Sparkle and iOS release credential flow so Apple signing credentials are retrieved from Fleet-OPS Infisical’s /asc path instead of stale repository signing secrets.
  • Makes scripts/install-release-secrets-tooling.sh own the complete credential lifecycle: it installs the pinned Infisical CLI and Fleet reader, mints a universal-auth session, and runs scripts/stage-asc-key.mjs in the same process tree.
  • Removes the temporary release-secrets-env.sh handoff. The Infisical token and project ID now pass directly through the staged consumer’s environment without being persisted by the workflow.
  • Preserves credential cleanup when setup fails: the staging consumer always runs so it can remove key-bearing entries from GITHUB_ENV; the installer error is returned when setup fails, otherwise the staging error is returned.
  • Continues staging the ASC private key in a mode-600 file, with only its path and the key/issuer identifiers crossing into later workflow steps.
  • Hardens environment-file handling by removing malformed or truncated ASC entries before publishing replacement values, preventing corrupt blocks from shadowing later variables, and failing closed on empty corrupted credential entries.
  • Adds the tooling script to the macOS release and iOS ship path filters and updates contract and unit tests for the consolidated flow.
  • Renames the pinned reader-commit constant and narrowly ignores its historical gitleaks fingerprint, which was incorrectly identified as an API key; other scanner rules remain active.

Test Coverage

Updated workflow tests verify the single-command credential bootstrap, removal of the token file handoff, token/project export to the consumer, cleanup and exit-status behavior, and inclusion of the tooling script in release path filters. Staging tests cover environment-first credential resolution, reader fallback, key removal, malformed-entry cleanup, and fail-closed handling of truncated entries.

Outstanding Review Findings

  • High: The explicit Fleet reader pre-check was removed, so broken reader tooling may now be reported downstream as a missing /asc credential rather than a tooling failure.
  • Medium: The token-disclosure assertion only checks printf; output emitted through say or fail is not covered.
  • Low: docs/AUTO-UPDATE.md still misstates the ASC credential source and key-handling behavior.

Summary

  • Added an integration-style regression test for Sparkle notarization credential staging when the release-secret tooling installer cannot download its pinned dependency.
  • Verifies that an installation failure remains visible as a nonzero exit with a download-specific error.
  • Confirms the credential cleanup consumer still runs, stages the ASC key, forwards its path, removes key-bearing environment entries, and prevents secret bytes from appearing in stdout or stderr.

Test Infrastructure

  • Added a helper to execute the installer shell script with a controlled environment while capturing its exit status, stdout, and stderr.
  • The failure scenario uses temporary environment, home, and staging directories, with a failing curl stub to avoid network access.

Coverage Notes

  • Failure is injected only during the installer’s initial download step; later installer failure points are not exercised.
  • ASC credential values are seeded directly into the environment file, so this test validates cleanup behavior after setup failure rather than the full Fleet-OPS Infisical credential-retrieval path.

Summary

  • Updated the Sparkle App Store Connect key staging tests to mark synthetic private-key fixtures with gitleaks:allow, preventing GitLeaks from misidentifying test data as leaked notarization credentials.
  • Applied the allowance to the primary multiline key fixture and the environment-sourced key fixture.
  • No production credential retrieval or runtime behavior is changed by this patch.

Review Note

  • The inline allowances apply only to the newly annotated lines. Earlier PEM fixtures in the test file may still trigger GitLeaks and require separate suppression or fixture cleanup.

Summary

Restores Sparkle notarization credential retrieval from Fleet-OPS production Infisical (/asc) through the fleet’s fleet-secrets reader rather than stale repository credentials.

Changes

  • Adds a read-only reader preflight that runs fleet-secrets names --path /asc before App Store Connect credentials are staged.
  • Classifies reader failures so operators can distinguish:
    • Reader/configuration errors.
    • Rejected access tokens or unreachable vaults.
    • Unresolved Infisical secret references, which are left for the credential consumer to report.
  • Adds a standalone check-reader mode for testing an existing token and reader without running a release.
  • Preserves token handling: the minted token remains in memory and the consumer environment, and is not written to disk, logs, or $GITHUB_ENV.
  • Updates release documentation to explain the Fleet-OPS credential flow, preflight behavior, and protected staging of the multiline notarization key.
  • Expands workflow tests to guard against token leakage through all output helpers and verify that reader validation occurs before credential staging.
  • Adds installer tests covering successful reads, rejected tokens, refused reader configurations, unresolved references, and a missing reader.

Summary

  • Updated the Sparkle notarization installer-failure regression test to match the release workflow, where ASC_KEY_ID and ASC_ISSUER_ID are not preloaded into the job environment and no Infisical token is available.
  • Added a deterministic failing Fleet-OPS secret reader and cleared ambient credential variables, ensuring the fallback lookup for Fleet-OPS prod /asc is exercised without relying on local secrets.
  • Verified that the consumer reports the missing ASC_KEY_ID, does not stage an ASC_KEY_PATH, removes the key-bearing entry from the job environment, and never exposes the key in output.

Summary

Restores the Sparkle release’s App Store Connect notarization credentials from the Fleet-OPS production Infisical /asc folder through fleet-secrets. Credential retrieval is validated before staging, while failures are classified so CI operators can distinguish missing vault entries, unresolved secret references, invalid tokens, and reader/tooling problems.

Functional changes

  • Verifies that the Fleet-OPS /asc folder contains all required credential names: ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8.
  • Reports specific missing credential names instead of misleadingly attributing them to insufficient vault read permissions.
  • Preserves environment-first credential resolution; the Fleet-OPS vault remains the fallback source.
  • Propagates the reader’s unresolved-reference result so an unexpanded Infisical secret is not reported as a missing credential.
  • Requires INFISICAL_TOKEN for standalone check-reader runs and distinguishes an absent token from a rejected token.
  • Redacts access-token values from reader diagnostics while preserving the remaining error detail.
  • Updates release workflow tests to confirm the reader is proven after token minting and before credentials are staged.
  • Updates docs/AUTO-UPDATE.md with the credential source, validation behavior, failure distinctions, and standalone verification instructions.

Outstanding review findings

  • Critical: Rejected-token errors do not include the required masked first-eight/last-four token preview.
  • High: The new vault-name validation always requires all credential names in the vault, even when valid environment-first credential sources should bypass that requirement.

Summary

Restores Sparkle notarization credential setup through Fleet-OPS Infisical and makes the release gate more resilient to canceled Swift CI jobs.

Changes

  • Aligns Infisical reader validation with the ASC credential staging flow: ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8 supplied through the job environment are no longer incorrectly reported as missing from the vault.
  • Masks the Infisical client ID, client secret, and access token in tooling errors using a partial preview, allowing operators to identify which configured credential was rejected. Values shorter than five characters remain fully redacted.
  • Treats canceled Swift CI checks as pending rather than failed because a canceled run provides no verdict about the code. Genuine failures continue to take precedence.
  • Allows the macOS release gate to dispatch a replacement swift-ci.yml run for the same commit when a compile check is canceled, with a maximum of three replacement attempts. The release workflow grants the gate actions: write to enable this behavior.
  • Isolates push-to-main Swift CI concurrency groups by commit SHA, preventing a later merge from canceling another merge’s compile run.
  • Expands Python, shell, and Node test coverage for canceled-check handling, replacement dispatch behavior, opt-in behavior, masked credential diagnostics, and environment-provided ASC credentials.

Security review follow-up

The automated review reported three critical findings in scripts/install-release-secrets-tooling.sh at line 94:

  • Short credentials can be printed in full by the masking helper, reported twice.
  • Rejected short credentials can still be exposed in full.

These findings remain outstanding and should be addressed before merge.


Summary

Updates the Fleet-OPS Infisical release-secrets tooling used by the Sparkle notarization workflow to improve credential-failure diagnostics and align reader validation with credential resolution.

Changes

  • Replaces fully redacted client IDs, client secrets, and access tokens in scrubbed error output with partial previews showing the first eight and last four characters.
  • Redacts credentials shorter than five characters as <redacted>.
  • Updates check_reader so ASC_KEY_ID, ASC_ISSUER_ID, and ASC_KEY_P8 do not need corresponding vault entries when they are already supplied through the job environment, matching stage-asc-key.mjs environment-first resolution.
  • Updates the rejected-reader test to expect the masked token preview.
  • Adds coverage confirming that an environment-provided ASC credential is not reported as missing from the vault.

Automated Review Findings

The automated review reported six unresolved findings:

  • Critical: Short credentials can be reconstructed from the masked preview.
  • Critical: Short credentials can be fully exposed by the masked preview.
  • Critical: The scrub replacement can let Bash expand & into the full secret.
  • Critical: The rejected-token preview reveals the complete short token.
  • High: Scrubbing publishes 12 characters of the long-lived client secret, weakening runner masking.
  • Medium: The environment-skip path accepts a blank value that the consumer rejects, allowing the reader check to pass incorrectly.

Summary

Hardens the Fleet-OPS Infisical credential checks used by the Sparkle notarization workflow so authentication failures remain actionable without exposing complete secrets.

Changes

  • Prevents Bash 5.2+ substitution behavior from expanding credential text containing & back into logs.
  • Fully redacts access tokens that are 12 characters or shorter, or contain &; longer tokens retain only an eight-character prefix and four-character suffix.
  • Fully redacts long-lived Infisical client IDs and client secrets instead of exposing partial previews.
  • Adds the CLI exit status and client-credential guidance to universal-auth login failures.
  • Treats whitespace-only environment values as absent, ensuring the corresponding ASC credential is still required from Infisical.
  • Expands tests to cover masking boundaries, ampersand handling across Bash versions, complete client-credential redaction, and whitespace-only environment values.

Fixes CC-177

@kody-ai

This comment has been minimized.

@kody-ai

kody-ai Bot commented Oct 18, 2026

Copy link
Copy Markdown

kody code-review Business Logic medium

🤔 Insufficient Task Context

I found a task linked to this PR, but it only contains minimal information (title only, no description or acceptance criteria). To perform a meaningful business rules validation, I need more details.

🔍 What I need to validate:

  • Business requirements and acceptance criteria
  • Expected behavior and business rules
  • Edge cases and constraints to consider

💡 How to improve the task context:

  • Add a description to the linked ticket
  • Include acceptance criteria or business rules
  • Describe the expected behavior after the change

⚠️ Important:

A task title alone is not sufficient to determine whether the implementation is correct or complete.


💡 This validation runs automatically only on the first review of a pull request. To run it again, comment @kody -v business-logic.

Comment thread .github/workflows/mac-release.yml Outdated
Comment thread .github/workflows/mac-release.yml
kody-ai[bot]
kody-ai Bot previously requested changes Oct 18, 2026

@kody-ai kody-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@kody-ai

This comment has been minimized.

The Mac release has failed on every merge to main since 2026-10-11.  The
cause was the credential source and not the release script: the workflow read
the App Store Connect key from repository secrets that are a revoked copy
(HTTP 401, CC-163), while the working key lives in Fleet-OPS prod /asc.

This replaces the bare `infisical secrets get` calls with the repository's
own safe fetch, in both workflows that read the key.

- ASC_KEY_ID and ASC_ISSUER_ID come from the infisical-secrets composite
  action, which masks every line of every value and never prints one.
- ASC_KEY_P8 is fetched by scripts/infisical-fetch-asc-key.mjs and streamed
  into scripts/ios-stage-asc-key.sh over a pipe, so the private key still
  never becomes an environment variable and only its mode-600 file path
  crosses the step boundary.

The project id is read from the INFISICAL_FLEET_OPS_PROJECT_ID secret
instead of being written into a public workflow, where it was also
interpolated into the job log on every failed notarization.

A dispatch-only job, "Verify the release credentials load", now fetches the
key and checks that Apple's notarization service still accepts it, and
publishes nothing.  A green pull-request check proves the workflow's own
contract, not that a credential can be fetched, which is why this outage ran
for nine days without a check noticing.

Fixes CC-177
A workflow_dispatch run of the release job would fetch the signing key and
publish a build from whatever branch it was dispatched on.  The new
verify-only input stops the release job at the publish gate, so the
credential check can be run against a branch before a merge without putting
a build in front of users.

The contract test pins the input and the gate that honours it.
A manual run can now be asked to fetch the release credentials and check
that Apple's notarization service still accepts them without publishing
anything.  The contract test pins the input and the gate that honours it, so
the two cannot drift apart.
@kody-ai

This comment has been minimized.

@jaywedgeworth22

Copy link
Copy Markdown
Member Author

Both findings are addressed in a1711aa, and the gate that was open is answered.

Finding 1 — the hard-coded project id (Rule 60). The id is no longer in this public file.  It now comes from the INFISICAL_FLEET_OPS_PROJECT_ID secret:

  • mac-release.yml and ios-ship.yml both read project-id: ${{ secrets.INFISICAL_FLEET_OPS_PROJECT_ID }}, and the id no longer appears in any error message, so a failed notarization no longer prints it either.
  • The value is the fleet's own project id, unchanged: the same id the fleet tool reads from ~/.agent-sync/fleet-ops-project-id, and the same project that holds ASC_KEY_ID, ASC_ISSUER_ID and ASC_KEY_P8 at /asc.

Finding 2 — the bare infisical secrets get (Rule 37). The CLI is gone from all three workflows.  grep -n 'infisical secrets get\|infisical/get-cli' .github/workflows/*.yml now returns nothing.

  • ASC_KEY_ID and ASC_ISSUER_ID come from the infisical-secrets composite action, which masks every line of every value and never prints one.
  • ASC_KEY_P8 is fetched by the new scripts/infisical-fetch-asc-key.mjs, which uses the same login, list and mask helpers, and streams the key into scripts/ios-stage-asc-key.sh over a pipe.  The private key therefore still never becomes an environment variable, and only its mode-600 file path crosses the step boundary.  That is the same handoff as before, with the unsafe read replaced.

The open gate. INFISICAL_FLEET_OPS_PROJECT_ID is not an Actions variable — it is an Actions secret, at the organisation level.  The repository has no Actions variables at all, and the organisation has exactly one, SWU_KODY_AUTOFIX_ENABLED.  A vars. reference to it would have resolved to nothing, which is why the metadata lookup returned 403. The secrets. spelling resolves.

One thing added beyond the findings. A dispatch-only job, "Verify the release credentials load", fetches the key and checks that Apple's notarization service still accepts it, and publishes nothing.  A green pull-request check proves the workflow's own contract, not that a credential can be fetched, which is why this outage ran for nine days without a check noticing.  It can be run against a branch before a merge, and the contract test pins its loader to the release job's so the two cannot drift.

Gates. 32 contract tests and the key-handoff shell test pass, including a new offline test that drives the real fetch script and the real staging script against a stubbed Infisical and checks that the key reaches a mode-600 file and never the environment or the log.

Fixes CC-177

@jaywedgeworth22
jaywedgeworth22 marked this pull request as ready for review October 20, 2026 20:57
Comment thread scripts/infisical-fetch-asc-key.mjs Outdated
loginUniversalAuth returns the token string directly, so `const { token }`
set it to undefined and the list call went out as `Bearer undefined`.
Assign the return value straight to `token`, matching the other caller in
scripts/infisical-fetch.mjs, and assert the bearer header in the contract
test so the stub can no longer accept an unauthenticated call.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@jaywedgeworth22
jaywedgeworth22 enabled auto-merge (squash) October 20, 2026 21:07
Comment thread .github/workflows/mac-release.yml
Comment thread .github/workflows/mac-release.yml Outdated
Comment thread .github/workflows/mac-release.yml Outdated
Comment thread scripts/infisical-fetch-asc-key.mjs Outdated
@kody-ai

This comment has been minimized.

@jaywedgeworth22
jaywedgeworth22 dismissed stale reviews from kody-ai[bot], kody-ai[bot], kody-ai[bot], kody-ai[bot], kody-ai[bot], kody-ai[bot], and kody-ai[bot] October 21, 2026 19:02

Addressed: every finding in this round was answered on a later commit and its thread is resolved. This review-level state is stale.

@jaywedgeworth22

Copy link
Copy Markdown
Member Author

Security finding: the mask helper reveals short values in full

The review summary on this pull request says the two critical findings in scripts/install-release-secrets-tooling.sh are still outstanding.  One of them is real on the current head, and it is measured rather than theoretical.

mask() was added in 8ef6a7e:

mask() {
  local value="$1"
  if [ "${#value}" -lt 5 ]; then
    printf '%s' "<redacted>"
    return
  fi
  printf '%s…%s' "${value:0:8}" "${value: -4}"
}

A first-eight and last-four preview shows the whole value whenever the value is eleven characters or shorter, because the two pieces overlap.  Measured on the branch's own function:

length output result
4 <redacted> hidden
5 abcde…bcde full value exposed
8 abcdefgh…efgh full value exposed
9 abcdefghi…fghi full value exposed
11 abcdefghijk…hijk full value exposed

The redacted branch only covers four characters or fewer, so the guard is off by the whole range that matters.  The second finding, that a rejected short credential can still be exposed in full, is this same defect seen from the rejected-token path, because the rejected value goes through this same helper.

The helper's own comment says the preview "is not reversible, because the ellipsis stands for the part that is not shown" and that "a value too short to have a hidden middle is shown as redacted instead".  With this threshold neither statement is true.

The fix

Keep the first-eight and last-four shape and move the threshold to where the hidden middle is at least as long as the prefix that is shown, so the preview can never be reconstructed:

  if [ "${#value}" -lt 20 ]; then
    printf '%s' "<redacted>"
    return
  fi
  printf '%s…%s' "${value:0:8}" "${value: -4}"

A value of twenty characters shows the first eight and the last four and hides eight characters in the middle.  A value of nineteen or shorter is redacted outright.

The test moves with it

scripts/stage-asc-key.node-test.mjs line 635 asserts the preview for the fixture test-token, which is ten characters, so under the corrected threshold that fixture is redacted and the assertion no longer describes a preview.  The fixture should be a realistic length so the preview case still exercises the preview, and a second case should assert that a short value is fully redacted and that none of its characters appear in the output.

What is not wrong here

The mac-release gate, the vault-only credential path and the ASC staging flow all pass on this head.  The gate is the credential-free half of the release, and the read-only notarization preflight against the Fleet-OPS prod key succeeds.

@jaywedgeworth22

Copy link
Copy Markdown
Member Author

Update: the owning session has this fixed in progress, and its threshold is better than the one above

The release branch's working tree already carries the fix for this finding, and it uses the exact boundary rather than the one proposed above.  The helper now redacts a value of twelve characters or fewer, and a new test drives four lengths through it: ten, twelve, thirteen and thirty two, asserting both the shape of the preview and that the hidden middle stays hidden.

The reasoning matches the measurement here: the two slices the preview is built from cover twelve characters between them, so a value of twelve characters or fewer is reconstructed in full by the two of them together, and thirteen is the first length with a middle left over.  That is the boundary, and it is tighter than the threshold proposed above, which is withdrawn.

No further change is needed from this seat, and this seat will not touch the branch or the lane the owning session is working in.

The masked preview was built from the first eight characters and the last
four of a value, and those two slices cover twelve characters between them.
A credential of twelve characters or fewer was therefore reconstructed in
full by the two of them together, and the ellipsis between them was only
decoration.  The preview now applies to a value of thirteen characters or
more, and a shorter one is redacted outright.

The message that carries the preview names the credential, so an operator
still knows which value was refused:  the universal auth login failure now
says that the client id or the client secret was refused, and the reader
failure already says that the minted access token was rejected.

The test that asserted the preview for a ten character token asserted the
leak it was meant to prevent, because that preview was the whole value.
It now asserts the redaction, and a new test walks the boundary:  ten and
twelve character values are redacted, thirteen and thirty character values
carry a preview, and no slice of any of them reaches the log.

Refs CC-177
Comment thread scripts/install-release-secrets-tooling.sh
Comment thread scripts/install-release-secrets-tooling.sh
Comment thread scripts/install-release-secrets-tooling.sh
Comment thread scripts/install-release-secrets-tooling.sh
Comment thread scripts/install-release-secrets-tooling.sh Outdated
Comment thread scripts/stage-asc-key.node-test.mjs Outdated
kody-ai[bot]
kody-ai Bot previously requested changes Oct 21, 2026

@kody-ai kody-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Three faults in the credential path, each with a test that fails without
its fix.

The replacement in a bash substitution is a pattern, and in bash 5.2 and
later an ampersand in it expands to the text that matched, which here was
the credential itself:  a value whose preview carried an ampersand was
written back into the log in full.  The option is now turned off where the
shell has it, and the preview refuses a value that could put an ampersand
in it.

The client id and the client secret stand for months and the runner masks
them by value, so a preview of either defeated that masking and put
twelve characters of a standing credential in a public log.  Both are
redacted whole now, and only the access token, which is minted for one
job, may carry a preview.

The proof read a name the environment carries as resolved, so a name the
environment held as whitespace was skipped and the vault was not asked
for it, while the consumer trims a value and refuses one that holds
nothing but whitespace.  The proof now ignores a value with nothing but
whitespace in it, so the vault still has to carry the name.

Refs CC-177
@jaywedgeworth22
jaywedgeworth22 dismissed stale reviews from kody-ai[bot], kody-ai[bot], and kody-ai[bot] October 21, 2026 19:43

Every finding in this round was answered on a later commit and its thread is resolved, so this review-level state is stale for the current head.

@jaywedgeworth22
jaywedgeworth22 merged commit ad87965 into main Oct 21, 2026
30 of 31 checks passed
@kody-ai

kody-ai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

​

@jaywedgeworth22

Copy link
Copy Markdown
Member Author

The release gate's cancelled-run recovery on this branch cannot work, and it is armed on the release path, so it will burn the release window instead of saving it.

The mechanism, in scripts/await-swift-ci-checks.sh lines 121 to 127 on the current head.  When the Swift CI checks report a cancelled run, the gate posts a replacement to the workflow dispatch endpoint with the commit sha as the ref:

gh api --method POST \
  "repos/${REPO}/actions/workflows/${REDISPATCH_WORKFLOW}/dispatches" \
  -f ref="$SHA"

The dispatch endpoint takes a branch or a tag name and rejects a commit sha.  The request answers 422 No ref found, so no run is created and the run count does not move.  This was proved live rather than read: the same request against a branch ref resolves, and the same request against a workflow without the trigger answers Workflow does not have workflow_dispatch trigger, so the failure is the ref and not the permission and not the trigger.

The armed path is mac-release.yml line 106, which sets REDISPATCH_ON_CANCEL to 1 on the await-swift-ci job.  The job has actions: write, so the request is permitted.  The gate therefore takes the dispatch branch, the request fails, and three things follow.

The budget is spent on failures.  The counter moves at line 119 before the request is made at line 121, so an attempt is counted even when nothing was created.  With MAX_REDISPATCH at 3, the gate can burn all three attempts on three requests that never produce a run.

The failure is swallowed.  The request result goes to /dev/null and the else branch at line 126 prints a warning that the replacement could not be dispatched and that the gate keeps waiting for a run to appear.  The gate then keeps polling for a run that was never created, until the deadline at DEADLINE_SECONDS, which is 5100 seconds.

The release job has a 90 minute timeout, so the gate spends about 85 of those minutes waiting for a run that does not exist and then fails with a timeout rather than with the real reason.

There is also a concurrency half, and it matters for what happens next.  The swift-ci.yml on this branch already gives a push to main one group per commit, so a later merge cannot cancel the compile run an earlier merge's release is waiting on.  That is the right shape and it stays.  But the pull request path is still one shared group per pull request with cancel-in-progress on, so every new push to this pull request cancels the compile run that the previous head started.  The run 39661660828 at head 7dc639b is one of those cancellations, and the head moved six times today.  That cancellation is exactly the condition the broken dispatch path is there to recover from, so the gate and the cancellation are going to meet.

The fix is ready on minimax/ci-redispatch at f6e8f6e, pushed and green.  It changes the mechanism from dispatching a new run to re-running the cancelled one, through the run rerun endpoint:

gh api --method POST "repos/${REPO}/actions/runs/${eligible_run_id}/rerun"

A re-run keeps the exact commit and the exact event of the run it replaces and produces a new, higher check run id, which is what lets the gate see that the replacement arrived.  The dispatch endpoint cannot do that, because it cannot name a commit.  The attempt counter now moves only after the re-run succeeds, so a token without actions: write cannot turn the gate into a loop, and a cancelled run with the flag off is a fast failure instead of a stall.

Verification on that branch: python3 scripts/compile_gate_checks_test.py ran 30 tests and passed, and node --test over the workflow test files ran 31 tests and passed, with 0 failures.  The new scripts/swift-ci-workflow.test.mjs asserts that the re-run endpoint is used and that the dispatch endpoint and its ref=sha form are absent.

This seat is not pushing to this branch, for the same reason as the earlier finding about the credential preview: another session owns it and has been moving the head today.  The change is the gate script, the workflow and their tests.

One correction to an earlier note from this seat, which said the concurrency half of this work was already in this branch.  It is there for a push to main, in the shape described above.  The pull request path still shares one group, and the event is not part of the group, so two runs at the same commit started by different events can cancel one another.  The branch on minimax/ci-redispatch adds the event to the group for everything that is not a pull request, so a push to main and a manual dispatch at the same commit do not share a group.

Sequencing: the pull request for this work opens after the release publishes, and the branch is moved onto the main that this pull request leaves behind before it opens.  Merging it will conflict with this branch in all five changed files, and the resolution keeps this branch's per-commit group and takes the re-run gate.

Refs CC-185

jaywedgeworth22 pushed a commit that referenced this pull request Oct 21, 2026
#257 merged to main as ad87965 and it reworked the iOS ship workflow's
credential path: the tooling and the staging script now run in one step and
the private key never enters a step environment.  This branch still carried
the pre-#257 copy of that workflow, with the inline Infisical login and a
hard-coded project id, so merging it as it stood would have reverted the
release fix for the iOS ship path.

Merging main brings the hardened step back.  No branch work was lost: the
branch had no changes to the files this merge replaced.
jaywedgeworth22 pushed a commit that referenced this pull request Oct 21, 2026
The squash of PR 271 landed while the credential handoff fix was still on
the branch, so main has the hardened credential load from #257 and the old
standing-testers step that calls the removed asc-key.mjs exec subcommand.
Merging main in picks up the squashed work and leaves the loader change as
the only difference.
# operator to fix vault permissions when the fault is the tooling. This check
# names the fault instead.
#
# The proof is read-only and lists names, never values: `fleet-secrets names

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug medium

Login failure message blames the client credentials for any non-zero exit

A non-zero infisical login --method=universal-auth exit can reflect a network or API failure, so blaming the credentials may prompt an unnecessary secret rotation. Change say to present the client ID, client secret, and API as checks rather than asserting that either credential was refused.

Prompt for LLM
File scripts/install-release-secrets-tooling.sh, line 227:

Login failure message blames the client credentials for any non-zero exit

The login-failure message now asserts a specific cause ("the client id or the client secret was refused") for every non-zero exit of `infisical login --method=universal-auth`, but that exit code is also produced by network failures, HTTP 429 and 5xx responses, none of which are a refused credential. A transient API or network fault during the release now tells the operator that the standing client credential was rejected, sending them to rotate a long-lived secret for a failure that has nothing to do with it, and the previous wording (exit code plus the CLI's own detail) let the detail speak for itself. Name the credential as the thing to check without claiming it was the cause, e.g. `say "the Infisical CLI refused the universal auth login (exit $status); check the client id, the client secret and the API: $(scrub "$(cat "$work/login.err")")"`.

Suggested code:

say "the Infisical CLI refused the universal auth login (exit $status); check the client id, the client secret and the API: $(scrub "$(cat "$work/login.err")")"

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

const cases = [
{ token: 'abcdefghij', preview: '<redacted>', hidden: 'abcdefghij' },
{ token: 'abcdefghijkl', preview: '<redacted>', hidden: 'abcdefghijkl' },
{ token: 'abcdefghZijkl', preview: 'abcdefgh…ijkl', hidden: 'Z' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Bug low

Preview test's one-character hidden middle makes it depend on error wording

A one-character hidden value makes the result.stderr.includes check depend on unrelated error wording and can fail without any credential leakage. Use a multi-character hidden value and scope the assertion to the rendered preview.

Prompt for LLM
File scripts/stage-asc-key.node-test.mjs, line 714:

Preview test's one-character hidden middle makes it depend on error wording

The 13-character preview case uses the single character `Z` as its "hidden middle" and asserts `!result.stderr.includes('Z')`, which scans the entire failure message rather than the credential preview. The case is coupled to the exact wording of the script's error text, so any future rewording that happens to contain a capital "Z" fails the contract test even though nothing leaked, and it contradicts the case list's own stated invariant that the hidden middle "does not occur in the prose around it" (the other three cases use multi-character strings for exactly that reason). Use a multi-character hidden middle so a collision with prose is implausible, and scope the assertion to the preview instead of the whole stderr.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

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