Skip to content

Refactor safe-outputs config parsing into focused workflow modules#44313

Merged
pelikhan merged 4 commits into
mainfrom
copilot/file-diet-refactor-safe-outputs-config
Jul 9, 2026
Merged

Refactor safe-outputs config parsing into focused workflow modules#44313
pelikhan merged 4 commits into
mainfrom
copilot/file-diet-refactor-safe-outputs-config

Conversation

Copilot AI commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

pkg/workflow/safe_outputs_config.go had grown into a 1,254-line mixed-responsibility file centered around a ~760-line extraction function. This change breaks that logic into smaller modules, keeps behavior intact, and removes duplicated bounded-integer parsing for global safe-outputs settings.

  • File split by responsibility

    • safe_outputs_config_types.go: shared config types, logger, module boundary docs
    • safe_outputs_config_extraction.go: top-level frontmatter extraction flow
    • safe_outputs_config_global.go: global safe-outputs field parsing
    • safe_outputs_config_base.go: shared per-handler base parsing helpers
    • safe_outputs_config_runtime.go: runtime handler-manager config assembly and serialization
    • removed the original monolithic safe_outputs_config.go
  • Global field parsing cleanup

    • extracted shared bounded-integer parsing into parseBoundedIntField / parseBoundedIntFieldOrDefault
    • reused that logic for:
      • max-patch-size
      • max-patch-files
      • timeout-minutes
    • preserved existing defaults and render-time behavior
  • Behavior-preserving extraction refactor

    • kept extractSafeOutputsConfig as the single entrypoint
    • moved handler-specific extraction and global field extraction behind clearer internal boundaries
    • preserved existing safe-output handler wiring and public config surface
  • Focused direct coverage

    • added a small unit test for the shared bounded-int helper to cover truncation, clamping, and invalid-value handling

Example of the new shared parsing path:

config.MaximumPatchSize = parseBoundedIntFieldOrDefault(
	outputMap,
	"max-patch-size",
	4096,
	safeOutputsConfigLog,
)

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor pkg/workflow/safe_outputs_config.go into focused modules Refactor safe-outputs config parsing into focused workflow modules Jul 8, 2026
Copilot AI requested a review from pelikhan July 8, 2026 15:08
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor
Risk 🟡 medium
Score 31/100 (impact:18 urgency:5 quality:8)
Action ⏸ defer

Status: DRAFT — needs undraft + CI before review.

Splits safe_outputs_config.go (1,254 lines) into 5 focused modules by responsibility. Behavior-preserving per description. Needs CI to confirm no regressions.

Run §28966928999

Generated by 🔧 PR Triage Agent · 106.9 AIC · ⌖ 10.9 AIC · ⊞ 5.4K ·

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category refactor
Risk 🟡 Medium
Priority Score 40 / 100
Action defer

Score breakdown: Impact 18 + Urgency 12 + Quality 10

Rationale: DRAFT. Refactors safe-outputs config parsing into focused modules — no behaviour change expected. Medium risk due to size (1282 add / 1254 del). Defer until undraft + CI.

Batch: similar refactor/chore drafts

Run §28986426320

Generated by 🔧 PR Triage Agent · 74.6 AIC · ⌖ 8.8 AIC · ⊞ 5.4K ·

@pelikhan pelikhan marked this pull request as ready for review July 9, 2026 01:13
Copilot AI review requested due to automatic review settings July 9, 2026 01:13
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

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

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

Refactors the safe-outputs frontmatter/config extraction code by splitting the former monolithic safe_outputs_config.go into smaller, responsibility-focused modules, while also consolidating repeated bounded-integer parsing logic into shared helpers (with direct unit test coverage).

Changes:

  • Split safe-outputs config parsing/types/runtime serialization into dedicated files to reduce mixed responsibilities.
  • Introduced parseBoundedIntField / parseBoundedIntFieldOrDefault and reused them for global bounded integer fields.
  • Added a focused unit test covering truncation/clamping/invalid handling for the bounded-int helper.
Show a summary per file
File Description
pkg/workflow/safe_outputs_config.go Deleted the former monolithic safe-outputs config implementation.
pkg/workflow/safe_outputs_config_types.go Introduces shared safe-outputs config types and the module logger.
pkg/workflow/safe_outputs_config_extraction.go Hosts the top-level extractSafeOutputsConfig flow and delegates global field parsing.
pkg/workflow/safe_outputs_config_global.go Implements global safe-outputs field parsing and new bounded-int helpers.
pkg/workflow/safe_outputs_config_base.go Centralizes shared per-handler “base” parsing helpers (max/token/app/staged/samples).
pkg/workflow/safe_outputs_config_runtime.go Contains runtime handler-manager config assembly + serialization helpers.
pkg/workflow/safe_outputs_config_global_test.go Adds unit coverage for the new bounded-int parsing helper.

