@effected/github-commands
The GitHub Actions workflow-command grammar as pure functions: render a command with the runner's escaping, and neutralize text so the runner cannot read it as one. Strings in, strings out. No service, no layer, no platform, no effect, and no dependency at all.
Pre-
1.0.0. This package is part of the@effected/*kit, still in0.xdevelopment. It has no dependencies, not eveneffect.Stability: unstable. This package's API surface is not yet considered complete and may change across
0.xreleases. Pin an exact version. Full policy: release strategy.
Why @effected/github-commands
The runner reads every line a step writes, and a line it recognises as a command does what the command says: ::add-mask:: redacts, ::error:: raises an annotation, ::stop-commands:: turns processing off. Text a program did not write itself, such as an error message or a file name, can therefore carry a command. The runner has two parsers, so "starts with ::" is only half the rule: the legacy one finds ##[ anywhere in a line.
This package is the rule written once. WorkflowCommand builds the commands you mean to write, with the escaping the protocol needs. CommandNeutralizer defangs the text you did not mean as one.
Install
npm install @effected/github-commands
pnpm add @effected/github-commands
Requires Node.js >=24.11.0. No dependencies and no peers. ESM-only: import it from an ES module.
Usage
import { CommandNeutralizer, WorkflowCommand } from "@effected/github-commands";
WorkflowCommand.error("build failed", { file: "src/main.ts", startLine: 12 });
// "::error file=src/main.ts,line=12::build failed"
WorkflowCommand.warning("deprecated: 50% of calls\nuse v2", { title: "API: v1" });
// "::warning title=API%3A v1::deprecated: 50%25 of calls%0Ause v2"
CommandNeutralizer.text("prefix ##[add-mask]secret\n::error::x");
// a braille pattern blank (U+2800) inside the ##[ and before the ::, so neither parser reads a command
WorkflowCommand:render(name, properties, message)and thedebug,notice,warning,error,group,endGroupandaddMaskhelpers. Annotation properties use readable names (startLine,startColumn) and go out as the wire's (line,col). The message escapes%, CR and LF; a property value also escapes:and,; the%goes first so an escape is never escaped twice.CommandNeutralizer:lines(text)andtext(text). A bare##is left alone, so a markdown heading is unharmed. It splits at CR, LF and CRLF as the runner does, and it is idempotent: neutralizing twice changes nothing.
@effected/github-actions re-exports WorkflowCommand, and @effected/cli-kit neutralizes its output through CommandNeutralizer whenever the runner is GitHub Actions.
Documentation
The guide and the API reference are at effected.spencerbeg.gs/github-commands.