@savvy-web/github-action-effects
v2.0.0
Published
Effect-based utility library for building robust, well-logged, and schema-validated GitHub Actions.
Downloads
1,115
Maintainers
Readme
@savvy-web/github-action-effects
Effect services for building GitHub Actions. You get schema-validated inputs, structured logging that maps to workflow commands and typed wrappers around the GitHub API, with no @actions/* packages anywhere in the dependency tree.
Features
- Zero CJS dependencies — native ESM implementations of the GitHub Actions runtime protocol replace all
@actions/*packages - 37 composable services — action I/O, GitHub API calls, git operations, package publishing and software attestation, each with its own
Context.Tag - Schema-validated inputs — read action inputs via Effect's
ConfigAPI with built-in parsing and defaults - Structured logging — Effect Logger maps to workflow commands with collapsible groups; buffered verbose output flushes inside its group when a step fails
- Step-buffered execution —
Step.withStepbuffers debug output per logical step, emits one success line on pass and spills the full buffer prefixed with the step name on failure - Software attestation — sign and upload SLSA provenance and CycloneDX SBOMs to GitHub's attestation store via the
Attest,SigstoreSigner,OidcTokenIssuerandSbomservices - In-memory test layers — every service ships a test layer for fast, deterministic unit tests
Install
npm install @savvy-web/github-action-effects effect @effect/platform @effect/platform-nodeQuick start
// src/main.ts
import { Config, Effect } from "effect";
import { Action, ActionOutputs } from "@savvy-web/github-action-effects";
const program = Effect.gen(function* () {
const name = yield* Config.string("package-name");
const outputs = yield* ActionOutputs;
yield* outputs.set("result", `checked ${name}`);
});
Action.run(program);Action.run provides ActionsRuntime.Default (ConfigProvider, Logger, core services, and Node.js platform layers), catches errors, and sets the workflow exit status automatically.
GitHub API clients
GitHubClientLive builds a GitHubClient layer one of three ways:
GitHubClientLive.fromEnv()— reads the ambientprocess.env.GITHUB_TOKEN, the repo-scoped workflow token. It is a function; call it with no arguments.GitHubClientLive.fromToken(token)— an explicit token with noprocess.envdependency. The token is aRedacted<string>— wrap a bare string withRedacted.make(...).GitHubClientLive.fromApp({ clientId, privateKey, installationId? })— mints an installation token from GitHub App credentials, withprivateKeyas aRedacted<string>. It is a scoped layer that revokes the token on scope close and requiresHttpClient.HttpClient; wrap a bareEffect.provideinEffect.scoped.
import { Effect } from "effect";
import { Action, GitHubClient, GitHubClientLive } from "@savvy-web/github-action-effects";
const program = Effect.gen(function* () {
const client = yield* GitHubClient;
const { owner, repo } = yield* client.repo;
return yield* client.rest("issues.list", (octokit) =>
octokit.rest.issues.listForRepo({ owner, repo }),
);
}).pipe(Effect.provide(GitHubClientLive.fromEnv()));
Action.run(program);The repo-scoped token is often too weak for permission-sensitive work. When that happens, pass fromToken a token you constructed yourself, or use fromApp to act as a GitHub App installation.
GitHub App token lifecycle
A GitHub Action runs in three phases — pre, main and post. The GitHubToken namespace generates one installation token in pre, hands main a client built from it and revokes it in post.
GitHubToken.provision and GitHubToken.dispose require a GitHubApp layer. In production, compose GitHubAppLive with OctokitAuthAppLive and provide the result to those effects.
// pre.ts — generate and persist the installation token
import { Effect, Layer } from "effect";
import { Action, GitHubAppLive, GitHubToken, OctokitAuthAppLive } from "@savvy-web/github-action-effects";
const appLayer = Layer.provide(GitHubAppLive, OctokitAuthAppLive);
Action.run(
GitHubToken.provision({
permissions: { contents: "write", pull_requests: "write" },
}).pipe(Effect.provide(appLayer)),
);// main.ts — build a GitHubClient from the persisted token
import { Effect } from "effect";
import { Action, GitHubClient, GitHubToken } from "@savvy-web/github-action-effects";
const program = Effect.gen(function* () {
const client = yield* GitHubClient;
const { owner, repo } = yield* client.repo;
return yield* client.rest("repos.get", (octokit) =>
octokit.rest.repos.get({ owner, repo }),
);
}).pipe(Effect.provide(GitHubToken.client()));
Action.run(program);// post.ts — revoke the token
import { Effect, Layer } from "effect";
import { Action, GitHubAppLive, GitHubToken, OctokitAuthAppLive } from "@savvy-web/github-action-effects";
const appLayer = Layer.provide(GitHubAppLive, OctokitAuthAppLive);
Action.run(GitHubToken.dispose().pipe(Effect.provide(appLayer)));provision reads App credentials from its options object or, by default, from the app-client-id and app-private-key action inputs. Passing permissions verifies the generated token grants those scopes before it is persisted. It also resolves the App's public identity (slug, bot user ID, name) best-effort and stores it on the token, so later phases can call GitHubToken.botIdentity() without an extra API call.
Two additional accessors are available in any phase after provision:
GitHubToken.read()— anEffect<InstallationToken, ActionStateError, ActionState>that reads the full persisted token envelope, including the optionalappSlug,appUserIdandappNamefields resolved duringprovision.GitHubToken.botIdentity()— anEffect<BotIdentity, ActionStateError, ActionState>that derives a commit-attribution identity from the persisted token. When the App's slug and user ID were resolved, the returned email uses the<userId>+<slug>[bot]@users.noreply.github.comformat that GitHub recognises for verified attribution; otherwise it falls back to the well-knowngithub-actions[bot]identity.
Documentation
- Building a GitHub Action with Effect — An end-to-end walkthrough: validated inputs, logging, a step summary and typed outputs.
- Advanced action: three-stage app — A complete pre/main/post action with GitHub App auth, cross-phase state and buffered logging.
- Services guide — A usage example for every service in the library.
- Common patterns — Dry-run mode, error accumulation, permission checks and workspace detection.
- Building a robust action — Principles and pointers: wiring, the pre/main/post pattern, dry runs, permission checks, idempotency and secret handling.
- Coming from
@actions/*— The migration map from each@actions/*package to its native ESM replacement. - Logging and error handling — The log-level model, groups, buffered output, annotations, secret masking and the error-handling boundary.
- Resilient GitHub API calls — Default-on retry,
ResilienceOptions, theRateLimiterservice and streaming pagination. - Step-buffered logging patterns — Quiet-on-success, verbose-on-failure step logging with
withStep,collapseandgroupStep. - Generating SLSA attestations — Provenance and SBOM attestations, the layer stack and idempotent recovery.
- Publishing packages with the publish chain — Pack, probe and publish a tarball, plus registry classification.
- Peer dependencies — Which packages to install and why.
- Error handling — Tagged errors,
Action.formatCauseand the[Tag] messageformat. - Architecture — The runtime layer, layer composition and the logging pipeline.
- Filesystem I/O —
IoUtil(which/findInPath) and thecp/mv/rmRF/mkdirP→FileSystemrecipe. - Testing GitHub Actions — How to test an action with in-memory test layers.
