Skip to content

refactor(workflow): split threat_detection.go (1542 lines) into focused modules#41231

Merged
pelikhan merged 5 commits into
mainfrom
copilot/file-diet-refactor-threat-detection
Jun 24, 2026
Merged

refactor(workflow): split threat_detection.go (1542 lines) into focused modules#41231
pelikhan merged 5 commits into
mainfrom
copilot/file-diet-refactor-threat-detection

Conversation

Copilot AI commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

pkg/workflow/threat_detection.go had grown to 1542 lines, making it hard to navigate and review. Splits it into 6 focused files, all under 500 lines, with no logic or API changes.

File breakdown

File Lines Scope
threat_detection_config.go 195 ThreatDetectionConfig struct + methods, config parsers, extractRawExpression
threat_detection_helpers.go 117 IsDetectionJobEnabled, IsConditionalDetection, shared predicates + constants (detectionStepCondition, stepEnvIndent, threatLog)
threat_detection_steps.go 446 Inline step builders: guard, clear, prepare, conclude, analysis, upload, custom steps
threat_detection_inline_engine.go 181 buildDetectionEngineExecutionStep — inline AWF engine execution
threat_detection_external.go 488 External-detector install/run/conclude, engine ID resolution, extractStepEnvLines
threat_detection_job.go 153 buildDetectionJob top-level assembler

Notes

  • All files remain in package workflow — no import path changes for callers.
  • The original issue proposed 5 files (~460 lines for steps.go), but that estimate excluded buildDetectionEngineExecutionStep (~168 lines). Extracting it to threat_detection_inline_engine.go keeps every file under the 500-line target.
  • Public API (IsDetectionJobEnabled, IsConditionalDetection, ThreatDetectionConfig) is unchanged.

pr-sous-chef branch update requested for workflow run https://github.com/github/gh-aw/actions/runs/28109601576.

Generated by 👨‍🍳 PR Sous Chef · 48 AIC · ⌖ 1.34 AIC · ⊞ 17.4K ·

Copilot AI and others added 2 commits June 24, 2026 14:28
Splits the 1542-line file into 6 focused files:
- threat_detection_config.go (195 lines): ThreatDetectionConfig struct, methods, config parsers, extractRawExpression
- threat_detection_helpers.go (117 lines): threatLog, IsDetectionJobEnabled, IsConditionalDetection, utility helpers, constants
- threat_detection_steps.go (446 lines): inline step builders (guard, clear, prepare, conclude, analysis, upload)
- threat_detection_inline_engine.go (181 lines): buildDetectionEngineExecutionStep for inline AWF path
- threat_detection_external.go (488 lines): external-detector install, run, conclude, and engine resolution
- threat_detection_job.go (153 lines): buildDetectionJob top-level assembler

All tests pass, no breaking changes to public API.

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] Refactor pkg/workflow/threat_detection.go into focused modules refactor(workflow): split threat_detection.go (1542 lines) into focused modules Jun 24, 2026
Copilot AI requested a review from pelikhan June 24, 2026 14:32
@pelikhan pelikhan marked this pull request as ready for review June 24, 2026 14:48
Copilot AI review requested due to automatic review settings June 24, 2026 14:48

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

This PR refactors the threat-detection workflow compiler implementation by splitting the former pkg/workflow/threat_detection.go (1542 lines) into smaller, focused modules under pkg/workflow/, with the intent of preserving behavior and public API while improving navigability.

Changes:

  • Split threat-detection config parsing, helpers/predicates, job assembly, and step builders into separate files.
  • Isolated inline-engine execution logic and external-detector logic into dedicated modules.
  • Removed the monolithic pkg/workflow/threat_detection.go source file after migrating its contents.
Show a summary per file
File Description
pkg/workflow/threat_detection.go Removed the previous monolithic implementation after splitting into focused modules.
pkg/workflow/threat_detection_config.go ThreatDetectionConfig model + parsing helpers (including extractRawExpression).
pkg/workflow/threat_detection_helpers.go Shared constants (threatLog, detectionStepCondition, stepEnvIndent) and predicates/helpers.
pkg/workflow/threat_detection_steps.go Detection job step assembly (inline + external-detector paths) and supporting step builders.
pkg/workflow/threat_detection_inline_engine.go Inline engine execution step builder (buildDetectionEngineExecutionStep).
pkg/workflow/threat_detection_external.go External-detector install/run/conclude and env extraction utilities.
pkg/workflow/threat_detection_job.go Top-level detection job assembler (buildDetectionJob).

Copilot's findings

Tip

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

  • Files reviewed: 7/7 changed files
  • Comments generated: 2

Comment on lines +12 to +14
// buildDetectionEngineExecutionStep creates the engine execution step for inline threat detection.
// It uses the same agentic engine already installed in the agent job, but runs it through
// sandbox.agent (AWF) with no allowed domains (network fully blocked) and no MCP configured.
@@ -0,0 +1,446 @@
// Package workflow - step builders for the standard (inline) threat detection flow.
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

