Skip to content

Support array (and object) runner specs for runs-on-slim, safe-outputs.runs-on, and safe-outputs.threat-detection.runs-on #39205

@shubhamtanwar23

Description

@shubhamtanwar23

Summary

The top-level runs-on field accepts a string, an array of labels, or a group/labels object, which is the idiomatic way to target self-hosted runners (e.g. [self-hosted, linux, x64]). However, three sibling runner-selection fields only accept a single string:

  • runs-on-slim (framework/generated jobs)
  • safe-outputs.runs-on (all safe-output jobs)
  • safe-outputs.threat-detection.runs-on (detection job)

This forces self-hosted-runner users to register a single-label alias just to satisfy these fields, even though every other runner field in the project (top-level runs-on, per-job jobs.*.runs-on, custom safe-outputs.jobs.*.runs-on / runner, and aw.json maintenance.runs_on) already supports arrays.

Based on a source-code analysis (below), this is not an intentional restriction — it's an inconsistency. The array-handling machinery already exists in the codebase and is simply not wired up for these three fields.

Analysis

Behavior of runs-on (works — array + object supported)

  • Schema: runs-on references #/$defs/github_actions_runs_on, which is a oneOf of string / array / {group, labels} object. (pkg/parser/schemas/main_workflow_schema.json ~line 2625)
  • Parsing preserves the full YAML shape via extractTopLevelYAMLSection(frontmatter, "runs-on"). (pkg/workflow/workflow_builder.go line 279)
  • Stored as FrontmatterConfig.RunsOn any ("Supports string, array, or object GitHub Actions runner forms"). (pkg/workflow/frontmatter_types.go line 329)
  • Validated for all three shapes by validateRunsOnValue and extractRunnerLabels. (pkg/workflow/runs_on_validation.go)

Behavior of the three affected fields (broken — string only)

1. runs-on-slim

  • Schema is "type": "string". (main_workflow_schema.json ~line 2636)
  • Parser only accepts a string and silently drops non-string values:
	if v, ok := frontmatter["runs-on-slim"]; ok {
		if s, ok := v.(string); ok {
			workflowData.RunsOnSlim = s
		}
	}
  • Stored as WorkflowData.RunsOnSlim string (pkg/workflow/compiler_types.go line 491) and FrontmatterConfig.RunsOnSlim string (frontmatter_types.go line 330).
  • Rendered via naive string concatenation:
func (c *Compiler) formatFrameworkJobRunsOn(data *WorkflowData) string {
	if data != nil && data.SafeOutputs != nil && data.SafeOutputs.RunsOn != "" {
		safeOutputsRuntimeLog.Printf("Framework job runs-on from safe-outputs: %s", data.SafeOutputs.RunsOn)
		return "runs-on: " + data.SafeOutputs.RunsOn
	}
	if data != nil && data.RunsOnSlim != "" {
		safeOutputsRuntimeLog.Printf("Framework job runs-on from runs-on-slim: %s", data.RunsOnSlim)
		return "runs-on: " + data.RunsOnSlim
	}
	safeOutputsRuntimeLog.Printf("Framework job runs-on using default: %s", constants.DefaultActivationJobRunnerImage)
	return "runs-on: " + constants.DefaultActivationJobRunnerImage
}

2. safe-outputs.runs-on

  • Schema is "type": "string" and the description explicitly says "Single runner label". (main_workflow_schema.json ~line 10299)
  • Parser only accepts a string:
			// Handle runs-on configuration
			if runsOn, exists := outputMap["runs-on"]; exists {
				if runsOnStr, ok := runsOn.(string); ok {
					config.RunsOn = runsOnStr
				}
			}
  • Stored as SafeOutputsConfig.RunsOn string (compiler_types.go line 704) and rendered by the same formatFrameworkJobRunsOn above.

3. safe-outputs.threat-detection.runs-on

  • Schema is "type": "string". (main_workflow_schema.json ~line 9825)
  • Parser only accepts a string:
	// Parse runs-on field
	if runOn, exists := configMap["runs-on"]; exists {
		if runOnStr, ok := runOn.(string); ok {
			threatConfig.RunsOn = runOnStr
		}
	}
  • Stored as ThreatDetectionConfig.RunsOn string (threat_detection.go line 25) and rendered by string concatenation:
	runsOn := "runs-on: ubuntu-latest"
	if data.SafeOutputs.ThreatDetection.RunsOn != "" {
		runsOn = "runs-on: " + data.SafeOutputs.ThreatDetection.RunsOn
	}

There is also a third consumer of RunsOnSlim / SafeOutputs.RunsOn that treats them as strings: resolveCentralSlashRunsOn in pkg/workflow/central_slash_command_workflow.go (~lines 475–490).

The array-handling machinery already exists

The project already solved this exact problem in several places, so the fix can reuse existing patterns:

  • RunsOnValue (a []string with a string-or-array UnmarshalJSON) plus FormatRunsOn, which serializes 0/1/N labels into a YAML-safe value (single label inline, multi-label as a JSON-encoded flow sequence). Used today by aw.json maintenance.runs_on. (pkg/workflow/repo_config.go lines 51–74 and 254–283)
  • Custom safe-jobs already handle both string and []any for runs-on / runner. (pkg/workflow/safe_jobs.go lines 191–207)
  • validateRunsOnValue / extractRunnerLabels already understand string, array, and object shapes. (pkg/workflow/runs_on_validation.go)

