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
1 change: 0 additions & 1 deletion .github/aw/github-agentic-workflows.md

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

6 changes: 0 additions & 6 deletions actions/setup/js/generate_observability_summary.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,6 @@ function buildObservabilitySummary(data) {
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good simplification — removing the GH_AW_OBSERVABILITY_JOB_SUMMARY mode gate makes the behavior cleaner. The summary is now always generated when called, which aligns with the new OTLP-gated invocation in the compiler.


async function main(core) {
const mode = process.env.GH_AW_OBSERVABILITY_JOB_SUMMARY || "";
if (mode !== "on") {
core.info(`Skipping observability summary: mode=${mode || "unset"}`);
return;
}

const data = collectObservabilityData();
const markdown = buildObservabilitySummary(data);
await core.summary.addRaw(markdown).write();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice simplification — removing the GH_AW_OBSERVABILITY_JOB_SUMMARY mode gate makes main() unconditionally run. Since this is now always-on, it might be worth adding a comment here (e.g., // Called only when OTLP is configured; see compiler_orchestrator_workflow.go) so future readers understand the guard moved to the compiler rather than the runtime.

Expand Down
11 changes: 3 additions & 8 deletions actions/setup/js/generate_observability_summary.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ describe("generate_observability_summary.cjs", () => {
beforeEach(async () => {
vi.clearAllMocks();
fs.mkdirSync("/tmp/gh-aw/mcp-logs", { recursive: true });
process.env.GH_AW_OBSERVABILITY_JOB_SUMMARY = "on";
module = await import("./generate_observability_summary.cjs");
});

afterEach(() => {
delete process.env.GH_AW_OBSERVABILITY_JOB_SUMMARY;
for (const path of ["/tmp/gh-aw/aw_info.json", "/tmp/gh-aw/agent_output.json", "/tmp/gh-aw/mcp-logs/gateway.jsonl", "/tmp/gh-aw/mcp-logs/rpc-messages.jsonl"]) {
if (fs.existsSync(path)) {
fs.unlinkSync(path);
Expand Down Expand Up @@ -87,13 +85,10 @@ describe("generate_observability_summary.cjs", () => {
expect(summary).toContain("- **trace id**: 12345678901-1");
});

it("skips summary generation when opt-in mode is disabled", async () => {
process.env.GH_AW_OBSERVABILITY_JOB_SUMMARY = "off";

it("always generates summary regardless of env vars", async () => {
await module.main(mockCore);

expect(mockCore.summary.addRaw).not.toHaveBeenCalled();
expect(mockCore.summary.write).not.toHaveBeenCalled();
expect(mockCore.info).toHaveBeenCalledWith("Skipping observability summary: mode=off");
expect(mockCore.summary.addRaw).toHaveBeenCalledTimes(1);
expect(mockCore.summary.write).toHaveBeenCalledTimes(1);
});
});
9 changes: 1 addition & 8 deletions docs/public/editor/autocomplete-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1646,14 +1646,7 @@
"observability": {
"type": "object",
"desc": "Optional observability output settings for workflow runs.",
"children": {
"job-summary": {
"type": "string",
"desc": "If set to 'on', append a compact observability section to the GitHub Actions job summary.",
"enum": ["on", "off"],
"leaf": true
}
}
"children": {}
},
Comment on lines 1646 to 1650
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

observability autocomplete entries currently have an empty children object, which removes completion for the still-supported observability.otlp config (and its endpoint/headers). Please add otlp (and its subkeys) back to the autocomplete schema so editor suggestions match main_workflow_schema.json.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👋 Smoke test agent here — just verifying reply functionality works! Great catch on the autocomplete regression.

📰 BREAKING: Report filed by Smoke Copilot · ● 1.1M

"bots": {
"type": "array",
Expand Down
6 changes: 1 addition & 5 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -5905,11 +5905,7 @@ secret-masking:

# Optional observability output settings for workflow runs.
# (optional)
observability:
# If set to 'on', append a compact observability section to the GitHub Actions job
# summary. Defaults to off when omitted.
# (optional)
job-summary: "on"
observability: {}

Comment on lines 5906 to 5909
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

The frontmatter-full.md reference now shows observability: {} which implies there are no configurable observability options. Since the schema still supports observability.otlp.endpoint/headers, this section should document those fields (similar to other nested objects) to avoid misleading readers.

Copilot uses AI. Check for mistakes.
# Allow list of bot identifiers that can trigger the workflow even if they don't
# meet the required role permissions. When the actor is in this list, the bot must
Expand Down
13 changes: 13 additions & 0 deletions pkg/parser/import_field_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type importAccumulator struct {
secretMaskingBuilder strings.Builder
postStepsBuilder strings.Builder
jobsBuilder strings.Builder // Jobs from imported YAML workflows
observabilityBuilder strings.Builder // observability config (first-wins for OTLP endpoint)
engines []string
safeOutputs []string
mcpScripts []string
Expand Down Expand Up @@ -350,6 +351,17 @@ func (acc *importAccumulator) extractAllImportFields(content []byte, item import
}
}

// Extract observability from imported file (first-wins: only set if not yet populated).
// This ensures an imported workflow's OTLP config is visible to injectOTLPConfig even
// when the main workflow's frontmatter does not contain an observability section.
if acc.observabilityBuilder.Len() == 0 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new MergedObservability field extraction from imported workflows is a great improvement. Consider adding a comment explaining the merge strategy (e.g., last-write-wins vs deep merge) when multiple imports each define observability.otlp.endpoint, since conflicts could silently drop a configured endpoint.

obsContent, obsErr := extractFieldJSONFromMap(fm, "observability", "{}")
if obsErr == nil && obsContent != "" && obsContent != "{}" {
acc.observabilityBuilder.WriteString(obsContent)
log.Printf("Extracted observability from import: %s", item.fullPath)
}
}

return nil
}

Expand Down Expand Up @@ -381,6 +393,7 @@ func (acc *importAccumulator) toImportsResult(topologicalOrder []string) *Import
MergedCaches: acc.caches,
MergedJobs: acc.jobsBuilder.String(),
MergedFeatures: acc.features,
MergedObservability: acc.observabilityBuilder.String(),
ImportedFiles: topologicalOrder,
AgentFile: acc.agentFile,
AgentImportSpec: acc.agentImportSpec,
Expand Down
1 change: 1 addition & 0 deletions pkg/parser/import_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type ImportsResult struct {
MergedCaches []string // Merged cache configurations from all imports (appended in order)
MergedJobs string // Merged jobs from imported YAML workflows (JSON format)
MergedFeatures []map[string]any // Merged features configuration from all imports (parsed YAML structures)
MergedObservability string // Observability config (JSON) from first import that defines it (first-wins)
ImportedFiles []string // List of imported file paths (for manifest)
AgentFile string // Path to custom agent file (if imported)
AgentImportSpec string // Original import specification for agent file (e.g., "owner/repo/path@ref")
Expand Down
22 changes: 0 additions & 22 deletions pkg/parser/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,25 +194,3 @@ func TestGetSafeOutputTypeKeys(t *testing.T) {
}
}
}

func TestValidateMainWorkflowFrontmatterWithSchema_AllowsObservabilityJobSummary(t *testing.T) {
frontmatter := map[string]any{
"on": "push",
"observability": map[string]any{
"job-summary": "on",
},
}

tempFile := "/tmp/gh-aw/test_observability_frontmatter.md"
if err := os.MkdirAll("/tmp/gh-aw", 0755); err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
if err := os.WriteFile(tempFile, []byte("---\non: push\nobservability:\n job-summary: on\n---\n"), 0644); err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer os.Remove(tempFile)

if err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, tempFile); err != nil {
t.Fatalf("Expected observability config to validate, got: %v", err)
}
}
5 changes: 0 additions & 5 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8324,11 +8324,6 @@
"type": "object",
"description": "Optional observability output settings for workflow runs.",
"properties": {
"job-summary": {
"type": "string",
"enum": ["on", "off"],
"description": "If set to 'on', append a compact observability section to the GitHub Actions job summary. Defaults to off when omitted."
},
"otlp": {
"type": "object",
"description": "OTLP (OpenTelemetry Protocol) trace export configuration.",
Expand Down
12 changes: 12 additions & 0 deletions pkg/workflow/compiler_orchestrator_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error)
// Extract YAML configuration sections from frontmatter
c.extractYAMLSections(result.Frontmatter, workflowData)

// Merge observability config from imports into RawFrontmatter so that injectOTLPConfig
// can see an OTLP endpoint defined in an imported workflow (first-wins from imports).
if obs := engineSetup.importsResult.MergedObservability; obs != "" {
if _, hasObs := workflowData.RawFrontmatter["observability"]; !hasObs {
var obsMap map[string]any
if err := json.Unmarshal([]byte(obs), &obsMap); err == nil {
workflowData.RawFrontmatter["observability"] = obsMap
orchestratorWorkflowLog.Printf("Merged observability config from imports into RawFrontmatter")
}
}
}

// Inject OTLP configuration: add endpoint domain to firewall allowlist and
// set OTEL env vars in the workflow env block (no-op when not configured).
c.injectOTLPConfig(workflowData)
Expand Down
46 changes: 15 additions & 31 deletions pkg/workflow/compiler_yaml_ai_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,6 @@ import (
"github.com/github/gh-aw/pkg/constants"
)

func getObservabilityJobSummaryMode(data *WorkflowData) string {
if data == nil {
return ""
}

mode := ""
if data.ParsedFrontmatter != nil && data.ParsedFrontmatter.Observability != nil {
mode = data.ParsedFrontmatter.Observability.JobSummary
}

if mode == "" && data.RawFrontmatter != nil {
if rawObservability, ok := data.RawFrontmatter["observability"].(map[string]any); ok {
if rawMode, ok := rawObservability["job-summary"].(string); ok {
mode = rawMode
}
}
}

if mode == "off" {
return ""
}

return mode
}

// generateEngineExecutionSteps generates the GitHub Actions steps for executing the AI engine
func (c *Compiler) generateEngineExecutionSteps(yaml *strings.Builder, data *WorkflowData, engine CodingAgentEngine, logFile string) {

Expand Down Expand Up @@ -119,21 +94,19 @@ func (c *Compiler) generateMCPGatewayLogParsing(yaml *strings.Builder) {
yaml.WriteString(" await main();\n")
}

// generateObservabilitySummary generates an opt-in step that synthesizes a compact
// generateObservabilitySummary generates a step that synthesizes a compact
// observability section for the GitHub Actions step summary from existing runtime files.
// The step is only emitted when OTLP is configured in the workflow.
func (c *Compiler) generateObservabilitySummary(yaml *strings.Builder, data *WorkflowData) {
mode := getObservabilityJobSummaryMode(data)
if mode == "" {
if !isOTLPEnabled(data) {
return
}

compilerYamlLog.Printf("Generating observability step summary: mode=%s", mode)
compilerYamlLog.Print("Generating observability step summary")

yaml.WriteString(" - name: Generate observability summary\n")
yaml.WriteString(" if: always()\n")
fmt.Fprintf(yaml, " uses: %s\n", GetActionPin("actions/github-script"))
yaml.WriteString(" env:\n")
fmt.Fprintf(yaml, " GH_AW_OBSERVABILITY_JOB_SUMMARY: %q\n", mode)
yaml.WriteString(" with:\n")
yaml.WriteString(" script: |\n")
yaml.WriteString(" const { setupGlobals } = require('" + SetupActionDestination + "/setup_globals.cjs');\n")
Expand All @@ -142,6 +115,17 @@ func (c *Compiler) generateObservabilitySummary(yaml *strings.Builder, data *Wor
yaml.WriteString(" await main(core);\n")
}

// isOTLPEnabled returns true when OTLP has been configured in the workflow (including
// imported frontmatter). It checks whether injectOTLPConfig has already written the
// OTEL_EXPORTER_OTLP_ENDPOINT env var into workflowData.Env, which is the authoritative
// result of OTLP detection after all frontmatter (main + imports) has been processed.
func isOTLPEnabled(data *WorkflowData) bool {
if data == nil {
return false
}
return strings.Contains(data.Env, "OTEL_EXPORTER_OTLP_ENDPOINT")
}

// generateStopMCPGateway generates a step that stops the MCP gateway process using its PID from step output
// It passes the gateway port and API key to enable graceful shutdown via /close endpoint
func (c *Compiler) generateStopMCPGateway(yaml *strings.Builder, data *WorkflowData) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/compiler_yaml_main_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ func (c *Compiler) generateMainJobSteps(yaml *strings.Builder, data *WorkflowDat
artifactPaths = append(artifactPaths, "/tmp/gh-aw/"+constants.TokenUsageFilename)
}

// Optionally synthesize a compact observability section from runtime artifacts.
// Synthesize a compact observability section from runtime artifacts when OTLP is enabled.
c.generateObservabilitySummary(yaml, data)

// Collect agent stdio logs path for unified upload
Expand Down
3 changes: 1 addition & 2 deletions pkg/workflow/frontmatter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,7 @@ type OTLPConfig struct {

// ObservabilityConfig represents workflow observability options.
type ObservabilityConfig struct {
JobSummary string `json:"job-summary,omitempty"`
OTLP *OTLPConfig `json:"otlp,omitempty"`
OTLP *OTLPConfig `json:"otlp,omitempty"`
}

// FrontmatterConfig represents the structured configuration from workflow frontmatter
Expand Down
8 changes: 5 additions & 3 deletions pkg/workflow/frontmatter_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ func TestParseFrontmatterConfig(t *testing.T) {
t.Run("handles observability configuration", func(t *testing.T) {
frontmatter := map[string]any{
"observability": map[string]any{
"job-summary": "on",
"otlp": map[string]any{
"endpoint": "https://traces.example.com",
},
},
}

Expand All @@ -261,8 +263,8 @@ func TestParseFrontmatterConfig(t *testing.T) {
t.Fatal("Observability should not be nil")
}

if config.Observability.JobSummary != "on" {
t.Errorf("JobSummary = %q, want %q", config.Observability.JobSummary, "on")
if config.Observability.OTLP == nil {
t.Fatal("OTLP should not be nil")
}
Comment on lines 248 to 268
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

This test was updated to use an OTLP example, but it only asserts config.Observability.OTLP != nil. To keep coverage for OTLP parsing behavior, please also assert the parsed values (e.g., endpoint and/or headers) match the input.

Copilot uses AI. Check for mistakes.
})

Expand Down
Loading
Loading