Review details

Tip

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

  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines +27 to +45
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

configMap := map[string]any{}
if tt.name != "missing field" {
configMap["field"] = tt.input
}

got, ok := parseBoundedIntField(configMap, "field", safeOutputsConfigLog)
if ok != tt.wantOK {
t.Fatalf("parseBoundedIntField() ok = %v, want %v", ok, tt.wantOK)
}
if got != tt.want {
t.Fatalf("parseBoundedIntField() = %d, want %d", got, tt.want)
}
})
}
}
Comment on lines +264 to +270
// Handle jobs (safe-jobs must be under safe-outputs)
if jobs, exists := outputMap["jobs"]; exists {
if jobsMap, ok := jobs.(map[string]any); ok {
c := NewCompiler() // Create a temporary compiler instance for parsing
config.Jobs = c.parseSafeJobsConfig(jobsMap)
}
}
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent (fails due to guideline violation)

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

📊 Metrics (1 test)
Metric Value
Analyzed 1 (Go: 1, JS: 0)
✅ Design 1 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 1 (100%)
Duplicate clusters 0
Inflation No
🚨 Violations 1 (missing build tag)
Test File Classification Issues
TestParseBoundedIntField safe_outputs_config_global_test.go:8 design_test / behavioral_contract Missing //go:build tag
⚠️ Flagged Tests (1)

TestParseBoundedIntField (safe_outputs_config_global_test.go:1) — Hard violation: missing //go:build build tag on line 1.

Every other *_test.go in pkg/workflow/ starts with //go:build !integration (or //go:build integration for integration tests). The new file starts directly with package workflow, omitting the mandatory tag. This causes the test to run under all build modes unintentionally, bypassing the project's unit/integration split.

Fix: Add //go:build !integration as line 1 (before the blank line and package workflow).

(go/redacted):build !integration

package workflow

Note: The test logic itself is high-quality — table-driven with 8 cases covering the full input space: nil, positive int, zero, negative, uint64 overflow/clamp, float truncate, NaN, and Inf. Edge coverage is excellent.

Verdict

Failed. 0% implementation tests (threshold: 30%) — numeric score is 100/100. Failing due to coding guideline violation: safe_outputs_config_global_test.go is missing the mandatory //go:build tag on line 1. All other test files in this package carry this tag.

Warning

Firewall blocked 1 domain

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

  • awmgmcpg

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

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 30.1 AIC · ⌖ 10.6 AIC · ⊞ 6.8K ·
Comment /review to run again

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

❌ Test Quality Sentinel: 100/100 (numeric) but FAILS. Hard violation: safe_outputs_config_global_test.go is missing the mandatory //go:build !integration tag on line 1. All other unit test files in pkg/workflow/ carry this tag. Review flagged tests in the comment above.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (1,282 new lines across pkg/workflow/) but does not have a linked Architecture Decision Record (ADR).

Draft ADR committed: docs/adr/44313-split-safe-outputs-config-into-focused-modules.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
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-44313: Split Safe-Outputs Config Parsing into Focused Modules

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.

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., 0044313-split-...md for PR #44313).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 56.6 AIC · ⌖ 12.2 AIC · ⊞ 8.5K ·
Comment /review to run again

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

Review: Refactor safe-outputs config parsing into focused workflow modules

This is a clean, well-executed refactor. The split by responsibility is logical, behavior is preserved (and improved in a few edge cases), and the new parseBoundedIntField helper eliminates real duplication.

Correctness improvements detected (net positive)

Old max-patch-size float handler had no NaN/Inf guard

The deleted code for max-patch-size (unlike the max-patch-files code that came later) did not guard against float64(NaN) or float64(Inf) before calling int(v). In Go, int(NaN) and int(Inf) are implementation-defined and typically produce 0, which then silently fell through to the default of 4096. The new parseBoundedIntField explicitly rejects these via math.IsNaN / math.IsInf, making the behavior deterministic and logged.

Old max-patch-size int64/uint64 paths lacked overflow clamping

The old int64 and uint64 branches for max-patch-size did int(v) directly without an overflow check — unsafe on 32-bit platforms. The new parseBoundedIntField clamps to math.MaxInt, matching the more defensive handling that was already in the max-patch-files and timeout-minutes branches.

