npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gitlab-ci-parser

v0.3.2

Published

Parse, normalize, and simulate GitLab CI pipelines in TypeScript

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-parser

Requires 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.yml from dir
  • pulls all include: (local with globs, project, template, component, remote)
  • expands extends: and !reference
  • evaluates workflow:rules, include rules:, and job rules: against the context you supply
  • expands parallel: N and parallel: matrix: into concrete jobs
  • prunes optional: true needs that point to excluded jobs
  • returns SimulatedPipeline — list of jobs, stages, the matched workflow outcome, and an excluded list 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