diff --git a/.github/aw/workflow-constraints.md b/.github/aw/workflow-constraints.md index 526a6532be1..78e409c4220 100644 --- a/.github/aw/workflow-constraints.md +++ b/.github/aw/workflow-constraints.md @@ -69,7 +69,7 @@ When a requested feature increases risk: When a workflow targets a self-hosted runner (any `runs-on` value other than GitHub-hosted labels such as `ubuntu-latest`, `ubuntu-slim`, `windows-latest`, or `macos-latest`), keep the generated workflow compatible with self-hosted constraints: -- Set `runs-on` explicitly (it is not inherited from imports) to the runner the user's setup provides; `runs-on` accepts a string, array, or runner-group object. Framework/generated jobs (activation, safe-outputs, unlock, etc.) default to the hosted `ubuntu-slim`, so also set `runs-on-slim` to route them to the self-hosted runner, otherwise they try to run on a hosted runner. `runs-on-slim` takes a single string label, so give it a self-hosted label the runner answers to (it cannot mirror an array or object value). +- Set `runs-on` explicitly (it is not inherited from imports) to the runner the user's setup provides; `runs-on` accepts a string, array, or runner-group object. Framework/generated jobs (activation, safe-outputs, unlock, etc.) default to the hosted `ubuntu-slim`, so also set `runs-on-slim` to route them to the self-hosted runner, otherwise they try to run on a hosted runner. `runs-on-slim` accepts the same string, array, or runner-group object forms as `runs-on`. - Write transient state, tool downloads, and intermediate outputs under `$RUNNER_TEMP`, not `/tmp`, which can persist across jobs on shared runners. - The agent job's own steps run as the runner user, not root — don't write steps that assume root (for example, installing to system-wide paths). Separately, the egress firewall needs host-level privileges (sudo) on the runner; if the host cannot provide that, the firewall can be disabled, which removes egress filtering. Surface that trade-off to the user rather than encoding it in the workflow. - Declare every outbound domain the workflow contacts in `network.allowed` (keep `defaults` for the core GitHub/Copilot/registry endpoints). When the egress firewall is enabled (the default once network permissions are set), any domain that is not allow-listed is blocked. diff --git a/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md b/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md new file mode 100644 index 00000000000..2d4e67ad98b --- /dev/null +++ b/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md @@ -0,0 +1,37 @@ +# ADR-38965: Reuse the runs-on schema and rendering pipeline for runs-on-slim + +**Date**: 2026-06-13 +**Status**: Accepted + +## Context + +`runs-on-slim` selects the runner for all framework/generated jobs (activation, safe-outputs, unlock, APM, etc.), while `runs-on` selects the runner for the main agent job. `runs-on` already accepts the full set of GitHub Actions runner forms — a plain string label, an array of labels, or a `{ group, labels }` runner-group object — but `runs-on-slim` was schema-validated and parsed as a string only. Self-hosted users who select runners by label array or runner group therefore could not route framework jobs to the same runner they use for `runs-on`, and any such value failed to compile. The two fields configure the same concept (runner selection) and should accept the same syntax. + +## Decision + +We will treat `runs-on-slim` as the same kind of value as `runs-on` rather than as a distinct string field. Concretely: the JSON schema entry for `runs-on-slim` now `$ref`s the shared `#/$defs/github_actions_runs_on` definition; the in-memory `FrontmatterConfig.RunsOnSlim` field changes from `string` to `any` and is validated through the existing `validateRunsOnValue`; and `WorkflowData.RunsOnSlim` holds a **rendered `runs-on:` YAML snippet** (produced by the same extraction path as `runs-on`) instead of a bare label. Downstream consumers re-indent that snippet for the framework job context via helpers (`formatRunsOnSnippetForInlineValue`, `indentYAMLLines`). This guarantees parity with `runs-on` and avoids a second, drift-prone validation/rendering path. + +## Alternatives Considered + +### Alternative 1: Keep `runs-on-slim` as a string and document the limitation +Leave the type as `string` and tell users that `runs-on-slim` cannot mirror an array or runner-group value. Rejected because it permanently blocks legitimate self-hosted configurations and forces an inconsistent mental model where two runner-selection fields accept different syntax. + +### Alternative 2: Add a separate schema and parser for `runs-on-slim`'s array/object forms +Duplicate the array/runner-group validation and YAML-rendering logic specifically for `runs-on-slim`. Rejected because it duplicates non-trivial logic already maintained for `runs-on`, inviting divergence over time as one path gains features or fixes the other misses. + +## Consequences + +### Positive +- `runs-on-slim` reaches full parity with `runs-on`, accepting string, label-array, and `{ group, labels }` forms. +- Validation and rendering reuse the existing shared `runs-on` schema and code paths, so future changes apply to both fields automatically. +- Self-hosted setups that select runners by label array or runner group can now route framework jobs correctly. + +### Negative +- The internal contract of `RunsOnSlim` changes: `FrontmatterConfig.RunsOnSlim` becomes `any` and `WorkflowData.RunsOnSlim` now carries a rendered `runs-on:` snippet rather than a bare label, requiring every consumer (serialization, framework-job formatting, central slash-command resolution) to be updated and re-tested. +- Indentation handling adds complexity: snippets must be re-indented for differing YAML contexts, introducing helper functions whose correctness depends on the exact upstream rendering format. + +### Neutral +- Existing tests were updated to expect rendered `runs-on:` snippets instead of bare labels, and new tests cover the array and runner-group forms. +- Reference docs, self-hosted-runner guidance, workflow constraints, and editor autocomplete metadata were updated to describe the expanded syntax. + +--- diff --git a/docs/public/editor/autocomplete-data.json b/docs/public/editor/autocomplete-data.json index b24a883f0d1..c1b1597ca35 100644 --- a/docs/public/editor/autocomplete-data.json +++ b/docs/public/editor/autocomplete-data.json @@ -484,9 +484,21 @@ "array": true }, "runs-on-slim": { - "type": "string", + "type": "string|array|object", "desc": "Runner for all framework/generated jobs (activation, pre-activation, safe-outputs, unlock, APM, etc.).", - "leaf": true + "children": { + "group": { + "type": "string", + "desc": "Runner group name for self-hosted runners or GitHub-hosted runner groups", + "leaf": true + }, + "labels": { + "type": "array", + "desc": "List of runner labels for self-hosted runners or GitHub-hosted runner selection", + "array": true + } + }, + "array": true }, "timeout-minutes": { "type": "integer|string", diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md index adbb8833fd7..798347d4402 100644 --- a/docs/src/content/docs/reference/frontmatter.md +++ b/docs/src/content/docs/reference/frontmatter.md @@ -195,7 +195,7 @@ runs-on-slim: ubuntu-slim # Defaults to ubuntu-slim (framework jobs o timeout-minutes: 30 # Defaults to 20 minutes ``` -`runs-on` applies to the main agent job only. `runs-on-slim` applies to all framework/generated jobs (activation, safe-outputs, unlock, etc.) and defaults to `ubuntu-slim`. `safe-outputs.runs-on` takes precedence over `runs-on-slim` for safe-output jobs specifically. +`runs-on` applies to the main agent job only. `runs-on-slim` applies to all framework/generated jobs (activation, safe-outputs, unlock, etc.), accepts the same string, array, or runner-group object forms as `runs-on`, and defaults to `ubuntu-slim`. `safe-outputs.runs-on` takes precedence over `runs-on-slim` for safe-output jobs specifically. `timeout-minutes` accepts an integer or a GitHub Actions expression string (e.g. `${{ inputs.timeout }}`), letting a reusable `workflow_call` workflow parameterize its own timeout from caller inputs. It applies to the workflow being compiled, **not** to plain caller jobs that invoke a reusable workflow with job-level `uses:` — GitHub rejects `timeout-minutes` there. diff --git a/docs/src/content/docs/reference/self-hosted-runners.md b/docs/src/content/docs/reference/self-hosted-runners.md index 8b36f9ec9ee..e84234f9878 100644 --- a/docs/src/content/docs/reference/self-hosted-runners.md +++ b/docs/src/content/docs/reference/self-hosted-runners.md @@ -113,6 +113,7 @@ safe-outputs: > [!NOTE] > `runs-on` controls only the main agent job. `runs-on-slim` controls all framework/generated jobs. `safe-outputs.runs-on` still takes precedence over `runs-on-slim` for safe-output jobs specifically. +> `runs-on-slim` accepts the same string, array, or runner-group object forms as `runs-on`. ## Configuring the maintenance workflow runner diff --git a/pkg/parser/schema_location_test.go b/pkg/parser/schema_location_test.go index 64e67559eec..4a67cffb0ad 100644 --- a/pkg/parser/schema_location_test.go +++ b/pkg/parser/schema_location_test.go @@ -415,6 +415,33 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnAr } } +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsRunsOnSlimArrayForm(t *testing.T) { + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "runs-on-slim": []any{"self-hosted", "linux"}, + } + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md") + if err != nil { + t.Fatalf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() unexpected error = %v", err) + } +} + +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsRunsOnSlimObjectForm(t *testing.T) { + frontmatter := map[string]any{ + "on": "workflow_dispatch", + "runs-on-slim": map[string]any{ + "group": "arc-custom", + "labels": []any{"ubuntu2404", "x64"}, + }, + } + + 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 60ad3a3d2a9..623352e85c1 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -2634,9 +2634,18 @@ ] }, "runs-on-slim": { - "type": "string", - "description": "Runner for all framework/generated jobs (activation, pre-activation, safe-outputs, unlock, APM, etc.). Provides a compile-stable override for generated job runners without requiring a safe-outputs section. Overridden by safe-outputs.runs-on when both are set. Defaults to 'ubuntu-slim'. Use this when your infrastructure does not provide the default runner or when you need consistent runner selection across all jobs.", - "examples": ["self-hosted", "ubuntu-latest", "ubuntu-22.04"] + "$ref": "#/$defs/github_actions_runs_on", + "description": "Runner for all framework/generated jobs (activation, pre-activation, safe-outputs, unlock, APM, etc.). Provides a compile-stable override for generated job runners without requiring a safe-outputs section. Supports the same string, array, and runner-group object forms as runs-on. Overridden by safe-outputs.runs-on when both are set. Defaults to 'ubuntu-slim'. Use this when your infrastructure does not provide the default runner or when you need consistent runner selection across all jobs.", + "examples": [ + "self-hosted", + "ubuntu-latest", + "ubuntu-22.04", + ["self-hosted", "ubuntu2404", "x64", "host"], + { + "group": "larger-runners", + "labels": ["ubuntu-latest-8-cores"] + } + ] }, "timeout-minutes": { "$ref": "#/$defs/templatable_integer", diff --git a/pkg/workflow/central_slash_command_workflow.go b/pkg/workflow/central_slash_command_workflow.go index c2061425f8e..028c95f4ff8 100644 --- a/pkg/workflow/central_slash_command_workflow.go +++ b/pkg/workflow/central_slash_command_workflow.go @@ -487,7 +487,7 @@ func resolveCentralSlashRunsOn(workflowDataList []*WorkflowData) string { if wd.SafeOutputs != nil && strings.TrimSpace(wd.SafeOutputs.RunsOn) != "" { resolved = strings.TrimSpace(wd.SafeOutputs.RunsOn) } else if strings.TrimSpace(wd.RunsOnSlim) != "" { - resolved = strings.TrimSpace(wd.RunsOnSlim) + resolved = formatRunsOnSnippetForInlineValue(wd.RunsOnSlim) } counts[resolved]++ } @@ -503,6 +503,29 @@ func resolveCentralSlashRunsOn(workflowDataList []*WorkflowData) string { return best } +func formatRunsOnSnippetForInlineValue(runsOn string) string { + runsOn = strings.TrimSpace(runsOn) + if !strings.HasPrefix(runsOn, "runs-on:") { + return runsOn + } + + value := strings.TrimPrefix(runsOn, "runs-on:") + if !strings.HasPrefix(value, "\n") { + return strings.TrimSpace(value) + } + + value = strings.TrimPrefix(value, "\n") + lines := strings.Split(value, "\n") + for i, line := range lines { + // The 2-space strip matches DefaultMarshalOptions map indentation. + // The 6-space re-indent aligns with the central slash command template, + // where runs-on: lives at 4-space job-level indent (4 + 2 = 6). + line = strings.TrimPrefix(line, " ") + lines[i] = " " + line + } + return "\n" + strings.Join(lines, "\n") +} + func writeCentralSlashEventsYAML(b *strings.Builder, mergedEvents map[string]map[string]bool) { eventOrder := []string{ "issues", diff --git a/pkg/workflow/central_slash_command_workflow_test.go b/pkg/workflow/central_slash_command_workflow_test.go index e82dc56ff42..ae97640ee85 100644 --- a/pkg/workflow/central_slash_command_workflow_test.go +++ b/pkg/workflow/central_slash_command_workflow_test.go @@ -299,7 +299,7 @@ func TestGenerateCentralSlashCommandWorkflow_UsesCentralizedRunsOnResolution(t * Command: []string{"one"}, CommandEvents: []string{"issue_comment"}, CommandCentralized: true, - RunsOnSlim: "ubuntu-latest", + RunsOnSlim: "runs-on: ubuntu-latest", }, { WorkflowID: "two", @@ -327,6 +327,41 @@ func TestGenerateCentralSlashCommandWorkflow_UsesCentralizedRunsOnResolution(t * require.Contains(t, string(content), "runs-on: self-hosted") } +func TestFormatRunsOnSnippetForInlineValue(t *testing.T) { + tests := []struct { + name string + runsOn string + want string + }{ + { + name: "plain label", + runsOn: "ubuntu-latest", + want: "ubuntu-latest", + }, + { + name: "rendered string snippet", + runsOn: "runs-on: self-hosted", + want: "self-hosted", + }, + { + name: "rendered array snippet", + runsOn: "runs-on:\n- self-hosted\n- linux", + want: "\n - self-hosted\n - linux", + }, + { + name: "rendered object snippet", + runsOn: "runs-on:\n group: runner-group\n labels:\n - linux", + want: "\n group: runner-group\n labels:\n - linux", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, formatRunsOnSnippetForInlineValue(tt.runsOn)) + }) + } +} + func TestBuildCommandsHeaderMetadata_UsesReleaseVersionOnlyForReleaseBuilds(t *testing.T) { originalVersion := compilerVersion originalIsRelease := isReleaseBuild diff --git a/pkg/workflow/compiler_orchestrator_workflow_test.go b/pkg/workflow/compiler_orchestrator_workflow_test.go index e9a118af17e..6d7c4f7328e 100644 --- a/pkg/workflow/compiler_orchestrator_workflow_test.go +++ b/pkg/workflow/compiler_orchestrator_workflow_test.go @@ -212,6 +212,45 @@ func TestExtractYAMLSections_MissingSections(t *testing.T) { assert.Empty(t, workflowData.Cache) } +func TestExtractYAMLSections_EmptyRunsOnSlimTreatedAsUnset(t *testing.T) { + compiler := NewCompiler() + + tests := []struct { + name string + value any + }{ + { + name: "empty string", + value: "", + }, + { + name: "empty array", + value: []any{}, + }, + { + name: "empty object", + value: map[string]any{}, + }, + { + name: "object with empty group and labels", + value: map[string]any{"group": "", "labels": []any{}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + workflowData := &WorkflowData{} + frontmatter := map[string]any{ + "runs-on-slim": tt.value, + } + + compiler.extractYAMLSections(frontmatter, workflowData) + + assert.Empty(t, workflowData.RunsOnSlim) + }) + } +} + func TestValidateWorkflowEngineSettings_PreservesLegacyErrorOrder(t *testing.T) { compiler := NewCompiler() compiler.strictMode = true diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go index 3594f48c26c..774c130e6b4 100644 --- a/pkg/workflow/compiler_types.go +++ b/pkg/workflow/compiler_types.go @@ -488,7 +488,7 @@ type WorkflowData struct { PreAgentSteps string // steps to run immediately before the agent execution step PostSteps string // steps to run after AI execution RunsOn string - RunsOnSlim string // runner override for all framework/generated jobs (activation, safe-outputs, unlock, etc.) + RunsOnSlim string // rendered runs-on snippet for framework/generated jobs (activation, safe-outputs, unlock, etc.) Environment string // environment setting for the main job Container string // container setting for the main job Services string // services setting for the main job diff --git a/pkg/workflow/frontmatter_parsing.go b/pkg/workflow/frontmatter_parsing.go index 95a4663b79e..ba49622afeb 100644 --- a/pkg/workflow/frontmatter_parsing.go +++ b/pkg/workflow/frontmatter_parsing.go @@ -31,6 +31,9 @@ func ParseFrontmatterConfig(frontmatter map[string]any) (*FrontmatterConfig, err if err := validateRunsOnValue(config.RunsOn); err != nil { return nil, err } + if err := validateRunsOnValue(config.RunsOnSlim); err != nil { + return nil, err + } // Parse typed Runtimes field if runtimes exist if len(config.Runtimes) > 0 { diff --git a/pkg/workflow/frontmatter_serialization.go b/pkg/workflow/frontmatter_serialization.go index 2b7c13a1d1d..7996eb3a7c3 100644 --- a/pkg/workflow/frontmatter_serialization.go +++ b/pkg/workflow/frontmatter_serialization.go @@ -182,7 +182,7 @@ func (fc *FrontmatterConfig) ToMap() map[string]any { if !isNilValue(fc.RunsOn) { result["runs-on"] = fc.RunsOn } - if fc.RunsOnSlim != "" { + if !isEmptyRunsOnValue(fc.RunsOnSlim) { result["runs-on-slim"] = fc.RunsOnSlim } if fc.RunName != "" { diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go index eca08ea0490..5fb9d4e4d74 100644 --- a/pkg/workflow/frontmatter_types.go +++ b/pkg/workflow/frontmatter_types.go @@ -327,7 +327,7 @@ type FrontmatterConfig struct { // Workflow execution settings 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.) + RunsOnSlim any `json:"runs-on-slim,omitempty"` // Runner for all framework/generated jobs; supports the same forms as runs-on RunName string `json:"run-name,omitempty"` PreSteps []any `json:"pre-steps,omitempty"` // Pre-workflow steps (run before checkout) Steps []any `json:"steps,omitempty"` // Custom workflow steps diff --git a/pkg/workflow/frontmatter_types_test.go b/pkg/workflow/frontmatter_types_test.go index 833229fe17a..efcee762cff 100644 --- a/pkg/workflow/frontmatter_types_test.go +++ b/pkg/workflow/frontmatter_types_test.go @@ -1075,6 +1075,16 @@ func TestFrontmatterConfigIntegration(t *testing.T) { assert.Equal(t, true, networkMap["allowed-input"], "allowed-input should be preserved") assert.Equal(t, []string{"defaults"}, networkMap["allowed"], "allowed list should be preserved") }) + + t.Run("ToMap omits empty runs-on-slim", func(t *testing.T) { + config := &FrontmatterConfig{ + RunsOnSlim: "", + } + + reconstructed := config.ToMap() + _, ok := reconstructed["runs-on-slim"] + assert.False(t, ok, "empty runs-on-slim should be omitted") + }) } // TestRuntimesConfigTyping tests the new typed RuntimesConfig field diff --git a/pkg/workflow/runs_on_validation.go b/pkg/workflow/runs_on_validation.go index eb388d1b450..9af09569cf0 100644 --- a/pkg/workflow/runs_on_validation.go +++ b/pkg/workflow/runs_on_validation.go @@ -107,6 +107,30 @@ func validateRunsOnValue(value any) error { } } +func isEmptyRunsOnValue(value any) bool { + switch v := value.(type) { + case nil: + return true + case string: + return strings.TrimSpace(v) == "" + case []any: + return len(v) == 0 + case map[string]any: + if len(v) == 0 { + return true + } + + group, hasGroup := v["group"].(string) + labels, hasLabels := v["labels"].([]any) + if !hasGroup && !hasLabels { + return false + } + return strings.TrimSpace(group) == "" && len(labels) == 0 + default: + return false + } +} + // extractRunnerLabels extracts individual runner label strings from a runs-on value. // Handles all supported GitHub Actions runs-on forms: // - string: "ubuntu-latest" diff --git a/pkg/workflow/safe_outputs_runs_on_test.go b/pkg/workflow/safe_outputs_runs_on_test.go index 542651028e0..964f0278060 100644 --- a/pkg/workflow/safe_outputs_runs_on_test.go +++ b/pkg/workflow/safe_outputs_runs_on_test.go @@ -258,6 +258,34 @@ This is a test workflow.`, expectedRunsOn: "runs-on: self-hosted", checkJobPatterns: []string{"\n activation:", "\n safe_outputs:"}, }, + { + name: "runs-on-slim supports array labels", + frontmatter: `--- +on: push +runs-on-slim: [self-hosted, ubuntu2404, x64, host] +--- + +# Test Workflow + +This is a test workflow.`, + expectedRunsOn: "runs-on:\n - self-hosted\n - ubuntu2404\n - x64\n - host", + checkJobPatterns: []string{"\n activation:"}, + }, + { + name: "runs-on-slim supports group and labels object", + frontmatter: `--- +on: push +runs-on-slim: + group: runner-group + labels: [ubuntu2404, x64] +--- + +# Test Workflow + +This is a test workflow.`, + expectedRunsOn: "runs-on:\n group: runner-group\n labels:\n - ubuntu2404\n - x64", + checkJobPatterns: []string{"\n activation:"}, + }, { name: "default used when neither runs-on-slim nor safe-outputs.runs-on is set", frontmatter: `--- @@ -330,14 +358,14 @@ func TestFormatFrameworkJobRunsOn(t *testing.T) { { name: "runs-on-slim used when safe-outputs.runs-on is empty", data: &WorkflowData{ - RunsOnSlim: "self-hosted", + RunsOnSlim: "runs-on: self-hosted", }, expectedRunsOn: "runs-on: self-hosted", }, { name: "safe-outputs.runs-on takes precedence over runs-on-slim", data: &WorkflowData{ - RunsOnSlim: "ubuntu-22.04", + RunsOnSlim: "runs-on: ubuntu-22.04", SafeOutputs: &SafeOutputsConfig{RunsOn: "self-hosted"}, }, expectedRunsOn: "runs-on: self-hosted", @@ -357,6 +385,13 @@ func TestFormatFrameworkJobRunsOn(t *testing.T) { }, expectedRunsOn: "runs-on: " + constants.DefaultActivationJobRunnerImage, }, + { + name: "runs-on-slim array snippet indents continuation lines by 4 spaces", + data: &WorkflowData{ + RunsOnSlim: "runs-on:\n- self-hosted\n- ubuntu2404", + }, + expectedRunsOn: "runs-on:\n - self-hosted\n - ubuntu2404", + }, } for _, tt := range tests { diff --git a/pkg/workflow/safe_outputs_runtime.go b/pkg/workflow/safe_outputs_runtime.go index 04155866f94..d28d1120951 100644 --- a/pkg/workflow/safe_outputs_runtime.go +++ b/pkg/workflow/safe_outputs_runtime.go @@ -29,7 +29,7 @@ func (c *Compiler) formatFrameworkJobRunsOn(data *WorkflowData) string { } if data != nil && data.RunsOnSlim != "" { safeOutputsRuntimeLog.Printf("Framework job runs-on from runs-on-slim: %s", data.RunsOnSlim) - return "runs-on: " + data.RunsOnSlim + return c.indentYAMLLines(data.RunsOnSlim, " ") } safeOutputsRuntimeLog.Printf("Framework job runs-on using default: %s", constants.DefaultActivationJobRunnerImage) return "runs-on: " + constants.DefaultActivationJobRunnerImage diff --git a/pkg/workflow/workflow_builder.go b/pkg/workflow/workflow_builder.go index 2d789151e13..86bd89b2905 100644 --- a/pkg/workflow/workflow_builder.go +++ b/pkg/workflow/workflow_builder.go @@ -277,11 +277,8 @@ func (c *Compiler) extractYAMLSections(frontmatter map[string]any, workflowData workflowData.TimeoutMinutes = c.extractTopLevelYAMLSection(frontmatter, "timeout-minutes") workflowData.RunsOn = c.extractTopLevelYAMLSection(frontmatter, "runs-on") - // Extract runs-on-slim as a plain string (no YAML formatting needed) - if v, ok := frontmatter["runs-on-slim"]; ok { - if s, ok := v.(string); ok { - workflowData.RunsOnSlim = s - } + if v, ok := frontmatter["runs-on-slim"]; ok && !isEmptyRunsOnValue(v) { + workflowData.RunsOnSlim = c.extractTopLevelYAMLSection(map[string]any{"runs-on": v}, "runs-on") } workflowData.Environment = c.extractTopLevelYAMLSection(frontmatter, "environment") workflowData.Container = c.extractTopLevelYAMLSection(frontmatter, "container")