Skip to content

Support array/object runner specs for runs-on-slim and safe-outputs runner fields#39654

Merged
pelikhan merged 6 commits into
mainfrom
copilot/support-array-object-runner-specs
Jun 16, 2026
Merged

Support array/object runner specs for runs-on-slim and safe-outputs runner fields#39654
pelikhan merged 6 commits into
mainfrom
copilot/support-array-object-runner-specs

Conversation

Copilot AI commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

runs-on-slim, safe-outputs.runs-on, and safe-outputs.threat-detection.runs-on were inconsistent with the rest of the workflow surface by accepting only string labels in practice. This change aligns them with top-level runs-on semantics so string, label-array, and {group, labels} forms are handled consistently end-to-end.

  • Schema + editor metadata parity

    • Updated main_workflow_schema.json to allow github_actions_runs_on shapes for:
      • runs-on-slim
      • safe-outputs.runs-on
      • safe-outputs.threat-detection.runs-on
    • Added/updated examples and descriptions to reflect array/object support.
    • Updated docs/public/editor/autocomplete-data.json typing for safe-outputs runs-on.
  • Parsing + normalization

    • Added shared runs-on snippet normalization/serialization helpers.
    • Safe-outputs and threat-detection runs-on values now parse as any supported shape and normalize to a canonical runs-on: YAML snippet.
    • Preserves existing precedence behavior (safe-outputs.runs-on over runs-on-slim).
  • Rendering + downstream consumers

    • Framework job runner rendering now handles normalized snippet values (string/array/object) uniformly.
    • Threat-detection job rendering now supports non-string runner specs without string-concat assumptions.
    • Central slash-command runner resolution now interprets safe-outputs runs-on snippets consistently with runs-on-slim.
  • Validation hardening

    • Extended macOS runner rejection coverage to:
      • runs-on-slim
      • safe-outputs.runs-on
      • safe-outputs.threat-detection.runs-on
    • Error wording follows the project format and includes expected shape guidance + example.
  • Tests + docs updates

    • Expanded workflow/parser tests for array/object acceptance and rendering behavior.
    • Added schema-location tests for safe-outputs/threat-detection runs-on forms.
    • Updated reference docs (self-hosted-runners, frontmatter, frontmatter-full, safe-outputs) with array/object examples and type language.
safe-outputs:
  runs-on: [self-hosted, linux, x64]
  threat-detection:
    runs-on:
      group: runner-group
      labels: [linux, x64]

Copilot AI and others added 2 commits June 16, 2026 18:59
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…s-on fields

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Support array and object runner specs for runs-on-slim and safe-outputs Support array/object runner specs for runs-on-slim and safe-outputs runner fields Jun 16, 2026
Copilot AI requested a review from pelikhan June 16, 2026 19:35
@pelikhan pelikhan marked this pull request as ready for review June 16, 2026 19:55
Copilot AI review requested due to automatic review settings June 16, 2026 19:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns runs-on-slim, safe-outputs.runs-on, and safe-outputs.threat-detection.runs-on with top-level runs-on semantics by accepting string / label-array / {group, labels} forms across schema, parsing, validation, and rendering.

Changes:

  • Expands schema + editor metadata so safe-outputs and threat-detection runner fields accept full GitHub Actions runs-on shapes.
  • Adds shared normalization/serialization helpers and updates parsing/rendering paths to propagate non-string runner specs end-to-end.
  • Extends macOS runner rejection and adds tests/docs examples for the new runner spec forms.
Show a summary per file
File Description
pkg/workflow/threat_detection.go Parses threat-detection runs-on as a snippet and normalizes job runner rendering.
pkg/workflow/threat_detection_test.go Adds coverage for threat-detection runner array/object forms.
pkg/workflow/safe_outputs_runtime.go Updates framework job runs-on selection to use normalized snippet values.
pkg/workflow/safe_outputs_runs_on_test.go Adds compilation/runtime expectations for safe-outputs runner array/object forms and indentation behavior.
pkg/workflow/safe_outputs_import_test.go Updates import assertions to reflect normalized runs-on snippet storage.
pkg/workflow/safe_outputs_config.go Extracts safe-outputs runs-on using the shared snippet renderer.
pkg/workflow/runs_on_validation.go Extends macOS runner rejection to runs-on-slim and safe-outputs runner fields; adds runs-on shape validation helpers.
pkg/workflow/runs_on_validation_test.go Adds tests asserting macOS rejection across the newly-covered runner fields/forms.
pkg/workflow/runs_on_unmarshal.go Adds JSON unmarshaling support for safe-outputs and threat-detection runner shapes into normalized snippets.
pkg/workflow/runs_on_snippet.go Introduces shared helpers to render/normalize runs-on YAML snippets.
pkg/workflow/imports.go Adjusts safe-outputs import merge logic to treat “empty” runner values consistently across shapes.
pkg/workflow/frontmatter_types_test.go Adds tests ensuring ParseFrontmatterConfig handles safe-outputs runner forms.
pkg/workflow/frontmatter_parsing.go Adds validation for safe-outputs and threat-detection runs-on shapes during typed frontmatter parsing.
pkg/workflow/central_slash_command_workflow.go Ensures central slash-command runner resolution formats safe-outputs snippets consistently for inline YAML usage.
pkg/parser/schemas/main_workflow_schema.json Updates schema definitions for safe-outputs and threat-detection runs-on to reference github_actions_runs_on.
pkg/parser/schema_location_test.go Adds schema-location tests for safe-outputs/threat-detection runner array/object acceptance.
docs/src/content/docs/reference/self-hosted-runners.md Updates examples to show array/object runner specs for safe-outputs/threat-detection and runs-on-slim.
docs/src/content/docs/reference/safe-outputs.md Updates docs examples to include safe-outputs runner array support.
docs/src/content/docs/reference/frontmatter.md Updates runner precedence/type language to include safe-outputs and threat-detection runner forms.
docs/src/content/docs/reference/frontmatter-full.md Refreshes the full reference doc (includes runner form documentation and other regenerated sections).
docs/public/editor/autocomplete-data.json Updates editor typing/autocomplete metadata for safe-outputs runs-on unions.
.github/workflows/pr-code-quality-reviewer.lock.yml Updates generated workflow lock metadata after recompilation.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 22/22 changed files
  • Comments generated: 5

Comment on lines 26 to +29
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
snippet := normalizeRunsOnSnippet(data.SafeOutputs.RunsOn)
safeOutputsRuntimeLog.Printf("Framework job runs-on from safe-outputs: %s", snippet)
return c.indentYAMLLines(snippet, " ")
Comment on lines 967 to 970
runsOn := "runs-on: ubuntu-latest"
if data.SafeOutputs.ThreatDetection.RunsOn != "" {
runsOn = "runs-on: " + data.SafeOutputs.ThreatDetection.RunsOn
runsOn = normalizeRunsOnSnippet(data.SafeOutputs.ThreatDetection.RunsOn)
}
Comment thread docs/src/content/docs/reference/frontmatter-full.md Outdated
Comment on lines 6926 to 6930
# Maximum allowed size for git patches in kilobytes (KB) for
# push-to-pull-request-branch only. Overrides safe-outputs max-patch-size for this
# output type. Defaults to 4096 KB (4 MB) when unset.
# output type. Defaults to 1024 KB (1 MB) when unset.
# (optional)
max-patch-size: 1
Comment on lines 4913 to 4917
# Maximum allowed size for git patches in kilobytes (KB) for create-pull-request
# only. Overrides safe-outputs max-patch-size for this output type. Defaults to
# 4096 KB (4 MB) when unset.
# 1024 KB (1 MB) when unset.
# (optional)
max-patch-size: 1
@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel failed during test quality analysis.

