Skip to content

Comments

You can attach comments to any model and they appear in the generated YAML. This is useful for documenting intent, explaining non-obvious configuration, and adding context for anyone reading the generated files.

Set the comment field on any model to render a comment above that node in the YAML output:

Job(
name="Test",
runs_on="ubuntu-latest",
comment="Run the full test suite against all supported Python versions",
steps=[...],
)

Generated YAML:

test:
# Run the full test suite against all supported Python versions
name: Test
runs-on: ubuntu-latest

Set the eolComment field to render a comment after the node’s value on the same line:

Step(
name="Ruff",
run="ruff check .",
eol_comment="fast Python linter",
)

Generated YAML:

- name: Ruff
run: ruff check . # fast Python linter

Wrap a field value with with_comment() / withComment() to add a comment above that field in the output:

from ghagen import with_comment
Workflow(
name=with_comment("CI", "The name shown in the GitHub UI"),
on=On(push=PushTrigger(branches=["main"])),
jobs={...},
)

Generated YAML:

# The name shown in the GitHub UI
name: CI

Wrap a field value with with_eol_comment() / withEolComment() to add an end-of-line comment after that field’s value:

from ghagen import with_eol_comment
Workflow(
name="CI",
on=with_eol_comment(On(push=PushTrigger(branches=["main"])), "trigger configuration"),
jobs={...},
)

Generated YAML:

name: CI
on: # trigger configuration
push:
branches:
- main

Both with_comment and with_eol_comment are chainable — you can wrap a value with both a block comment and an EOL comment:

python name=with_eol_comment(with_comment("CI", "Block comment"), "EOL comment")

Here is a workflow that uses all four comment types:

from ghagen import with_comment, with_eol_comment
Workflow(
name=with_comment("Commented Workflow", "The name shown in the GitHub UI"),
on=with_eol_comment(On(push=PushTrigger(branches=["main"])), "trigger configuration"),
jobs={
"lint": Job(
name="Lint",
runs_on="ubuntu-latest",
comment="Run linters before tests",
steps=[
Step(uses="actions/checkout@v4"),
Step(
name="Ruff",
run="ruff check .",
eol_comment="fast Python linter",
),
],
),
},
)

This produces the following YAML:

# The name shown in the GitHub UI
name: Commented Workflow
on: # trigger configuration
push:
branches:
- main
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- # fast Python linter
name: Ruff
run: ruff check .

Every generated file starts with an auto-generated header comment block. By default it points back at the source file that defined the workflow or action:

# This file is generated by ghagen from .github/ghagen_workflows.py.
# Do not edit manually.

The {source_file} path is resolved relative to the directory containing .ghagen.yml (the marker file that identifies your project root). Each workflow/action gets its own source path based on where it was constructed — if you split workflows across multiple files, each emitted YAML names its own origin file.

Pass a template to App(header=...) to override the default. Templates use {variable} placeholder syntax so you can reference a fixed set of variables:

VariableDescription
{source_file}Source file that defined this item, relative to the app root.
{source_line}Line number where the Workflow / Action was constructed.
{tool}The string ghagen.
{version}The installed ghagen version (e.g. 0.2.1).

Example:

app = App(
header=(
"Generated by {tool} {version} from {source_file}.\n"
"Edit the Python source and run `ghagen synth` instead of hand-editing this file."
),
)

Emits:

# Generated by ghagen 0.2.1 from .github/ghagen_workflows.py.
# Edit the Python source and run `ghagen synth` instead of hand-editing this file.

Multiline templates are fine — every line gets a leading # . To include a literal { or } in the header, double it as {{ or }} (standard Python format-string escaping). TypeScript uses the same {variable} template syntax with the same escaping rules. Using an unknown variable like {oops} raises a clear error at synth time listing every valid name.

To skip the header entirely on a single to_yaml() call, pass include_header=False — this is what ghagen’s own snapshot tests use to decouple body assertions from header text.

EOL comments on steps (and other mapping items inside sequences): When a step has an eolComment, it renders on its own line above the step rather than at the end of the first line. The comment is still attached to the right node and appears in the output — only its visual placement differs from a true end-of-line comment.