No test files were added or modified in this PR. Test Quality Sentinel skipped. PR #41231 is a pure refactor that splits threat_detection.go (1542 lines) into 6 focused modules with no changes to any *_test.go, *.test.cjs, or *.test.js files.

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot mentioned this pull request Jun 24, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (1580 new lines in pkg/) but did not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/41231-split-threat-detection-into-focused-modules.md — review and complete it before merging.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR description and file breakdown.
  2. Complete the missing sections — confirm the context, refine the decision rationale, and verify the alternatives reflect what you actually 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-41231: Split threat_detection.go into Focused Single-Responsibility 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

ADRs create a searchable, permanent record of why the codebase looks the way it does. Even a pure structural refactor like this one captures a real decision (file-organization strategy, the 500-line target, why six files instead of five) that future contributors will want to understand. This repo already records many similar split/decompose decisions as ADRs.

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

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

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · 77.4 AIC · ⌖ 10.2 AIC · ⊞ 6.7K ·

@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 /zoom-out, /improve-codebase-architecture, and /tdd — commenting with improvement opportunities (no blocking issues).

📋 Key Themes & Highlights

Key Themes

  • Module ownership drift (3 comments): Two functions in helpers.go are exclusively called by external.go and should live there; getThreatDetectionEngineID lives in external.go but is called from three files and belongs in helpers.go.
  • Vacuous wrapper (1 comment): getExternalThreatDetectionEngineID delegates 100% to getThreatDetectionEngineID — the alias should be removed.
  • Missing unit tests for pure functions (3 comments): engineCoreSecretVarNames, extractStepEnvLines, extractRawExpression, and codexProxyWebsocketBaseURL are all pure/deterministic and untested; the first has security consequences.

Positive Highlights

  • ✅ Mechanically clean split — addition/deletion delta is symmetric and all six files stay in package workflow with zero import path changes.
  • ✅ Public API (IsDetectionJobEnabled, IsConditionalDetection, ThreatDetectionConfig) preserved exactly.
  • ✅ Every new file is under the 500-line target; no single file dominates navigation.
  • ✅ Well-written PR description with a clear file-breakdown table — makes review straightforward.
  • ✅ Existing test suite provides solid end-to-end coverage for the refactored logic.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 105.1 AIC · ⌖ 9.79 AIC · ⊞ 6.5K

// threat-detect path. Threat-detection engine resolution is centralized in
// getThreatDetectionEngineID, including Pi -> Copilot normalization.
func (c *Compiler) getExternalThreatDetectionEngineID(data *WorkflowData) string {
return c.getThreatDetectionEngineID(data)

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.

[/improve-codebase-architecture] getExternalThreatDetectionEngineID is a vacuous wrapper — it adds a second name with no semantic distinction from getThreatDetectionEngineID.\n\n

\n💡 Suggestion\n\nReplace all three call sites (lines 13, 173, 255) with direct calls to getThreatDetectionEngineID, then delete this wrapper. The two functions are identical; keeping both creates a false impression that the external path uses different resolution logic.\n\n


// getThreatDetectionEngineID returns the effective engine ID for the detection job.
// It mirrors threat-detection engine resolution: threat-detection.engine overrides main engine.
func (c *Compiler) getThreatDetectionEngineID(data *WorkflowData) string {

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] getThreatDetectionEngineID lives in threat_detection_external.go but is called by three files: threat_detection_external.go, threat_detection_inline_engine.go, and threat_detection_steps.go. This is a cross-cutting concern, not an external-path concern.\n\n

\n💡 Suggestion\n\nMove getThreatDetectionEngineID (and its vanishing alias getExternalThreatDetectionEngineID) to threat_detection_helpers.go, alongside the other shared predicates IsDetectionJobEnabled and IsConditionalDetection. A reader navigating threat_detection_inline_engine.go will reach for helpers first.\n\n

return additional
}

func canReuseThreatDetectionEngineConfigForExternalDetector(data *WorkflowData, engineID string) 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.

[/zoom-out] canReuseThreatDetectionEngineConfigForExternalDetector and engineCoreSecretVarNames (lines 94–118) are only ever called by threat_detection_external.go, yet they live in helpers.go. This muddies the helpers file's purpose as the home for shared, cross-cutting predicates.\n\n

\n💡 Suggestion\n\nMove both functions to threat_detection_external.go next to their callers. The helpers file should remain the source of truth for shared predicates (IsDetectionJobEnabled, IsConditionalDetection, threatLog, constants) and cross-cutting utilities. External-only decision functions should live closer to their use.\n\n

// engineCoreSecretVarNames returns the secret-backed env var names for the given engine ID
// that must be excluded from the AWF container via --exclude-env. These are the credentials
// that AWF's API proxy intercepts, so the container itself does not need them.
func engineCoreSecretVarNames(engineID string) []string {

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] engineCoreSecretVarNames has no unit test. A wrong credential mapping here means the detection container gets access to secrets it should not — the security consequence is high enough to warrant direct test coverage.\n\n

