Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/dev.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions docs/src/content/docs/reference/frontmatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ The YAML frontmatter supports standard GitHub Actions properties plus additional
- `cache`: Cache configuration for workflow dependencies

**Properties specific to GitHub Agentic Workflows:**
- `description`: Human-readable description rendered as a comment in the lock file
- `source`: Source reference tracking where the workflow was added from (format: `owner/repo/path@ref`)
- `engine`: AI engine configuration (copilot/claude/codex) with optional max-turns setting
- `strict`: Enable strict mode validation (boolean, defaults to false)
- `roles`: Permission restrictions based on repository access levels
Expand Down Expand Up @@ -140,6 +142,81 @@ on:

This filtering is especially useful for [LabelOps workflows](/gh-aw/guides/labelops/) where specific labels trigger different automation behaviors.

## Description (`description:`)

The `description:` field provides a human-readable description of the workflow that is rendered as a comment in the generated lock file. This helps document the purpose and functionality of the workflow.

```yaml
description: "Workflow that analyzes pull requests and provides feedback"
```

The description appears in the lock file header as a comment:

```yaml
# This file was automatically generated by gh-aw. DO NOT EDIT.
# To update this file, edit the corresponding .md file and run:
# gh aw compile
# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/instructions/github-agentic-workflows.instructions.md
#
# Workflow that analyzes pull requests and provides feedback

name: "PR Analyzer"
...
```

## Source Tracking (`source:`)

The `source:` field tracks the origin of workflows added using the `gh aw add` command. This field is automatically populated when installing workflows from external repositories and provides traceability for workflow provenance.

**Format:** `owner/repo/path@ref`

```yaml
source: "githubnext/agentics/workflows/ci-doctor.md@v1.0.0"
```

**Examples:**
- `githubnext/agentics/workflows/ci-doctor.md@v1.0.0` - Workflow from a specific version tag
- `githubnext/agentics/workflows/daily-plan.md@main` - Workflow from the main branch
- `githubnext/agentics/workflows/helper-bot.md` - Workflow without version specification

**Automatic Population:**

When you use the `gh aw add` command, the source field is automatically added to the workflow frontmatter:

```bash
# Command
gh aw add githubnext/agentics/ci-doctor@v1.0.0

# Generated frontmatter includes:
source: "githubnext/agentics/workflows/ci-doctor.md@v1.0.0"
```

**Rendering in Lock Files:**

The source field is rendered as a comment in the lock file header, positioned after the description:

```yaml
# This file was automatically generated by gh-aw. DO NOT EDIT.
# To update this file, edit the corresponding .md file and run:
# gh aw compile
# For more information: https://github.com/githubnext/gh-aw/blob/main/.github/instructions/github-agentic-workflows.instructions.md
#
# CI Doctor workflow - added from githubnext/agentics
#
# Source: githubnext/agentics/workflows/ci-doctor.md@v1.0.0

name: "CI Doctor"
...
```

**Benefits:**
- **Traceability**: Know exactly where a workflow came from and which version
- **Updates**: Easy identification of the source repository for checking updates
- **Documentation**: Automatic documentation of workflow provenance
- **Auditing**: Track workflow origins for security and compliance purposes

**Note:** The `source` field is optional. Workflows created manually or without using `gh aw add` don't need to include it.

## Permissions (`permissions:`)

The `permissions:` section uses standard GitHub Actions permissions syntax to specify the permissions relevant to the agentic (natural language) part of the execution of the workflow. See [GitHub Actions permissions documentation](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions).
Expand Down
71 changes: 71 additions & 0 deletions pkg/cli/add_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/parser"
"github.com/githubnext/gh-aw/pkg/workflow"
"github.com/goccy/go-yaml"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -442,6 +444,19 @@ func addWorkflowWithTracking(workflow *WorkflowSpec, number int, verbose bool, e
content = updateWorkflowTitle(content, i)
}

