Skip to content
Merged
29 changes: 15 additions & 14 deletions .github/workflows/dead-code-remover.lock.yml

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

2 changes: 2 additions & 0 deletions pkg/workflow/compiler_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func (c *Compiler) generateWorkflowHeader(yaml *strings.Builder, data *WorkflowD
agentInfo.DetectionAgentID = data.SafeOutputs.ThreatDetection.EngineConfig.ID
agentInfo.DetectionAgentModel = data.SafeOutputs.ThreatDetection.EngineConfig.Model
}
agentInfo.EngineVersions = collectEngineVersionsForMetadata(data)
agentInfo.AgentImageRunner = resolveAgentImageRunnerIdentifier(data.RawFrontmatter)
metadata := GenerateLockMetadata(LockHashInfo{FrontmatterHash: frontmatterHash, BodyHash: bodyHash}, data.StopTime, c.effectiveStrictMode(data.RawFrontmatter), agentInfo)
metadataJSON, err := metadata.ToJSON()
if err != nil {
Expand Down
66 changes: 66 additions & 0 deletions pkg/workflow/compiler_yaml_lookups.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package workflow

import (
"encoding/json"
"regexp"
"strings"

Expand Down Expand Up @@ -159,3 +160,68 @@ func versionToGitRef(version string) string {
compilerYamlLookupsLog.Printf("Using version as git ref: %s -> %s", version, clean)
return clean
}

// collectEngineVersionsForMetadata returns engine version metadata for gh-aw lock files.
// It includes default versions for built-in engines, applies explicit overrides for configured
// engines, and includes copilot-sdk only when enabled.
func collectEngineVersionsForMetadata(data *WorkflowData) map[string]string {
versions := map[string]string{
string(constants.CopilotEngine): string(constants.DefaultCopilotVersion),
string(constants.ClaudeEngine): string(constants.DefaultClaudeCodeVersion),
string(constants.CodexEngine): string(constants.DefaultCodexVersion),
string(constants.GeminiEngine): string(constants.DefaultGeminiVersion),
string(constants.AntigravityEngine): string(constants.DefaultAntigravityVersion),
string(constants.OpenCodeEngine): string(constants.DefaultOpenCodeVersion),
string(constants.CrushEngine): string(constants.DefaultCrushVersion),
string(constants.PiEngine): string(constants.DefaultPiVersion),
}

if data == nil {
return versions
}

applyMetadataEngineVersionOverrides(versions, data.EngineConfig, data.AI)
if data.SafeOutputs != nil && data.SafeOutputs.ThreatDetection != nil {
applyMetadataEngineVersionOverrides(versions, data.SafeOutputs.ThreatDetection.EngineConfig, "")
}

return versions
}

func applyMetadataEngineVersionOverrides(versions map[string]string, engineConfig *EngineConfig, fallbackEngineID string) {
if engineConfig == nil {
return
}

engineID := strings.TrimSpace(engineConfig.ID)
if engineID == "" {
engineID = strings.TrimSpace(fallbackEngineID)
}
if engineID != "" && strings.TrimSpace(engineConfig.Version) != "" {
versions[engineID] = strings.TrimSpace(engineConfig.Version)
}
if engineID == string(constants.CopilotEngine) && engineConfig.CopilotSDK {
versions["copilot-sdk"] = string(constants.DefaultCopilotSDKVersion)
}
}

// resolveAgentImageRunnerIdentifier returns a stable identifier for the configured runs-on value.
// For string values it returns the value directly; for array/object values it returns JSON.
func resolveAgentImageRunnerIdentifier(frontmatter map[string]any) string {
if frontmatter == nil {
return ""
}
runsOn, exists := frontmatter["runs-on"]
if !exists || runsOn == nil {
return ""
}
if rawRunner, ok := runsOn.(string); ok {
return strings.TrimSpace(rawRunner)
}

serialized, err := json.Marshal(runsOn)
if err != nil {
return ""
}
return string(serialized)
}
43 changes: 43 additions & 0 deletions pkg/workflow/compiler_yaml_lookups_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//go:build !integration

package workflow

import "testing"

func TestCollectEngineVersionsForMetadata(t *testing.T) {
data := &WorkflowData{
AI: "copilot",
EngineConfig: &EngineConfig{
ID: "copilot",
Version: "1.2.3-custom",
CopilotSDK: true,
},
}

versions := collectEngineVersionsForMetadata(data)
if versions["copilot"] != "1.2.3-custom" {
t.Fatalf("Expected copilot override version, got: %q", versions["copilot"])
}
if versions["copilot-sdk"] == "" {
t.Fatal("Expected copilot-sdk version when copilot-sdk is enabled")
}
if versions["claude"] == "" {
t.Fatal("Expected default claude version in metadata map")
}
}

func TestResolveAgentImageRunnerIdentifier(t *testing.T) {
t.Run("string value", func(t *testing.T) {
frontmatter := map[string]any{"runs-on": "ubuntu-latest"}
if got := resolveAgentImageRunnerIdentifier(frontmatter); got != "ubuntu-latest" {
t.Fatalf("Expected string runner identifier, got: %q", got)
}
})

t.Run("array value", func(t *testing.T) {
frontmatter := map[string]any{"runs-on": []any{"self-hosted", "linux"}}
if got := resolveAgentImageRunnerIdentifier(frontmatter); got != `["self-hosted","linux"]` {
t.Fatalf("Expected serialized array runner identifier, got: %q", got)
}
})
}
60 changes: 60 additions & 0 deletions pkg/workflow/compiler_yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package workflow

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -1438,3 +1439,62 @@ Test prompt.
})
}
}

