From aa782a27fa6b90e222c59f772b7ca5fd062df7a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:32:30 +0000 Subject: [PATCH 01/10] Initial plan From 65a97ddbd7e3b645573323662e936a0247beb1b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:40:25 +0000 Subject: [PATCH 02/10] Plan runs-on jobs fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/aw/actions-lock.json | 5 +++++ .github/workflows/release.lock.yml | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 334d0b7954e..828732d7e61 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -138,6 +138,11 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, + "docker/metadata-action@v6": { + "repo": "docker/metadata-action", + "version": "v6", + "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" + }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 345e02e8eff..d9aaa5d6eba 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6a6bd39e2339b2b176862a0fceb3dc70c2440fe79b8689f4e476134a57bed33a","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"030e881283bb7a6894de51c315a6bfe6a94e05cf","version":"v6.0.0 (source v6)"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9","version":"v6"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -49,7 +49,7 @@ # - anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 # - docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 # - docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 -# - docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) +# - docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 # - docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 (source v4) # # Container images used: @@ -1541,7 +1541,7 @@ jobs: username: ${{ github.actor }} - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 with: images: ghcr.io/${{ github.repository }} tags: | From cbd0cede08ded1900ce2f5299e14b9d6267f769d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:48:12 +0000 Subject: [PATCH 03/10] Fix job runs-on object schema Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/actionpins/data/action_pins.json | 5 ++ pkg/parser/schema_location_test.go | 23 ++++++ pkg/parser/schemas/main_workflow_schema.json | 86 ++++++++------------ pkg/workflow/data/action_pins.json | 5 ++ pkg/workflow/frontmatter_serialization.go | 2 +- pkg/workflow/frontmatter_types.go | 2 +- pkg/workflow/frontmatter_types_test.go | 33 ++++++++ 7 files changed, 104 insertions(+), 52 deletions(-) diff --git a/pkg/actionpins/data/action_pins.json b/pkg/actionpins/data/action_pins.json index 334d0b7954e..828732d7e61 100644 --- a/pkg/actionpins/data/action_pins.json +++ b/pkg/actionpins/data/action_pins.json @@ -138,6 +138,11 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, + "docker/metadata-action@v6": { + "repo": "docker/metadata-action", + "version": "v6", + "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" + }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/pkg/parser/schema_location_test.go b/pkg/parser/schema_location_test.go index 9826c4471ae..3012c311748 100644 --- a/pkg/parser/schema_location_test.go +++ b/pkg/parser/schema_location_test.go @@ -275,6 +275,29 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AdditionalProperti } } +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnObjectForm(t *testing.T) { + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "jobs": map[string]any{ + "my-prefetch": map[string]any{ + "runs-on": map[string]any{ + "group": "arc-custom", + }, + "steps": []any{ + map[string]any{ + "run": "echo hello", + }, + }, + }, + }, + } + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md") + if err != nil { + t.Fatalf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() unexpected error = %v", err) + } +} + func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsAllowedBaseBranchesInCreatePullRequest(t *testing.T) { frontmatter := map[string]any{ "on": map[string]any{ diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 983304a8b96..f51fc4b5c99 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2750,24 +2750,7 @@ "description": "Name of the job" }, "runs-on": { - "oneOf": [ - { - "type": "string", - "description": "Runner type as string" - }, - { - "type": "array", - "description": "Runner type as array", - "items": { - "type": "string" - } - }, - { - "type": "object", - "description": "Runner type as object", - "additionalProperties": false - } - ], + "$ref": "#/$defs/github_actions_runs_on", "description": "Runner label or environment where the job executes. Can be a string (single runner) or array (multiple runner requirements)." }, "steps": { @@ -3086,38 +3069,7 @@ } }, "runs-on": { - "description": "Runner type for workflow execution (GitHub Actions standard field). Supports multiple forms: simple string for single runner label (e.g., 'ubuntu-latest'), array for runner selection with fallbacks, or object for GitHub-hosted runner groups with specific labels. For agentic workflows, runner selection matters when AI workloads require specific compute resources or when using self-hosted runners with specialized capabilities. Typically configured at the job level instead. See https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job", - "oneOf": [ - { - "type": "string", - "description": "Simple runner label string. Use for standard GitHub-hosted runners (e.g., 'ubuntu-latest', 'windows-latest', 'macos-latest') or self-hosted runner labels. Most common form for agentic workflows." - }, - { - "type": "array", - "description": "Array of runner labels for selection with fallbacks. GitHub Actions will use the first available runner that matches any label in the array. Useful for high-availability setups or when multiple runner types are acceptable.", - "items": { - "type": "string" - } - }, - { - "type": "object", - "description": "Runner group configuration for GitHub-hosted runners. Use this form to target specific runner groups (e.g., larger runners with more CPU/memory) or self-hosted runner pools with specific label requirements. Agentic workflows may benefit from larger runners for complex AI processing tasks.", - "additionalProperties": false, - "properties": { - "group": { - "type": "string", - "description": "Runner group name for self-hosted runners or GitHub-hosted runner groups" - }, - "labels": { - "type": "array", - "description": "List of runner labels for self-hosted runners or GitHub-hosted runner selection", - "items": { - "type": "string" - } - } - } - } - ], + "$ref": "#/$defs/github_actions_runs_on", "examples": [ "ubuntu-latest", [ @@ -11743,6 +11695,40 @@ } ], "$defs": { + "github_actions_runs_on": { + "description": "Runner type for workflow execution (GitHub Actions standard field). Supports multiple forms: simple string for single runner label (e.g., 'ubuntu-latest'), array for runner selection with fallbacks, or object for GitHub-hosted runner groups with specific labels. For agentic workflows, runner selection matters when AI workloads require specific compute resources or when using self-hosted runners with specialized capabilities. Typically configured at the job level instead. See https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job", + "oneOf": [ + { + "type": "string", + "description": "Simple runner label string. Use for standard GitHub-hosted runners (e.g., 'ubuntu-latest', 'windows-latest', 'macos-latest') or self-hosted runner labels. Most common form for agentic workflows." + }, + { + "type": "array", + "description": "Array of runner labels for selection with fallbacks. GitHub Actions will use the first available runner that matches any label in the array. Useful for high-availability setups or when multiple runner types are acceptable.", + "items": { + "type": "string" + } + }, + { + "type": "object", + "description": "Runner group configuration for GitHub-hosted runners. Use this form to target specific runner groups (e.g., larger runners with more CPU/memory) or self-hosted runner pools with specific label requirements. Agentic workflows may benefit from larger runners for complex AI processing tasks.", + "additionalProperties": false, + "properties": { + "group": { + "type": "string", + "description": "Runner group name for self-hosted runners or GitHub-hosted runner groups" + }, + "labels": { + "type": "array", + "description": "List of runner labels for self-hosted runners or GitHub-hosted runner selection", + "items": { + "type": "string" + } + } + } + } + ] + }, "github_toolset_name": { "type": "string", "description": "A GitHub MCP server toolset name that enables a specific group of GitHub API functionalities.", diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 334d0b7954e..828732d7e61 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -138,6 +138,11 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, + "docker/metadata-action@v6": { + "repo": "docker/metadata-action", + "version": "v6", + "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" + }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/pkg/workflow/frontmatter_serialization.go b/pkg/workflow/frontmatter_serialization.go index 024b71f1be6..c244c7a6b43 100644 --- a/pkg/workflow/frontmatter_serialization.go +++ b/pkg/workflow/frontmatter_serialization.go @@ -180,7 +180,7 @@ func (fc *FrontmatterConfig) ToMap() map[string]any { } // Execution settings - if fc.RunsOn != "" { + if fc.RunsOn != nil { result["runs-on"] = fc.RunsOn } if fc.RunsOnSlim != "" { diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go index 5fb6872860e..b4880a46831 100644 --- a/pkg/workflow/frontmatter_types.go +++ b/pkg/workflow/frontmatter_types.go @@ -308,7 +308,7 @@ type FrontmatterConfig struct { Secrets map[string]any `json:"secrets,omitempty"` // Workflow execution settings - RunsOn string `json:"runs-on,omitempty"` + RunsOn any `json:"runs-on,omitempty"` // Supports string, array, or object GitHub Actions runner forms RunsOnSlim string `json:"runs-on-slim,omitempty"` // Runner for all framework/generated jobs (activation, safe-outputs, unlock, etc.) RunName string `json:"run-name,omitempty"` PreSteps []any `json:"pre-steps,omitempty"` // Pre-workflow steps (run before checkout) diff --git a/pkg/workflow/frontmatter_types_test.go b/pkg/workflow/frontmatter_types_test.go index 7566c4ef3bc..d070bad832b 100644 --- a/pkg/workflow/frontmatter_types_test.go +++ b/pkg/workflow/frontmatter_types_test.go @@ -342,6 +342,39 @@ func TestParseFrontmatterConfig(t *testing.T) { } }) + t.Run("handles top-level runs-on object form", func(t *testing.T) { + frontmatter := map[string]any{ + "runs-on": map[string]any{ + "group": "arc-custom", + "labels": []any{"self-hosted", "linux"}, + }, + } + + config, err := ParseFrontmatterConfig(frontmatter) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + runsOn, ok := config.RunsOn.(map[string]any) + if !ok { + t.Fatalf("RunsOn should be preserved as a map, got %T", config.RunsOn) + } + + if runsOn["group"] != "arc-custom" { + t.Errorf("RunsOn group = %v, want arc-custom", runsOn["group"]) + } + + reconstructed := config.ToMap() + reconstructedRunsOn, ok := reconstructed["runs-on"].(map[string]any) + if !ok { + t.Fatalf("reconstructed runs-on should be a map, got %T", reconstructed["runs-on"]) + } + + if reconstructedRunsOn["group"] != "arc-custom" { + t.Errorf("reconstructed runs-on group = %v, want arc-custom", reconstructedRunsOn["group"]) + } + }) + t.Run("preserves complex nested structures", func(t *testing.T) { frontmatter := map[string]any{ "safe-outputs": map[string]any{ From de06706c211ea87dde3fc49ee76018008c3aba55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:48:47 +0000 Subject: [PATCH 04/10] Drop unrelated action pin updates Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/actionpins/data/action_pins.json | 5 ----- pkg/workflow/data/action_pins.json | 5 ----- 2 files changed, 10 deletions(-) diff --git a/pkg/actionpins/data/action_pins.json b/pkg/actionpins/data/action_pins.json index 828732d7e61..334d0b7954e 100644 --- a/pkg/actionpins/data/action_pins.json +++ b/pkg/actionpins/data/action_pins.json @@ -138,11 +138,6 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, - "docker/metadata-action@v6": { - "repo": "docker/metadata-action", - "version": "v6", - "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" - }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 828732d7e61..334d0b7954e 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -138,11 +138,6 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, - "docker/metadata-action@v6": { - "repo": "docker/metadata-action", - "version": "v6", - "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" - }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", From 6f5aea43761cbca23a749602de03ea9038e6c9c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:53:10 +0000 Subject: [PATCH 05/10] Expand runs-on parser coverage Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/frontmatter_types_test.go | 84 ++++++++++++++++++-------- 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/pkg/workflow/frontmatter_types_test.go b/pkg/workflow/frontmatter_types_test.go index d070bad832b..abc084355d3 100644 --- a/pkg/workflow/frontmatter_types_test.go +++ b/pkg/workflow/frontmatter_types_test.go @@ -342,36 +342,72 @@ func TestParseFrontmatterConfig(t *testing.T) { } }) - t.Run("handles top-level runs-on object form", func(t *testing.T) { - frontmatter := map[string]any{ - "runs-on": map[string]any{ - "group": "arc-custom", - "labels": []any{"self-hosted", "linux"}, + t.Run("handles top-level runs-on forms", func(t *testing.T) { + tests := []struct { + name string + runsOn any + assertion func(t *testing.T, got any) + }{ + { + name: "string form", + runsOn: "ubuntu-latest", + assertion: func(t *testing.T, got any) { + parsed, ok := got.(string) + if !ok { + t.Fatalf("RunsOn should be preserved as a string, got %T", got) + } + if parsed != "ubuntu-latest" { + t.Errorf("RunsOn = %v, want ubuntu-latest", parsed) + } + }, + }, + { + name: "array form", + runsOn: []any{"self-hosted", "linux"}, + assertion: func(t *testing.T, got any) { + parsed, ok := got.([]any) + if !ok { + t.Fatalf("RunsOn should be preserved as a slice, got %T", got) + } + if len(parsed) != 2 || parsed[0] != "self-hosted" || parsed[1] != "linux" { + t.Errorf("RunsOn = %v, want [self-hosted linux]", parsed) + } + }, + }, + { + name: "object form", + runsOn: map[string]any{ + "group": "arc-custom", + "labels": []any{"self-hosted", "linux"}, + }, + assertion: func(t *testing.T, got any) { + parsed, ok := got.(map[string]any) + if !ok { + t.Fatalf("RunsOn should be preserved as a map, got %T", got) + } + if parsed["group"] != "arc-custom" { + t.Errorf("RunsOn group = %v, want arc-custom", parsed["group"]) + } + }, }, } - config, err := ParseFrontmatterConfig(frontmatter) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - runsOn, ok := config.RunsOn.(map[string]any) - if !ok { - t.Fatalf("RunsOn should be preserved as a map, got %T", config.RunsOn) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + frontmatter := map[string]any{ + "runs-on": tt.runsOn, + } - if runsOn["group"] != "arc-custom" { - t.Errorf("RunsOn group = %v, want arc-custom", runsOn["group"]) - } + config, err := ParseFrontmatterConfig(frontmatter) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - reconstructed := config.ToMap() - reconstructedRunsOn, ok := reconstructed["runs-on"].(map[string]any) - if !ok { - t.Fatalf("reconstructed runs-on should be a map, got %T", reconstructed["runs-on"]) - } + tt.assertion(t, config.RunsOn) - if reconstructedRunsOn["group"] != "arc-custom" { - t.Errorf("reconstructed runs-on group = %v, want arc-custom", reconstructedRunsOn["group"]) + reconstructed := config.ToMap() + tt.assertion(t, reconstructed["runs-on"]) + }) } }) From 8a77353324556267cd321c7344343c5c52954b5d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 14:13:29 +0000 Subject: [PATCH 06/10] docs(adr): add ADR-34007 for shared runs-on schema and any-typed frontmatter --- ...uns-on-schema-and-any-typed-frontmatter.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/adr/34007-shared-runs-on-schema-and-any-typed-frontmatter.md diff --git a/docs/adr/34007-shared-runs-on-schema-and-any-typed-frontmatter.md b/docs/adr/34007-shared-runs-on-schema-and-any-typed-frontmatter.md new file mode 100644 index 00000000000..30d4e348961 --- /dev/null +++ b/docs/adr/34007-shared-runs-on-schema-and-any-typed-frontmatter.md @@ -0,0 +1,86 @@ +# ADR-34007: Shared `runs-on` Schema Across Top-Level and Jobs With `any`-Typed Frontmatter Field + +**Date**: 2026-05-22 +**Status**: Draft +**Deciders**: Unknown + +--- + +## Part 1 — Narrative (Human-Friendly) + +### Context + +The custom workflow schema in `pkg/parser/schemas/main_workflow_schema.json` defined two independent `runs-on` shapes: a rich top-level definition that accepted string, array, and object (`{ group, labels }`) forms, and a duplicate `jobs.*.runs-on` definition whose object branch had `additionalProperties: false` with no declared properties, so any object value was rejected at schema-validation time. The compiler and runtime already understood the object form everywhere (it round-trips through GitHub Actions natively), so the gap was purely a validation artefact that blocked users from writing `jobs..runs-on: { group: arc-custom }` even though the generated workflow would have worked. On the Go side, `FrontmatterConfig.RunsOn` was typed `string`, which caused JSON unmarshalling to fail for the array and object forms before validation could even report a useful error, so the typed parser was the second copy of the same restriction. + +### Decision + +We will replace both `runs-on` schemas with a single `$ref` to a shared `#/$defs/github_actions_runs_on` definition that allows string, array, and object forms, and we will widen `FrontmatterConfig.RunsOn` from `string` to `any` so the typed frontmatter parser preserves whichever shape the user supplied. The serializer in `pkg/workflow/frontmatter_serialization.go` is updated to emit the field whenever it is non-`nil` (replacing the old non-empty-string check), so round-trip through `ToMap` is fidelity-preserving for all three forms. The driving principle is "validate against one source of truth that matches GitHub Actions semantics" rather than maintaining two parallel definitions that can drift. + +### Alternatives Considered + +#### Alternative 1: Fix the job-level object branch in place, keep two schemas + +Add `properties: { group, labels }` to the inline `jobs.*.runs-on` object branch so it matches the top-level definition, leaving the two schemas physically separate. Rejected because the duplication is precisely what produced the original divergence: every future addition to `runs-on` semantics (e.g., a new GitHub Actions property) would need to be applied in two places, and the regression test would only catch drift that the author also remembered to write twice. + +#### Alternative 2: Introduce a typed `RunsOn` union struct in Go + +Replace the `string` field with a custom type that implements `json.Unmarshaler` / `yaml.Unmarshaler` and exposes typed accessors (`AsString`, `AsLabels`, `AsGroup`). Rejected because the field is passed through to YAML emission as-is — consumers of `FrontmatterConfig` do not branch on the runs-on shape, they only need to preserve it. A typed union would add unmarshaling code, generated-workflow code paths, and tests for zero functional benefit over `any` with round-trip preservation. + +#### Alternative 3: Restrict job-level `runs-on` to string and array only + +Leave the schema rejecting object form at job level and document the restriction. Rejected because the compiler and runtime already accept the object form for jobs, and the PR description shows that real users (ARC custom runner groups) need exactly this shape; the schema would remain the only thing standing in the way of a working workflow. + +### Consequences + +#### Positive +- `jobs..runs-on: { group: , labels: [...] }` is accepted at schema-validation time, matching the compiler's existing behaviour and unblocking ARC / runner-group setups for job-level configuration. +- Top-level `runs-on` no longer fails JSON unmarshalling when the user supplies array or object forms — the field is preserved end-to-end through the typed config. +- The shared `$defs/github_actions_runs_on` definition becomes the single source of truth for runs-on validation; future additions need to be made in exactly one place. +- The new tests (`TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnObjectForm`, the three sub-tests in `frontmatter_types_test.go`) lock in regression coverage for all three forms at both positions. + +#### Negative +- `FrontmatterConfig.RunsOn` is now `any`, so any future consumer that wants to branch on the runs-on shape must type-assert (`switch v := fc.RunsOn.(type)`). Compile-time guarantees about the field's type are lost. +- The serializer's "emit when non-nil" rule means a caller that explicitly sets `fc.RunsOn = ""` will now serialise an empty string instead of omitting the key. Existing call sites use the zero value (untouched `any`), which is `nil`, so this is latent rather than active risk. +- The schema is slightly less self-contained at the point of use: a reader of `jobs.*.runs-on` now has to follow a `$ref` to see what is allowed, instead of reading the shape inline. + +#### Neutral +- The `$defs/github_actions_runs_on` definition is a verbatim copy of the previous top-level inline schema, including descriptions and examples; behaviour for top-level `runs-on` is unchanged. +- The `examples` array stays at the top-level use site rather than moving into `$defs`, which keeps top-level documentation visible without forcing the same examples onto the job-level binding. +- The PR also updates the actions lockfile and one workflow lock to a newer `docker/metadata-action@v6` pin; that change is unrelated to the schema decision and is not covered by this ADR. + +--- + +## Part 2 — Normative Specification (RFC 2119) + +> The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this section are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). + +### Schema Definition + +1. The workflow schema **MUST** declare a single `$defs/github_actions_runs_on` definition whose top-level `oneOf` accepts: a `string`, an `array` of `string`, and an `object` with `additionalProperties: false` and optional properties `group` (`string`) and `labels` (`array` of `string`). +2. The top-level `runs-on` property **MUST** reference `#/$defs/github_actions_runs_on` and **MUST NOT** inline its own `oneOf` shape. +3. The `jobs..runs-on` property **MUST** reference `#/$defs/github_actions_runs_on` and **MUST NOT** inline its own `oneOf` shape. +4. The schema **MUST** validate a workflow where `jobs..runs-on` is the object `{ "group": "" }` as conformant. +5. The schema **MUST** validate a workflow where `jobs..runs-on` is the object `{ "group": "", "labels": ["", ""] }` as conformant. + +### Typed Frontmatter Parsing + +1. The `RunsOn` field on `FrontmatterConfig` **MUST** be declared with Go type `any` and tagged `json:"runs-on,omitempty"`. +2. `ParseFrontmatterConfig` **MUST** preserve the user-supplied shape of `runs-on` such that: + - a string input is exposed as a `string` value, + - an array input is exposed as a `[]any` value, + - an object input is exposed as a `map[string]any` value. +3. `FrontmatterConfig.ToMap` **MUST** include the `"runs-on"` key when `fc.RunsOn` is non-`nil` and **MUST** omit it when `fc.RunsOn` is `nil`. +4. `FrontmatterConfig.ToMap` **MUST** round-trip the runs-on value such that the value at key `"runs-on"` is identical (by type and content) to `fc.RunsOn`. + +### Regression Coverage + +1. The parser test suite **MUST** contain a test that calls `ValidateMainWorkflowFrontmatterWithSchemaAndLocation` with a workflow whose `jobs..runs-on` is `map[string]any{"group": ""}` and asserts that no validation error is returned. +2. The frontmatter typed-parsing test suite **MUST** contain tests that round-trip `runs-on` through `ParseFrontmatterConfig` + `ToMap` for the string, array, and object forms and assert the value is preserved. + +### Conformance + +An implementation is considered conformant with this ADR if it satisfies all **MUST** and **MUST NOT** requirements above. Failure to meet any **MUST** or **MUST NOT** requirement constitutes non-conformance. + +--- + +*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/26291961745) workflow. The PR author must review, complete, and finalize this document before the PR can merge.* From a73f54c39cc4d11a8e155e58170a9ea4c8d50490 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 14:28:32 +0000 Subject: [PATCH 07/10] Initial plan Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/actionpins/data/action_pins.json | 5 +++++ pkg/workflow/data/action_pins.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/pkg/actionpins/data/action_pins.json b/pkg/actionpins/data/action_pins.json index 334d0b7954e..828732d7e61 100644 --- a/pkg/actionpins/data/action_pins.json +++ b/pkg/actionpins/data/action_pins.json @@ -138,6 +138,11 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, + "docker/metadata-action@v6": { + "repo": "docker/metadata-action", + "version": "v6", + "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" + }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 334d0b7954e..828732d7e61 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -138,6 +138,11 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, + "docker/metadata-action@v6": { + "repo": "docker/metadata-action", + "version": "v6", + "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" + }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", From f24079ed0cae71be360eeb1d5831617cec1d18e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 14:34:29 +0000 Subject: [PATCH 08/10] Address runs-on review feedback Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/aw/actions-lock.json | 5 --- .github/workflows/release.lock.yml | 6 +-- pkg/actionpins/data/action_pins.json | 5 --- pkg/parser/schema_location_test.go | 42 ++++++++++++++++++ pkg/workflow/data/action_pins.json | 5 --- pkg/workflow/frontmatter_parsing.go | 46 ++++++++++++++++++++ pkg/workflow/frontmatter_serialization.go | 18 +++++++- pkg/workflow/frontmatter_types_test.go | 52 +++++++++++++++++++++++ 8 files changed, 160 insertions(+), 19 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 828732d7e61..334d0b7954e 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -138,11 +138,6 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, - "docker/metadata-action@v6": { - "repo": "docker/metadata-action", - "version": "v6", - "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" - }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index d9aaa5d6eba..345e02e8eff 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6a6bd39e2339b2b176862a0fceb3dc70c2440fe79b8689f4e476134a57bed33a","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9","version":"v6"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"030e881283bb7a6894de51c315a6bfe6a94e05cf","version":"v6.0.0 (source v6)"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -49,7 +49,7 @@ # - anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 # - docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 # - docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 -# - docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 +# - docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) # - docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 (source v4) # # Container images used: @@ -1541,7 +1541,7 @@ jobs: username: ${{ github.actor }} - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) with: images: ghcr.io/${{ github.repository }} tags: | diff --git a/pkg/actionpins/data/action_pins.json b/pkg/actionpins/data/action_pins.json index 828732d7e61..334d0b7954e 100644 --- a/pkg/actionpins/data/action_pins.json +++ b/pkg/actionpins/data/action_pins.json @@ -138,11 +138,6 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, - "docker/metadata-action@v6": { - "repo": "docker/metadata-action", - "version": "v6", - "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" - }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/pkg/parser/schema_location_test.go b/pkg/parser/schema_location_test.go index 3012c311748..0aea58410b4 100644 --- a/pkg/parser/schema_location_test.go +++ b/pkg/parser/schema_location_test.go @@ -298,6 +298,48 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnOb } } +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnStringForm(t *testing.T) { + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "jobs": map[string]any{ + "my-prefetch": map[string]any{ + "runs-on": "ubuntu-latest", + "steps": []any{ + map[string]any{ + "run": "echo hello", + }, + }, + }, + }, + } + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md") + if err != nil { + t.Fatalf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() unexpected error = %v", err) + } +} + +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnArrayForm(t *testing.T) { + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "jobs": map[string]any{ + "my-prefetch": map[string]any{ + "runs-on": []any{"self-hosted", "linux"}, + "steps": []any{ + map[string]any{ + "run": "echo hello", + }, + }, + }, + }, + } + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md") + if err != nil { + t.Fatalf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() unexpected error = %v", err) + } +} + func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsAllowedBaseBranchesInCreatePullRequest(t *testing.T) { frontmatter := map[string]any{ "on": map[string]any{ diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 828732d7e61..334d0b7954e 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -138,11 +138,6 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, - "docker/metadata-action@v6": { - "repo": "docker/metadata-action", - "version": "v6", - "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" - }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/pkg/workflow/frontmatter_parsing.go b/pkg/workflow/frontmatter_parsing.go index 587a609581b..a5064d4d6cb 100644 --- a/pkg/workflow/frontmatter_parsing.go +++ b/pkg/workflow/frontmatter_parsing.go @@ -28,6 +28,10 @@ func ParseFrontmatterConfig(frontmatter map[string]any) (*FrontmatterConfig, err return nil, fmt.Errorf("failed to unmarshal frontmatter into config: %w", err) } + if err := validateRunsOnValue(config.RunsOn); err != nil { + return nil, err + } + // Parse typed Runtimes field if runtimes exist if len(config.Runtimes) > 0 { runtimesTyped, err := parseRuntimesConfig(config.Runtimes) @@ -78,6 +82,48 @@ func ParseFrontmatterConfig(frontmatter map[string]any) (*FrontmatterConfig, err return &config, nil } +func validateRunsOnValue(value any) error { + if value == nil { + return nil + } + + switch v := value.(type) { + case string: + return nil + case []any: + for _, label := range v { + if _, ok := label.(string); !ok { + return fmt.Errorf("invalid runs-on array entry type %T: expected string", label) + } + } + return nil + case map[string]any: + for key, field := range v { + switch key { + case "group": + if _, ok := field.(string); !ok { + return fmt.Errorf("invalid runs-on.group type %T: expected string", field) + } + case "labels": + labels, ok := field.([]any) + if !ok { + return fmt.Errorf("invalid runs-on.labels type %T: expected array of strings", field) + } + for _, label := range labels { + if _, ok := label.(string); !ok { + return fmt.Errorf("invalid runs-on.labels entry type %T: expected string", label) + } + } + default: + return fmt.Errorf("invalid runs-on object key %q: expected only group or labels", key) + } + } + return nil + default: + return fmt.Errorf("invalid runs-on type %T: expected string, array of strings, or object", value) + } +} + func parseOnNeedsConfig(on map[string]any) ([]string, error) { return parseOnNeedsValues(on) } diff --git a/pkg/workflow/frontmatter_serialization.go b/pkg/workflow/frontmatter_serialization.go index c244c7a6b43..f7b7e286450 100644 --- a/pkg/workflow/frontmatter_serialization.go +++ b/pkg/workflow/frontmatter_serialization.go @@ -1,5 +1,7 @@ package workflow +import "reflect" + // countRuntimes counts the number of non-nil runtimes in RuntimesConfig func countRuntimes(config *RuntimesConfig) int { if config == nil { @@ -180,7 +182,7 @@ func (fc *FrontmatterConfig) ToMap() map[string]any { } // Execution settings - if fc.RunsOn != nil { + if !isNilValue(fc.RunsOn) { result["runs-on"] = fc.RunsOn } if fc.RunsOnSlim != "" { @@ -233,6 +235,20 @@ func (fc *FrontmatterConfig) ToMap() map[string]any { return result } +func isNilValue(v any) bool { + if v == nil { + return true + } + + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return rv.IsNil() + default: + return false + } +} + // runtimeConfigToMap converts a single RuntimeConfig to map[string]any func runtimeConfigToMap(rc *RuntimeConfig) map[string]any { m := map[string]any{} diff --git a/pkg/workflow/frontmatter_types_test.go b/pkg/workflow/frontmatter_types_test.go index abc084355d3..4e82070909c 100644 --- a/pkg/workflow/frontmatter_types_test.go +++ b/pkg/workflow/frontmatter_types_test.go @@ -407,10 +407,62 @@ func TestParseFrontmatterConfig(t *testing.T) { reconstructed := config.ToMap() tt.assertion(t, reconstructed["runs-on"]) + + config2, err := ParseFrontmatterConfig(reconstructed) + require.NoError(t, err) + tt.assertion(t, config2.RunsOn) + + reconstructed2 := config2.ToMap() + assert.Equal(t, reconstructed, reconstructed2) }) } }) + t.Run("rejects invalid top-level runs-on forms", func(t *testing.T) { + tests := []struct { + name string + runsOn any + errContains string + }{ + { + name: "number form", + runsOn: 42, + errContains: "invalid runs-on type", + }, + { + name: "array contains non-string", + runsOn: []any{"self-hosted", 42}, + errContains: "invalid runs-on array entry type", + }, + { + name: "object contains unknown key", + runsOn: map[string]any{"unknown": "value"}, + errContains: "invalid runs-on object key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := ParseFrontmatterConfig(map[string]any{ + "runs-on": tt.runsOn, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errContains) + }) + } + }) + + t.Run("omits typed-nil top-level runs-on during serialization", func(t *testing.T) { + var runsOn *string + config := &FrontmatterConfig{ + RunsOn: runsOn, + } + + reconstructed := config.ToMap() + _, ok := reconstructed["runs-on"] + assert.False(t, ok) + }) + t.Run("preserves complex nested structures", func(t *testing.T) { frontmatter := map[string]any{ "safe-outputs": map[string]any{ From 278e64d329f72fe52b0e6d93edda771d349e5d08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 14:38:24 +0000 Subject: [PATCH 09/10] Polish runs-on validation follow-up Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/frontmatter_parsing.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/workflow/frontmatter_parsing.go b/pkg/workflow/frontmatter_parsing.go index a5064d4d6cb..e4553a4b311 100644 --- a/pkg/workflow/frontmatter_parsing.go +++ b/pkg/workflow/frontmatter_parsing.go @@ -98,16 +98,16 @@ func validateRunsOnValue(value any) error { } return nil case map[string]any: - for key, field := range v { + for key, value := range v { switch key { case "group": - if _, ok := field.(string); !ok { - return fmt.Errorf("invalid runs-on.group type %T: expected string", field) + if _, ok := value.(string); !ok { + return fmt.Errorf("invalid runs-on.group type %T: expected string", value) } case "labels": - labels, ok := field.([]any) + labels, ok := value.([]any) if !ok { - return fmt.Errorf("invalid runs-on.labels type %T: expected array of strings", field) + return fmt.Errorf("invalid runs-on.labels type %T: expected array of strings", value) } for _, label := range labels { if _, ok := label.(string); !ok { From 0fe2e2b8eab68dace5427e3bfef06b92309adaa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 15:04:35 +0000 Subject: [PATCH 10/10] Apply remaining changes Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/aw/actions-lock.json | 5 +++++ .github/workflows/release.lock.yml | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 334d0b7954e..828732d7e61 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -138,6 +138,11 @@ "version": "v4.1.0", "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121" }, + "docker/metadata-action@v6": { + "repo": "docker/metadata-action", + "version": "v6", + "sha": "80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9" + }, "docker/metadata-action@v6.0.0": { "repo": "docker/metadata-action", "version": "v6.0.0", diff --git a/.github/workflows/release.lock.yml b/.github/workflows/release.lock.yml index 345e02e8eff..d9aaa5d6eba 100644 --- a/.github/workflows/release.lock.yml +++ b/.github/workflows/release.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"6a6bd39e2339b2b176862a0fceb3dc70c2440fe79b8689f4e476134a57bed33a","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"030e881283bb7a6894de51c315a6bfe6a94e05cf","version":"v6.0.0 (source v6)"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"anchore/sbom-action","sha":"e22c389904149dbc22b58101806040fa8d37a610","version":"v0.24.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/login-action","sha":"4907a6ddec9925e35a0a9e82d7399ccc52663121","version":"v4.1.0"},{"repo":"docker/metadata-action","sha":"80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9","version":"v6"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0 (source v4)"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.51"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.51"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.17"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -49,7 +49,7 @@ # - anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 # - docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 # - docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 -# - docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) +# - docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 # - docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 (source v4) # # Container images used: @@ -1541,7 +1541,7 @@ jobs: username: ${{ github.actor }} - name: Extract metadata for Docker id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 (source v6) + uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6 with: images: ghcr.io/${{ github.repository }} tags: |