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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions