Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 192ab3198b | |||
| f197c368d3 | |||
| d6afb148ca | |||
| 522c637b75 | |||
| 9342ce0eff |
@@ -0,0 +1,66 @@
|
|||||||
|
# .glint.yml — glint project configuration
|
||||||
|
#
|
||||||
|
# Place this file anywhere between your .gitlab-ci.yml and the repository root.
|
||||||
|
# glint searches upward from the pipeline file and stops at the first .git
|
||||||
|
# boundary, so the repo root is the typical location.
|
||||||
|
#
|
||||||
|
# All keys are optional. Omit or comment out anything you don't need.
|
||||||
|
|
||||||
|
# ── Rule suppression ──────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Suppress rules globally for this project. Suppressed rules produce no output
|
||||||
|
# and do not affect the exit code.
|
||||||
|
#
|
||||||
|
ignore:
|
||||||
|
- GL007 # only:/except: used (migrating from legacy syntax)
|
||||||
|
- GL032 # rules:if: references undeclared variable (injected at runtime)
|
||||||
|
|
||||||
|
# ── Severity overrides ────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Override the default severity of any rule.
|
||||||
|
# Valid values: error | warning | ignore
|
||||||
|
# "ignore" is equivalent to listing the rule in `ignore:` above.
|
||||||
|
#
|
||||||
|
severity:
|
||||||
|
GL004: warning # demote "unknown stage" to warning during a stage migration
|
||||||
|
GL035: error # promote absolute-path warning to a hard error
|
||||||
|
GL007: ignore # equivalent to adding GL007 to ignore:
|
||||||
|
|
||||||
|
# ── Extra stage names ─────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Declare stage names that are valid for this project beyond what is listed in
|
||||||
|
# the pipeline's own `stages:` block. Jobs referencing these stages will not
|
||||||
|
# be flagged by GL004. Useful when stages are defined in a shared parent
|
||||||
|
# template that glint cannot reach.
|
||||||
|
#
|
||||||
|
stages:
|
||||||
|
- quality
|
||||||
|
- security
|
||||||
|
- compliance
|
||||||
|
|
||||||
|
# ── GitLab token ──────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Default personal access token (read_api scope) used to fetch `include:
|
||||||
|
# project:` templates. This is the lowest-priority token source; it is
|
||||||
|
# overridden by the --token flag and the GITLAB_TOKEN / CI_JOB_TOKEN /
|
||||||
|
# GITLAB_PRIVATE_TOKEN environment variables.
|
||||||
|
#
|
||||||
|
# Avoid committing real tokens — use environment variables instead.
|
||||||
|
#
|
||||||
|
# token: glpat-xxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
||||||
|
# ── GitLab instance URL ───────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Default GitLab instance URL, used when fetching project: and component:
|
||||||
|
# includes. Overridden by --gitlab-url, CI_SERVER_URL, and GITLAB_URL.
|
||||||
|
#
|
||||||
|
# url: https://gitlab.example.com
|
||||||
|
|
||||||
|
# ── Include cache directory ───────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Default directory for caching fetched remote includes (project: and
|
||||||
|
# component:). The directory is created on first use. Overridden by
|
||||||
|
# --cache-dir. When --offline is given without --cache-dir, glint defaults
|
||||||
|
# to ~/.cache/glint regardless of this setting.
|
||||||
|
#
|
||||||
|
# cache_dir: ~/.cache/glint
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
- id: glint
|
||||||
|
name: glint — validate GitLab CI pipeline
|
||||||
|
description: >-
|
||||||
|
Lint .gitlab-ci.yml with glint before committing. Catches misconfigured
|
||||||
|
stages, invalid keywords, broken needs: graphs, deprecated patterns, and
|
||||||
|
more — without a GitLab server.
|
||||||
|
entry: glint check
|
||||||
|
language: golang
|
||||||
|
files: '(^|/)\.gitlab-ci\.yml$'
|
||||||
|
pass_filenames: true
|
||||||
|
minimum_pre_commit_version: '3.0.0'
|
||||||
@@ -5,6 +5,24 @@ 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.2.28] - 2026-06-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Pre-commit hook** — `.pre-commit-hooks.yaml` defines a `glint` hook with `language: golang`; pre-commit builds glint from source automatically on first run and re-runs `glint check` on any staged `.gitlab-ci.yml` changes. Reference: `repo: https://git.k3nny.fr/k3nny/glint, rev: v0.2.28`.
|
||||||
|
|
||||||
|
- **GitLab CI component** (`templates/check.yml`) — a GitLab CI/CD Catalog–compatible component that downloads the glint Linux binary and runs `glint check` as a pipeline job. Accepts inputs: `stage` (default `validate`), `pipeline_file` (default `.gitlab-ci.yml`), `version` (default `latest`), `allow_failure` (default `false`), and `extra_args`. Can also be used as a plain local or remote include without the Catalog.
|
||||||
|
|
||||||
|
- **GitHub Actions composite action** (`action.yml`) — downloads the glint Linux binary into `$RUNNER_TEMP`, adds it to `$GITHUB_PATH`, and runs `glint check`. Inputs: `version`, `file`, `args`. Mirror this repository to GitHub as `k3nny/glint` to reference it as `uses: k3nny/glint@v0.2.28`.
|
||||||
|
|
||||||
|
## [0.2.27] - 2026-06-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Fuzz testing** — `FuzzParseBytes` and `FuzzSanitizeYAMLEscapes` in `internal/model/fuzz_test.go` verify that neither the YAML parser nor the escape sanitizer panics on arbitrary input. Successful parses are also checked for structural integrity (non-nil pipeline, no empty job names). Run with `task fuzz` (default 30 s per target; set `FUZZ_TIME=60s` to extend). Found failures are saved to `testdata/fuzz/` for regression.
|
||||||
|
|
||||||
|
- **Changelog automation** — `cliff.toml` configures [git-cliff](https://git-cliff.org) to generate Keep-a-Changelog–compatible release notes from Conventional Commits. `task changelog` regenerates `CHANGELOG.md` from the full git history; `task changelog-next` previews only unreleased commits without writing. Install git-cliff with `brew install git-cliff` or `cargo install git-cliff`.
|
||||||
|
|
||||||
## [0.2.26] - 2026-06-25
|
## [0.2.26] - 2026-06-25
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -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.2.26-blue.svg" alt="Release"></a>
|
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/release-v0.2.28-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.
|
||||||
@@ -32,7 +32,7 @@ See [FEATURES.md](FEATURES.md) for the complete feature reference and lint rules
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.k3nny.fr/glint
|
git clone https://git.k3nny.fr/k3nny/glint
|
||||||
cd glint
|
cd glint
|
||||||
go build -o glint ./cmd/glint/...
|
go build -o glint ./cmd/glint/...
|
||||||
```
|
```
|
||||||
@@ -58,6 +58,54 @@ Run `glint <command> --help` for all flags. See [USAGE.md](USAGE.md) for full
|
|||||||
examples covering output formats, context simulation, remote includes, cache,
|
examples covering output formats, context simulation, remote includes, cache,
|
||||||
graph modes, and project configuration.
|
graph modes, and project configuration.
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
### Pre-commit hook
|
||||||
|
|
||||||
|
Add to `.pre-commit-config.yaml` in your repository to run glint automatically whenever `.gitlab-ci.yml` changes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
repos:
|
||||||
|
- repo: https://git.k3nny.fr/k3nny/glint
|
||||||
|
rev: v0.2.28
|
||||||
|
hooks:
|
||||||
|
- id: glint
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires [pre-commit](https://pre-commit.com) and Go 1.21+. On first run, pre-commit builds glint from source automatically.
|
||||||
|
|
||||||
|
### GitLab CI component
|
||||||
|
|
||||||
|
Copy [`templates/check.yml`](templates/check.yml) into your repository and include it as a local file, or publish this repository to a GitLab CI/CD Catalog and reference it as a component:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# As a local include (copy templates/check.yml to your repo first):
|
||||||
|
include:
|
||||||
|
- local: .gitlab/glint-check.yml
|
||||||
|
|
||||||
|
# As a Catalog component (after publishing to a GitLab instance):
|
||||||
|
include:
|
||||||
|
- component: $CI_SERVER_FQDN/k3nny/glint/check@v0.2.28
|
||||||
|
inputs:
|
||||||
|
stage: validate # optional, default: validate
|
||||||
|
allow_failure: true # optional, default: false
|
||||||
|
```
|
||||||
|
|
||||||
|
The component downloads the glint Linux binary, runs `glint check`, and respects all inputs defined in the `spec:` block.
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
Copy [`action.yml`](action.yml) from this repository, or mirror this repo to GitHub as `k3nny/glint` and reference it directly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: k3nny/glint@v0.2.28
|
||||||
|
with:
|
||||||
|
file: .gitlab-ci.yml # optional, default: .gitlab-ci.yml
|
||||||
|
args: '--format sarif' # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
The action downloads the glint Linux binary into `$RUNNER_TEMP` and runs `glint check`. Only Linux runners are supported (matches the available release binary).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
This project uses [Task](https://taskfile.dev) as a task runner.
|
This project uses [Task](https://taskfile.dev) as a task runner.
|
||||||
@@ -69,11 +117,18 @@ task test # run Go unit tests
|
|||||||
task lint-go # run go vet
|
task lint-go # run go vet
|
||||||
task validate # run the binary against all testdata fixtures
|
task validate # run the binary against all testdata fixtures
|
||||||
task ci # full check: vet → test → build → validate
|
task ci # full check: vet → test → build → validate
|
||||||
|
task fuzz # run fuzz tests for the YAML parser (Ctrl-C to stop; FUZZ_TIME=60s to set duration)
|
||||||
|
task changelog # regenerate CHANGELOG.md from git history via git-cliff
|
||||||
|
task changelog-next # preview unreleased section (dry-run, no file written)
|
||||||
task build-windows # cross-compile for Windows x64 (requires a tagged commit → glint-<tag>.exe)
|
task build-windows # cross-compile for Windows x64 (requires a tagged commit → glint-<tag>.exe)
|
||||||
task build-linux # cross-compile for Linux x64 (requires a tagged commit → glint-<tag>-linux-amd64)
|
task build-linux # cross-compile for Linux x64 (requires a tagged commit → glint-<tag>-linux-amd64)
|
||||||
task clean # remove build artifacts
|
task clean # remove build artifacts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Optional tools:**
|
||||||
|
|
||||||
|
- [git-cliff](https://git-cliff.org) — changelog generator used by `task changelog`. Install with `brew install git-cliff` or `cargo install git-cliff`.
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+5
-5
@@ -98,9 +98,9 @@ The SVG renderer and terminal tree cover the basic layout. These would bring it
|
|||||||
|
|
||||||
## CI / editor integration
|
## CI / editor integration
|
||||||
|
|
||||||
- **GitLab CI template** — a `.gitlab-ci.yml` snippet that runs `glint` as a pipeline-validation job before the real pipeline executes; publishable to the GitLab CI/CD Catalog
|
- ~~**GitLab CI template**~~ — ✓ shipped v0.2.28; `templates/check.yml` is a GitLab CI/CD Catalog component with `spec:` inputs for stage, file, version, allow_failure, and extra args; also usable as a plain local/remote include
|
||||||
- **GitHub Actions action** — `uses: k3nny/glint@v1` wrapper for repositories that mirror or manage GitLab pipelines from GitHub
|
- ~~**GitHub Actions action**~~ — ✓ shipped v0.2.28; `action.yml` composite action downloads the glint Linux binary and runs `glint check`; mirror to GitHub as `k3nny/glint` to reference as `uses: k3nny/glint@v0.2.28`
|
||||||
- **Pre-commit hook** — entry for [pre-commit](https://pre-commit.com) so `glint` runs automatically on `git commit` when `.gitlab-ci.yml` changes
|
- ~~**Pre-commit hook**~~ — ✓ shipped v0.2.28; `.pre-commit-hooks.yaml` defines `language: golang` hook; pre-commit builds glint from source on first run and re-runs on staged `.gitlab-ci.yml` changes
|
||||||
- **LSP server** — `glint lsp` mode exposing diagnostics over the Language Server Protocol; enables inline squiggles in VS Code, JetBrains, Neovim, etc. without a dedicated extension
|
- **LSP server** — `glint lsp` mode exposing diagnostics over the Language Server Protocol; enables inline squiggles in VS Code, JetBrains, Neovim, etc. without a dedicated extension
|
||||||
- **VS Code extension** — thin wrapper around the LSP server with syntax highlighting for `.gitlab-ci.yml`
|
- **VS Code extension** — thin wrapper around the LSP server with syntax highlighting for `.gitlab-ci.yml`
|
||||||
|
|
||||||
@@ -119,5 +119,5 @@ The SVG renderer and terminal tree cover the basic layout. These would bring it
|
|||||||
- ~~**`glint explain <rule-id>`**~~ — ✓ shipped v0.2.20; prints rule description, rationale, bad-YAML example, and fix; `glint explain` (no arg) lists all rules
|
- ~~**`glint explain <rule-id>`**~~ — ✓ shipped v0.2.20; prints rule description, rationale, bad-YAML example, and fix; `glint explain` (no arg) lists all rules
|
||||||
- ~~**Semantic versioning and first release**~~ — shipped as `v0.1.0` (2026-06-07)
|
- ~~**Semantic versioning and first release**~~ — shipped as `v0.1.0` (2026-06-07)
|
||||||
- ~~**Subcommand CLI**~~ — shipped as `v0.2.0` (2026-06-11); `glint check` / `glint graph [mode]` with ruff-style `--help`
|
- ~~**Subcommand CLI**~~ — shipped as `v0.2.0` (2026-06-11); `glint check` / `glint graph [mode]` with ruff-style `--help`
|
||||||
- **Changelog automation** — generate release notes from Conventional Commits via `git-cliff` or similar
|
- ~~**Changelog automation**~~ — ✓ shipped v0.2.27; `cliff.toml` configures git-cliff to produce Keep-a-Changelog–compatible release notes from Conventional Commits; `task changelog` regenerates `CHANGELOG.md`, `task changelog-next` previews unreleased entries
|
||||||
- **Fuzz testing** — add a `go test -fuzz` target for the YAML parser to harden it against malformed input
|
- ~~**Fuzz testing**~~ — ✓ shipped v0.2.27; `FuzzParseBytes` and `FuzzSanitizeYAMLEscapes` in `internal/model/fuzz_test.go`; seeds run as regular tests in CI; `task fuzz` runs them continuously (default 30 s)
|
||||||
|
|||||||
@@ -175,6 +175,23 @@ tasks:
|
|||||||
generates:
|
generates:
|
||||||
- "{{.BINARY}}-{{.TAG}}-linux-amd64"
|
- "{{.BINARY}}-{{.TAG}}-linux-amd64"
|
||||||
|
|
||||||
|
fuzz:
|
||||||
|
desc: "Run all fuzz targets (set FUZZ_TIME=60s to control per-target duration, default 30s)"
|
||||||
|
cmds:
|
||||||
|
- "{{.GO}} test -fuzz=FuzzParseBytes -fuzztime=${FUZZ_TIME:-30s} ./internal/model/"
|
||||||
|
- "{{.GO}} test -fuzz=FuzzSanitizeYAMLEscapes -fuzztime=${FUZZ_TIME:-30s} ./internal/model/"
|
||||||
|
- "{{.GO}} test -fuzz=FuzzEvalIf -fuzztime=${FUZZ_TIME:-30s} ./internal/cicontext/"
|
||||||
|
- "{{.GO}} test -fuzz=FuzzExpandVarRefs -fuzztime=${FUZZ_TIME:-30s} ./internal/cicontext/"
|
||||||
|
- "{{.GO}} test -fuzz=FuzzLint -fuzztime=${FUZZ_TIME:-30s} ./internal/linter/"
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
desc: "Regenerate CHANGELOG.md from git history (requires git-cliff — see README)"
|
||||||
|
cmd: git cliff --config cliff.toml --output CHANGELOG.md
|
||||||
|
|
||||||
|
changelog-next:
|
||||||
|
desc: "Preview unreleased changelog entries without writing (requires git-cliff)"
|
||||||
|
cmd: git cliff --config cliff.toml --unreleased
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
# GitHub Actions composite action — glint pipeline validator
|
||||||
|
#
|
||||||
|
# To use this action, mirror this repository to GitHub as k3nny/glint, then:
|
||||||
|
#
|
||||||
|
# - uses: k3nny/glint@v0.2.28
|
||||||
|
# with:
|
||||||
|
# file: .gitlab-ci.yml # optional
|
||||||
|
#
|
||||||
|
# Alternatively, copy this file into your own repository and reference it
|
||||||
|
# as a local action:
|
||||||
|
#
|
||||||
|
# - uses: ./.github/actions/glint
|
||||||
|
|
||||||
|
name: 'glint'
|
||||||
|
description: 'Validate a GitLab CI pipeline file with glint'
|
||||||
|
author: 'k3nny'
|
||||||
|
|
||||||
|
branding:
|
||||||
|
icon: 'check-circle'
|
||||||
|
color: 'orange'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: >-
|
||||||
|
glint release tag to install (e.g. 'v0.2.28'). Defaults to 'latest'
|
||||||
|
which resolves to the newest published release.
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
file:
|
||||||
|
description: 'Path to the pipeline file to validate.'
|
||||||
|
required: false
|
||||||
|
default: '.gitlab-ci.yml'
|
||||||
|
args:
|
||||||
|
description: 'Additional arguments passed to glint check (e.g. --format sarif).'
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Install glint
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GLINT_VERSION: ${{ inputs.version }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [ "$GLINT_VERSION" = "latest" ]; then
|
||||||
|
GLINT_VERSION=$(curl -sf \
|
||||||
|
"https://git.k3nny.fr/api/v1/repos/k3nny/glint/releases?limit=1" \
|
||||||
|
| grep '"tag_name"' | head -1 | cut -d'"' -f4)
|
||||||
|
fi
|
||||||
|
DEST="$RUNNER_TEMP/glint-bin"
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
URL="https://git.k3nny.fr/k3nny/glint/releases/download/${GLINT_VERSION}/glint-${GLINT_VERSION}-linux-amd64"
|
||||||
|
curl -sfL "$URL" -o "$DEST/glint"
|
||||||
|
chmod +x "$DEST/glint"
|
||||||
|
echo "$DEST" >> "$GITHUB_PATH"
|
||||||
|
"$DEST/glint" --version
|
||||||
|
|
||||||
|
- name: Run glint check
|
||||||
|
shell: bash
|
||||||
|
run: glint check ${{ inputs.args }} "${{ inputs.file }}"
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
# git-cliff configuration for glint
|
||||||
|
# Install: brew install git-cliff OR cargo install git-cliff
|
||||||
|
# Usage: task changelog -- regenerate full CHANGELOG.md
|
||||||
|
# task changelog-next -- preview unreleased section (dry-run)
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
header = """
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
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/).
|
||||||
|
This project uses [Semantic Versioning](https://semver.org).
|
||||||
|
"""
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
|
||||||
|
{% else %}\
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}\
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
|
||||||
|
{% for commit in commits %}\
|
||||||
|
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}\
|
||||||
|
{{ commit.message | upper_first }}\
|
||||||
|
{% if commit.breaking %} **[BREAKING]**{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}\
|
||||||
|
{% endfor %}\
|
||||||
|
"""
|
||||||
|
trim = true
|
||||||
|
footer = ""
|
||||||
|
postprocessors = []
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = true
|
||||||
|
filter_unconventional = true
|
||||||
|
split_commits = false
|
||||||
|
commit_preprocessors = [
|
||||||
|
# Drop Co-Authored-By trailers (should not appear in subjects, but guard anyway).
|
||||||
|
{ pattern = "Co-Authored-By:.*", replace = "" },
|
||||||
|
]
|
||||||
|
commit_parsers = [
|
||||||
|
# Breaking changes (type! or scope!) — promote above everything else.
|
||||||
|
{ message = "^[a-z]+\\([a-z-]+\\)!:|^[a-z]+!:", group = "Breaking Changes" },
|
||||||
|
{ message = "^feat", group = "Added" },
|
||||||
|
{ message = "^fix", group = "Fixed" },
|
||||||
|
{ message = "^perf", group = "Changed" },
|
||||||
|
{ message = "^refactor", group = "Changed" },
|
||||||
|
# Maintenance commits — omit from the changelog body.
|
||||||
|
{ message = "^docs", skip = true },
|
||||||
|
{ message = "^style", skip = true },
|
||||||
|
{ message = "^test", skip = true },
|
||||||
|
{ message = "^chore", skip = true },
|
||||||
|
{ message = "^build", skip = true },
|
||||||
|
{ message = "^claude", skip = true },
|
||||||
|
]
|
||||||
|
protect_breaking_commits = false
|
||||||
|
filter_commits = true
|
||||||
|
tag_pattern = "v[0-9].*"
|
||||||
|
topo_order = false
|
||||||
|
sort_commits = "oldest"
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package cicontext
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// FuzzEvalIf ensures the rules:if: expression evaluator never panics on
|
||||||
|
// arbitrary input. It accepts two strings: the expression and a variable value
|
||||||
|
// substituted for every variable reference encountered.
|
||||||
|
// Run with: go test -fuzz=FuzzEvalIf ./internal/cicontext/
|
||||||
|
func FuzzEvalIf(f *testing.F) {
|
||||||
|
// Seed corpus: representative expressions exercising every code path in
|
||||||
|
// the hand-rolled recursive-descent parser.
|
||||||
|
seeds := []struct{ expr, varVal string }{
|
||||||
|
// Simple comparisons
|
||||||
|
{`$VAR == "main"`, "main"},
|
||||||
|
{`$VAR != "main"`, "main"},
|
||||||
|
{`$VAR == null`, ""},
|
||||||
|
{`$VAR != null`, "x"},
|
||||||
|
// Regex operators
|
||||||
|
{`$VAR =~ /^v\d+\.\d+/`, "v1.2.3"},
|
||||||
|
{`$VAR !~ /^v\d+\.\d+/`, "not-a-version"},
|
||||||
|
{`$VAR =~ /^us\//`, "us/west"},
|
||||||
|
// Boolean operators
|
||||||
|
{`$A == "x" && $B == "y"`, "x"},
|
||||||
|
{`$A == "x" || $B == "y"`, "z"},
|
||||||
|
{`!($VAR == "main")`, "main"},
|
||||||
|
// Nested parens
|
||||||
|
{`($VAR == "a" || $VAR == "b") && $VAR != "c"`, "a"},
|
||||||
|
// Bare true / false
|
||||||
|
{`$VAR == true`, "true"},
|
||||||
|
{`$VAR == false`, "false"},
|
||||||
|
// Integer comparison (GitLab CI compares as strings)
|
||||||
|
{`$VAR == 42`, "42"},
|
||||||
|
// Regex with flags
|
||||||
|
{`$VAR =~ /main/i`, "MAIN"},
|
||||||
|
// Syntax errors / incomplete expressions
|
||||||
|
{``, ""},
|
||||||
|
{`&&`, ""},
|
||||||
|
{`$VAR =~`, "x"},
|
||||||
|
{`($VAR`, "x"},
|
||||||
|
{`$VAR == `, "x"},
|
||||||
|
// Variable syntax variants
|
||||||
|
{`${VAR} == "main"`, "main"},
|
||||||
|
// Deeply nested
|
||||||
|
{`((($VAR == "a")))`, "a"},
|
||||||
|
// String with escapes
|
||||||
|
{`$VAR == "hello\nworld"`, "hello\nworld"},
|
||||||
|
// Null literal
|
||||||
|
{`null == null`, ""},
|
||||||
|
{`$VAR == null`, ""},
|
||||||
|
}
|
||||||
|
for _, s := range seeds {
|
||||||
|
f.Add(s.expr, s.varVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, expr, varVal string) {
|
||||||
|
// EvalIf must never panic; it may return any bool.
|
||||||
|
_ = EvalIf(expr, func(string) string { return varVal })
|
||||||
|
_ = EvalIfStrict(expr, func(string) string { return varVal })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzExpandVarRefs ensures variable expansion in expression strings never
|
||||||
|
// panics and never produces a longer output than the worst-case expansion bound.
|
||||||
|
func FuzzExpandVarRefs(f *testing.F) {
|
||||||
|
seeds := []struct{ s, val string }{
|
||||||
|
{"$VAR", "hello"},
|
||||||
|
{"${VAR}", "hello"},
|
||||||
|
{"$A $B $C", "x"},
|
||||||
|
{"no vars here", ""},
|
||||||
|
{"$$double", "x"},
|
||||||
|
{"$", "x"},
|
||||||
|
{"${", "x"},
|
||||||
|
{"${}", "x"},
|
||||||
|
{"prefix_$VAR_suffix", "mid"},
|
||||||
|
{"$1INVALID", "x"},
|
||||||
|
}
|
||||||
|
for _, s := range seeds {
|
||||||
|
f.Add(s.s, s.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, s, val string) {
|
||||||
|
vars := map[string]string{"VAR": val, "A": val, "B": val, "C": val}
|
||||||
|
_ = expandVarRefs(s, vars)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package linter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.k3nny.fr/glint/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuzzLint ensures that the full Parse → Lint pipeline never panics on
|
||||||
|
// arbitrary YAML input. Lint rules type-assert model fields extensively;
|
||||||
|
// this fuzzer drives those assertions against malformed-but-parseable YAML.
|
||||||
|
// Run with: go test -fuzz=FuzzLint ./internal/linter/
|
||||||
|
func FuzzLint(f *testing.F) {
|
||||||
|
seeds := []string{
|
||||||
|
// Minimal valid pipeline
|
||||||
|
"stages: [build]\njob:\n stage: build\n script: echo ok\n",
|
||||||
|
// Manual / delayed / trigger / on_failure job types
|
||||||
|
"job:\n script: ok\n when: manual\n",
|
||||||
|
"job:\n script: ok\n when: delayed\n start_in: 5 minutes\n",
|
||||||
|
"job:\n trigger:\n project: group/repo\n",
|
||||||
|
"job:\n script: ok\n when: on_failure\n",
|
||||||
|
// needs: and dependencies:
|
||||||
|
"stages: [a,b]\na:\n stage: a\n script: ok\nb:\n stage: b\n needs: [a]\n script: ok\n",
|
||||||
|
"stages: [a,b]\na:\n stage: a\n script: ok\nb:\n stage: b\n dependencies: [a]\n script: ok\n",
|
||||||
|
// rules:
|
||||||
|
"job:\n script: ok\n rules:\n - if: '$CI_COMMIT_BRANCH == \"main\"'\n when: on_success\n",
|
||||||
|
// parallel matrix
|
||||||
|
"job:\n script: ok\n parallel:\n matrix:\n - ARCH: [amd64, arm64]\n",
|
||||||
|
// image as string / map
|
||||||
|
"job:\n script: ok\n image: golang:1.21\n",
|
||||||
|
"job:\n script: ok\n image:\n name: golang:1.21\n entrypoint: ['']\n",
|
||||||
|
// artifacts / cache with both string and map when:
|
||||||
|
"job:\n script: ok\n artifacts:\n when: on_success\n paths: [dist/]\n",
|
||||||
|
"job:\n script: ok\n cache:\n key: $CI_COMMIT_REF_SLUG\n paths: [vendor/]\n",
|
||||||
|
// environment / release / coverage
|
||||||
|
"job:\n script: ok\n environment:\n name: production\n url: https://example.com\n",
|
||||||
|
"job:\n script: ok\n release:\n tag_name: $CI_COMMIT_TAG\n description: Release\n",
|
||||||
|
"job:\n script: ok\n coverage: '/^TOTAL.*?(\\d+%)$/'\n",
|
||||||
|
// retry / timeout
|
||||||
|
"job:\n script: ok\n retry: 2\n",
|
||||||
|
"job:\n script: ok\n timeout: 2h30m\n",
|
||||||
|
// workflow
|
||||||
|
"workflow:\n rules:\n - if: '$CI_COMMIT_BRANCH'\n when: always\njob:\n script: ok\n",
|
||||||
|
// extends
|
||||||
|
".base:\n script: ok\nchild:\n extends: .base\n stage: test\n",
|
||||||
|
// id_tokens / secrets
|
||||||
|
"job:\n script: ok\n id_tokens:\n TOKEN:\n aud: https://example.com\n",
|
||||||
|
// services
|
||||||
|
"job:\n script: ok\n services:\n - name: postgres:14\n alias: db\n",
|
||||||
|
// pages job
|
||||||
|
"pages:\n script: make docs\n artifacts:\n paths: [public/]\n",
|
||||||
|
// inherit
|
||||||
|
"default:\n retry: 1\njob:\n script: ok\n inherit:\n default: false\n",
|
||||||
|
// allow_failure
|
||||||
|
"job:\n script: ok\n allow_failure:\n exit_codes: [1, 2]\n",
|
||||||
|
}
|
||||||
|
for _, s := range seeds {
|
||||||
|
f.Add([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
p, err := model.ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Lint must never panic regardless of pipeline content.
|
||||||
|
_ = Lint(p, nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// FuzzParseBytes ensures the YAML parser never panics on arbitrary input and
|
||||||
|
// that successful parses return a structurally sound Pipeline.
|
||||||
|
// Run with: go test -fuzz=FuzzParseBytes ./internal/model/
|
||||||
|
// Found failures are saved to testdata/fuzz/FuzzParseBytes/.
|
||||||
|
func FuzzParseBytes(f *testing.F) {
|
||||||
|
// Seed corpus: representative inputs covering the main code paths in
|
||||||
|
// ParseBytes, including the sanitizeYAMLEscapes pre-processing step.
|
||||||
|
seeds := [][]byte{
|
||||||
|
{},
|
||||||
|
[]byte("null"),
|
||||||
|
[]byte("stages: [build]\nbuild-job:\n stage: build\n script: echo ok\n"),
|
||||||
|
[]byte(".base:\n script: [make]\nchild:\n extends: .base\n stage: test\n"),
|
||||||
|
[]byte("stages: [a, b]\njob-a:\n stage: a\n script: run\njob-b:\n stage: b\n needs: [job-a]\n script: run\n"),
|
||||||
|
[]byte("*undefined_anchor"),
|
||||||
|
[]byte("- item1\n- item2\n"),
|
||||||
|
[]byte("my-job: \"just a string\"\n"),
|
||||||
|
[]byte("my-job:\n stage: [build, test]\n"),
|
||||||
|
[]byte("# glint: ignore GL007\nlegacy:\n only: [main]\n script: ok\n"),
|
||||||
|
[]byte("workflow:\n rules:\n - if: '$CI_COMMIT_BRANCH == \"main\"'\n when: always\njob:\n script: echo ok\n"),
|
||||||
|
[]byte("include:\n - local: other.yml\njob:\n script: echo ok\n"),
|
||||||
|
[]byte("job:\n script: echo ok\n when: on_failure\n rules:\n - if: '$VAR =~ /^us\\//'\n"),
|
||||||
|
[]byte("job:\n stage: test\n image:\n name: golang:1.21\n entrypoint: ['']\n parallel:\n matrix:\n - PLATFORM: [linux, darwin]\n script: go build\n"),
|
||||||
|
[]byte("default:\n retry: 2\n timeout: 1h30m\nvariables:\n ENV: production\nstages: [build, test, deploy]\n"),
|
||||||
|
[]byte("&anchor\n script: [echo ok]\njob:\n <<: *anchor\n stage: build\n"),
|
||||||
|
[]byte("?"), // null/empty YAML key — must error, not produce an empty-named job
|
||||||
|
}
|
||||||
|
for _, s := range seeds {
|
||||||
|
f.Add(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
p, err := ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return // errors are acceptable; panics are not
|
||||||
|
}
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("ParseBytes returned nil pipeline with nil error")
|
||||||
|
}
|
||||||
|
for name := range p.Jobs {
|
||||||
|
if name == "" {
|
||||||
|
t.Fatal("ParseBytes produced a job with an empty name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzSanitizeYAMLEscapes ensures the escape sanitizer never panics and never
|
||||||
|
// produces output shorter than its input (it can only expand \/ to \\/).
|
||||||
|
func FuzzSanitizeYAMLEscapes(f *testing.F) {
|
||||||
|
seeds := [][]byte{
|
||||||
|
{},
|
||||||
|
[]byte("stage: build"),
|
||||||
|
[]byte(`if: "$CI_BRANCH =~ /^us\//"`),
|
||||||
|
[]byte(`"pattern: /^us\//"`),
|
||||||
|
[]byte(`'single quoted \/ unchanged'`),
|
||||||
|
[]byte(`"\n\t\r"`),
|
||||||
|
[]byte(`"nested \"quote\" inside"`),
|
||||||
|
[]byte(`'it''s fine'`),
|
||||||
|
[]byte(`"unclosed`),
|
||||||
|
{'"', '\\'}, // double-quoted string ending with a lone backslash
|
||||||
|
{'"', '\\', '/'}, // the exact sequence being rewritten
|
||||||
|
}
|
||||||
|
for _, s := range seeds {
|
||||||
|
f.Add(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
out := sanitizeYAMLEscapes(data)
|
||||||
|
if len(out) < len(data) {
|
||||||
|
t.Fatalf("sanitizeYAMLEscapes shrank output: input len=%d output len=%d\ninput: %q",
|
||||||
|
len(data), len(out), data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -57,6 +57,9 @@ func ParseBytes(data []byte) (*Pipeline, error) {
|
|||||||
keyNode := root.Content[i]
|
keyNode := root.Content[i]
|
||||||
valNode := root.Content[i+1]
|
valNode := root.Content[i+1]
|
||||||
key := keyNode.Value
|
key := keyNode.Value
|
||||||
|
if key == "" {
|
||||||
|
return nil, fmt.Errorf("job name cannot be empty (null or missing YAML key)")
|
||||||
|
}
|
||||||
if ReservedKeys[key] {
|
if ReservedKeys[key] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ func TestParseBytes_EdgeCases(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("wrong field type: expected error from ParseBytes (stage must be string)")
|
t.Error("wrong field type: expected error from ParseBytes (stage must be string)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Null/empty YAML key (e.g. bare "?"): job name cannot be empty.
|
||||||
|
_, err = ParseBytes([]byte("?"))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("null key: expected error from ParseBytes (job name cannot be empty)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestParse_ParseBytesError exercises the Parse → ParseBytes error path (line 18).
|
// TestParse_ParseBytesError exercises the Parse → ParseBytes error path (line 18).
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
go test fuzz v1
|
||||||
|
[]byte("?")
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# glint — GitLab CI/CD Catalog component
|
||||||
|
#
|
||||||
|
# Validates a pipeline file with glint before the rest of the pipeline runs.
|
||||||
|
#
|
||||||
|
# Usage (after publishing to a GitLab instance as a Catalog component):
|
||||||
|
#
|
||||||
|
# include:
|
||||||
|
# - component: $CI_SERVER_FQDN/k3nny/glint/check@v0.2.28
|
||||||
|
# inputs:
|
||||||
|
# stage: validate # optional — see inputs below
|
||||||
|
#
|
||||||
|
# Or as a plain remote include (no Catalog required):
|
||||||
|
#
|
||||||
|
# include:
|
||||||
|
# - remote: https://raw.githubusercontent.com/.../templates/check.yml
|
||||||
|
#
|
||||||
|
# Or copy this file into your repository and use a local include.
|
||||||
|
|
||||||
|
spec:
|
||||||
|
inputs:
|
||||||
|
stage:
|
||||||
|
description: "Stage in which to run the glint validation job."
|
||||||
|
default: validate
|
||||||
|
pipeline_file:
|
||||||
|
description: "Path to the pipeline file to validate."
|
||||||
|
default: .gitlab-ci.yml
|
||||||
|
version:
|
||||||
|
description: >-
|
||||||
|
glint release tag to download (e.g. 'v0.2.28'). Use 'latest' to
|
||||||
|
always pull the newest release — not recommended for production
|
||||||
|
pipelines since it may break on a new release.
|
||||||
|
default: latest
|
||||||
|
allow_failure:
|
||||||
|
description: "Set to true to let the job fail without blocking the pipeline."
|
||||||
|
default: false
|
||||||
|
extra_args:
|
||||||
|
description: "Additional arguments passed to 'glint check' (e.g. '--format sarif')."
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
---
|
||||||
|
glint:check:
|
||||||
|
stage: $[[ inputs.stage ]]
|
||||||
|
image: alpine:3.19
|
||||||
|
variables:
|
||||||
|
GLINT_VERSION: "$[[ inputs.version ]]"
|
||||||
|
GLINT_FILE: "$[[ inputs.pipeline_file ]]"
|
||||||
|
GLINT_ARGS: "$[[ inputs.extra_args ]]"
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache curl
|
||||||
|
- |
|
||||||
|
if [ "$GLINT_VERSION" = "latest" ]; then
|
||||||
|
GLINT_VERSION=$(curl -sf \
|
||||||
|
"https://git.k3nny.fr/api/v1/repos/k3nny/glint/releases?limit=1" \
|
||||||
|
| grep '"tag_name"' | head -1 | cut -d'"' -f4)
|
||||||
|
fi
|
||||||
|
URL="https://git.k3nny.fr/k3nny/glint/releases/download/${GLINT_VERSION}/glint-${GLINT_VERSION}-linux-amd64"
|
||||||
|
curl -sfL "$URL" -o /usr/local/bin/glint
|
||||||
|
chmod +x /usr/local/bin/glint
|
||||||
|
glint --version
|
||||||
|
script:
|
||||||
|
- glint check $GLINT_ARGS "$GLINT_FILE"
|
||||||
|
allow_failure: $[[ inputs.allow_failure ]]
|
||||||
Reference in New Issue
Block a user