func TestCompileWorkflowMetadataIncludesEngineVersionsAndRunnerIdentifier(t *testing.T) {
tmpDir := testutil.TempDir(t, "lock-metadata-engine-versions")

workflowContent := `---
engine:
id: copilot
copilot-sdk: true
runs-on:
- self-hosted
- linux
on: issues
---
# Test Workflow

Test prompt.
`
workflowPath := filepath.Join(tmpDir, "metadata-engine-versions.md")
if err := os.WriteFile(workflowPath, []byte(workflowContent), 0o644); err != nil {
t.Fatalf("Failed to write workflow file: %v", err)
}

compiler := NewCompiler()
if err := compiler.CompileWorkflow(workflowPath); err != nil {
t.Fatalf("Failed to compile workflow: %v", err)
}

lockFile := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml"
lockContent, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("Failed to read lock file: %v", err)
}

var metadataLine string
for line := range strings.SplitSeq(string(lockContent), "\n") {
if strings.HasPrefix(line, "# gh-aw-metadata: ") {
metadataLine = strings.TrimPrefix(line, "# gh-aw-metadata: ")
break
}
}
if metadataLine == "" {
t.Fatal("Could not find gh-aw-metadata in lock file")
}

var metadata LockMetadata
if err := json.Unmarshal([]byte(metadataLine), &metadata); err != nil {
t.Fatalf("Failed to parse lock metadata JSON: %v", err)
}

if got := metadata.EngineVersions["copilot"]; got == "" {
t.Fatal("Expected copilot version in metadata engine_versions")
}
if got := metadata.EngineVersions["copilot-sdk"]; got == "" {
t.Fatal("Expected copilot-sdk version in metadata engine_versions when copilot-sdk is enabled")
}
if metadata.AgentImageRunner != `["self-hosted","linux"]` {
t.Fatalf("Expected serialized array runner identifier, got: %q", metadata.AgentImageRunner)
}
}
6 changes: 6 additions & 0 deletions pkg/workflow/lock_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type LockMetadata struct {
AgentModel string `json:"agent_model,omitempty"`
DetectionAgentID string `json:"detection_agent_id,omitempty"`
DetectionAgentModel string `json:"detection_agent_model,omitempty"`
EngineVersions map[string]string `json:"engine_versions,omitempty"`
AgentImageRunner string `json:"agent_image_runner,omitempty"`
}

// AgentMetadataInfo holds agent and detection agent information for embedding in lock file metadata
Expand All @@ -51,6 +53,8 @@ type AgentMetadataInfo struct {
AgentModel string
DetectionAgentID string
DetectionAgentModel string
EngineVersions map[string]string
AgentImageRunner string
}

// SupportedSchemaVersions lists all schema versions this build can consume
Expand Down Expand Up @@ -126,6 +130,8 @@ func GenerateLockMetadata(hashInfo LockHashInfo, stopTime string, strict bool, a
AgentModel: agentInfo.AgentModel,
DetectionAgentID: agentInfo.DetectionAgentID,
DetectionAgentModel: agentInfo.DetectionAgentModel,
EngineVersions: agentInfo.EngineVersions,
AgentImageRunner: agentInfo.AgentImageRunner,
}

// Include compiler version only for release builds
Expand Down
21 changes: 20 additions & 1 deletion pkg/workflow/lock_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,11 @@ func TestGenerateLockMetadataWithAgentInfo(t *testing.T) {
AgentModel: "gpt-5",
DetectionAgentID: "copilot",
DetectionAgentModel: "gpt-5.1-codex-mini",
EngineVersions: map[string]string{
"copilot": "1.0.57",
"claude": "2.1.160",
},
AgentImageRunner: "ubuntu-latest",
}
metadata := GenerateLockMetadata(LockHashInfo{FrontmatterHash: hash}, "", false, agentInfo)

Expand All @@ -638,6 +643,8 @@ func TestGenerateLockMetadataWithAgentInfo(t *testing.T) {
assert.Equal(t, "gpt-5", metadata.AgentModel, "Should preserve agent model")
assert.Equal(t, "copilot", metadata.DetectionAgentID, "Should preserve detection agent ID")
assert.Equal(t, "gpt-5.1-codex-mini", metadata.DetectionAgentModel, "Should preserve detection agent model")
assert.Equal(t, "1.0.57", metadata.EngineVersions["copilot"], "Should preserve engine versions")
assert.Equal(t, "ubuntu-latest", metadata.AgentImageRunner, "Should preserve agent image runner")
}

func TestGenerateLockMetadataAgentFieldsOmittedWhenEmpty(t *testing.T) {
Expand All @@ -661,6 +668,12 @@ func TestLockMetadataToJSONWithAgentFields(t *testing.T) {
AgentModel: "claude-sonnet-4.5",
DetectionAgentID: "copilot",
DetectionAgentModel: "gpt-5.1-codex-mini",
EngineVersions: map[string]string{
"claude": "2.1.160",
"copilot": "1.0.57",
"copilot-sdk": "1.0.0",
},
AgentImageRunner: `["self-hosted","linux"]`,
}

json, err := metadata.ToJSON()
Expand All @@ -672,10 +685,12 @@ func TestLockMetadataToJSONWithAgentFields(t *testing.T) {
assert.Contains(t, json, `"agent_model":"claude-sonnet-4.5"`)
assert.Contains(t, json, `"detection_agent_id":"copilot"`)
assert.Contains(t, json, `"detection_agent_model":"gpt-5.1-codex-mini"`)
assert.Contains(t, json, `"engine_versions":{"claude":"2.1.160","copilot":"1.0.57","copilot-sdk":"1.0.0"}`)
assert.Contains(t, json, `"agent_image_runner":"[\"self-hosted\",\"linux\"]"`)
}

func TestExtractMetadataWithAgentFields(t *testing.T) {
content := `# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"abc123","strict":true,"agent_id":"copilot","agent_model":"gpt-5","detection_agent_id":"copilot","detection_agent_model":"gpt-5.1-codex-mini"}
content := `# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"abc123","strict":true,"agent_id":"copilot","agent_model":"gpt-5","detection_agent_id":"copilot","detection_agent_model":"gpt-5.1-codex-mini","engine_versions":{"copilot":"1.0.57","claude":"2.1.160","copilot-sdk":"1.0.0"},"agent_image_runner":"[\"self-hosted\",\"linux\"]"}
name: test
`
metadata, isLegacy, err := ExtractMetadataFromLockFile(content)
Expand All @@ -689,4 +704,8 @@ name: test
assert.Equal(t, "gpt-5", metadata.AgentModel, "Should extract agent model")
assert.Equal(t, "copilot", metadata.DetectionAgentID, "Should extract detection agent ID")
assert.Equal(t, "gpt-5.1-codex-mini", metadata.DetectionAgentModel, "Should extract detection agent model")
assert.Equal(t, "1.0.57", metadata.EngineVersions["copilot"], "Should extract engine versions")
assert.Equal(t, "2.1.160", metadata.EngineVersions["claude"], "Should extract engine versions")
assert.Equal(t, "1.0.0", metadata.EngineVersions["copilot-sdk"], "Should extract copilot-sdk version")
assert.Equal(t, `["self-hosted","linux"]`, metadata.AgentImageRunner, "Should extract agent image runner")
}
Loading