Skip to content

Support object-form runs-on in custom jobs schema#34007

Merged
pelikhan merged 11 commits into
mainfrom
copilot/fix-jobs-runs-on-schema-validation
May 22, 2026
Merged

Support object-form runs-on in custom jobs schema#34007
pelikhan merged 11 commits into
mainfrom
copilot/fix-jobs-runs-on-schema-validation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 22, 2026

jobs.*.runs-on accepted string and array forms in practice but still rejected the standard GitHub Actions object form ({ group: ... }) at schema-validation time. This change aligns custom job validation with the already-supported top-level runs-on behavior and preserves the same shape through typed parsing.

  • Schema

    • Replaced the duplicated runs-on definitions with a shared github_actions_runs_on schema.
    • Pointed both top-level runs-on and jobs.*.runs-on at the same definition.
    • This brings job-level validation in line with the compiler/runtime behavior for:
      • string
      • array
      • object (group, labels)
  • Typed frontmatter parsing

    • Changed FrontmatterConfig.RunsOn from string to any.
    • Allows top-level runs-on to preserve object and array forms instead of failing JSON unmarshalling.
    • Updated frontmatter serialization to round-trip non-string runs-on values correctly.
  • Regression coverage

    • Added a schema test that accepts job-level object-form runs-on.
    • Added typed parsing/round-trip coverage for top-level runs-on in string, array, and object forms.

Example now accepted in both places:

runs-on:
  group: arc-custom

jobs:
  my-prefetch:
    runs-on:
      group: arc-custom
    steps:
      - run: echo "hello"

pr-sous-chef: branch updated from https://github.com/github/gh-aw/actions/runs/26296266826

Generated by 👨‍🍳 PR Sous Chef · ● 5.2M ·

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Hey @copilot-swe-agent 👋 — thank you for the interest in this project!

Unfortunately, this PR cannot be accepted because it violates the contribution process outlined in our CONTRIBUTING.md:

🚫 This project does not accept traditional pull requests from non-core team members. The CONTRIBUTING.md clearly states:

Traditional Pull Requests Are Not Enabled for non-Core team members: If you are not part of the core team, please do not create pull requests directly. Instead, you create detailed agentic plans in issues, discuss with the team, and a core team member will create and implement the PR for you using agents.

The current PR appears to be created by a bot agent (app/copilot-swe-agent) and contains only lock file updates (dependency SHA updates) rather than addressing the described issue about runs-on schema validation.

What happens next?

  1. Close this PR — it doesn't follow the contribution process
  2. Open an issue instead — describe the problem and your proposed solution as an agentic plan
  3. Discuss with the core team — they will pick up the plan and implement it using their agents

This contribution model ensures consistency, quality, and dogfooding of the project's own agentic workflow tools.

Thank you for understanding! If you'd like to contribute, please review the How Development Works section and create a detailed issue with your agentic plan. 🤖

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • patchdiff.githubusercontent.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "patchdiff.githubusercontent.com"

See Network Configuration for more information.

Generated by ✅ Contribution Check · ● 2.3M ·

Copilot AI and others added 3 commits May 22, 2026 13:48
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
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] Fix jobs: runs-on does not accept group object form Support object-form runs-on in custom jobs schema May 22, 2026
Copilot AI requested a review from pelikhan May 22, 2026 13:54
@pelikhan pelikhan marked this pull request as ready for review May 22, 2026 13:54
Copilot AI review requested due to automatic review settings May 22, 2026 13:54
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

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

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Code Quality Review: Schema DRY Refactoring + Type System Alignment

Verdict: Approve with one non-blocking suggestion

This PR successfully consolidates the duplicated runs-on schema definitions and aligns validation with runtime behavior. The DRY refactoring improves maintainability, and the typed parsing changes correctly preserve all three GitHub Actions runner forms.

✅ What's done well
  • Clean schema consolidation: Moving the repeated runs-on definition to $defs/github_actions_runs_on eliminates duplication and ensures consistent validation across top-level and job-level configuration
  • Type system correctness: Changing RunsOn from string to any correctly reflects the actual GitHub Actions schema (which accepts string, array, or object)
  • Comprehensive test coverage: The new tests cover all three forms (string, array, object) and verify round-trip serialization, which is exactly the right level of regression protection
  • Consistent with existing patterns: The any type usage follows the same approach as other dynamic fields in FrontmatterConfig
💡 Non-blocking suggestion

I've left one inline comment about the nil-check pattern in frontmatter_serialization.go. The current != nil check works for the common case but has a subtle edge case with typed nil values. Consider adding explicit empty-string handling for robustness, but this is not a blocker.

🔎 Code quality review by PR Code Quality Reviewer · ● 431.9K


// Execution settings
if fc.RunsOn != "" {
if fc.RunsOn != nil {
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.

Inconsistent nil-check pattern: This change introduces if fc.RunsOn != nil for an any field, but the pattern is inconsistent with how other any fields are handled in this file.

💡 Suggested fix

The zero value for any is nil, but checking != nil for any types has a gotcha: a typed nil (e.g., (*string)(nil)) is not equal to nil in Go.

Options:

  1. Check the underlying value explicitly (safest):
if fc.RunsOn != nil {
    // Add validation for empty string if RunsOn is string type
    if s, ok := fc.RunsOn.(string); ok && s == "" {
        // skip empty string
    } else {
        result["runs-on"] = fc.RunsOn
    }
}
  1. Use reflection for zero-value check (more robust but heavier):
if fc.RunsOn != nil && !reflect.ValueOf(fc.RunsOn).IsZero() {
    result["runs-on"] = fc.RunsOn
}
  1. Accept the current pattern but document the limitation in a comment:
// RunsOn is any, so nil check prevents zero-value emission
// Note: typed nil values (e.g., (*string)(nil)) will pass this check
if fc.RunsOn != nil {
    result["runs-on"] = fc.RunsOn
}

Why this matters: If RunsOn is ever set to a typed nil (e.g., through pointer field initialization), it will be serialized as runs-on: null in YAML, which may cause validation errors downstream.

Recommendation: Use option 1 (explicit string empty check) since the vast majority of runs-on values will be strings, and this provides the clearest intent.

@github-actions
Copy link
Copy Markdown
Contributor

Triage the failing build-wasm check and refresh the branch once the root cause is clear.

Generated by 👨‍🍳 PR Sous Chef · ● 2.7M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /zoom-out and /tdd — requesting changes on test coverage and type safety.

📋 Key Themes & Recommendations

Key Themes

  1. Test coverage gaps — Only object form tested; array/string forms lack regression coverage
  2. Type safety erosionany removes compile-time guarantees without runtime validation
  3. Round-trip stability — Serialization tests only validate one direction
  4. Scope drift — Lock file changes appear unrelated to schema work

Recommendations Priority

🔴 High priority (blocking):

  • Add schema validation tests for array and string runs-on forms
  • Add explanation for lock file changes or revert them

🟡 Medium priority (nice to have):

  • Add runtime validation for RunsOn field to catch invalid types early
  • Strengthen round-trip tests with a second parse-serialize cycle

Positive Highlights

✅ Excellent schema deduplication — the $defs/github_actions_runs_on pattern is clean and maintainable
✅ Type change aligns with runtime behavior — pragmatic solution to support all GitHub Actions forms
✅ Clear PR description with concrete examples
✅ Good use of table-driven tests for the three runs-on forms


Overall: This is solid refactoring work that correctly unifies schema validation. The main concerns are test coverage completeness and the unexplained lock file changes. Once those are addressed, this will be ready to merge.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 787.4K

}
}

func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnObjectForm(t *testing.T) {
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.

[/tdd] Missing test coverage for array form — only object form is tested.

💡 Suggested test expansion

The schema accepts three forms for runs-on (string, array, object), but this test only validates the object form at the job level. Add test cases for the other two forms to prevent regressions:

func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnArrayForm(t *testing.T) {
    frontmatter := map[string]any{
        "on": "workflow_dispatch",
        "jobs": map[string]any{
            "my-job": map[string]any{
                "runs-on": []any{"self-hosted", "linux"},
                "steps": []any{
                    map[string]any{"run": "echo hello"},
                },
            },
        },
    }
    err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md")
    assert.NoError(t, err, "Array form should be valid")
}

Without these tests, a future schema change could break array or string forms without triggering test failures.


// Workflow execution settings
RunsOn string `json:"runs-on,omitempty"`
RunsOn any `json:"runs-on,omitempty"` // Supports string, array, or object GitHub Actions runner forms
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.

[/zoom-out] Type safety erosion — any loses compile-time guarantees.

💡 Consider a typed approach

Changing from string to any aligns with runtime behavior but removes all compile-time type safety. Invalid values like runs-on: 42 will now pass Go type checking and only fail at GitHub Actions runtime.

Options to preserve some type safety:

  1. Custom type with validation:
type RunsOnConfig struct {
    value any
}

func (r *RunsOnConfig) UnmarshalJSON(data []byte) error {
    var v any
    if err := json.Unmarshal(data, &v); err != nil {
        return err
    }
    // Validate v is string, []string, or map[string]any
    switch v.(type) {
    case string, []any, map[string]any:
        r.value = v
        return nil
    default:
        return fmt.Errorf("runs-on must be string, array, or object")
    }
}
  1. Runtime validation in ParseFrontmatterConfig:
if runsOn, ok := frontmatter["runs-on"]; ok {
    if !isValidRunsOn(runsOn) {
        return nil, fmt.Errorf("invalid runs-on type")
    }
    config.RunsOn = runsOn
}

Either approach catches invalid values before they reach GitHub Actions.

}
})