Documentation confirms the gap

docs/.../reference/self-hosted-runners.md shows runs-on with string/array/object examples, but only single-label examples for runs-on-slim (runs-on-slim: self-hosted) and threat-detection.runs-on (runs-on: ubuntu-latest). aw.json maintenance.runs_on is documented with both single and multi-label forms — highlighting the inconsistency.

Conclusion

This is a feature gap / inconsistency, not an intended limitation. Users on self-hosted runners that require multiple labels (the standard [self-hosted, linux, x64] pattern) cannot configure framework, safe-output, or threat-detection jobs the same way they configure the main agent job.

Reproduction

---
on: issues
runs-on: [self-hosted, linux, x64]        # works
runs-on-slim: [self-hosted, linux, x64]   # silently ignored -> falls back to ubuntu-slim
safe-outputs:
  create-issue: {}
  runs-on: [self-hosted, linux, x64]       # silently ignored
  threat-detection:
    runs-on: [self-hosted, linux, x64]     # silently ignored -> falls back to ubuntu-latest
---

Triage this issue.

Compile and inspect the generated .lock.yml: the framework, safe-outputs, and detection jobs do not pick up the array values, while the main agent job does.

Expected behavior

runs-on-slim, safe-outputs.runs-on, and safe-outputs.threat-detection.runs-on should accept the same runner shapes as runs-on — at minimum a string and an array of labels (ideally also the {group, labels} object form for parity), and emit the corresponding runs-on: YAML in the generated jobs.

Implementation Plan

Please implement the following changes. Reuse the existing RunsOnValue + FormatRunsOn helpers (or the extractRunnerLabels / validateRunsOnValue shape handling) so all runner fields behave consistently.

  1. Update Schema (pkg/parser/schemas/main_workflow_schema.json):

    • runs-on-slim (~line 2636): replace "type": "string" with a oneOf of string + array of strings (or reuse #/$defs/github_actions_runs_on for full parity with runs-on). Update the description and examples to include an array such as ["self-hosted", "linux", "x64"].
    • safe-outputs.runs-on (~line 10299): same change; remove "Single runner label" wording from the description.
    • safe-outputs.threat-detection.runs-on (~line 9825): same change.
    • Mirror these in any duplicated/generated schema or autocomplete data if applicable (e.g. docs/public/editor/autocomplete-data.json).
  2. Update parsing:

    • pkg/workflow/workflow_builder.go (lines 281–285): accept array (and ideally object) values for runs-on-slim, not just string.
    • pkg/workflow/safe_outputs_config.go (lines 528–533): accept array/object for safe-outputs.runs-on.
    • pkg/workflow/threat_detection.go (lines 199–204): accept array/object for threat-detection.runs-on.
  3. Update types to carry richer-than-string values (choose the lowest-churn approach):

    • Option A (recommended): change the frontmatter-facing types to RunsOnValue / any and normalize to a YAML string early using FormatRunsOn, keeping the WorkflowData string fields as the already-rendered YAML value.
    • Affected: WorkflowData.RunsOnSlim (compiler_types.go line 491), FrontmatterConfig.RunsOnSlim (frontmatter_types.go line 330), SafeOutputsConfig.RunsOn (compiler_types.go line 704), ThreatDetectionConfig.RunsOn (threat_detection.go line 25).
  4. Update rendering so arrays serialize correctly instead of "runs-on: " + string:

    • pkg/workflow/safe_outputs_runtime.go formatFrameworkJobRunsOn (lines 25–36) — use FormatRunsOn/normalized value for both SafeOutputs.RunsOn and RunsOnSlim.
    • pkg/workflow/threat_detection.go (lines 966–969) — same for the detection job.
    • pkg/workflow/central_slash_command_workflow.go resolveCentralSlashRunsOn (~lines 475–490) — handle the non-string values consistently.
  5. Validation / safety:

    • Ensure the macOS-runner rejection in validateRunsOn (pkg/workflow/runs_on_validation.go) is also applied to these fields when arrays are allowed, so macos-* can't sneak in via runs-on-slim / safe-outputs.runs-on / threat-detection.runs-on.
    • Reuse validateRunsOnValue for shape validation and produce errors following the project style: [what's wrong]. [what's expected]. [example].
  6. Add/extend tests:

    • pkg/workflow/safe_outputs_runs_on_test.go — extend TestRunsOnSlimField and the safe-outputs runner tests with array (and object) cases; assert the generated YAML contains the flow-sequence form (e.g. runs-on: ["self-hosted","linux","x64"]).
    • pkg/workflow/threat_detection_test.go — add array/object cases for threat-detection.runs-on.
    • pkg/workflow/runs_on_validation_test.go — add macOS-in-array rejection cases for the new fields.
    • Add a schema-validation test confirming arrays are now accepted for the three fields.
  7. Update documentation:

    • docs/src/content/docs/reference/self-hosted-runners.md — show array examples for runs-on-slim, safe-outputs.runs-on, and threat-detection.runs-on (currently only single-label).
    • docs/src/content/docs/reference/frontmatter.md / frontmatter-full.md and docs/src/content/docs/reference/safe-outputs.md — update field type/descriptions.
  8. Follow project guidelines:

    • Run make fmt after Go changes and make recompile after any workflow markdown changes.
    • Run make agent-finish (build, test, recompile, lint, lint-errors) before completing.

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions