-
Notifications
You must be signed in to change notification settings - Fork 454
perf(workflow): cut YAMLGeneration overhead in run-script expression guardrail #44438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -232,6 +232,10 @@ func validateNoGitHubExpressionsInRunScriptsFromParsed(workflow map[string]any) | |
| var violations []TemplateInjectionViolation | ||
|
|
||
| for _, runContent := range runBlocks { | ||
| if !mayContainInlineExpression(runContent) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard asymmetry with sibling function makes security auditing harder: 💡 DetailsBoth guards are safe in terms of false negatives — // mayContainInlineExpression is zero-false-negative (see its doc), so it is
// safe to use here instead of the full InlineExpressionPattern regex.
if !mayContainInlineExpression(runContent) {
continue
} |
||
| continue | ||
| } | ||
|
|
||
| // Align with template-injection validation by excluding non-executable regions: | ||
| // heredoc bodies and bash # comments. | ||
| contentWithoutHeredocs := stripShellLineComments(removeHeredocContent(runContent)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| package workflow | ||
|
|
||
| import "testing" | ||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/goccy/go-yaml" | ||
| ) | ||
|
|
||
| func BenchmarkValidateTemplateInjectionFastPath(b *testing.B) { | ||
| compiler := NewCompiler() | ||
|
|
@@ -27,3 +31,31 @@ jobs: | |
| } | ||
| } | ||
| } | ||
|
|
||
| func BenchmarkValidateNoGitHubExpressionsInRunScriptsFromParsed_NoExpressions(b *testing.B) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The benchmark covers the no-expression fast path but there is no unit test that exercises 💡 Suggested additionsAdding targeted unit tests would make the fast-path correctness guarantee explicit and catch regressions faster: func TestValidateNoGitHubExpressionsInRunScriptsFromParsed(t *testing.T) {
tests := []struct {
name string
yaml string
wantErr bool
}{
{
name: "no expressions — fast path, no error",
yaml: `jobs:
test:
steps:
- run: echo hello`,
wantErr: false,
},
{
name: "allowed generated expression passes",
yaml: `jobs:
test:
steps:
- run: node ${{ runner.temp }}/foo.cjs`,
wantErr: false,
},
{
name: "disallowed raw expression is rejected",
yaml: `jobs:
test:
steps:
- run: echo ${{ github.actor }}`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var parsed map[string]any
require.NoError(t, yaml.Unmarshal([]byte(tt.yaml), &parsed))
err := validateNoGitHubExpressionsInRunScriptsFromParsed(parsed)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}The no-expression case is particularly important to lock in the short-circuit behaviour introduced by this PR. @copilot please address this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Benchmark only covers the fast-path skip; the optimization's benefit is not measurable from this benchmark alone: The new benchmark measures a single 💡 DetailsThe PR body claims a significant regression was fixed in
A minimal companion benchmark: func BenchmarkValidateNoGitHubExpressionsInRunScriptsFromParsed_WithExpressions(b *testing.B) {
// workflow with allowed expressions present
yamlContent := `name: with-expressions\non: workflow_dispatch\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo ${{ env.FOO }}\n`
var parsed map[string]any
if err := yaml.Unmarshal([]byte(yamlContent), &parsed); err != nil {
b.Fatal(err)
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = validateNoGitHubExpressionsInRunScriptsFromParsed(parsed)
}
}This also locks in that the slow path itself does not regress in future refactors. |
||
| yamlContent := ` | ||
| name: no-inline-expression | ||
| on: workflow_dispatch | ||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: No expression run | ||
| run: | | ||
| echo "hello" | ||
| echo "world" | ||
| ` | ||
|
|
||
| var parsed map[string]any | ||
| if err := yaml.Unmarshal([]byte(yamlContent), &parsed); err != nil { | ||
| b.Fatal(err) | ||
| } | ||
|
|
||
| b.ResetTimer() | ||
| b.ReportAllocs() | ||
| for b.Loop() { | ||
|
Comment on lines
+49
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new benchmark uses (nolint/redacted):intrange // Use the standard testing.B loop form for broad tool compatibility.
for i := 0; i < b.N; i++ {Consider using the same (nolint/redacted):intrange // Use the standard testing.B loop form for broad tool compatibility.
for i := 0; i < b.N; i++ {
if err := validateNoGitHubExpressionsInRunScriptsFromParsed(parsed); err != nil {
b.Fatalf("validateNoGitHubExpressionsInRunScriptsFromParsed() error = %v", err)
}
}
@copilot please address this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnosing-bugs] Benchmark loop style is inconsistent with the sibling benchmark in this file — the existing 💡 Options
Neither blocks this PR, but leaving two incompatible patterns in the same file is a maintenance hazard. @copilot please address this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
💡 DetailsEither both benchmarks should use the same style, or this one should carry a comment explaining the intentional divergence. Without that, the next maintainer reading both benchmarks back-to-back will wonder which convention to follow when adding new benchmarks. To stay consistent with the existing benchmark: b.ReportAllocs()
(nolint/redacted):intrange // Use the standard testing.B loop form for broad tool compatibility.
for i := 0; i < b.N; i++ {
if err := validateNoGitHubExpressionsInRunScriptsFromParsed(parsed); err != nil {
b.Fatalf("validateNoGitHubExpressionsInRunScriptsFromParsed() error = %v", err)
}
}Or, if |
||
| if err := validateNoGitHubExpressionsInRunScriptsFromParsed(parsed); err != nil { | ||
| b.Fatalf("validateNoGitHubExpressionsInRunScriptsFromParsed() error = %v", err) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/diagnosing-bugs] The sibling function
validateNoTemplateInjectionFromParsed(line 183) usesInlineExpressionPattern.MatchString(runContent)as its precheck, while this function now usesmayContainInlineExpression(runContent). These are semantically different — full regex vs cheap substring scan — and the asymmetry may confuse future readers.💡 Suggested clarifying comment
A brief inline comment would make the intentional trade-off explicit:
Alternatively, the sibling function at line 183 could be updated to use
mayContainInlineExpressionfor consistency — it would be a strict improvement there too.@copilot please address this.