RFC 067: std.ci — deterministic CI and automation scripting primitives¶
- Status: Draft
- Created: 2026-04-16
- Author(s): Danny Meijer (@dannymeijer)
- Related:
- RFC 015 (hatch-like tooling and project lifecycle CLI)
- RFC 051 (
JsonValueforstd.json) - RFC 055 (
std.fspath-centric filesystem APIs) - RFC 063 (
std.processprocess spawning and command execution) - RFC 066 (
std.httpHTTP client surface)
- Issue: https://github.com/dannys-code-corner/incan/issues/85
- RFC PR: —
- Written against: v0.2
- Shipped in: —
Summary¶
This RFC proposes std.ci as Incan's standard library surface for deterministic CI and automation scripts. The module family standardizes environment access, workflow input and output files, small automation-oriented filesystem helpers, argument and exit handling, and integration with std.http and JsonValue so repository automation does not need to fall back to fragile shell glue or JavaScript snippets.
Core model¶
Read this RFC as one foundation plus three mechanisms:
- Foundation: CI scripting is ordinary Incan automation work, not a special-purpose embedded DSL or provider-specific builtin.
- Mechanism A:
std.ciprovides a narrow set of primitives for the execution environment: env vars, argument access, output files, and explicit exit behavior. - Mechanism B: network access remains a separate general-purpose concern through
std.http, not a CI-only HTTP surface. - Mechanism C: provider-specific behavior such as GitHub payload interpretation should live in libraries built on these primitives rather than as a special builtin contract.
Motivation¶
CI automation is usually held together by shell scripts, YAML snippets, and tiny bits of JavaScript. That works, but it is a poor foundation for a language that wants to be credible in tooling and automation. Scripts become hard to test locally, hard to refactor, hard to type-check, and hard to reuse.
This is especially visible for workflows such as issue triage, label synchronization, release automation, or event-driven repository checks. Those tasks typically need the same small set of capabilities:
- read environment variables
- read an event payload file
- write outputs or step summaries
- make HTTP requests
- exit clearly and deterministically
Today, each of those usually gets reinvented with ad hoc shell behavior or backend-specific libraries. std.ci should provide one explicit and testable base layer instead.
Goals¶
- Provide a small stdlib surface for deterministic CI and automation scripts.
- Standardize environment-variable access, argument access, exit behavior, and workflow-output file handling.
- Keep HTTP out of the CI module itself by composing through RFC 066
std.http. - Make the surface portable across hosted CI systems while still being usable for GitHub Actions-style workflows.
- Support local fixture-driven testing of automation scripts.
- Keep secrets handling explicit and conservative.
Non-Goals¶
- Shipping a GitHub-specific, GitLab-specific, or provider-specific SDK as part of
std.ci. - Replacing
std.http,std.fs, orstd.process;std.cishould compose with them, not duplicate them. - Defining a full JWT or cryptography story here beyond whatever minimal hooks later prove necessary.
- Defining a workflow YAML format or a hosted-runner orchestration system.
- Making CI automation a language feature rather than a stdlib surface.
Guide-level explanation¶
Reading the execution environment¶
An automation script should be able to read environment variables explicitly:
from std.ci.env import get, get_optional
event_path = get("GITHUB_EVENT_PATH")?
token = get_optional("GITHUB_TOKEN")
Missing required values should be explicit failures, not silent None unless the user asked for optional access.
Reading workflow payloads¶
Scripts should be able to combine std.ci primitives with JsonValue:
from std.ci.env import get
from std.ci.fs import read_text
from std.json import JsonValue
payload = JsonValue.parse(read_text(get("GITHUB_EVENT_PATH")?)?)?
issue_number = payload["issue"]["number"].as_int()
This is still ordinary Incan. The CI-specific part is only how the script learns where the payload file lives.
Writing outputs¶
Workflow systems often expose file-based output channels. The stdlib should make that explicit:
from std.ci.output import set_output
set_output("triaged", "true")?
or, for a more direct file-oriented model:
from std.ci.env import get
from std.ci.fs import append_text
append_text(get("GITHUB_OUTPUT")?, "triaged=true\n")?
The important point is that the file-write behavior is explicit and testable.
Exiting clearly¶
Automation scripts should be able to fail or succeed explicitly:
from std.ci.process import exit
if not ok:
exit(1)
This does not replace structured Result-returning APIs. It only gives the script one explicit terminal action when needed.
Reference-level explanation¶
Module family¶
std.ci must provide a narrow but sufficient set of modules or equivalent surfaces covering:
- environment access
- workflow input and output file handling
- process arguments and exit behavior
- deterministic helper behavior suitable for local testing
The module family may be split as:
std.ci.envstd.ci.fsstd.ci.processstd.ci.output
or another equivalent subdivision, but the public contract must remain small and explicit.
Environment access¶
The environment-access surface must provide:
- a required getter that fails clearly when a variable is absent
- an optional getter that returns
Nonewhen absent
It must not silently coerce missing values into empty strings.
Workflow file handling¶
The CI surface must support the common workflow pattern of reading payload files and writing output files. It may do this either through:
- explicit helpers such as
set_output(...), or - a smaller primitive model built on environment variables and append-safe file helpers
This RFC does not require one exact spelling yet, but it does require the capability.
Exit behavior¶
std.ci must provide explicit process-exit behavior or a functionally equivalent way to terminate with a chosen exit code.
Determinism and testability¶
The module family should prefer deterministic behavior:
- no hidden network
- no hidden retries
- no hidden provider-specific fallbacks
CI scripts must remain locally testable with fixture payloads and explicit environment setup.
Secrets and diagnostics¶
The CI surface should avoid exposing secrets carelessly in debug-facing or error-facing helpers. It does not need to define a complete secret type system, but it should follow conservative diagnostic practices and defer richer secret handling to dedicated follow-up work where needed.
Design details¶
Syntax¶
This RFC introduces no new language syntax.
Semantics¶
The semantic center is narrowness:
std.ciis not a general automation frameworkstd.ciis not an HTTP stackstd.ciis not a provider-specific SDK
It is a small bridge between hosted automation environments and ordinary Incan code.
Interaction with existing features¶
- RFC 066 (
std.http): HTTP access for automation scripts should go throughstd.http, not a CI-only network API. - RFC 051 (
JsonValue): event payload parsing should compose naturally withJsonValue. - RFC 055 (
std.fs): path and file handling should stay coherent with the broader filesystem surface where possible. - RFC 063 (
std.process): argument access and exit behavior should align with the broader process model rather than diverge. - RFC 015: a future CLI wrapper such as
incan cimay exist, but it is not required for the base stdlib contract.
Compatibility / migration¶
This feature is additive. Existing bash, shell, or JavaScript-based CI steps remain valid. The design claim is that Incan should offer a better standard path for new automation scripts once the module exists.
Alternatives considered¶
- Keep shell and JavaScript glue only
- Rejected because it leaves automation brittle, weakly typed, and difficult to test.
- Make GitHub a builtin
- Rejected because it introduces too much provider policy too early.
- Push everything into
std.httpplusstd.fs - Rejected because CI environments do have a small amount of distinct execution-context behavior worth standardizing.
Drawbacks¶
- There is a real risk of overfitting the module to one provider if the design is not kept disciplined.
- Output-file and env-var conventions vary across systems, so portability rules need careful wording.
- Secret-handling expectations need to be conservative even if the module stays intentionally small.
Implementation architecture¶
(Non-normative.) A sensible rollout starts with env, fs, and process primitives plus fixture-driven examples that demonstrate local testing against payload files. Once std.http is available, real CI automation flows can compose on top without needing any provider-specific builtin APIs.
Layers affected¶
- Stdlib / runtime: must provide the CI module family and its deterministic environment-facing helpers.
- Language surface: the module family must be available as specified and must compose cleanly with ordinary Incan code.
- Execution handoff: implementations must preserve explicit environment, file, and exit semantics without hidden provider-specific behavior.
- Docs / tooling: examples should show local fixture-driven testing and explicit composition with
std.httpandJsonValue.
Unresolved questions¶
- Should output-file helpers such as
set_output(...)be first-class, or should the contract stay one layer lower and rely on environment variables plus file append helpers? - Should
std.cistandardize step-summary and annotation helpers in the base module family, or leave those to follow-up provider libraries? - Does a minimal JWT-signing helper belong here for GitHub App workflows, or should all cryptographic behavior remain outside this RFC?
- Should a future
incan ciCLI wrapper be part of the same design family, or remain separate from the stdlib contract?