Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5f926b55f | |||
| 8c3ce050f5 | |||
| c4ab64391d | |||
| 4cc50afb5f | |||
| dfbafd8ed3 | |||
| 58cfbb4a57 |
@@ -6,52 +6,45 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
release:
|
||||||
name: Build (${{ matrix.goos }}/${{ matrix.goarch }})
|
name: Build and publish release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
container:
|
||||||
matrix:
|
image: golang:1.26-alpine
|
||||||
include:
|
|
||||||
- goos: linux
|
|
||||||
goarch: amd64
|
|
||||||
suffix: -linux-amd64
|
|
||||||
- goos: windows
|
|
||||||
goarch: amd64
|
|
||||||
suffix: .exe
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Install tools
|
||||||
|
run: apk add --no-cache curl git jq
|
||||||
|
|
||||||
- uses: actions/setup-go@v5
|
- name: Checkout
|
||||||
with:
|
|
||||||
go-version-file: go.mod
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
env:
|
||||||
GOOS: ${{ matrix.goos }}
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
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)
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
run: |
|
run: |
|
||||||
go build -trimpath -ldflags="-s -w" \
|
go build -trimpath -ldflags="-s -w" \
|
||||||
-o glint-${{ github.ref_name }}${{ matrix.suffix }} \
|
-o glint-${{ github.ref_name }}-linux-amd64 \
|
||||||
./cmd/glint/...
|
./cmd/glint/...
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- name: Build Windows (amd64)
|
||||||
with:
|
env:
|
||||||
name: binary-${{ matrix.goos }}-${{ matrix.goarch }}
|
GOOS: windows
|
||||||
path: glint-${{ github.ref_name }}${{ matrix.suffix }}
|
GOARCH: amd64
|
||||||
retention-days: 7
|
CGO_ENABLED: "0"
|
||||||
|
run: |
|
||||||
release:
|
go build -trimpath -ldflags="-s -w" \
|
||||||
name: Publish release
|
-o glint-${{ github.ref_name }}.exe \
|
||||||
needs: build
|
./cmd/glint/...
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
pattern: binary-*
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Create release and upload assets
|
- name: Create release and upload assets
|
||||||
env:
|
env:
|
||||||
@@ -67,11 +60,11 @@ 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-*; do
|
for file in glint-${{ github.ref_name }}-linux-amd64 glint-${{ github.ref_name }}.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" \
|
||||||
"$API_URL/repos/$REPO/releases/$release_id/assets?name=$(basename "$file")" \
|
"$API_URL/repos/$REPO/releases/$release_id/assets?name=$file" \
|
||||||
--data-binary "@$file"
|
--data-binary "@$file"
|
||||||
echo "uploaded: $file"
|
echo "uploaded: $file"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# glint
|
<p align="center">
|
||||||
|
<img src="assets/glint-logo.png" alt="glint logo" width="220" />
|
||||||
|
</p>
|
||||||
|
|
||||||
[](LICENSE)
|
<h1 align="center">glint</h1>
|
||||||
[](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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
Vendored
+87
@@ -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
|
||||||
Reference in New Issue
Block a user