Context Simulation
glci simulates different CI pipeline contexts so you can test how rules: evaluate without pushing to GitLab. The default context is merge_request.
Usage#
glci run --context branch=main
glci run --context merge_request --mr-source feature --mr-target main
glci run --context tag=v1.2.3
glci run --context env=production
glci run --context tag=v1.0 --env RELEASE=true
glci show --context merge_request
Context types#
Each context type sets specific CI variables that drive rules: evaluation:
branch#
glci run --context branch=main
Sets CI_COMMIT_BRANCH, CI_COMMIT_REF_NAME, CI_COMMIT_REF_SLUG, CI_DEFAULT_BRANCH, and CI_PIPELINE_SOURCE=push.
merge_request (default)#
glci run --context merge_request --mr-source feature/login --mr-target main
Source defaults to the current git branch, target defaults to the detected default branch. Sets CI_PIPELINE_SOURCE=merge_request_event and the 23 CI_MERGE_REQUEST_* variables a detached merge request pipeline publishes — see Merge request variables.
tag#
glci run --context tag=v1.0.0
Sets CI_COMMIT_TAG, CI_COMMIT_REF_NAME, CI_COMMIT_REF_SLUG, and CI_PIPELINE_SOURCE=push. Does not set CI_COMMIT_BRANCH.
env#
glci run --context env=production
Sets CI_ENVIRONMENT_NAME and CI_PIPELINE_SOURCE=push, while preserving the current branch/tag from git.
CI variables by context type#
Variables not listed here (CI_COMMIT_SHA, CI_PROJECT_NAME, etc.) are derived from git and remain the same across all contexts.
| Variable | branch=X | merge_request | tag=X | env=X |
|---|---|---|---|---|
CI_COMMIT_BRANCH | X | (not set) | (not set) | (from git) |
CI_COMMIT_TAG | (not set) | (not set) | X | (from git) |
CI_COMMIT_REF_NAME | X | source branch | X | (from git) |
CI_COMMIT_REF_SLUG | slug of X | slug of source branch | slug of X | (from git) |
CI_DEFAULT_BRANCH | resolved | (resolved) | (resolved) | (resolved) |
CI_PIPELINE_SOURCE | push | merge_request_event | push | push |
CI_MERGE_REQUEST_* | (not set) | 23 variables | (not set) | (not set) |
CI_OPEN_MERGE_REQUESTS | (not set) | <project path>!1 | (not set) | (not set) |
CI_ENVIRONMENT_NAME | (not set) | (not set) | (not set) | X |
Merge request variables#
--context merge_request publishes the 23 CI_MERGE_REQUEST_* variables a detached merge request pipeline publishes on GitLab, plus CI_OPEN_MERGE_REQUESTS, which GitLab publishes on the same pipeline because the merge request being built is itself an open one for its source branch. There is no merge request locally, so the values describing one are synthetic; the rest are derived from git or mirror the project’s own CI_PROJECT_* values. Three are conditional on what the local repository can supply and are marked below.
| Variable | glci value |
|---|---|
CI_MERGE_REQUEST_EVENT_TYPE | detached |
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME | Current git branch (or --mr-source) |
CI_OPEN_MERGE_REQUESTS | <CI_PROJECT_PATH>!1, omitted when the project path is not derivable |
CI_MERGE_REQUEST_TARGET_BRANCH_NAME | Default branch (or --mr-target) |
CI_MERGE_REQUEST_DIFF_BASE_SHA | git merge-base HEAD <target>, resolving <target> as a local branch or as origin/<target>, omitted when uncomputable |
CI_MERGE_REQUEST_REF_PATH | refs/merge-requests/1/head |
CI_MERGE_REQUEST_IID | 1 |
CI_MERGE_REQUEST_ID, CI_MERGE_REQUEST_DIFF_ID | 1 |
CI_MERGE_REQUEST_TITLE | local |
CI_MERGE_REQUEST_DESCRIPTION | empty |
CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED | false |
CI_MERGE_REQUEST_DRAFT | false |
CI_MERGE_REQUEST_SQUASH_ON_MERGE | false |
CI_MERGE_REQUEST_SOURCE_BRANCH_PROTECTED, CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED | false |
CI_MERGE_REQUEST_SOURCE_BRANCH_SHA, CI_MERGE_REQUEST_TARGET_BRANCH_SHA | empty |
CI_MERGE_REQUEST_PROJECT_ID, CI_MERGE_REQUEST_SOURCE_PROJECT_ID | CI_PROJECT_ID (0) |
CI_MERGE_REQUEST_PROJECT_PATH, CI_MERGE_REQUEST_SOURCE_PROJECT_PATH | CI_PROJECT_PATH |
CI_MERGE_REQUEST_PROJECT_URL, CI_MERGE_REQUEST_SOURCE_PROJECT_URL | CI_PROJECT_URL — omitted without a remote |
A simulated merge request is same-project, so the project and source project triples carry identical values — as they do on GitLab for a merge request that is not from a fork.
Empty is not the same as unset. CI_MERGE_REQUEST_DESCRIPTION, CI_MERGE_REQUEST_SOURCE_BRANCH_SHA and CI_MERGE_REQUEST_TARGET_BRANCH_SHA are published empty rather than omitted. They are defined, so if: $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA == null is false. GitLab fills the two SHAs only in merged-result pipelines, which have no local equivalent, so a detached pipeline publishes them empty; the description is empty for the same reason CI_MERGE_REQUEST_TITLE is local — there is no merge request to read one from.
Four variables are never published. CI_MERGE_REQUEST_LABELS, CI_MERGE_REQUEST_MILESTONE, CI_MERGE_REQUEST_ASSIGNEES and CI_MERGE_REQUEST_APPROVED are omitted entirely, because GitLab defines them only once the merge request carries a label, a milestone, an assignee or an approval. Publishing them as constants would make a rule see a defined variable where GitLab defines none.
CI_MERGE_REQUEST_DIFF_BASE_SHA is omitted when the merge base cannot be computed: the repository knows the target branch under no name, or it is a shallow clone that shares no history with it. GitLab always publishes it with a value, so an empty one is a shape it cannot produce.
The target branch is named bare (main), and a clone need not have a local branch of that name: git clone --single-branch -b feature, a worktree cut from a bare mirror and a CI checkout all leave origin/main with no local main. glci tries main, then origin/main, then refs/remotes/origin/main, and rules: changes: resolves the target the same way, so the two always describe the same range.
On a detached HEAD (a repository checked out at a SHA, a bisect, a rebase) git symbolic-ref names nothing, and a merge request with no source branch is a shape GitLab cannot produce. glci then names the commit by the tag an exact match finds, or by a branch whose tip it is. Only a commit reached by none of the three leaves CI_MERGE_REQUEST_SOURCE_BRANCH_NAME empty.
CI_MERGE_REQUEST_PROJECT_URL and CI_MERGE_REQUEST_SOURCE_PROJECT_URL are omitted in a repository with no remote, because CI_PROJECT_URL itself is — mirroring an absent value as an empty one would invent a project. A missing remote does not affect the _PATH mirrors, since CI_PROJECT_PATH falls back to local/<basename>. In a directory git cannot read as a repository, or one with no commits yet, neither pair is published, because nothing is derivable there.
--mr-source sets CI_MERGE_REQUEST_SOURCE_BRANCH_NAME and CI_COMMIT_REF_NAME only. The diff base is always the merge base of the target branch and the checked-out HEAD, as rules: changes: already is, so naming a source branch that is not the one checked out renames the variable without moving the diff.
CI_MERGE_REQUEST_DIFF_BASE_SHA and CI_MERGE_REQUEST_REF_PATH name things in your repository, not in the one the job checks out. glci serves a single synthetic commit and no refs/merge-requests/*, so git diff "$CI_MERGE_REQUEST_DIFF_BASE_SHA"...HEAD and git fetch origin "$CI_MERGE_REQUEST_REF_PATH" both fail inside a job, the same way git diff origin/<default>...HEAD does. Read the values; do not resolve them against the job’s checkout.
CI_DEFAULT_BRANCH#
CI_DEFAULT_BRANCH is the project’s default branch, not the branch you happen to have checked out. glci resolves it from [defaults] default_branch, then refs/remotes/origin/HEAD, then main — so rules: if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH matches locally only where it would match on GitLab.
Because it is a project property, it is published in every context, together with CI_DEFAULT_BRANCH_SLUG, including in a repository with no commits yet, where nothing else is derivable from git. A rule comparing against it is never comparing against the empty string.
For a repository whose clone has no origin/HEAD and whose default branch is not main, name it in the project’s .glciconfig.toml:
[defaults]
default_branch = "master"
git remote set-head origin -a also works, by teaching git the project’s default branch so glci follows it — but it is per-clone, so it has to be repeated wherever the pipeline runs.
--env CI_DEFAULT_BRANCH=<branch> is a different thing: it overrides the variable for a single run, and does not change the branch the served repository advertises (below) or CI_DEFAULT_BRANCH_SLUG. GitLab behaves the same way — both keys are built from the project’s default branch rather than from each other — so use the config key when you mean “this is the project’s default branch”.
The repository glci serves to the runner advertises the same branch as its HEAD, the way a GitLab-hosted repository does. With git 2.47 or newer in the job image, git records that as refs/remotes/origin/HEAD during the checkout, so a job reading it agrees with $CI_DEFAULT_BRANCH; on older git the ref is simply absent, exactly as in real CI.
Careful with diffs against the default branch. The repository glci serves is a single synthetic commit, and it carries the default branch as a second name for that same commit. So origin/<default-branch> resolves in a job, but it points at the commit under test rather than at the project’s real default branch: git diff origin/main...HEAD comes back empty instead of failing with unknown revision. A job that lints or scans “only what changed against the default branch” therefore checks nothing locally, and passes. Don’t read a green result from such a job as meaningful — run it against real history.
Comparing contexts#
# Compare job sets across contexts
diff <(glci show --json --context branch=main) \
<(glci show --json --context tag=v2.0.0)
Combining context with –env#
--env adds or overrides variables on top of the context-derived set. Precedence: --env > context-derived > git-derived. See Variables & Secrets for the full table.
glci run --context branch=staging --env DEPLOY_ENV=staging
glci run --context tag=v1.0 --env-file release.env
--env/--env-file work the same way on glci show, glci jobs, and glci variables, so you can preview which jobs a variable-scoped rules: gate selects — without running anything:
# Which jobs run when DEPLOY_ENV=prod, under a tag pipeline?
glci show --context tag=v1.0 --env DEPLOY_ENV=prod
glci jobs --env DEPLOY_ENV=prod
Context presets#
Save frequently-used contexts in .glciconfig.toml or ~/.glci/config.toml:
[contexts.staging]
context = "branch=staging"
env = { DEPLOY_ENV = "staging" }
[contexts.mr-to-main]
context = "merge_request"
mr_source = "feature/current"
mr_target = "main"
Use by name:
glci run --context staging
glci show --context mr-to-main
Context presets can also be embedded in pipeline presets:
[pipelines.quick]
stages = ["lint", "test"]
context = "merge_request"