t.Run("handles top-level runs-on forms", func(t *testing.T) {
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.

[/tdd] Round-trip test only validates one direction — add a second cycle.

💡 Strengthen the round-trip assertion

The current test verifies parse → ToMap() but doesn't verify that the result can be re-parsed correctly. A bug in ToMap() could produce YAML that breaks when parsed a second time.

Add a full round-trip cycle:

t.Run("handles top-level runs-on forms", func(t *testing.T) {
    // ... existing test code ...
    
    // First round-trip (existing)
    config, err := ParseFrontmatterConfig(frontmatter)
    require.NoError(t, err)
    tt.assertion(t, config.RunsOn)
    
    reconstructed := config.ToMap()
    tt.assertion(t, reconstructed["runs-on"])
    
    // Second round-trip (new) — ensures ToMap() produces valid input
    config2, err := ParseFrontmatterConfig(reconstructed)
    require.NoError(t, err, "ToMap output should be parseable")
    tt.assertion(t, config2.RunsOn)
    
    reconstructed2 := config2.ToMap()
    assert.Equal(t, reconstructed, reconstructed2, "Round-trip should be stable")
})

This catches serialization bugs that only appear in the second parse cycle.

# - 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
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.

[/zoom-out] Unexplained lock file change — Docker action version unpinned.

🔍 Scope drift concern

This PR is about runs-on schema validation, but the lock files show docker/metadata-action changed from a pinned SHA (v6.0.0 → commit 030e881...) to an unpinned major version reference (v6 → commit 80c7e94...).

Issues:

  1. Scope drift: Makes the PR harder to review — reviewers must verify this unrelated change
  2. Supply chain risk: Major version tags can move (though the SHA is still pinned in practice)
  3. Missing context: No explanation in the PR description for why this changed

Questions:

  • Is this an intentional policy change (unpinned major versions for Docker actions)?
  • Was this caused by running make recompile on a different branch?
  • Should this be reverted and filed separately?

If unintentional, revert the lock file changes. If intentional, add a note to the PR description explaining the action version policy change.

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (130 new lines across pkg/parser/ and pkg/workflow/, above the 100-line threshold) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/34007-shared-runs-on-schema-and-any-typed-frontmatter.md — review and complete it before merging.

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

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff and captures: the shared $defs/github_actions_runs_on schema, the string → any widening of FrontmatterConfig.RunsOn, and the serializer's != nil round-trip rule.
  2. Complete the missing sections — confirm or correct the rationale, alternatives considered, and consequences. In particular, sanity-check whether any is the long-term right shape or whether a typed union should be revisited.
  3. Commit the finalized ADR to docs/adr/ on your branch (an updated commit on top of the draft is fine).
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-34007: Shared runs-on schema and any-typed frontmatter

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

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you — especially when they wonder, "why is RunsOn typed any instead of a discriminated union?"

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered 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)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 34007-...md for PR #34007).

References:

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · ● 4.9M ·

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent test quality

Analyzed 2 test(s): 2 design, 0 implementation, 0 guideline violation(s).

📊 Metrics & Test Classification (2 tests analyzed)
Metric Value
New/modified tests analyzed 2
✅ Design tests (behavioral contracts) 2 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (100%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnObjectForm pkg/parser/schema_location_test.go:278 ✅ Design None — validates schema accepts object-form runs-on
TestParseFrontmatterConfig ("handles top-level runs-on forms") pkg/workflow/frontmatter_types_test.go:345 ✅ Design None — table-driven test covering all 3 runs-on forms

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 2 tests — both unit tests (//go:build !integration)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). Both tests enforce behavioral contracts: schema validation accepts object-form runs-on, and the frontmatter parser preserves type integrity across all three runs-on forms (string, array, object).

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · ● 1.3M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%).

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments and reviews

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI and others added 3 commits May 22, 2026 14:28
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 22, 2026

@copilot review all comments and reviews

Addressed in 278e64d: reverted the unrelated action-pin churn, added job-level string/array/object runs-on coverage, strengthened top-level runs-on round-trip/validation, and confirmed the build-wasm failure is unrelated Claude wasm-golden drift.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all reviews

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 22, 2026

@copilot review all reviews

Reviewed the current reviews: the actionable items are already covered in 278e64d (job-level string/array/object coverage, second round-trip coverage, runs-on runtime validation, and the unrelated lockfile churn reverted). Focused runs-on tests still pass locally. The only broader failure I hit in pkg/workflow is the pre-existing Claude wasm golden drift, so I don’t have an additional code change to make for these reviews.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

@copilot review all comments and address the unresolved review feedback on this PR.

Generated by 👨‍🍳 PR Sous Chef · ● 5.2M ·

@github-actions
Copy link
Copy Markdown
Contributor

@copilot please rebase onto main, rerun checks, and summarize any remaining blockers.

Generated by 👨‍🍳 PR Sous Chef · ● 5.2M ·

@pelikhan pelikhan merged commit 0ec4cb5 into main May 22, 2026
@pelikhan pelikhan deleted the copilot/fix-jobs-runs-on-schema-validation branch May 22, 2026 15:59
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.

jobs: runs-on does not accept group: object form — schema validator rejects it while top-level runs-on accepts it fine

3 participants