// Add source field to frontmatter
sourceString := buildSourceString(workflow)
if sourceString != "" {
updatedContent, err := addSourceToWorkflow(content, sourceString, verbose)
if err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to add source field: %v", err)))
}
} else {
content = updatedContent
}
}

// Track the file based on whether it existed before (if tracker is available)
if tracker != nil {
if fileExists {
Expand Down Expand Up @@ -804,3 +819,59 @@ func createPR(branchName, title, body string, verbose bool) error {

return nil
}

// buildSourceString builds the source string in the format owner/repo/path@ref
func buildSourceString(workflow *WorkflowSpec) string {
if workflow.Repo == "" || workflow.WorkflowPath == "" {
return ""
}

// Format: owner/repo/path@ref (consistent with add command syntax)
source := workflow.Repo + "/" + workflow.WorkflowPath
if workflow.Version != "" {
source += "@" + workflow.Version
}

return source
}

// addSourceToWorkflow adds the source field to the workflow's frontmatter
func addSourceToWorkflow(content, source string, verbose bool) (string, error) {
// Parse frontmatter using parser package
result, err := parser.ExtractFrontmatterFromContent(content)
if err != nil {
return "", fmt.Errorf("failed to parse frontmatter: %w", err)
}

// Initialize frontmatter if it doesn't exist
if result.Frontmatter == nil {
result.Frontmatter = make(map[string]any)
}

// Add source field (will be last in YAML output due to alphabetical sorting)
result.Frontmatter["source"] = source

// Convert back to YAML
updatedFrontmatter, err := yaml.Marshal(result.Frontmatter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use helper that fixes yaml marshaling issues with "on"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied UnquoteYAMLKey helper to fix YAML marshaling issues with 'on' keyword.

Changes:

  • Exported unquoteYAMLKey as UnquoteYAMLKey
  • Applied in addSourceToWorkflow to prevent "on" from being quoted
  • Added test case to verify "on" remains unquoted
  • Updated all references across codebase

The "on" keyword is now properly unquoted when adding source field to workflows.

Commit: 3ee8a9c

if err != nil {
return "", fmt.Errorf("failed to marshal updated frontmatter: %w", err)
}

// Clean up quoted keys - replace "on": with on: at the start of a line
// This handles cases where YAML marshaling adds unnecessary quotes around reserved words like "on"
frontmatterStr := strings.TrimSuffix(string(updatedFrontmatter), "\n")
frontmatterStr = workflow.UnquoteYAMLKey(frontmatterStr, "on")

// Reconstruct the file
var lines []string
lines = append(lines, "---")
if frontmatterStr != "" {
lines = append(lines, strings.Split(frontmatterStr, "\n")...)
}
lines = append(lines, "---")
if result.Markdown != "" {
lines = append(lines, result.Markdown)
}

return strings.Join(lines, "\n"), nil
}
188 changes: 188 additions & 0 deletions pkg/cli/add_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package cli

import (
"strings"
"testing"
)

// TestBuildSourceString tests the buildSourceString function
func TestBuildSourceString(t *testing.T) {
tests := []struct {
name string
workflow *WorkflowSpec
expected string
}{
{
name: "full_spec_with_version",
workflow: &WorkflowSpec{
Repo: "githubnext/agentics",
WorkflowPath: "workflows/ci-doctor.md",
Version: "v1.0.0",
},
expected: "githubnext/agentics/workflows/ci-doctor.md@v1.0.0",
},
{
name: "spec_without_version",
workflow: &WorkflowSpec{
Repo: "githubnext/agentics",
WorkflowPath: "workflows/ci-doctor.md",
Version: "",
},
expected: "githubnext/agentics/workflows/ci-doctor.md",
},
{
name: "spec_with_branch",
workflow: &WorkflowSpec{
Repo: "githubnext/agentics",
WorkflowPath: "workflows/daily-plan.md",
Version: "main",
},
expected: "githubnext/agentics/workflows/daily-plan.md@main",
},
{
name: "empty_repo",
workflow: &WorkflowSpec{
Repo: "",
WorkflowPath: "workflows/test.md",
Version: "v1.0.0",
},
expected: "",
},
{
name: "empty_workflow_path",
workflow: &WorkflowSpec{
Repo: "githubnext/agentics",
WorkflowPath: "",
Version: "v1.0.0",
},
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildSourceString(tt.workflow)
if result != tt.expected {
t.Errorf("buildSourceString() = %v, want %v", result, tt.expected)
}
})
}
}

// TestAddSourceToWorkflow tests the addSourceToWorkflow function
func TestAddSourceToWorkflow(t *testing.T) {
tests := []struct {
name string
content string
source string
expectError bool
checkSource bool
}{
{
name: "add_source_to_workflow_with_frontmatter",
content: `---
on: push
permissions:
contents: read
engine: claude
---

# Test Workflow

This is a test workflow.`,
source: "githubnext/agentics/workflows/ci-doctor.md@v1.0.0",
expectError: false,
checkSource: true,
},
{
name: "add_source_to_workflow_without_frontmatter",
content: `# Test Workflow

This is a test workflow without frontmatter.`,
source: "githubnext/agentics/workflows/test.md@main",
expectError: false,
checkSource: true,
},
{
name: "add_source_to_existing_workflow_with_fields",
content: `---
description: "Test workflow description"
on: push
permissions:
contents: read
engine: claude
tools:
github:
allowed: [list_commits]
---

# Test Workflow

This is a test workflow.`,
source: "githubnext/agentics/workflows/complex.md@v1.0.0",
expectError: false,
checkSource: true,
},
{
name: "verify_on_keyword_not_quoted",
content: `---
on:
push:
branches: [main]
pull_request:
types: [opened]
permissions:
contents: read
engine: claude
---

# Test Workflow

This workflow has complex 'on' triggers.`,
source: "githubnext/agentics/workflows/test.md@v1.0.0",
expectError: false,
checkSource: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := addSourceToWorkflow(tt.content, tt.source, false)

if tt.expectError && err == nil {
t.Errorf("addSourceToWorkflow() expected error, got nil")
return
}

if !tt.expectError && err != nil {
t.Errorf("addSourceToWorkflow() error = %v", err)
return
}

if !tt.expectError && tt.checkSource {
// Verify that the source field is present in the result
if !strings.Contains(result, "source:") {
t.Errorf("addSourceToWorkflow() result does not contain 'source:' field")
}
if !strings.Contains(result, tt.source) {
t.Errorf("addSourceToWorkflow() result does not contain source value '%s'", tt.source)
}

// Verify that frontmatter delimiters are present
if !strings.Contains(result, "---") {
t.Errorf("addSourceToWorkflow() result does not contain frontmatter delimiters")
}

// Verify that markdown content is preserved
if strings.Contains(tt.content, "# Test Workflow") && !strings.Contains(result, "# Test Workflow") {
t.Errorf("addSourceToWorkflow() result does not preserve markdown content")
}

// Verify that "on" keyword is not quoted
if strings.Contains(result, `"on":`) {
t.Errorf("addSourceToWorkflow() result contains quoted 'on' keyword, should be unquoted. Result:\n%s", result)
}
}
})
}
}
4 changes: 4 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"type": "string",
"description": "Optional workflow description that is rendered as a comment in the generated GitHub Actions YAML file (.lock.yml)"
},
"source": {
"type": "string",
"description": "Optional source reference indicating where this workflow was added from. Format: owner/repo/path@ref (e.g., githubnext/agentics/workflows/ci-doctor.md@v1.0.0). Rendered as a comment in the generated lock file."
},
"on": {
"description": "Workflow triggers that define when the agentic workflow should run. Supports standard GitHub Actions trigger events plus special command triggers for /commands (required)",
"oneOf": [
Expand Down
Loading