Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46a1cf3c08 |
@@ -0,0 +1,40 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: vet, staticcheck, test, build
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: golang:1.26-alpine
|
||||
|
||||
steps:
|
||||
- name: Install tools
|
||||
run: apk add --no-cache git
|
||||
|
||||
- name: Checkout
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
REPO: ${{ github.repository }}
|
||||
REF: ${{ github.ref_name }}
|
||||
run: |
|
||||
git clone --depth 1 --branch "$REF" \
|
||||
"$(echo "$SERVER_URL" | sed "s|https://|https://oauth2:${TOKEN}@|")/${REPO}.git" .
|
||||
|
||||
- name: vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: staticcheck
|
||||
run: go tool staticcheck ./...
|
||||
|
||||
- name: test
|
||||
run: go test ./...
|
||||
|
||||
- name: build
|
||||
run: go build ./cmd/glint/...
|
||||
@@ -11,6 +11,8 @@ This project uses [Semantic Versioning](https://semver.org).
|
||||
|
||||
- **Variable reference validation (GL032)** — glint now warns when a `rules:if:` expression references a variable (`$VAR` or `${VAR}`) that is not declared anywhere in the pipeline YAML: pipeline-level `variables:`, the job's own `variables:`, or any `workflow:rules:variables:` block. Predefined GitLab CI variable namespaces (`CI_*`, `GITLAB_*`, `FF_*`, `RUNNER_*`, `TRIGGER_*`, `CHAT_*`) are exempt. Because variables can also be set in GitLab CI/CD project settings (invisible to glint), the finding is a `[WARNING]` rather than an error. Each undeclared variable is reported at most once per job to keep the output concise.
|
||||
|
||||
- **Included-file variables now visible to all lint rules** — `variables:` blocks declared in included files (local, remote, project, and component includes) are now merged into the pipeline's variable namespace before linting. This eliminates false-positive GL032 warnings for variables declared in shared CI templates. Root-pipeline variables take precedence over included-file variables when the same key appears in both (matching GitLab's own override behaviour).
|
||||
|
||||
- **Structured rule IDs** — every finding now carries a stable `GL###` identifier (e.g. `GL003`) that appears in the output between the location and the message: `[ERROR] job "deploy" (file.yml:14) GL003: missing required field 'script'`. IDs are assigned per check function across 31 rules (GL001–GL031) and are stable across versions. The `linter.Finding` struct exposes the ID as a `Rule string` field for programmatic consumers. The README lint rules table is updated with ID columns.
|
||||
|
||||
- **`workflow:rules:variables:` now propagate to job rule evaluation** — when a `workflow:rules:` entry matches, any `variables:` it defines are injected into the evaluation context so job `rules:if:` expressions can reference them. Pipeline-level `variables:` are also available as defaults (lower priority). Variable priority order, highest first: `--var` CLI overrides → `--branch`/`--tag`/`--source` shortcuts → workflow-rule variables → pipeline-level variable defaults. This means `$DEPLOY_TARGET == "production"` in a job rule correctly evaluates when a workflow rule sets `DEPLOY_TARGET: production` for the matching branch. The `glint graph tree` command benefits from the same enrichment.
|
||||
|
||||
+11
-4
@@ -75,21 +75,28 @@ tasks:
|
||||
ignore_error: false
|
||||
- cmd: ./{{.BINARY}} check testdata/variable_refs.yml
|
||||
ignore_error: false
|
||||
- cmd: ./{{.BINARY}} check samba-testdata/.gitlab-ci.yml
|
||||
- cmd: ./{{.BINARY}} check testdata/variable_refs_included.yml
|
||||
ignore_error: false
|
||||
- cmd: ./{{.BINARY}} check samba-testdata/.gitlab-ci-coverage.yml
|
||||
- cmd: ./{{.BINARY}} check testdata/samba/.gitlab-ci.yml
|
||||
ignore_error: false
|
||||
- cmd: ./{{.BINARY}} check samba-testdata/.gitlab-ci-private.yml
|
||||
- cmd: ./{{.BINARY}} check testdata/samba/.gitlab-ci-coverage.yml
|
||||
ignore_error: false
|
||||
- cmd: ./{{.BINARY}} check testdata/samba/.gitlab-ci-private.yml
|
||||
ignore_error: false
|
||||
|
||||
lint-go:
|
||||
desc: Run go vet on all packages
|
||||
cmd: "{{.GO}} vet ./..."
|
||||
|
||||
lint-static:
|
||||
desc: Run staticcheck on all packages
|
||||
cmd: "{{.GO}} tool staticcheck ./..."
|
||||
|
||||
ci:
|
||||
desc: Full CI check — vet, test, build, validate
|
||||
desc: Full CI check — vet, staticcheck, test, build, validate
|
||||
cmds:
|
||||
- task: lint-go
|
||||
- task: lint-static
|
||||
- task: test
|
||||
- task: build
|
||||
- task: validate
|
||||
|
||||
@@ -2,4 +2,14 @@ module git.k3nny.fr/glint
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/tools v0.40.1-0.20260108161641-ca281cf95054 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
honnef.co/go/tools v0.7.0 // indirect
|
||||
)
|
||||
|
||||
tool honnef.co/go/tools/cmd/staticcheck
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
|
||||
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/tools v0.40.1-0.20260108161641-ca281cf95054 h1:CHVDrNHx9ZoOrNN9kKWYIbT5Rj+WF2rlwPkhbQQ5V4U=
|
||||
golang.org/x/tools v0.40.1-0.20260108161641-ca281cf95054/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
||||
|
||||
@@ -55,9 +55,7 @@ func (b *treeBuilder) nextID() string {
|
||||
|
||||
func (b *treeBuilder) buildChildren(parent *treeNode, rawIncludes []any) {
|
||||
for _, entry := range rawIncludes {
|
||||
for _, child := range b.parseEntry(entry) {
|
||||
parent.children = append(parent.children, child)
|
||||
}
|
||||
parent.children = append(parent.children, b.parseEntry(entry)...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ func checkEnvironment(name string, job model.Job) []Finding {
|
||||
return nil
|
||||
}
|
||||
var findings []Finding
|
||||
envName, _ := m["name"]
|
||||
envName := m["name"]
|
||||
_, hasURL := m["url"]
|
||||
if (envName == nil || envName == "") && hasURL {
|
||||
findings = append(findings, Finding{
|
||||
@@ -391,7 +391,7 @@ func checkArtifacts(name string, job model.Job) []Finding {
|
||||
})
|
||||
}
|
||||
if _, hasExposeAs := m["expose_as"]; hasExposeAs {
|
||||
paths, _ := m["paths"]
|
||||
paths := m["paths"]
|
||||
if paths == nil {
|
||||
findings = append(findings, Finding{
|
||||
Severity: Error,
|
||||
@@ -484,7 +484,7 @@ func checkImage(name string, job model.Job) []Finding {
|
||||
if !ok {
|
||||
return nil // String form is valid.
|
||||
}
|
||||
imgName, _ := m["name"]
|
||||
imgName := m["name"]
|
||||
if imgName == nil || imgName == "" {
|
||||
return []Finding{{
|
||||
Severity: Error,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// pipeline that is valid on GitLab) produces no Error findings.
|
||||
// These files exercise local include resolution and multi-level extends chains.
|
||||
func TestSambaCI(t *testing.T) {
|
||||
entryPoint := "../../samba-testdata/.gitlab-ci.yml"
|
||||
entryPoint := "../../testdata/samba/.gitlab-ci.yml"
|
||||
|
||||
p, err := model.Parse(entryPoint)
|
||||
if err != nil {
|
||||
@@ -51,9 +51,9 @@ func TestSambaCIEntryFiles(t *testing.T) {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{"default", "../../samba-testdata/.gitlab-ci.yml"},
|
||||
{"coverage", "../../samba-testdata/.gitlab-ci-coverage.yml"},
|
||||
{"private", "../../samba-testdata/.gitlab-ci-private.yml"},
|
||||
{"default", "../../testdata/samba/.gitlab-ci.yml"},
|
||||
{"coverage", "../../testdata/samba/.gitlab-ci-coverage.yml"},
|
||||
{"private", "../../testdata/samba/.gitlab-ci-private.yml"},
|
||||
}
|
||||
|
||||
for _, tc := range entryPoints {
|
||||
|
||||
@@ -326,8 +326,9 @@ func includeFiles(entry map[string]any) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeIncluded copies jobs and stages from src into dst.
|
||||
// dst (the main pipeline) always wins when a key already exists.
|
||||
// mergeIncluded copies jobs, stages, and variables from src into dst.
|
||||
// dst (the main pipeline) always wins when a key already exists — this matches
|
||||
// GitLab's precedence rule where root-pipeline values override included templates.
|
||||
func mergeIncluded(dst, src *model.Pipeline) {
|
||||
stageSet := make(map[string]bool, len(dst.Stages))
|
||||
for _, s := range dst.Stages {
|
||||
@@ -348,4 +349,16 @@ func mergeIncluded(dst, src *model.Pipeline) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge pipeline-level variables: dst wins on conflict (root overrides includes).
|
||||
if len(src.Variables) > 0 {
|
||||
if dst.Variables == nil {
|
||||
dst.Variables = make(map[string]any, len(src.Variables))
|
||||
}
|
||||
for k, v := range src.Variables {
|
||||
if _, exists := dst.Variables[k]; !exists {
|
||||
dst.Variables[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
Vendored
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# variable_refs_included.yml
|
||||
# GL032 must NOT fire for variables declared in an included file.
|
||||
# The included file (variable_refs_included_template.yml) declares TEMPLATE_VAR.
|
||||
# Expected: exits 0 (no errors; no GL032 warnings).
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
include:
|
||||
- local: /variable_refs_included_template.yml
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script: make build
|
||||
rules:
|
||||
# TEMPLATE_VAR comes from the included file — GL032 must not fire.
|
||||
- if: '$TEMPLATE_VAR == "enabled"'
|
||||
when: on_success
|
||||
# ROOT_VAR is declared in this file — GL032 must not fire.
|
||||
- if: '$ROOT_VAR == "yes"'
|
||||
when: manual
|
||||
|
||||
variables:
|
||||
ROOT_VAR: "yes"
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# Included by variable_refs_included.yml — declares a pipeline-level variable
|
||||
# that the parent pipeline's jobs reference in rules:if: expressions.
|
||||
|
||||
variables:
|
||||
TEMPLATE_VAR: "enabled"
|
||||
Reference in New Issue
Block a user