@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (>100 new lines across schema, parsing, and rendering) but does not have a linked Architecture Decision Record (ADR).

⚠️ A draft ADR was generated and committed locally as docs/adr/39654-extend-runs-on-schema-to-safe-outputs-runner-fields.md, but the automated push step could not push it (the runner reported a detached-HEAD / missing GITHUB_HEAD_REF environment, unrelated to your branch). Please add the draft below to your branch manually.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📄 Draft ADR — copy to docs/adr/39654-extend-runs-on-schema-to-safe-outputs-runner-fields.md
# ADR-39654: Extend the shared runs-on schema and snippet normalization to safe-outputs runner fields

**Date**: 2026-06-16
**Status**: Draft

## Context

ADR-38965 brought `runs-on-slim` to parity with top-level `runs-on` by reusing the shared `#/$defs/github_actions_runs_on` schema and rendering a canonical `runs-on:` YAML snippet. However, three sibling runner-selection fields were left behind and, in practice, accepted only a plain string label: `runs-on-slim`, `safe-outputs.runs-on`, and `safe-outputs.threat-detection.runs-on`. These fields configure the same concept — which runner a generated job executes on — yet they could not express the array-of-labels or `{ group, labels }` runner-group forms that `runs-on` already supports. Self-hosted users could not target safe-outputs or threat-detection jobs consistently, and such values failed to compile. Downstream rendering also assumed string concatenation, which breaks on non-string runner specs.

## Decision

We will treat `runs-on-slim`, `safe-outputs.runs-on`, and `safe-outputs.threat-detection.runs-on` as the same kind of value as top-level `runs-on`: their schema references the shared `github_actions_runs_on` shapes (string, label array, or `{ group, labels }` object); values are parsed into a single canonical `runs-on:` YAML snippet via shared helpers (`renderRunsOnSnippet`, `normalizeRunsOnSnippet`); and framework job rendering, threat-detection job rendering, and central slash-command resolution all consume the normalized snippet uniformly. Existing precedence (`safe-outputs.runs-on` over `runs-on-slim`) is preserved, and macOS runner rejection is extended to all three fields with consistent error wording.

## Alternatives Considered

### Alternative 1: Keep the safe-outputs runner fields string-only and document the limitation
Leave them typed as strings and tell users they cannot mirror an array or runner-group value. Rejected because it permanently blocks legitimate self-hosted configs and entrenches an inconsistent model where fields configuring the identical concept accept different syntax.

### Alternative 2: Add per-field validation and rendering for the array/object forms
Duplicate validation and YAML-rendering logic separately for each safe-outputs runner field. Rejected because it duplicates non-trivial logic already maintained for `runs-on`/`runs-on-slim`, inviting drift — the same reasoning behind ADR-38965.

## Consequences

### Positive
- All four runner fields reach parity, accepting string, label-array, and `{ group, labels }` forms.
- A single normalization path feeds every consumer, so future runner-spec changes apply uniformly.
- Self-hosted setups can route safe-outputs and threat-detection jobs to their existing runners.
- macOS rejection is enforced uniformly with actionable errors.

### Negative
- The internal contract widens from `string` to a normalized snippet, requiring every consumer to be updated and re-tested.
- Snippet normalization adds indentation-sensitivity; correctness depends on the exact upstream rendering format.

### Neutral
- Tests, editor autocomplete metadata, and reference docs were expanded for the new shapes.
- This is an incremental extension of ADR-38965, not a new architectural direction; precedence is unchanged.
📋 What to do next
  1. Add the draft ADR above to docs/adr/39654-extend-runs-on-schema-to-safe-outputs-runner-fields.md on your branch.
  2. Complete the missing sections — refine context, decision rationale, and the real alternatives you weighed.
  3. Commit the finalized ADR to docs/adr/.
  4. Reference the ADR in this PR body, e.g.:

    ADR: ADR-39654: Extend the shared runs-on schema to safe-outputs runner fields

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

ADRs create a searchable, permanent record of why the codebase looks the way it does. This decision builds directly on ADR-38965; recording the extension keeps the runner-spec design history coherent for future contributors.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

ADRs live in docs/adr/ numbered by PR number (e.g., 39654-*.md for PR #39654).

🔒 This PR cannot merge until an ADR is linked in the PR body.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ ·

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — commenting (no blocking issues found, but several test gaps and documentation concerns worth addressing).

📋 Key Themes & Highlights

Key Themes

  • Untested utility functions: renderRunsOnSnippet, normalizeRunsOnSnippet, and isEmptyRunsOnValue are the shared foundation for all three runner fields; their edge-case behaviour (partial objects, idempotency, silent error paths) lacks dedicated unit tests.
  • Dual parse paths: SafeOutputsConfig.RunsOn is set via both extractSafeOutputsConfig (YAML-map path, compilation time) and UnmarshalJSON (JSON round-trip via ParseFrontmatterConfig). The relationship is undocumented and could diverge.
  • Doc regeneration side-effect: frontmatter-full.md regeneration pulled in several unrelated wording changes, including the removal of a security caveat on roles exact-match semantics.
  • Incomplete test sub-table: The new handles safe-outputs runs-on forms test block covers only string/array for safe-outputs.runs-on; object form and threat-detection.runs-on (any form) are missing.

Positive Highlights

  • ✅ Clean extraction of helpers into dedicated runs_on_snippet.go and runs_on_unmarshal.go files.
  • ✅ macOS validation now covers all four runner fields symmetrically; new test cases are clear and named well.
  • isEmptyRunsOnValue correctly handles nil, empty array, and empty-string cases — good defensive coding.
  • imports.go correctly migrated from result.RunsOn == "" to isEmptyRunsOnValue(result.RunsOn), keeping import merge semantics consistent with the new snippet format.
  • normalizeRunsOnSnippet idempotency guard prevents double-prefixing in the rendering pipeline — smart safety net.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

Comments that could not be inline-anchored

pkg/workflow/runs_on_snippet.go:24

[/tdd] Silent YAML marshal failure could produce malformed workflow output with no diagnostic — add a log call here.

If yaml.Marshal ever fails for a valid Go value (e.g., a deeply nested map[string]any that triggers the marshaler), renderRunsOnSnippet returns &quot;&quot;. The caller then sets SafeOutputsConfig.RunsOn = &quot;&quot;, which causes formatFrameworkJobRunsOn to fall back to the default runner silently — not a syntax error, but the user-specified runner is silently ignored.

<details>…

pkg/workflow/runs_on_unmarshal.go:20

[/tdd] Two parallel paths write SafeOutputsConfig.RunsOn; the relationship should be documented to prevent divergence.

UnmarshalJSON here handles the JSON round-trip in ParseFrontmatterConfig (map → JSON → struct). A second path in safe_outputs_config.go:530 (extractSafeOutputsConfig) handles direct YAML-map parsing at compile time. Both call renderRunsOnSnippet, so they are currently equivalent — but there is nothing preventing a future change to one without the other.

<detai…

pkg/workflow/frontmatter_types_test.go:421

[/tdd] The new test sub-table covers only string and array forms for safe-outputs.runs-on; two gaps remain.

  1. Object form ({group, labels}) for safe-outputs.runs-on is not tested through ParseFrontmatterConfig, so the UnmarshalJSON object branch is exercised only indirectly by TestSafeOutputsRunsOnConfiguration.
  2. safe-outputs.threat-detection.runs-on has no tests here at all — the new frontmatter_parsing.go validation block for it (lines 41-44) is untested.

<det…

pkg/workflow/runs_on_validation.go:136

[/tdd] isEmptyRunsOnValue partial-object logic is not covered by the new tests — consider adding table entries for the tricky branches.

The branch at line 136 (!hasGroup &amp;&amp; !hasLabels → return false) handles unknown keys like {&quot;unknown&quot;: &quot;value&quot;}, and the line-139 check handles {&quot;group&quot;: &quot;&quot;} (treated as empty). Neither case has a dedicated test, and the edge of {&quot;group&quot;: &quot;some-group&quot;} without a &quot;labels&quot; key is also untested (it should be non-empty — only group is enough).

<det…

pkg/workflow/runs_on_snippet.go:36

[/tdd] normalizeRunsOnSnippet has an idempotency guard (HasPrefix(value, &quot;runs-on:&quot;)) that is untested — easy to lose in a future refactor.

The function is called from safe_outputs_runtime.go and threat_detection.go with values that renderRunsOnSnippet has already normalized (so they start with &quot;runs-on:&quot;). If that guard is ever removed or the prefix changes, double-prefixed output like runs-on: runs-on: ubuntu-latest would silently land in the generated YAML.

<details>
<sum…

pkg/workflow/safe_outputs_runs_on_test.go:438

[/tdd] The 6-space indent on the object form here is not self-documenting — the existing runs-on-slim object test has the clarifying comment but this new case does not.

Line 449 of the same file explains: "Object continuation lines start at 2-space (DefaultMarshalOptions) so indentYAMLLines(&quot; &quot;) produces 2+4=6 spaces". The same reasoning applies here but is missing.

<details>
<summary>💡 Suggested change</summary>

{
    // Object continuation lines start at 2-space YAML in…

</details>

<details><summary>pkg/workflow/frontmatter_parsing.go:37</summary>

**[/grill-with-docs]** The post-unmarshal validation reads the raw frontmatter map again rather than the already-deserialized `config.SafeOutputs.RunsOn`worth a comment explaining why.

`config.SafeOutputs.RunsOn` is already a normalized `string` at this point (set by `UnmarshalJSON`), so `validateRunsOnValue(config.SafeOutputs.RunsOn)` would always pass (it only rejects non-string/array/object types). Re-reading the raw map is necessary to validate the *original* value before coercion.

Wit</details>

<details><summary>docs/src/content/docs/reference/frontmatter-full.md:960</summary>

**[/grill-with-docs]** The `roles` documentation drops the exact-match security caveat without explanationthis seems unrelated to the runner-spec changes.

The previous doc explicitly warned: _&quot;⚠️ `roles: [write]` rejects actors with `admin` or `maintainer` roles because matching is exact, not a minimum threshold.&quot;_ The new text changes the framing to _&quot;permission level&quot;_ and removes that warning entirely. If the matching behavior is still exact-match, this is a meaningful security regressio</details>

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot Run or-finisher skill

Copilot AI commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Run or-finisher skill

Ran the PR finisher pass on this branch. Current HEAD is dbab453; no additional code changes were made in this pass. Local validation succeeded (make fmt, make lint, make test-unit, make test, make recompile).

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot review and update changes using DRY/YAGNI/SOLID

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot review and update changes using DRY/YAGNI/SOLID

Addressed in 94ed72e.

I tightened the implementation with a single normalization path for runs-on snippets (DRY), kept only the needed behavior changes (YAGNI), and ensured responsibilities stay focused (SOLID):

  • normalized runs-on-slim through the same runs-on snippet path as safe-outputs
  • fixed array/object runs-on rendering so generated YAML nesting is valid for framework and threat-detection jobs
  • aligned frontmatter-full max-patch-size defaults with runtime behavior (4096 KB / 4 MB)
  • updated related tests for the canonical runs-on snippet formatting

Validation run: make agent-report-progress, targeted workflow tests, and parallel validation completed.

@github-actions

Copy link
Copy Markdown
Contributor

@copilot review all comments and summarize the remaining blockers, then rerun the stale smoke workflows to refresh CI status.

Generated by 👨‍🍳 PR Sous Chef ·

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants