gitlab-ci-parser
v0.3.2
Published
Parse, normalize, and simulate GitLab CI pipelines in TypeScript
Maintainers
Readme
gitlab-ci-parser
Parse, normalize, and simulate GitLab CI pipelines in TypeScript — figure out which jobs would run for a given commit, merge request, or scheduled run, without pushing anything to GitLab.
Install
npm install gitlab-ci-parserRequires Node 22+.
Quick start
import { simulatePipeline, mrContext } from "gitlab-ci-parser";
const pipeline = await simulatePipeline(
"./my-project",
mrContext({
sourceBranch: "feature-x",
targetBranch: "main",
defaultBranch: "main",
projectPath: "my-group/my-project",
}),
);
for (const job of pipeline.jobs) {
console.log(`${job.stage.value.padEnd(10)} ${job.name.value} (${job.when.value})`);
}Every value-bearing field on SimulatedJob is wrapped as Node<T> = { value: T; pos: SourcePos } so source positions travel with the value. Use .value to read the underlying value; .pos carries { file, line, col } and file is a tagged shape distinguishing root YAML, include:d files, components, templates, remote URLs, and synthetic positions for library defaults / computed values.
simulatePipeline(dir, ctx):
- reads
.gitlab-ci.ymlfromdir - pulls all
include:(local with globs, project, template, component, remote) - expands
extends:and!reference - evaluates
workflow:rules, includerules:, and jobrules:against the context you supply - expands
parallel: Nandparallel: matrix:into concrete jobs - prunes
optional: trueneeds that point to excluded jobs - returns
SimulatedPipeline— list of jobs, stages, the matched workflow outcome, and anexcludedlist explaining why each filtered job was dropped
Context presets
| Preset | Pipeline source |
| ------------------------------------------------ | --------------------- |
| pushContext({ branch, ... }) | push |
| mrContext({ sourceBranch, targetBranch, ... }) | merge_request_event |
| tagContext({ tag, ... }) | push (with tag) |
| scheduleContext({ branch, ... }) | schedule |
All four take the common shape:
{
projectPath: string; // "group/project"
serverHost?: string; // default "gitlab.com"
defaultBranch?: string; // default "main"
variables?: Record<string, string>;
changes?: string[]; // for `changes:` rules
existsRoot?: string; // local repo root for `exists:` rules
}You can also build a PipelineContext object directly if you need finer
control (e.g. set commitSha, refProtected, projectId, or richer
mergeRequest fields).
Filtering by changes:
import { simulatePipeline, mrContext } from "gitlab-ci-parser";
const pipeline = await simulatePipeline(
".",
mrContext({
sourceBranch: "feature-x",
targetBranch: "main",
defaultBranch: "main",
projectPath: "my-group/my-project",
changes: ["src/components/Button.tsx", "package.json"],
}),
);Jobs whose rules: use changes: ["src/**"] will match; ones using
changes: ["docs/**"] won't.
Lower-level entry points
If you only want the merged YAML config (with includes/extends/!reference resolved) and not the simulation layer:
import { processProject } from "gitlab-ci-parser";
const config = await processProject("./my-project");For private GitLab instances, pass { gitlabHost, gitlabToken } or your own
fetchProjectFile/fetchRemote to processProject / simulatePipeline —
both functions accept the same ProcessOptions.
Output shape
Every field that originated in YAML is wrapped as Node<T> = { value: T; pos: SourcePos }.
type SourceFile =
| { kind: "local"; path: string } // root .gitlab-ci.yml or include: local:
| { kind: "remote"; url: string } // include: remote:
| { kind: "project"; project: string; ref?: string; file: string }
| { kind: "component"; component: string; version?: string; file: string }
| { kind: "template"; name: string }
| { kind: "synthetic"; reason: "library_default" | "predefined" | "ctx" | "matrix" | "computed" };
type SourcePos = { file: SourceFile; line: number; col: number };
type Node<T> = { value: T; pos: SourcePos };
type SimulatedPipeline = {
jobs: SimulatedJob[];
stages: Node<string>[];
variables: ScopedVariables;
workflow?: WorkflowOutcome;
excluded: ExcludedJob[];
};
type SimulatedJob = {
name: Node<string>; // post-parallel: synthetic pos
stage: Node<string>;
when: Node<"on_success" | "on_failure" | "manual" | "delayed" | "always">;
allowFailure: Node<boolean>;
interruptible?: Node<boolean>;
needs: Node<{ job: string; optional?: boolean }>[];
variables: Record<string, { value: string; origin: VariableOrigin; pos: SourcePos }>;
script?: Node<string>[]; // per-element positions
beforeScript?: Node<string>[];
afterScript?: Node<string>[];
tags?: Node<string>[];
timeout?: Node<string>;
// these carry the whole subtree as a Node, with positions inside:
image?: Node;
services?: Node;
retry?: Node;
cache?: Node;
artifacts?: Node;
matchedRule?: { index: number; rule: Node };
trace: { rules?: RulesTrace };
};Library defaults (e.g. stage = "test" when unset) and predefined / matrix variables get a synthetic SourceFile so callers can distinguish "I picked this" from "the YAML at this line says this".
Status
0.x — usable but pre-1.0; the public API may still shift. See
ROADMAP.md for what's done and what's left.
License
MIT
