Skip to content
Closed
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
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/compilation-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ Pre-activation runs checks sequentially. Any failure sets `activated=false`, pre

**Maintainability**: Use imports for shared configuration, document complex workflows with `description:`, compile frequently during development, version control lock files and action pins (`.github/aw/actions-lock.json`).

**Performance**: Enable caching (`cache:` and `cache-memory:`), minimize imports to essentials, optimize tool configurations with restricted `allowed:` lists, use safe-jobs for custom logic.
**Performance**: Enable caching (`cache:` and `cache-memory:`), minimize imports to essentials, optimize tool configurations with restricted `allowed:` lists, use custom safe output jobs (`safe-outputs.jobs:`) for custom logic.

**Debugging**: Enable verbose logging (`--verbose`), check job dependency graphs in headers, inspect artifacts and firewall logs (`gh aw logs`), validate without file generation (`--no-emit`).

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The agent requests issue creation; a separate job with `issues: write` creates i
Create custom post-processing jobs registered as Model Context Protocol (MCP) tools. Support standard GitHub Actions properties and auto-access agent output via `$GH_AW_AGENT_OUTPUT`. See [Custom Safe Output Jobs](/gh-aw/guides/custom-safe-outputs/).

> [!NOTE]
> **Internal Implementation**: Custom safe output jobs are internally referred to as "safe-jobs" in the compiler code (`pkg/workflow/safe_jobs.go`), but they are user-facing only through the `safe-outputs.jobs:` configuration. The top-level `safe-jobs:` key is deprecated and not supported.
> **Internal Implementation Note**: Custom safe output jobs are internally referred to as "safe-jobs" in the compiler code (`pkg/workflow/safe_jobs.go`) for parsing purposes. However, users should ONLY configure these jobs via the `safe-outputs.jobs:` frontmatter syntax. The top-level `safe-jobs:` key is an internal implementation detail and is not supported in user workflows.

### Issue Creation (`create-issue:`)

