-
Notifications
You must be signed in to change notification settings - Fork 421
fix(codemod): skip shell comment lines when scanning run blocks for expression hoisting #38682
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 2 commits
0a76345
a179f04
209c091
fdcf4e5
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 |
|---|---|---|
|
|
@@ -231,6 +231,11 @@ func rewriteStepRunSecretsToEnv(stepLines []string, stepIndent string) ([]string | |
| if effectiveStepLineIndentLen(t, getIndentation(stepLines[j]), stepIndent) <= runKeyIndentLen { | ||
| break | ||
| } | ||
| // Skip bash comment lines – expressions inside comments are | ||
| // documentation-only and must not generate env bindings. | ||
| if strings.HasPrefix(t, "#") { | ||
|
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. [/diagnose] This guard handles full-line comments ( 💡 ContextThis is likely out of scope for this fix (the PR title targets "bash comment lines"), but documenting the known limitation prevents future confusion. Consider adding a // NOTE: trailing inline comments (e.g. `cmd # ${{ expr }}`) are not yet skipped;
// they continue to generate env bindings. Tracked as a separate follow-up.
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. Heredoc lines starting with 💡 DetailsThe scanner has no heredoc-boundary tracking — it does not detect GitHub Actions evaluates run: |
cat <<EOF
# config: ${{ env.CONFIG }}
EOFAfter this change the codemod silently passes over that expression, leaving the run block non-strict. Minimal mitigation: add an explicit comment above the guard stating the heredoc limitation, or track heredoc open/close state before applying the skip. |
||
| continue | ||
| } | ||
| updatedLine, bindings := replaceStepExpressionRefs(stepLines[j], shellIsPowerShell, bindingExprs) | ||
| if len(bindings) > 0 { | ||
| stepLines[j] = updatedLine | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -569,6 +569,71 @@ steps: | |
| assert.NotContains(t, result, "$env:EXPR_GITHUB_ACTOR", "bash step must not use $env:VARNAME") | ||
| }) | ||
|
|
||
| t.Run("ignores expressions inside bash comment lines in run block", func(t *testing.T) { | ||
|
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] No test covers a PowerShell step ( 💡 Suggested test skeletont.Run("ignores expressions inside pwsh comment lines in run block", func(t *testing.T) {
content := `---
on: push
steps:
- name: PS step
shell: pwsh
run: |
Write-Output "hello"
# ${{ steps.parse.outputs.value }} is documented here
---
`
// ... frontmatter ...
result, applied, err := codemod.Apply(content, frontmatter)
require.NoError(t, err)
assert.False(t, applied)
assert.NotContains(t, result, "EXPR_")
}) |
||
| // Expressions inside bash comments are documentation-only and must not | ||
| // generate env bindings or be rewritten. | ||
| content := `--- | ||
| on: workflow_dispatch | ||
| steps: | ||
| - name: Use parsed value | ||
| env: | ||
| VALUE: ${{ steps.parse.outputs.value }} | ||
| run: | | ||
| echo "Got: $VALUE" | ||
| # Note: the prompt placeholders ${{ steps.parse.outputs.* }} resolve to | ||
| # empty strings because they're evaluated in a different context. | ||
| --- | ||
| ` | ||
| frontmatter := map[string]any{ | ||
| "on": "workflow_dispatch", | ||
| "steps": []any{ | ||
| map[string]any{ | ||
| "name": "Use parsed value", | ||
| "env": map[string]any{ | ||
| "VALUE": "${{ steps.parse.outputs.value }}", | ||
| }, | ||
| "run": "echo \"Got: $VALUE\"\n# Note: the prompt placeholders ${{ steps.parse.outputs.* }} resolve to\n# empty strings because they're evaluated in a different context.", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| result, applied, err := codemod.Apply(content, frontmatter) | ||
| require.NoError(t, err, "codemod should apply cleanly") | ||
| assert.False(t, applied, "codemod should not modify content when only expression is in a bash comment") | ||
| assert.Equal(t, content, result, "content should be unchanged") | ||
| assert.NotContains(t, result, "EXPR_", "no EXPR_ bindings should be generated for comment-only expressions") | ||
|
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. Missing test coverage for folded-block-scalar ( 💡 Suggested additionAdd a minimal test variant: t.Run("ignores comment lines in folded run block", func(t *testing.T) {
content := `---
on: workflow_dispatch
steps:
- name: S
run: >
echo "${{ github.actor }}"
# doc ${{ steps.x.outputs.y }}
---
`
// frontmatter omitted for brevity
result, applied, err := codemod.Apply(content, frontmatter)
require.NoError(t, err)
assert.True(t, applied, "real expression should be hoisted")
assert.Contains(t, result, "EXPR_GITHUB_ACTOR")
assert.Contains(t, result, "${{ steps.x.outputs.y }}", "comment expression must be preserved verbatim")
}) |
||
| assert.Contains(t, result, "${{ steps.parse.outputs.* }}", "original comment text should be preserved verbatim") | ||
| }) | ||
|
|
||
| t.Run("ignores expressions in bash comments but still hoists real run expressions", func(t *testing.T) { | ||
|
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 fix applies to 💡 Suggested additionAdd a parallel test using |
||
| content := `--- | ||
| on: workflow_dispatch | ||
| steps: | ||
| - name: Use parsed value | ||
| run: | | ||
| echo "${{ steps.parse.outputs.value }}" | ||
| # See also ${{ steps.parse.outputs.* }} for all outputs | ||
| --- | ||
| ` | ||
| frontmatter := map[string]any{ | ||
| "on": "workflow_dispatch", | ||
| "steps": []any{ | ||
| map[string]any{ | ||
| "name": "Use parsed value", | ||
| "run": "echo \"${{ steps.parse.outputs.value }}\"\n# See also ${{ steps.parse.outputs.* }} for all outputs", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| result, applied, err := codemod.Apply(content, frontmatter) | ||
| require.NoError(t, err, "codemod should apply cleanly") | ||
| assert.True(t, applied, "codemod should apply for real run expression") | ||
| assert.Contains(t, result, "EXPR_STEPS_PARSE_OUTPUTS_VALUE: ${{ steps.parse.outputs.value }}", "real expression should be hoisted") | ||
| assert.Contains(t, result, `echo "$EXPR_STEPS_PARSE_OUTPUTS_VALUE"`, "real expression reference should be rewritten") | ||
| assert.Contains(t, result, "${{ steps.parse.outputs.* }}", "comment expression should be preserved verbatim") | ||
| assert.NotContains(t, result, "steps.parse.outputs.*:", "wildcard comment expression must not generate an env binding") | ||
|
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. Assertion tests an impossible condition: 💡 Suggested replacementReplace with an assertion that directly validates the invariant — that only one assert.Equal(t, 1, strings.Count(result, "EXPR_"),
"only the real expression should produce an EXPR_ binding")Or more narrowly, assert the comment line was not rewritten: assert.Contains(t, result, "# See also ${{ steps.parse.outputs.* }} for all outputs",
"comment line must not be rewritten in place")The |
||
| }) | ||
|
|
||
| t.Run("uses distinct bindings when different bodies collide to the same EXPR_ name", func(t *testing.T) { | ||
| // inputs.my-input and inputs.my_input both sanitize to EXPR_INPUTS_MY_INPUT. | ||
| // The second one must fall back to a hash-based name to avoid being silently | ||
|
|
||
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.
[/diagnose] Comment says "bash comment lines" but
#is also the comment character for PowerShell (pwsh/powershell),sh, and most other shells used in Actions. The wording overstates the scope.💡 Suggested wording
This is a minor documentation accuracy issue; the logic itself is correct for all shell types.