\n💡 Suggested test\n\ngo\nfunc TestEngineCoreSecretVarNames(t *testing.T) {\n cases := []struct{ engine string; want []string }{\n {"copilot", []string{"COPILOT_GITHUB_TOKEN"}},\n {"claude", []string{"ANTHROPIC_API_KEY"}},\n {"codex", []string{"OPENAI_API_KEY", "CODEX_API_KEY"}},\n {"gemini", []string{"GEMINI_API_KEY", "ANTIGRAVITY_API_KEY"}},\n {"antigravity", []string{"GEMINI_API_KEY", "ANTIGRAVITY_API_KEY"}},\n {"unknown", []string{}},\n }\n for _, tc := range cases {\n got := engineCoreSecretVarNames(tc.engine)\n if !reflect.DeepEqual(got, tc.want) {\n t.Errorf("%s: got %v, want %v", tc.engine, got, tc.want)\n }\n }\n}\n\n\n

// extractStepEnvLines copies the YAML env: block from a rendered engine execution step.
// It intentionally stops when a comment line appears because comments in step templates
// are section separators, and consuming past them may bleed into non-env content.
func extractStepEnvLines(step GitHubActionStep) []string {

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] extractStepEnvLines has no unit tests despite non-trivial edge cases: the comment-break condition, the indentation-prefix boundary, and an env: marker that appears before index zero. A silent mis-extraction would pass wrong env vars to the external detector.\n\n

\n💡 Key edge cases to test\n\n- Step with no env: block → returns nil\n- Step with env: followed by correctly indented vars → returns those lines only\n- Step where a # comment line immediately after env: terminates extraction\n- Line with stepEnvIndent-width indent followed by a non-env line stopping extraction\n- Empty-string lines within the env block (should be skipped per current logic)\n\n

// embedded directly into a YAML if: condition expression tree.
// Callers must ensure the input is a valid expression (verified by isExpression()) before
// calling this function; non-expression strings are returned with no modification.
func extractRawExpression(expr string) string {

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] extractRawExpression is a pure transformation function with no test. It handles three cases — proper ${{ ... }} wrappers, already-bare expressions, and malformed input — and the callers rely on correct trimming for downstream YAML expression trees.\n\n

\n💡 Suggested test cases\n\ngo\nfunc TestExtractRawExpression(t *testing.T) {\n cases := []struct{ in, want string }{\n {"${{ inputs.flag }}", "inputs.flag"},\n {"${{inputs.flag}}", "inputs.flag"}, // no spaces\n {"inputs.flag", "inputs.flag"}, // already bare\n {"${{ inputs.a && inputs.b }}", "inputs.a && inputs.b"},\n }\n for _, tc := range cases {\n if got := extractRawExpression(tc.in); got != tc.want {\n t.Errorf("extractRawExpression(%q) = %q, want %q", tc.in, got, tc.want)\n }\n }\n}\n\n\n

}, "\n")
}

func codexProxyWebsocketBaseURL(apiBase string) string {

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] codexProxyWebsocketBaseURL has three branches (https→wss, http→ws, default passthrough) and no unit test. The default pass-through silently returns an unchanged URL that is neither a ws nor wss scheme — worth asserting this behaviour is intentional.\n\n

\n💡 Suggested test cases\n\ngo\nfunc TestCodexProxyWebsocketBaseURL(t *testing.T) {\n cases := []struct{ in, want string }{\n {"(api.example.com/redacted), "(api.example.com/redacted) {"(localhost/redacted) "(localhost/redacted) {"(example.com/redacted) "(example.com/redacted) // passthrough\n {"", ""}, // empty\n }\n for _, tc := range cases {\n if got := codexProxyWebsocketBaseURL(tc.in); got != tc.want {\n t.Errorf("codexProxyWebsocketBaseURL(%q) = %q, want %q", tc.in, got, tc.want)\n }\n }\n}\n\n\n

@github-actions

Copy link
Copy Markdown
Contributor

@copilot review all comments and address unresolved review feedback.

Generated by 👨‍🍳 PR Sous Chef · 48 AIC · ⌖ 1.34 AIC · ⊞ 17.4K ·

@github-actions

Copy link
Copy Markdown
Contributor

@copilot please refresh the branch and rerun checks after any follow-up edits.

Generated by 👨‍🍳 PR Sous Chef · 48 AIC · ⌖ 1.34 AIC · ⊞ 17.4K ·

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

Copilot AI commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran a PR-finisher pass on #41231.

  • Local validation passed: make fmt, make lint, make test-unit, make test, make recompile.
  • Mergeability is clean (open, non-draft, mergeable_state clean).
  • Prior referenced workflow run 28109601576 is success with no failed jobs.
  • Remaining blocker to merge-ready: unresolved review threads are still present.

No code changes were made in this pass.

@pelikhan pelikhan merged commit fefe3ef into main Jun 24, 2026
30 checks passed
@pelikhan pelikhan deleted the copilot/file-diet-refactor-threat-detection branch June 24, 2026 17:53
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