From ef99b45c08ee822f88e0da1477a00f45d76b5cd5 Mon Sep 17 00:00:00 2001 From: Yuna Date: Fri, 26 Jun 2026 13:16:07 +0200 Subject: [PATCH 1/2] fix: detect grouped workflow outputs (#95) * fix: detect grouped workflow outputs * fix: cover gitea grouped outputs --------- Co-authored-by: Yuna Morgenstern --- CHANGELOG.md | 4 ++ .../syntax/WorkflowContextCatalog.java | 10 ++-- .../githubworkflow/syntax/WorkflowPsi.java | 14 ++++- .../run/WorkflowGutterTest.java | 22 ++++++++ .../syntax/WorkflowSyntaxCompletionTest.java | 38 ++++++++++++++ .../syntax/WorkflowValidationTest.java | 52 +++++++++++++++++++ 6 files changed, 133 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e35bb..4e18670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## [Unreleased] +### Fixes + +- Step outputs written inside grouped shell redirects to `$GITHUB_OUTPUT` or `$GITEA_OUTPUT` are now recognized. + ## [2026.6.20] - 2026-06-20 ### Plugin Wiring diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java index 3659a8b..ea131ef 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowContextCatalog.java @@ -2,8 +2,6 @@ import com.github.yunabraska.githubworkflow.i18n.GitHubWorkflowBundle; - - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -19,9 +17,11 @@ @SuppressWarnings("java:S2386") public class WorkflowContextCatalog { - public static final Pattern PATTERN_GITHUB_OUTPUT = Pattern.compile("(?:echo\\s+)?[\"']([A-Za-z_][A-Za-z0-9_-]*)=(.*?)[\"']\\s*>>\\s*\"?\\$\\w*:?\\{?GITHUB_OUTPUT\\}?\"?"); - public static final Pattern PATTERN_GITHUB_OUTPUT_TEE = Pattern.compile("(?:echo\\s+)?[\"']([A-Za-z_][A-Za-z0-9_-]*)=(.*?)[\"']\\s*\\|\\s*tee\\s+(?:-[A-Za-z]+\\s+)*.*\\$\\w*:?\\{?GITHUB_OUTPUT\\}?"); - public static final Pattern PATTERN_GITHUB_ENV = Pattern.compile("(?:echo\\s+)?[\"']([A-Za-z_][A-Za-z0-9_-]*)=(.*?)[\"']\\s*>>\\s*\"?\\$\\w*:?\\{?GITHUB_ENV\\}?\"?"); + public static final Pattern PATTERN_GITHUB_OUTPUT = Pattern.compile("(?:echo\\s+)?[\"']([A-Za-z_][A-Za-z0-9_-]*)=(.*?)[\"']\\s*>>\\s*\"?\\$\\w*:?\\{?(?:GITHUB|GITEA)_OUTPUT\\}?\"?"); + public static final Pattern PATTERN_GITHUB_OUTPUT_TEE = Pattern.compile("(?:echo\\s+)?[\"']([A-Za-z_][A-Za-z0-9_-]*)=(.*?)[\"']\\s*\\|\\s*tee\\s+(?:-[A-Za-z]+\\s+)*.*\\$\\w*:?\\{?(?:GITHUB|GITEA)_OUTPUT\\}?"); + public static final Pattern PATTERN_GITHUB_OUTPUT_GROUP = Pattern.compile("(?m)^\\s*\\{\\s*\\R([\\s\\S]*?)^\\s*}\\s*>>\\s*\"?\\$\\w*:?\\{?(?:GITHUB|GITEA)_OUTPUT\\}?\"?"); + public static final Pattern PATTERN_SHELL_OUTPUT_ASSIGNMENT = Pattern.compile("(?m)^\\s*(?:echo\\s+)?[\"']([A-Za-z_][A-Za-z0-9_-]*)=(.*?)[\"']\\s*;?\\s*$"); + public static final Pattern PATTERN_GITHUB_ENV = Pattern.compile("(?:echo\\s+)?[\"']([A-Za-z_][A-Za-z0-9_-]*)=(.*?)[\"']\\s*>>\\s*\"?\\$\\w*:?\\{?(?:GITHUB|GITEA)_ENV\\}?\"?"); public static final Pattern PATTERN_GITHUB_OUTPUT_MULTILINE = Pattern.compile("(?:echo\\s+)?[\"']?([A-Za-z_][A-Za-z0-9_-]*)<<[^\"'\\r\\n]+[\"']?"); public static final Pattern PATTERN_GITHUB_ENV_MULTILINE = Pattern.compile("(?:echo\\s+)?[\"']?([A-Za-z_][A-Za-z0-9_-]*)<<[^\"'\\r\\n]+[\"']?"); public static final long CACHE_ONE_DAY = 24L * 60 * 60 * 1000; diff --git a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowPsi.java b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowPsi.java index d12285a..4d598f1 100644 --- a/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowPsi.java +++ b/src/main/java/com/github/yunabraska/githubworkflow/syntax/WorkflowPsi.java @@ -43,8 +43,10 @@ import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.PATTERN_GITHUB_ENV; import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.PATTERN_GITHUB_ENV_MULTILINE; import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.PATTERN_GITHUB_OUTPUT; +import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.PATTERN_GITHUB_OUTPUT_GROUP; import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.PATTERN_GITHUB_OUTPUT_MULTILINE; import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.PATTERN_GITHUB_OUTPUT_TEE; +import static com.github.yunabraska.githubworkflow.syntax.WorkflowContextCatalog.PATTERN_SHELL_OUTPUT_ASSIGNMENT; import static java.util.Collections.unmodifiableList; import static java.util.Optional.ofNullable; @@ -332,9 +334,10 @@ public static String goToDeclarationString() { private static Map toGithubOutputs(final String text) { final Map variables = new HashMap<>(); - if (text.contains("GITHUB_OUTPUT")) { + if (text.contains("GITHUB_OUTPUT") || text.contains("GITEA_OUTPUT")) { putMatches(variables, PATTERN_GITHUB_OUTPUT.matcher(text), false); putMatches(variables, PATTERN_GITHUB_OUTPUT_TEE.matcher(text), false); + putGroupedOutputMatches(variables, text); putMatches(variables, PATTERN_GITHUB_OUTPUT_MULTILINE.matcher(text), true); } return variables; @@ -342,13 +345,20 @@ private static Map toGithubOutputs(final String text) { private static Map toGithubEnvs(final String text) { final Map variables = new HashMap<>(); - if (text.contains("GITHUB_ENV")) { + if (text.contains("GITHUB_ENV") || text.contains("GITEA_ENV")) { putMatches(variables, PATTERN_GITHUB_ENV.matcher(text), false); putMatches(variables, PATTERN_GITHUB_ENV_MULTILINE.matcher(text), true); } return variables; } + private static void putGroupedOutputMatches(final Map variables, final String text) { + final Matcher matcher = PATTERN_GITHUB_OUTPUT_GROUP.matcher(text); + while (matcher.find()) { + putMatches(variables, PATTERN_SHELL_OUTPUT_ASSIGNMENT.matcher(matcher.group(1)), false); + } + } + private static void putMatches(final Map variables, final Matcher matcher, final boolean multiline) { while (matcher.find()) { if (matcher.groupCount() >= 1) { diff --git a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowGutterTest.java b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowGutterTest.java index ed5aca0..bee9ca3 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowGutterTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/run/WorkflowGutterTest.java @@ -183,6 +183,28 @@ public void testWorkflowDispatchShowsRunLineMarker() throws Exception { } } + public void testWorkflowDispatchShowsOneRunLineMarker() { + configureWorkflowProjectFile(""" + name: Gutter + on: + workflow_dispatch: + jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo ok + """); + final WorkflowRun.LineMarkerContributor.RepositoryAvailability previous = + WorkflowRun.LineMarkerContributor.useRepositoryAvailabilityForTests((project, file) -> true); + try { + assertThat(myFixture.findAllGutters().stream() + .filter(gutter -> gutter.getIcon() == AllIcons.Actions.Execute) + .count()).isEqualTo(1); + } finally { + WorkflowRun.LineMarkerContributor.useRepositoryAvailabilityForTests(previous); + } + } + public void testWorkflowDispatchDoesNotShowRunLineMarkerWithoutGitRepository() { configureWorkflowProjectFile(""" name: Gutter diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java index 8f7023b..d286d59 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowSyntaxCompletionTest.java @@ -1493,6 +1493,44 @@ public void testStepsOutputCompletionSuggestsRunOutput() { """)).contains("artifact"); } + public void testStepsOutputCompletionSuggestsGroupedRunOutput() { + assertThat(completeWorkflow(""" + name: Completion + on: workflow_dispatch + jobs: + build: + runs-on: ubuntu-latest + steps: + - id: prepare + run: | + echo "output1=value1" >> "${GITHUB_OUTPUT}" + { + echo "output2=value2" + echo "output3=value3" + } >> "${GITHUB_OUTPUT}" + - run: echo "${{ steps.prepare.outputs. }}" + """)).contains("output1", "output2", "output3"); + } + + public void testGiteaStepsOutputCompletionSuggestsGroupedRunOutput() { + assertThat(completeGiteaWorkflow(""" + name: Completion + on: workflow_dispatch + jobs: + build: + runs-on: ubuntu-latest + steps: + - id: prepare + run: | + echo "output1=value1" >> "${GITEA_OUTPUT}" + { + echo "output2=value2" + echo "output3=value3" + } >> "${GITEA_OUTPUT}" + - run: echo "${{ steps.prepare.outputs. }}" + """)).contains("output1", "output2", "output3"); + } + public void testBracketStepsOutputCompletionSuggestsRunOutput() { assertThat(completeWorkflow(""" name: Completion diff --git a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java index 0ee29ad..1cea406 100644 --- a/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java +++ b/src/test/java/com/github/yunabraska/githubworkflow/syntax/WorkflowValidationTest.java @@ -1261,6 +1261,58 @@ public void testIssue79MultilineStepOutputFromGroupedEchoIsAccepted() { """); } + public void testIssue94StepOutputFromGroupedEchoIsAccepted() { + assertWorkflowHighlights(""" + name: grouping bug + on: + workflow_dispatch: + jobs: + grouping-bug: + runs-on: ubuntu-latest + steps: + - name: Prepare outputs + id: prepare + run: | + echo "output1=value1" >> "${GITHUB_OUTPUT}" + + { + echo "output2=value2" + echo "output3=value3" + } >> "${GITHUB_OUTPUT}" + - name: Print outputs + run: | + echo "Output 1: ${{ steps.prepare.outputs.output1 }}" + echo "Output 2: ${{ steps.prepare.outputs.output2 }}" + echo "Output 3: ${{ steps.prepare.outputs.output3 }}" + """); + } + + public void testGiteaStepOutputFromGroupedEchoIsAccepted() { + assertGiteaWorkflowHighlights(""" + name: grouping bug + on: + workflow_dispatch: + jobs: + grouping-bug: + runs-on: ubuntu-latest + steps: + - name: Prepare outputs + id: prepare + run: | + echo "output1=value1" >> "${GITEA_OUTPUT}" + + { + echo "output2=value2" + echo "output3=value3" + } >> "${GITEA_OUTPUT}" + - name: Print outputs + run: | + echo "Output 1: ${{ steps.prepare.outputs.output1 }}" + echo "Output 2: ${{ steps.prepare.outputs.output2 }}" + echo "Output 3: ${{ steps.prepare.outputs.output3 }}" + """); + } + public void testIssue73StepOutputFromTeePipeIsAccepted() { assertWorkflowHighlights(""" name: Issue 73 From 01043db093847705843f4bcf9dd246031a96b62f Mon Sep 17 00:00:00 2001 From: Yuna Morgenstern Date: Fri, 26 Jun 2026 14:09:37 +0200 Subject: [PATCH 2/2] docs: credit grouped output report --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e18670..d5d32ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ ### Fixes -- Step outputs written inside grouped shell redirects to `$GITHUB_OUTPUT` or `$GITEA_OUTPUT` are now recognized. +- Step outputs written inside grouped shell redirects to `$GITHUB_OUTPUT` or `$GITEA_OUTPUT` are now recognized. Thanks + to Adam Głowienka (@aglowienka) for pointing out the grouped-output edge case. ## [2026.6.20] - 2026-06-20