Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
986162d270 | ||
|
|
3eea496b9f | ||
|
|
ef0d7b118a | ||
|
|
b222105e1f |
@@ -32,18 +32,64 @@ jobs:
|
|||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
run: |
|
run: |
|
||||||
go build -trimpath -ldflags="-s -w" \
|
go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.version=${{ github.ref_name }}" \
|
||||||
-o glint-${{ github.ref_name }}-linux-amd64 \
|
-o glint-${{ github.ref_name }}-linux-amd64 \
|
||||||
./cmd/glint/...
|
./cmd/glint/...
|
||||||
|
|
||||||
|
- name: Build Linux (arm64)
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: arm64
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
run: |
|
||||||
|
go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.version=${{ github.ref_name }}" \
|
||||||
|
-o glint-${{ github.ref_name }}-linux-arm64 \
|
||||||
|
./cmd/glint/...
|
||||||
|
|
||||||
|
- name: Build macOS (amd64)
|
||||||
|
env:
|
||||||
|
GOOS: darwin
|
||||||
|
GOARCH: amd64
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
run: |
|
||||||
|
go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.version=${{ github.ref_name }}" \
|
||||||
|
-o glint-${{ github.ref_name }}-darwin-amd64 \
|
||||||
|
./cmd/glint/...
|
||||||
|
|
||||||
|
- name: Build macOS (arm64)
|
||||||
|
env:
|
||||||
|
GOOS: darwin
|
||||||
|
GOARCH: arm64
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
run: |
|
||||||
|
go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.version=${{ github.ref_name }}" \
|
||||||
|
-o glint-${{ github.ref_name }}-darwin-arm64 \
|
||||||
|
./cmd/glint/...
|
||||||
|
|
||||||
- name: Build Windows (amd64)
|
- name: Build Windows (amd64)
|
||||||
env:
|
env:
|
||||||
GOOS: windows
|
GOOS: windows
|
||||||
GOARCH: amd64
|
GOARCH: amd64
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
run: |
|
run: |
|
||||||
go build -trimpath -ldflags="-s -w" \
|
go build -trimpath \
|
||||||
-o glint-${{ github.ref_name }}.exe \
|
-ldflags="-s -w -X main.version=${{ github.ref_name }}" \
|
||||||
|
-o glint-${{ github.ref_name }}-windows-amd64.exe \
|
||||||
|
./cmd/glint/...
|
||||||
|
|
||||||
|
- name: Build Windows (arm64)
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
GOARCH: arm64
|
||||||
|
CGO_ENABLED: "0"
|
||||||
|
run: |
|
||||||
|
go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.version=${{ github.ref_name }}" \
|
||||||
|
-o glint-${{ github.ref_name }}-windows-arm64.exe \
|
||||||
./cmd/glint/...
|
./cmd/glint/...
|
||||||
|
|
||||||
- name: Create release and upload assets
|
- name: Create release and upload assets
|
||||||
@@ -60,7 +106,13 @@ jobs:
|
|||||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}" \
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":false,\"prerelease\":false}" \
|
||||||
| jq -r .id)
|
| jq -r .id)
|
||||||
|
|
||||||
for file in glint-${{ github.ref_name }}-linux-amd64 glint-${{ github.ref_name }}.exe; do
|
for file in \
|
||||||
|
glint-${TAG}-linux-amd64 \
|
||||||
|
glint-${TAG}-linux-arm64 \
|
||||||
|
glint-${TAG}-darwin-amd64 \
|
||||||
|
glint-${TAG}-darwin-arm64 \
|
||||||
|
glint-${TAG}-windows-amd64.exe \
|
||||||
|
glint-${TAG}-windows-arm64.exe; do
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token $TOKEN" \
|
-H "Authorization: token $TOKEN" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ coverage.txt
|
|||||||
# Task runner cache
|
# Task runner cache
|
||||||
.task/
|
.task/
|
||||||
|
|
||||||
|
# Tmp
|
||||||
|
.tmp/
|
||||||
|
|
||||||
# Claude Code project memory
|
# Claude Code project memory
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
This project uses [Semantic Versioning](https://semver.org).
|
This project uses [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-07-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Windows ARM64 build target** — `task build-windows-arm64` cross-compiles `glint-<tag>-windows-arm64.exe`; the Gitea release CI uploads it alongside the other five platform binaries.
|
||||||
|
- **`workflow.rules` AND-group support** — the `- -` nested-array rule form (AND-groups) is now parsed and evaluated with correct AND logic: all conditions in a group must match for the group to fire; `when:` and `variables:` from all matching members are merged. Previously this form caused a YAML unmarshal crash.
|
||||||
|
- **`workflow.name` and `workflow.auto_cancel` fields** — no longer cause a parse error when present; both are decoded into the `Workflow` struct.
|
||||||
|
- **GL046 — `pull_policy` validation** — errors when `image.pull_policy` or `services[n].pull_policy` uses a value other than `always`, `if-not-present`, or `never`; both the string scalar and list-of-strings forms are checked.
|
||||||
|
- **GL047 — `variables.options` default validation** — errors when a pipeline-level or job-level variable declares an `options:` list and its `value:` (default) is absent from that list; GitLab rejects such pipelines at creation time.
|
||||||
|
- **GL048 — `trigger.forward` key validation** — errors on unrecognised keys inside `trigger.forward:`; only `pipeline_variables` and `yaml_variables` are valid.
|
||||||
|
- **GL049 — `rules[n].allow_failure` validation** — validates rule-level `allow_failure:` (GitLab CI 15.0+): must be a boolean or a map with an `exit_codes:` key; applies the same checks as the job-level GL014.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **GL032 false positive in workflow rules** — variables set by any workflow rule's `variables:` block are now excluded from GL032 undeclared-variable warnings when referenced in sibling workflow rule `if:` expressions; previously only pipeline-level `variables:` entries were exempt.
|
||||||
|
|
||||||
|
## [0.4.1] - 2026-06-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Release CI** — added Linux arm64, macOS Intel, and macOS Apple Silicon build steps; renamed Windows output to `glint-<tag>-windows-amd64.exe` for consistency; added missing `-X main.version=<tag>` ldflags so release binaries report the correct version instead of `"dev"`.
|
||||||
|
|
||||||
## [0.4.0] - 2026-06-26
|
## [0.4.0] - 2026-06-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Pre-built binaries are attached to each [release](https://git.k3nny.fr/k3nny/gli
|
|||||||
| macOS Intel | `glint-vX.Y.Z-darwin-amd64` |
|
| macOS Intel | `glint-vX.Y.Z-darwin-amd64` |
|
||||||
| macOS Apple Silicon | `glint-vX.Y.Z-darwin-arm64` |
|
| macOS Apple Silicon | `glint-vX.Y.Z-darwin-arm64` |
|
||||||
| Windows x86-64 | `glint-vX.Y.Z-windows-amd64.exe` |
|
| Windows x86-64 | `glint-vX.Y.Z-windows-amd64.exe` |
|
||||||
|
| Windows ARM64 | `glint-vX.Y.Z-windows-arm64.exe` |
|
||||||
|
|
||||||
### Linux (amd64)
|
### Linux (amd64)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License"></a>
|
||||||
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/release-v0.4.0-blue.svg" alt="Release"></a>
|
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/release-v0.5.0-blue.svg" alt="Release"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> **Disclaimer:** This tool was built through iterative AI-assisted development with [Claude](https://claude.ai). It is experimental, incomplete, and not intended for production use. Coverage of GitLab CI keywords is best-effort and may lag behind GitLab's evolving spec. Use it at your own discretion — no correctness guarantees are made. Contributions and bug reports are welcome.
|
> **Disclaimer:** This tool was built through iterative AI-assisted development with [Claude](https://claude.ai). It is experimental, incomplete, and not intended for production use. Coverage of GitLab CI keywords is best-effort and may lag behind GitLab's evolving spec. Use it at your own discretion — no correctness guarantees are made. Contributions and bug reports are welcome.
|
||||||
@@ -15,7 +15,7 @@ A local tool to validate and lint `.gitlab-ci.yml` pipelines without needing a G
|
|||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
- **Lints** — 45 rules covering pipeline structure, keyword constraints, `needs:`/`dependencies:` graphs, expression reachability, and deprecations (GL001–GL045); run `glint explain <ID>` for any rule
|
- **Lints** — 49 rules covering pipeline structure, keyword constraints, `needs:`/`dependencies:` graphs, expression reachability, and deprecations (GL001–GL049); run `glint explain <ID>` for any rule
|
||||||
- **Resolves includes** — local files, HTTPS URLs, GitLab project templates, and CI/CD Catalog components, with offline cache support and HTTP proxy support (`--proxy` flag or `proxy:` in `.glint.yml`)
|
- **Resolves includes** — local files, HTTPS URLs, GitLab project templates, and CI/CD Catalog components, with offline cache support and HTTP proxy support (`--proxy` flag or `proxy:` in `.glint.yml`)
|
||||||
- **Renders merged pipeline** — `glint render` resolves all includes and `extends:` chains into a single flat YAML file, matching what GitLab CI actually processes
|
- **Renders merged pipeline** — `glint render` resolves all includes and `extends:` chains into a single flat YAML file, matching what GitLab CI actually processes
|
||||||
- **Simulates context** — `--branch`, `--tag`, `--source` flags evaluate `rules:if:` and `only`/`except` to show which jobs would be active, manual, or skipped; `--context branch=main --context branch=develop` prints a multi-column comparison table across multiple contexts in one run
|
- **Simulates context** — `--branch`, `--tag`, `--source` flags evaluate `rules:if:` and `only`/`except` to show which jobs would be active, manual, or skipped; `--context branch=main --context branch=develop` prints a multi-column comparison table across multiple contexts in one run
|
||||||
@@ -158,6 +158,7 @@ task build-linux-arm64 # cross-compile for Linux ARM64 (requires a tagged comm
|
|||||||
task build-darwin-amd64 # cross-compile for macOS Intel (requires a tagged commit)
|
task build-darwin-amd64 # cross-compile for macOS Intel (requires a tagged commit)
|
||||||
task build-darwin-arm64 # cross-compile for macOS Apple Silicon (requires a tagged commit)
|
task build-darwin-arm64 # cross-compile for macOS Apple Silicon (requires a tagged commit)
|
||||||
task build-windows # cross-compile for Windows x86-64 (requires a tagged commit)
|
task build-windows # cross-compile for Windows x86-64 (requires a tagged commit)
|
||||||
|
task build-windows-arm64 # cross-compile for Windows ARM64 (requires a tagged commit)
|
||||||
task build-release # build all platform binaries at once (requires a tagged commit)
|
task build-release # build all platform binaries at once (requires a tagged commit)
|
||||||
task clean # remove build artifacts
|
task clean # remove build artifacts
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ This document tracks planned improvements to `glint`. Items are grouped by theme
|
|||||||
|
|
||||||
Pass `--branch`, `--tag`, `--source`, or `--var` to `glint check` or `glint graph` to evaluate `rules:if:` expressions and `only`/`except` filters against a specific pipeline event.
|
Pass `--branch`, `--tag`, `--source`, or `--var` to `glint check` or `glint graph` to evaluate `rules:if:` expressions and `only`/`except` filters against a specific pipeline event.
|
||||||
|
|
||||||
|
- [x] **`workflow.rules` AND-group semantics** — shipped v0.5.0; the `- -` nested-array rule form (AND-groups) is parsed and evaluated with correct AND logic: all conditions in a group must match for the group to fire; variables and `when:` from all matching group members are merged
|
||||||
- [x] **Single-context simulation** — shipped v0.2.0; `--branch`, `--tag`, `--source`, `--var` flags on both subcommands; jobs classified as active / manual / skipped
|
- [x] **Single-context simulation** — shipped v0.2.0; `--branch`, `--tag`, `--source`, `--var` flags on both subcommands; jobs classified as active / manual / skipped
|
||||||
- [x] **`workflow:rules:variables:` propagation** — shipped post-v0.2.0; variables from the matching workflow rule entry injected into the evaluation context before job `rules:if:` expressions are evaluated
|
- [x] **`workflow:rules:variables:` propagation** — shipped post-v0.2.0; variables from the matching workflow rule entry injected into the evaluation context before job `rules:if:` expressions are evaluated
|
||||||
- [x] **Expression evaluator: multi-line `if:` values** — shipped post-v0.2.0; newlines in block-scalar and folded YAML `if:` values treated as whitespace
|
- [x] **Expression evaluator: multi-line `if:` values** — shipped post-v0.2.0; newlines in block-scalar and folded YAML `if:` values treated as whitespace
|
||||||
@@ -32,6 +33,10 @@ Pass `--branch`, `--tag`, `--source`, or `--var` to `glint check` or `glint grap
|
|||||||
|
|
||||||
The current rule set covers the most common sources of broken pipelines. These are the gaps most likely to matter in practice.
|
The current rule set covers the most common sources of broken pipelines. These are the gaps most likely to matter in practice.
|
||||||
|
|
||||||
|
- [x] **`image.pull_policy` / `services[n].pull_policy` validation (GL046)** — shipped v0.5.0; validates that `pull_policy` values are one of `always`, `if-not-present`, `never`; applies to both `image:` map form and service entries; list form also checked element-by-element
|
||||||
|
- [x] **`variables.options` default value validation (GL047)** — shipped v0.5.0; errors when a variable declares an `options:` list and its `value:` (default) is not listed; checked at pipeline level and per-job; GitLab rejects such pipelines at creation time
|
||||||
|
- [x] **`trigger.forward` key validation (GL048)** — shipped v0.5.0; errors on unrecognised keys inside `trigger.forward:`; only `pipeline_variables` and `yaml_variables` are valid
|
||||||
|
- [x] **`rules[n].allow_failure` validation (GL049)** — shipped v0.5.0; validates rule-level `allow_failure:` (GitLab CI 15.0+) using the same rules as job-level GL014: must be a boolean or a `{exit_codes:}` map
|
||||||
- [x] **Variable reference validation (GL032)** — shipped v0.2.11; warns when a `rules:if:` expression references `$VAR` / `${VAR}` not declared anywhere in pipeline YAML; predefined GitLab namespaces (`CI_*`, `GITLAB_*`, …) exempt; variables from included files are also considered
|
- [x] **Variable reference validation (GL032)** — shipped v0.2.11; warns when a `rules:if:` expression references `$VAR` / `${VAR}` not declared anywhere in pipeline YAML; predefined GitLab namespaces (`CI_*`, `GITLAB_*`, …) exempt; variables from included files are also considered
|
||||||
- [x] **`rules:if:` static reachability (GL033)** — shipped v0.2.15; warns when every rule in a job's `rules:` block has `when: never`, making the job permanently excluded from any pipeline run; no `if:` evaluation required
|
- [x] **`rules:if:` static reachability (GL033)** — shipped v0.2.15; warns when every rule in a job's `rules:` block has `when: never`, making the job permanently excluded from any pipeline run; no `if:` evaluation required
|
||||||
- [x] **`services:` validation (GL034)** — shipped v0.2.16; map form requires `name`; `alias` must be a valid DNS label
|
- [x] **`services:` validation (GL034)** — shipped v0.2.16; map form requires `name`; `alias` must be a valid DNS label
|
||||||
|
|||||||
+20
-1
@@ -117,6 +117,8 @@ tasks:
|
|||||||
ignore_error: false
|
ignore_error: false
|
||||||
- cmd: ./{{.BINARY}} check testdata/rules_needs_invalid.yml
|
- cmd: ./{{.BINARY}} check testdata/rules_needs_invalid.yml
|
||||||
ignore_error: true
|
ignore_error: true
|
||||||
|
- cmd: ./{{.BINARY}} check testdata/nested_rules.yml
|
||||||
|
ignore_error: true
|
||||||
- cmd: ./{{.BINARY}} explain GL007
|
- cmd: ./{{.BINARY}} explain GL007
|
||||||
ignore_error: false
|
ignore_error: false
|
||||||
- cmd: ./{{.BINARY}} explain gl042
|
- cmd: ./{{.BINARY}} explain gl042
|
||||||
@@ -226,6 +228,22 @@ tasks:
|
|||||||
generates:
|
generates:
|
||||||
- "{{.BINARY}}-{{.TAG}}-windows-amd64.exe"
|
- "{{.BINARY}}-{{.TAG}}-windows-amd64.exe"
|
||||||
|
|
||||||
|
build-windows-arm64:
|
||||||
|
desc: Build the glint binary for Windows ARM64 (requires a tagged commit)
|
||||||
|
vars:
|
||||||
|
TAG:
|
||||||
|
sh: git describe --tags --exact-match
|
||||||
|
preconditions:
|
||||||
|
- sh: git describe --tags --exact-match
|
||||||
|
msg: "Current commit is not tagged — release build requires a git tag"
|
||||||
|
cmds:
|
||||||
|
- "GOOS=windows GOARCH=arm64 {{.GO}} build -ldflags \"-X main.version={{.TAG}}\" -o {{.BINARY}}-{{.TAG}}-windows-arm64.exe ./cmd/glint/..."
|
||||||
|
sources:
|
||||||
|
- "**/*.go"
|
||||||
|
- go.mod
|
||||||
|
generates:
|
||||||
|
- "{{.BINARY}}-{{.TAG}}-windows-arm64.exe"
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
desc: Build release binaries for all supported platforms (requires a tagged commit)
|
desc: Build release binaries for all supported platforms (requires a tagged commit)
|
||||||
cmds:
|
cmds:
|
||||||
@@ -234,6 +252,7 @@ tasks:
|
|||||||
- task: build-darwin-amd64
|
- task: build-darwin-amd64
|
||||||
- task: build-darwin-arm64
|
- task: build-darwin-arm64
|
||||||
- task: build-windows
|
- task: build-windows
|
||||||
|
- task: build-windows-arm64
|
||||||
|
|
||||||
fuzz:
|
fuzz:
|
||||||
desc: "Run all fuzz targets (set FUZZ_TIME=60s to control per-target duration, default 30s)"
|
desc: "Run all fuzz targets (set FUZZ_TIME=60s to control per-target duration, default 30s)"
|
||||||
@@ -270,4 +289,4 @@ tasks:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
desc: Remove build artifacts
|
desc: Remove build artifacts
|
||||||
cmd: rm -f {{.BINARY}} {{.BINARY}}-*.exe {{.BINARY}}-*-linux-amd64
|
cmd: rm -f {{.BINARY}} {{.BINARY}}-*.exe {{.BINARY}}-*-linux-amd64 {{.BINARY}}-*-linux-arm64 {{.BINARY}}-*-darwin-amd64 {{.BINARY}}-*-darwin-arm64
|
||||||
|
|||||||
@@ -42,22 +42,18 @@ func EvalWorkflow(p *model.Pipeline, ctx *Context) (bool, map[string]string) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
vars := ctx.Get
|
vars := ctx.Get
|
||||||
for _, rule := range p.Workflow.Rules {
|
// Workflow rules use strict evaluation: an unparseable condition is treated
|
||||||
// Workflow rules use strict evaluation: an unparseable condition is
|
// as no-match so later rules are reached. Permissive-true would cause an
|
||||||
// treated as no-match so later rules (with valid conditions or a
|
// early rule with a complex condition to block all subsequent rules.
|
||||||
// bare when:) are reached. Permissive-true would cause an early rule
|
for _, group := range p.Workflow.Rules.Groups() {
|
||||||
// with a complex/invalid condition to block all subsequent rules.
|
matched, when, groupVars := evalRuleGroup(group, vars, ctx, true)
|
||||||
if !ruleIfMatchesStrict(rule.If, vars) {
|
if !matched {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !changesMatch(rule.Changes, ctx) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
when := rule.When
|
|
||||||
if when == "" {
|
if when == "" {
|
||||||
when = "always"
|
when = "always"
|
||||||
}
|
}
|
||||||
return when != "never", ExtractStringVars(rule.Variables)
|
return when != "never", groupVars
|
||||||
}
|
}
|
||||||
return false, nil // no rule matched → pipeline does not run
|
return false, nil // no rule matched → pipeline does not run
|
||||||
}
|
}
|
||||||
@@ -74,14 +70,12 @@ func EvalJob(job model.Job, ctx *Context) JobState {
|
|||||||
|
|
||||||
// rules: takes priority over only/except.
|
// rules: takes priority over only/except.
|
||||||
if len(job.Rules) > 0 {
|
if len(job.Rules) > 0 {
|
||||||
for _, rule := range job.Rules {
|
for _, group := range job.Rules.Groups() {
|
||||||
if !ruleIfMatches(rule.If, vars) {
|
matched, when, _ := evalRuleGroup(group, vars, ctx, false)
|
||||||
|
if !matched {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !changesMatch(rule.Changes, ctx) {
|
return whenToState(when)
|
||||||
continue
|
|
||||||
}
|
|
||||||
return whenToState(rule.When)
|
|
||||||
}
|
}
|
||||||
return JobSkipped // no rule matched → job is excluded
|
return JobSkipped // no rule matched → job is excluded
|
||||||
}
|
}
|
||||||
@@ -98,6 +92,35 @@ func EvalJob(job model.Job, ctx *Context) JobState {
|
|||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// evalRuleGroup evaluates one rule group (one or more rules forming an AND-group).
|
||||||
|
// All conditions in the group must match for the group to fire.
|
||||||
|
// Returns (matched, effectiveWhen, mergedVars). When strict=true, unparseable
|
||||||
|
// if: expressions count as no-match; when false, they count as match (permissive).
|
||||||
|
func evalRuleGroup(group []model.Rule, vars func(string) string, ctx *Context, strict bool) (bool, string, map[string]string) {
|
||||||
|
for _, rule := range group {
|
||||||
|
var ifOk bool
|
||||||
|
if strict {
|
||||||
|
ifOk = ruleIfMatchesStrict(rule.If, vars)
|
||||||
|
} else {
|
||||||
|
ifOk = ruleIfMatches(rule.If, vars)
|
||||||
|
}
|
||||||
|
if !ifOk || !changesMatch(rule.Changes, ctx) {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged := make(map[string]string)
|
||||||
|
effectiveWhen := ""
|
||||||
|
for _, rule := range group {
|
||||||
|
for k, v := range ExtractStringVars(rule.Variables) {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
if rule.When != "" {
|
||||||
|
effectiveWhen = rule.When
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, effectiveWhen, merged
|
||||||
|
}
|
||||||
|
|
||||||
func ruleIfMatches(ifExpr string, vars func(string) string) bool {
|
func ruleIfMatches(ifExpr string, vars func(string) string) bool {
|
||||||
if ifExpr == "" {
|
if ifExpr == "" {
|
||||||
return true // no if: condition → rule always matches
|
return true // no if: condition → rule always matches
|
||||||
|
|||||||
@@ -867,4 +867,86 @@ test:
|
|||||||
Fix: `include:
|
Fix: `include:
|
||||||
- remote: https://ci-templates.example.com/build.yml`,
|
- remote: https://ci-templates.example.com/build.yml`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
RuleInvalidPullPolicy: {
|
||||||
|
Title: "'pull_policy:' has invalid value",
|
||||||
|
Severity: Error,
|
||||||
|
Description: "'image.pull_policy' and 'services[n].pull_policy' must be one of " +
|
||||||
|
"'always', 'if-not-present', or 'never', or a list of those values. " +
|
||||||
|
"Any other string is rejected by GitLab at pipeline creation time.",
|
||||||
|
Example: `my-job:
|
||||||
|
image:
|
||||||
|
name: alpine
|
||||||
|
pull_policy: on-demand # not a valid value
|
||||||
|
script: echo hi`,
|
||||||
|
Fix: `my-job:
|
||||||
|
image:
|
||||||
|
name: alpine
|
||||||
|
pull_policy: if-not-present
|
||||||
|
script: echo hi`,
|
||||||
|
},
|
||||||
|
|
||||||
|
RuleVariableValueNotInOptions: {
|
||||||
|
Title: "variable default value not listed in 'options'",
|
||||||
|
Severity: Error,
|
||||||
|
Description: "A pipeline variable declares an 'options' list that constrains what " +
|
||||||
|
"values can be chosen when triggering the pipeline manually. If the 'value' " +
|
||||||
|
"(the default) is not in that list, GitLab rejects the pipeline at creation " +
|
||||||
|
"time with a validation error.",
|
||||||
|
Example: `variables:
|
||||||
|
DEPLOY_ENV:
|
||||||
|
value: staging
|
||||||
|
options:
|
||||||
|
- production
|
||||||
|
- review # 'staging' is missing from the list`,
|
||||||
|
Fix: `variables:
|
||||||
|
DEPLOY_ENV:
|
||||||
|
value: staging
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
- review`,
|
||||||
|
},
|
||||||
|
|
||||||
|
RuleInvalidTriggerForward: {
|
||||||
|
Title: "'trigger.forward:' has unrecognised key",
|
||||||
|
Severity: Error,
|
||||||
|
Description: "'trigger.forward' controls which variables are forwarded to the " +
|
||||||
|
"downstream pipeline. Only 'pipeline_variables' and 'yaml_variables' are " +
|
||||||
|
"valid keys. Any other key is silently ignored by some GitLab versions and " +
|
||||||
|
"rejected by others.",
|
||||||
|
Example: `deploy:
|
||||||
|
trigger:
|
||||||
|
include:
|
||||||
|
- artifact: pipeline.yml
|
||||||
|
job: build
|
||||||
|
forward:
|
||||||
|
all_variables: true # not a valid key`,
|
||||||
|
Fix: `deploy:
|
||||||
|
trigger:
|
||||||
|
include:
|
||||||
|
- artifact: pipeline.yml
|
||||||
|
job: build
|
||||||
|
forward:
|
||||||
|
pipeline_variables: true
|
||||||
|
yaml_variables: true`,
|
||||||
|
},
|
||||||
|
|
||||||
|
RuleInvalidRulesAllowFailure: {
|
||||||
|
Title: "'rules[n].allow_failure:' invalid value",
|
||||||
|
Severity: Error,
|
||||||
|
Description: "'allow_failure' inside a 'rules:' entry (GitLab CI 15.0+) must be " +
|
||||||
|
"a boolean (true/false) or a map with an 'exit_codes' key. This overrides " +
|
||||||
|
"the job-level 'allow_failure' when the rule matches.",
|
||||||
|
Example: `my-job:
|
||||||
|
script: ./test.sh
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
allow_failure: maybe # must be true/false or a map`,
|
||||||
|
Fix: `my-job:
|
||||||
|
script: ./test.sh
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
allow_failure: true`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ func checkJobKeywords(name string, job model.Job) []Finding {
|
|||||||
findings = append(findings, checkSecrets(name, job)...)
|
findings = append(findings, checkSecrets(name, job)...)
|
||||||
findings = append(findings, checkPagesKeyword(name, job)...)
|
findings = append(findings, checkPagesKeyword(name, job)...)
|
||||||
findings = append(findings, checkCacheKeyFiles(name, job)...)
|
findings = append(findings, checkCacheKeyFiles(name, job)...)
|
||||||
|
findings = append(findings, checkPullPolicy(name, job)...)
|
||||||
|
findings = append(findings, checkRulesAllowFailure(name, job)...)
|
||||||
return findings
|
return findings
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +315,20 @@ func checkTrigger(name string, job model.Job) []Finding {
|
|||||||
Message: "'trigger' map must specify 'project' or 'include'",
|
Message: "'trigger' map must specify 'project' or 'include'",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// GL048: validate trigger.forward keys.
|
||||||
|
if fwd, ok := m["forward"].(map[string]any); ok {
|
||||||
|
validForwardKeys := map[string]bool{"pipeline_variables": true, "yaml_variables": true}
|
||||||
|
for k := range fwd {
|
||||||
|
if !validForwardKeys[k] {
|
||||||
|
findings = append(findings, Finding{
|
||||||
|
Severity: Error,
|
||||||
|
Rule: RuleInvalidTriggerForward,
|
||||||
|
Job: name,
|
||||||
|
Message: fmt.Sprintf("'trigger.forward' has unrecognised key %q; valid keys: pipeline_variables, yaml_variables", k),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return findings
|
return findings
|
||||||
}
|
}
|
||||||
@@ -793,6 +809,85 @@ func checkPagesKeyword(name string, job model.Job) []Finding {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL046: image.pull_policy and services[n].pull_policy must use recognised values.
|
||||||
|
var validPullPolicy = map[string]bool{
|
||||||
|
"always": true, "if-not-present": true, "never": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPullPolicy(name string, job model.Job) []Finding {
|
||||||
|
var findings []Finding
|
||||||
|
findings = append(findings, checkPullPolicyValue(name, "image", job.Image)...)
|
||||||
|
for i, svc := range job.Services {
|
||||||
|
findings = append(findings, checkPullPolicyValue(name, fmt.Sprintf("services[%d]", i), svc)...)
|
||||||
|
}
|
||||||
|
return findings
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPullPolicyValue(jobName, field string, v any) []Finding {
|
||||||
|
m, ok := v.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pp, exists := m["pull_policy"]
|
||||||
|
if !exists || pp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var policies []string
|
||||||
|
switch x := pp.(type) {
|
||||||
|
case string:
|
||||||
|
policies = []string{x}
|
||||||
|
case []any:
|
||||||
|
for _, item := range x {
|
||||||
|
if s, ok := item.(string); ok {
|
||||||
|
policies = append(policies, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var findings []Finding
|
||||||
|
for _, p := range policies {
|
||||||
|
if !validPullPolicy[p] {
|
||||||
|
findings = append(findings, Finding{
|
||||||
|
Severity: Error,
|
||||||
|
Rule: RuleInvalidPullPolicy,
|
||||||
|
Job: jobName,
|
||||||
|
Message: fmt.Sprintf("%s.pull_policy has unrecognised value %q; valid: always, if-not-present, never", field, p),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findings
|
||||||
|
}
|
||||||
|
|
||||||
|
// GL049: rules[n].allow_failure must be a boolean or a map with exit_codes:.
|
||||||
|
func checkRulesAllowFailure(name string, job model.Job) []Finding {
|
||||||
|
var findings []Finding
|
||||||
|
for i, rule := range job.Rules {
|
||||||
|
if rule.AllowFailure == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch v := rule.AllowFailure.(type) {
|
||||||
|
case bool:
|
||||||
|
// valid
|
||||||
|
case map[string]any:
|
||||||
|
if _, ok := v["exit_codes"]; !ok {
|
||||||
|
findings = append(findings, Finding{
|
||||||
|
Severity: Error,
|
||||||
|
Rule: RuleInvalidRulesAllowFailure,
|
||||||
|
Job: name,
|
||||||
|
Message: fmt.Sprintf("rules[%d].allow_failure map form must contain 'exit_codes'", i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
findings = append(findings, Finding{
|
||||||
|
Severity: Error,
|
||||||
|
Rule: RuleInvalidRulesAllowFailure,
|
||||||
|
Job: name,
|
||||||
|
Message: fmt.Sprintf("rules[%d].allow_failure must be a boolean or a map with 'exit_codes'", i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findings
|
||||||
|
}
|
||||||
|
|
||||||
// GL041: cache.key.files must be a list of exact file paths, not glob patterns.
|
// GL041: cache.key.files must be a list of exact file paths, not glob patterns.
|
||||||
func checkCacheKeyFiles(name string, job model.Job) []Finding {
|
func checkCacheKeyFiles(name string, job model.Job) []Finding {
|
||||||
if job.Cache == nil {
|
if job.Cache == nil {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func Lint(p *model.Pipeline, skipped map[string]bool) []Finding {
|
|||||||
findings = append(findings, checkDuplicateStages(p)...)
|
findings = append(findings, checkDuplicateStages(p)...)
|
||||||
findings = append(findings, checkDefault(p)...)
|
findings = append(findings, checkDefault(p)...)
|
||||||
findings = append(findings, checkWorkflow(p)...)
|
findings = append(findings, checkWorkflow(p)...)
|
||||||
|
findings = append(findings, checkPipelineVariableOptions(p)...)
|
||||||
findings = append(findings, checkJobs(p)...)
|
findings = append(findings, checkJobs(p)...)
|
||||||
findings = append(findings, checkNeeds(p, skipped)...)
|
findings = append(findings, checkNeeds(p, skipped)...)
|
||||||
findings = append(findings, checkRulesNeeds(p, skipped)...)
|
findings = append(findings, checkRulesNeeds(p, skipped)...)
|
||||||
@@ -223,6 +224,7 @@ func checkJob(name string, job model.Job, stageSet map[string]bool) []Finding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findings = append(findings, checkJobKeywords(name, job)...)
|
findings = append(findings, checkJobKeywords(name, job)...)
|
||||||
|
findings = append(findings, checkVariableOptionsForJob(name, job)...)
|
||||||
|
|
||||||
// Attach source location to every job-scoped finding collected above.
|
// Attach source location to every job-scoped finding collected above.
|
||||||
for i := range findings {
|
for i := range findings {
|
||||||
@@ -235,6 +237,55 @@ func checkJob(name string, job model.Job, stageSet map[string]bool) []Finding {
|
|||||||
return findings
|
return findings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GL047: variable declared with options: must have its default value in the options list.
|
||||||
|
|
||||||
|
func checkPipelineVariableOptions(p *model.Pipeline) []Finding {
|
||||||
|
return checkVariableOptionsMap(p.Variables, "", p.SourceFile, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVariableOptionsForJob(name string, job model.Job) []Finding {
|
||||||
|
return checkVariableOptionsMap(job.Variables, name, job.File, job.Line, job.Column)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkVariableOptionsMap(vars map[string]any, jobName, file string, line, col int) []Finding {
|
||||||
|
var findings []Finding
|
||||||
|
for varName, v := range vars {
|
||||||
|
m, ok := v.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rawOpts, hasOpts := m["options"]
|
||||||
|
rawVal, hasVal := m["value"]
|
||||||
|
if !hasOpts || !hasVal || rawVal == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opts, ok := rawOpts.([]any)
|
||||||
|
if !ok || len(opts) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := fmt.Sprint(rawVal)
|
||||||
|
inOptions := false
|
||||||
|
for _, opt := range opts {
|
||||||
|
if fmt.Sprint(opt) == val {
|
||||||
|
inOptions = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inOptions {
|
||||||
|
findings = append(findings, Finding{
|
||||||
|
Severity: Error,
|
||||||
|
Rule: RuleVariableValueNotInOptions,
|
||||||
|
Job: jobName,
|
||||||
|
File: file,
|
||||||
|
Line: line,
|
||||||
|
Column: col,
|
||||||
|
Message: fmt.Sprintf("variable %q: default value %q is not listed in 'options'", varName, val),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findings
|
||||||
|
}
|
||||||
|
|
||||||
// scriptNonEmpty reports whether a script/before_script/after_script field
|
// scriptNonEmpty reports whether a script/before_script/after_script field
|
||||||
// (which may be a []any list or a plain string) is non-empty.
|
// (which may be a []any list or a plain string) is non-empty.
|
||||||
func scriptNonEmpty(v any) bool {
|
func scriptNonEmpty(v any) bool {
|
||||||
|
|||||||
@@ -163,4 +163,19 @@ const (
|
|||||||
// CI templates fetched over HTTP are transmitted in cleartext and can be
|
// CI templates fetched over HTTP are transmitted in cleartext and can be
|
||||||
// intercepted or modified in transit.
|
// intercepted or modified in transit.
|
||||||
RuleInsecureRemoteInclude = "GL045"
|
RuleInsecureRemoteInclude = "GL045"
|
||||||
|
|
||||||
|
// GL046: image: or services[n]: pull_policy: contains an unrecognised value.
|
||||||
|
// Valid values: always, if-not-present, never (or a list of those values).
|
||||||
|
RuleInvalidPullPolicy = "GL046"
|
||||||
|
|
||||||
|
// GL047: a variable declared with options: has a default value: that is not
|
||||||
|
// listed in the options list. GitLab rejects the pipeline at creation time.
|
||||||
|
RuleVariableValueNotInOptions = "GL047"
|
||||||
|
|
||||||
|
// GL048: trigger.forward: contains an unrecognised key. Only
|
||||||
|
// pipeline_variables and yaml_variables are valid.
|
||||||
|
RuleInvalidTriggerForward = "GL048"
|
||||||
|
|
||||||
|
// GL049: rules[n].allow_failure: is not a boolean or a map with exit_codes:.
|
||||||
|
RuleInvalidRulesAllowFailure = "GL049"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func checkVariableRefs(p *model.Pipeline) []Finding {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, varName := range extractIfVars(rule.If) {
|
for _, varName := range extractIfVars(rule.If) {
|
||||||
if isPredefinedVar(varName) || pipelineVars[varName] || seen[varName] {
|
if isPredefinedVar(varName) || pipelineVars[varName] || workflowRuleVars[varName] || seen[varName] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[varName] = true
|
seen[varName] = true
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "gopkg.in/yaml.v3"
|
||||||
|
|
||||||
// Pipeline represents the top-level structure of a .gitlab-ci.yml file.
|
// Pipeline represents the top-level structure of a .gitlab-ci.yml file.
|
||||||
// Unknown top-level keys are collected into Jobs.
|
// Unknown top-level keys are collected into Jobs.
|
||||||
type Pipeline struct {
|
type Pipeline struct {
|
||||||
@@ -41,7 +43,9 @@ type DefaultConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Workflow struct {
|
type Workflow struct {
|
||||||
Rules []Rule `yaml:"rules"`
|
Name string `yaml:"name"`
|
||||||
|
AutoCancel any `yaml:"auto_cancel"`
|
||||||
|
Rules Rules `yaml:"rules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Job struct {
|
type Job struct {
|
||||||
@@ -57,7 +61,7 @@ type Job struct {
|
|||||||
Image any `yaml:"image"`
|
Image any `yaml:"image"`
|
||||||
Services []any `yaml:"services"`
|
Services []any `yaml:"services"`
|
||||||
Variables map[string]any `yaml:"variables"` // string or {value,description,options} map
|
Variables map[string]any `yaml:"variables"` // string or {value,description,options} map
|
||||||
Rules []Rule `yaml:"rules"`
|
Rules Rules `yaml:"rules"`
|
||||||
Only any `yaml:"only"`
|
Only any `yaml:"only"`
|
||||||
Except any `yaml:"except"`
|
Except any `yaml:"except"`
|
||||||
Needs []any `yaml:"needs"`
|
Needs []any `yaml:"needs"`
|
||||||
@@ -85,12 +89,71 @@ type Job struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
If string `yaml:"if"`
|
If string `yaml:"if"`
|
||||||
When string `yaml:"when"`
|
When string `yaml:"when"`
|
||||||
Changes any `yaml:"changes"` // []string or {paths,compare_to} map
|
Changes any `yaml:"changes"` // []string or {paths,compare_to} map
|
||||||
Exists any `yaml:"exists"` // []string or map form
|
Exists any `yaml:"exists"` // []string or map form
|
||||||
Variables map[string]any `yaml:"variables"` // set/override variables when rule matches (GitLab CI 15.0+)
|
Variables map[string]any `yaml:"variables"` // set/override variables when rule matches (GitLab CI 15.0+)
|
||||||
Needs []any `yaml:"needs"` // override needs: when this rule matches (GitLab CI 16.4+)
|
Needs []any `yaml:"needs"` // override needs: when this rule matches (GitLab CI 16.4+)
|
||||||
|
AllowFailure any `yaml:"allow_failure"` // bool or {exit_codes:} map (GitLab CI 15.0+)
|
||||||
|
GroupID int `yaml:"-"` // >0 means AND-group; set by Rules.UnmarshalYAML
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rules is a list of Rule entries that also supports GitLab CI's nested-array
|
||||||
|
// form where each outer entry can itself be a sequence of rules (AND-group).
|
||||||
|
// Rules with the same non-zero GroupID form an AND-group: all conditions must
|
||||||
|
// match for the group to fire. GroupID == 0 means standalone rule.
|
||||||
|
type Rules []Rule
|
||||||
|
|
||||||
|
func (r *Rules) UnmarshalYAML(value *yaml.Node) error {
|
||||||
|
if value.Kind != yaml.SequenceNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nextGroup := 1
|
||||||
|
for _, item := range value.Content {
|
||||||
|
switch item.Kind {
|
||||||
|
case yaml.MappingNode:
|
||||||
|
var rule Rule // GroupID stays 0 (standalone)
|
||||||
|
if err := item.Decode(&rule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*r = append(*r, rule)
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
// AND-group: assign a shared GroupID so the evaluator can apply AND logic.
|
||||||
|
var group []Rule
|
||||||
|
if err := item.Decode(&group); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range group {
|
||||||
|
group[i].GroupID = nextGroup
|
||||||
|
}
|
||||||
|
*r = append(*r, group...)
|
||||||
|
nextGroup++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups returns the rules partitioned into AND-groups. Standalone rules
|
||||||
|
// (GroupID == 0) form single-element groups; rules with a shared GroupID form
|
||||||
|
// one group where all conditions must match.
|
||||||
|
func (r Rules) Groups() [][]Rule {
|
||||||
|
var groups [][]Rule
|
||||||
|
seen := map[int]int{} // groupID → index in groups
|
||||||
|
for _, rule := range r {
|
||||||
|
if rule.GroupID == 0 {
|
||||||
|
groups = append(groups, []Rule{rule})
|
||||||
|
} else {
|
||||||
|
idx, ok := seen[rule.GroupID]
|
||||||
|
if !ok {
|
||||||
|
idx = len(groups)
|
||||||
|
seen[rule.GroupID] = idx
|
||||||
|
groups = append(groups, nil)
|
||||||
|
}
|
||||||
|
groups[idx] = append(groups[idx], rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReservedKeys are top-level GitLab CI keys that are NOT job definitions.
|
// ReservedKeys are top-level GitLab CI keys that are NOT job definitions.
|
||||||
|
|||||||
Vendored
+54
@@ -2,6 +2,13 @@ stages:
|
|||||||
- build
|
- build
|
||||||
- test
|
- test
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DEPLOY_ENV:
|
||||||
|
value: canary # ERROR: not listed in options
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
|
||||||
workflow:
|
workflow:
|
||||||
rules:
|
rules:
|
||||||
- if: '$CI_PIPELINE_SOURCE == "push"'
|
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||||
@@ -126,3 +133,50 @@ bad-rule-when-job:
|
|||||||
rules:
|
rules:
|
||||||
- if: '$CI_MERGE_REQUEST_ID'
|
- if: '$CI_MERGE_REQUEST_ID'
|
||||||
when: sometimes # ERROR: invalid rules[0].when
|
when: sometimes # ERROR: invalid rules[0].when
|
||||||
|
|
||||||
|
bad-pull-policy-job:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- echo hello
|
||||||
|
image:
|
||||||
|
name: alpine
|
||||||
|
pull_policy: on-demand # ERROR: invalid pull_policy value
|
||||||
|
|
||||||
|
bad-pull-policy-service-job:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- echo hello
|
||||||
|
services:
|
||||||
|
- name: postgres:15
|
||||||
|
pull_policy: lazy # ERROR: invalid service pull_policy value
|
||||||
|
|
||||||
|
bad-trigger-forward-job:
|
||||||
|
trigger:
|
||||||
|
project: mygroup/myproject
|
||||||
|
forward:
|
||||||
|
all_variables: true # ERROR: unrecognised trigger.forward key
|
||||||
|
|
||||||
|
bad-trigger-forward-key2-job:
|
||||||
|
trigger:
|
||||||
|
include:
|
||||||
|
- artifact: child.yml
|
||||||
|
job: build
|
||||||
|
forward:
|
||||||
|
inherit: false # ERROR: unrecognised trigger.forward key
|
||||||
|
|
||||||
|
bad-rules-allow-failure-job:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- echo hello
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
allow_failure: maybe # ERROR: invalid rules[0].allow_failure value
|
||||||
|
|
||||||
|
bad-rules-allow-failure-map-job:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- echo hello
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
allow_failure:
|
||||||
|
codes: [1] # ERROR: map missing exit_codes key
|
||||||
|
|||||||
Vendored
+25
@@ -15,12 +15,19 @@ default:
|
|||||||
|
|
||||||
variables:
|
variables:
|
||||||
GO_VERSION: "1.26"
|
GO_VERSION: "1.26"
|
||||||
|
DEPLOY_ENV:
|
||||||
|
value: staging
|
||||||
|
options:
|
||||||
|
- staging
|
||||||
|
- production
|
||||||
|
- review
|
||||||
|
|
||||||
build-job:
|
build-job:
|
||||||
stage: build
|
stage: build
|
||||||
image:
|
image:
|
||||||
name: golang:1.26
|
name: golang:1.26
|
||||||
entrypoint: [""]
|
entrypoint: [""]
|
||||||
|
pull_policy: if-not-present
|
||||||
script:
|
script:
|
||||||
- go build ./...
|
- go build ./...
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -72,6 +79,24 @@ release-job:
|
|||||||
action: start
|
action: start
|
||||||
when: on_success
|
when: on_success
|
||||||
|
|
||||||
|
.trigger-valid:
|
||||||
|
trigger:
|
||||||
|
include:
|
||||||
|
- artifact: child.yml
|
||||||
|
job: build
|
||||||
|
forward:
|
||||||
|
pipeline_variables: true
|
||||||
|
yaml_variables: false
|
||||||
|
|
||||||
|
.rules-allow-failure-valid:
|
||||||
|
script: echo ok
|
||||||
|
rules:
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
allow_failure: true
|
||||||
|
- if: $CI_COMMIT_TAG
|
||||||
|
allow_failure:
|
||||||
|
exit_codes: [1, 2]
|
||||||
|
|
||||||
.template:
|
.template:
|
||||||
before_script:
|
before_script:
|
||||||
- echo "before"
|
- echo "before"
|
||||||
|
|||||||
Vendored
+31
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
# Tests that workflow.rules and job.rules accept the nested-array (AND-group)
|
||||||
|
# form where each outer entry can itself be a sequence of rule conditions.
|
||||||
|
workflow:
|
||||||
|
name: "${PIPELINE_NAME}"
|
||||||
|
auto_cancel:
|
||||||
|
on_new_commit: interruptible
|
||||||
|
rules:
|
||||||
|
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
|
||||||
|
variables:
|
||||||
|
PIPELINE_NAME: "main"
|
||||||
|
- - if: "$CI_PIPELINE_SOURCE == 'push' && $CI_COMMIT_BRANCH =~ /^feat\\///"
|
||||||
|
variables:
|
||||||
|
PIPELINE_NAME: "feature"
|
||||||
|
- - if: "$CI_COMMIT_TAG"
|
||||||
|
- if: "$DEPLOY"
|
||||||
|
variables:
|
||||||
|
PIPELINE_NAME: "deploy"
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DEPLOY: "false"
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
script: echo building
|
||||||
|
rules:
|
||||||
|
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
|
||||||
|
- - if: "$CI_PIPELINE_SOURCE == 'push'"
|
||||||
|
allow_failure: true
|
||||||
|
- if: "$DEPLOY"
|
||||||
|
when: manual
|
||||||
Vendored
+4
@@ -17,6 +17,10 @@ workflow:
|
|||||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||||
variables:
|
variables:
|
||||||
DEPLOY_TARGET: "production"
|
DEPLOY_TARGET: "production"
|
||||||
|
WORKFLOW_FLAG: "true"
|
||||||
|
# workflow rule referencing a variable set by a sibling rule's variables: — GL032 must not fire
|
||||||
|
- if: '$WORKFLOW_FLAG == "true"'
|
||||||
|
when: always
|
||||||
- when: always
|
- when: always
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user