cinzel gitlab -hConvert GitLab-oriented HCL blocks into a single .gitlab-ci.yml file.
cinzel gitlab parse --file ./cinzel/pipeline.hcl --output-directory .Convert .gitlab-ci.yml (or other GitLab CI YAML) into HCL.
cinzel gitlab unparse --file ./.gitlab-ci.yml --output-directory ./cinzelUse --dry-run to print generated files to stdout.
stages = ["build", "test", "deploy"]
variable "deploy_env" {
name = "DEPLOY_ENV"
value = "production"
description = "Target environment"
}
job "build" {
stage = "build"
image = "golang:1.26"
script = ["go build -o app ./..."]
}
job "test" {
extends = [template.go_base]
stage = "test"
depends_on = [job.build]
script = ["go test ./..."]
rule {
if = "$${CI_PIPELINE_SOURCE} == \"merge_request_event\""
when = "on_success"
}
}
workflow {
rule {
if = "$${CI_COMMIT_BRANCH} == \"main\""
when = "always"
}
}
include {
local = ".gitlab/base.yml"
}
default {
image = "alpine:3.20"
service {
name = "postgres:16"
alias = "db"
}
}
template "go_base" {
image = "golang:1.26"
}- HCL uses
depends_on; YAML usesneeds:. - A
needs:entry that is an object rather than a plain job name becomes aneed {}block, whosejobis ajob.<id>reference so it tracks a sanitized name likedepends_ondoes. - A job needs no
scriptof its own: atriggerjob has none by definition, and a job thatextendsa template inherits one. Both are written asjobblocks. $${VAR}in HCL becomes${VAR}in YAML.${VAR}in YAML becomes$${VAR}in HCL output.- Parse output is one file:
.gitlab-ci.ymlin the selected output directory. - A pipeline of nothing but
include:entries is unparsed like any other; a YAML file that is not a pipeline at all is still skipped. only:andexcept:, the older way to spellrules:, are carried through in both their list and object forms.- A
script,before_scriptorafter_scriptmay be a single command as a bare string, which is what GitLab's own schema takes, as well as a list. - An explicitly empty
needs:,cache:,services:orrules:survives the roundtrip. GitLab reads one as "override whatever this would inherit", which is not the same as leaving the keyword out, so each is written as an empty attribute where the schema otherwise uses blocks (cache = [],depends_on = []). - A
rules:,artifacts:,cache:,only:orexcept:written as null, and an emptyextends: [], survive the roundtrip the same way. GitLab reads either spelling as clearing an inherited value. A null on any other keyword is dropped rather than written back, since GitLab's own schema does not accept one there. - A
spec:header becomes aspecblock, and is written back as its own YAML document ahead of the rest of the configuration, which is the only place GitLab reads one. Every document of a multi-document pipeline is read, not just the first. - A job written in the steps syntax carries its commands under
runinstead ofscript, and needs noscriptof its own. It still needs a stage. image:,before_script:,after_script:,cache:andservices:are also accepted at the top level, where GitLab reads each as thedefaultof the same name. Each stays where it was written rather than being folded into adefaultblock, since GitLab does not document which wins when a pipeline has both.- A
variableblock takesexpandandoptionsalongsidevalueanddescription; a variable that carries nothing else stays a plain scalar. - Parse output includes cinzel provider markers in YAML headers (
generated-byandcinzel-provider). template.<id>andjob.<id>references inextendsmap to YAMLextendsentries.- A job or template name that is not a valid HCL identifier is sanitized to make the block referenceable (
build-appbecomes the labelbuild_app) and the original name is kept in anidattribute, so the name and anyneedsorextendspointing at it survive the roundtrip. - Repeated
include {}blocks map to YAMLinclude:entries. - The HCL schema in
provider/gitlab/config.gocovers the documented GitLab keywords, includingdependencies,identity,manual_confirmation,inherit,secrets,id_tokens,hooks,pages,runanddast_configurationon a job,start_inandinterruptibleon a rule,expose_as/public/accessonartifacts,unprotectoncache,dockerandkuberneteson a service,rulesandintegrityon an include, andoptionson a variable. It is checked against GitLab's own editor schema,app/assets/javascripts/editor/schema/ci.json. - A
ruleblock takesvariables,needs,start_in,interruptibleandauto_cancelalongsideif,when,allow_failure,changesandexists, for both workflow and job rules. - Whether a nested map becomes an HCL block or an object attribute follows the schema in
provider/gitlab/config.go, not the value's shape:artifacts.reportsis a block, whilecache.key,service.variables,default.retryandinclude.inputsare attributes. - Repeated
service {}blocks map to YAMLservices:entries underdefaultor ajob. - Repeated
cache {}blocks map to a YAMLcache:list underdefaultor ajob; a single block stays acache:object. - Parse schema is defined by typed HCL structs in
provider/gitlab/config.go;hcl:",remain"is used only for intentional pass-through islands. - Unparse schema validation favors strict typed YAML decode over manual key allowlist tables.