feat: add go lint
This commit is contained in:
@@ -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.
|
- **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.
|
- **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.
|
- **`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
|
ignore_error: false
|
||||||
- cmd: ./{{.BINARY}} check testdata/variable_refs.yml
|
- cmd: ./{{.BINARY}} check testdata/variable_refs.yml
|
||||||
ignore_error: false
|
ignore_error: false
|
||||||
- cmd: ./{{.BINARY}} check samba-testdata/.gitlab-ci.yml
|
- cmd: ./{{.BINARY}} check testdata/variable_refs_included.yml
|
||||||
ignore_error: false
|
ignore_error: false
|
||||||
- cmd: ./{{.BINARY}} check samba-testdata/.gitlab-ci-coverage.yml
|
- cmd: ./{{.BINARY}} check testdata/samba/.gitlab-ci.yml
|
||||||
ignore_error: false
|
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
|
ignore_error: false
|
||||||
|
|
||||||
lint-go:
|
lint-go:
|
||||||
desc: Run go vet on all packages
|
desc: Run go vet on all packages
|
||||||
cmd: "{{.GO}} vet ./..."
|
cmd: "{{.GO}} vet ./..."
|
||||||
|
|
||||||
|
lint-static:
|
||||||
|
desc: Run staticcheck on all packages
|
||||||
|
cmd: "{{.GO}} tool staticcheck ./..."
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
desc: Full CI check — vet, test, build, validate
|
desc: Full CI check — vet, staticcheck, test, build, validate
|
||||||
cmds:
|
cmds:
|
||||||
- task: lint-go
|
- task: lint-go
|
||||||
|
- task: lint-static
|
||||||
- task: test
|
- task: test
|
||||||
- task: build
|
- task: build
|
||||||
- task: validate
|
- task: validate
|
||||||
|
|||||||
@@ -2,4 +2,14 @@ module git.k3nny.fr/glint
|
|||||||
|
|
||||||
go 1.26.4
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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) {
|
func (b *treeBuilder) buildChildren(parent *treeNode, rawIncludes []any) {
|
||||||
for _, entry := range rawIncludes {
|
for _, entry := range rawIncludes {
|
||||||
for _, child := range b.parseEntry(entry) {
|
parent.children = append(parent.children, b.parseEntry(entry)...)
|
||||||
parent.children = append(parent.children, child)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ func checkEnvironment(name string, job model.Job) []Finding {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var findings []Finding
|
var findings []Finding
|
||||||
envName, _ := m["name"]
|
envName := m["name"]
|
||||||
_, hasURL := m["url"]
|
_, hasURL := m["url"]
|
||||||
if (envName == nil || envName == "") && hasURL {
|
if (envName == nil || envName == "") && hasURL {
|
||||||
findings = append(findings, Finding{
|
findings = append(findings, Finding{
|
||||||
@@ -391,7 +391,7 @@ func checkArtifacts(name string, job model.Job) []Finding {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if _, hasExposeAs := m["expose_as"]; hasExposeAs {
|
if _, hasExposeAs := m["expose_as"]; hasExposeAs {
|
||||||
paths, _ := m["paths"]
|
paths := m["paths"]
|
||||||
if paths == nil {
|
if paths == nil {
|
||||||
findings = append(findings, Finding{
|
findings = append(findings, Finding{
|
||||||
Severity: Error,
|
Severity: Error,
|
||||||
@@ -484,7 +484,7 @@ func checkImage(name string, job model.Job) []Finding {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil // String form is valid.
|
return nil // String form is valid.
|
||||||
}
|
}
|
||||||
imgName, _ := m["name"]
|
imgName := m["name"]
|
||||||
if imgName == nil || imgName == "" {
|
if imgName == nil || imgName == "" {
|
||||||
return []Finding{{
|
return []Finding{{
|
||||||
Severity: Error,
|
Severity: Error,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// pipeline that is valid on GitLab) produces no Error findings.
|
// pipeline that is valid on GitLab) produces no Error findings.
|
||||||
// These files exercise local include resolution and multi-level extends chains.
|
// These files exercise local include resolution and multi-level extends chains.
|
||||||
func TestSambaCI(t *testing.T) {
|
func TestSambaCI(t *testing.T) {
|
||||||
entryPoint := "../../samba-testdata/.gitlab-ci.yml"
|
entryPoint := "../../testdata/samba/.gitlab-ci.yml"
|
||||||
|
|
||||||
p, err := model.Parse(entryPoint)
|
p, err := model.Parse(entryPoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,9 +51,9 @@ func TestSambaCIEntryFiles(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
path string
|
path string
|
||||||
}{
|
}{
|
||||||
{"default", "../../samba-testdata/.gitlab-ci.yml"},
|
{"default", "../../testdata/samba/.gitlab-ci.yml"},
|
||||||
{"coverage", "../../samba-testdata/.gitlab-ci-coverage.yml"},
|
{"coverage", "../../testdata/samba/.gitlab-ci-coverage.yml"},
|
||||||
{"private", "../../samba-testdata/.gitlab-ci-private.yml"},
|
{"private", "../../testdata/samba/.gitlab-ci-private.yml"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range entryPoints {
|
for _, tc := range entryPoints {
|
||||||
|
|||||||
@@ -326,8 +326,9 @@ func includeFiles(entry map[string]any) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeIncluded copies jobs and stages from src into dst.
|
// mergeIncluded copies jobs, stages, and variables from src into dst.
|
||||||
// dst (the main pipeline) always wins when a key already exists.
|
// 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) {
|
func mergeIncluded(dst, src *model.Pipeline) {
|
||||||
stageSet := make(map[string]bool, len(dst.Stages))
|
stageSet := make(map[string]bool, len(dst.Stages))
|
||||||
for _, s := range 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