4 Commits

Author SHA1 Message Date
k3nny e5f926b55f docs: 📝 add logo
release / Build and publish release (push) Successful in 1m47s
2026-06-11 20:40:39 +02:00
k3nny 8c3ce050f5 Merge pull request 'fix(model): handle YAML map forms that caused unmarshall errors' (#3) from fix/unmarshall_errors into main
Reviewed-on: #3
2026-06-11 20:31:30 +02:00
k3nny c4ab64391d fix(model): handle YAML map forms that caused unmarshall errors
Variables with value/description/options sub-keys, default.image in map
form, default.before_script / default.after_script as block scalars, and
rules.changes / rules.exists in {paths, compare_to} map form all caused
"yaml: cannot unmarshal !!map into string" because the struct fields were
typed too narrowly.

Changed types in model.Pipeline, model.DefaultConfig, and model.Rule to
accept any to match GitLab CI spec flexibility (13.7+ variable declarations,
15.3+ rules.changes map form, image map form in default block).

Adds testdata/script_multiline.yml covering all these patterns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 20:25:53 +02:00
k3nny 4cc50afb5f fix(build): replace actions/checkout@v4 with plain git clone
release / Build and publish release (push) Successful in 1m3s
actions/checkout@v4 requires Node.js which is absent in golang:alpine.
Clone the repo directly with git using an oauth2 token in the URL,
removing the Node.js dependency entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 01:04:27 +02:00
7 changed files with 129 additions and 18 deletions
+9 -1
View File
@@ -16,7 +16,15 @@ jobs:
- name: Install tools - name: Install tools
run: apk add --no-cache curl git jq run: apk add --no-cache curl git jq
- uses: actions/checkout@v4 - 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: Build Linux (amd64) - name: Build Linux (amd64)
env: env:
+7
View File
@@ -7,6 +7,13 @@ This project uses [Semantic Versioning](https://semver.org).
## [Unreleased] ## [Unreleased]
### Fixed
- **Variable map form now parses correctly** — `variables:` entries that use the extended `{value, description, options}` form (GitLab CI 13.7+) no longer cause `yaml: cannot unmarshal !!map into string`. Both `Pipeline.Variables` and per-job `Variables` now accept either plain strings or map-form declarations.
- **`default.image` map form now parses correctly** — `default: image: {name: ..., pull_policy: ...}` used to cause `yaml: cannot unmarshal !!map into string`; `DefaultConfig.Image` is now typed as `any` to match `Job.Image`.
- **`default.before_script` / `default.after_script` now accept both list and scalar forms** — previously `DefaultConfig.BeforeScript` and `DefaultConfig.AfterScript` were `[]string`, causing a parse error when the field was written as a block scalar string. They are now typed as `any` to match the corresponding `Job` fields.
- **`rules.changes` / `rules.exists` map form now parses correctly** — extended `changes: {paths: [...], compare_to: "..."}` syntax (GitLab CI 15.3+) used to cause `yaml: cannot unmarshal !!map into []string`.
## [0.2.0] - 2026-06-11 ## [0.2.0] - 2026-06-11
### Added ### Added
+10 -3
View File
@@ -1,7 +1,13 @@
# glint <p align="center">
<img src="assets/glint-logo.png" alt="glint logo" width="220" />
</p>
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) <h1 align="center">glint</h1>
[![Release](https://img.shields.io/badge/release-v0.2.0-blue.svg)](CHANGELOG.md)
<p align="center">
<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.0-blue.svg" alt="Release"></a>
</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.
@@ -19,6 +25,7 @@ A local tool to validate and lint `.gitlab-ci.yml` pipelines without needing a G
- **CI/CD catalog components** — resolves `include: component:` references from the GitLab CI/CD Catalog; public components work without a token - **CI/CD catalog components** — resolves `include: component:` references from the GitLab CI/CD Catalog; public components work without a token
- **Deprecation warnings** — flags `only`/`except` usage in favour of `rules` - **Deprecation warnings** — flags `only`/`except` usage in favour of `rules`
- **Local include resolution** — `include: local:` entries are read from disk and recursively merged before linting, so multi-file pipelines are fully validated - **Local include resolution** — `include: local:` entries are read from disk and recursively merged before linting, so multi-file pipelines are fully validated
- **Extended variable declarations** — `variables:` entries may use the `{value, description, options}` map form (GitLab CI 13.7+); `default.image` accepts both string and map form; `rules.changes`/`rules.exists` accept both list and `{paths, compare_to}` map form
- **Graph output** — `glint graph` prints a job tree (stages → jobs) to the terminal; `glint graph includes` emits a Mermaid include dependency diagram; `glint graph pipeline` renders a GitLab CI-style PNG/SVG - **Graph output** — `glint graph` prints a job tree (stages → jobs) to the terminal; `glint graph includes` emits a Mermaid include dependency diagram; `glint graph pipeline` renders a GitLab CI-style PNG/SVG
- **Context simulation** — pass `--branch`, `--tag`, or `--source` to `glint check` or `glint graph` to see which jobs would be active, manual, or skipped for a specific pipeline event; evaluates `rules:if:` expressions and `only`/`except` filters - **Context simulation** — pass `--branch`, `--tag`, or `--source` to `glint check` or `glint graph` to see which jobs would be active, manual, or skipped for a specific pipeline event; evaluates `rules:if:` expressions and `only`/`except` filters
+2
View File
@@ -45,6 +45,8 @@ tasks:
ignore_error: false ignore_error: false
- cmd: ./{{.BINARY}} check testdata/includes_component.yml - cmd: ./{{.BINARY}} check testdata/includes_component.yml
ignore_error: false ignore_error: false
- cmd: ./{{.BINARY}} check testdata/script_multiline.yml
ignore_error: false
- cmd: ./{{.BINARY}} check testdata/context_rules.yml - cmd: ./{{.BINARY}} check testdata/context_rules.yml
ignore_error: false ignore_error: false
- cmd: ./{{.BINARY}} check --branch main testdata/context_rules.yml - cmd: ./{{.BINARY}} check --branch main testdata/context_rules.yml
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+14 -14
View File
@@ -3,20 +3,20 @@ package model
// 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 {
Stages []string `yaml:"stages"` Stages []string `yaml:"stages"`
Variables map[string]string `yaml:"variables"` Variables map[string]any `yaml:"variables"` // string or {value,description,options} map
Default *DefaultConfig `yaml:"default"` Default *DefaultConfig `yaml:"default"`
Include []any `yaml:"include"` Include []any `yaml:"include"`
Workflow *Workflow `yaml:"workflow"` Workflow *Workflow `yaml:"workflow"`
// Jobs holds every non-reserved top-level key (i.e. job definitions). // Jobs holds every non-reserved top-level key (i.e. job definitions).
Jobs map[string]Job `yaml:"-"` Jobs map[string]Job `yaml:"-"`
RawJobs map[string]map[string]any `yaml:"-"` // pre-resolution raw maps, used by the resolver RawJobs map[string]map[string]any `yaml:"-"` // pre-resolution raw maps, used by the resolver
} }
type DefaultConfig struct { type DefaultConfig struct {
Image string `yaml:"image"` Image any `yaml:"image"` // string or {name,pull_policy,...} map
BeforeScript []string `yaml:"before_script"` BeforeScript any `yaml:"before_script"` // []string or string (block scalar)
AfterScript []string `yaml:"after_script"` AfterScript any `yaml:"after_script"` // []string or string
Cache any `yaml:"cache"` Cache any `yaml:"cache"`
Artifacts any `yaml:"artifacts"` Artifacts any `yaml:"artifacts"`
Retry any `yaml:"retry"` Retry any `yaml:"retry"`
@@ -37,8 +37,8 @@ type Job struct {
AfterScript any `yaml:"after_script"` // []string or string AfterScript any `yaml:"after_script"` // []string or string
Image any `yaml:"image"` Image any `yaml:"image"`
Services []any `yaml:"services"` Services []any `yaml:"services"`
Variables map[string]string `yaml:"variables"` Variables map[string]any `yaml:"variables"` // string or {value,description,options} map
Rules []Rule `yaml:"rules"` Rules []Rule `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"`
@@ -68,8 +68,8 @@ 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 []string `yaml:"changes"` Changes any `yaml:"changes"` // []string or {paths,compare_to} map
Exists []string `yaml:"exists"` Exists any `yaml:"exists"` // []string or map form
} }
// ReservedKeys are top-level GitLab CI keys that are NOT job definitions. // ReservedKeys are top-level GitLab CI keys that are NOT job definitions.
+87
View File
@@ -0,0 +1,87 @@
---
# Exercises multi-line script patterns and extended variable declarations.
# Ref: https://docs.gitlab.com/ci/yaml/script/#split-long-commands
# Ref: https://docs.gitlab.com/ee/ci/yaml/#variablesdescription
# All patterns here must parse cleanly (exit 0).
stages:
- build
- test
- deploy
# Pipeline-level variables: plain strings and extended {value, description} map form.
variables:
PLAIN_VAR: "hello"
DEPLOY_ENV:
value: "staging"
description: "The deployment target. Set to staging or production."
RETRIES:
value: "3"
description: "Number of retry attempts."
options:
- "1"
- "3"
- "5"
default:
# image in map form (name + pull_policy)
image:
name: alpine:latest
pull_policy: if-not-present
# before_script as a block scalar (not a list)
before_script:
- apk add --no-cache curl git
build-literal-block:
stage: build
# script items using literal block scalar (|)
script:
- |
if [[ "$DEPLOY_ENV" == "production" ]]; then
echo "Production build"
else
echo "Non-production build"
fi
- echo "Build step done"
build-folded-block:
stage: build
# script items using folded block scalar (>)
script:
- >
apt-get update -qq &&
apt-get install -y curl wget
- echo "Packages installed"
before_script:
- |
echo "Job-level before_script"
echo "Using literal block scalar"
test-job:
stage: test
script:
- echo "Running tests"
- |
set -e
go test ./...
echo "Tests passed"
# Job-level variable with extended form
variables:
TEST_FLAG:
value: "true"
description: "Enable verbose test output"
# rules.changes in map form (GitLab 15.3+)
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
paths:
- "**/*.go"
compare_to: "main"
when: on_success
- when: on_success
deploy-job:
stage: deploy
script:
- echo "Deploying to $DEPLOY_ENV"
when: manual