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.
Comment types
Section titled “Comment types”Block comments
Section titled “Block comments”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=[...],)job({ name: "Test", runsOn: "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-latestEnd-of-line comments
Section titled “End-of-line comments”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",)step({ name: "Ruff", run: "ruff check .", eolComment: "fast Python linter",});Generated YAML:
- name: Ruff run: ruff check . # fast Python linterField-level block comments
Section titled “Field-level block comments”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={...},)import { withComment } from "ghagen";
workflow({ name: withComment("CI", "The name shown in the GitHub UI"), on: { push: { branches: ["main"] } }, jobs: { ... },});Generated YAML:
# The name shown in the GitHub UIname: CIField-level EOL comments
Section titled “Field-level EOL comments”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={...},)import { withEolComment } from "ghagen";
workflow({ name: "CI", on: withEolComment({ push: { branches: ["main"] } }, "trigger configuration"), jobs: { ... },});Generated YAML:
name: CIon: # trigger configuration push: branches: - mainBoth 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")
typescript name: withEolComment(withComment("CI", "Block comment"), "EOL comment")
Full example
Section titled “Full example”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",),],),},)import { withComment, withEolComment } from "ghagen";
workflow({ name: withComment("Commented Workflow", "The name shown in the GitHub UI"), on: withEolComment({ push: { branches: ["main"] } }, "trigger configuration"), jobs: { lint: job({ name: "Lint", runsOn: "ubuntu-latest", comment: "Run linters before tests", steps: [ step({ uses: "actions/checkout@v4" }), step({ name: "Ruff", run: "ruff check .", eolComment: "fast Python linter", }), ], }), },});This produces the following YAML:
# The name shown in the GitHub UIname: Commented Workflowon: # trigger configuration push: branches: - mainjobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - # fast Python linter name: Ruff run: ruff check .Header comments
Section titled “Header comments”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.
Customizing the header
Section titled “Customizing the header”Pass a template to App(header=...) to override the default. Templates use {variable} placeholder syntax so you can reference a fixed set of variables:
| Variable | Description |
|---|---|
{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." ),)const app = new App({ header: "Generated by {tool} {version} from {source_file}.\n" + "Edit the TypeScript 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.
Known limitations
Section titled “Known limitations”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.