One non-blocking suggestion

See the inline comment on safe_outputs_config_global_test.go line 33: the tt.name != "missing field" test guard is a fragile name-coupling smell. Not blocking.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 119.4 AIC · ⌖ 6.12 AIC · ⊞ 4.8K


configMap := map[string]any{}
if tt.name != "missing field" {
configMap["field"] = tt.input

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.

The test gate tt.name != "missing field" couples setup logic to a magic string — if the case name ever changes, the field is silently inserted with a nil value instead of being absent, giving a false pass for the wrong reason.

Prefer an explicit absent bool field in the test struct:

tests := []struct {
    name   string
    absent bool  // when true, don't insert field into configMap
    input  any
    want   int
    wantOK bool
}{
    {name: "missing field", absent: true, want: 0, wantOK: false},
    // ...
}
// ...
if !tt.absent {
    configMap["field"] = tt.input
}

This makes the "key absent" contract explicit and survives future renames.

@copilot please address this.

@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, /codebase-design, and /grill-with-docs — requesting changes on test coverage gaps and one structural concern.

Key Themes

Issues (4 actionable)

  • int64 branch never testedparseBoundedIntField has 4 switch cases; only int, uint64, and float64 are exercised. The int64 clamp path is untested (comments 1).
  • Fragile missing field sentinelif tt.name != "missing field" couples test logic to the row name; a simple boolean field is safer (comment 2).
  • NewCompiler() shadows the receiverextractGlobalConfigFields is a *Compiler method, but internally creates a fresh compiler for parseSafeJobsConfig, silently discarding forceStaged, useSamples, and the engine registry. Even if intentional (and it was in the original), the refactor makes this invisible to readers (comment 3).
  • No default branch / no log for unsupported types — a quoted YAML integer (max-patch-size: "1024") silently falls back to 4096 with no diagnostic (comment 4).

Minor (non-blocking)

  • parseBoundedIntFieldOrDefault has no direct test (comment 5).
  • Schema Generation Architecture block is misplaced in the extraction file (comment 6).

Positive Highlights

  • The split boundaries are clean and well-reasoned: types / extraction / global / base / runtime.
  • Deduplication of parseBoundedIntField into a single helper is the right call.
  • The test covers NaN, Inf, truncation, and clamping — good baseline.
  • Module boundary doc in safe_outputs_config_types.go is a nice touch for navigability.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 137.7 AIC · ⌖ 7.35 AIC · ⊞ 6.6K
Comment /matt to run again

{name: "float truncate", input: 12.75, want: 12, wantOK: true},
{name: "float nan", input: math.NaN(), want: 0, wantOK: false},
{name: "float inf", input: math.Inf(1), want: 0, wantOK: false},
}

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] The int64 switch case in parseBoundedIntField has no test coverage — all three branches (normal, clamp, non-positive) are untested.

Add at least three rows to the table:

{name: "int64 normal",       input: int64(42),            want: 42,         wantOK: true},
{name: "int64 clamp",        input: int64(math.MaxInt64), want: math.MaxInt, wantOK: true},
{name: "int64 non-positive", input: int64(-1),            want: 0,          wantOK: false},

The int64 path has its own bounds check (v > int64(math.MaxInt)) that mirrors uint64 but is never exercised, leaving a silent regression risk.

@copilot please address this.

t.Parallel()

configMap := map[string]any{}
if tt.name != "missing field" {

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] Controlling the "missing field" case via tt.name is fragile — if the test name changes, the field-injection guard silently breaks.

Replace the name-based sentinel with a dedicated boolean field on the test struct:

tests := []struct {
    name         string
    input        any
    omitFromMap  bool   // true => do not set the key in configMap
    want         int
    wantOK       bool
}{
    {name: "missing field", omitFromMap: true, want: 0, wantOK: false},
    ...
}
// in the loop:
if !tt.omitFromMap {
    configMap["field"] = tt.input
}

This makes the intent explicit and decouples the test logic from the name string.

@copilot please address this.

// Handle jobs (safe-jobs must be under safe-outputs)
if jobs, exists := outputMap["jobs"]; exists {
if jobsMap, ok := jobs.(map[string]any); ok {
c := NewCompiler() // Create a temporary compiler instance for parsing

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.

[/codebase-design] c := NewCompiler() inside extractGlobalConfigFields (which is a method on *Compiler) shadows the receiver and discards all caller-configured state (engine registry, forceStaged, useSamples, etc.).

This pattern was copied from the original file, but moving it into extractGlobalConfigFields makes the hazard invisible — callers see a *Compiler receiver and reasonably expect it is used throughout. Either call c.parseSafeJobsConfig(jobsMap) directly with the receiver, or document explicitly why a fresh compiler is intentional here.

@copilot please address this.

// parseBoundedIntField parses a positive integer field from a heterogeneous YAML map.
// It accepts int, int64, uint64, and float64 values, clamps integer overflow to math.MaxInt,
// logs float truncation, and rejects non-positive, NaN, infinite, or otherwise invalid values.
func parseBoundedIntField(configMap map[string]any, key string, debugLog *logger.Logger) (int, bool) {

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] parseBoundedIntField silently ignores unrecognised types (e.g. string, bool) — the switch falls through to return 0, false with no log. When a workflow author writes max-patch-size: "1024" (a quoted string, which YAML happily passes), they get the 4 096 default with no diagnostic, making the mistake hard to debug.

Add a default branch that logs the unexpected type:

default:
    debugLog.Printf("%s: unsupported type %T, ignoring", key, raw)

Also add a test case for this path:

{name: "string input", input: "1024", want: 0, wantOK: false},

@copilot please address this.

return 0, false
}

func parseBoundedIntFieldOrDefault(configMap map[string]any, key string, defaultValue int, debugLog *logger.Logger) int {

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] parseBoundedIntFieldOrDefault is not tested directly. Its behaviour when given a value of 0 (non-positive → falls through to default) and when given a valid value that overrides the default is implicit. Add a small focused test, or extend TestParseBoundedIntField with a wrapper:

func TestParseBoundedIntFieldOrDefault(t *testing.T) {
    m := map[string]any{"k": 10}
    if got := parseBoundedIntFieldOrDefault(m, "k", 99, safeOutputsConfigLog); got != 10 {
        t.Fatalf("want 10, got %d", got)
    }
    m2 := map[string]any{}
    if got := parseBoundedIntFieldOrDefault(m2, "k", 99, safeOutputsConfigLog); got != 99 {
        t.Fatalf("want default 99, got %d", got)
    }
}

@copilot please address this.

// Safe Output Configuration Extraction
// ========================================
//
// ## Schema Generation Architecture

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.

[/grill-with-docs] The 37-line Schema Generation Architecture comment block lives at the top of the extraction module but describes schema generation and validation — work that belongs to safe_outputs_config_generation.go and the test files, not to the extraction flow. This creates a navigation mismatch: a reader opening safe_outputs_config_extraction.go to understand frontmatter extraction sees a lengthy detour about JSON schemas.

Consider moving this documentation into a doc.go comment or into pkg/workflow/README.md (which already exists and covers the workflow package), keeping the file header focused on extraction.

@copilot please address this.

@pelikhan

pelikhan commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

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

The refactor is structurally sound and the parseBoundedIntField consolidation is a net improvement over three divergent ad-hoc switch blocks. The extraction flow, runtime config, and types split all read cleanly.

🔎 Findings

Medium — parseBoundedIntFieldOrDefault has no test coverage

The new parseBoundedIntFieldOrDefault wrapper is the critical integration point for max-patch-size (default 4096) and max-patch-files (default 100). Its contract — return defaultVal when the field is absent or the raw value is invalid — is untested. The timeout-minutes caller intentionally passes defaultVal=0 to mean "no default"; if that subtle contract is ever broken, compiled workflows silently get a wrong timeout. See inline comment on line 143 of safe_outputs_config_global.go.

Medium — int64 branch untested in TestParseBoundedIntField

The test suite skips the int64 case entirely; the at-min, below-min, and overflow-clamp sub-branches for that type are unverified. See inline comment on the test file.

Low — DispatchRepository yaml struct tag changed from underscore to hyphen

The struct tag on SafeOutputsConfig.DispatchRepository changed from dispatch_repository to dispatch-repository. This is harmless if SafeOutputsConfig is never marshaled to YAML (it's only read via parseDispatchRepositoryConfig), but that assumption is worth an explicit confirmation grep before merge. See inline comment on safe_outputs_config_types.go line 81.

🔎 Code quality review by PR Code Quality Reviewer · 238.4 AIC · ⌖ 5.89 AIC · ⊞ 5.4K
Comment /review to run again

// Handle group-reports flag
if groupReports, exists := outputMap["group-reports"]; exists {
if groupReportsBool, ok := groupReports.(bool); ok {
config.GroupReports = groupReportsBool

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.

parseBoundedIntFieldOrDefault is untested and its zero-default behavior is invisible: No test exercises parseBoundedIntFieldOrDefault directly — only parseBoundedIntField is tested. The function's contract (return defaultValue when field is absent or invalid) is load-bearing for max-patch-size and max-patch-files, and the timeout-minutes case deliberately passes defaultValue=0 to express "no default" — a subtlety that could silently regress.

💡 Suggested fix

Add at minimum:

func TestParseBoundedIntFieldOrDefault(t *testing.T) {
	t.Parallel()
	// absent → returns default
	got := parseBoundedIntFieldOrDefault(map[string]any{}, "k", 4096, safeOutputsConfigLog)
	if got != 4096 { t.Fatalf("want 4096, got %d", got) }
	// present valid → overrides default
	got = parseBoundedIntFieldOrDefault(map[string]any{"k": 10}, "k", 4096, safeOutputsConfigLog)
	if got != 10 { t.Fatalf("want 10, got %d", got) }
	// invalid (zero) → returns default
	got = parseBoundedIntFieldOrDefault(map[string]any{"k": 0}, "k", 4096, safeOutputsConfigLog)
	if got != 4096 { t.Fatalf("want 4096, got %d", got) }
	// zero-default pattern (timeout-minutes semantics)
	got = parseBoundedIntFieldOrDefault(map[string]any{}, "k", 0, safeOutputsConfigLog)
	if got != 0 { t.Fatalf("want 0, got %d", got) }
}

SetIssueType *SetIssueTypeConfig `yaml:"set-issue-type,omitempty"` // Set the type of an issue (empty string clears the type)
SetIssueField *SetIssueFieldConfig `yaml:"set-issue-field,omitempty"` // Set a single issue field value by name/value
DispatchWorkflow *DispatchWorkflowConfig `yaml:"dispatch-workflow,omitempty"` // Dispatch workflow_dispatch events to other workflows
DispatchRepository *DispatchRepositoryConfig `yaml:"dispatch-repository,omitempty"` // Dispatch repository_dispatch events to external repositories; the underscore alias remains supported via parseDispatchRepositoryConfig.

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.

DispatchRepository yaml struct tag changed from dispatch_repository to dispatch-repository: The old struct tag used an underscore (yaml:"dispatch_repository,omitempty"); the new one uses a hyphen (yaml:"dispatch-repository,omitempty"). Any code path that round-trips SafeOutputsConfig through YAML marshal/unmarshal (e.g., serialization in tests, config snapshots, or any external tool consuming compiled output) will silently change the emitted key name and fail to re-read data written by the old code.

💡 Detail

The comment on line 81 says "the underscore alias remains supported via parseDispatchRepositoryConfig" — which handles reading the YAML key — but the struct tag governs what is emitted when marshaling SafeOutputsConfig to YAML. Any consumer of marshaled SafeOutputsConfig data (snapshots, cached configs, test fixtures) that expects dispatch_repository will break silently after this change.

If SafeOutputsConfig is never marshaled to YAML (only parsed from user input), this is safe. If it is marshaled anywhere, the tag must stay dispatch_repository or migration must be done explicitly.

Verify with: grep -rn "dispatch_repository" pkg/ --include="*.go" to confirm no marshal path exists before merging.

}
})
}
}

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.

Test cases for int64 type are missing: The test covers int, uint64, and float64 but skips int64 entirely. The int64 branch has distinct logic (comparison against int64(1), overflow clamping to math.MaxInt) that could regress independently.

💡 Suggested additions
{name: "int64 above min",   input: int64(100),            want: 100,         wantOK: true},
{name: "int64 at min",     input: int64(1),              want: 1,           wantOK: true},
{name: "int64 below min",  input: int64(0),              want: 0,           wantOK: false},
{name: "int64 clamp",      input: int64(math.MaxInt64),  want: math.MaxInt, wantOK: true},

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

Copilot AI commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran pr-finisher flow and pushed follow-up fixes in b2e8cde (safe-outputs global parsing/test review items + full local validation). CI on this new head is stale and needs a maintainer re-trigger before merge.

@pelikhan pelikhan merged commit 09afd9e into main Jul 9, 2026
29 checks passed
@pelikhan pelikhan deleted the copilot/file-diet-refactor-safe-outputs-config branch July 9, 2026 02:00
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.7

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[file-diet] Refactor pkg/workflow/safe_outputs_config.go (1,254 lines) into focused modules

3 participants