Expand Down
25 changes: 13 additions & 12 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2504,7 +2504,7 @@
[
{
"name": "Verify Post-Steps Execution",
"run": "echo \" Post-steps are executing correctly\"\necho \"This step runs after the AI agent completes\"\n"
"run": "echo \"\u2705 Post-steps are executing correctly\"\necho \"This step runs after the AI agent completes\"\n"
},
{
"name": "Upload Test Results",
Expand Down Expand Up @@ -5647,7 +5647,8 @@
"additionalProperties": false
}
},
"additionalProperties": false
"additionalProperties": false,
"$comment": "INTERNAL IMPLEMENTATION: This user-facing 'safe-outputs.jobs:' configuration is transformed internally into a 'safe-jobs' key for parsing by the compiler (see pkg/workflow/safe_jobs.go). The top-level 'safe-jobs:' key is NOT supported in user workflows and is used only internally."
},
"messages": {
"type": "object",
Expand Down Expand Up @@ -5675,8 +5676,8 @@
},
"staged-title": {
"type": "string",
"description": "Custom title template for staged mode preview. Available placeholders: {operation}. Example: '🎭 Preview: {operation}'",
"examples": ["🎭 Preview: {operation}", "## Staged Mode: {operation}"]
"description": "Custom title template for staged mode preview. Available placeholders: {operation}. Example: '\ud83c\udfad Preview: {operation}'",
"examples": ["\ud83c\udfad Preview: {operation}", "## Staged Mode: {operation}"]
},
"staged-description": {
"type": "string",
Expand All @@ -5690,18 +5691,18 @@
},
"run-success": {
"type": "string",
"description": "Custom message template for successful workflow completion. Available placeholders: {workflow_name}, {run_url}. Default: ' Agentic [{workflow_name}]({run_url}) completed successfully.'",
"examples": [" Agentic [{workflow_name}]({run_url}) completed successfully.", " [{workflow_name}]({run_url}) finished."]
"description": "Custom message template for successful workflow completion. Available placeholders: {workflow_name}, {run_url}. Default: '\u2705 Agentic [{workflow_name}]({run_url}) completed successfully.'",
"examples": ["\u2705 Agentic [{workflow_name}]({run_url}) completed successfully.", "\u2705 [{workflow_name}]({run_url}) finished."]
},
"run-failure": {
"type": "string",
"description": "Custom message template for failed workflow. Available placeholders: {workflow_name}, {run_url}, {status}. Default: ' Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.'",
"examples": [" Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.", " [{workflow_name}]({run_url}) {status}."]
"description": "Custom message template for failed workflow. Available placeholders: {workflow_name}, {run_url}, {status}. Default: '\u274c Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.'",
"examples": ["\u274c Agentic [{workflow_name}]({run_url}) {status} and wasn't able to produce a result.", "\u274c [{workflow_name}]({run_url}) {status}."]
},
"detection-failure": {
"type": "string",
"description": "Custom message template for detection job failure. Available placeholders: {workflow_name}, {run_url}. Default: '⚠️ Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.'",
"examples": ["⚠️ Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.", "⚠️ Detection job failed in [{workflow_name}]({run_url})."]
"description": "Custom message template for detection job failure. Available placeholders: {workflow_name}, {run_url}. Default: '\u26a0\ufe0f Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.'",
"examples": ["\u26a0\ufe0f Security scanning failed for [{workflow_name}]({run_url}). Review the logs for details.", "\u26a0\ufe0f Detection job failed in [{workflow_name}]({run_url})."]
},
"append-only-comments": {
"type": "boolean",
Expand Down Expand Up @@ -5781,12 +5782,12 @@
"additionalProperties": false
},
"roles": {
"description": "Repository access roles required to trigger agentic workflows. Defaults to ['admin', 'maintainer', 'write'] for security. Use 'all' to allow any authenticated user (⚠️ security consideration).",
"description": "Repository access roles required to trigger agentic workflows. Defaults to ['admin', 'maintainer', 'write'] for security. Use 'all' to allow any authenticated user (\u26a0\ufe0f security consideration).",
"oneOf": [
{
"type": "string",
"enum": ["all"],
"description": "Allow any authenticated user to trigger the workflow (⚠️ disables permission checking entirely - use with caution)"
"description": "Allow any authenticated user to trigger the workflow (\u26a0\ufe0f disables permission checking entirely - use with caution)"
},
{
"type": "array",
Expand Down
36 changes: 32 additions & 4 deletions pkg/workflow/safe_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ import (
"github.com/githubnext/gh-aw/pkg/stringutil"
)

// Package workflow implements custom safe output jobs for GitHub Agentic Workflows.
//
// ARCHITECTURE OVERVIEW:
//
// User-Facing API:
// - Users configure custom safe output jobs via "safe-outputs.jobs:" in workflow frontmatter
// - This is documented in docs/src/content/docs/reference/safe-outputs.md
// - Schema definition in pkg/parser/schemas/main_workflow_schema.json under safe-outputs.properties.jobs
//
// Internal Implementation:
// - The internal "safe-jobs" key is used ONLY for parsing and is NOT user-facing
// - extractSafeJobsFromFrontmatter() reads "safe-outputs.jobs:" and transforms it for parsing
// - parseSafeJobsConfig() expects the internal "safe-jobs" key format
// - The top-level "safe-jobs:" key is NOT supported in user workflows
//
// This separation allows the code to use a consistent internal format while maintaining
// a clean user-facing API under the safe-outputs namespace.

var safeJobsLog = logger.New("workflow:safe_jobs")

// SafeJobConfig defines a safe job configuration with GitHub Actions job properties
Expand All @@ -35,8 +53,13 @@ func HasSafeJobsEnabled(safeJobs map[string]*SafeJobConfig) bool {
}

// parseSafeJobsConfig parses safe-jobs configuration from a frontmatter map.
// This is an internal helper function that expects a map with a "safe-jobs" key.
// User workflows should use "safe-outputs.jobs" syntax; the top-level "safe-jobs" key is NOT supported.
//
// INTERNAL USE ONLY: This is an internal helper function that expects a map with a "safe-jobs" key
// for parsing purposes. This key is NOT user-facing and NOT in the schema.
//
// Users should configure custom safe output jobs via the "safe-outputs.jobs:" frontmatter syntax,
// which is then extracted by extractSafeJobsFromFrontmatter() and transformed into the internal
// "safe-jobs" format for parsing. The top-level "safe-jobs:" key is not supported in user workflows.
func (c *Compiler) parseSafeJobsConfig(frontmatter map[string]any) map[string]*SafeJobConfig {
safeJobsSection, exists := frontmatter["safe-jobs"]
if !exists {
Expand Down Expand Up @@ -292,8 +315,13 @@ func (c *Compiler) buildSafeJobs(data *WorkflowData, threatDetectionEnabled bool
return safeJobNames, nil
}

// extractSafeJobsFromFrontmatter extracts safe-jobs configuration from frontmatter.
// Only checks the safe-outputs.jobs location. The old top-level "safe-jobs" syntax is NOT supported.
// extractSafeJobsFromFrontmatter extracts custom safe output jobs from the user-facing
// "safe-outputs.jobs:" frontmatter configuration and transforms them into the internal
// SafeJobConfig format.
//
// USER-FACING API: Users configure custom safe output jobs via "safe-outputs.jobs:" in frontmatter.
// INTERNAL DETAIL: The extracted configuration is temporarily wrapped with a "safe-jobs" key
// for internal parsing by parseSafeJobsConfig(). This internal key is not user-facing.
func extractSafeJobsFromFrontmatter(frontmatter map[string]any) map[string]*SafeJobConfig {
// Check location: safe-outputs.jobs
if safeOutputs, exists := frontmatter["safe-outputs"]; exists {
Expand Down
8 changes: 5 additions & 3 deletions pkg/workflow/safe_jobs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
func TestParseSafeJobsConfig(t *testing.T) {
c := NewCompiler(false, "", "test")

// Test parseSafeJobsConfig internal function which expects a "safe-jobs" key.
// Note: User workflows should use "safe-outputs.jobs" syntax; this test validates
// the internal parsing logic used by extractSafeJobsFromFrontmatter and safe_outputs.go.
// Test parseSafeJobsConfig internal function which expects a "safe-jobs" key for parsing.
// NOTE: The "safe-jobs" key is INTERNAL ONLY and not user-facing. Users configure custom
// safe output jobs via "safe-outputs.jobs:" in frontmatter. This test validates the internal
// parsing logic used by extractSafeJobsFromFrontmatter() which transforms the user-facing
// "safe-outputs.jobs:" configuration into the internal "safe-jobs" format.
frontmatter := map[string]any{
"safe-jobs": map[string]any{
"deploy": map[string]any{
Expand Down