From 6b2a85ade57f84c975a3754a343434436e9f3c3b Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 18:41:59 +0200 Subject: [PATCH 001/190] almost initial commit: add action and some tests --- .github/workflows/test.yml | 45 ++++- Action.java | 323 +++++++++++++++++++++++++++++++++ Dockerfile | 12 ++ README.md | 123 +++++++++++++ action.yml | 54 ++++++ test/resources/test.properties | 24 +++ 6 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 Action.java create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 action.yml create mode 100644 test/resources/test.properties diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cb65e0..9d3e535 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,12 +3,55 @@ on: pull_request jobs: test: - runs-on: ubuntu + runs-on: ubuntu-latest defaults: run: shell: bash steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: 'test (should fail): invalid input: resultType' + id: error-input-resultType + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + resultType: rt1 - name: 'validate "test (should fail): invalid input: resultType"' run: |- [ '${{steps.error-input-resultType.outcome}}' = failure ] + + - name: 'test: query all properties as action outputs' + id: all-output + uses: ./ + with: + file: test/resources/test.properties + - name: 'validate "test: query all properties as action outputs"' + env: + RESULT1: ${{steps.all-output.outputs._sourceJavaVersion}} + RESULT2: ${{steps.all-output.outputs._targetJavaVersion}} + RESULT3: ${{steps.all-output.outputs._org.gradle.jvmargs}} + run: |- + [ "$RESULT1" = 21 ] + [ "$RESULT2" = 17 ] + [ "$RESULT3" = '-ea -showversion' ] + + - name: 'test: query all properties as action outputs' + id: multi-output + uses: ./ + with: + file: test/resources/test.properties + keys: sourceJavaVersion,targetJavaVersion + - name: 'validate "test: query all properties as action outputs"' + env: + RESULT1: ${{steps.multi-output.outputs._sourceJavaVersion}} + RESULT2: ${{steps.multi-output.outputs._targetJavaVersion}} + run: |- + [ "$RESULT1" = 21 ] + [ "$RESULT2" = 17 ] diff --git a/Action.java b/Action.java new file mode 100644 index 0000000..c474166 --- /dev/null +++ b/Action.java @@ -0,0 +1,323 @@ +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.AbstractMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class Action { + static void main(String[] args) throws Exception { + Input input = Input.fromEnv(); + ResultWriter resultWriter = ResultWriter.of(input.resultType()); + for (String file : args) { + Properties properties = input.selectedKeys() == null ? Util.readProperties(file) + : Util.selectProperties(Util.readProperties(file), input.selectedKeys(), file); + resultWriter.write(properties, input); + } + } +} + + +record Input(String[] selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, + String resultNameSeparator, String outputPrefix) { + public static Input fromEnv() { + // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. + String keySeparator = Util.getRequiredEnv("KEY_SEPARATOR"); + // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty + // string: + String keysStr = System.getenv().getOrDefault("KEYS", ""); + String[] keys = Util.splitArray(keysStr, keySeparator, null); + String resultNameSeparator = Util.getRequiredEnv("RESULT_NAME_SEPARATOR"); + + // RESULT_TYPE format: "[:]" + String resultTypeWithArg = Util.getRequiredEnv("RESULT_TYPE"); + Matcher matcher = Pattern.compile("([^:]+)(?::(.*))?").matcher(resultTypeWithArg); + if (!matcher.matches()) { + throw new IllegalArgumentException("invalid resultType: " + resultTypeWithArg); + } + String resultType = matcher.group(1); + String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); + String outputPrefix = Util.getRequiredEnv("OUTPUT_PREFIX"); + return new Input(keys, keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); + } + + public String requiredResultTypeArg() { + String arg = resultTypeArg(); + if ("".equals(arg)) { + throw new IllegalArgumentException("invalid resultType " + resultTypeWithArg() + " (missing argument)"); + } + return arg; + } + + public void requireNoArg() { + if (!"".equals(resultTypeArg())) { + throw new IllegalArgumentException("invalid resultType " + resultTypeWithArg() + " (non-empty argument)"); + } + } +} + + +enum ResultWriter { + OUTPUT("output") { + @Override + public void write(Properties props, Input input) throws IOException { + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + for (Map.Entry entry : Util.stringEntries(props)) { + writer.write(input.outputPrefix() + entry.getKey(), entry.getValue()); + } + + if (props.size() == 1) { + String value = Util.stringEntries(props).iterator().next().getValue(); + writer.write("value", value); + } + } + } + }, + OUTPUT_NAMED("output-named") { + @Override + public void write(Properties props, Input input) throws IOException { + writeNamedImpl(props, input, GitHubOutputFile.OUTPUT); + } + }, + ENV_NAMED("env-named") { + @Override + public void write(Properties props, Input input) throws IOException { + writeNamedImpl(props, input, GitHubOutputFile.ENV); + } + }, + ENV("env") { + @Override + public void write(Properties props, Input input) throws IOException { + String prefix = input.resultTypeArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { + for (Map.Entry entry : Util.stringEntries(props)) { + writer.write(prefix + entry.getKey(), entry.getValue()); + } + } + } + }, + JSON("json") { + @Override + public void write(Properties props, Input input) throws IOException { + input.requireNoArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + writer.write("json", Util.toJson(props)); + } + } + }, + JSON_FILE("json-file") { + @Override + public void write(Properties props, Input input) throws IOException { + try (Writer writer = Util.openFile(input.requiredResultTypeArg(), StandardOpenOption.CREATE)) { + writer.write(Util.toJson(props)); + writer.write('\n'); + writer.flush(); + } + } + }; + + private final String inputName; + + private ResultWriter(String inputName) { + this.inputName = inputName; + } + + public static ResultWriter of(String name) { + for (ResultWriter rw : ResultWriter.values()) { + if (rw.inputName.equals(name)) { + return rw; + } + } + throw new IllegalArgumentException("invalid resultType: " + name); + } + + public abstract void write(Properties props, Input input) throws IOException; + + private static void writeNamedImpl(Properties props, Input input, GitHubOutputFile gitHubOutputFile) throws IOException { + String[] selectedKeys = input.selectedKeys(); + String[] resultNames = Util.splitArray(input.requiredResultTypeArg(), input.resultNameSeparator(), null); + if (resultNames.length != selectedKeys.length && resultNames.length != 1) { + throw new IllegalArgumentException("resultType " + input.resultTypeWithArg() + " has " + resultNames.length + + " arguments, but " + selectedKeys.length + " keys are selected"); + } + try (GitHubVariableWriter writer = gitHubOutputFile.open()) { + for (int i = 0; i < selectedKeys.length; i++) { + String varName = resultNames[resultNames.length == 1 ? 0 : i]; + String value = props.getProperty(selectedKeys[i]); + writer.write(varName, value); + } + } + } +} + + +enum GitHubOutputFile { + OUTPUT("GITHUB_OUTPUT"), ENV("GITHUB_ENV"); + + private final String fileName; + + private GitHubOutputFile(String fileNameEnvVar) { + this.fileName = Util.getRequiredEnv(fileNameEnvVar); + } + + public GitHubVariableWriter open() throws IOException { + return new GitHubVariableWriter(fileName); + } +} + + +class GitHubVariableWriter implements AutoCloseable { + private static final Pattern SIMPLE_VALUE = Pattern.compile("[\\w.-]+"); + + private final Writer writer; + + public GitHubVariableWriter(String fileName) throws IOException { + this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); + } + + public void write(String key, String value) throws IOException { + // write (very) simple values in format "=": + if (SIMPLE_VALUE.matcher(value).matches()) { + this.writer.write(key); + this.writer.write('='); + this.writer.write(value); + this.writer.write('\n'); + return; + } + + // determine separator line which does not occur in value: + Set valueLines = Set.of(value.split("(?s)\n")); + String separatorPart = "----"; + String separator = separatorPart; + while (valueLines.contains(separator)) { + separator = separator + separatorPart; + } + + // write in multiline format + // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings]: + this.writer.write(key); + this.writer.write("<<"); + this.writer.write(separator); + this.writer.write('\n'); + this.writer.write(value); + this.writer.write('\n'); + this.writer.write(separator); + this.writer.write('\n'); + } + + @Override + public void close() throws IOException { + this.writer.close(); + } +} + + +class Util { + private Util() {} + + public static String getRequiredEnv(String varName) { + String value = System.getenv(varName); + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException("missing or empty environment variable " + varName); + } + return value; + } + + public static String[] splitArray(String arrayStr, String separator, String[] defaultResults) { + return arrayStr.isEmpty() ? defaultResults : arrayStr.split(Pattern.quote(separator), -1); + } + + public static Writer openFile(String name, OpenOption... options) throws IOException { + return new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(Paths.get(name), options), StandardCharsets.UTF_8)); + } + + public static Properties readProperties(String file) throws IOException { + Properties allProps = new Properties(); + try (InputStream in = Files.newInputStream(Paths.get(file))) { + allProps.load(in); + } + return allProps; + } + + public static Properties selectProperties(Properties allProps, String[] selectedKeys, String file) { + Set selectedKeysSet = new LinkedHashSet<>(List.of(selectedKeys)); + Properties props = new Properties(); + for (Map.Entry entry : stringEntries(allProps)) { + if (selectedKeysSet.contains(entry.getKey())) { + selectedKeysSet.remove(entry.getKey()); + props.setProperty(entry.getKey(), entry.getValue()); + } + } + for (String key : selectedKeysSet) { + System.err.format("Property %s not found in %s%n", key, file); + } + return props; + } + + public static Iterable> stringEntries(Properties props) { + return () -> new Iterator<>() { + private final Iterator keysIter = props.stringPropertyNames().iterator(); + + @Override + public Map.Entry next() { + String key = keysIter.next(); + return new AbstractMap.SimpleImmutableEntry<>(key, props.getProperty(key)); + } + + @Override + public boolean hasNext() { + return keysIter.hasNext(); + } + }; + } + + public static String toJson(Properties props) { + StringBuilder s = new StringBuilder(50).append('{'); + int initialLength = s.length(); + for (Map.Entry entry : stringEntries(props)) { + s.append(s.length() == initialLength ? '"' : ", \""); + appendJsonString(s, entry.getKey()); + s.append("\":\""); + appendJsonString(s, entry.getValue()); + s.append('"'); + } + return s.append('}').toString(); + } + + private static void appendJsonString(StringBuilder buffer, String s) { + for (char c : s.toCharArray()) { + if (c == '"') { + buffer.append('\\').append('"'); + } else if (c == '\t') { + buffer.append('\\').append('t'); + } else if (c == '\n') { + buffer.append('\\').append('n'); + } else if (c == '\r') { + buffer.append('\\').append('r'); + } else if (c == '\f') { + buffer.append('\\').append('f'); + } else if (c == '\b') { + buffer.append('\\').append('b'); + } else if (c < ' ' || Character.isWhitespace(c)) { + buffer.append('\\').append('u').append(String.format("%04X", (int) c)); + } else { + buffer.append(c); + } + } + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4a43212 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM azul/zulu-openjdk:25-latest + +LABEL "com.github.actions.name"="read Java properties" +LABEL "com.github.actions.description"="read Java properties file and return one or more as property values as plain text or JSON" +LABEL "repository"="https://github.com/freenet-actions/read-java-properties" +LABEL "homepage"="https://github.com/freenet-actions" +LABEL "maintainer"="Team MCBS Core " + +WORKDIR /action +COPY *.java . + +ENTRYPOINT ["java", "Action.java"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5330f0a --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# read-java-properties + +Github Action to read a Java .properties file and output one, multiple, or all properties as plain strings or JSON. + +## Usage example: + +Suppose file gradle.properties contains properties `sourceJavaVersion= 21`, `targetJavaVersion= 17`, and `org.gradle.jvmargs= -ea -showversion`. + +Query all properties as action outputs: +``` +- id: readProp + uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties +``` +⇒ `${{steps.readProp.outputs._sourceJavaVersion}}` == `21`, `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, `${{steps.readProp.outputs._org.gradle.jvmargs}}` == `-ea -showversion`. + +Query multiple properties as action outputs: +``` +- id: readProp + uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: sourceJavaVersion,targetJavaVersion +``` +or with another key separator: +``` +- id: readProp + uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: "sourceJavaVersion; targetJavaVersion" + keySeparator: "; " +``` +⇒ `${{steps.readProp.outputs._sourceJavaVersion}}` == `21`, `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, `${{steps.readProp.outputs.value}}` == `17`. + +Query multiple properties as action output of which only one is found: +``` +- id: readProp + uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: foo,targetJavaVersion,bar +``` +⇒ `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, `${{steps.readProp.outputs.value}}` == `17`. + +Query a single property as action output: +``` +- id: readProp + uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: org.gradle.jvmargs +``` +⇒ `${{steps.readProp.outputs.org.gradle.jvmargs}}` == `-ea -showversion`, `${{steps.readProp.outputs.value}}` == `-ea -showversion`. + +Query all properties as environment variables of the same names: +``` +- uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + resultType: env +``` +⇒ Environment variables `sourceJavaVersion` == `21`, `targetJavaVersion` == `17`, `org.gradle.jvmargs` == `-ea -showversion`. + +Query all properties as environment variables with prefix: +``` +- uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + resultType: "env:GRADLE_PROP_" +``` +⇒ Environment variables `GRADLE_PROP_sourceJavaVersion` == `21`, `GRADLE_PROP_targetJavaVersion` == `17`, `GRADLE_PROP_org.gradle.jvmargs` == `-ea -showversion`. + +Query multiple properties as environment variables: +``` +- uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: sourceJavaVersion,targetJavaVersion + resultType: env +``` +⇒ Environment variables `sourceJavaVersion` == `21`, `targetJavaVersion` == `17`. + +Query multiple properties as environment variables of given names: +``` +- uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: sourceJavaVersion,targetJavaVersion + resultType: env-named:SOURCE_JAVA_VERSION,TARGET_JAVA_VERSION +``` +⇒ Environment variables `SOURCE_JAVA_VERSION` == `21`, `TARGET_JAVA_VERSION` == `17`. + +Query multiple (alternative) properties as a single environment variable: +``` +- uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: sourceJavaVersion,targetJavaVersion + resultType: "env-named:JAVA_VERSION" +``` +⇒ Environment variable `JAVA_VERSION` == `17`. + +Query all properties as JSON action output: +``` +- id: readProp + uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + resultType: json +``` +⇒ `${{steps.readProp.outputs.json}}` == `{"sourceJavaVersion": "21", "targetJavaVersion": "17", "org.gradle.jvmargs": "-ea -showversion"}` (or similar; order not guaranteed). + +Query all properties as JSON file: +``` +- id: readProp + uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + resultType: json-file:/tmp/gradle-properties.json +``` +⇒ File /tmp/gradle-properties.json contains `{"sourceJavaVersion": "21", "targetJavaVersion": "17", "org.gradle.jvmargs": "-ea -showversion"}` (or similar; order and formatting not guaranteed). diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..e2a1491 --- /dev/null +++ b/action.yml @@ -0,0 +1,54 @@ +name: Read Java Properties File +author: freenet-actions +description: >- + Read Java properties file and return one or more as property values as plain text or JSON. + +inputs: + file: + description: the properties file name + required: true + keys: + description: |- + Selects the property keys (names). Format: `keySeparator`-separated list. + If not specified or empty, all properties are returned. + required: false + keySeparator: + description: 'The separator string used in `keys`' + required: false + default: ' ' + resultNameSeparator: + description: 'The separator string used in some types of `resultType`' + required: false + default: ' ' + resultType: + description: |- + The result format and target. One of: + - "output": For each property key , set an output named "_". (The leading underscore serves to avoid conflicts with future output names used by this action.) Additionally, set output "value" to the last found value (which is only usefull if `keys` is non-empty, i.e. last given key wins; without keys, the last property in the undefined java.util.Properties iteration order wins). + - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. + - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. + - "env": For each property key , set an environment variable . + - "env:": For each property key , set an environment variable . + - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. + - "env-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same environment variable; the last found key wins. In this mode, `keys` is required. + - "json": Set an action output "json" to all properties as a JSON object, formatted as a single-line string. + - "json-file:": Write a file with name with contents: all properties as a JSON object (formatting not specified). + required: false + default: 'output' + +outputs: + value: + description: The (plain) property value. Only set in certain modes, see input `resultType`. + json: + description: All found properties matching the `keys` input as a single-line JSON object. Only set in certain modes, see input `resultType`. + +runs: + using: docker + image: Dockerfile + args: + - ${{inputs.file}} + env: + KEYS: ${{inputs.keys}} + KEY_SEPARATOR: ${{inputs.keySeparator}} + RESULT_NAME_SEPARATOR: ${{inputs.resultNameSeparator}} + RESULT_TYPE: ${{inputs.resultType}} + OUTPUT_PREFIX: '_' diff --git a/test/resources/test.properties b/test/resources/test.properties new file mode 100644 index 0000000..22a896e --- /dev/null +++ b/test/resources/test.properties @@ -0,0 +1,24 @@ +sourceJavaVersion : 21 +targetJavaVersion : 17 +org.gradle.jvmargs: -ea -showversion + +colon\:in\:Key: c +whitespace_escapes = a\fb\rc\n +uni = a\u00AB,\u00BBc + +# For white box tests: When writing to $GITHUB_OUTPUT or $GITHUB_ENV, "----" is the default delimiter used in this action. +# Whenever the value contains a line "----", another delimiter must be chosen. +dash_1 = - +dash_2 = -- +dash_3 = --- +dash_4 = ---- +dash_5 = ----- +dash_6 = ------ +dash_7 = ------- +dash_8 = -------- +dash_9 = --------- +dash_10 = ---------- +dash_11 = ----------- +dash_12 = ------------ +dash_3_4_1 = ---\n----\n- +dash_3_4_8_1 = ---\n----\n--------\n- From 539e9057ae12928deec36f6a8366fbad674e39a9 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 18:48:13 +0200 Subject: [PATCH 002/190] encode punctuation chars in output names --- .github/workflows/test.yml | 2 +- Action.java | 33 ++++++++++++++++++++++++++++----- action.yml | 4 ++++ test/resources/test.properties | 4 ++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d3e535..23c3d17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: env: RESULT1: ${{steps.all-output.outputs._sourceJavaVersion}} RESULT2: ${{steps.all-output.outputs._targetJavaVersion}} - RESULT3: ${{steps.all-output.outputs._org.gradle.jvmargs}} + RESULT3: ${{steps.all-output.outputs._org-002Egradle-002Ejvmargs}} run: |- [ "$RESULT1" = 21 ] [ "$RESULT2" = 17 ] diff --git a/Action.java b/Action.java index c474166..756f1cd 100644 --- a/Action.java +++ b/Action.java @@ -13,9 +13,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -167,16 +169,33 @@ private static void writeNamedImpl(Properties props, Input input, GitHubOutputFi enum GitHubOutputFile { - OUTPUT("GITHUB_OUTPUT"), ENV("GITHUB_ENV"); + OUTPUT("GITHUB_OUTPUT", v -> encodeOutputValue(v)), // + ENV("GITHUB_ENV"); private final String fileName; + private final Function keyReplacer; private GitHubOutputFile(String fileNameEnvVar) { + this(fileNameEnvVar, v -> v); + } + + private GitHubOutputFile(String fileNameEnvVar, Function valueReplacer) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); + this.keyReplacer = valueReplacer; } public GitHubVariableWriter open() throws IOException { - return new GitHubVariableWriter(fileName); + return new GitHubVariableWriter(fileName, keyReplacer); + } + + private static String encodeOutputValue(String value) { + StringBuilder result = new StringBuilder(value.length() + 4); + Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(value); + while (matcher.find()) { + matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); + } + matcher.appendTail(result); + return result.toString(); } } @@ -184,16 +203,20 @@ public GitHubVariableWriter open() throws IOException { class GitHubVariableWriter implements AutoCloseable { private static final Pattern SIMPLE_VALUE = Pattern.compile("[\\w.-]+"); + private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String fileName) throws IOException { + public GitHubVariableWriter(String fileName, Function valueReplacer) throws IOException { this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); + this.keyReplacer = Objects.requireNonNull(valueReplacer); } public void write(String key, String value) throws IOException { + String encodedKey = keyReplacer.apply(key); + // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { - this.writer.write(key); + this.writer.write(encodedKey); this.writer.write('='); this.writer.write(value); this.writer.write('\n'); @@ -210,7 +233,7 @@ public void write(String key, String value) throws IOException { // write in multiline format // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings]: - this.writer.write(key); + this.writer.write(encodedKey); this.writer.write("<<"); this.writer.write(separator); this.writer.write('\n'); diff --git a/action.yml b/action.yml index e2a1491..dc2d6d7 100644 --- a/action.yml +++ b/action.yml @@ -32,6 +32,10 @@ inputs: - "env-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same environment variable; the last found key wins. In this mode, `keys` is required. - "json": Set an action output "json" to all properties as a JSON object, formatted as a single-line string. - "json-file:": Write a file with name with contents: all properties as a JSON object (formatting not specified). + + For all output names, punctuation characters except the underscore ("_") are replaced with "-" followed by four hex digits + of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name + "a-002Eb-002Dc_d". required: false default: 'output' diff --git a/test/resources/test.properties b/test/resources/test.properties index 22a896e..9b0b9b0 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -3,6 +3,10 @@ targetJavaVersion : 17 org.gradle.jvmargs: -ea -showversion colon\:in\:Key: c +colonInValue: cv: +dash-in-Key: d +dashInValue: d- + whitespace_escapes = a\fb\rc\n uni = a\u00AB,\u00BBc From fc52849a1af9eaa8e5a4320dd8f4623bde303f3c Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 18:53:11 +0200 Subject: [PATCH 003/190] fix test: default keySeparator is space --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23c3d17..ad13a22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,13 +42,13 @@ jobs: [ "$RESULT2" = 17 ] [ "$RESULT3" = '-ea -showversion' ] - - name: 'test: query all properties as action outputs' + - name: 'test: query multiple properties as action outputs' id: multi-output uses: ./ with: file: test/resources/test.properties - keys: sourceJavaVersion,targetJavaVersion - - name: 'validate "test: query all properties as action outputs"' + keys: 'sourceJavaVersion targetJavaVersion' + - name: 'validate "test: query multiple properties as action outputs"' env: RESULT1: ${{steps.multi-output.outputs._sourceJavaVersion}} RESULT2: ${{steps.multi-output.outputs._targetJavaVersion}} From 3b72ca4410eeae095ba6305c079b3daa97f68a2e Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 19:01:01 +0200 Subject: [PATCH 004/190] add stderr logging --- .github/workflows/test.yml | 20 ++++++++++---------- Action.java | 7 +++++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad13a22..b1d473b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,13 +34,13 @@ jobs: file: test/resources/test.properties - name: 'validate "test: query all properties as action outputs"' env: - RESULT1: ${{steps.all-output.outputs._sourceJavaVersion}} - RESULT2: ${{steps.all-output.outputs._targetJavaVersion}} - RESULT3: ${{steps.all-output.outputs._org-002Egradle-002Ejvmargs}} + RESULT_SJV: ${{steps.all-output.outputs._sourceJavaVersion}} + RESULT_TJV: ${{steps.all-output.outputs._targetJavaVersion}} + RESULT_GRO: ${{steps.all-output.outputs._org-002Egradle-002Ejvmargs}} run: |- - [ "$RESULT1" = 21 ] - [ "$RESULT2" = 17 ] - [ "$RESULT3" = '-ea -showversion' ] + [ "$RESULT_SJV" = 21 ] + [ "$RESULT_TJV" = 17 ] + [ "$RESULT_GRO" = '-ea -showversion' ] - name: 'test: query multiple properties as action outputs' id: multi-output @@ -50,8 +50,8 @@ jobs: keys: 'sourceJavaVersion targetJavaVersion' - name: 'validate "test: query multiple properties as action outputs"' env: - RESULT1: ${{steps.multi-output.outputs._sourceJavaVersion}} - RESULT2: ${{steps.multi-output.outputs._targetJavaVersion}} + RESULT_SJV: ${{steps.multi-output.outputs._sourceJavaVersion}} + RESULT_TJV: ${{steps.multi-output.outputs._targetJavaVersion}} run: |- - [ "$RESULT1" = 21 ] - [ "$RESULT2" = 17 ] + [ "$RESULT_SJV" = 21 ] + [ "$RESULT_TJV" = 17 ] diff --git a/Action.java b/Action.java index 756f1cd..839b99a 100644 --- a/Action.java +++ b/Action.java @@ -185,7 +185,7 @@ private GitHubOutputFile(String fileNameEnvVar, Function valueRe } public GitHubVariableWriter open() throws IOException { - return new GitHubVariableWriter(fileName, keyReplacer); + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, keyReplacer); } private static String encodeOutputValue(String value) { @@ -203,16 +203,19 @@ private static String encodeOutputValue(String value) { class GitHubVariableWriter implements AutoCloseable { private static final Pattern SIMPLE_VALUE = Pattern.compile("[\\w.-]+"); + private final String description; private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String fileName, Function valueReplacer) throws IOException { + public GitHubVariableWriter(String description, String fileName, Function valueReplacer) throws IOException { + this.description = description; this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); this.keyReplacer = Objects.requireNonNull(valueReplacer); } public void write(String key, String value) throws IOException { String encodedKey = keyReplacer.apply(key); + System.err.format("%s %s\t:= \"%s\"\n", description, encodedKey, value); // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { From c7a83e73d8f71cfff375a09e31df5f018bbbb157 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 21:00:57 +0200 Subject: [PATCH 005/190] resultType "json-file": create parent dir --- Action.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Action.java b/Action.java index 839b99a..1cf2cf0 100644 --- a/Action.java +++ b/Action.java @@ -125,7 +125,9 @@ public void write(Properties props, Input input) throws IOException { JSON_FILE("json-file") { @Override public void write(Properties props, Input input) throws IOException { - try (Writer writer = Util.openFile(input.requiredResultTypeArg(), StandardOpenOption.CREATE)) { + String outputFile = input.requiredResultTypeArg(); + Files.createDirectories((Paths.get(outputFile).getParent())); + try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE)) { writer.write(Util.toJson(props)); writer.write('\n'); writer.flush(); From 368470c4ac9f6375d8842449f6e83a49de1d4433 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 21:03:33 +0200 Subject: [PATCH 006/190] more tests --- .github/workflows/test.yml | 72 ++++++++++++++++++++++++++++++++++ test/resources/test.properties | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1d473b..48c1ce7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,8 @@ jobs: defaults: run: shell: bash + env: + SORT_OBJECT_JQ_EXPR: 'to_entries | sort_by(.key) []' steps: - name: Checkout @@ -55,3 +57,73 @@ jobs: run: |- [ "$RESULT_SJV" = 21 ] [ "$RESULT_TJV" = 17 ] + + - name: 'test: query all properties as environment variables' + id: all-env + uses: ./ + with: + file: test/resources/test.properties + resultType: env + - name: 'validate "test: query all properties as environment variables"' + run: |- + [ "$sourceJavaVersion" = 21 ] + [ "$targetJavaVersion" = 17 ] + [ "$org.gradle.jvmargs" = '-ea -showversion' ] + [ "$whitespace_escapes" = $'a\fb\rc\n' ] + [ "$unicode_escapes" = 'a«,»c' ] + + - name: 'test: query multiple properties as environment variables' + id: multi-env + uses: ./ + with: + file: test/resources/test.properties + keys: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes' + resultType: env + - name: 'validate "test: query multiple properties as environment variables"' + run: |- + [ "$sourceJavaVersion" = 21 ] + [ "$org.gradle.jvmargs" = '-ea -showversion' ] + [ "$unicode_escapes" = 'a«,»c' ] + [ "$targetJavaVersion" = '' ] + [ "$whitespace_escapes" = '' ] + + - name: 'test: query all properties as JSON action output' + id: all-json + uses: ./ + with: + file: test/resources/test.properties + resultType: json + - name: 'validate "test: query all properties as JSON action output"' + env: + RESULT: ${{steps.all-json.outputs.json}} + run: |- + jq <<<"$RESULT" + [ "$(jq --raw-output '.unicode_escapes')" = 'a«,»c' ] + [ "$(jq --raw-output '.["colon:in:Key"]')" = c ] + + - name: 'test: query multiple properties as JSON action output' + id: multiple-json + uses: ./ + with: + file: test/resources/test.properties + keys: 'colon:in:Key unicode_escapes' + resultType: json + - name: 'validate "test: query multiple properties as JSON action output"' + env: + RESULT: ${{steps.multiple-json.outputs.json}} + run: |- + diff --minimal \ + <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $SORT_OBJECT_JQ_EXPR") \ + <(jq "$SORT_OBJECT_JQ_EXPR" <<<"$RESULT") + + - name: 'test: query multiple properties as JSON file' + uses: ./ + with: + file: test/resources/test.properties + keys: 'colon:in:Key unicode_escapes' + resultType: json-file:tmp/multi.json + - name: 'validate "test: query multiple properties as JSON file"' + run: |- + diff --minimal \ + <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $SORT_OBJECT_JQ_EXPR") \ + <(jq "$SORT_OBJECT_JQ_EXPR" Date: Fri, 24 Apr 2026 21:25:45 +0200 Subject: [PATCH 007/190] tests: print message on assertion errors --- .github/workflows/test.yml | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48c1ce7..82961e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,8 @@ jobs: run: shell: bash env: + ASSERT_EQUALS: >- + [ "$1" = "$2" ] || { printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2; exit 1; } SORT_OBJECT_JQ_EXPR: 'to_entries | sort_by(.key) []' steps: @@ -27,7 +29,7 @@ jobs: resultType: rt1 - name: 'validate "test (should fail): invalid input: resultType"' run: |- - [ '${{steps.error-input-resultType.outcome}}' = failure ] + bash -c "$ASSERT_EQUALS" -- '${{steps.error-input-resultType.outcome}}' failure - name: 'test: query all properties as action outputs' id: all-output @@ -40,9 +42,9 @@ jobs: RESULT_TJV: ${{steps.all-output.outputs._targetJavaVersion}} RESULT_GRO: ${{steps.all-output.outputs._org-002Egradle-002Ejvmargs}} run: |- - [ "$RESULT_SJV" = 21 ] - [ "$RESULT_TJV" = 17 ] - [ "$RESULT_GRO" = '-ea -showversion' ] + bash -c "$ASSERT_EQUALS" -- "$RESULT_SJV" 21 + bash -c "$ASSERT_EQUALS" -- "$RESULT_TJV" 17 + bash -c "$ASSERT_EQUALS" -- "$RESULT_GRO" '-ea -showversion' - name: 'test: query multiple properties as action outputs' id: multi-output @@ -55,8 +57,8 @@ jobs: RESULT_SJV: ${{steps.multi-output.outputs._sourceJavaVersion}} RESULT_TJV: ${{steps.multi-output.outputs._targetJavaVersion}} run: |- - [ "$RESULT_SJV" = 21 ] - [ "$RESULT_TJV" = 17 ] + bash -c "$ASSERT_EQUALS" "$RESULT_SJV" 21 + bash -c "$ASSERT_EQUALS" "$RESULT_TJV" 17 - name: 'test: query all properties as environment variables' id: all-env @@ -66,11 +68,11 @@ jobs: resultType: env - name: 'validate "test: query all properties as environment variables"' run: |- - [ "$sourceJavaVersion" = 21 ] - [ "$targetJavaVersion" = 17 ] - [ "$org.gradle.jvmargs" = '-ea -showversion' ] - [ "$whitespace_escapes" = $'a\fb\rc\n' ] - [ "$unicode_escapes" = 'a«,»c' ] + bash -c "$ASSERT_EQUALS" -- "$sourceJavaVersion" 21 + bash -c "$ASSERT_EQUALS" -- "$targetJavaVersion" 17 + bash -c "$ASSERT_EQUALS" -- "$org.gradle.jvmargs" '-ea -showversion' + bash -c "$ASSERT_EQUALS" -- "$whitespace_escapes" $'a\fb\rc\n' + bash -c "$ASSERT_EQUALS" -- "$unicode_escapes" 'a«,»c' - name: 'test: query multiple properties as environment variables' id: multi-env @@ -81,11 +83,11 @@ jobs: resultType: env - name: 'validate "test: query multiple properties as environment variables"' run: |- - [ "$sourceJavaVersion" = 21 ] - [ "$org.gradle.jvmargs" = '-ea -showversion' ] - [ "$unicode_escapes" = 'a«,»c' ] - [ "$targetJavaVersion" = '' ] - [ "$whitespace_escapes" = '' ] + bash -c "$ASSERT_EQUALS" -- "$sourceJavaVersion" 21 + bash -c "$ASSERT_EQUALS" -- "$org.gradle.jvmargs" '-ea -showversion' + bash -c "$ASSERT_EQUALS" -- "$unicode_escapes" 'a«,»c' + bash -c "$ASSERT_EQUALS" -- "$targetJavaVersion" '' + bash -c "$ASSERT_EQUALS" -- "$whitespace_escapes" '' - name: 'test: query all properties as JSON action output' id: all-json @@ -98,8 +100,8 @@ jobs: RESULT: ${{steps.all-json.outputs.json}} run: |- jq <<<"$RESULT" - [ "$(jq --raw-output '.unicode_escapes')" = 'a«,»c' ] - [ "$(jq --raw-output '.["colon:in:Key"]')" = c ] + bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.unicode_escapes')" 'a«,»c' + bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '["colon:in:Key"]')" c - name: 'test: query multiple properties as JSON action output' id: multiple-json From 75b8e28d5e4a922e428813736d2970080c709b43 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 21:30:04 +0200 Subject: [PATCH 008/190] fix test: missing -- in assertion --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82961e2..511f958 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,8 +57,8 @@ jobs: RESULT_SJV: ${{steps.multi-output.outputs._sourceJavaVersion}} RESULT_TJV: ${{steps.multi-output.outputs._targetJavaVersion}} run: |- - bash -c "$ASSERT_EQUALS" "$RESULT_SJV" 21 - bash -c "$ASSERT_EQUALS" "$RESULT_TJV" 17 + bash -c "$ASSERT_EQUALS" -- "$RESULT_SJV" 21 + bash -c "$ASSERT_EQUALS" -- "$RESULT_TJV" 17 - name: 'test: query all properties as environment variables' id: all-env From bf2c9c11d4e1b16ee051d4bbc70c8f966886df59 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 21:34:20 +0200 Subject: [PATCH 009/190] fix tests: bash does not support variable names with dot --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 511f958..8b01a98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,7 +70,7 @@ jobs: run: |- bash -c "$ASSERT_EQUALS" -- "$sourceJavaVersion" 21 bash -c "$ASSERT_EQUALS" -- "$targetJavaVersion" 17 - bash -c "$ASSERT_EQUALS" -- "$org.gradle.jvmargs" '-ea -showversion' + bash -c "$ASSERT_EQUALS" -- "$(printenv org.gradle.jvmargs)" '-ea -showversion' bash -c "$ASSERT_EQUALS" -- "$whitespace_escapes" $'a\fb\rc\n' bash -c "$ASSERT_EQUALS" -- "$unicode_escapes" 'a«,»c' @@ -84,7 +84,7 @@ jobs: - name: 'validate "test: query multiple properties as environment variables"' run: |- bash -c "$ASSERT_EQUALS" -- "$sourceJavaVersion" 21 - bash -c "$ASSERT_EQUALS" -- "$org.gradle.jvmargs" '-ea -showversion' + bash -c "$ASSERT_EQUALS" -- "$(printenv org.gradle.jvmargs)" '-ea -showversion' bash -c "$ASSERT_EQUALS" -- "$unicode_escapes" 'a«,»c' bash -c "$ASSERT_EQUALS" -- "$targetJavaVersion" '' bash -c "$ASSERT_EQUALS" -- "$whitespace_escapes" '' From 18deed8618017cf08ff93d9cd8bf70a991726877 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 21:41:39 +0200 Subject: [PATCH 010/190] fix tests with env variables: isolation --- .github/workflows/test.yml | 79 ++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b01a98..4595fac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,18 +1,17 @@ name: Test on: pull_request +defaults: + run: + shell: bash +env: + ASSERT_EQUALS: >- + [ "$1" = "$2" ] || { printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2; exit 1; } + SORT_OBJECT_JQ_EXPR: 'to_entries | sort_by(.key) []' jobs: test: runs-on: ubuntu-latest - defaults: - run: - shell: bash - env: - ASSERT_EQUALS: >- - [ "$1" = "$2" ] || { printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2; exit 1; } - SORT_OBJECT_JQ_EXPR: 'to_entries | sort_by(.key) []' steps: - - name: Checkout uses: actions/checkout@v6 with: @@ -60,35 +59,6 @@ jobs: bash -c "$ASSERT_EQUALS" -- "$RESULT_SJV" 21 bash -c "$ASSERT_EQUALS" -- "$RESULT_TJV" 17 - - name: 'test: query all properties as environment variables' - id: all-env - uses: ./ - with: - file: test/resources/test.properties - resultType: env - - name: 'validate "test: query all properties as environment variables"' - run: |- - bash -c "$ASSERT_EQUALS" -- "$sourceJavaVersion" 21 - bash -c "$ASSERT_EQUALS" -- "$targetJavaVersion" 17 - bash -c "$ASSERT_EQUALS" -- "$(printenv org.gradle.jvmargs)" '-ea -showversion' - bash -c "$ASSERT_EQUALS" -- "$whitespace_escapes" $'a\fb\rc\n' - bash -c "$ASSERT_EQUALS" -- "$unicode_escapes" 'a«,»c' - - - name: 'test: query multiple properties as environment variables' - id: multi-env - uses: ./ - with: - file: test/resources/test.properties - keys: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes' - resultType: env - - name: 'validate "test: query multiple properties as environment variables"' - run: |- - bash -c "$ASSERT_EQUALS" -- "$sourceJavaVersion" 21 - bash -c "$ASSERT_EQUALS" -- "$(printenv org.gradle.jvmargs)" '-ea -showversion' - bash -c "$ASSERT_EQUALS" -- "$unicode_escapes" 'a«,»c' - bash -c "$ASSERT_EQUALS" -- "$targetJavaVersion" '' - bash -c "$ASSERT_EQUALS" -- "$whitespace_escapes" '' - - name: 'test: query all properties as JSON action output' id: all-json uses: ./ @@ -129,3 +99,38 @@ jobs: diff --minimal \ <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $SORT_OBJECT_JQ_EXPR") \ <(jq "$SORT_OBJECT_JQ_EXPR" Date: Fri, 24 Apr 2026 21:45:28 +0200 Subject: [PATCH 011/190] fix tests: missing jq input; missing checkout --- .github/workflows/test.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4595fac..4280ded 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,8 +70,8 @@ jobs: RESULT: ${{steps.all-json.outputs.json}} run: |- jq <<<"$RESULT" - bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.unicode_escapes')" 'a«,»c' - bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '["colon:in:Key"]')" c + bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' + bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '["colon:in:Key"]' <<<"$RESULT")" c - name: 'test: query multiple properties as JSON action output' id: multiple-json @@ -104,6 +104,13 @@ jobs: test-all-env: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: 'test: query all properties as environment variables' uses: ./ with: @@ -120,6 +127,13 @@ jobs: test-multi-env: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: 'test: query multiple properties as environment variables' uses: ./ with: From 58dccb172fbfea817a7e58034b589bc85c100b3c Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 21:47:19 +0200 Subject: [PATCH 012/190] fix test: missing "." in jq expr --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4280ded..e1edfc1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,8 +70,8 @@ jobs: RESULT: ${{steps.all-json.outputs.json}} run: |- jq <<<"$RESULT" - bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' - bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '["colon:in:Key"]' <<<"$RESULT")" c + bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' + bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" c - name: 'test: query multiple properties as JSON action output' id: multiple-json From 258195a7b42b4e3cd3a51323ad5a3be967c3e4be Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 24 Apr 2026 21:51:36 +0200 Subject: [PATCH 013/190] stderr logging also for succeeding assertions --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1edfc1..ee637ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ defaults: shell: bash env: ASSERT_EQUALS: >- - [ "$1" = "$2" ] || { printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2; exit 1; } + if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2; else printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2; exit 1; fi SORT_OBJECT_JQ_EXPR: 'to_entries | sort_by(.key) []' jobs: From 85d731b77985230ee4270be90bfc87f49d46e9e6 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 11 May 2026 17:35:09 +0200 Subject: [PATCH 014/190] YAML cleanup: use anchors for repeated step --- .github/workflows/test.yml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee637ab..23e2e33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,8 @@ jobs: test: runs-on: ubuntu-latest steps: - - name: Checkout + - &checkoutStep + name: Checkout uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} @@ -104,13 +105,7 @@ jobs: test-all-env: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - + - *checkoutStep - name: 'test: query all properties as environment variables' uses: ./ with: @@ -127,13 +122,7 @@ jobs: test-multi-env: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - + - *checkoutStep - name: 'test: query multiple properties as environment variables' uses: ./ with: From bdd177321858ac3f00f3291175e9c4a534016441 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 11 May 2026 18:30:28 +0200 Subject: [PATCH 015/190] test workflow: simplify bash code reuse --- .github/workflows/test.yml | 66 ++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23e2e33..da1c159 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,16 @@ defaults: run: shell: bash env: - ASSERT_EQUALS: >- - if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2; else printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2; exit 1; fi - SORT_OBJECT_JQ_EXPR: 'to_entries | sort_by(.key) []' + SHELL_SETUP: >- + function assertEquals() { + if [ "$1" = "$2" ]; then + printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 + else + printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 + return 1 + fi + } + sortObjectJqExpr='to_entries | sort_by(.key) []' jobs: test: @@ -29,7 +36,8 @@ jobs: resultType: rt1 - name: 'validate "test (should fail): invalid input: resultType"' run: |- - bash -c "$ASSERT_EQUALS" -- '${{steps.error-input-resultType.outcome}}' failure + eval "$SHELL_SETUP" + assertEquals '${{steps.error-input-resultType.outcome}}' failure - name: 'test: query all properties as action outputs' id: all-output @@ -42,9 +50,10 @@ jobs: RESULT_TJV: ${{steps.all-output.outputs._targetJavaVersion}} RESULT_GRO: ${{steps.all-output.outputs._org-002Egradle-002Ejvmargs}} run: |- - bash -c "$ASSERT_EQUALS" -- "$RESULT_SJV" 21 - bash -c "$ASSERT_EQUALS" -- "$RESULT_TJV" 17 - bash -c "$ASSERT_EQUALS" -- "$RESULT_GRO" '-ea -showversion' + eval "$SHELL_SETUP" + assertEquals "$RESULT_SJV" 21 + assertEquals "$RESULT_TJV" 17 + assertEquals "$RESULT_GRO" '-ea -showversion' - name: 'test: query multiple properties as action outputs' id: multi-output @@ -57,8 +66,9 @@ jobs: RESULT_SJV: ${{steps.multi-output.outputs._sourceJavaVersion}} RESULT_TJV: ${{steps.multi-output.outputs._targetJavaVersion}} run: |- - bash -c "$ASSERT_EQUALS" -- "$RESULT_SJV" 21 - bash -c "$ASSERT_EQUALS" -- "$RESULT_TJV" 17 + eval "$SHELL_SETUP" + assertEquals "$RESULT_SJV" 21 + assertEquals "$RESULT_TJV" 17 - name: 'test: query all properties as JSON action output' id: all-json @@ -70,9 +80,10 @@ jobs: env: RESULT: ${{steps.all-json.outputs.json}} run: |- - jq <<<"$RESULT" - bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' - bash -c "$ASSERT_EQUALS" -- "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" c + eval "$SHELL_SETUP" + jq <<<"$RESULT" # assert parseable (and print) + assertEquals "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' + assertEquals "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" c - name: 'test: query multiple properties as JSON action output' id: multiple-json @@ -86,8 +97,8 @@ jobs: RESULT: ${{steps.multiple-json.outputs.json}} run: |- diff --minimal \ - <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $SORT_OBJECT_JQ_EXPR") \ - <(jq "$SORT_OBJECT_JQ_EXPR" <<<"$RESULT") + <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $sortObjectJqExpr") \ + <(jq "$sortObjectJqExpr" <<<"$RESULT") - name: 'test: query multiple properties as JSON file' uses: ./ @@ -98,8 +109,8 @@ jobs: - name: 'validate "test: query multiple properties as JSON file"' run: |- diff --minimal \ - <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $SORT_OBJECT_JQ_EXPR") \ - <(jq "$SORT_OBJECT_JQ_EXPR" Date: Mon, 11 May 2026 18:36:05 +0200 Subject: [PATCH 016/190] test workflow: fix missing semicolon in eval'ed string --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da1c159..2934c6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,8 +12,8 @@ env: printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 return 1 fi - } - sortObjectJqExpr='to_entries | sort_by(.key) []' + }; + sortObjectJqExpr='to_entries | sort_by(.key) []'; jobs: test: From 1ad8d8e045a8e31754b946b0e3f51ca3fbeb5b09 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 11 May 2026 18:38:25 +0200 Subject: [PATCH 017/190] test workflow: make SHELL_SETUP variable less error-prone --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2934c6d..d367996 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,7 @@ defaults: run: shell: bash env: - SHELL_SETUP: >- + SHELL_SETUP: |- function assertEquals() { if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 @@ -12,8 +12,8 @@ env: printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 return 1 fi - }; - sortObjectJqExpr='to_entries | sort_by(.key) []'; + } + sortObjectJqExpr='to_entries | sort_by(.key) []' jobs: test: From be44bf35ba24fc8f914bf5f824174c3ab29d5689 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 11 May 2026 19:07:46 +0200 Subject: [PATCH 018/190] test workflow: fix missing $sortObjectJqExpr --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d367996..7afdc80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,6 +96,7 @@ jobs: env: RESULT: ${{steps.multiple-json.outputs.json}} run: |- + eval "$SHELL_SETUP" diff --minimal \ <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $sortObjectJqExpr") \ <(jq "$sortObjectJqExpr" <<<"$RESULT") @@ -108,6 +109,7 @@ jobs: resultType: json-file:tmp/multi.json - name: 'validate "test: query multiple properties as JSON file"' run: |- + eval "$SHELL_SETUP" diff --minimal \ <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $sortObjectJqExpr") \ <(jq "$sortObjectJqExpr" Date: Mon, 11 May 2026 19:10:48 +0200 Subject: [PATCH 019/190] test workflow: simplify assertions One value per assertion instead of sorted JSON diff. Plus: add JSON size assertions. --- .github/workflows/test.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7afdc80..c0134ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,6 @@ env: return 1 fi } - sortObjectJqExpr='to_entries | sort_by(.key) []' jobs: test: @@ -82,24 +81,29 @@ jobs: run: |- eval "$SHELL_SETUP" jq <<<"$RESULT" # assert parseable (and print) + # check some values: assertEquals "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' - assertEquals "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" c + assertEquals "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" 'c' + # assert size >= 10: + test "$(jq --raw-output 'keys | length' <<<"$RESULT")" -ge 10 - name: 'test: query multiple properties as JSON action output' id: multiple-json uses: ./ with: file: test/resources/test.properties - keys: 'colon:in:Key unicode_escapes' + keys: 'colon:in:Key colonInValue unicode_escapes' resultType: json - name: 'validate "test: query multiple properties as JSON action output"' env: RESULT: ${{steps.multiple-json.outputs.json}} run: |- eval "$SHELL_SETUP" - diff --minimal \ - <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $sortObjectJqExpr") \ - <(jq "$sortObjectJqExpr" <<<"$RESULT") + assertEquals "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' + assertEquals "$(jq --raw-output '.colonInValue' <<<"$RESULT")" 'cv:' + assertEquals "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" 'c' + # assert size 3 (i.e. no additional results): + assertEquals "$(jq --raw-output 'keys | length' <<<"$RESULT")" 3 - name: 'test: query multiple properties as JSON file' uses: ./ @@ -110,9 +114,11 @@ jobs: - name: 'validate "test: query multiple properties as JSON file"' run: |- eval "$SHELL_SETUP" - diff --minimal \ - <(jq --null-input --arg col c --arg uni 'a«,»c' '{ "colon:in:Key": $col, "unicode_escapes": $uni }'"| $sortObjectJqExpr") \ - <(jq "$sortObjectJqExpr" Date: Mon, 11 May 2026 19:25:44 +0200 Subject: [PATCH 020/190] test workflow: fix `keys` input for new assertion --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0134ac..59ad5e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: 'colon:in:Key unicode_escapes' + keys: 'colon:in:Key colonInValue unicode_escapes' resultType: json-file:tmp/multi.json - name: 'validate "test: query multiple properties as JSON file"' run: |- From cc78e68dd81e27a23e0b43275783a12621da2cb4 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 11 May 2026 19:26:07 +0200 Subject: [PATCH 021/190] test workflow: DRY getting of value from JSON; optimize result file reading --- .github/workflows/test.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59ad5e1..dd6c0be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,10 @@ env: return 1 fi } + function jsonGet() { + local key=$1; shift + jq --arg k "$key" --raw-output '.[$k] // ""' + } jobs: test: @@ -82,8 +86,8 @@ jobs: eval "$SHELL_SETUP" jq <<<"$RESULT" # assert parseable (and print) # check some values: - assertEquals "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' - assertEquals "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" 'c' + assertEquals "$(jsonGet 'unicode_escapes' <<<"$RESULT")" 'a«,»c' + assertEquals "$(jsonGet 'colon:in:Key' <<<"$RESULT")" 'c' # assert size >= 10: test "$(jq --raw-output 'keys | length' <<<"$RESULT")" -ge 10 @@ -99,9 +103,9 @@ jobs: RESULT: ${{steps.multiple-json.outputs.json}} run: |- eval "$SHELL_SETUP" - assertEquals "$(jq --raw-output '.unicode_escapes' <<<"$RESULT")" 'a«,»c' - assertEquals "$(jq --raw-output '.colonInValue' <<<"$RESULT")" 'cv:' - assertEquals "$(jq --raw-output '.["colon:in:Key"]' <<<"$RESULT")" 'c' + assertEquals "$(jsonGet 'unicode_escapes' <<<"$RESULT")" 'a«,»c' + assertEquals "$(jsonGet 'colonInValue' <<<"$RESULT")" 'cv:' + assertEquals "$(jsonGet 'colon:in:Key' <<<"$RESULT")" 'c' # assert size 3 (i.e. no additional results): assertEquals "$(jq --raw-output 'keys | length' <<<"$RESULT")" 3 @@ -114,11 +118,12 @@ jobs: - name: 'validate "test: query multiple properties as JSON file"' run: |- eval "$SHELL_SETUP" - assertEquals "$(jq --raw-output '.unicode_escapes' Date: Tue, 12 May 2026 19:24:10 +0200 Subject: [PATCH 022/190] test workflow: more cases; refactor: reuse script blocks with YAML anchors --- .github/workflows/test.yml | 498 +++++++++++++++++++++++++++++---- test/resources/test.properties | 2 + 2 files changed, 441 insertions(+), 59 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd6c0be..5a51fa1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,8 +4,16 @@ defaults: run: shell: bash env: + ENV_VAR_PREFIX: PROP_TEST__ + JSON_OUTPUT_FILE: tmp/out.json + SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' + SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' + SELECTED_SINGLE_NAME: 'my/prop' + SELECTED_NAMES: 'java-version jvm_args unicode colon' + + NOT_FOUND_STATUS: 54 SHELL_SETUP: |- - function assertEquals() { + assertEquals() { if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else @@ -13,9 +21,68 @@ env: return 1 fi } - function jsonGet() { + + # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. + assertUndefined() { + local getterFunction=$1; shift + local value st + if value="$("$getterFunction" "$@")"; then + printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 + return 1 + elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then + printf 'ok: %s is undefined\n' "$*" >&2 + else + # no error message, assuming failed $getterFunction has already written one + return $st + fi + } + + # The following helper functions to get a value for a given property key from the result have a uniform + # API (property key as sole argument) so that the actual test code can be identical (and thereby reusable + # with YAML anchors) for different result types. This leads to some assumptions which might seem + # a little odd, like the JSON result file must be called $JSON_OUTPUT_FILE. + + # Expects JSON as stdin and extracts the value for key $1. + getJsonValue() { + local key=$1; shift + jq --arg k "$key" --raw-output '.[$k] // halt_error(env.NOT_FOUND_STATUS | tonumber)' + } + + # Expects JSON in variable RESULT_JSON and extracts the value for key $1. + getEnvJsonValue() { + getJsonValue "$@" <<<"$RESULT_JSON" + } + + # Expects JSON in variable RESULT_JSON and extracts the value for key . + encodeKeyAndGetEnvJsonValue() { + local key=$1; shift + local encodedKey + encodedKey="_$(perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge' <<<"$key")" + getEnvJsonValue "$encodedKey" + } + + # Prints the value of the environment variable set for key $1 (when not using a prefix). + getEnv() { + local key=$1; shift + local st + if printenv -- "$key"; then + : # OK + elif [ ${st-$?} -eq 1 ]; then + return $NOT_FOUND_STATUS + else + # no error message, assuming printenv has already written one + return $st + fi + } + + # Prints the value of the environment variable set for key $1 when using a prefix. + getEnvWithPrefix() { local key=$1; shift - jq --arg k "$key" --raw-output '.[$k] // ""' + getEnv "${ENV_VAR_PREFIX}${key}" "$@" + } + + getFileJsonValue() { + getJsonValue "$@" <"$JSON_OUTPUT_FILE" } jobs: @@ -30,6 +97,7 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} + ############################################################ - name: 'test (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true @@ -42,6 +110,86 @@ jobs: eval "$SHELL_SETUP" assertEquals '${{steps.error-input-resultType.outcome}}' failure + ############################################################ + - name: 'test (should fail): resultType "output-named" without names' + id: error-resultType-output-without-names + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output-named:' + - name: 'validate "test (should fail): resultType "output-named" without names"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-output-without-names.outcome}}' failure + + ############################################################ + - name: 'test (should fail): resultType "env-named" without names' + id: error-resultType-env-without-names + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'env-named:' + - name: 'validate "test (should fail): resultType "env-named" without names"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-env-without-names.outcome}}' failure + + ############################################################ + - name: 'test (should fail): resultType "output-named" without keys' + id: error-resultType-output-without-keys + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + resultType: 'output-named:a b' + - name: 'validate "test (should fail): resultType "output-named" without keys"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-output-without-keys.outcome}}' failure + + ############################################################ + - name: 'test (should fail): resultType "env-named" without keys' + id: error-resultType-env-without-keys + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + resultType: 'env-named:a b' + - name: 'validate "test (should fail): resultType "env-named" without keys"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-env-without-keys.outcome}}' failure + + ############################################################ + # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output + # name (instead of re-implemented). + # It also demonstrates how you'd usually use outputs with one environment variable per output. + - name: 'test: query multiple properties as action outputs (simple)' + id: multi-output-simple + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}} dash-in-Key' + - name: 'validate "test: query multiple properties as action outputs (simple)"' + env: + RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} + RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} + RESULT_COLONK: ${{steps.multi-output-simple.outputs._colon-003Ain-003AKey}} + RESULT_DASHK: ${{steps.multi-output-simple.outputs._dash-002Din-002DKey}} + RESULT_UNI: ${{steps.multi-output-simple.outputs._unicode_escapes}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT_SJV" '21' + assertEquals "$RESULT_JVMARGS" '-ea -showversion' + assertEquals "$RESULT_COLONK" 'c' + assertEquals "$RESULT_DASHK" 'd' + assertEquals "$RESULT_UNI" 'a«,»c' + + ############################################################ - name: 'test: query all properties as action outputs' id: all-output uses: ./ @@ -49,30 +197,176 @@ jobs: file: test/resources/test.properties - name: 'validate "test: query all properties as action outputs"' env: - RESULT_SJV: ${{steps.all-output.outputs._sourceJavaVersion}} - RESULT_TJV: ${{steps.all-output.outputs._targetJavaVersion}} - RESULT_GRO: ${{steps.all-output.outputs._org-002Egradle-002Ejvmargs}} - run: |- + RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateAllImpl |- eval "$SHELL_SETUP" - assertEquals "$RESULT_SJV" 21 - assertEquals "$RESULT_TJV" 17 - assertEquals "$RESULT_GRO" '-ea -showversion' + assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' + assertEquals "$($RESULT_GETTER targetJavaVersion )" '17' + assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' + assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' + assertEquals "$($RESULT_GETTER colonInValue )" 'cv:' + assertEquals "$($RESULT_GETTER dash-in-Key )" 'd' + assertEquals "$($RESULT_GETTER dashInValue )" 'd-' + assertEquals "$($RESULT_GETTER empty )" '' + assertEquals "$($RESULT_GETTER null )" 'null' + assertEquals "$($RESULT_GETTER whitespace_escapes)" $'a\fb\rc\n' + assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' + assertEquals "$($RESULT_GETTER dash_1 )" '-' + assertEquals "$($RESULT_GETTER dash_2 )" '--' + assertEquals "$($RESULT_GETTER dash_3 )" '---' + assertEquals "$($RESULT_GETTER dash_4 )" '----' + assertEquals "$($RESULT_GETTER dash_5 )" '-----' + assertEquals "$($RESULT_GETTER dash_6 )" '------' + assertEquals "$($RESULT_GETTER dash_7 )" '-------' + assertEquals "$($RESULT_GETTER dash_8 )" '--------' + assertEquals "$($RESULT_GETTER dash_9 )" '---------' + assertEquals "$($RESULT_GETTER dash_10 )" '----------' + assertEquals "$($RESULT_GETTER dash_11 )" '-----------' + assertEquals "$($RESULT_GETTER dash_12 )" '------------' + assertEquals "$($RESULT_GETTER dash_3_4_1 )" $'---\n----\n-' + assertEquals "$($RESULT_GETTER dash_3_4_8_1 )" $'---\n----\n--------\n-' + ############################################################ - name: 'test: query multiple properties as action outputs' id: multi-output uses: ./ with: file: test/resources/test.properties - keys: 'sourceJavaVersion targetJavaVersion' + keys: '${{env.SELECTED_KEYS}}' - name: 'validate "test: query multiple properties as action outputs"' env: - RESULT_SJV: ${{steps.multi-output.outputs._sourceJavaVersion}} - RESULT_TJV: ${{steps.multi-output.outputs._targetJavaVersion}} - run: |- + RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateMultiImpl |- eval "$SHELL_SETUP" - assertEquals "$RESULT_SJV" 21 - assertEquals "$RESULT_TJV" 17 + assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' + assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' + assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' + assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' + assertUndefined $RESULT_GETTER targetJavaVersion + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash-in-Key + assertUndefined $RESULT_GETTER dashInValue + assertUndefined $RESULT_GETTER empty + assertUndefined $RESULT_GETTER null + assertUndefined $RESULT_GETTER whitespace_escapes + assertUndefined $RESULT_GETTER dash_1 + assertUndefined $RESULT_GETTER dash_2 + assertUndefined $RESULT_GETTER dash_3 + assertUndefined $RESULT_GETTER dash_4 + assertUndefined $RESULT_GETTER dash_5 + assertUndefined $RESULT_GETTER dash_6 + assertUndefined $RESULT_GETTER dash_7 + assertUndefined $RESULT_GETTER dash_8 + assertUndefined $RESULT_GETTER dash_9 + assertUndefined $RESULT_GETTER dash_10 + assertUndefined $RESULT_GETTER dash_11 + assertUndefined $RESULT_GETTER dash_12 + assertUndefined $RESULT_GETTER dash_3_4_1 + assertUndefined $RESULT_GETTER dash_3_4_8_1 + + ############################################################ + - name: 'test: query multiple properties as action outputs of given names' + id: multi-output-named + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output-named:${{env.SELECTED_NAMES}}' + - name: 'validate "test: query multiple properties as action outputs of given names"' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} + # Output names specified as input are also subject to encoding (prefix "_", replace punctuation except "_"): + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateMultiNamedImpl |- + eval "$SHELL_SETUP" + assertEquals "$($RESULT_GETTER java-version)" '21' + assertEquals "$($RESULT_GETTER jvm_args )" '-ea -showversion' + assertEquals "$($RESULT_GETTER colon )" 'c' + assertEquals "$($RESULT_GETTER unicode )" 'a«,»c' + + # The outputs with the default names are not set: + assertUndefined $RESULT_GETTER sourceJavaVersion + assertUndefined $RESULT_GETTER org.gradle.jvmargs + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER unicode_escapes + + # Outputs for non-selected keys are not set, for example: + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash_1 + + ############################################################ + - name: 'test: query multiple properties as action outputs of given name' + id: multi-output-named1 + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' + - name: 'validate "test: query multiple properties as action outputs of given name"' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} + # Output names specified as input are also subject to encoding (prefix "_", replace punctuation except "_"): + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateMultiNamed1Impl |- + eval "$SHELL_SETUP" + assertEquals "$($RESULT_GETTER my/prop)" 'c' + + # The outputs with the default names are not set: + assertUndefined $RESULT_GETTER sourceJavaVersion + assertUndefined $RESULT_GETTER org.gradle.jvmargs + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER unicode_escapes + + # Outputs for non-selected keys are not set, for example: + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash_1 + + ############################################################ + - name: 'test: query single property as action outputs' + id: single-output + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_SINGLE_KEY}}' + - name: 'validate "test: query multiple properties as action outputs"' + env: + RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateSingleImpl |- + eval "$SHELL_SETUP" + assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' + + assertUndefined $RESULT_GETTER sourceJavaVersion + assertUndefined $RESULT_GETTER targetJavaVersion + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash-in-Key + assertUndefined $RESULT_GETTER dashInValue + assertUndefined $RESULT_GETTER empty + assertUndefined $RESULT_GETTER null + assertUndefined $RESULT_GETTER whitespace_escapes + assertUndefined $RESULT_GETTER unicode_escapes + assertUndefined $RESULT_GETTER dash_1 + assertUndefined $RESULT_GETTER dash_2 + assertUndefined $RESULT_GETTER dash_3 + assertUndefined $RESULT_GETTER dash_4 + assertUndefined $RESULT_GETTER dash_5 + assertUndefined $RESULT_GETTER dash_6 + assertUndefined $RESULT_GETTER dash_7 + assertUndefined $RESULT_GETTER dash_8 + assertUndefined $RESULT_GETTER dash_9 + assertUndefined $RESULT_GETTER dash_10 + assertUndefined $RESULT_GETTER dash_11 + assertUndefined $RESULT_GETTER dash_12 + assertUndefined $RESULT_GETTER dash_3_4_1 + assertUndefined $RESULT_GETTER dash_3_4_8_1 + + ############################################################ - name: 'test: query all properties as JSON action output' id: all-json uses: ./ @@ -81,51 +375,49 @@ jobs: resultType: json - name: 'validate "test: query all properties as JSON action output"' env: - RESULT: ${{steps.all-json.outputs.json}} - run: |- - eval "$SHELL_SETUP" - jq <<<"$RESULT" # assert parseable (and print) - # check some values: - assertEquals "$(jsonGet 'unicode_escapes' <<<"$RESULT")" 'a«,»c' - assertEquals "$(jsonGet 'colon:in:Key' <<<"$RESULT")" 'c' - # assert size >= 10: - test "$(jq --raw-output 'keys | length' <<<"$RESULT")" -ge 10 + RESULT_JSON: ${{steps.all-json.outputs.json}} + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: *validateAllImpl + ############################################################ - name: 'test: query multiple properties as JSON action output' id: multiple-json uses: ./ with: file: test/resources/test.properties - keys: 'colon:in:Key colonInValue unicode_escapes' + keys: '${{env.SELECTED_KEYS}}' resultType: json - name: 'validate "test: query multiple properties as JSON action output"' env: - RESULT: ${{steps.multiple-json.outputs.json}} - run: |- - eval "$SHELL_SETUP" - assertEquals "$(jsonGet 'unicode_escapes' <<<"$RESULT")" 'a«,»c' - assertEquals "$(jsonGet 'colonInValue' <<<"$RESULT")" 'cv:' - assertEquals "$(jsonGet 'colon:in:Key' <<<"$RESULT")" 'c' - # assert size 3 (i.e. no additional results): - assertEquals "$(jq --raw-output 'keys | length' <<<"$RESULT")" 3 + RESULT_JSON: ${{steps.multiple-json.outputs.json}} + run: *validateMultiImpl + ############################################################ + - name: 'test: query all properties as JSON file' + uses: ./ + with: + file: test/resources/test.properties + resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' + - name: 'validate "test: query all properties as JSON file"' + env: + RESULT_GETTER: getFileJsonValue + run: *validateAllImpl + + ############################################################ - name: 'test: query multiple properties as JSON file' uses: ./ with: file: test/resources/test.properties - keys: 'colon:in:Key colonInValue unicode_escapes' - resultType: json-file:tmp/multi.json + keys: '${{env.SELECTED_KEYS}}' + resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' - name: 'validate "test: query multiple properties as JSON file"' - run: |- - eval "$SHELL_SETUP" - result=$( Date: Tue, 12 May 2026 19:38:23 +0200 Subject: [PATCH 023/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a51fa1..514c01d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -210,7 +210,7 @@ jobs: assertEquals "$($RESULT_GETTER dashInValue )" 'd-' assertEquals "$($RESULT_GETTER empty )" '' assertEquals "$($RESULT_GETTER null )" 'null' - assertEquals "$($RESULT_GETTER whitespace_escapes)" $'a\fb\rc\n' + assertEquals "$($RESULT_GETTER whitespace_escapes)•" $'a\fb\rc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' assertEquals "$($RESULT_GETTER dash_1 )" '-' assertEquals "$($RESULT_GETTER dash_2 )" '--' @@ -489,7 +489,7 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SINGLE_SELECTED_KEY}}' + keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: env - name: 'validate "test: query single property as environment variable"' env: @@ -536,7 +536,7 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SINGLE_SELECTED_KEY}}' + keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: env - name: 'validate "test: query single property as environment variable with prefix"' env: From d72d0a0c0f5304ed0c6c6328a2be520a4bbf1dad Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 19:43:02 +0200 Subject: [PATCH 024/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 514c01d..f7c1ed8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -210,7 +210,7 @@ jobs: assertEquals "$($RESULT_GETTER dashInValue )" 'd-' assertEquals "$($RESULT_GETTER empty )" '' assertEquals "$($RESULT_GETTER null )" 'null' - assertEquals "$($RESULT_GETTER whitespace_escapes)•" $'a\fb\rc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed + assertEquals "$($RESULT_GETTER whitespace_escapes && printf •" $'a\fb\rc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' assertEquals "$($RESULT_GETTER dash_1 )" '-' assertEquals "$($RESULT_GETTER dash_2 )" '--' From 0ead6ea9eb0363ecc6abbf752d4fb58bff9ec017 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 19:44:19 +0200 Subject: [PATCH 025/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f7c1ed8..1191007 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -210,7 +210,7 @@ jobs: assertEquals "$($RESULT_GETTER dashInValue )" 'd-' assertEquals "$($RESULT_GETTER empty )" '' assertEquals "$($RESULT_GETTER null )" 'null' - assertEquals "$($RESULT_GETTER whitespace_escapes && printf •" $'a\fb\rc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed + assertEquals "$($RESULT_GETTER whitespace_escapes && printf •)" $'a\fb\rc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' assertEquals "$($RESULT_GETTER dash_1 )" '-' assertEquals "$($RESULT_GETTER dash_2 )" '--' From 386721cde015169160166bf312613c2ceca23cf7 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 19:48:22 +0200 Subject: [PATCH 026/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1191007..c2d7760 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,9 @@ env: # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { local key=$1; shift - jq --arg k "$key" --raw-output '.[$k] // halt_error(env.NOT_FOUND_STATUS | tonumber)' + local result + result=$(jq --arg k "$key" --raw-output '.[$k] // halt_error(env.NOT_FOUND_STATUS | tonumber)') + printf %s "$result" # without extra trailing linefeed } # Expects JSON in variable RESULT_JSON and extracts the value for key $1. From 373fb8b32786fdb11cf2b12c89639caf9966533f Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 19:50:49 +0200 Subject: [PATCH 027/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- test/resources/test.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2d7760..0466077 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -212,7 +212,7 @@ jobs: assertEquals "$($RESULT_GETTER dashInValue )" 'd-' assertEquals "$($RESULT_GETTER empty )" '' assertEquals "$($RESULT_GETTER null )" 'null' - assertEquals "$($RESULT_GETTER whitespace_escapes && printf •)" $'a\fb\rc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed + assertEquals "$($RESULT_GETTER whitespace_escapes && printf •)" $'a\fb\nc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' assertEquals "$($RESULT_GETTER dash_1 )" '-' assertEquals "$($RESULT_GETTER dash_2 )" '--' diff --git a/test/resources/test.properties b/test/resources/test.properties index 822f22b..2a46ef4 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -9,7 +9,7 @@ dashInValue: d- empty: null: null -whitespace_escapes = a\fb\rc\n +whitespace_escapes = a\fb\nc\n unicode_escapes = a\u00AB,\u00BBc # For white box tests: When writing to $GITHUB_OUTPUT or $GITHUB_ENV, "----" is the default delimiter used in this action. From 2f90ff553cb40ed5a2d4946da9d03493f7251446 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 19:51:44 +0200 Subject: [PATCH 028/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- test/resources/test.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0466077..0247757 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -212,7 +212,7 @@ jobs: assertEquals "$($RESULT_GETTER dashInValue )" 'd-' assertEquals "$($RESULT_GETTER empty )" '' assertEquals "$($RESULT_GETTER null )" 'null' - assertEquals "$($RESULT_GETTER whitespace_escapes && printf •)" $'a\fb\nc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed + assertEquals "$($RESULT_GETTER whitespace_escapes && printf •)" $'a\tb\nc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' assertEquals "$($RESULT_GETTER dash_1 )" '-' assertEquals "$($RESULT_GETTER dash_2 )" '--' diff --git a/test/resources/test.properties b/test/resources/test.properties index 2a46ef4..ba9b062 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -9,7 +9,7 @@ dashInValue: d- empty: null: null -whitespace_escapes = a\fb\nc\n +whitespace_escapes = a\tb\nc\n unicode_escapes = a\u00AB,\u00BBc # For white box tests: When writing to $GITHUB_OUTPUT or $GITHUB_ENV, "----" is the default delimiter used in this action. From a6de5b60f3bb35a9bf818bec833ad8fe40035c5a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 20:01:39 +0200 Subject: [PATCH 029/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0247757..8161571 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ env: if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else - printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 + printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 | tee >(od --width=1000 --address-radix=n --format=c >&2) return 1 fi } From c2250292cadc461d62479ff563ce620ec8d56cd3 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 20:03:54 +0200 Subject: [PATCH 030/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8161571..fdb6a23 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ env: if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else - printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 | tee >(od --width=1000 --address-radix=n --format=c >&2) + printf 'assertion error: "%s" != "%s"\n' "$1" "$2" | tee >(od --width=1000 --address-radix=n --format=c >&2) return 1 fi } From 79be6b865b185b4b058c3aff2c7832079c8fb509 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 20:12:35 +0200 Subject: [PATCH 031/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++++---- test/resources/test.properties | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdb6a23..90dee72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,9 +45,7 @@ env: # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { local key=$1; shift - local result - result=$(jq --arg k "$key" --raw-output '.[$k] // halt_error(env.NOT_FOUND_STATUS | tonumber)') - printf %s "$result" # without extra trailing linefeed + jq --arg k "$key" --raw-output '.[$k] // halt_error(env.NOT_FOUND_STATUS | tonumber)' } # Expects JSON in variable RESULT_JSON and extracts the value for key $1. @@ -212,7 +210,9 @@ jobs: assertEquals "$($RESULT_GETTER dashInValue )" 'd-' assertEquals "$($RESULT_GETTER empty )" '' assertEquals "$($RESULT_GETTER null )" 'null' - assertEquals "$($RESULT_GETTER whitespace_escapes && printf •)" $'a\tb\nc\n•' # add a trailing dummy char to preserve the otherwise trailing linefeed + # Expect additional linefeed (the usual output of commands, e.g. jq); + # add a trailing dummy char to preserve the otherwise trailing linefeed in bash command substitution [https://stackoverflow.com/a/15184414]: + assertEquals "$($RESULT_GETTER whitespace_escapes && printf '|')" $'A\tB\fC C\nD\n\n|' assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' assertEquals "$($RESULT_GETTER dash_1 )" '-' assertEquals "$($RESULT_GETTER dash_2 )" '--' diff --git a/test/resources/test.properties b/test/resources/test.properties index ba9b062..06bd6b4 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -9,7 +9,7 @@ dashInValue: d- empty: null: null -whitespace_escapes = a\tb\nc\n +whitespace_escapes = A\tB\fC C\nD\n unicode_escapes = a\u00AB,\u00BBc # For white box tests: When writing to $GITHUB_OUTPUT or $GITHUB_ENV, "----" is the default delimiter used in this action. From 32e7a544ddd23505795d454ab2f03b34cd29e0de Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 20:36:43 +0200 Subject: [PATCH 032/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++++++-- Action.java | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 90dee72..9e4b369 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ env: if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else - printf 'assertion error: "%s" != "%s"\n' "$1" "$2" | tee >(od --width=1000 --address-radix=n --format=c >&2) + printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 return 1 fi } @@ -26,15 +26,19 @@ env: assertUndefined() { local getterFunction=$1; shift local value st + set -x if value="$("$getterFunction" "$@")"; then printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 + set +x return 1 elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then printf 'ok: %s is undefined\n' "$*" >&2 else # no error message, assuming failed $getterFunction has already written one + set +x return $st fi + set +x } # The following helper functions to get a value for a given property key from the result have a uniform @@ -58,7 +62,7 @@ env: local key=$1; shift local encodedKey encodedKey="_$(perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge' <<<"$key")" - getEnvJsonValue "$encodedKey" + getEnvJsonValue "$encodedKey" "$@" } # Prints the value of the environment variable set for key $1 (when not using a prefix). diff --git a/Action.java b/Action.java index 1cf2cf0..d1c4a9b 100644 --- a/Action.java +++ b/Action.java @@ -191,7 +191,7 @@ public GitHubVariableWriter open() throws IOException { } private static String encodeOutputValue(String value) { - StringBuilder result = new StringBuilder(value.length() + 4); + StringBuilder result = new StringBuilder(value.length() + 4).append('_'); Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(value); while (matcher.find()) { matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); From a740fa9c7db483b8bf26563057bdb4c7cca634d5 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 20:40:58 +0200 Subject: [PATCH 033/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Action.java b/Action.java index d1c4a9b..1cf2cf0 100644 --- a/Action.java +++ b/Action.java @@ -191,7 +191,7 @@ public GitHubVariableWriter open() throws IOException { } private static String encodeOutputValue(String value) { - StringBuilder result = new StringBuilder(value.length() + 4).append('_'); + StringBuilder result = new StringBuilder(value.length() + 4); Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(value); while (matcher.find()) { matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); From a72083d048264af42ffc3db1b9b41beb048923a6 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 20:48:59 +0200 Subject: [PATCH 034/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e4b369..2b5f5aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,19 +26,15 @@ env: assertUndefined() { local getterFunction=$1; shift local value st - set -x if value="$("$getterFunction" "$@")"; then printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 - set +x return 1 elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then printf 'ok: %s is undefined\n' "$*" >&2 else # no error message, assuming failed $getterFunction has already written one - set +x return $st fi - set +x } # The following helper functions to get a value for a given property key from the result have a uniform @@ -49,7 +45,7 @@ env: # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { local key=$1; shift - jq --arg k "$key" --raw-output '.[$k] // halt_error(env.NOT_FOUND_STATUS | tonumber)' + jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' } # Expects JSON in variable RESULT_JSON and extracts the value for key $1. From 03bf5660d7cf5b41e5416ea1f9a15af3415814dc Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 12 May 2026 20:55:13 +0200 Subject: [PATCH 035/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Action.java b/Action.java index 1cf2cf0..171b833 100644 --- a/Action.java +++ b/Action.java @@ -80,7 +80,7 @@ enum ResultWriter { public void write(Properties props, Input input) throws IOException { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { for (Map.Entry entry : Util.stringEntries(props)) { - writer.write(input.outputPrefix() + entry.getKey(), entry.getValue()); + writer.write(entry.getKey(), entry.getValue()); } if (props.size() == 1) { @@ -181,9 +181,9 @@ private GitHubOutputFile(String fileNameEnvVar) { this(fileNameEnvVar, v -> v); } - private GitHubOutputFile(String fileNameEnvVar, Function valueReplacer) { + private GitHubOutputFile(String fileNameEnvVar, Function keyReplacer) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); - this.keyReplacer = valueReplacer; + this.keyReplacer = keyReplacer; } public GitHubVariableWriter open() throws IOException { @@ -191,7 +191,8 @@ public GitHubVariableWriter open() throws IOException { } private static String encodeOutputValue(String value) { - StringBuilder result = new StringBuilder(value.length() + 4); + StringBuilder result = new StringBuilder(value.length() + input.outputPrefix() + 4); + result.append(input.outputPrefix()); Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(value); while (matcher.find()) { matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); @@ -209,10 +210,10 @@ class GitHubVariableWriter implements AutoCloseable { private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String description, String fileName, Function valueReplacer) throws IOException { + public GitHubVariableWriter(String description, String fileName, Function keyReplacer) throws IOException { this.description = description; this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); - this.keyReplacer = Objects.requireNonNull(valueReplacer); + this.keyReplacer = Objects.requireNonNull(keyReplacer); } public void write(String key, String value) throws IOException { From bb3628c854faaf16961fd534967f12c3524d73c7 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 12:22:29 +0200 Subject: [PATCH 036/190] fix missing output name prefix "_" (depending on resultType) --- Action.java | 116 +++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/Action.java b/Action.java index 1cf2cf0..aa2b476 100644 --- a/Action.java +++ b/Action.java @@ -24,20 +24,20 @@ public class Action { static void main(String[] args) throws Exception { - Input input = Input.fromEnv(); - ResultWriter resultWriter = ResultWriter.of(input.resultType()); + Config config = Config.fromEnv(); + ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { - Properties properties = input.selectedKeys() == null ? Util.readProperties(file) - : Util.selectProperties(Util.readProperties(file), input.selectedKeys(), file); - resultWriter.write(properties, input); + Properties properties = config.selectedKeys() == null ? Util.readProperties(file) + : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); + resultWriter.write(properties, config); } } } -record Input(String[] selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, +record Config(String[] selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, String resultNameSeparator, String outputPrefix) { - public static Input fromEnv() { + public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. String keySeparator = Util.getRequiredEnv("KEY_SEPARATOR"); // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty @@ -55,7 +55,7 @@ public static Input fromEnv() { String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); String outputPrefix = Util.getRequiredEnv("OUTPUT_PREFIX"); - return new Input(keys, keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); + return new Config(keys, keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); } public String requiredResultTypeArg() { @@ -77,10 +77,10 @@ public void requireNoArg() { enum ResultWriter { OUTPUT("output") { @Override - public void write(Properties props, Input input) throws IOException { - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + public void write(Properties props, Config config) throws IOException { + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { for (Map.Entry entry : Util.stringEntries(props)) { - writer.write(input.outputPrefix() + entry.getKey(), entry.getValue()); + writer.write(entry.getKey(), entry.getValue()); } if (props.size() == 1) { @@ -92,21 +92,21 @@ public void write(Properties props, Input input) throws IOException { }, OUTPUT_NAMED("output-named") { @Override - public void write(Properties props, Input input) throws IOException { - writeNamedImpl(props, input, GitHubOutputFile.OUTPUT); + public void write(Properties props, Config config) throws IOException { + writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); } }, ENV_NAMED("env-named") { @Override - public void write(Properties props, Input input) throws IOException { - writeNamedImpl(props, input, GitHubOutputFile.ENV); + public void write(Properties props, Config config) throws IOException { + writeNamedImpl(props, config, GitHubOutputFile.ENV); } }, ENV("env") { @Override - public void write(Properties props, Input input) throws IOException { - String prefix = input.resultTypeArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { + public void write(Properties props, Config config) throws IOException { + String prefix = config.resultTypeArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open(config)) { for (Map.Entry entry : Util.stringEntries(props)) { writer.write(prefix + entry.getKey(), entry.getValue()); } @@ -115,17 +115,17 @@ public void write(Properties props, Input input) throws IOException { }, JSON("json") { @Override - public void write(Properties props, Input input) throws IOException { - input.requireNoArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + public void write(Properties props, Config config) throws IOException { + config.requireNoArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { writer.write("json", Util.toJson(props)); } } }, JSON_FILE("json-file") { @Override - public void write(Properties props, Input input) throws IOException { - String outputFile = input.requiredResultTypeArg(); + public void write(Properties props, Config config) throws IOException { + String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE)) { writer.write(Util.toJson(props)); @@ -141,25 +141,25 @@ private ResultWriter(String inputName) { this.inputName = inputName; } - public static ResultWriter of(String name) { + public static ResultWriter of(String inputName) { for (ResultWriter rw : ResultWriter.values()) { - if (rw.inputName.equals(name)) { + if (rw.inputName.equals(inputName)) { return rw; } } - throw new IllegalArgumentException("invalid resultType: " + name); + throw new IllegalArgumentException("invalid result type: " + inputName); } - public abstract void write(Properties props, Input input) throws IOException; + public abstract void write(Properties props, Config config) throws IOException; - private static void writeNamedImpl(Properties props, Input input, GitHubOutputFile gitHubOutputFile) throws IOException { - String[] selectedKeys = input.selectedKeys(); - String[] resultNames = Util.splitArray(input.requiredResultTypeArg(), input.resultNameSeparator(), null); + private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { + String[] selectedKeys = config.selectedKeys(); + String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator(), null); if (resultNames.length != selectedKeys.length && resultNames.length != 1) { - throw new IllegalArgumentException("resultType " + input.resultTypeWithArg() + " has " + resultNames.length + throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length + " arguments, but " + selectedKeys.length + " keys are selected"); } - try (GitHubVariableWriter writer = gitHubOutputFile.open()) { + try (GitHubVariableWriter writer = gitHubOutputFile.open(config)) { for (int i = 0; i < selectedKeys.length; i++) { String varName = resultNames[resultNames.length == 1 ? 0 : i]; String value = props.getProperty(selectedKeys[i]); @@ -171,34 +171,38 @@ private static void writeNamedImpl(Properties props, Input input, GitHubOutputFi enum GitHubOutputFile { - OUTPUT("GITHUB_OUTPUT", v -> encodeOutputValue(v)), // - ENV("GITHUB_ENV"); + OUTPUT("GITHUB_OUTPUT") { + @Override + public GitHubVariableWriter open(Config config) throws IOException { + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, + k -> encodeKey(config, k)); + } - private final String fileName; - private final Function keyReplacer; + private static String encodeKey(Config config, String key) { + StringBuilder result = new StringBuilder(key.length() + config.outputPrefix() + 4); + result.append(config.outputPrefix()); + Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); + while (matcher.find()) { + matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); + } + matcher.appendTail(result); + return result.toString(); + } + }, + ENV("GITHUB_ENV") { + @Override + public GitHubVariableWriter open(Config config) throws IOException { + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, k -> k); + } + }; - private GitHubOutputFile(String fileNameEnvVar) { - this(fileNameEnvVar, v -> v); - } + final String fileName; - private GitHubOutputFile(String fileNameEnvVar, Function valueReplacer) { + private GitHubOutputFile(String fileNameEnvVar) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); - this.keyReplacer = valueReplacer; } - public GitHubVariableWriter open() throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, keyReplacer); - } - - private static String encodeOutputValue(String value) { - StringBuilder result = new StringBuilder(value.length() + 4); - Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(value); - while (matcher.find()) { - matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); - } - matcher.appendTail(result); - return result.toString(); - } + public abstract GitHubVariableWriter open(Config config) throws IOException; } @@ -209,10 +213,10 @@ class GitHubVariableWriter implements AutoCloseable { private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String description, String fileName, Function valueReplacer) throws IOException { + public GitHubVariableWriter(String description, String fileName, Function keyReplacer) throws IOException { this.description = description; this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); - this.keyReplacer = Objects.requireNonNull(valueReplacer); + this.keyReplacer = Objects.requireNonNull(keyReplacer); } public void write(String key, String value) throws IOException { From 803cde598101604f687ffcee326e9db64c858be1 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 12:22:29 +0200 Subject: [PATCH 037/190] fix missing output name prefix "_" (depending on resultType) --- Action.java | 116 +++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/Action.java b/Action.java index 1cf2cf0..475374e 100644 --- a/Action.java +++ b/Action.java @@ -24,20 +24,20 @@ public class Action { static void main(String[] args) throws Exception { - Input input = Input.fromEnv(); - ResultWriter resultWriter = ResultWriter.of(input.resultType()); + Config config = Config.fromEnv(); + ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { - Properties properties = input.selectedKeys() == null ? Util.readProperties(file) - : Util.selectProperties(Util.readProperties(file), input.selectedKeys(), file); - resultWriter.write(properties, input); + Properties properties = config.selectedKeys() == null ? Util.readProperties(file) + : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); + resultWriter.write(properties, config); } } } -record Input(String[] selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, +record Config(String[] selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, String resultNameSeparator, String outputPrefix) { - public static Input fromEnv() { + public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. String keySeparator = Util.getRequiredEnv("KEY_SEPARATOR"); // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty @@ -55,7 +55,7 @@ public static Input fromEnv() { String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); String outputPrefix = Util.getRequiredEnv("OUTPUT_PREFIX"); - return new Input(keys, keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); + return new Config(keys, keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); } public String requiredResultTypeArg() { @@ -77,10 +77,10 @@ public void requireNoArg() { enum ResultWriter { OUTPUT("output") { @Override - public void write(Properties props, Input input) throws IOException { - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + public void write(Properties props, Config config) throws IOException { + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { for (Map.Entry entry : Util.stringEntries(props)) { - writer.write(input.outputPrefix() + entry.getKey(), entry.getValue()); + writer.write(entry.getKey(), entry.getValue()); } if (props.size() == 1) { @@ -92,21 +92,21 @@ public void write(Properties props, Input input) throws IOException { }, OUTPUT_NAMED("output-named") { @Override - public void write(Properties props, Input input) throws IOException { - writeNamedImpl(props, input, GitHubOutputFile.OUTPUT); + public void write(Properties props, Config config) throws IOException { + writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); } }, ENV_NAMED("env-named") { @Override - public void write(Properties props, Input input) throws IOException { - writeNamedImpl(props, input, GitHubOutputFile.ENV); + public void write(Properties props, Config config) throws IOException { + writeNamedImpl(props, config, GitHubOutputFile.ENV); } }, ENV("env") { @Override - public void write(Properties props, Input input) throws IOException { - String prefix = input.resultTypeArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { + public void write(Properties props, Config config) throws IOException { + String prefix = config.resultTypeArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open(config)) { for (Map.Entry entry : Util.stringEntries(props)) { writer.write(prefix + entry.getKey(), entry.getValue()); } @@ -115,17 +115,17 @@ public void write(Properties props, Input input) throws IOException { }, JSON("json") { @Override - public void write(Properties props, Input input) throws IOException { - input.requireNoArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + public void write(Properties props, Config config) throws IOException { + config.requireNoArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { writer.write("json", Util.toJson(props)); } } }, JSON_FILE("json-file") { @Override - public void write(Properties props, Input input) throws IOException { - String outputFile = input.requiredResultTypeArg(); + public void write(Properties props, Config config) throws IOException { + String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE)) { writer.write(Util.toJson(props)); @@ -141,25 +141,25 @@ private ResultWriter(String inputName) { this.inputName = inputName; } - public static ResultWriter of(String name) { + public static ResultWriter of(String inputName) { for (ResultWriter rw : ResultWriter.values()) { - if (rw.inputName.equals(name)) { + if (rw.inputName.equals(inputName)) { return rw; } } - throw new IllegalArgumentException("invalid resultType: " + name); + throw new IllegalArgumentException("invalid result type: " + inputName); } - public abstract void write(Properties props, Input input) throws IOException; + public abstract void write(Properties props, Config config) throws IOException; - private static void writeNamedImpl(Properties props, Input input, GitHubOutputFile gitHubOutputFile) throws IOException { - String[] selectedKeys = input.selectedKeys(); - String[] resultNames = Util.splitArray(input.requiredResultTypeArg(), input.resultNameSeparator(), null); + private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { + String[] selectedKeys = config.selectedKeys(); + String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator(), null); if (resultNames.length != selectedKeys.length && resultNames.length != 1) { - throw new IllegalArgumentException("resultType " + input.resultTypeWithArg() + " has " + resultNames.length + throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length + " arguments, but " + selectedKeys.length + " keys are selected"); } - try (GitHubVariableWriter writer = gitHubOutputFile.open()) { + try (GitHubVariableWriter writer = gitHubOutputFile.open(config)) { for (int i = 0; i < selectedKeys.length; i++) { String varName = resultNames[resultNames.length == 1 ? 0 : i]; String value = props.getProperty(selectedKeys[i]); @@ -171,34 +171,38 @@ private static void writeNamedImpl(Properties props, Input input, GitHubOutputFi enum GitHubOutputFile { - OUTPUT("GITHUB_OUTPUT", v -> encodeOutputValue(v)), // - ENV("GITHUB_ENV"); + OUTPUT("GITHUB_OUTPUT") { + @Override + public GitHubVariableWriter open(Config config) throws IOException { + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, + k -> encodeKey(config, k)); + } - private final String fileName; - private final Function keyReplacer; + private static String encodeKey(Config config, String key) { + StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); + result.append(config.outputPrefix()); + Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); + while (matcher.find()) { + matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); + } + matcher.appendTail(result); + return result.toString(); + } + }, + ENV("GITHUB_ENV") { + @Override + public GitHubVariableWriter open(Config config) throws IOException { + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, k -> k); + } + }; - private GitHubOutputFile(String fileNameEnvVar) { - this(fileNameEnvVar, v -> v); - } + final String fileName; - private GitHubOutputFile(String fileNameEnvVar, Function valueReplacer) { + private GitHubOutputFile(String fileNameEnvVar) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); - this.keyReplacer = valueReplacer; } - public GitHubVariableWriter open() throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, keyReplacer); - } - - private static String encodeOutputValue(String value) { - StringBuilder result = new StringBuilder(value.length() + 4); - Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(value); - while (matcher.find()) { - matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); - } - matcher.appendTail(result); - return result.toString(); - } + public abstract GitHubVariableWriter open(Config config) throws IOException; } @@ -209,10 +213,10 @@ class GitHubVariableWriter implements AutoCloseable { private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String description, String fileName, Function valueReplacer) throws IOException { + public GitHubVariableWriter(String description, String fileName, Function keyReplacer) throws IOException { this.description = description; this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); - this.keyReplacer = Objects.requireNonNull(valueReplacer); + this.keyReplacer = Objects.requireNonNull(keyReplacer); } public void write(String key, String value) throws IOException { From 5598565d0519fcc1545854c7ba16194f4cfc95ff Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 12:41:54 +0200 Subject: [PATCH 038/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 53 +++++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/Action.java b/Action.java index 475374e..a607646 100644 --- a/Action.java +++ b/Action.java @@ -13,11 +13,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -80,7 +78,7 @@ enum ResultWriter { public void write(Properties props, Config config) throws IOException { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { for (Map.Entry entry : Util.stringEntries(props)) { - writer.write(entry.getKey(), entry.getValue()); + writer.write(encodeKey(config, entry.getKey()), entry.getValue()); } if (props.size() == 1) { @@ -89,6 +87,17 @@ public void write(Properties props, Config config) throws IOException { } } } + + private static String encodeKey(Config config, String key) { + StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); + result.append(config.outputPrefix()); + Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); + while (matcher.find()) { + matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); + } + matcher.appendTail(result); + return result.toString(); + } }, OUTPUT_NAMED("output-named") { @Override @@ -174,25 +183,13 @@ enum GitHubOutputFile { OUTPUT("GITHUB_OUTPUT") { @Override public GitHubVariableWriter open(Config config) throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, - k -> encodeKey(config, k)); - } - - private static String encodeKey(Config config, String key) { - StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); - result.append(config.outputPrefix()); - Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); - while (matcher.find()) { - matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); - } - matcher.appendTail(result); - return result.toString(); + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName); } }, ENV("GITHUB_ENV") { @Override public GitHubVariableWriter open(Config config) throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, k -> k); + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName); } }; @@ -210,28 +207,34 @@ class GitHubVariableWriter implements AutoCloseable { private static final Pattern SIMPLE_VALUE = Pattern.compile("[\\w.-]+"); private final String description; - private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String description, String fileName, Function keyReplacer) throws IOException { + public GitHubVariableWriter(String description, String fileName) throws IOException { this.description = description; this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); - this.keyReplacer = Objects.requireNonNull(keyReplacer); } public void write(String key, String value) throws IOException { - String encodedKey = keyReplacer.apply(key); - System.err.format("%s %s\t:= \"%s\"\n", description, encodedKey, value); + System.err.format("%s %s\t:= \"%s\"\n", description, key, value); // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { - this.writer.write(encodedKey); + this.writer.write(key); this.writer.write('='); this.writer.write(value); this.writer.write('\n'); return; + } else { + writeMultiLine(key, value); } + } + /** + * Writes a key-value pair in + * multiline + * format. + */ + private void writeMultiLine(String key, String value) throws IOException { // determine separator line which does not occur in value: Set valueLines = Set.of(value.split("(?s)\n")); String separatorPart = "----"; @@ -240,9 +243,7 @@ public void write(String key, String value) throws IOException { separator = separator + separatorPart; } - // write in multiline format - // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings]: - this.writer.write(encodedKey); + this.writer.write(key); this.writer.write("<<"); this.writer.write(separator); this.writer.write('\n'); From 347bd6e53cb83371a03cd69b985c6c77a5a58cab Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 15:56:54 +0200 Subject: [PATCH 039/190] encode & prefix only generated, not given output names --- Action.java | 77 +++++++++++++++++++++++------------------------------ action.yml | 11 ++++---- 2 files changed, 39 insertions(+), 49 deletions(-) diff --git a/Action.java b/Action.java index 475374e..bf50184 100644 --- a/Action.java +++ b/Action.java @@ -13,11 +13,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -78,17 +76,31 @@ enum ResultWriter { OUTPUT("output") { @Override public void write(Properties props, Config config) throws IOException { - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { + String lastValue = null; + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { for (Map.Entry entry : Util.stringEntries(props)) { - writer.write(entry.getKey(), entry.getValue()); + String key = encodeKey(config, config.outputPrefix() + entry.getKey()); + lastValue = entry.getValue(); + writer.write(key, lastValue); } - if (props.size() == 1) { - String value = Util.stringEntries(props).iterator().next().getValue(); - writer.write("value", value); + // TODO props must be in order of config.selectedKeys() + // Otherwise, this is arbitrary: + if (lastValue != null) { + writer.write("value", lastValue); } } } + + private static String encodeKey(Config config, String key) { + StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); + Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); + while (matcher.find()) { + matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); + } + matcher.appendTail(result); + return result.toString(); + } }, OUTPUT_NAMED("output-named") { @Override @@ -106,7 +118,7 @@ public void write(Properties props, Config config) throws IOException { @Override public void write(Properties props, Config config) throws IOException { String prefix = config.resultTypeArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open(config)) { + try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { for (Map.Entry entry : Util.stringEntries(props)) { writer.write(prefix + entry.getKey(), entry.getValue()); } @@ -117,7 +129,7 @@ public void write(Properties props, Config config) throws IOException { @Override public void write(Properties props, Config config) throws IOException { config.requireNoArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write("json", Util.toJson(props)); } } @@ -159,11 +171,11 @@ private static void writeNamedImpl(Properties props, Config config, GitHubOutput throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length + " arguments, but " + selectedKeys.length + " keys are selected"); } - try (GitHubVariableWriter writer = gitHubOutputFile.open(config)) { + try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.length; i++) { - String varName = resultNames[resultNames.length == 1 ? 0 : i]; + String name = resultNames[resultNames.length == 1 ? 0 : i]; String value = props.getProperty(selectedKeys[i]); - writer.write(varName, value); + writer.write(name, value); } } } @@ -171,30 +183,8 @@ private static void writeNamedImpl(Properties props, Config config, GitHubOutput enum GitHubOutputFile { - OUTPUT("GITHUB_OUTPUT") { - @Override - public GitHubVariableWriter open(Config config) throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, - k -> encodeKey(config, k)); - } - - private static String encodeKey(Config config, String key) { - StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); - result.append(config.outputPrefix()); - Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); - while (matcher.find()) { - matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); - } - matcher.appendTail(result); - return result.toString(); - } - }, - ENV("GITHUB_ENV") { - @Override - public GitHubVariableWriter open(Config config) throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, k -> k); - } - }; + OUTPUT("GITHUB_OUTPUT"), // + ENV("GITHUB_ENV"); final String fileName; @@ -202,7 +192,9 @@ private GitHubOutputFile(String fileNameEnvVar) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); } - public abstract GitHubVariableWriter open(Config config) throws IOException; + public GitHubVariableWriter open() throws IOException { + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName); + } } @@ -210,22 +202,19 @@ class GitHubVariableWriter implements AutoCloseable { private static final Pattern SIMPLE_VALUE = Pattern.compile("[\\w.-]+"); private final String description; - private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String description, String fileName, Function keyReplacer) throws IOException { + public GitHubVariableWriter(String description, String fileName) throws IOException { this.description = description; this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); - this.keyReplacer = Objects.requireNonNull(keyReplacer); } public void write(String key, String value) throws IOException { - String encodedKey = keyReplacer.apply(key); - System.err.format("%s %s\t:= \"%s\"\n", description, encodedKey, value); + System.err.format("%s %s\t:= \"%s\"\n", description, key, value); // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { - this.writer.write(encodedKey); + this.writer.write(key); this.writer.write('='); this.writer.write(value); this.writer.write('\n'); @@ -242,7 +231,7 @@ public void write(String key, String value) throws IOException { // write in multiline format // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings]: - this.writer.write(encodedKey); + this.writer.write(key); this.writer.write("<<"); this.writer.write(separator); this.writer.write('\n'); diff --git a/action.yml b/action.yml index dc2d6d7..6b530a1 100644 --- a/action.yml +++ b/action.yml @@ -23,8 +23,12 @@ inputs: resultType: description: |- The result format and target. One of: - - "output": For each property key , set an output named "_". (The leading underscore serves to avoid conflicts with future output names used by this action.) Additionally, set output "value" to the last found value (which is only usefull if `keys` is non-empty, i.e. last given key wins; without keys, the last property in the undefined java.util.Properties iteration order wins). - - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. + - "output": For each property key , set an output named "_", but with special characters encoded. (The leading underscore serves to avoid conflicts with future output names used by this action.) + Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits + of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name + "a-002Eb-002Dc_d". + Additionally, set output "value" to the last found value, if any (which is only usefull if `keys` is non-empty, i.e. last given key wins; without keys, the last property in the undefined java.util.Properties iteration order wins). + - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. - "env": For each property key , set an environment variable . - "env:": For each property key , set an environment variable . @@ -33,9 +37,6 @@ inputs: - "json": Set an action output "json" to all properties as a JSON object, formatted as a single-line string. - "json-file:": Write a file with name with contents: all properties as a JSON object (formatting not specified). - For all output names, punctuation characters except the underscore ("_") are replaced with "-" followed by four hex digits - of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name - "a-002Eb-002Dc_d". required: false default: 'output' From b67551080338b42ddd980ac4fc0f56570bf64182 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 16:20:27 +0200 Subject: [PATCH 040/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b5f5aa..37bff00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ env: JSON_OUTPUT_FILE: tmp/out.json SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' - SELECTED_SINGLE_NAME: 'my/prop' + SELECTED_SINGLE_NAME: 'my-prop' SELECTED_NAMES: 'java-version jvm_args unicode colon' NOT_FOUND_STATUS: 54 @@ -280,8 +280,9 @@ jobs: - name: 'validate "test: query multiple properties as action outputs of given names"' env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} - # Output names specified as input are also subject to encoding (prefix "_", replace punctuation except "_"): - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): + RESULT_GETTER: getEnvJsonValue + UNUSED_RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateMultiNamedImpl |- eval "$SHELL_SETUP" assertEquals "$($RESULT_GETTER java-version)" '21' @@ -295,8 +296,18 @@ jobs: assertUndefined $RESULT_GETTER colon:in:Key assertUndefined $RESULT_GETTER unicode_escapes + # The outputs with the encoded names are not set (when different from given name): + assertUndefined $UNUSED_RESULT_GETTER java-version + assertUndefined $UNUSED_RESULT_GETTER colon + assertUndefined $UNUSED_RESULT_GETTER unicode + + # The outputs with the encoded default names are not set (when different from given name): + assertUndefined $UNUSED_RESULT_GETTER sourceJavaVersion + assertUndefined $UNUSED_RESULT_GETTER org.gradle.jvmargs + assertUndefined $UNUSED_RESULT_GETTER colon:in:Key + assertUndefined $UNUSED_RESULT_GETTER unicode_escapes + # Outputs for non-selected keys are not set, for example: - assertUndefined $RESULT_GETTER colon:in:Key assertUndefined $RESULT_GETTER colonInValue assertUndefined $RESULT_GETTER dash_1 @@ -311,11 +322,12 @@ jobs: - name: 'validate "test: query multiple properties as action outputs of given name"' env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} - # Output names specified as input are also subject to encoding (prefix "_", replace punctuation except "_"): - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): + RESULT_GETTER: getEnvJsonValue + UNUSED_RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER my/prop)" 'c' + assertEquals "$($RESULT_GETTER my-prop)" 'c' # The outputs with the default names are not set: assertUndefined $RESULT_GETTER sourceJavaVersion @@ -323,8 +335,16 @@ jobs: assertUndefined $RESULT_GETTER colon:in:Key assertUndefined $RESULT_GETTER unicode_escapes + # The output with the encoded name is not set: + assertUndefined $UNUSED_RESULT_GETTER my-prop + + # The outputs with the encoded default names are not set (when different from given name): + assertUndefined $UNUSED_RESULT_GETTER sourceJavaVersion + assertUndefined $UNUSED_RESULT_GETTER org.gradle.jvmargs + assertUndefined $UNUSED_RESULT_GETTER colon:in:Key + assertUndefined $UNUSED_RESULT_GETTER unicode_escapes + # Outputs for non-selected keys are not set, for example: - assertUndefined $RESULT_GETTER colon:in:Key assertUndefined $RESULT_GETTER colonInValue assertUndefined $RESULT_GETTER dash_1 From a0a12009f88eac7a106c97ea9372a58897d7d507 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 16:30:39 +0200 Subject: [PATCH 041/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37bff00..73d8423 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -282,7 +282,6 @@ jobs: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: getEnvJsonValue - UNUSED_RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateMultiNamedImpl |- eval "$SHELL_SETUP" assertEquals "$($RESULT_GETTER java-version)" '21' @@ -296,17 +295,6 @@ jobs: assertUndefined $RESULT_GETTER colon:in:Key assertUndefined $RESULT_GETTER unicode_escapes - # The outputs with the encoded names are not set (when different from given name): - assertUndefined $UNUSED_RESULT_GETTER java-version - assertUndefined $UNUSED_RESULT_GETTER colon - assertUndefined $UNUSED_RESULT_GETTER unicode - - # The outputs with the encoded default names are not set (when different from given name): - assertUndefined $UNUSED_RESULT_GETTER sourceJavaVersion - assertUndefined $UNUSED_RESULT_GETTER org.gradle.jvmargs - assertUndefined $UNUSED_RESULT_GETTER colon:in:Key - assertUndefined $UNUSED_RESULT_GETTER unicode_escapes - # Outputs for non-selected keys are not set, for example: assertUndefined $RESULT_GETTER colonInValue assertUndefined $RESULT_GETTER dash_1 @@ -324,7 +312,6 @@ jobs: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: getEnvJsonValue - UNUSED_RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" assertEquals "$($RESULT_GETTER my-prop)" 'c' @@ -335,15 +322,6 @@ jobs: assertUndefined $RESULT_GETTER colon:in:Key assertUndefined $RESULT_GETTER unicode_escapes - # The output with the encoded name is not set: - assertUndefined $UNUSED_RESULT_GETTER my-prop - - # The outputs with the encoded default names are not set (when different from given name): - assertUndefined $UNUSED_RESULT_GETTER sourceJavaVersion - assertUndefined $UNUSED_RESULT_GETTER org.gradle.jvmargs - assertUndefined $UNUSED_RESULT_GETTER colon:in:Key - assertUndefined $UNUSED_RESULT_GETTER unicode_escapes - # Outputs for non-selected keys are not set, for example: assertUndefined $RESULT_GETTER colonInValue assertUndefined $RESULT_GETTER dash_1 From da864a90bfb812a6c5745b10490d23ef66fb37ef Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 16:33:38 +0200 Subject: [PATCH 042/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 73d8423..ca28852 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -376,7 +376,7 @@ jobs: - name: 'validate "test: query all properties as JSON action output"' env: RESULT_JSON: ${{steps.all-json.outputs.json}} - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + RESULT_GETTER: getEnvJsonValue run: *validateAllImpl ############################################################ @@ -390,6 +390,7 @@ jobs: - name: 'validate "test: query multiple properties as JSON action output"' env: RESULT_JSON: ${{steps.multiple-json.outputs.json}} + RESULT_GETTER: getEnvJsonValue run: *validateMultiImpl ############################################################ From d765c2d4bd6c6f15e1892ffbc4beeffa756c1c29 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 16:46:11 +0200 Subject: [PATCH 043/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca28852..5efc114 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,13 @@ env: NOT_FOUND_STATUS: 54 SHELL_SETUP: |- + debugPrintf() { + local pattern=$1; shift + printf "::debug::${pattern}" "$@" >&2 + } + assertEquals() { + debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else @@ -45,6 +51,7 @@ env: # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { local key=$1; shift + debugPrintf 'getJsonValue("%s")\n' "$key" jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' } @@ -64,6 +71,7 @@ env: # Prints the value of the environment variable set for key $1 (when not using a prefix). getEnv() { local key=$1; shift + debugPrintf 'getEnv("%s")\n' "$key" local st if printenv -- "$key"; then : # OK From 3f04ccdaf7fe41e43e6f4b597fd8c43acc4f7df1 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 16:50:19 +0200 Subject: [PATCH 044/190] =?UTF-8?q?Revert=20"=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d765c2d4bd6c6f15e1892ffbc4beeffa756c1c29. --- .github/workflows/test.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5efc114..ca28852 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,13 +13,7 @@ env: NOT_FOUND_STATUS: 54 SHELL_SETUP: |- - debugPrintf() { - local pattern=$1; shift - printf "::debug::${pattern}" "$@" >&2 - } - assertEquals() { - debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else @@ -51,7 +45,6 @@ env: # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { local key=$1; shift - debugPrintf 'getJsonValue("%s")\n' "$key" jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' } @@ -71,7 +64,6 @@ env: # Prints the value of the environment variable set for key $1 (when not using a prefix). getEnv() { local key=$1; shift - debugPrintf 'getEnv("%s")\n' "$key" local st if printenv -- "$key"; then : # OK From d284c40414ffa6d93c7c48f4a8db6627d9cc97ff Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 16:56:52 +0200 Subject: [PATCH 045/190] =?UTF-8?q?Reapply=20"=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3f04ccdaf7fe41e43e6f4b597fd8c43acc4f7df1. --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca28852..5efc114 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,13 @@ env: NOT_FOUND_STATUS: 54 SHELL_SETUP: |- + debugPrintf() { + local pattern=$1; shift + printf "::debug::${pattern}" "$@" >&2 + } + assertEquals() { + debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else @@ -45,6 +51,7 @@ env: # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { local key=$1; shift + debugPrintf 'getJsonValue("%s")\n' "$key" jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' } @@ -64,6 +71,7 @@ env: # Prints the value of the environment variable set for key $1 (when not using a prefix). getEnv() { local key=$1; shift + debugPrintf 'getEnv("%s")\n' "$key" local st if printenv -- "$key"; then : # OK From 3c4154a27af11ac96f6dfbbdc5c935576eaf8e50 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 17:04:59 +0200 Subject: [PATCH 046/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5efc114..875e561 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -482,8 +482,8 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_SINGLE_KEY}}' - resultType: 'env-named:${{env.SELECTED_NAMES}}' + keys: '${{env.SELECTED_KEYS}}' + resultType: 'env-named:${{env.SELECTED_SINGLE_NAME}}' - name: 'validate "test: query multiple properties as environment variables of given names"' env: RESULT_GETTER: getEnv From f57db1e63d0de4e5eb8d1aa6a943566bd0e660fe Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 17:09:07 +0200 Subject: [PATCH 047/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 875e561..bbfe1d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -514,7 +514,7 @@ jobs: uses: ./ with: file: test/resources/test.properties - resultType: env + resultType: 'env:${{env.ENV_VAR_PREFIX}}' - name: 'validate "test: query all properties as environment variables"' env: RESULT_GETTER: getEnvWithPrefix @@ -530,7 +530,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: env + resultType: 'env:${{env.ENV_VAR_PREFIX}}' - name: 'validate "test: query multiple properties as environment variables with prefix"' env: RESULT_GETTER: getEnvWithPrefix @@ -546,7 +546,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' - resultType: env + resultType: 'env:${{env.ENV_VAR_PREFIX}}' - name: 'validate "test: query single property as environment variable with prefix"' env: RESULT_GETTER: getEnvWithPrefix From e4ec807089ef78ec102ef947001d6cf837c781f6 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 15:56:54 +0200 Subject: [PATCH 048/190] encode & prefix only generated, not given output names --- Action.java | 88 +++++++++++++++++++++++++---------------------------- action.yml | 11 ++++--- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/Action.java b/Action.java index 475374e..9429838 100644 --- a/Action.java +++ b/Action.java @@ -13,11 +13,9 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -78,17 +76,31 @@ enum ResultWriter { OUTPUT("output") { @Override public void write(Properties props, Config config) throws IOException { - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { + String lastValue = null; + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { for (Map.Entry entry : Util.stringEntries(props)) { - writer.write(entry.getKey(), entry.getValue()); + String key = encodeKey(config, config.outputPrefix() + entry.getKey()); + lastValue = entry.getValue(); + writer.write(key, lastValue); } - if (props.size() == 1) { - String value = Util.stringEntries(props).iterator().next().getValue(); - writer.write("value", value); + // TODO props must be in order of config.selectedKeys() + // Otherwise, this is arbitrary: + if (lastValue != null) { + writer.write("value", lastValue); } } } + + private static String encodeKey(Config config, String key) { + StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); + Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); + while (matcher.find()) { + matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); + } + matcher.appendTail(result); + return result.toString(); + } }, OUTPUT_NAMED("output-named") { @Override @@ -106,7 +118,7 @@ public void write(Properties props, Config config) throws IOException { @Override public void write(Properties props, Config config) throws IOException { String prefix = config.resultTypeArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open(config)) { + try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { for (Map.Entry entry : Util.stringEntries(props)) { writer.write(prefix + entry.getKey(), entry.getValue()); } @@ -117,7 +129,7 @@ public void write(Properties props, Config config) throws IOException { @Override public void write(Properties props, Config config) throws IOException { config.requireNoArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open(config)) { + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write("json", Util.toJson(props)); } } @@ -159,11 +171,11 @@ private static void writeNamedImpl(Properties props, Config config, GitHubOutput throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length + " arguments, but " + selectedKeys.length + " keys are selected"); } - try (GitHubVariableWriter writer = gitHubOutputFile.open(config)) { + try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.length; i++) { - String varName = resultNames[resultNames.length == 1 ? 0 : i]; + String name = resultNames[resultNames.length == 1 ? 0 : i]; String value = props.getProperty(selectedKeys[i]); - writer.write(varName, value); + writer.write(name, value); } } } @@ -171,30 +183,8 @@ private static void writeNamedImpl(Properties props, Config config, GitHubOutput enum GitHubOutputFile { - OUTPUT("GITHUB_OUTPUT") { - @Override - public GitHubVariableWriter open(Config config) throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, - k -> encodeKey(config, k)); - } - - private static String encodeKey(Config config, String key) { - StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); - result.append(config.outputPrefix()); - Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); - while (matcher.find()) { - matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); - } - matcher.appendTail(result); - return result.toString(); - } - }, - ENV("GITHUB_ENV") { - @Override - public GitHubVariableWriter open(Config config) throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName, k -> k); - } - }; + OUTPUT("GITHUB_OUTPUT"), // + ENV("GITHUB_ENV"); final String fileName; @@ -202,7 +192,9 @@ private GitHubOutputFile(String fileNameEnvVar) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); } - public abstract GitHubVariableWriter open(Config config) throws IOException; + public GitHubVariableWriter open() throws IOException { + return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName); + } } @@ -210,28 +202,34 @@ class GitHubVariableWriter implements AutoCloseable { private static final Pattern SIMPLE_VALUE = Pattern.compile("[\\w.-]+"); private final String description; - private final Function keyReplacer; private final Writer writer; - public GitHubVariableWriter(String description, String fileName, Function keyReplacer) throws IOException { + public GitHubVariableWriter(String description, String fileName) throws IOException { this.description = description; this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); - this.keyReplacer = Objects.requireNonNull(keyReplacer); } public void write(String key, String value) throws IOException { - String encodedKey = keyReplacer.apply(key); - System.err.format("%s %s\t:= \"%s\"\n", description, encodedKey, value); + System.err.format("%s %s\t:= \"%s\"\n", description, key, value); // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { - this.writer.write(encodedKey); + this.writer.write(key); this.writer.write('='); this.writer.write(value); this.writer.write('\n'); return; + } else { + writeMultiLine(key, value); } + } + /** + * Writes a key-value pair in + * multiline + * format. + */ + private void writeMultiLine(String key, String value) throws IOException { // determine separator line which does not occur in value: Set valueLines = Set.of(value.split("(?s)\n")); String separatorPart = "----"; @@ -240,9 +238,7 @@ public void write(String key, String value) throws IOException { separator = separator + separatorPart; } - // write in multiline format - // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#multiline-strings]: - this.writer.write(encodedKey); + this.writer.write(key); this.writer.write("<<"); this.writer.write(separator); this.writer.write('\n'); diff --git a/action.yml b/action.yml index dc2d6d7..6b530a1 100644 --- a/action.yml +++ b/action.yml @@ -23,8 +23,12 @@ inputs: resultType: description: |- The result format and target. One of: - - "output": For each property key , set an output named "_". (The leading underscore serves to avoid conflicts with future output names used by this action.) Additionally, set output "value" to the last found value (which is only usefull if `keys` is non-empty, i.e. last given key wins; without keys, the last property in the undefined java.util.Properties iteration order wins). - - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. + - "output": For each property key , set an output named "_", but with special characters encoded. (The leading underscore serves to avoid conflicts with future output names used by this action.) + Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits + of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name + "a-002Eb-002Dc_d". + Additionally, set output "value" to the last found value, if any (which is only usefull if `keys` is non-empty, i.e. last given key wins; without keys, the last property in the undefined java.util.Properties iteration order wins). + - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. - "env": For each property key , set an environment variable . - "env:": For each property key , set an environment variable . @@ -33,9 +37,6 @@ inputs: - "json": Set an action output "json" to all properties as a JSON object, formatted as a single-line string. - "json-file:": Write a file with name with contents: all properties as a JSON object (formatting not specified). - For all output names, punctuation characters except the underscore ("_") are replaced with "-" followed by four hex digits - of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name - "a-002Eb-002Dc_d". required: false default: 'output' From 593fc577340eb87f1a62ed3f2f6063d9bab2c8d2 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 17:24:01 +0200 Subject: [PATCH 049/190] test workflow: more cases; refactor into reusable script blocks --- .github/workflows/test.yml | 507 +++++++++++++++++++++++++++++---- test/resources/test.properties | 4 +- 2 files changed, 451 insertions(+), 60 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd6c0be..bbfe1d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,8 +4,22 @@ defaults: run: shell: bash env: + ENV_VAR_PREFIX: PROP_TEST__ + JSON_OUTPUT_FILE: tmp/out.json + SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' + SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' + SELECTED_SINGLE_NAME: 'my-prop' + SELECTED_NAMES: 'java-version jvm_args unicode colon' + + NOT_FOUND_STATUS: 54 SHELL_SETUP: |- - function assertEquals() { + debugPrintf() { + local pattern=$1; shift + printf "::debug::${pattern}" "$@" >&2 + } + + assertEquals() { + debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 else @@ -13,9 +27,70 @@ env: return 1 fi } - function jsonGet() { + + # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. + assertUndefined() { + local getterFunction=$1; shift + local value st + if value="$("$getterFunction" "$@")"; then + printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 + return 1 + elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then + printf 'ok: %s is undefined\n' "$*" >&2 + else + # no error message, assuming failed $getterFunction has already written one + return $st + fi + } + + # The following helper functions to get a value for a given property key from the result have a uniform + # API (property key as sole argument) so that the actual test code can be identical (and thereby reusable + # with YAML anchors) for different result types. This leads to some assumptions which might seem + # a little odd, like the JSON result file must be called $JSON_OUTPUT_FILE. + + # Expects JSON as stdin and extracts the value for key $1. + getJsonValue() { + local key=$1; shift + debugPrintf 'getJsonValue("%s")\n' "$key" + jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' + } + + # Expects JSON in variable RESULT_JSON and extracts the value for key $1. + getEnvJsonValue() { + getJsonValue "$@" <<<"$RESULT_JSON" + } + + # Expects JSON in variable RESULT_JSON and extracts the value for key . + encodeKeyAndGetEnvJsonValue() { + local key=$1; shift + local encodedKey + encodedKey="_$(perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge' <<<"$key")" + getEnvJsonValue "$encodedKey" "$@" + } + + # Prints the value of the environment variable set for key $1 (when not using a prefix). + getEnv() { local key=$1; shift - jq --arg k "$key" --raw-output '.[$k] // ""' + debugPrintf 'getEnv("%s")\n' "$key" + local st + if printenv -- "$key"; then + : # OK + elif [ ${st-$?} -eq 1 ]; then + return $NOT_FOUND_STATUS + else + # no error message, assuming printenv has already written one + return $st + fi + } + + # Prints the value of the environment variable set for key $1 when using a prefix. + getEnvWithPrefix() { + local key=$1; shift + getEnv "${ENV_VAR_PREFIX}${key}" "$@" + } + + getFileJsonValue() { + getJsonValue "$@" <"$JSON_OUTPUT_FILE" } jobs: @@ -30,6 +105,7 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} + ############################################################ - name: 'test (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true @@ -42,6 +118,86 @@ jobs: eval "$SHELL_SETUP" assertEquals '${{steps.error-input-resultType.outcome}}' failure + ############################################################ + - name: 'test (should fail): resultType "output-named" without names' + id: error-resultType-output-without-names + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output-named:' + - name: 'validate "test (should fail): resultType "output-named" without names"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-output-without-names.outcome}}' failure + + ############################################################ + - name: 'test (should fail): resultType "env-named" without names' + id: error-resultType-env-without-names + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'env-named:' + - name: 'validate "test (should fail): resultType "env-named" without names"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-env-without-names.outcome}}' failure + + ############################################################ + - name: 'test (should fail): resultType "output-named" without keys' + id: error-resultType-output-without-keys + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + resultType: 'output-named:a b' + - name: 'validate "test (should fail): resultType "output-named" without keys"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-output-without-keys.outcome}}' failure + + ############################################################ + - name: 'test (should fail): resultType "env-named" without keys' + id: error-resultType-env-without-keys + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + resultType: 'env-named:a b' + - name: 'validate "test (should fail): resultType "env-named" without keys"' + run: |- + eval "$SHELL_SETUP" + assertEquals '${{steps.error-resultType-env-without-keys.outcome}}' failure + + ############################################################ + # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output + # name (instead of re-implemented). + # It also demonstrates how you'd usually use outputs with one environment variable per output. + - name: 'test: query multiple properties as action outputs (simple)' + id: multi-output-simple + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}} dash-in-Key' + - name: 'validate "test: query multiple properties as action outputs (simple)"' + env: + RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} + RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} + RESULT_COLONK: ${{steps.multi-output-simple.outputs._colon-003Ain-003AKey}} + RESULT_DASHK: ${{steps.multi-output-simple.outputs._dash-002Din-002DKey}} + RESULT_UNI: ${{steps.multi-output-simple.outputs._unicode_escapes}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT_SJV" '21' + assertEquals "$RESULT_JVMARGS" '-ea -showversion' + assertEquals "$RESULT_COLONK" 'c' + assertEquals "$RESULT_DASHK" 'd' + assertEquals "$RESULT_UNI" 'a«,»c' + + ############################################################ - name: 'test: query all properties as action outputs' id: all-output uses: ./ @@ -49,30 +205,176 @@ jobs: file: test/resources/test.properties - name: 'validate "test: query all properties as action outputs"' env: - RESULT_SJV: ${{steps.all-output.outputs._sourceJavaVersion}} - RESULT_TJV: ${{steps.all-output.outputs._targetJavaVersion}} - RESULT_GRO: ${{steps.all-output.outputs._org-002Egradle-002Ejvmargs}} - run: |- + RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateAllImpl |- eval "$SHELL_SETUP" - assertEquals "$RESULT_SJV" 21 - assertEquals "$RESULT_TJV" 17 - assertEquals "$RESULT_GRO" '-ea -showversion' + assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' + assertEquals "$($RESULT_GETTER targetJavaVersion )" '17' + assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' + assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' + assertEquals "$($RESULT_GETTER colonInValue )" 'cv:' + assertEquals "$($RESULT_GETTER dash-in-Key )" 'd' + assertEquals "$($RESULT_GETTER dashInValue )" 'd-' + assertEquals "$($RESULT_GETTER empty )" '' + assertEquals "$($RESULT_GETTER null )" 'null' + # Expect additional linefeed (the usual output of commands, e.g. jq); + # add a trailing dummy char to preserve the otherwise trailing linefeed in bash command substitution [https://stackoverflow.com/a/15184414]: + assertEquals "$($RESULT_GETTER whitespace_escapes && printf '|')" $'A\tB\fC C\nD\n\n|' + assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' + assertEquals "$($RESULT_GETTER dash_1 )" '-' + assertEquals "$($RESULT_GETTER dash_2 )" '--' + assertEquals "$($RESULT_GETTER dash_3 )" '---' + assertEquals "$($RESULT_GETTER dash_4 )" '----' + assertEquals "$($RESULT_GETTER dash_5 )" '-----' + assertEquals "$($RESULT_GETTER dash_6 )" '------' + assertEquals "$($RESULT_GETTER dash_7 )" '-------' + assertEquals "$($RESULT_GETTER dash_8 )" '--------' + assertEquals "$($RESULT_GETTER dash_9 )" '---------' + assertEquals "$($RESULT_GETTER dash_10 )" '----------' + assertEquals "$($RESULT_GETTER dash_11 )" '-----------' + assertEquals "$($RESULT_GETTER dash_12 )" '------------' + assertEquals "$($RESULT_GETTER dash_3_4_1 )" $'---\n----\n-' + assertEquals "$($RESULT_GETTER dash_3_4_8_1 )" $'---\n----\n--------\n-' + ############################################################ - name: 'test: query multiple properties as action outputs' id: multi-output uses: ./ with: file: test/resources/test.properties - keys: 'sourceJavaVersion targetJavaVersion' + keys: '${{env.SELECTED_KEYS}}' - name: 'validate "test: query multiple properties as action outputs"' env: - RESULT_SJV: ${{steps.multi-output.outputs._sourceJavaVersion}} - RESULT_TJV: ${{steps.multi-output.outputs._targetJavaVersion}} - run: |- + RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateMultiImpl |- + eval "$SHELL_SETUP" + assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' + assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' + assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' + assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' + + assertUndefined $RESULT_GETTER targetJavaVersion + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash-in-Key + assertUndefined $RESULT_GETTER dashInValue + assertUndefined $RESULT_GETTER empty + assertUndefined $RESULT_GETTER null + assertUndefined $RESULT_GETTER whitespace_escapes + assertUndefined $RESULT_GETTER dash_1 + assertUndefined $RESULT_GETTER dash_2 + assertUndefined $RESULT_GETTER dash_3 + assertUndefined $RESULT_GETTER dash_4 + assertUndefined $RESULT_GETTER dash_5 + assertUndefined $RESULT_GETTER dash_6 + assertUndefined $RESULT_GETTER dash_7 + assertUndefined $RESULT_GETTER dash_8 + assertUndefined $RESULT_GETTER dash_9 + assertUndefined $RESULT_GETTER dash_10 + assertUndefined $RESULT_GETTER dash_11 + assertUndefined $RESULT_GETTER dash_12 + assertUndefined $RESULT_GETTER dash_3_4_1 + assertUndefined $RESULT_GETTER dash_3_4_8_1 + + ############################################################ + - name: 'test: query multiple properties as action outputs of given names' + id: multi-output-named + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output-named:${{env.SELECTED_NAMES}}' + - name: 'validate "test: query multiple properties as action outputs of given names"' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} + # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): + RESULT_GETTER: getEnvJsonValue + run: &validateMultiNamedImpl |- + eval "$SHELL_SETUP" + assertEquals "$($RESULT_GETTER java-version)" '21' + assertEquals "$($RESULT_GETTER jvm_args )" '-ea -showversion' + assertEquals "$($RESULT_GETTER colon )" 'c' + assertEquals "$($RESULT_GETTER unicode )" 'a«,»c' + + # The outputs with the default names are not set: + assertUndefined $RESULT_GETTER sourceJavaVersion + assertUndefined $RESULT_GETTER org.gradle.jvmargs + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER unicode_escapes + + # Outputs for non-selected keys are not set, for example: + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash_1 + + ############################################################ + - name: 'test: query multiple properties as action outputs of given name' + id: multi-output-named1 + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' + - name: 'validate "test: query multiple properties as action outputs of given name"' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} + # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): + RESULT_GETTER: getEnvJsonValue + run: &validateMultiNamed1Impl |- + eval "$SHELL_SETUP" + assertEquals "$($RESULT_GETTER my-prop)" 'c' + + # The outputs with the default names are not set: + assertUndefined $RESULT_GETTER sourceJavaVersion + assertUndefined $RESULT_GETTER org.gradle.jvmargs + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER unicode_escapes + + # Outputs for non-selected keys are not set, for example: + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash_1 + + ############################################################ + - name: 'test: query single property as action outputs' + id: single-output + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_SINGLE_KEY}}' + - name: 'validate "test: query multiple properties as action outputs"' + env: + RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} + RESULT_GETTER: encodeKeyAndGetEnvJsonValue + run: &validateSingleImpl |- eval "$SHELL_SETUP" - assertEquals "$RESULT_SJV" 21 - assertEquals "$RESULT_TJV" 17 + assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' + assertUndefined $RESULT_GETTER sourceJavaVersion + assertUndefined $RESULT_GETTER targetJavaVersion + assertUndefined $RESULT_GETTER colon:in:Key + assertUndefined $RESULT_GETTER colonInValue + assertUndefined $RESULT_GETTER dash-in-Key + assertUndefined $RESULT_GETTER dashInValue + assertUndefined $RESULT_GETTER empty + assertUndefined $RESULT_GETTER null + assertUndefined $RESULT_GETTER whitespace_escapes + assertUndefined $RESULT_GETTER unicode_escapes + assertUndefined $RESULT_GETTER dash_1 + assertUndefined $RESULT_GETTER dash_2 + assertUndefined $RESULT_GETTER dash_3 + assertUndefined $RESULT_GETTER dash_4 + assertUndefined $RESULT_GETTER dash_5 + assertUndefined $RESULT_GETTER dash_6 + assertUndefined $RESULT_GETTER dash_7 + assertUndefined $RESULT_GETTER dash_8 + assertUndefined $RESULT_GETTER dash_9 + assertUndefined $RESULT_GETTER dash_10 + assertUndefined $RESULT_GETTER dash_11 + assertUndefined $RESULT_GETTER dash_12 + assertUndefined $RESULT_GETTER dash_3_4_1 + assertUndefined $RESULT_GETTER dash_3_4_8_1 + + ############################################################ - name: 'test: query all properties as JSON action output' id: all-json uses: ./ @@ -81,51 +383,50 @@ jobs: resultType: json - name: 'validate "test: query all properties as JSON action output"' env: - RESULT: ${{steps.all-json.outputs.json}} - run: |- - eval "$SHELL_SETUP" - jq <<<"$RESULT" # assert parseable (and print) - # check some values: - assertEquals "$(jsonGet 'unicode_escapes' <<<"$RESULT")" 'a«,»c' - assertEquals "$(jsonGet 'colon:in:Key' <<<"$RESULT")" 'c' - # assert size >= 10: - test "$(jq --raw-output 'keys | length' <<<"$RESULT")" -ge 10 + RESULT_JSON: ${{steps.all-json.outputs.json}} + RESULT_GETTER: getEnvJsonValue + run: *validateAllImpl + ############################################################ - name: 'test: query multiple properties as JSON action output' id: multiple-json uses: ./ with: file: test/resources/test.properties - keys: 'colon:in:Key colonInValue unicode_escapes' + keys: '${{env.SELECTED_KEYS}}' resultType: json - name: 'validate "test: query multiple properties as JSON action output"' env: - RESULT: ${{steps.multiple-json.outputs.json}} - run: |- - eval "$SHELL_SETUP" - assertEquals "$(jsonGet 'unicode_escapes' <<<"$RESULT")" 'a«,»c' - assertEquals "$(jsonGet 'colonInValue' <<<"$RESULT")" 'cv:' - assertEquals "$(jsonGet 'colon:in:Key' <<<"$RESULT")" 'c' - # assert size 3 (i.e. no additional results): - assertEquals "$(jq --raw-output 'keys | length' <<<"$RESULT")" 3 + RESULT_JSON: ${{steps.multiple-json.outputs.json}} + RESULT_GETTER: getEnvJsonValue + run: *validateMultiImpl + + ############################################################ + - name: 'test: query all properties as JSON file' + uses: ./ + with: + file: test/resources/test.properties + resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' + - name: 'validate "test: query all properties as JSON file"' + env: + RESULT_GETTER: getFileJsonValue + run: *validateAllImpl + ############################################################ - name: 'test: query multiple properties as JSON file' uses: ./ with: file: test/resources/test.properties - keys: 'colon:in:Key colonInValue unicode_escapes' - resultType: json-file:tmp/multi.json + keys: '${{env.SELECTED_KEYS}}' + resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' - name: 'validate "test: query multiple properties as JSON file"' - run: |- - eval "$SHELL_SETUP" - result=$( Date: Wed, 13 May 2026 17:40:54 +0200 Subject: [PATCH 050/190] fix NullPointerException on missing keys input --- Action.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Action.java b/Action.java index 9429838..85d6e6f 100644 --- a/Action.java +++ b/Action.java @@ -166,6 +166,9 @@ public static ResultWriter of(String inputName) { private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { String[] selectedKeys = config.selectedKeys(); + if (selectedKeys == null) { + throw new IllegalArgumentException("invalid use of resultType " + config.resultType() + " (missing keys)"); + } String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator(), null); if (resultNames.length != selectedKeys.length && resultNames.length != 1) { throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length From 0dac66e01d1e25980d5673b15910da7174b6232f Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 17:52:57 +0200 Subject: [PATCH 051/190] logging (result for type "json-file") --- .github/workflows/test.yml | 1 + Action.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbfe1d9..356658c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,6 +90,7 @@ env: } getFileJsonValue() { + debugPrintf 'getFileJsonValue("%s") with file contents "%s"\n' "$*" "$(<"$JSON_OUTPUT_FILE")" getJsonValue "$@" <"$JSON_OUTPUT_FILE" } diff --git a/Action.java b/Action.java index 85d6e6f..f832bfb 100644 --- a/Action.java +++ b/Action.java @@ -140,7 +140,9 @@ public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE)) { - writer.write(Util.toJson(props)); + String jsonResult = Util.toJson(props); + System.err.format("writing to %s: %s\n", outputFile, jsonResult); + writer.write(jsonResult); writer.write('\n'); writer.flush(); } From da3e49fcf544c2b56ba143346b7bca7b63a78e44 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 19:01:31 +0200 Subject: [PATCH 052/190] fix test workflow: do not ignore result getter return status --- .github/workflows/test.yml | 218 ++++++++++++++++++++----------------- 1 file changed, 121 insertions(+), 97 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 356658c..53102ff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,17 @@ env: printf "::debug::${pattern}" "$@" >&2 } + # Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is + # missing. + # Piping a command to this function works around the removal of trailing LFs in command substitution + # [https://stackoverflow.com/a/15184414] and preserves meaningful LFs. + # For example, when the command is `jq --raw-output …` and the result is a string with ends with LF, that LF is + # preserved, and the LF added by jq (like line-based commands generally do) is replaced. + # This enables precise comparisons. + replaceLastLF() { + perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' + } + assertEquals() { debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then @@ -29,7 +40,22 @@ env: } # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. - assertUndefined() { + getAndAssertEquals() { + local getterFunction=$1; shift + local key=$1; shift + local expectedValue=$1; shift + local value st + # Invocation of $getterFunction must not ignore its return status. This is why tests use this function in the form + # getAndAssertEquals $RESULT_GETTER "$key" "$expectedValue" + # instead of + # assertEquals "$($RESULT_GETTER "$key")" "$expectedValue" + value="$(set -o pipefail; "$getterFunction" "$key" | replaceLastLF)" + expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" + assertEquals "$value" "$expectedValue" + } + + # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. + getAndAssertUndefined() { local getterFunction=$1; shift local value st if value="$("$getterFunction" "$@")"; then @@ -210,33 +236,31 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateAllImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' - assertEquals "$($RESULT_GETTER targetJavaVersion )" '17' - assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' - assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' - assertEquals "$($RESULT_GETTER colonInValue )" 'cv:' - assertEquals "$($RESULT_GETTER dash-in-Key )" 'd' - assertEquals "$($RESULT_GETTER dashInValue )" 'd-' - assertEquals "$($RESULT_GETTER empty )" '' - assertEquals "$($RESULT_GETTER null )" 'null' - # Expect additional linefeed (the usual output of commands, e.g. jq); - # add a trailing dummy char to preserve the otherwise trailing linefeed in bash command substitution [https://stackoverflow.com/a/15184414]: - assertEquals "$($RESULT_GETTER whitespace_escapes && printf '|')" $'A\tB\fC C\nD\n\n|' - assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' - assertEquals "$($RESULT_GETTER dash_1 )" '-' - assertEquals "$($RESULT_GETTER dash_2 )" '--' - assertEquals "$($RESULT_GETTER dash_3 )" '---' - assertEquals "$($RESULT_GETTER dash_4 )" '----' - assertEquals "$($RESULT_GETTER dash_5 )" '-----' - assertEquals "$($RESULT_GETTER dash_6 )" '------' - assertEquals "$($RESULT_GETTER dash_7 )" '-------' - assertEquals "$($RESULT_GETTER dash_8 )" '--------' - assertEquals "$($RESULT_GETTER dash_9 )" '---------' - assertEquals "$($RESULT_GETTER dash_10 )" '----------' - assertEquals "$($RESULT_GETTER dash_11 )" '-----------' - assertEquals "$($RESULT_GETTER dash_12 )" '------------' - assertEquals "$($RESULT_GETTER dash_3_4_1 )" $'---\n----\n-' - assertEquals "$($RESULT_GETTER dash_3_4_8_1 )" $'---\n----\n--------\n-' + getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + getAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' + getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + getAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' + getAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' + getAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' + getAndAssertEquals "$RESULT_GETTER" empty '' + getAndAssertEquals "$RESULT_GETTER" null 'null' + getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\nD\n\n' + getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + getAndAssertEquals "$RESULT_GETTER" dash_1 '-' + getAndAssertEquals "$RESULT_GETTER" dash_2 '--' + getAndAssertEquals "$RESULT_GETTER" dash_3 '---' + getAndAssertEquals "$RESULT_GETTER" dash_4 '----' + getAndAssertEquals "$RESULT_GETTER" dash_5 '-----' + getAndAssertEquals "$RESULT_GETTER" dash_6 '------' + getAndAssertEquals "$RESULT_GETTER" dash_7 '-------' + getAndAssertEquals "$RESULT_GETTER" dash_8 '--------' + getAndAssertEquals "$RESULT_GETTER" dash_9 '---------' + getAndAssertEquals "$RESULT_GETTER" dash_10 '----------' + getAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' + getAndAssertEquals "$RESULT_GETTER" dash_12 '------------' + getAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' + getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' ############################################################ - name: 'test: query multiple properties as action outputs' @@ -251,32 +275,32 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateMultiImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' - assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' - assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' - assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' - - assertUndefined $RESULT_GETTER targetJavaVersion - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash-in-Key - assertUndefined $RESULT_GETTER dashInValue - assertUndefined $RESULT_GETTER empty - assertUndefined $RESULT_GETTER null - assertUndefined $RESULT_GETTER whitespace_escapes - assertUndefined $RESULT_GETTER dash_1 - assertUndefined $RESULT_GETTER dash_2 - assertUndefined $RESULT_GETTER dash_3 - assertUndefined $RESULT_GETTER dash_4 - assertUndefined $RESULT_GETTER dash_5 - assertUndefined $RESULT_GETTER dash_6 - assertUndefined $RESULT_GETTER dash_7 - assertUndefined $RESULT_GETTER dash_8 - assertUndefined $RESULT_GETTER dash_9 - assertUndefined $RESULT_GETTER dash_10 - assertUndefined $RESULT_GETTER dash_11 - assertUndefined $RESULT_GETTER dash_12 - assertUndefined $RESULT_GETTER dash_3_4_1 - assertUndefined $RESULT_GETTER dash_3_4_8_1 + getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + + getAndAssertUndefined $RESULT_GETTER targetJavaVersion + getAndAssertUndefined $RESULT_GETTER colonInValue + getAndAssertUndefined $RESULT_GETTER dash-in-Key + getAndAssertUndefined $RESULT_GETTER dashInValue + getAndAssertUndefined $RESULT_GETTER empty + getAndAssertUndefined $RESULT_GETTER null + getAndAssertUndefined $RESULT_GETTER whitespace_escapes + getAndAssertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined $RESULT_GETTER dash_2 + getAndAssertUndefined $RESULT_GETTER dash_3 + getAndAssertUndefined $RESULT_GETTER dash_4 + getAndAssertUndefined $RESULT_GETTER dash_5 + getAndAssertUndefined $RESULT_GETTER dash_6 + getAndAssertUndefined $RESULT_GETTER dash_7 + getAndAssertUndefined $RESULT_GETTER dash_8 + getAndAssertUndefined $RESULT_GETTER dash_9 + getAndAssertUndefined $RESULT_GETTER dash_10 + getAndAssertUndefined $RESULT_GETTER dash_11 + getAndAssertUndefined $RESULT_GETTER dash_12 + getAndAssertUndefined $RESULT_GETTER dash_3_4_1 + getAndAssertUndefined $RESULT_GETTER dash_3_4_8_1 ############################################################ - name: 'test: query multiple properties as action outputs of given names' @@ -293,20 +317,20 @@ jobs: RESULT_GETTER: getEnvJsonValue run: &validateMultiNamedImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER java-version)" '21' - assertEquals "$($RESULT_GETTER jvm_args )" '-ea -showversion' - assertEquals "$($RESULT_GETTER colon )" 'c' - assertEquals "$($RESULT_GETTER unicode )" 'a«,»c' + getAndAssertEquals "$RESULT_GETTER" java-version '21' + getAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' + getAndAssertEquals "$RESULT_GETTER" colon 'c' + getAndAssertEquals "$RESULT_GETTER" unicode 'a«,»c' # The outputs with the default names are not set: - assertUndefined $RESULT_GETTER sourceJavaVersion - assertUndefined $RESULT_GETTER org.gradle.jvmargs - assertUndefined $RESULT_GETTER colon:in:Key - assertUndefined $RESULT_GETTER unicode_escapes + getAndAssertUndefined $RESULT_GETTER sourceJavaVersion + getAndAssertUndefined $RESULT_GETTER org.gradle.jvmargs + getAndAssertUndefined $RESULT_GETTER colon:in:Key + getAndAssertUndefined $RESULT_GETTER unicode_escapes # Outputs for non-selected keys are not set, for example: - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined $RESULT_GETTER colonInValue + getAndAssertUndefined $RESULT_GETTER dash_1 ############################################################ - name: 'test: query multiple properties as action outputs of given name' @@ -323,17 +347,17 @@ jobs: RESULT_GETTER: getEnvJsonValue run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER my-prop)" 'c' + getAndAssertEquals "$RESULT_GETTER" my-prop 'c' # The outputs with the default names are not set: - assertUndefined $RESULT_GETTER sourceJavaVersion - assertUndefined $RESULT_GETTER org.gradle.jvmargs - assertUndefined $RESULT_GETTER colon:in:Key - assertUndefined $RESULT_GETTER unicode_escapes + getAndAssertUndefined $RESULT_GETTER sourceJavaVersion + getAndAssertUndefined $RESULT_GETTER org.gradle.jvmargs + getAndAssertUndefined $RESULT_GETTER colon:in:Key + getAndAssertUndefined $RESULT_GETTER unicode_escapes # Outputs for non-selected keys are not set, for example: - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined $RESULT_GETTER colonInValue + getAndAssertUndefined $RESULT_GETTER dash_1 ############################################################ - name: 'test: query single property as action outputs' @@ -348,32 +372,32 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateSingleImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' - - assertUndefined $RESULT_GETTER sourceJavaVersion - assertUndefined $RESULT_GETTER targetJavaVersion - assertUndefined $RESULT_GETTER colon:in:Key - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash-in-Key - assertUndefined $RESULT_GETTER dashInValue - assertUndefined $RESULT_GETTER empty - assertUndefined $RESULT_GETTER null - assertUndefined $RESULT_GETTER whitespace_escapes - assertUndefined $RESULT_GETTER unicode_escapes - assertUndefined $RESULT_GETTER dash_1 - assertUndefined $RESULT_GETTER dash_2 - assertUndefined $RESULT_GETTER dash_3 - assertUndefined $RESULT_GETTER dash_4 - assertUndefined $RESULT_GETTER dash_5 - assertUndefined $RESULT_GETTER dash_6 - assertUndefined $RESULT_GETTER dash_7 - assertUndefined $RESULT_GETTER dash_8 - assertUndefined $RESULT_GETTER dash_9 - assertUndefined $RESULT_GETTER dash_10 - assertUndefined $RESULT_GETTER dash_11 - assertUndefined $RESULT_GETTER dash_12 - assertUndefined $RESULT_GETTER dash_3_4_1 - assertUndefined $RESULT_GETTER dash_3_4_8_1 + getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + + getAndAssertUndefined $RESULT_GETTER sourceJavaVersion + getAndAssertUndefined $RESULT_GETTER targetJavaVersion + getAndAssertUndefined $RESULT_GETTER colon:in:Key + getAndAssertUndefined $RESULT_GETTER colonInValue + getAndAssertUndefined $RESULT_GETTER dash-in-Key + getAndAssertUndefined $RESULT_GETTER dashInValue + getAndAssertUndefined $RESULT_GETTER empty + getAndAssertUndefined $RESULT_GETTER null + getAndAssertUndefined $RESULT_GETTER whitespace_escapes + getAndAssertUndefined $RESULT_GETTER unicode_escapes + getAndAssertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined $RESULT_GETTER dash_2 + getAndAssertUndefined $RESULT_GETTER dash_3 + getAndAssertUndefined $RESULT_GETTER dash_4 + getAndAssertUndefined $RESULT_GETTER dash_5 + getAndAssertUndefined $RESULT_GETTER dash_6 + getAndAssertUndefined $RESULT_GETTER dash_7 + getAndAssertUndefined $RESULT_GETTER dash_8 + getAndAssertUndefined $RESULT_GETTER dash_9 + getAndAssertUndefined $RESULT_GETTER dash_10 + getAndAssertUndefined $RESULT_GETTER dash_11 + getAndAssertUndefined $RESULT_GETTER dash_12 + getAndAssertUndefined $RESULT_GETTER dash_3_4_1 + getAndAssertUndefined $RESULT_GETTER dash_3_4_8_1 ############################################################ - name: 'test: query all properties as JSON action output' From a1dff5fe6995b9b9ff072ae7edeb7bec039495c2 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 19:10:56 +0200 Subject: [PATCH 053/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 170 +++++++++++++++++---------------- test/resources/test.properties | 4 +- 2 files changed, 91 insertions(+), 83 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53102ff..338a353 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -236,31 +236,33 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateAllImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' - getAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' - getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' - getAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' - getAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' - getAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' - getAndAssertEquals "$RESULT_GETTER" empty '' - getAndAssertEquals "$RESULT_GETTER" null 'null' - getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\nD\n\n' - getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - getAndAssertEquals "$RESULT_GETTER" dash_1 '-' - getAndAssertEquals "$RESULT_GETTER" dash_2 '--' - getAndAssertEquals "$RESULT_GETTER" dash_3 '---' - getAndAssertEquals "$RESULT_GETTER" dash_4 '----' - getAndAssertEquals "$RESULT_GETTER" dash_5 '-----' - getAndAssertEquals "$RESULT_GETTER" dash_6 '------' - getAndAssertEquals "$RESULT_GETTER" dash_7 '-------' - getAndAssertEquals "$RESULT_GETTER" dash_8 '--------' - getAndAssertEquals "$RESULT_GETTER" dash_9 '---------' - getAndAssertEquals "$RESULT_GETTER" dash_10 '----------' - getAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' - getAndAssertEquals "$RESULT_GETTER" dash_12 '------------' - getAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' - getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' + getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + getAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' + getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + getAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' + getAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' + getAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' + getAndAssertEquals "$RESULT_GETTER" empty '' + getAndAssertEquals "$RESULT_GETTER" null 'null' + getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\nD\n' + getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_1 $'one trailing LF\n' + getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_2 $'two trailing LFs\n\n' + getAndAssertEquals "$RESULT_GETTER" dash_1 '-' + getAndAssertEquals "$RESULT_GETTER" dash_2 '--' + getAndAssertEquals "$RESULT_GETTER" dash_3 '---' + getAndAssertEquals "$RESULT_GETTER" dash_4 '----' + getAndAssertEquals "$RESULT_GETTER" dash_5 '-----' + getAndAssertEquals "$RESULT_GETTER" dash_6 '------' + getAndAssertEquals "$RESULT_GETTER" dash_7 '-------' + getAndAssertEquals "$RESULT_GETTER" dash_8 '--------' + getAndAssertEquals "$RESULT_GETTER" dash_9 '---------' + getAndAssertEquals "$RESULT_GETTER" dash_10 '----------' + getAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' + getAndAssertEquals "$RESULT_GETTER" dash_12 '------------' + getAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' + getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' ############################################################ - name: 'test: query multiple properties as action outputs' @@ -280,27 +282,29 @@ jobs: getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - getAndAssertUndefined $RESULT_GETTER targetJavaVersion - getAndAssertUndefined $RESULT_GETTER colonInValue - getAndAssertUndefined $RESULT_GETTER dash-in-Key - getAndAssertUndefined $RESULT_GETTER dashInValue - getAndAssertUndefined $RESULT_GETTER empty - getAndAssertUndefined $RESULT_GETTER null - getAndAssertUndefined $RESULT_GETTER whitespace_escapes - getAndAssertUndefined $RESULT_GETTER dash_1 - getAndAssertUndefined $RESULT_GETTER dash_2 - getAndAssertUndefined $RESULT_GETTER dash_3 - getAndAssertUndefined $RESULT_GETTER dash_4 - getAndAssertUndefined $RESULT_GETTER dash_5 - getAndAssertUndefined $RESULT_GETTER dash_6 - getAndAssertUndefined $RESULT_GETTER dash_7 - getAndAssertUndefined $RESULT_GETTER dash_8 - getAndAssertUndefined $RESULT_GETTER dash_9 - getAndAssertUndefined $RESULT_GETTER dash_10 - getAndAssertUndefined $RESULT_GETTER dash_11 - getAndAssertUndefined $RESULT_GETTER dash_12 - getAndAssertUndefined $RESULT_GETTER dash_3_4_1 - getAndAssertUndefined $RESULT_GETTER dash_3_4_8_1 + getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash-in-Key + getAndAssertUndefined "$RESULT_GETTER" dashInValue + getAndAssertUndefined "$RESULT_GETTER" empty + getAndAssertUndefined "$RESULT_GETTER" null + getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + getAndAssertUndefined "$RESULT_GETTER" dash_1 + getAndAssertUndefined "$RESULT_GETTER" dash_2 + getAndAssertUndefined "$RESULT_GETTER" dash_3 + getAndAssertUndefined "$RESULT_GETTER" dash_4 + getAndAssertUndefined "$RESULT_GETTER" dash_5 + getAndAssertUndefined "$RESULT_GETTER" dash_6 + getAndAssertUndefined "$RESULT_GETTER" dash_7 + getAndAssertUndefined "$RESULT_GETTER" dash_8 + getAndAssertUndefined "$RESULT_GETTER" dash_9 + getAndAssertUndefined "$RESULT_GETTER" dash_10 + getAndAssertUndefined "$RESULT_GETTER" dash_11 + getAndAssertUndefined "$RESULT_GETTER" dash_12 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: 'test: query multiple properties as action outputs of given names' @@ -323,14 +327,14 @@ jobs: getAndAssertEquals "$RESULT_GETTER" unicode 'a«,»c' # The outputs with the default names are not set: - getAndAssertUndefined $RESULT_GETTER sourceJavaVersion - getAndAssertUndefined $RESULT_GETTER org.gradle.jvmargs - getAndAssertUndefined $RESULT_GETTER colon:in:Key - getAndAssertUndefined $RESULT_GETTER unicode_escapes + getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + getAndAssertUndefined "$RESULT_GETTER" colon:in:Key + getAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - getAndAssertUndefined $RESULT_GETTER colonInValue - getAndAssertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: 'test: query multiple properties as action outputs of given name' @@ -350,14 +354,14 @@ jobs: getAndAssertEquals "$RESULT_GETTER" my-prop 'c' # The outputs with the default names are not set: - getAndAssertUndefined $RESULT_GETTER sourceJavaVersion - getAndAssertUndefined $RESULT_GETTER org.gradle.jvmargs - getAndAssertUndefined $RESULT_GETTER colon:in:Key - getAndAssertUndefined $RESULT_GETTER unicode_escapes + getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + getAndAssertUndefined "$RESULT_GETTER" colon:in:Key + getAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - getAndAssertUndefined $RESULT_GETTER colonInValue - getAndAssertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: 'test: query single property as action outputs' @@ -374,30 +378,32 @@ jobs: eval "$SHELL_SETUP" getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - getAndAssertUndefined $RESULT_GETTER sourceJavaVersion - getAndAssertUndefined $RESULT_GETTER targetJavaVersion - getAndAssertUndefined $RESULT_GETTER colon:in:Key - getAndAssertUndefined $RESULT_GETTER colonInValue - getAndAssertUndefined $RESULT_GETTER dash-in-Key - getAndAssertUndefined $RESULT_GETTER dashInValue - getAndAssertUndefined $RESULT_GETTER empty - getAndAssertUndefined $RESULT_GETTER null - getAndAssertUndefined $RESULT_GETTER whitespace_escapes - getAndAssertUndefined $RESULT_GETTER unicode_escapes - getAndAssertUndefined $RESULT_GETTER dash_1 - getAndAssertUndefined $RESULT_GETTER dash_2 - getAndAssertUndefined $RESULT_GETTER dash_3 - getAndAssertUndefined $RESULT_GETTER dash_4 - getAndAssertUndefined $RESULT_GETTER dash_5 - getAndAssertUndefined $RESULT_GETTER dash_6 - getAndAssertUndefined $RESULT_GETTER dash_7 - getAndAssertUndefined $RESULT_GETTER dash_8 - getAndAssertUndefined $RESULT_GETTER dash_9 - getAndAssertUndefined $RESULT_GETTER dash_10 - getAndAssertUndefined $RESULT_GETTER dash_11 - getAndAssertUndefined $RESULT_GETTER dash_12 - getAndAssertUndefined $RESULT_GETTER dash_3_4_1 - getAndAssertUndefined $RESULT_GETTER dash_3_4_8_1 + getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + getAndAssertUndefined "$RESULT_GETTER" colon:in:Key + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash-in-Key + getAndAssertUndefined "$RESULT_GETTER" dashInValue + getAndAssertUndefined "$RESULT_GETTER" empty + getAndAssertUndefined "$RESULT_GETTER" null + getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + getAndAssertUndefined "$RESULT_GETTER" unicode_escapes + getAndAssertUndefined "$RESULT_GETTER" dash_1 + getAndAssertUndefined "$RESULT_GETTER" dash_2 + getAndAssertUndefined "$RESULT_GETTER" dash_3 + getAndAssertUndefined "$RESULT_GETTER" dash_4 + getAndAssertUndefined "$RESULT_GETTER" dash_5 + getAndAssertUndefined "$RESULT_GETTER" dash_6 + getAndAssertUndefined "$RESULT_GETTER" dash_7 + getAndAssertUndefined "$RESULT_GETTER" dash_8 + getAndAssertUndefined "$RESULT_GETTER" dash_9 + getAndAssertUndefined "$RESULT_GETTER" dash_10 + getAndAssertUndefined "$RESULT_GETTER" dash_11 + getAndAssertUndefined "$RESULT_GETTER" dash_12 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: 'test: query all properties as JSON action output' diff --git a/test/resources/test.properties b/test/resources/test.properties index 06bd6b4..5bfa550 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -9,8 +9,10 @@ dashInValue: d- empty: null: null -whitespace_escapes = A\tB\fC C\nD\n unicode_escapes = a\u00AB,\u00BBc +whitespace_escapes = A\tB\fC C\nD\n +trailing_linefeed_1 = one trailing LF\n +trailing_linefeed_2 = two trailing LFs\n\n # For white box tests: When writing to $GITHUB_OUTPUT or $GITHUB_ENV, "----" is the default delimiter used in this action. # Whenever the value contains a line "----", another delimiter must be chosen. From 2252d4e386cd2d116f3ad4ec7710994f8239d6c2 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 19:11:46 +0200 Subject: [PATCH 054/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- test/resources/test.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 338a353..bbba533 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -246,7 +246,7 @@ jobs: getAndAssertEquals "$RESULT_GETTER" empty '' getAndAssertEquals "$RESULT_GETTER" null 'null' getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\nD\n' + getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\r\n\nD' getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_1 $'one trailing LF\n' getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_2 $'two trailing LFs\n\n' getAndAssertEquals "$RESULT_GETTER" dash_1 '-' diff --git a/test/resources/test.properties b/test/resources/test.properties index 5bfa550..64bd76c 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -10,7 +10,7 @@ empty: null: null unicode_escapes = a\u00AB,\u00BBc -whitespace_escapes = A\tB\fC C\nD\n +whitespace_escapes = A\tB\fC C\r\n\nD trailing_linefeed_1 = one trailing LF\n trailing_linefeed_2 = two trailing LFs\n\n From 5a8bcaf9dac2dbc0746d4f05d4b71a2c047d65c8 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 19:01:31 +0200 Subject: [PATCH 055/190] fix test workflow: do not ignore result getter return status --- .github/workflows/test.yml | 224 +++++++++++++++++++-------------- test/resources/test.properties | 4 +- 2 files changed, 130 insertions(+), 98 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 356658c..bbba533 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,17 @@ env: printf "::debug::${pattern}" "$@" >&2 } + # Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is + # missing. + # Piping a command to this function works around the removal of trailing LFs in command substitution + # [https://stackoverflow.com/a/15184414] and preserves meaningful LFs. + # For example, when the command is `jq --raw-output …` and the result is a string with ends with LF, that LF is + # preserved, and the LF added by jq (like line-based commands generally do) is replaced. + # This enables precise comparisons. + replaceLastLF() { + perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' + } + assertEquals() { debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then @@ -29,7 +40,22 @@ env: } # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. - assertUndefined() { + getAndAssertEquals() { + local getterFunction=$1; shift + local key=$1; shift + local expectedValue=$1; shift + local value st + # Invocation of $getterFunction must not ignore its return status. This is why tests use this function in the form + # getAndAssertEquals $RESULT_GETTER "$key" "$expectedValue" + # instead of + # assertEquals "$($RESULT_GETTER "$key")" "$expectedValue" + value="$(set -o pipefail; "$getterFunction" "$key" | replaceLastLF)" + expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" + assertEquals "$value" "$expectedValue" + } + + # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. + getAndAssertUndefined() { local getterFunction=$1; shift local value st if value="$("$getterFunction" "$@")"; then @@ -210,33 +236,33 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateAllImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' - assertEquals "$($RESULT_GETTER targetJavaVersion )" '17' - assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' - assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' - assertEquals "$($RESULT_GETTER colonInValue )" 'cv:' - assertEquals "$($RESULT_GETTER dash-in-Key )" 'd' - assertEquals "$($RESULT_GETTER dashInValue )" 'd-' - assertEquals "$($RESULT_GETTER empty )" '' - assertEquals "$($RESULT_GETTER null )" 'null' - # Expect additional linefeed (the usual output of commands, e.g. jq); - # add a trailing dummy char to preserve the otherwise trailing linefeed in bash command substitution [https://stackoverflow.com/a/15184414]: - assertEquals "$($RESULT_GETTER whitespace_escapes && printf '|')" $'A\tB\fC C\nD\n\n|' - assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' - assertEquals "$($RESULT_GETTER dash_1 )" '-' - assertEquals "$($RESULT_GETTER dash_2 )" '--' - assertEquals "$($RESULT_GETTER dash_3 )" '---' - assertEquals "$($RESULT_GETTER dash_4 )" '----' - assertEquals "$($RESULT_GETTER dash_5 )" '-----' - assertEquals "$($RESULT_GETTER dash_6 )" '------' - assertEquals "$($RESULT_GETTER dash_7 )" '-------' - assertEquals "$($RESULT_GETTER dash_8 )" '--------' - assertEquals "$($RESULT_GETTER dash_9 )" '---------' - assertEquals "$($RESULT_GETTER dash_10 )" '----------' - assertEquals "$($RESULT_GETTER dash_11 )" '-----------' - assertEquals "$($RESULT_GETTER dash_12 )" '------------' - assertEquals "$($RESULT_GETTER dash_3_4_1 )" $'---\n----\n-' - assertEquals "$($RESULT_GETTER dash_3_4_8_1 )" $'---\n----\n--------\n-' + getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + getAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' + getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + getAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' + getAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' + getAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' + getAndAssertEquals "$RESULT_GETTER" empty '' + getAndAssertEquals "$RESULT_GETTER" null 'null' + getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\r\n\nD' + getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_1 $'one trailing LF\n' + getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_2 $'two trailing LFs\n\n' + getAndAssertEquals "$RESULT_GETTER" dash_1 '-' + getAndAssertEquals "$RESULT_GETTER" dash_2 '--' + getAndAssertEquals "$RESULT_GETTER" dash_3 '---' + getAndAssertEquals "$RESULT_GETTER" dash_4 '----' + getAndAssertEquals "$RESULT_GETTER" dash_5 '-----' + getAndAssertEquals "$RESULT_GETTER" dash_6 '------' + getAndAssertEquals "$RESULT_GETTER" dash_7 '-------' + getAndAssertEquals "$RESULT_GETTER" dash_8 '--------' + getAndAssertEquals "$RESULT_GETTER" dash_9 '---------' + getAndAssertEquals "$RESULT_GETTER" dash_10 '----------' + getAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' + getAndAssertEquals "$RESULT_GETTER" dash_12 '------------' + getAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' + getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' ############################################################ - name: 'test: query multiple properties as action outputs' @@ -251,32 +277,34 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateMultiImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER sourceJavaVersion )" '21' - assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' - assertEquals "$($RESULT_GETTER colon:in:Key )" 'c' - assertEquals "$($RESULT_GETTER unicode_escapes )" 'a«,»c' - - assertUndefined $RESULT_GETTER targetJavaVersion - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash-in-Key - assertUndefined $RESULT_GETTER dashInValue - assertUndefined $RESULT_GETTER empty - assertUndefined $RESULT_GETTER null - assertUndefined $RESULT_GETTER whitespace_escapes - assertUndefined $RESULT_GETTER dash_1 - assertUndefined $RESULT_GETTER dash_2 - assertUndefined $RESULT_GETTER dash_3 - assertUndefined $RESULT_GETTER dash_4 - assertUndefined $RESULT_GETTER dash_5 - assertUndefined $RESULT_GETTER dash_6 - assertUndefined $RESULT_GETTER dash_7 - assertUndefined $RESULT_GETTER dash_8 - assertUndefined $RESULT_GETTER dash_9 - assertUndefined $RESULT_GETTER dash_10 - assertUndefined $RESULT_GETTER dash_11 - assertUndefined $RESULT_GETTER dash_12 - assertUndefined $RESULT_GETTER dash_3_4_1 - assertUndefined $RESULT_GETTER dash_3_4_8_1 + getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + + getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash-in-Key + getAndAssertUndefined "$RESULT_GETTER" dashInValue + getAndAssertUndefined "$RESULT_GETTER" empty + getAndAssertUndefined "$RESULT_GETTER" null + getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + getAndAssertUndefined "$RESULT_GETTER" dash_1 + getAndAssertUndefined "$RESULT_GETTER" dash_2 + getAndAssertUndefined "$RESULT_GETTER" dash_3 + getAndAssertUndefined "$RESULT_GETTER" dash_4 + getAndAssertUndefined "$RESULT_GETTER" dash_5 + getAndAssertUndefined "$RESULT_GETTER" dash_6 + getAndAssertUndefined "$RESULT_GETTER" dash_7 + getAndAssertUndefined "$RESULT_GETTER" dash_8 + getAndAssertUndefined "$RESULT_GETTER" dash_9 + getAndAssertUndefined "$RESULT_GETTER" dash_10 + getAndAssertUndefined "$RESULT_GETTER" dash_11 + getAndAssertUndefined "$RESULT_GETTER" dash_12 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: 'test: query multiple properties as action outputs of given names' @@ -293,20 +321,20 @@ jobs: RESULT_GETTER: getEnvJsonValue run: &validateMultiNamedImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER java-version)" '21' - assertEquals "$($RESULT_GETTER jvm_args )" '-ea -showversion' - assertEquals "$($RESULT_GETTER colon )" 'c' - assertEquals "$($RESULT_GETTER unicode )" 'a«,»c' + getAndAssertEquals "$RESULT_GETTER" java-version '21' + getAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' + getAndAssertEquals "$RESULT_GETTER" colon 'c' + getAndAssertEquals "$RESULT_GETTER" unicode 'a«,»c' # The outputs with the default names are not set: - assertUndefined $RESULT_GETTER sourceJavaVersion - assertUndefined $RESULT_GETTER org.gradle.jvmargs - assertUndefined $RESULT_GETTER colon:in:Key - assertUndefined $RESULT_GETTER unicode_escapes + getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + getAndAssertUndefined "$RESULT_GETTER" colon:in:Key + getAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: 'test: query multiple properties as action outputs of given name' @@ -323,17 +351,17 @@ jobs: RESULT_GETTER: getEnvJsonValue run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER my-prop)" 'c' + getAndAssertEquals "$RESULT_GETTER" my-prop 'c' # The outputs with the default names are not set: - assertUndefined $RESULT_GETTER sourceJavaVersion - assertUndefined $RESULT_GETTER org.gradle.jvmargs - assertUndefined $RESULT_GETTER colon:in:Key - assertUndefined $RESULT_GETTER unicode_escapes + getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + getAndAssertUndefined "$RESULT_GETTER" colon:in:Key + getAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash_1 + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: 'test: query single property as action outputs' @@ -348,32 +376,34 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateSingleImpl |- eval "$SHELL_SETUP" - assertEquals "$($RESULT_GETTER org.gradle.jvmargs)" '-ea -showversion' - - assertUndefined $RESULT_GETTER sourceJavaVersion - assertUndefined $RESULT_GETTER targetJavaVersion - assertUndefined $RESULT_GETTER colon:in:Key - assertUndefined $RESULT_GETTER colonInValue - assertUndefined $RESULT_GETTER dash-in-Key - assertUndefined $RESULT_GETTER dashInValue - assertUndefined $RESULT_GETTER empty - assertUndefined $RESULT_GETTER null - assertUndefined $RESULT_GETTER whitespace_escapes - assertUndefined $RESULT_GETTER unicode_escapes - assertUndefined $RESULT_GETTER dash_1 - assertUndefined $RESULT_GETTER dash_2 - assertUndefined $RESULT_GETTER dash_3 - assertUndefined $RESULT_GETTER dash_4 - assertUndefined $RESULT_GETTER dash_5 - assertUndefined $RESULT_GETTER dash_6 - assertUndefined $RESULT_GETTER dash_7 - assertUndefined $RESULT_GETTER dash_8 - assertUndefined $RESULT_GETTER dash_9 - assertUndefined $RESULT_GETTER dash_10 - assertUndefined $RESULT_GETTER dash_11 - assertUndefined $RESULT_GETTER dash_12 - assertUndefined $RESULT_GETTER dash_3_4_1 - assertUndefined $RESULT_GETTER dash_3_4_8_1 + getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + + getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + getAndAssertUndefined "$RESULT_GETTER" colon:in:Key + getAndAssertUndefined "$RESULT_GETTER" colonInValue + getAndAssertUndefined "$RESULT_GETTER" dash-in-Key + getAndAssertUndefined "$RESULT_GETTER" dashInValue + getAndAssertUndefined "$RESULT_GETTER" empty + getAndAssertUndefined "$RESULT_GETTER" null + getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + getAndAssertUndefined "$RESULT_GETTER" unicode_escapes + getAndAssertUndefined "$RESULT_GETTER" dash_1 + getAndAssertUndefined "$RESULT_GETTER" dash_2 + getAndAssertUndefined "$RESULT_GETTER" dash_3 + getAndAssertUndefined "$RESULT_GETTER" dash_4 + getAndAssertUndefined "$RESULT_GETTER" dash_5 + getAndAssertUndefined "$RESULT_GETTER" dash_6 + getAndAssertUndefined "$RESULT_GETTER" dash_7 + getAndAssertUndefined "$RESULT_GETTER" dash_8 + getAndAssertUndefined "$RESULT_GETTER" dash_9 + getAndAssertUndefined "$RESULT_GETTER" dash_10 + getAndAssertUndefined "$RESULT_GETTER" dash_11 + getAndAssertUndefined "$RESULT_GETTER" dash_12 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: 'test: query all properties as JSON action output' diff --git a/test/resources/test.properties b/test/resources/test.properties index 06bd6b4..64bd76c 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -9,8 +9,10 @@ dashInValue: d- empty: null: null -whitespace_escapes = A\tB\fC C\nD\n unicode_escapes = a\u00AB,\u00BBc +whitespace_escapes = A\tB\fC C\r\n\nD +trailing_linefeed_1 = one trailing LF\n +trailing_linefeed_2 = two trailing LFs\n\n # For white box tests: When writing to $GITHUB_OUTPUT or $GITHUB_ENV, "----" is the default delimiter used in this action. # Whenever the value contains a line "----", another delimiter must be chosen. From c226f197446111e61145e6856c669d7d064757ad Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 13 May 2026 19:41:44 +0200 Subject: [PATCH 056/190] fix resultType "json-file": truncate output file --- Action.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Action.java b/Action.java index f832bfb..d6fcc00 100644 --- a/Action.java +++ b/Action.java @@ -139,7 +139,7 @@ public void write(Properties props, Config config) throws IOException { public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); - try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE)) { + try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); System.err.format("writing to %s: %s\n", outputFile, jsonResult); writer.write(jsonResult); @@ -211,7 +211,7 @@ class GitHubVariableWriter implements AutoCloseable { public GitHubVariableWriter(String description, String fileName) throws IOException { this.description = description; - this.writer = Util.openFile(fileName, StandardOpenOption.APPEND); + this.writer = Util.openFile(fileName, StandardOpenOption.CREATE, StandardOpenOption.APPEND); } public void write(String key, String value) throws IOException { From 617ea128516d91f976a1302131a36548cf569e5e Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 10:00:02 +0200 Subject: [PATCH 057/190] linting --- Action.java | 60 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/Action.java b/Action.java index d6fcc00..dbdf27d 100644 --- a/Action.java +++ b/Action.java @@ -21,6 +21,8 @@ public class Action { + private Action() {} + static void main(String[] args) throws Exception { Config config = Config.fromEnv(); ResultWriter resultWriter = ResultWriter.of(config.resultType()); @@ -33,7 +35,7 @@ static void main(String[] args) throws Exception { } -record Config(String[] selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, +record Config(List selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. @@ -41,7 +43,7 @@ public static Config fromEnv() { // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty // string: String keysStr = System.getenv().getOrDefault("KEYS", ""); - String[] keys = Util.splitArray(keysStr, keySeparator, null); + Optional keys = Util.splitArray(keysStr, keySeparator); String resultNameSeparator = Util.getRequiredEnv("RESULT_NAME_SEPARATOR"); // RESULT_TYPE format: "[:]" @@ -53,7 +55,8 @@ public static Config fromEnv() { String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); String outputPrefix = Util.getRequiredEnv("OUTPUT_PREFIX"); - return new Config(keys, keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); + return new Config(keys.map(List::of).orElse(null), keySeparator, resultTypeWithArg, resultType, resultTypeArg, + resultNameSeparator, outputPrefix); } public String requiredResultTypeArg() { @@ -141,7 +144,7 @@ public void write(Properties props, Config config) throws IOException { Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); - System.err.format("writing to %s: %s\n", outputFile, jsonResult); + System.err.format("writing to %s: %s%n", outputFile, jsonResult); writer.write(jsonResult); writer.write('\n'); writer.flush(); @@ -167,19 +170,21 @@ public static ResultWriter of(String inputName) { public abstract void write(Properties props, Config config) throws IOException; private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { - String[] selectedKeys = config.selectedKeys(); + List selectedKeys = config.selectedKeys(); if (selectedKeys == null) { throw new IllegalArgumentException("invalid use of resultType " + config.resultType() + " (missing keys)"); } - String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator(), null); - if (resultNames.length != selectedKeys.length && resultNames.length != 1) { + @SuppressWarnings("java:S3655") // Sonar rule: "Optional value should only be accessed after calling isPresent()". + // requiredResultTypeArg() is not empty, so splitArray returns non-empty + String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator()).get(); + if (resultNames.length != selectedKeys.size() && resultNames.length != 1) { throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length - + " arguments, but " + selectedKeys.length + " keys are selected"); + + " arguments, but " + selectedKeys.size() + " keys are selected"); } try (GitHubVariableWriter writer = gitHubOutputFile.open()) { - for (int i = 0; i < selectedKeys.length; i++) { + for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; - String value = props.getProperty(selectedKeys[i]); + String value = props.getProperty(selectedKeys.get(i)); writer.write(name, value); } } @@ -215,7 +220,7 @@ public GitHubVariableWriter(String description, String fileName) throws IOExcept } public void write(String key, String value) throws IOException { - System.err.format("%s %s\t:= \"%s\"\n", description, key, value); + System.err.format("%s %s\t:= \"%s\"%n", description, key, value); // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { @@ -223,7 +228,6 @@ public void write(String key, String value) throws IOException { this.writer.write('='); this.writer.write(value); this.writer.write('\n'); - return; } else { writeMultiLine(key, value); } @@ -235,13 +239,7 @@ public void write(String key, String value) throws IOException { * format. */ private void writeMultiLine(String key, String value) throws IOException { - // determine separator line which does not occur in value: - Set valueLines = Set.of(value.split("(?s)\n")); - String separatorPart = "----"; - String separator = separatorPart; - while (valueLines.contains(separator)) { - separator = separator + separatorPart; - } + String separator = computeSeparator(value); this.writer.write(key); this.writer.write("<<"); @@ -253,6 +251,22 @@ private void writeMultiLine(String key, String value) throws IOException { this.writer.write('\n'); } + /** + * Computes a separator line which does not occur in value. + */ + @SuppressWarnings("java:S1643") // Sonar rule: "Strings should not be concatenated using '+' in a loop". + // False positive: We would need that StringBuilder's toString for each iteration for the contains check anyway. + private static String computeSeparator(String value) { + Set valueLines = Set.of(value.split("(?s)\n")); + String separatorPart = "----"; + @SuppressWarnings("java:S1643") + String separator = separatorPart; + while (valueLines.contains(separator)) { + separator = separator + separatorPart; + } + return separator; + } + @Override public void close() throws IOException { this.writer.close(); @@ -271,8 +285,8 @@ public static String getRequiredEnv(String varName) { return value; } - public static String[] splitArray(String arrayStr, String separator, String[] defaultResults) { - return arrayStr.isEmpty() ? defaultResults : arrayStr.split(Pattern.quote(separator), -1); + public static Optional splitArray(String arrayStr, String separator) { + return arrayStr.isEmpty() ? Optional.empty() : Optional.of(arrayStr.split(Pattern.quote(separator), -1)); } public static Writer openFile(String name, OpenOption... options) throws IOException { @@ -287,8 +301,8 @@ public static Properties readProperties(String file) throws IOException { return allProps; } - public static Properties selectProperties(Properties allProps, String[] selectedKeys, String file) { - Set selectedKeysSet = new LinkedHashSet<>(List.of(selectedKeys)); + public static Properties selectProperties(Properties allProps, List selectedKeys, String file) { + Set selectedKeysSet = new LinkedHashSet<>(selectedKeys); Properties props = new Properties(); for (Map.Entry entry : stringEntries(allProps)) { if (selectedKeysSet.contains(entry.getKey())) { From 8b150ab65e064a9b7b97707457310cc67e7fe2e5 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 14:36:09 +0200 Subject: [PATCH 058/190] test truncating (resultType "json-file") explicitely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Was already tested, because "query multiple…" & "query all…" used the same file, and all was first. - Because we need arrange steps now, rename steps with clearer prefixes. --- .github/workflows/test.yml | 138 +++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 51 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbba533..915edbb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,6 @@ defaults: shell: bash env: ENV_VAR_PREFIX: PROP_TEST__ - JSON_OUTPUT_FILE: tmp/out.json SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' SELECTED_SINGLE_NAME: 'my-prop' @@ -72,7 +71,8 @@ env: # The following helper functions to get a value for a given property key from the result have a uniform # API (property key as sole argument) so that the actual test code can be identical (and thereby reusable # with YAML anchors) for different result types. This leads to some assumptions which might seem - # a little odd, like the JSON result file must be called $JSON_OUTPUT_FILE. + # a little odd, like variable JSON_OUTPUT_FILE must contain the name of the JSON result instead of passing + # that name as an argument. # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { @@ -133,20 +133,20 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ############################################################ - - name: 'test (should fail): invalid input: resultType' + - name: '[act] test (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: rt1 - - name: 'validate "test (should fail): invalid input: resultType"' + - name: '[assert] test (should fail): invalid input: resultType' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-input-resultType.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "output-named" without names' + - name: '[act] test (should fail): resultType "output-named" without names' id: error-resultType-output-without-names continue-on-error: true uses: ./ @@ -154,13 +154,13 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:' - - name: 'validate "test (should fail): resultType "output-named" without names"' + - name: '[assert] test (should fail): resultType "output-named" without names' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-output-without-names.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "env-named" without names' + - name: '[act] test (should fail): resultType "env-named" without names' id: error-resultType-env-without-names continue-on-error: true uses: ./ @@ -168,33 +168,33 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:' - - name: 'validate "test (should fail): resultType "env-named" without names"' + - name: '[assert] test (should fail): resultType "env-named" without names' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-env-without-names.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "output-named" without keys' + - name: '[act] test (should fail): resultType "output-named" without keys' id: error-resultType-output-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'output-named:a b' - - name: 'validate "test (should fail): resultType "output-named" without keys"' + - name: '[assert] test (should fail): resultType "output-named" without keys' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-output-without-keys.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "env-named" without keys' + - name: '[act] test (should fail): resultType "env-named" without keys' id: error-resultType-env-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'env-named:a b' - - name: 'validate "test (should fail): resultType "env-named" without keys"' + - name: '[assert] test (should fail): resultType "env-named" without keys' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-env-without-keys.outcome}}' failure @@ -203,13 +203,13 @@ jobs: # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. - - name: 'test: query multiple properties as action outputs (simple)' + - name: '[act] test: query multiple properties as action outputs (simple)' id: multi-output-simple uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}} dash-in-Key' - - name: 'validate "test: query multiple properties as action outputs (simple)"' + - name: '[assert] test: query multiple properties as action outputs (simple)' env: RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} @@ -225,12 +225,12 @@ jobs: assertEquals "$RESULT_UNI" 'a«,»c' ############################################################ - - name: 'test: query all properties as action outputs' + - name: '[act] test: query all properties as action outputs' id: all-output uses: ./ with: file: test/resources/test.properties - - name: 'validate "test: query all properties as action outputs"' + - name: '[assert] test: query all properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} RESULT_GETTER: encodeKeyAndGetEnvJsonValue @@ -265,13 +265,13 @@ jobs: getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' ############################################################ - - name: 'test: query multiple properties as action outputs' + - name: '[act] test: query multiple properties as action outputs' id: multi-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - - name: 'validate "test: query multiple properties as action outputs"' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} RESULT_GETTER: encodeKeyAndGetEnvJsonValue @@ -307,14 +307,14 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - - name: 'test: query multiple properties as action outputs of given names' + - name: '[act] test: query multiple properties as action outputs of given names' id: multi-output-named uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_NAMES}}' - - name: 'validate "test: query multiple properties as action outputs of given names"' + - name: '[assert] test: query multiple properties as action outputs of given names' env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -337,14 +337,14 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - - name: 'test: query multiple properties as action outputs of given name' + - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' - - name: 'validate "test: query multiple properties as action outputs of given name"' + - name: '[assert] test: query multiple properties as action outputs of given name' env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -364,13 +364,13 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - - name: 'test: query single property as action outputs' + - name: '[act] test: query single property as action outputs' id: single-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' - - name: 'validate "test: query multiple properties as action outputs"' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} RESULT_GETTER: encodeKeyAndGetEnvJsonValue @@ -406,55 +406,91 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - - name: 'test: query all properties as JSON action output' + - name: '[act] test: query all properties as JSON action output' id: all-json uses: ./ with: file: test/resources/test.properties resultType: json - - name: 'validate "test: query all properties as JSON action output"' + - name: '[assert] test: query all properties as JSON action output' env: RESULT_JSON: ${{steps.all-json.outputs.json}} RESULT_GETTER: getEnvJsonValue run: *validateAllImpl ############################################################ - - name: 'test: query multiple properties as JSON action output' + - name: '[act] test: query multiple properties as JSON action output' id: multiple-json uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: json - - name: 'validate "test: query multiple properties as JSON action output"' + - name: '[assert] test: query multiple properties as JSON action output' env: RESULT_JSON: ${{steps.multiple-json.outputs.json}} RESULT_GETTER: getEnvJsonValue run: *validateMultiImpl ############################################################ - - name: 'test: query all properties as JSON file' + - name: '[arrange] test: query multiple properties as JSON file (creating file)' + id: multiple-json-file-create-arrange + run: |- + dir=$(mktemp --directory --dry-run --tmpdir test_json-file_multi_create_XXXXXX) + printf 'file=%s/out/result.json\n' "$dir" | tee -- "$GITHUB_OUTPUT" + # ⇒ action must create two parent dirs: $dir, $dir/out + - name: '[act] test: query multiple properties as JSON file (creating file)' uses: ./ with: file: test/resources/test.properties - resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' - - name: 'validate "test: query all properties as JSON file"' + keys: '${{env.SELECTED_KEYS}}' + resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' + - name: '[assert] test: query multiple properties as JSON file (creating file)' env: + JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} RESULT_GETTER: getFileJsonValue - run: *validateAllImpl + run: *validateMultiImpl ############################################################ - - name: 'test: query multiple properties as JSON file' + - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' + id: multiple-json-file-overwrite-arrange + run: |- + file=$(mktemp --tmpdir test_json-file_multi_overwrite_XXXXXX.json) + # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be + # non-parseable remains and parsing in function getFileJsonValue will fail. + for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" + ls -lAF -- "$file" + printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" + - name: '[act] test: query multiple properties as JSON file (overwriting file)' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' - - name: 'validate "test: query multiple properties as JSON file"' + resultType: 'json-file:${{steps.multiple-json-file-overwrite-arrange.outputs.file}}' + - name: '[assert] test: query multiple properties as JSON file (overwriting file)' env: + JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} RESULT_GETTER: getFileJsonValue run: *validateMultiImpl + ############################################################ + - name: '[arrange] test: query all properties as JSON file' + id: all-json-file-arrange + # Distinction between creating and overwriting need not be tested again. + # This test just uses an existing empty file. + run: |- + printf 'file=%s\n' "$(mktemp --tmpdir test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" + - name: '[act] test: query all properties as JSON file' + uses: ./ + with: + file: test/resources/test.properties + resultType: 'json-file:${{steps.all-json-file-arrange.outputs.file}}' + - name: '[assert] test: query all properties as JSON file' + env: + JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} + RESULT_GETTER: getFileJsonValue + run: *validateAllImpl + # Each test which sets environment variables is a separate job to be independant of other tests. ############################################################ @@ -462,12 +498,12 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query all properties as environment variables' + - name: '[act] test: query all properties as environment variables' uses: ./ with: file: test/resources/test.properties resultType: env - - name: 'validate "test: query all properties as environment variables"' + - name: '[assert] test: query all properties as environment variables' env: RESULT_GETTER: getEnv run: *validateAllImpl @@ -477,13 +513,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables' + - name: '[act] test: query multiple properties as environment variables' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: env - - name: 'validate "test: query multiple properties as environment variables"' + - name: '[assert] test: query multiple properties as environment variables' env: RESULT_GETTER: getEnv run: *validateMultiImpl @@ -493,13 +529,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables of given names' + - name: '[act] test: query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_NAMES}}' - - name: 'validate "test: query multiple properties as environment variables of given names"' + - name: '[assert] test: query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamedImpl @@ -509,13 +545,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables of given names' + - name: '[act] test: query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_SINGLE_NAME}}' - - name: 'validate "test: query multiple properties as environment variables of given names"' + - name: '[assert] test: query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamed1Impl @@ -525,13 +561,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query single property as environment variable' + - name: '[act] test: query single property as environment variable' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: env - - name: 'validate "test: query single property as environment variable"' + - name: '[assert] test: query single property as environment variable' env: RESULT_GETTER: getEnv run: *validateSingleImpl @@ -541,12 +577,12 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query all properties as environment variables with prefix' + - name: '[act] test: query all properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties resultType: 'env:${{env.ENV_VAR_PREFIX}}' - - name: 'validate "test: query all properties as environment variables"' + - name: '[assert] test: query all properties as environment variables' env: RESULT_GETTER: getEnvWithPrefix run: *validateAllImpl @@ -556,13 +592,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables with prefix' + - name: '[act] test: query multiple properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env:${{env.ENV_VAR_PREFIX}}' - - name: 'validate "test: query multiple properties as environment variables with prefix"' + - name: '[assert] test: query multiple properties as environment variables with prefix' env: RESULT_GETTER: getEnvWithPrefix run: *validateMultiImpl @@ -572,13 +608,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query single property as environment variable with prefix' + - name: '[act] test: query single property as environment variable with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: 'env:${{env.ENV_VAR_PREFIX}}' - - name: 'validate "test: query single property as environment variable with prefix"' + - name: '[assert] test: query single property as environment variable with prefix' env: RESULT_GETTER: getEnvWithPrefix run: *validateSingleImpl From e6641d79ff617f178d7cbfbc46b43e74b66b0f42 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 14:36:35 +0200 Subject: [PATCH 059/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Action.java b/Action.java index dbdf27d..ec0a2e0 100644 --- a/Action.java +++ b/Action.java @@ -142,7 +142,7 @@ public void write(Properties props, Config config) throws IOException { public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); - try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE/*TODO: , StandardOpenOption.TRUNCATE_EXISTING*/)) { String jsonResult = Util.toJson(props); System.err.format("writing to %s: %s%n", outputFile, jsonResult); writer.write(jsonResult); From 9df3c5a0a163a2afd86cf6c20940db03958b5643 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 14:45:13 +0200 Subject: [PATCH 060/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 915edbb..f1d1594 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -436,9 +436,9 @@ jobs: - name: '[arrange] test: query multiple properties as JSON file (creating file)' id: multiple-json-file-create-arrange run: |- - dir=$(mktemp --directory --dry-run --tmpdir test_json-file_multi_create_XXXXXX) + dir=$(mktemp --directory --dry-run --tmpdir=./tmp test_json-file_multi_create_XXXXXX) printf 'file=%s/out/result.json\n' "$dir" | tee -- "$GITHUB_OUTPUT" - # ⇒ action must create two parent dirs: $dir, $dir/out + # ⇒ action must create at least two parent dirs: $dir, $dir/out (probably one more: ./tmp) - name: '[act] test: query multiple properties as JSON file (creating file)' uses: ./ with: @@ -455,7 +455,7 @@ jobs: - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- - file=$(mktemp --tmpdir test_json-file_multi_overwrite_XXXXXX.json) + file=$(mktemp --tmpdir=./tmp test_json-file_multi_overwrite_XXXXXX.json) # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be # non-parseable remains and parsing in function getFileJsonValue will fail. for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" @@ -479,7 +479,7 @@ jobs: # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. run: |- - printf 'file=%s\n' "$(mktemp --tmpdir test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" + printf 'file=%s\n' "$(mktemp --tmpdir=./tmp test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" - name: '[act] test: query all properties as JSON file' uses: ./ with: From da3a88395e7188b7ddfaf3be5916ba533055006a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 14:49:05 +0200 Subject: [PATCH 061/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1d1594..41d8d9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -455,6 +455,7 @@ jobs: - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- + mkdir -p tmp file=$(mktemp --tmpdir=./tmp test_json-file_multi_overwrite_XXXXXX.json) # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be # non-parseable remains and parsing in function getFileJsonValue will fail. @@ -479,6 +480,7 @@ jobs: # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. run: |- + mkdir -p tmp printf 'file=%s\n' "$(mktemp --tmpdir=./tmp test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" - name: '[act] test: query all properties as JSON file' uses: ./ From 5dee38862cfe2773fc5f1b7f8a81cad818ad89d6 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 14:53:19 +0200 Subject: [PATCH 062/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41d8d9a..3c974f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -455,8 +455,9 @@ jobs: - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- - mkdir -p tmp - file=$(mktemp --tmpdir=./tmp test_json-file_multi_overwrite_XXXXXX.json) + mkdir --parents tmp + chmod --changes u+rwx tmp + file=$(mktemp --tmpdir=tmp test_json-file_multi_overwrite_XXXXXX.json) # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be # non-parseable remains and parsing in function getFileJsonValue will fail. for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" @@ -480,8 +481,9 @@ jobs: # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. run: |- - mkdir -p tmp - printf 'file=%s\n' "$(mktemp --tmpdir=./tmp test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" + mkdir --parents tmp + chmod --changes u+rwx tmp + printf 'file=%s\n' "$(mktemp --tmpdir=tmp test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" - name: '[act] test: query all properties as JSON file' uses: ./ with: From db54c2bd83389d25f24a17660dc35e0bd59200d2 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 14:58:53 +0200 Subject: [PATCH 063/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c974f7..7fb39bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -432,24 +432,24 @@ jobs: RESULT_GETTER: getEnvJsonValue run: *validateMultiImpl - ############################################################ - - name: '[arrange] test: query multiple properties as JSON file (creating file)' - id: multiple-json-file-create-arrange - run: |- - dir=$(mktemp --directory --dry-run --tmpdir=./tmp test_json-file_multi_create_XXXXXX) - printf 'file=%s/out/result.json\n' "$dir" | tee -- "$GITHUB_OUTPUT" - # ⇒ action must create at least two parent dirs: $dir, $dir/out (probably one more: ./tmp) - - name: '[act] test: query multiple properties as JSON file (creating file)' - uses: ./ - with: - file: test/resources/test.properties - keys: '${{env.SELECTED_KEYS}}' - resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' - - name: '[assert] test: query multiple properties as JSON file (creating file)' - env: - JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} - RESULT_GETTER: getFileJsonValue - run: *validateMultiImpl + ############################################################# + #- name: '[arrange] test: query multiple properties as JSON file (creating file)' + # id: multiple-json-file-create-arrange + # run: |- + # dir=$(mktemp --directory --dry-run --tmpdir=./tmp test_json-file_multi_create_XXXXXX) + # printf 'file=%s/out/result.json\n' "$dir" | tee -- "$GITHUB_OUTPUT" + # # ⇒ action must create at least two parent dirs: $dir, $dir/out (probably one more: ./tmp) + #- name: '[act] test: query multiple properties as JSON file (creating file)' + # uses: ./ + # with: + # file: test/resources/test.properties + # keys: '${{env.SELECTED_KEYS}}' + # resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' + #- name: '[assert] test: query multiple properties as JSON file (creating file)' + # env: + # JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} + # RESULT_GETTER: getFileJsonValue + # run: *validateMultiImpl ############################################################ - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' @@ -457,7 +457,7 @@ jobs: run: |- mkdir --parents tmp chmod --changes u+rwx tmp - file=$(mktemp --tmpdir=tmp test_json-file_multi_overwrite_XXXXXX.json) + file=$(mktemp --tmpdir=./tmp test_json-file_multi_overwrite_XXXXXX.json) # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be # non-parseable remains and parsing in function getFileJsonValue will fail. for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" From c369e9f06ad68322dce19d76d657c5e5d0e53489 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 15:01:28 +0200 Subject: [PATCH 064/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Action.java b/Action.java index ec0a2e0..dbdf27d 100644 --- a/Action.java +++ b/Action.java @@ -142,7 +142,7 @@ public void write(Properties props, Config config) throws IOException { public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); - try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE/*TODO: , StandardOpenOption.TRUNCATE_EXISTING*/)) { + try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); System.err.format("writing to %s: %s%n", outputFile, jsonResult); writer.write(jsonResult); From 50fa7e12d6c7755d3535a247b52f1471c8a62bd5 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 15:10:36 +0200 Subject: [PATCH 065/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 45 ++++++++++++++++++-------------------- Action.java | 2 +- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fb39bf..114aeed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -432,32 +432,31 @@ jobs: RESULT_GETTER: getEnvJsonValue run: *validateMultiImpl - ############################################################# - #- name: '[arrange] test: query multiple properties as JSON file (creating file)' - # id: multiple-json-file-create-arrange - # run: |- - # dir=$(mktemp --directory --dry-run --tmpdir=./tmp test_json-file_multi_create_XXXXXX) - # printf 'file=%s/out/result.json\n' "$dir" | tee -- "$GITHUB_OUTPUT" - # # ⇒ action must create at least two parent dirs: $dir, $dir/out (probably one more: ./tmp) - #- name: '[act] test: query multiple properties as JSON file (creating file)' - # uses: ./ - # with: - # file: test/resources/test.properties - # keys: '${{env.SELECTED_KEYS}}' - # resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' - #- name: '[assert] test: query multiple properties as JSON file (creating file)' - # env: - # JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} - # RESULT_GETTER: getFileJsonValue - # run: *validateMultiImpl + ############################################################ + - name: '[arrange] test: query multiple properties as JSON file (creating file)' + id: multiple-json-file-create-arrange + run: |- + parentDir=$(mktemp --directory --dry-run --tmpdir=. test_json-file_multi_create_XXXXXX) + file=$parentDir/out/result.json + printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" + # ⇒ action must create at least two parent dirs + - name: '[act] test: query multiple properties as JSON file (creating file)' + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' + - name: '[assert] test: query multiple properties as JSON file (creating file)' + env: + JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} + RESULT_GETTER: getFileJsonValue + run: *validateMultiImpl ############################################################ - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- - mkdir --parents tmp - chmod --changes u+rwx tmp - file=$(mktemp --tmpdir=./tmp test_json-file_multi_overwrite_XXXXXX.json) + file=$(mktemp --tmpdir=. test_json-file_multi_overwrite_XXXXXX.json) # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be # non-parseable remains and parsing in function getFileJsonValue will fail. for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" @@ -481,9 +480,7 @@ jobs: # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. run: |- - mkdir --parents tmp - chmod --changes u+rwx tmp - printf 'file=%s\n' "$(mktemp --tmpdir=tmp test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" + printf 'file=%s\n' "$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" - name: '[act] test: query all properties as JSON file' uses: ./ with: diff --git a/Action.java b/Action.java index dbdf27d..ec0a2e0 100644 --- a/Action.java +++ b/Action.java @@ -142,7 +142,7 @@ public void write(Properties props, Config config) throws IOException { public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); - try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE/*TODO: , StandardOpenOption.TRUNCATE_EXISTING*/)) { String jsonResult = Util.toJson(props); System.err.format("writing to %s: %s%n", outputFile, jsonResult); writer.write(jsonResult); From 26f960e8c2e56a84070664c66d2000d42d8e738c Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 15:17:29 +0200 Subject: [PATCH 066/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Action.java b/Action.java index ec0a2e0..dbdf27d 100644 --- a/Action.java +++ b/Action.java @@ -142,7 +142,7 @@ public void write(Properties props, Config config) throws IOException { public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); - try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE/*TODO: , StandardOpenOption.TRUNCATE_EXISTING*/)) { + try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); System.err.format("writing to %s: %s%n", outputFile, jsonResult); writer.write(jsonResult); From 29acfed48b9482c6931f0d196d182cef0bc7b1ca Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 15:37:00 +0200 Subject: [PATCH 067/190] test truncating (resultType "json-file") explicitely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Was already tested, because "query multiple…" & "query all…" used the same file, and all was first. - Because we need arrange steps now, rename steps with clearer prefixes. --- .github/workflows/test.yml | 139 +++++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbba533..114aeed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,6 @@ defaults: shell: bash env: ENV_VAR_PREFIX: PROP_TEST__ - JSON_OUTPUT_FILE: tmp/out.json SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' SELECTED_SINGLE_NAME: 'my-prop' @@ -72,7 +71,8 @@ env: # The following helper functions to get a value for a given property key from the result have a uniform # API (property key as sole argument) so that the actual test code can be identical (and thereby reusable # with YAML anchors) for different result types. This leads to some assumptions which might seem - # a little odd, like the JSON result file must be called $JSON_OUTPUT_FILE. + # a little odd, like variable JSON_OUTPUT_FILE must contain the name of the JSON result instead of passing + # that name as an argument. # Expects JSON as stdin and extracts the value for key $1. getJsonValue() { @@ -133,20 +133,20 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ############################################################ - - name: 'test (should fail): invalid input: resultType' + - name: '[act] test (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: rt1 - - name: 'validate "test (should fail): invalid input: resultType"' + - name: '[assert] test (should fail): invalid input: resultType' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-input-resultType.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "output-named" without names' + - name: '[act] test (should fail): resultType "output-named" without names' id: error-resultType-output-without-names continue-on-error: true uses: ./ @@ -154,13 +154,13 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:' - - name: 'validate "test (should fail): resultType "output-named" without names"' + - name: '[assert] test (should fail): resultType "output-named" without names' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-output-without-names.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "env-named" without names' + - name: '[act] test (should fail): resultType "env-named" without names' id: error-resultType-env-without-names continue-on-error: true uses: ./ @@ -168,33 +168,33 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:' - - name: 'validate "test (should fail): resultType "env-named" without names"' + - name: '[assert] test (should fail): resultType "env-named" without names' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-env-without-names.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "output-named" without keys' + - name: '[act] test (should fail): resultType "output-named" without keys' id: error-resultType-output-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'output-named:a b' - - name: 'validate "test (should fail): resultType "output-named" without keys"' + - name: '[assert] test (should fail): resultType "output-named" without keys' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-output-without-keys.outcome}}' failure ############################################################ - - name: 'test (should fail): resultType "env-named" without keys' + - name: '[act] test (should fail): resultType "env-named" without keys' id: error-resultType-env-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'env-named:a b' - - name: 'validate "test (should fail): resultType "env-named" without keys"' + - name: '[assert] test (should fail): resultType "env-named" without keys' run: |- eval "$SHELL_SETUP" assertEquals '${{steps.error-resultType-env-without-keys.outcome}}' failure @@ -203,13 +203,13 @@ jobs: # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. - - name: 'test: query multiple properties as action outputs (simple)' + - name: '[act] test: query multiple properties as action outputs (simple)' id: multi-output-simple uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}} dash-in-Key' - - name: 'validate "test: query multiple properties as action outputs (simple)"' + - name: '[assert] test: query multiple properties as action outputs (simple)' env: RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} @@ -225,12 +225,12 @@ jobs: assertEquals "$RESULT_UNI" 'a«,»c' ############################################################ - - name: 'test: query all properties as action outputs' + - name: '[act] test: query all properties as action outputs' id: all-output uses: ./ with: file: test/resources/test.properties - - name: 'validate "test: query all properties as action outputs"' + - name: '[assert] test: query all properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} RESULT_GETTER: encodeKeyAndGetEnvJsonValue @@ -265,13 +265,13 @@ jobs: getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' ############################################################ - - name: 'test: query multiple properties as action outputs' + - name: '[act] test: query multiple properties as action outputs' id: multi-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - - name: 'validate "test: query multiple properties as action outputs"' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} RESULT_GETTER: encodeKeyAndGetEnvJsonValue @@ -307,14 +307,14 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - - name: 'test: query multiple properties as action outputs of given names' + - name: '[act] test: query multiple properties as action outputs of given names' id: multi-output-named uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_NAMES}}' - - name: 'validate "test: query multiple properties as action outputs of given names"' + - name: '[assert] test: query multiple properties as action outputs of given names' env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -337,14 +337,14 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - - name: 'test: query multiple properties as action outputs of given name' + - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' - - name: 'validate "test: query multiple properties as action outputs of given name"' + - name: '[assert] test: query multiple properties as action outputs of given name' env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -364,13 +364,13 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - - name: 'test: query single property as action outputs' + - name: '[act] test: query single property as action outputs' id: single-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' - - name: 'validate "test: query multiple properties as action outputs"' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} RESULT_GETTER: encodeKeyAndGetEnvJsonValue @@ -406,55 +406,92 @@ jobs: getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - - name: 'test: query all properties as JSON action output' + - name: '[act] test: query all properties as JSON action output' id: all-json uses: ./ with: file: test/resources/test.properties resultType: json - - name: 'validate "test: query all properties as JSON action output"' + - name: '[assert] test: query all properties as JSON action output' env: RESULT_JSON: ${{steps.all-json.outputs.json}} RESULT_GETTER: getEnvJsonValue run: *validateAllImpl ############################################################ - - name: 'test: query multiple properties as JSON action output' + - name: '[act] test: query multiple properties as JSON action output' id: multiple-json uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: json - - name: 'validate "test: query multiple properties as JSON action output"' + - name: '[assert] test: query multiple properties as JSON action output' env: RESULT_JSON: ${{steps.multiple-json.outputs.json}} RESULT_GETTER: getEnvJsonValue run: *validateMultiImpl ############################################################ - - name: 'test: query all properties as JSON file' + - name: '[arrange] test: query multiple properties as JSON file (creating file)' + id: multiple-json-file-create-arrange + run: |- + parentDir=$(mktemp --directory --dry-run --tmpdir=. test_json-file_multi_create_XXXXXX) + file=$parentDir/out/result.json + printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" + # ⇒ action must create at least two parent dirs + - name: '[act] test: query multiple properties as JSON file (creating file)' uses: ./ with: file: test/resources/test.properties - resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' - - name: 'validate "test: query all properties as JSON file"' + keys: '${{env.SELECTED_KEYS}}' + resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' + - name: '[assert] test: query multiple properties as JSON file (creating file)' env: + JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} RESULT_GETTER: getFileJsonValue - run: *validateAllImpl + run: *validateMultiImpl ############################################################ - - name: 'test: query multiple properties as JSON file' + - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' + id: multiple-json-file-overwrite-arrange + run: |- + file=$(mktemp --tmpdir=. test_json-file_multi_overwrite_XXXXXX.json) + # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be + # non-parseable remains and parsing in function getFileJsonValue will fail. + for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" + ls -lAF -- "$file" + printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" + - name: '[act] test: query multiple properties as JSON file (overwriting file)' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'json-file:${{env.JSON_OUTPUT_FILE}}' - - name: 'validate "test: query multiple properties as JSON file"' + resultType: 'json-file:${{steps.multiple-json-file-overwrite-arrange.outputs.file}}' + - name: '[assert] test: query multiple properties as JSON file (overwriting file)' env: + JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} RESULT_GETTER: getFileJsonValue run: *validateMultiImpl + ############################################################ + - name: '[arrange] test: query all properties as JSON file' + id: all-json-file-arrange + # Distinction between creating and overwriting need not be tested again. + # This test just uses an existing empty file. + run: |- + printf 'file=%s\n' "$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" + - name: '[act] test: query all properties as JSON file' + uses: ./ + with: + file: test/resources/test.properties + resultType: 'json-file:${{steps.all-json-file-arrange.outputs.file}}' + - name: '[assert] test: query all properties as JSON file' + env: + JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} + RESULT_GETTER: getFileJsonValue + run: *validateAllImpl + # Each test which sets environment variables is a separate job to be independant of other tests. ############################################################ @@ -462,12 +499,12 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query all properties as environment variables' + - name: '[act] test: query all properties as environment variables' uses: ./ with: file: test/resources/test.properties resultType: env - - name: 'validate "test: query all properties as environment variables"' + - name: '[assert] test: query all properties as environment variables' env: RESULT_GETTER: getEnv run: *validateAllImpl @@ -477,13 +514,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables' + - name: '[act] test: query multiple properties as environment variables' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: env - - name: 'validate "test: query multiple properties as environment variables"' + - name: '[assert] test: query multiple properties as environment variables' env: RESULT_GETTER: getEnv run: *validateMultiImpl @@ -493,13 +530,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables of given names' + - name: '[act] test: query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_NAMES}}' - - name: 'validate "test: query multiple properties as environment variables of given names"' + - name: '[assert] test: query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamedImpl @@ -509,13 +546,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables of given names' + - name: '[act] test: query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_SINGLE_NAME}}' - - name: 'validate "test: query multiple properties as environment variables of given names"' + - name: '[assert] test: query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamed1Impl @@ -525,13 +562,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query single property as environment variable' + - name: '[act] test: query single property as environment variable' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: env - - name: 'validate "test: query single property as environment variable"' + - name: '[assert] test: query single property as environment variable' env: RESULT_GETTER: getEnv run: *validateSingleImpl @@ -541,12 +578,12 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query all properties as environment variables with prefix' + - name: '[act] test: query all properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties resultType: 'env:${{env.ENV_VAR_PREFIX}}' - - name: 'validate "test: query all properties as environment variables"' + - name: '[assert] test: query all properties as environment variables' env: RESULT_GETTER: getEnvWithPrefix run: *validateAllImpl @@ -556,13 +593,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query multiple properties as environment variables with prefix' + - name: '[act] test: query multiple properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env:${{env.ENV_VAR_PREFIX}}' - - name: 'validate "test: query multiple properties as environment variables with prefix"' + - name: '[assert] test: query multiple properties as environment variables with prefix' env: RESULT_GETTER: getEnvWithPrefix run: *validateMultiImpl @@ -572,13 +609,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: 'test: query single property as environment variable with prefix' + - name: '[act] test: query single property as environment variable with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: 'env:${{env.ENV_VAR_PREFIX}}' - - name: 'validate "test: query single property as environment variable with prefix"' + - name: '[assert] test: query single property as environment variable with prefix' env: RESULT_GETTER: getEnvWithPrefix run: *validateSingleImpl From 2aea4587b7e546273879cf8d6766ad6994179345 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 16:48:50 +0200 Subject: [PATCH 068/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 134 +++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 65 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 114aeed..a8b7fb2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,26 +38,40 @@ env: fi } - # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. - getAndAssertEquals() { + # Evaluates the command given as arguments except the last argument and asserts that the command's output + # equals the last argument. Only the first argument is evaled, the rest is treated as literal strings. + # + # That odd behavior is usefull for how the steps for similar test cases are written: They require + # different ways to get a result; for example, one needs `getJsonValue "$RESULT"` and another one + # `getJsonValue "$(<"$RESULT_FILE")`; this difference is moved to environment variables in order to keep + # the script identical, so it can be reused with a YAML anchor. + # Example usage: evalAndAssertEquals "$RESULT_GETTER" 'key with spaces' 'expected value' + # with RESULT_GETTER set to 'getJsonValue "$RESULT"'. + # + # Also, unlike directly using + # assertEquals "$(eval "value=$RESULT_GETTER 'key with spaces')" 'expected value' + # this function does not swallow a non-zero exit status in $RESULT_GETTER. + evalAndAssertEquals() { local getterFunction=$1; shift - local key=$1; shift - local expectedValue=$1; shift - local value st - # Invocation of $getterFunction must not ignore its return status. This is why tests use this function in the form - # getAndAssertEquals $RESULT_GETTER "$key" "$expectedValue" - # instead of - # assertEquals "$($RESULT_GETTER "$key")" "$expectedValue" - value="$(set -o pipefail; "$getterFunction" "$key" | replaceLastLF)" + local args=("$@") + local expectedValue="${args[@]:${#args[@]}-1}" + args=("${args[@]:0:${#args[@]}-1}") + + local quotedArgs value + quotedArgs=$(printf ' %q' "${args[@]}") + eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" assertEquals "$value" "$expectedValue" } # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. + # The command is handled like in →evalAndAssertEquals (except that there is no expected value as last argument which + # is not passed to the command). getAndAssertUndefined() { local getterFunction=$1; shift - local value st - if value="$("$getterFunction" "$@")"; then + local quotedArgs value st + quotedArgs=$(printf ' %q' "$@") + if eval "value=\$(${getterFunction}${quotedArgs}"; then printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 return 1 elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then @@ -68,30 +82,21 @@ env: fi } - # The following helper functions to get a value for a given property key from the result have a uniform - # API (property key as sole argument) so that the actual test code can be identical (and thereby reusable - # with YAML anchors) for different result types. This leads to some assumptions which might seem - # a little odd, like variable JSON_OUTPUT_FILE must contain the name of the JSON result instead of passing - # that name as an argument. - - # Expects JSON as stdin and extracts the value for key $1. + # Extracts the value for key $2 from JSON $1. getJsonValue() { + local json=$1; shift local key=$1; shift - debugPrintf 'getJsonValue("%s")\n' "$key" - jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' - } - - # Expects JSON in variable RESULT_JSON and extracts the value for key $1. - getEnvJsonValue() { - getJsonValue "$@" <<<"$RESULT_JSON" + debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" + jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" } - # Expects JSON in variable RESULT_JSON and extracts the value for key . - encodeKeyAndGetEnvJsonValue() { + # Extracts the value for key from JSON $2. + encodeKeyAndGetJsonValue() { + local json=$1; shift local key=$1; shift local encodedKey encodedKey="_$(perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge' <<<"$key")" - getEnvJsonValue "$encodedKey" "$@" + getJsonValue "$json" "$encodedKey" "$@" } # Prints the value of the environment variable set for key $1 (when not using a prefix). @@ -109,15 +114,11 @@ env: fi } - # Prints the value of the environment variable set for key $1 when using a prefix. + # Prints the value of the environment variable set for key $2 when using a prefix $1. getEnvWithPrefix() { + local prefix=$1; shift local key=$1; shift - getEnv "${ENV_VAR_PREFIX}${key}" "$@" - } - - getFileJsonValue() { - debugPrintf 'getFileJsonValue("%s") with file contents "%s"\n' "$*" "$(<"$JSON_OUTPUT_FILE")" - getJsonValue "$@" <"$JSON_OUTPUT_FILE" + getEnv "${prefix}${key}" "$@" } jobs: @@ -233,36 +234,36 @@ jobs: - name: '[assert] test: query all properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' - getAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' - getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' - getAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' - getAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' - getAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' - getAndAssertEquals "$RESULT_GETTER" empty '' - getAndAssertEquals "$RESULT_GETTER" null 'null' - getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\r\n\nD' - getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_1 $'one trailing LF\n' - getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_2 $'two trailing LFs\n\n' - getAndAssertEquals "$RESULT_GETTER" dash_1 '-' - getAndAssertEquals "$RESULT_GETTER" dash_2 '--' - getAndAssertEquals "$RESULT_GETTER" dash_3 '---' - getAndAssertEquals "$RESULT_GETTER" dash_4 '----' - getAndAssertEquals "$RESULT_GETTER" dash_5 '-----' - getAndAssertEquals "$RESULT_GETTER" dash_6 '------' - getAndAssertEquals "$RESULT_GETTER" dash_7 '-------' - getAndAssertEquals "$RESULT_GETTER" dash_8 '--------' - getAndAssertEquals "$RESULT_GETTER" dash_9 '---------' - getAndAssertEquals "$RESULT_GETTER" dash_10 '----------' - getAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' - getAndAssertEquals "$RESULT_GETTER" dash_12 '------------' - getAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' - getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' + evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' + evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' + evalAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' + evalAndAssertEquals "$RESULT_GETTER" empty '' + evalAndAssertEquals "$RESULT_GETTER" null 'null' + evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + evalAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\r\n\nD' + evalAndAssertEquals "$RESULT_GETTER" trailing_linefeed_1 $'one trailing LF\n' + evalAndAssertEquals "$RESULT_GETTER" trailing_linefeed_2 $'two trailing LFs\n\n' + evalAndAssertEquals "$RESULT_GETTER" dash_1 '-' + evalAndAssertEquals "$RESULT_GETTER" dash_2 '--' + evalAndAssertEquals "$RESULT_GETTER" dash_3 '---' + evalAndAssertEquals "$RESULT_GETTER" dash_4 '----' + evalAndAssertEquals "$RESULT_GETTER" dash_5 '-----' + evalAndAssertEquals "$RESULT_GETTER" dash_6 '------' + evalAndAssertEquals "$RESULT_GETTER" dash_7 '-------' + evalAndAssertEquals "$RESULT_GETTER" dash_8 '--------' + evalAndAssertEquals "$RESULT_GETTER" dash_9 '---------' + evalAndAssertEquals "$RESULT_GETTER" dash_10 '----------' + evalAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' + evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' + evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' + evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' ############################################################ - name: '[act] test: query multiple properties as action outputs' @@ -277,6 +278,8 @@ jobs: RESULT_GETTER: encodeKeyAndGetEnvJsonValue run: &validateMultiImpl |- eval "$SHELL_SETUP" + eval "value=${RESULT_GETTER}$(printf ' %q' dash_1)" && assertEquals "$value" '21' + evalAndAssertEquals "$RESULT_GETTER" dash_1 '21' getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' @@ -318,7 +321,7 @@ jobs: env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamedImpl |- eval "$SHELL_SETUP" getAndAssertEquals "$RESULT_GETTER" java-version '21' @@ -450,6 +453,7 @@ jobs: env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} RESULT_GETTER: getFileJsonValue + RESULT_GETTER2: "getFileJsonValue '${{steps.multiple-json-file-create-arrange.outputs.file}}'" run: *validateMultiImpl ############################################################ From 54142ba3b6b83152974b7019587eff0d25c8f246 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 16:58:04 +0200 Subject: [PATCH 069/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8b7fb2..f21c0a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -237,7 +237,11 @@ jobs: RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + #evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + eval "value=\$("$RESULT_GETTER" sourceJavaVersion)" + printf 'value == "%s"\n' "$value" + assertEquals "$value" 21 + echo '............' evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' From cfede371f04a6360062caf797805bfc202e78f2d Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:01:59 +0200 Subject: [PATCH 070/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f21c0a9..f9781ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,8 +13,7 @@ env: NOT_FOUND_STATUS: 54 SHELL_SETUP: |- debugPrintf() { - local pattern=$1; shift - printf "::debug::${pattern}" "$@" >&2 + printf "$@" | sed 's/^/::debug::/g' >&2 } # Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is From 2cefdcf72cb85d25b86c2b09e634a6adb3b27236 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:03:22 +0200 Subject: [PATCH 071/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9781ea..fc9e6b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -236,13 +236,9 @@ jobs: RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- eval "$SHELL_SETUP" - #evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' - eval "value=\$("$RESULT_GETTER" sourceJavaVersion)" - printf 'value == "%s"\n' "$value" - assertEquals "$value" 21 - echo '............' + evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' - evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea `-showversion' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' From e520c753602251bda1881c15822d899c92e10c66 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:16:43 +0200 Subject: [PATCH 072/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 179 ++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 91 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc9e6b0..7fae489 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,6 @@ defaults: run: shell: bash env: - ENV_VAR_PREFIX: PROP_TEST__ SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' SELECTED_SINGLE_NAME: 'my-prop' @@ -66,7 +65,7 @@ env: # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. # The command is handled like in →evalAndAssertEquals (except that there is no expected value as last argument which # is not passed to the command). - getAndAssertUndefined() { + evalAndAssertUndefined() { local getterFunction=$1; shift local quotedArgs value st quotedArgs=$(printf ' %q' "$@") @@ -238,7 +237,7 @@ jobs: eval "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' - evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea `-showversion' + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' @@ -274,39 +273,38 @@ jobs: - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + RESULT_GETTER: 'encodeKeyAndGetEnvJsonValue "$RESULT_JSON"' run: &validateMultiImpl |- eval "$SHELL_SETUP" - eval "value=${RESULT_GETTER}$(printf ' %q' dash_1)" && assertEquals "$value" '21' evalAndAssertEquals "$RESULT_GETTER" dash_1 '21' - getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' - getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' - getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - - getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash-in-Key - getAndAssertUndefined "$RESULT_GETTER" dashInValue - getAndAssertUndefined "$RESULT_GETTER" empty - getAndAssertUndefined "$RESULT_GETTER" null - getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 - getAndAssertUndefined "$RESULT_GETTER" dash_1 - getAndAssertUndefined "$RESULT_GETTER" dash_2 - getAndAssertUndefined "$RESULT_GETTER" dash_3 - getAndAssertUndefined "$RESULT_GETTER" dash_4 - getAndAssertUndefined "$RESULT_GETTER" dash_5 - getAndAssertUndefined "$RESULT_GETTER" dash_6 - getAndAssertUndefined "$RESULT_GETTER" dash_7 - getAndAssertUndefined "$RESULT_GETTER" dash_8 - getAndAssertUndefined "$RESULT_GETTER" dash_9 - getAndAssertUndefined "$RESULT_GETTER" dash_10 - getAndAssertUndefined "$RESULT_GETTER" dash_11 - getAndAssertUndefined "$RESULT_GETTER" dash_12 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + + evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key + evalAndAssertUndefined "$RESULT_GETTER" dashInValue + evalAndAssertUndefined "$RESULT_GETTER" empty + evalAndAssertUndefined "$RESULT_GETTER" null + evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_3 + evalAndAssertUndefined "$RESULT_GETTER" dash_4 + evalAndAssertUndefined "$RESULT_GETTER" dash_5 + evalAndAssertUndefined "$RESULT_GETTER" dash_6 + evalAndAssertUndefined "$RESULT_GETTER" dash_7 + evalAndAssertUndefined "$RESULT_GETTER" dash_8 + evalAndAssertUndefined "$RESULT_GETTER" dash_9 + evalAndAssertUndefined "$RESULT_GETTER" dash_10 + evalAndAssertUndefined "$RESULT_GETTER" dash_11 + evalAndAssertUndefined "$RESULT_GETTER" dash_12 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: '[act] test: query multiple properties as action outputs of given names' @@ -323,20 +321,20 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamedImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" java-version '21' - getAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' - getAndAssertEquals "$RESULT_GETTER" colon 'c' - getAndAssertEquals "$RESULT_GETTER" unicode 'a«,»c' + evalAndAssertEquals "$RESULT_GETTER" java-version '21' + evalAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" colon 'c' + evalAndAssertEquals "$RESULT_GETTER" unicode 'a«,»c' # The outputs with the default names are not set: - getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs - getAndAssertUndefined "$RESULT_GETTER" colon:in:Key - getAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: '[act] test: query multiple properties as action outputs of given name' @@ -353,17 +351,17 @@ jobs: RESULT_GETTER: getEnvJsonValue run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" my-prop 'c' + evalAndAssertEquals "$RESULT_GETTER" my-prop 'c' # The outputs with the default names are not set: - getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs - getAndAssertUndefined "$RESULT_GETTER" colon:in:Key - getAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: '[act] test: query single property as action outputs' @@ -375,37 +373,37 @@ jobs: - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + RESULT_GETTER: 'encodeKeyAndGetEnvJsonValue "$RESULT_JSON"' run: &validateSingleImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - - getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion - getAndAssertUndefined "$RESULT_GETTER" colon:in:Key - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash-in-Key - getAndAssertUndefined "$RESULT_GETTER" dashInValue - getAndAssertUndefined "$RESULT_GETTER" empty - getAndAssertUndefined "$RESULT_GETTER" null - getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 - getAndAssertUndefined "$RESULT_GETTER" unicode_escapes - getAndAssertUndefined "$RESULT_GETTER" dash_1 - getAndAssertUndefined "$RESULT_GETTER" dash_2 - getAndAssertUndefined "$RESULT_GETTER" dash_3 - getAndAssertUndefined "$RESULT_GETTER" dash_4 - getAndAssertUndefined "$RESULT_GETTER" dash_5 - getAndAssertUndefined "$RESULT_GETTER" dash_6 - getAndAssertUndefined "$RESULT_GETTER" dash_7 - getAndAssertUndefined "$RESULT_GETTER" dash_8 - getAndAssertUndefined "$RESULT_GETTER" dash_9 - getAndAssertUndefined "$RESULT_GETTER" dash_10 - getAndAssertUndefined "$RESULT_GETTER" dash_11 - getAndAssertUndefined "$RESULT_GETTER" dash_12 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + + evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key + evalAndAssertUndefined "$RESULT_GETTER" dashInValue + evalAndAssertUndefined "$RESULT_GETTER" empty + evalAndAssertUndefined "$RESULT_GETTER" null + evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_3 + evalAndAssertUndefined "$RESULT_GETTER" dash_4 + evalAndAssertUndefined "$RESULT_GETTER" dash_5 + evalAndAssertUndefined "$RESULT_GETTER" dash_6 + evalAndAssertUndefined "$RESULT_GETTER" dash_7 + evalAndAssertUndefined "$RESULT_GETTER" dash_8 + evalAndAssertUndefined "$RESULT_GETTER" dash_9 + evalAndAssertUndefined "$RESULT_GETTER" dash_10 + evalAndAssertUndefined "$RESULT_GETTER" dash_11 + evalAndAssertUndefined "$RESULT_GETTER" dash_12 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: '[act] test: query all properties as JSON action output' @@ -417,7 +415,7 @@ jobs: - name: '[assert] test: query all properties as JSON action output' env: RESULT_JSON: ${{steps.all-json.outputs.json}} - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl ############################################################ @@ -431,7 +429,7 @@ jobs: - name: '[assert] test: query multiple properties as JSON action output' env: RESULT_JSON: ${{steps.multiple-json.outputs.json}} - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiImpl ############################################################ @@ -451,8 +449,7 @@ jobs: - name: '[assert] test: query multiple properties as JSON file (creating file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} - RESULT_GETTER: getFileJsonValue - RESULT_GETTER2: "getFileJsonValue '${{steps.multiple-json-file-create-arrange.outputs.file}}'" + RESULT_GETTER: 'getJsonValue "$(<$"JSON_OUTPUT_FILE")"' run: *validateMultiImpl ############################################################ @@ -474,7 +471,7 @@ jobs: - name: '[assert] test: query multiple properties as JSON file (overwriting file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} - RESULT_GETTER: getFileJsonValue + RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl ############################################################ @@ -492,7 +489,7 @@ jobs: - name: '[assert] test: query all properties as JSON file' env: JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} - RESULT_GETTER: getFileJsonValue + RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateAllImpl @@ -585,10 +582,10 @@ jobs: uses: ./ with: file: test/resources/test.properties - resultType: 'env:${{env.ENV_VAR_PREFIX}}' + resultType: 'env:ALL_PROPS__' - name: '[assert] test: query all properties as environment variables' env: - RESULT_GETTER: getEnvWithPrefix + RESULT_GETTER: 'getEnvWithPrefix ALL_PROPS__' run: *validateAllImpl ############################################################ @@ -601,10 +598,10 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'env:${{env.ENV_VAR_PREFIX}}' + resultType: 'env:MULTI_PROPS__' - name: '[assert] test: query multiple properties as environment variables with prefix' env: - RESULT_GETTER: getEnvWithPrefix + RESULT_GETTER: 'getEnvWithPrefix MULTI_PROPS__' run: *validateMultiImpl ############################################################ @@ -617,8 +614,8 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' - resultType: 'env:${{env.ENV_VAR_PREFIX}}' + resultType: 'env:PROP__' - name: '[assert] test: query single property as environment variable with prefix' env: - RESULT_GETTER: getEnvWithPrefix + RESULT_GETTER: 'getEnvWithPrefix PROP__' run: *validateSingleImpl From 927c3d459ebd2c2b9a6785fabab4d0326e5c6c72 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:22:10 +0200 Subject: [PATCH 073/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fae489..9377332 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -273,10 +273,9 @@ jobs: - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} - RESULT_GETTER: 'encodeKeyAndGetEnvJsonValue "$RESULT_JSON"' + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateMultiImpl |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" dash_1 '21' evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' @@ -373,7 +372,7 @@ jobs: - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} - RESULT_GETTER: 'encodeKeyAndGetEnvJsonValue "$RESULT_JSON"' + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateSingleImpl |- eval "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' From 288a06b9d40726097125e779fbfc360a570ac770 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:24:34 +0200 Subject: [PATCH 074/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9377332..af808e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ env: local getterFunction=$1; shift local quotedArgs value st quotedArgs=$(printf ' %q' "$@") - if eval "value=\$(${getterFunction}${quotedArgs}"; then + if eval "value=\$(${getterFunction}${quotedArgs})"; then printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 return 1 elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then From 4e043ac6a0ccd5e15799740baa2061917f79aeeb Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:26:21 +0200 Subject: [PATCH 075/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af808e3..58288ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -347,7 +347,7 @@ jobs: env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" my-prop 'c' From fcbb57fd9e0fb4eafc01c7c04b5a9d5fe4dfc0f5 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:28:25 +0200 Subject: [PATCH 076/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58288ee..7581ac7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -448,7 +448,7 @@ jobs: - name: '[assert] test: query multiple properties as JSON file (creating file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} - RESULT_GETTER: 'getJsonValue "$(<$"JSON_OUTPUT_FILE")"' + RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl ############################################################ From 6e12b15df3ef100549b2b76149704c20c2e316fa Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:40:28 +0200 Subject: [PATCH 077/190] refactor tests: replace env variable assumptions in helper function with arguments --- .github/workflows/test.yml | 313 +++++++++++++++++++------------------ 1 file changed, 157 insertions(+), 156 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 114aeed..75ffe72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,7 +4,6 @@ defaults: run: shell: bash env: - ENV_VAR_PREFIX: PROP_TEST__ SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' SELECTED_SINGLE_NAME: 'my-prop' @@ -13,8 +12,7 @@ env: NOT_FOUND_STATUS: 54 SHELL_SETUP: |- debugPrintf() { - local pattern=$1; shift - printf "::debug::${pattern}" "$@" >&2 + printf "$@" | sed 's/^/::debug::/g' >&2 } # Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is @@ -38,26 +36,42 @@ env: fi } - # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. - getAndAssertEquals() { + # The last argument is the expected result. All other arguments are treated as a command (with arguments) + # to be evaluated which outputs the actual result. Both results are then asserted to be equal. + # + # Also, unlike directly using the form + # assertEquals "$(eval "$RESULT_GETTER 'key with spaces')" 'expected value' + # this function does not swallow a non-zero exit status in $RESULT_GETTER (but returns with that error + # status and skips the assertion). + # + # Of the arguments which form the command, only the first is evaled, the rest is treated literally. + # That odd behavior is useful for how the steps for similar test cases are written: They require + # different ways to get a result; for example, one needs `getJsonValue "$RESULT"` and another one + # `getJsonValue "$(<"$RESULT_FILE")`; this difference is moved to environment variables in order to keep + # the script identical, so it can be reused with a YAML anchor. + # Example usage: evalAndAssertEquals "$RESULT_GETTER" 'key with spaces' 'expected value' + # with RESULT_GETTER set to 'getJsonValue "$RESULT"'. + evalAndAssertEquals() { local getterFunction=$1; shift - local key=$1; shift - local expectedValue=$1; shift - local value st - # Invocation of $getterFunction must not ignore its return status. This is why tests use this function in the form - # getAndAssertEquals $RESULT_GETTER "$key" "$expectedValue" - # instead of - # assertEquals "$($RESULT_GETTER "$key")" "$expectedValue" - value="$(set -o pipefail; "$getterFunction" "$key" | replaceLastLF)" + local args=("$@") + local expectedValue="${args[@]:${#args[@]}-1}" + args=("${args[@]:0:${#args[@]}-1}") + + local quotedArgs value + quotedArgs=$(printf ' %q' "${args[@]}") + eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" assertEquals "$value" "$expectedValue" } # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. - getAndAssertUndefined() { + # The command is handled like in →evalAndAssertEquals (except that there is no expected value as last argument which + # is not passed to the command). + evalAndAssertUndefined() { local getterFunction=$1; shift - local value st - if value="$("$getterFunction" "$@")"; then + local quotedArgs value st + quotedArgs=$(printf ' %q' "$@") + if eval "value=\$(${getterFunction}${quotedArgs})"; then printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 return 1 elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then @@ -68,30 +82,21 @@ env: fi } - # The following helper functions to get a value for a given property key from the result have a uniform - # API (property key as sole argument) so that the actual test code can be identical (and thereby reusable - # with YAML anchors) for different result types. This leads to some assumptions which might seem - # a little odd, like variable JSON_OUTPUT_FILE must contain the name of the JSON result instead of passing - # that name as an argument. - - # Expects JSON as stdin and extracts the value for key $1. + # Extracts the value for key $2 from JSON $1. getJsonValue() { + local json=$1; shift local key=$1; shift - debugPrintf 'getJsonValue("%s")\n' "$key" - jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' + debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" + jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" } - # Expects JSON in variable RESULT_JSON and extracts the value for key $1. - getEnvJsonValue() { - getJsonValue "$@" <<<"$RESULT_JSON" - } - - # Expects JSON in variable RESULT_JSON and extracts the value for key . - encodeKeyAndGetEnvJsonValue() { + # Extracts the value for key from JSON $2. + encodeKeyAndGetJsonValue() { + local json=$1; shift local key=$1; shift local encodedKey encodedKey="_$(perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge' <<<"$key")" - getEnvJsonValue "$encodedKey" "$@" + getJsonValue "$json" "$encodedKey" "$@" } # Prints the value of the environment variable set for key $1 (when not using a prefix). @@ -109,15 +114,11 @@ env: fi } - # Prints the value of the environment variable set for key $1 when using a prefix. + # Prints the value of the environment variable set for key $2 when using a prefix $1. getEnvWithPrefix() { + local prefix=$1; shift local key=$1; shift - getEnv "${ENV_VAR_PREFIX}${key}" "$@" - } - - getFileJsonValue() { - debugPrintf 'getFileJsonValue("%s") with file contents "%s"\n' "$*" "$(<"$JSON_OUTPUT_FILE")" - getJsonValue "$@" <"$JSON_OUTPUT_FILE" + getEnv "${prefix}${key}" "$@" } jobs: @@ -233,36 +234,36 @@ jobs: - name: '[assert] test: query all properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' - getAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' - getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' - getAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' - getAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' - getAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' - getAndAssertEquals "$RESULT_GETTER" empty '' - getAndAssertEquals "$RESULT_GETTER" null 'null' - getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - getAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\r\n\nD' - getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_1 $'one trailing LF\n' - getAndAssertEquals "$RESULT_GETTER" trailing_linefeed_2 $'two trailing LFs\n\n' - getAndAssertEquals "$RESULT_GETTER" dash_1 '-' - getAndAssertEquals "$RESULT_GETTER" dash_2 '--' - getAndAssertEquals "$RESULT_GETTER" dash_3 '---' - getAndAssertEquals "$RESULT_GETTER" dash_4 '----' - getAndAssertEquals "$RESULT_GETTER" dash_5 '-----' - getAndAssertEquals "$RESULT_GETTER" dash_6 '------' - getAndAssertEquals "$RESULT_GETTER" dash_7 '-------' - getAndAssertEquals "$RESULT_GETTER" dash_8 '--------' - getAndAssertEquals "$RESULT_GETTER" dash_9 '---------' - getAndAssertEquals "$RESULT_GETTER" dash_10 '----------' - getAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' - getAndAssertEquals "$RESULT_GETTER" dash_12 '------------' - getAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' - getAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' + evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' + evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' + evalAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' + evalAndAssertEquals "$RESULT_GETTER" empty '' + evalAndAssertEquals "$RESULT_GETTER" null 'null' + evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + evalAndAssertEquals "$RESULT_GETTER" whitespace_escapes $'A\tB\fC C\r\n\nD' + evalAndAssertEquals "$RESULT_GETTER" trailing_linefeed_1 $'one trailing LF\n' + evalAndAssertEquals "$RESULT_GETTER" trailing_linefeed_2 $'two trailing LFs\n\n' + evalAndAssertEquals "$RESULT_GETTER" dash_1 '-' + evalAndAssertEquals "$RESULT_GETTER" dash_2 '--' + evalAndAssertEquals "$RESULT_GETTER" dash_3 '---' + evalAndAssertEquals "$RESULT_GETTER" dash_4 '----' + evalAndAssertEquals "$RESULT_GETTER" dash_5 '-----' + evalAndAssertEquals "$RESULT_GETTER" dash_6 '------' + evalAndAssertEquals "$RESULT_GETTER" dash_7 '-------' + evalAndAssertEquals "$RESULT_GETTER" dash_8 '--------' + evalAndAssertEquals "$RESULT_GETTER" dash_9 '---------' + evalAndAssertEquals "$RESULT_GETTER" dash_10 '----------' + evalAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' + evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' + evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' + evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' ############################################################ - name: '[act] test: query multiple properties as action outputs' @@ -274,37 +275,37 @@ jobs: - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateMultiImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' - getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - getAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' - getAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - - getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash-in-Key - getAndAssertUndefined "$RESULT_GETTER" dashInValue - getAndAssertUndefined "$RESULT_GETTER" empty - getAndAssertUndefined "$RESULT_GETTER" null - getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 - getAndAssertUndefined "$RESULT_GETTER" dash_1 - getAndAssertUndefined "$RESULT_GETTER" dash_2 - getAndAssertUndefined "$RESULT_GETTER" dash_3 - getAndAssertUndefined "$RESULT_GETTER" dash_4 - getAndAssertUndefined "$RESULT_GETTER" dash_5 - getAndAssertUndefined "$RESULT_GETTER" dash_6 - getAndAssertUndefined "$RESULT_GETTER" dash_7 - getAndAssertUndefined "$RESULT_GETTER" dash_8 - getAndAssertUndefined "$RESULT_GETTER" dash_9 - getAndAssertUndefined "$RESULT_GETTER" dash_10 - getAndAssertUndefined "$RESULT_GETTER" dash_11 - getAndAssertUndefined "$RESULT_GETTER" dash_12 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + + evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key + evalAndAssertUndefined "$RESULT_GETTER" dashInValue + evalAndAssertUndefined "$RESULT_GETTER" empty + evalAndAssertUndefined "$RESULT_GETTER" null + evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_3 + evalAndAssertUndefined "$RESULT_GETTER" dash_4 + evalAndAssertUndefined "$RESULT_GETTER" dash_5 + evalAndAssertUndefined "$RESULT_GETTER" dash_6 + evalAndAssertUndefined "$RESULT_GETTER" dash_7 + evalAndAssertUndefined "$RESULT_GETTER" dash_8 + evalAndAssertUndefined "$RESULT_GETTER" dash_9 + evalAndAssertUndefined "$RESULT_GETTER" dash_10 + evalAndAssertUndefined "$RESULT_GETTER" dash_11 + evalAndAssertUndefined "$RESULT_GETTER" dash_12 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: '[act] test: query multiple properties as action outputs of given names' @@ -318,23 +319,23 @@ jobs: env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamedImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" java-version '21' - getAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' - getAndAssertEquals "$RESULT_GETTER" colon 'c' - getAndAssertEquals "$RESULT_GETTER" unicode 'a«,»c' + evalAndAssertEquals "$RESULT_GETTER" java-version '21' + evalAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" colon 'c' + evalAndAssertEquals "$RESULT_GETTER" unicode 'a«,»c' # The outputs with the default names are not set: - getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs - getAndAssertUndefined "$RESULT_GETTER" colon:in:Key - getAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: '[act] test: query multiple properties as action outputs of given name' @@ -348,20 +349,20 @@ jobs: env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" my-prop 'c' + evalAndAssertEquals "$RESULT_GETTER" my-prop 'c' # The outputs with the default names are not set: - getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - getAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs - getAndAssertUndefined "$RESULT_GETTER" colon:in:Key - getAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes # Outputs for non-selected keys are not set, for example: - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - name: '[act] test: query single property as action outputs' @@ -373,37 +374,37 @@ jobs: - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} - RESULT_GETTER: encodeKeyAndGetEnvJsonValue + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateSingleImpl |- eval "$SHELL_SETUP" - getAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - - getAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - getAndAssertUndefined "$RESULT_GETTER" targetJavaVersion - getAndAssertUndefined "$RESULT_GETTER" colon:in:Key - getAndAssertUndefined "$RESULT_GETTER" colonInValue - getAndAssertUndefined "$RESULT_GETTER" dash-in-Key - getAndAssertUndefined "$RESULT_GETTER" dashInValue - getAndAssertUndefined "$RESULT_GETTER" empty - getAndAssertUndefined "$RESULT_GETTER" null - getAndAssertUndefined "$RESULT_GETTER" whitespace_escapes - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 - getAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 - getAndAssertUndefined "$RESULT_GETTER" unicode_escapes - getAndAssertUndefined "$RESULT_GETTER" dash_1 - getAndAssertUndefined "$RESULT_GETTER" dash_2 - getAndAssertUndefined "$RESULT_GETTER" dash_3 - getAndAssertUndefined "$RESULT_GETTER" dash_4 - getAndAssertUndefined "$RESULT_GETTER" dash_5 - getAndAssertUndefined "$RESULT_GETTER" dash_6 - getAndAssertUndefined "$RESULT_GETTER" dash_7 - getAndAssertUndefined "$RESULT_GETTER" dash_8 - getAndAssertUndefined "$RESULT_GETTER" dash_9 - getAndAssertUndefined "$RESULT_GETTER" dash_10 - getAndAssertUndefined "$RESULT_GETTER" dash_11 - getAndAssertUndefined "$RESULT_GETTER" dash_12 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 - getAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + + evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key + evalAndAssertUndefined "$RESULT_GETTER" dashInValue + evalAndAssertUndefined "$RESULT_GETTER" empty + evalAndAssertUndefined "$RESULT_GETTER" null + evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_3 + evalAndAssertUndefined "$RESULT_GETTER" dash_4 + evalAndAssertUndefined "$RESULT_GETTER" dash_5 + evalAndAssertUndefined "$RESULT_GETTER" dash_6 + evalAndAssertUndefined "$RESULT_GETTER" dash_7 + evalAndAssertUndefined "$RESULT_GETTER" dash_8 + evalAndAssertUndefined "$RESULT_GETTER" dash_9 + evalAndAssertUndefined "$RESULT_GETTER" dash_10 + evalAndAssertUndefined "$RESULT_GETTER" dash_11 + evalAndAssertUndefined "$RESULT_GETTER" dash_12 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 ############################################################ - name: '[act] test: query all properties as JSON action output' @@ -415,7 +416,7 @@ jobs: - name: '[assert] test: query all properties as JSON action output' env: RESULT_JSON: ${{steps.all-json.outputs.json}} - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl ############################################################ @@ -429,7 +430,7 @@ jobs: - name: '[assert] test: query multiple properties as JSON action output' env: RESULT_JSON: ${{steps.multiple-json.outputs.json}} - RESULT_GETTER: getEnvJsonValue + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiImpl ############################################################ @@ -449,7 +450,7 @@ jobs: - name: '[assert] test: query multiple properties as JSON file (creating file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} - RESULT_GETTER: getFileJsonValue + RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl ############################################################ @@ -471,7 +472,7 @@ jobs: - name: '[assert] test: query multiple properties as JSON file (overwriting file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} - RESULT_GETTER: getFileJsonValue + RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl ############################################################ @@ -489,7 +490,7 @@ jobs: - name: '[assert] test: query all properties as JSON file' env: JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} - RESULT_GETTER: getFileJsonValue + RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateAllImpl @@ -582,10 +583,10 @@ jobs: uses: ./ with: file: test/resources/test.properties - resultType: 'env:${{env.ENV_VAR_PREFIX}}' + resultType: 'env:ALL_PROPS__' - name: '[assert] test: query all properties as environment variables' env: - RESULT_GETTER: getEnvWithPrefix + RESULT_GETTER: 'getEnvWithPrefix ALL_PROPS__' run: *validateAllImpl ############################################################ @@ -598,10 +599,10 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'env:${{env.ENV_VAR_PREFIX}}' + resultType: 'env:MULTI_PROPS__' - name: '[assert] test: query multiple properties as environment variables with prefix' env: - RESULT_GETTER: getEnvWithPrefix + RESULT_GETTER: 'getEnvWithPrefix MULTI_PROPS__' run: *validateMultiImpl ############################################################ @@ -614,8 +615,8 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' - resultType: 'env:${{env.ENV_VAR_PREFIX}}' + resultType: 'env:PROP__' - name: '[assert] test: query single property as environment variable with prefix' env: - RESULT_GETTER: getEnvWithPrefix + RESULT_GETTER: 'getEnvWithPrefix PROP__' run: *validateSingleImpl From 94faf28e7f73000a2b490270bb971726e6c6ef7c Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 17:45:56 +0200 Subject: [PATCH 078/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 19 ++++++++++++------- test/resources/test.properties | 1 + 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7581ac7..f8c8a7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,19 +36,21 @@ env: fi } - # Evaluates the command given as arguments except the last argument and asserts that the command's output - # equals the last argument. Only the first argument is evaled, the rest is treated as literal strings. + # The last argument is the expected result. All other arguments are treated as a command (with arguments) + # to be evaluated which outputs the actual result. Both results are then asserted to be equal. # - # That odd behavior is usefull for how the steps for similar test cases are written: They require + # Also, unlike directly using the form + # assertEquals "$(eval "$RESULT_GETTER 'key with spaces')" 'expected value' + # this function does not swallow a non-zero exit status in $RESULT_GETTER (but returns with that error + # status and skips the assertion). + # + # Of the arguments which form the command, only the first is evaled, the rest is treated literally. + # That odd behavior is useful for how the steps for similar test cases are written: They require # different ways to get a result; for example, one needs `getJsonValue "$RESULT"` and another one # `getJsonValue "$(<"$RESULT_FILE")`; this difference is moved to environment variables in order to keep # the script identical, so it can be reused with a YAML anchor. # Example usage: evalAndAssertEquals "$RESULT_GETTER" 'key with spaces' 'expected value' # with RESULT_GETTER set to 'getJsonValue "$RESULT"'. - # - # Also, unlike directly using - # assertEquals "$(eval "value=$RESULT_GETTER 'key with spaces')" 'expected value' - # this function does not swallow a non-zero exit status in $RESULT_GETTER. evalAndAssertEquals() { local getterFunction=$1; shift local args=("$@") @@ -238,6 +240,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" 'space in Key' 'sp' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' @@ -282,6 +285,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue @@ -380,6 +384,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue diff --git a/test/resources/test.properties b/test/resources/test.properties index 64bd76c..9bb4b82 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -2,6 +2,7 @@ sourceJavaVersion : 21 targetJavaVersion : 17 org.gradle.jvmargs: -ea -showversion +space\ in\ Key: sp colon\:in\:Key: c colonInValue: cv: dash-in-Key: d From 9654c880a6b3e5ee3cecaea9d6c10688baefc75e Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 18:19:01 +0200 Subject: [PATCH 079/190] test: add property with space in name --- .github/workflows/test.yml | 3 +++ test/resources/test.properties | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75ffe72..f8c8a7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -240,6 +240,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" 'space in Key' 'sp' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' @@ -284,6 +285,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue @@ -382,6 +384,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue diff --git a/test/resources/test.properties b/test/resources/test.properties index 64bd76c..9bb4b82 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -2,6 +2,7 @@ sourceJavaVersion : 21 targetJavaVersion : 17 org.gradle.jvmargs: -ea -showversion +space\ in\ Key: sp colon\:in\:Key: c colonInValue: cv: dash-in-Key: d From a38188a5477026ebdb654299adc21974a3fa6c04 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 18:30:49 +0200 Subject: [PATCH 080/190] cosmetic: write space char unescaped in JSON values --- Action.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Action.java b/Action.java index dbdf27d..f9d6066 100644 --- a/Action.java +++ b/Action.java @@ -348,7 +348,9 @@ public static String toJson(Properties props) { private static void appendJsonString(StringBuilder buffer, String s) { for (char c : s.toCharArray()) { - if (c == '"') { + if (c == ' ') { + buffer.append(c); + } else if (c == '"') { buffer.append('\\').append('"'); } else if (c == '\t') { buffer.append('\\').append('t'); From f49ad5c8b7a2905ebf1f89ba1b430d89a97a5f4e Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 19:03:11 +0200 Subject: [PATCH 081/190] cosmetic: write space char unescaped in JSON values --- Action.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Action.java b/Action.java index dbdf27d..f9d6066 100644 --- a/Action.java +++ b/Action.java @@ -348,7 +348,9 @@ public static String toJson(Properties props) { private static void appendJsonString(StringBuilder buffer, String s) { for (char c : s.toCharArray()) { - if (c == '"') { + if (c == ' ') { + buffer.append(c); + } else if (c == '"') { buffer.append('\\').append('"'); } else if (c == '\t') { buffer.append('\\').append('t'); From 628247be17c1ff4a49b484b9356b3278a0758b6d Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 19:49:31 +0200 Subject: [PATCH 082/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 30 ++++++++++--- Action.java | 86 +++++++++++++++++++++++++++++--------- action.yml | 1 + 3 files changed, 93 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8c8a7c..bd69ed8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -142,9 +142,13 @@ jobs: file: test/resources/test.properties resultType: rt1 - name: '[assert] test (should fail): invalid input: resultType' + env: + STATUS: ${{steps.error-input-resultType.outcome}} + ERROR: ${{steps.error-input-resultType.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-input-resultType.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType: rt1' ############################################################ - name: '[act] test (should fail): resultType "output-named" without names' @@ -156,9 +160,13 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:' - name: '[assert] test (should fail): resultType "output-named" without names' + env: + STATUS: ${{steps.error-resultType-output-without-names.outcome}} + ERROR: ${{steps.error-resultType-output-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-output-without-names.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' ############################################################ - name: '[act] test (should fail): resultType "env-named" without names' @@ -170,9 +178,13 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:' - name: '[assert] test (should fail): resultType "env-named" without names' + env: + STATUS: ${{steps.error-resultType-env-without-names.outcome}} + ERROR: ${{steps.error-resultType-env-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-env-without-names.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' ############################################################ - name: '[act] test (should fail): resultType "output-named" without keys' @@ -183,9 +195,13 @@ jobs: file: test/resources/test.properties resultType: 'output-named:a b' - name: '[assert] test (should fail): resultType "output-named" without keys' + env: + STATUS: ${{steps.error-resultType-output-without-keys.outcome}} + ERROR: ${{steps.error-resultType-output-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-output-without-keys.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' ############################################################ - name: '[act] test (should fail): resultType "env-named" without keys' @@ -196,9 +212,13 @@ jobs: file: test/resources/test.properties resultType: 'env-named:a b' - name: '[assert] test (should fail): resultType "env-named" without keys' + env: + STATUS: ${{steps.error-resultType-env-without-keys.outcome}} + ERROR: ${{steps.error-resultType-env-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-env-without-keys.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' ############################################################ # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output diff --git a/Action.java b/Action.java index f9d6066..859e8c3 100644 --- a/Action.java +++ b/Action.java @@ -24,37 +24,78 @@ public class Action { private Action() {} static void main(String[] args) throws Exception { - Config config = Config.fromEnv(); - ResultWriter resultWriter = ResultWriter.of(config.resultType()); - for (String file : args) { - Properties properties = config.selectedKeys() == null ? Util.readProperties(file) - : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); - resultWriter.write(properties, config); + try { + Config config = Config.fromEnv(); + ResultWriter resultWriter = ResultWriter.of(config.resultType()); + for (String file : args) { + Properties properties = config.selectedKeys() == null ? Util.readProperties(file) + : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); + resultWriter.write(properties, config); + } + } catch (OutputException e) { + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + // This output is not documented + writer.write("error", e.getMessage()); + } + throw e; } } } +/** + * An exception for which an "error" output should be set. + */ +class OutputException extends RuntimeException { + public OutputException(String message, Throwable cause) { + super(message, cause); + } + + public static OutputException forIllegalArgument(String message) { + return new OutputException(message, new IllegalArgumentException(message)); + } +} + + +/** + * Configuration environment variable names and their corresponding action input names for error messages. + */ +enum ConfigVariable { + FILE("file"), // + KEYS("keys"), // + RESULT_TYPE("resultType"), // + KEY_SEPARATOR("keySeparator"), // + RESULT_NAME_SEPARATOR("resultNameSeparator"), // + OUTPUT_PREFIX(null); + + public final String inputName; + + private ConfigVariable(String inputName) { + this.inputName = inputName; + } +} + + record Config(List selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. - String keySeparator = Util.getRequiredEnv("KEY_SEPARATOR"); + String keySeparator = Util.getRequiredEnv(ConfigVariable.KEY_SEPARATOR); // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty // string: - String keysStr = System.getenv().getOrDefault("KEYS", ""); + String keysStr = System.getenv().getOrDefault(ConfigVariable.KEYS.name(), ""); Optional keys = Util.splitArray(keysStr, keySeparator); - String resultNameSeparator = Util.getRequiredEnv("RESULT_NAME_SEPARATOR"); + String resultNameSeparator = Util.getRequiredEnv(ConfigVariable.RESULT_NAME_SEPARATOR); // RESULT_TYPE format: "[:]" - String resultTypeWithArg = Util.getRequiredEnv("RESULT_TYPE"); + String resultTypeWithArg = Util.getRequiredEnv(ConfigVariable.RESULT_TYPE); Matcher matcher = Pattern.compile("([^:]+)(?::(.*))?").matcher(resultTypeWithArg); if (!matcher.matches()) { - throw new IllegalArgumentException("invalid resultType: " + resultTypeWithArg); + throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + resultTypeWithArg); } String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); - String outputPrefix = Util.getRequiredEnv("OUTPUT_PREFIX"); + String outputPrefix = Util.getRequiredEnv(ConfigVariable.OUTPUT_PREFIX); return new Config(keys.map(List::of).orElse(null), keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); } @@ -62,14 +103,16 @@ public static Config fromEnv() { public String requiredResultTypeArg() { String arg = resultTypeArg(); if ("".equals(arg)) { - throw new IllegalArgumentException("invalid resultType " + resultTypeWithArg() + " (missing argument)"); + throw OutputException.forIllegalArgument( + "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (missing argument)"); } return arg; } public void requireNoArg() { if (!"".equals(resultTypeArg())) { - throw new IllegalArgumentException("invalid resultType " + resultTypeWithArg() + " (non-empty argument)"); + throw OutputException.forIllegalArgument( + "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (non-empty argument)"); } } } @@ -164,7 +207,7 @@ public static ResultWriter of(String inputName) { return rw; } } - throw new IllegalArgumentException("invalid result type: " + inputName); + throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + inputName); } public abstract void write(Properties props, Config config) throws IOException; @@ -172,14 +215,15 @@ public static ResultWriter of(String inputName) { private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { List selectedKeys = config.selectedKeys(); if (selectedKeys == null) { - throw new IllegalArgumentException("invalid use of resultType " + config.resultType() + " (missing keys)"); + throw OutputException.forIllegalArgument( + "invalid use of " + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing keys)"); } @SuppressWarnings("java:S3655") // Sonar rule: "Optional value should only be accessed after calling isPresent()". // requiredResultTypeArg() is not empty, so splitArray returns non-empty String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator()).get(); if (resultNames.length != selectedKeys.size() && resultNames.length != 1) { - throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length - + " arguments, but " + selectedKeys.size() + " keys are selected"); + throw OutputException.forIllegalArgument(ConfigVariable.RESULT_TYPE + " " + config.resultTypeWithArg() + " has " + + resultNames.length + " arguments, but " + selectedKeys.size() + " keys are selected"); } try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.size(); i++) { @@ -280,11 +324,15 @@ private Util() {} public static String getRequiredEnv(String varName) { String value = System.getenv(varName); if (value == null || value.isEmpty()) { - throw new IllegalArgumentException("missing or empty environment variable " + varName); + throw OutputException.forIllegalArgument("missing or empty environment variable " + varName); } return value; } + public static String getRequiredEnv(ConfigVariable varName) { + return getRequiredEnv(varName.name()); + } + public static Optional splitArray(String arrayStr, String separator) { return arrayStr.isEmpty() ? Optional.empty() : Optional.of(arrayStr.split(Pattern.quote(separator), -1)); } diff --git a/action.yml b/action.yml index 6b530a1..36386cc 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,7 @@ outputs: description: The (plain) property value. Only set in certain modes, see input `resultType`. json: description: All found properties matching the `keys` input as a single-line JSON object. Only set in certain modes, see input `resultType`. + #error: Not documented, because it should only be parsed by our tests. runs: using: docker From db2532f61e9b3c457ef10e1fdb82efbb86868e55 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 19:52:36 +0200 Subject: [PATCH 083/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Action.java b/Action.java index 859e8c3..94e1091 100644 --- a/Action.java +++ b/Action.java @@ -73,6 +73,11 @@ enum ConfigVariable { private ConfigVariable(String inputName) { this.inputName = inputName; } + + @Override + public String toString() { + return inputName; + } } From f5b019192c0f7341934e347ecc12fcc0616e547a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 19:56:43 +0200 Subject: [PATCH 084/190] add output "error" --- .github/workflows/test.yml | 30 ++++++++++--- Action.java | 91 ++++++++++++++++++++++++++++++-------- action.yml | 1 + 3 files changed, 98 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8c8a7c..bd69ed8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -142,9 +142,13 @@ jobs: file: test/resources/test.properties resultType: rt1 - name: '[assert] test (should fail): invalid input: resultType' + env: + STATUS: ${{steps.error-input-resultType.outcome}} + ERROR: ${{steps.error-input-resultType.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-input-resultType.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType: rt1' ############################################################ - name: '[act] test (should fail): resultType "output-named" without names' @@ -156,9 +160,13 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:' - name: '[assert] test (should fail): resultType "output-named" without names' + env: + STATUS: ${{steps.error-resultType-output-without-names.outcome}} + ERROR: ${{steps.error-resultType-output-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-output-without-names.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' ############################################################ - name: '[act] test (should fail): resultType "env-named" without names' @@ -170,9 +178,13 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:' - name: '[assert] test (should fail): resultType "env-named" without names' + env: + STATUS: ${{steps.error-resultType-env-without-names.outcome}} + ERROR: ${{steps.error-resultType-env-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-env-without-names.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' ############################################################ - name: '[act] test (should fail): resultType "output-named" without keys' @@ -183,9 +195,13 @@ jobs: file: test/resources/test.properties resultType: 'output-named:a b' - name: '[assert] test (should fail): resultType "output-named" without keys' + env: + STATUS: ${{steps.error-resultType-output-without-keys.outcome}} + ERROR: ${{steps.error-resultType-output-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-output-without-keys.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' ############################################################ - name: '[act] test (should fail): resultType "env-named" without keys' @@ -196,9 +212,13 @@ jobs: file: test/resources/test.properties resultType: 'env-named:a b' - name: '[assert] test (should fail): resultType "env-named" without keys' + env: + STATUS: ${{steps.error-resultType-env-without-keys.outcome}} + ERROR: ${{steps.error-resultType-env-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" - assertEquals '${{steps.error-resultType-env-without-keys.outcome}}' failure + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' ############################################################ # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output diff --git a/Action.java b/Action.java index f9d6066..94e1091 100644 --- a/Action.java +++ b/Action.java @@ -24,37 +24,83 @@ public class Action { private Action() {} static void main(String[] args) throws Exception { - Config config = Config.fromEnv(); - ResultWriter resultWriter = ResultWriter.of(config.resultType()); - for (String file : args) { - Properties properties = config.selectedKeys() == null ? Util.readProperties(file) - : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); - resultWriter.write(properties, config); + try { + Config config = Config.fromEnv(); + ResultWriter resultWriter = ResultWriter.of(config.resultType()); + for (String file : args) { + Properties properties = config.selectedKeys() == null ? Util.readProperties(file) + : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); + resultWriter.write(properties, config); + } + } catch (OutputException e) { + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + // This output is not documented + writer.write("error", e.getMessage()); + } + throw e; } } } +/** + * An exception for which an "error" output should be set. + */ +class OutputException extends RuntimeException { + public OutputException(String message, Throwable cause) { + super(message, cause); + } + + public static OutputException forIllegalArgument(String message) { + return new OutputException(message, new IllegalArgumentException(message)); + } +} + + +/** + * Configuration environment variable names and their corresponding action input names for error messages. + */ +enum ConfigVariable { + FILE("file"), // + KEYS("keys"), // + RESULT_TYPE("resultType"), // + KEY_SEPARATOR("keySeparator"), // + RESULT_NAME_SEPARATOR("resultNameSeparator"), // + OUTPUT_PREFIX(null); + + public final String inputName; + + private ConfigVariable(String inputName) { + this.inputName = inputName; + } + + @Override + public String toString() { + return inputName; + } +} + + record Config(List selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. - String keySeparator = Util.getRequiredEnv("KEY_SEPARATOR"); + String keySeparator = Util.getRequiredEnv(ConfigVariable.KEY_SEPARATOR); // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty // string: - String keysStr = System.getenv().getOrDefault("KEYS", ""); + String keysStr = System.getenv().getOrDefault(ConfigVariable.KEYS.name(), ""); Optional keys = Util.splitArray(keysStr, keySeparator); - String resultNameSeparator = Util.getRequiredEnv("RESULT_NAME_SEPARATOR"); + String resultNameSeparator = Util.getRequiredEnv(ConfigVariable.RESULT_NAME_SEPARATOR); // RESULT_TYPE format: "[:]" - String resultTypeWithArg = Util.getRequiredEnv("RESULT_TYPE"); + String resultTypeWithArg = Util.getRequiredEnv(ConfigVariable.RESULT_TYPE); Matcher matcher = Pattern.compile("([^:]+)(?::(.*))?").matcher(resultTypeWithArg); if (!matcher.matches()) { - throw new IllegalArgumentException("invalid resultType: " + resultTypeWithArg); + throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + resultTypeWithArg); } String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); - String outputPrefix = Util.getRequiredEnv("OUTPUT_PREFIX"); + String outputPrefix = Util.getRequiredEnv(ConfigVariable.OUTPUT_PREFIX); return new Config(keys.map(List::of).orElse(null), keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); } @@ -62,14 +108,16 @@ public static Config fromEnv() { public String requiredResultTypeArg() { String arg = resultTypeArg(); if ("".equals(arg)) { - throw new IllegalArgumentException("invalid resultType " + resultTypeWithArg() + " (missing argument)"); + throw OutputException.forIllegalArgument( + "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (missing argument)"); } return arg; } public void requireNoArg() { if (!"".equals(resultTypeArg())) { - throw new IllegalArgumentException("invalid resultType " + resultTypeWithArg() + " (non-empty argument)"); + throw OutputException.forIllegalArgument( + "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (non-empty argument)"); } } } @@ -164,7 +212,7 @@ public static ResultWriter of(String inputName) { return rw; } } - throw new IllegalArgumentException("invalid result type: " + inputName); + throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + inputName); } public abstract void write(Properties props, Config config) throws IOException; @@ -172,14 +220,15 @@ public static ResultWriter of(String inputName) { private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { List selectedKeys = config.selectedKeys(); if (selectedKeys == null) { - throw new IllegalArgumentException("invalid use of resultType " + config.resultType() + " (missing keys)"); + throw OutputException.forIllegalArgument( + "invalid use of " + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing keys)"); } @SuppressWarnings("java:S3655") // Sonar rule: "Optional value should only be accessed after calling isPresent()". // requiredResultTypeArg() is not empty, so splitArray returns non-empty String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator()).get(); if (resultNames.length != selectedKeys.size() && resultNames.length != 1) { - throw new IllegalArgumentException("resultType " + config.resultTypeWithArg() + " has " + resultNames.length - + " arguments, but " + selectedKeys.size() + " keys are selected"); + throw OutputException.forIllegalArgument(ConfigVariable.RESULT_TYPE + " " + config.resultTypeWithArg() + " has " + + resultNames.length + " arguments, but " + selectedKeys.size() + " keys are selected"); } try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.size(); i++) { @@ -280,11 +329,15 @@ private Util() {} public static String getRequiredEnv(String varName) { String value = System.getenv(varName); if (value == null || value.isEmpty()) { - throw new IllegalArgumentException("missing or empty environment variable " + varName); + throw OutputException.forIllegalArgument("missing or empty environment variable " + varName); } return value; } + public static String getRequiredEnv(ConfigVariable varName) { + return getRequiredEnv(varName.name()); + } + public static Optional splitArray(String arrayStr, String separator) { return arrayStr.isEmpty() ? Optional.empty() : Optional.of(arrayStr.split(Pattern.quote(separator), -1)); } diff --git a/action.yml b/action.yml index 6b530a1..36386cc 100644 --- a/action.yml +++ b/action.yml @@ -45,6 +45,7 @@ outputs: description: The (plain) property value. Only set in certain modes, see input `resultType`. json: description: All found properties matching the `keys` input as a single-line JSON object. Only set in certain modes, see input `resultType`. + #error: Not documented, because it should only be parsed by our tests. runs: using: docker From 63e0a742dea9745b74c50b3d472667965817ea23 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 20:28:25 +0200 Subject: [PATCH 085/190] set additional "value" output only when it makes sense --- .github/workflows/test.yml | 40 ++++++++++++++++++++++++++++++++++++++ Action.java | 25 ++++++++++++------------ action.yml | 2 +- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd69ed8..24dc2c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ defaults: env: SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' + SELECTED_KEYS_OTHER_ORDER: 'colon:in:Key sourceJavaVersion org.gradle.jvmargs unicode_escapes' SELECTED_SINGLE_NAME: 'my-prop' SELECTED_NAMES: 'java-version jvm_args unicode colon' @@ -285,6 +286,11 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' + - name: '[assert] test: query all properties as action outputs (additional "value" output not set)' + env: + RESULT_VALUE: ${{steps.all-output.outputs.value || 'U_N_S_E_T'}} + run: |- + assertEquals "$RESULT_VALUE" U_N_S_E_T ############################################################ - name: '[act] test: query multiple properties as action outputs' @@ -293,6 +299,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} @@ -329,6 +336,30 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + - name: '[assert] test: query multiple properties as action outputs (additional "value" output)' + env: + RESULT_VALUE: ${{steps.multi-output.outputs.value}} + run: |- + assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" + + ############################################################ + - name: '[act] test: query multiple properties as action outputs (in another keys order)' + id: multi-output-2 + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS_OTHER_ORDER}}' + - name: '[assert] test: query multiple properties as action outputs (in another keys order)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-2.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: *validateMultiImpl + - name: '[assert] test: query multiple properties as action outputs (in another keys order) (additional "value" output)' + env: + RESULT_VALUE: ${{steps.multi-output-2.outputs.value}} + run: |- + assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" + ############################################################ - name: '[act] test: query multiple properties as action outputs of given names' id: multi-output-named @@ -337,6 +368,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_NAMES}}' + - name: '[assert] test: query multiple properties as action outputs of given names' env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} @@ -359,6 +391,12 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash_1 + - name: '[assert] test: query multiple properties as action outputs (additional "value" output not set)' + env: + RESULT_VALUE: ${{steps.multi-output-named.outputs.value || 'U_N_S_E_T'}} + run: |- + assertEquals "$RESULT_VALUE" U_N_S_E_T + ############################################################ - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 @@ -367,6 +405,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' + - name: '[assert] test: query multiple properties as action outputs of given name' env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} @@ -393,6 +432,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} diff --git a/Action.java b/Action.java index 94e1091..7884ef9 100644 --- a/Action.java +++ b/Action.java @@ -28,9 +28,10 @@ static void main(String[] args) throws Exception { Config config = Config.fromEnv(); ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { - Properties properties = config.selectedKeys() == null ? Util.readProperties(file) - : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); - resultWriter.write(properties, config); + Properties properties = Util.readProperties(file); + Properties selectedProperties = config.selectedKeys().map(keys -> Util.selectProperties(properties, keys, file)) + .orElse(properties); + resultWriter.write(selectedProperties, config); } } catch (OutputException e) { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { @@ -81,8 +82,8 @@ public String toString() { } -record Config(List selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, - String resultNameSeparator, String outputPrefix) { +record Config(Optional> selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, + String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. String keySeparator = Util.getRequiredEnv(ConfigVariable.KEY_SEPARATOR); @@ -101,8 +102,8 @@ public static Config fromEnv() { String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); String outputPrefix = Util.getRequiredEnv(ConfigVariable.OUTPUT_PREFIX); - return new Config(keys.map(List::of).orElse(null), keySeparator, resultTypeWithArg, resultType, resultTypeArg, - resultNameSeparator, outputPrefix); + return new Config(keys.map(List::of), keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, + outputPrefix); } public String requiredResultTypeArg() { @@ -137,7 +138,7 @@ public void write(Properties props, Config config) throws IOException { // TODO props must be in order of config.selectedKeys() // Otherwise, this is arbitrary: - if (lastValue != null) { + if (config.selectedKeys().isPresent() && lastValue != null) { writer.write("value", lastValue); } } @@ -218,11 +219,9 @@ public static ResultWriter of(String inputName) { public abstract void write(Properties props, Config config) throws IOException; private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { - List selectedKeys = config.selectedKeys(); - if (selectedKeys == null) { - throw OutputException.forIllegalArgument( - "invalid use of " + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing keys)"); - } + List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " + + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + ConfigVariable.KEYS + ")")); + @SuppressWarnings("java:S3655") // Sonar rule: "Optional value should only be accessed after calling isPresent()". // requiredResultTypeArg() is not empty, so splitArray returns non-empty String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator()).get(); diff --git a/action.yml b/action.yml index 36386cc..412728c 100644 --- a/action.yml +++ b/action.yml @@ -27,7 +27,7 @@ inputs: Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name "a-002Eb-002Dc_d". - Additionally, set output "value" to the last found value, if any (which is only usefull if `keys` is non-empty, i.e. last given key wins; without keys, the last property in the undefined java.util.Properties iteration order wins). + Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. - "env": For each property key , set an environment variable . From c7407aefd310b33b887285c35998e1fbcc2040b9 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 20:30:50 +0200 Subject: [PATCH 086/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24dc2c8..5cd01e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -290,6 +290,7 @@ jobs: env: RESULT_VALUE: ${{steps.all-output.outputs.value || 'U_N_S_E_T'}} run: |- + eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T ############################################################ @@ -340,6 +341,7 @@ jobs: env: RESULT_VALUE: ${{steps.multi-output.outputs.value}} run: |- + eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" ############################################################ @@ -358,6 +360,7 @@ jobs: env: RESULT_VALUE: ${{steps.multi-output-2.outputs.value}} run: |- + eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" ############################################################ @@ -395,6 +398,7 @@ jobs: env: RESULT_VALUE: ${{steps.multi-output-named.outputs.value || 'U_N_S_E_T'}} run: |- + eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T ############################################################ From 7de57de9c3fcda8cc090d7cd983a6c75706cee86 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 21:01:29 +0200 Subject: [PATCH 087/190] refactor: move naming dependencies into one class --- Action.java | 130 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 49 deletions(-) diff --git a/Action.java b/Action.java index 7884ef9..3024912 100644 --- a/Action.java +++ b/Action.java @@ -35,8 +35,7 @@ static void main(String[] args) throws Exception { } } catch (OutputException e) { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - // This output is not documented - writer.write("error", e.getMessage()); + writer.write(Ids.OutputName.ERROR, e.getMessage()); } throw e; } @@ -45,39 +44,68 @@ static void main(String[] args) throws Exception { /** - * An exception for which an "error" output should be set. + * Identifiers which must match those specified in the action YAML. */ -class OutputException extends RuntimeException { - public OutputException(String message, Throwable cause) { - super(message, cause); +class Ids { + private Ids() {} + + /** + * Configuration environment variable names and their corresponding action input names for error messages. + */ + enum ConfigVariable { + FILE("file"), // + KEYS("keys"), // + RESULT_TYPE("resultType"), // + KEY_SEPARATOR("keySeparator"), // + RESULT_NAME_SEPARATOR("resultNameSeparator"), // + OUTPUT_PREFIX(null); + + public final String externalName; + + private ConfigVariable(String externalName) { + this.externalName = externalName; + } + + @Override + public String toString() { + return externalName; + } } - public static OutputException forIllegalArgument(String message) { - return new OutputException(message, new IllegalArgumentException(message)); + enum ResultWriterName { + OUTPUT("output"), // + OUTPUT_NAMED("output-named"), // + ENV_NAMED("env-named"), // + ENV("env"), // + JSON("json"), // + JSON_FILE("json-file"); + + public final String externalName; + + private ResultWriterName(String externalName) { + this.externalName = externalName; + } + } + + enum OutputName { + JSON, // + VALUE, // + ERROR; // This output is not documented. + public final String externalName = name().toLowerCase(); } } /** - * Configuration environment variable names and their corresponding action input names for error messages. + * An exception for which an "error" output should be set. */ -enum ConfigVariable { - FILE("file"), // - KEYS("keys"), // - RESULT_TYPE("resultType"), // - KEY_SEPARATOR("keySeparator"), // - RESULT_NAME_SEPARATOR("resultNameSeparator"), // - OUTPUT_PREFIX(null); - - public final String inputName; - - private ConfigVariable(String inputName) { - this.inputName = inputName; +class OutputException extends RuntimeException { + public OutputException(String message, Throwable cause) { + super(message, cause); } - @Override - public String toString() { - return inputName; + public static OutputException forIllegalArgument(String message) { + return new OutputException(message, new IllegalArgumentException(message)); } } @@ -86,22 +114,22 @@ record Config(Optional> selectedKeys, String keySeparator, String r String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. - String keySeparator = Util.getRequiredEnv(ConfigVariable.KEY_SEPARATOR); + String keySeparator = Util.getRequiredEnv(Ids.ConfigVariable.KEY_SEPARATOR); // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty // string: - String keysStr = System.getenv().getOrDefault(ConfigVariable.KEYS.name(), ""); + String keysStr = System.getenv().getOrDefault(Ids.ConfigVariable.KEYS.name(), ""); Optional keys = Util.splitArray(keysStr, keySeparator); - String resultNameSeparator = Util.getRequiredEnv(ConfigVariable.RESULT_NAME_SEPARATOR); + String resultNameSeparator = Util.getRequiredEnv(Ids.ConfigVariable.RESULT_NAME_SEPARATOR); // RESULT_TYPE format: "[:]" - String resultTypeWithArg = Util.getRequiredEnv(ConfigVariable.RESULT_TYPE); + String resultTypeWithArg = Util.getRequiredEnv(Ids.ConfigVariable.RESULT_TYPE); Matcher matcher = Pattern.compile("([^:]+)(?::(.*))?").matcher(resultTypeWithArg); if (!matcher.matches()) { - throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + resultTypeWithArg); + throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + resultTypeWithArg); } String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); - String outputPrefix = Util.getRequiredEnv(ConfigVariable.OUTPUT_PREFIX); + String outputPrefix = Util.getRequiredEnv(Ids.ConfigVariable.OUTPUT_PREFIX); return new Config(keys.map(List::of), keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); } @@ -110,7 +138,7 @@ public String requiredResultTypeArg() { String arg = resultTypeArg(); if ("".equals(arg)) { throw OutputException.forIllegalArgument( - "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (missing argument)"); + "invalid " + Ids.ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (missing argument)"); } return arg; } @@ -118,14 +146,14 @@ public String requiredResultTypeArg() { public void requireNoArg() { if (!"".equals(resultTypeArg())) { throw OutputException.forIllegalArgument( - "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (non-empty argument)"); + "invalid " + Ids.ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (non-empty argument)"); } } } enum ResultWriter { - OUTPUT("output") { + OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override public void write(Properties props, Config config) throws IOException { String lastValue = null; @@ -139,7 +167,7 @@ public void write(Properties props, Config config) throws IOException { // TODO props must be in order of config.selectedKeys() // Otherwise, this is arbitrary: if (config.selectedKeys().isPresent() && lastValue != null) { - writer.write("value", lastValue); + writer.write(Ids.OutputName.VALUE, lastValue); } } } @@ -154,19 +182,19 @@ private static String encodeKey(Config config, String key) { return result.toString(); } }, - OUTPUT_NAMED("output-named") { + OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override public void write(Properties props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); } }, - ENV_NAMED("env-named") { + ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override public void write(Properties props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.ENV); } }, - ENV("env") { + ENV(Ids.ResultWriterName.ENV) { @Override public void write(Properties props, Config config) throws IOException { String prefix = config.resultTypeArg(); @@ -177,16 +205,16 @@ public void write(Properties props, Config config) throws IOException { } } }, - JSON("json") { + JSON(Ids.ResultWriterName.JSON) { @Override public void write(Properties props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - writer.write("json", Util.toJson(props)); + writer.write(Ids.OutputName.JSON, Util.toJson(props)); } } }, - JSON_FILE("json-file") { + JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); @@ -201,32 +229,32 @@ public void write(Properties props, Config config) throws IOException { } }; - private final String inputName; + private final String externalName; - private ResultWriter(String inputName) { - this.inputName = inputName; + private ResultWriter(Ids.ResultWriterName externalName) { + this.externalName = externalName.externalName; } - public static ResultWriter of(String inputName) { + public static ResultWriter of(String externalName) { for (ResultWriter rw : ResultWriter.values()) { - if (rw.inputName.equals(inputName)) { + if (rw.externalName.equals(externalName)) { return rw; } } - throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + inputName); + throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + externalName); } public abstract void write(Properties props, Config config) throws IOException; private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " - + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + ConfigVariable.KEYS + ")")); + + Ids.ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + Ids.ConfigVariable.KEYS + ")")); @SuppressWarnings("java:S3655") // Sonar rule: "Optional value should only be accessed after calling isPresent()". // requiredResultTypeArg() is not empty, so splitArray returns non-empty String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator()).get(); if (resultNames.length != selectedKeys.size() && resultNames.length != 1) { - throw OutputException.forIllegalArgument(ConfigVariable.RESULT_TYPE + " " + config.resultTypeWithArg() + " has " + throw OutputException.forIllegalArgument(Ids.ConfigVariable.RESULT_TYPE + " " + config.resultTypeWithArg() + " has " + resultNames.length + " arguments, but " + selectedKeys.size() + " keys are selected"); } try (GitHubVariableWriter writer = gitHubOutputFile.open()) { @@ -281,6 +309,10 @@ public void write(String key, String value) throws IOException { } } + public void write(Ids.OutputName key, String value) throws IOException { + write(key.externalName, value); + } + /** * Writes a key-value pair in * multiline @@ -333,7 +365,7 @@ public static String getRequiredEnv(String varName) { return value; } - public static String getRequiredEnv(ConfigVariable varName) { + public static String getRequiredEnv(Ids.ConfigVariable varName) { return getRequiredEnv(varName.name()); } From 20374bf930eb6fd590764c0e458f22d6af05250a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 21:51:23 +0200 Subject: [PATCH 088/190] fix resultType "output": obey selected keys order --- Action.java | 77 +++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/Action.java b/Action.java index 3024912..7120520 100644 --- a/Action.java +++ b/Action.java @@ -8,8 +8,7 @@ import java.nio.file.OpenOption; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.AbstractMap; -import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -29,8 +28,9 @@ static void main(String[] args) throws Exception { ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { Properties properties = Util.readProperties(file); - Properties selectedProperties = config.selectedKeys().map(keys -> Util.selectProperties(properties, keys, file)) - .orElse(properties); + Map selectedProperties = config.selectedKeys() + .map(keys -> Util.selectProperties(properties, keys, file)) // + .orElseGet(() -> Util.stringEntries(properties)); resultWriter.write(selectedProperties, config); } } catch (OutputException e) { @@ -155,17 +155,15 @@ public void requireNoArg() { enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - for (Map.Entry entry : Util.stringEntries(props)) { + for (Map.Entry entry : props.entrySet()) { String key = encodeKey(config, config.outputPrefix() + entry.getKey()); lastValue = entry.getValue(); writer.write(key, lastValue); } - // TODO props must be in order of config.selectedKeys() - // Otherwise, this is arbitrary: if (config.selectedKeys().isPresent() && lastValue != null) { writer.write(Ids.OutputName.VALUE, lastValue); } @@ -184,22 +182,22 @@ private static String encodeKey(Config config, String key) { }, OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); } }, ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.ENV); } }, ENV(Ids.ResultWriterName.ENV) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String prefix = config.resultTypeArg(); try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { - for (Map.Entry entry : Util.stringEntries(props)) { + for (Map.Entry entry : props.entrySet()) { writer.write(prefix + entry.getKey(), entry.getValue()); } } @@ -207,7 +205,7 @@ public void write(Properties props, Config config) throws IOException { }, JSON(Ids.ResultWriterName.JSON) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write(Ids.OutputName.JSON, Util.toJson(props)); @@ -216,7 +214,7 @@ public void write(Properties props, Config config) throws IOException { }, JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { @@ -244,9 +242,10 @@ public static ResultWriter of(String externalName) { throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + externalName); } - public abstract void write(Properties props, Config config) throws IOException; + public abstract void write(Map props, Config config) throws IOException; - private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { + private static void writeNamedImpl(Map props, Config config, GitHubOutputFile gitHubOutputFile) + throws IOException { List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " + Ids.ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + Ids.ConfigVariable.KEYS + ")")); @@ -260,7 +259,7 @@ private static void writeNamedImpl(Properties props, Config config, GitHubOutput try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; - String value = props.getProperty(selectedKeys.get(i)); + String value = props.get(selectedKeys.get(i)); writer.write(name, value); } } @@ -385,42 +384,34 @@ public static Properties readProperties(String file) throws IOException { return allProps; } - public static Properties selectProperties(Properties allProps, List selectedKeys, String file) { - Set selectedKeysSet = new LinkedHashSet<>(selectedKeys); - Properties props = new Properties(); - for (Map.Entry entry : stringEntries(allProps)) { - if (selectedKeysSet.contains(entry.getKey())) { - selectedKeysSet.remove(entry.getKey()); - props.setProperty(entry.getKey(), entry.getValue()); + public static Map selectProperties(Properties allProps, List selectedKeys, String file) { + Set unmatchedKeysSet = new LinkedHashSet<>(selectedKeys); + Map results = new LinkedHashMap<>(); + for (String key : selectedKeys) { + String value = allProps.getProperty(key); + if (value != null) { + results.put(key, value); + unmatchedKeysSet.remove(key); } } - for (String key : selectedKeysSet) { + for (String key : unmatchedKeysSet) { System.err.format("Property %s not found in %s%n", key, file); } - return props; + return results; } - public static Iterable> stringEntries(Properties props) { - return () -> new Iterator<>() { - private final Iterator keysIter = props.stringPropertyNames().iterator(); - - @Override - public Map.Entry next() { - String key = keysIter.next(); - return new AbstractMap.SimpleImmutableEntry<>(key, props.getProperty(key)); - } - - @Override - public boolean hasNext() { - return keysIter.hasNext(); - } - }; + public static Map stringEntries(Properties props) { + Map map = new LinkedHashMap<>(); + for (String key : props.stringPropertyNames()) { + map.put(key, props.getProperty(key)); + } + return map; } - public static String toJson(Properties props) { + public static String toJson(Map map) { StringBuilder s = new StringBuilder(50).append('{'); int initialLength = s.length(); - for (Map.Entry entry : stringEntries(props)) { + for (Map.Entry entry : map.entrySet()) { s.append(s.length() == initialLength ? '"' : ", \""); appendJsonString(s, entry.getKey()); s.append("\":\""); From 77e2a34a6b9b4731e2ef2dab323401aa1b5d6f6d Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 21:55:44 +0200 Subject: [PATCH 089/190] set additional "value" output only when it makes sense --- .github/workflows/test.yml | 44 ++++++++++++++++++++++++++++++++++++++ Action.java | 25 +++++++++++----------- action.yml | 2 +- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd69ed8..5cd01e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ defaults: env: SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' + SELECTED_KEYS_OTHER_ORDER: 'colon:in:Key sourceJavaVersion org.gradle.jvmargs unicode_escapes' SELECTED_SINGLE_NAME: 'my-prop' SELECTED_NAMES: 'java-version jvm_args unicode colon' @@ -285,6 +286,12 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' + - name: '[assert] test: query all properties as action outputs (additional "value" output not set)' + env: + RESULT_VALUE: ${{steps.all-output.outputs.value || 'U_N_S_E_T'}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT_VALUE" U_N_S_E_T ############################################################ - name: '[act] test: query multiple properties as action outputs' @@ -293,6 +300,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} @@ -329,6 +337,32 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + - name: '[assert] test: query multiple properties as action outputs (additional "value" output)' + env: + RESULT_VALUE: ${{steps.multi-output.outputs.value}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" + + ############################################################ + - name: '[act] test: query multiple properties as action outputs (in another keys order)' + id: multi-output-2 + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS_OTHER_ORDER}}' + - name: '[assert] test: query multiple properties as action outputs (in another keys order)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-2.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: *validateMultiImpl + - name: '[assert] test: query multiple properties as action outputs (in another keys order) (additional "value" output)' + env: + RESULT_VALUE: ${{steps.multi-output-2.outputs.value}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" + ############################################################ - name: '[act] test: query multiple properties as action outputs of given names' id: multi-output-named @@ -337,6 +371,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_NAMES}}' + - name: '[assert] test: query multiple properties as action outputs of given names' env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} @@ -359,6 +394,13 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash_1 + - name: '[assert] test: query multiple properties as action outputs (additional "value" output not set)' + env: + RESULT_VALUE: ${{steps.multi-output-named.outputs.value || 'U_N_S_E_T'}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT_VALUE" U_N_S_E_T + ############################################################ - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 @@ -367,6 +409,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' + - name: '[assert] test: query multiple properties as action outputs of given name' env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} @@ -393,6 +436,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' + - name: '[assert] test: query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} diff --git a/Action.java b/Action.java index 94e1091..7884ef9 100644 --- a/Action.java +++ b/Action.java @@ -28,9 +28,10 @@ static void main(String[] args) throws Exception { Config config = Config.fromEnv(); ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { - Properties properties = config.selectedKeys() == null ? Util.readProperties(file) - : Util.selectProperties(Util.readProperties(file), config.selectedKeys(), file); - resultWriter.write(properties, config); + Properties properties = Util.readProperties(file); + Properties selectedProperties = config.selectedKeys().map(keys -> Util.selectProperties(properties, keys, file)) + .orElse(properties); + resultWriter.write(selectedProperties, config); } } catch (OutputException e) { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { @@ -81,8 +82,8 @@ public String toString() { } -record Config(List selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, String resultTypeArg, - String resultNameSeparator, String outputPrefix) { +record Config(Optional> selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, + String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. String keySeparator = Util.getRequiredEnv(ConfigVariable.KEY_SEPARATOR); @@ -101,8 +102,8 @@ public static Config fromEnv() { String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); String outputPrefix = Util.getRequiredEnv(ConfigVariable.OUTPUT_PREFIX); - return new Config(keys.map(List::of).orElse(null), keySeparator, resultTypeWithArg, resultType, resultTypeArg, - resultNameSeparator, outputPrefix); + return new Config(keys.map(List::of), keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, + outputPrefix); } public String requiredResultTypeArg() { @@ -137,7 +138,7 @@ public void write(Properties props, Config config) throws IOException { // TODO props must be in order of config.selectedKeys() // Otherwise, this is arbitrary: - if (lastValue != null) { + if (config.selectedKeys().isPresent() && lastValue != null) { writer.write("value", lastValue); } } @@ -218,11 +219,9 @@ public static ResultWriter of(String inputName) { public abstract void write(Properties props, Config config) throws IOException; private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { - List selectedKeys = config.selectedKeys(); - if (selectedKeys == null) { - throw OutputException.forIllegalArgument( - "invalid use of " + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing keys)"); - } + List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " + + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + ConfigVariable.KEYS + ")")); + @SuppressWarnings("java:S3655") // Sonar rule: "Optional value should only be accessed after calling isPresent()". // requiredResultTypeArg() is not empty, so splitArray returns non-empty String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator()).get(); diff --git a/action.yml b/action.yml index 36386cc..412728c 100644 --- a/action.yml +++ b/action.yml @@ -27,7 +27,7 @@ inputs: Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name "a-002Eb-002Dc_d". - Additionally, set output "value" to the last found value, if any (which is only usefull if `keys` is non-empty, i.e. last given key wins; without keys, the last property in the undefined java.util.Properties iteration order wins). + Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. - "env": For each property key , set an environment variable . From 569e2088b9366e9fbd5db260ce460555d431f90f Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 21:56:21 +0200 Subject: [PATCH 090/190] refactor: move naming dependencies into one class --- Action.java | 130 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 49 deletions(-) diff --git a/Action.java b/Action.java index 7884ef9..3024912 100644 --- a/Action.java +++ b/Action.java @@ -35,8 +35,7 @@ static void main(String[] args) throws Exception { } } catch (OutputException e) { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - // This output is not documented - writer.write("error", e.getMessage()); + writer.write(Ids.OutputName.ERROR, e.getMessage()); } throw e; } @@ -45,39 +44,68 @@ static void main(String[] args) throws Exception { /** - * An exception for which an "error" output should be set. + * Identifiers which must match those specified in the action YAML. */ -class OutputException extends RuntimeException { - public OutputException(String message, Throwable cause) { - super(message, cause); +class Ids { + private Ids() {} + + /** + * Configuration environment variable names and their corresponding action input names for error messages. + */ + enum ConfigVariable { + FILE("file"), // + KEYS("keys"), // + RESULT_TYPE("resultType"), // + KEY_SEPARATOR("keySeparator"), // + RESULT_NAME_SEPARATOR("resultNameSeparator"), // + OUTPUT_PREFIX(null); + + public final String externalName; + + private ConfigVariable(String externalName) { + this.externalName = externalName; + } + + @Override + public String toString() { + return externalName; + } } - public static OutputException forIllegalArgument(String message) { - return new OutputException(message, new IllegalArgumentException(message)); + enum ResultWriterName { + OUTPUT("output"), // + OUTPUT_NAMED("output-named"), // + ENV_NAMED("env-named"), // + ENV("env"), // + JSON("json"), // + JSON_FILE("json-file"); + + public final String externalName; + + private ResultWriterName(String externalName) { + this.externalName = externalName; + } + } + + enum OutputName { + JSON, // + VALUE, // + ERROR; // This output is not documented. + public final String externalName = name().toLowerCase(); } } /** - * Configuration environment variable names and their corresponding action input names for error messages. + * An exception for which an "error" output should be set. */ -enum ConfigVariable { - FILE("file"), // - KEYS("keys"), // - RESULT_TYPE("resultType"), // - KEY_SEPARATOR("keySeparator"), // - RESULT_NAME_SEPARATOR("resultNameSeparator"), // - OUTPUT_PREFIX(null); - - public final String inputName; - - private ConfigVariable(String inputName) { - this.inputName = inputName; +class OutputException extends RuntimeException { + public OutputException(String message, Throwable cause) { + super(message, cause); } - @Override - public String toString() { - return inputName; + public static OutputException forIllegalArgument(String message) { + return new OutputException(message, new IllegalArgumentException(message)); } } @@ -86,22 +114,22 @@ record Config(Optional> selectedKeys, String keySeparator, String r String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. - String keySeparator = Util.getRequiredEnv(ConfigVariable.KEY_SEPARATOR); + String keySeparator = Util.getRequiredEnv(Ids.ConfigVariable.KEY_SEPARATOR); // System.getenv("KEYS") == null if set to empty string?! So this cannot be checked to be set if we want to allow empty // string: - String keysStr = System.getenv().getOrDefault(ConfigVariable.KEYS.name(), ""); + String keysStr = System.getenv().getOrDefault(Ids.ConfigVariable.KEYS.name(), ""); Optional keys = Util.splitArray(keysStr, keySeparator); - String resultNameSeparator = Util.getRequiredEnv(ConfigVariable.RESULT_NAME_SEPARATOR); + String resultNameSeparator = Util.getRequiredEnv(Ids.ConfigVariable.RESULT_NAME_SEPARATOR); // RESULT_TYPE format: "[:]" - String resultTypeWithArg = Util.getRequiredEnv(ConfigVariable.RESULT_TYPE); + String resultTypeWithArg = Util.getRequiredEnv(Ids.ConfigVariable.RESULT_TYPE); Matcher matcher = Pattern.compile("([^:]+)(?::(.*))?").matcher(resultTypeWithArg); if (!matcher.matches()) { - throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + resultTypeWithArg); + throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + resultTypeWithArg); } String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); - String outputPrefix = Util.getRequiredEnv(ConfigVariable.OUTPUT_PREFIX); + String outputPrefix = Util.getRequiredEnv(Ids.ConfigVariable.OUTPUT_PREFIX); return new Config(keys.map(List::of), keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); } @@ -110,7 +138,7 @@ public String requiredResultTypeArg() { String arg = resultTypeArg(); if ("".equals(arg)) { throw OutputException.forIllegalArgument( - "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (missing argument)"); + "invalid " + Ids.ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (missing argument)"); } return arg; } @@ -118,14 +146,14 @@ public String requiredResultTypeArg() { public void requireNoArg() { if (!"".equals(resultTypeArg())) { throw OutputException.forIllegalArgument( - "invalid " + ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (non-empty argument)"); + "invalid " + Ids.ConfigVariable.RESULT_TYPE + " " + resultTypeWithArg() + " (non-empty argument)"); } } } enum ResultWriter { - OUTPUT("output") { + OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override public void write(Properties props, Config config) throws IOException { String lastValue = null; @@ -139,7 +167,7 @@ public void write(Properties props, Config config) throws IOException { // TODO props must be in order of config.selectedKeys() // Otherwise, this is arbitrary: if (config.selectedKeys().isPresent() && lastValue != null) { - writer.write("value", lastValue); + writer.write(Ids.OutputName.VALUE, lastValue); } } } @@ -154,19 +182,19 @@ private static String encodeKey(Config config, String key) { return result.toString(); } }, - OUTPUT_NAMED("output-named") { + OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override public void write(Properties props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); } }, - ENV_NAMED("env-named") { + ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override public void write(Properties props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.ENV); } }, - ENV("env") { + ENV(Ids.ResultWriterName.ENV) { @Override public void write(Properties props, Config config) throws IOException { String prefix = config.resultTypeArg(); @@ -177,16 +205,16 @@ public void write(Properties props, Config config) throws IOException { } } }, - JSON("json") { + JSON(Ids.ResultWriterName.JSON) { @Override public void write(Properties props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - writer.write("json", Util.toJson(props)); + writer.write(Ids.OutputName.JSON, Util.toJson(props)); } } }, - JSON_FILE("json-file") { + JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override public void write(Properties props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); @@ -201,32 +229,32 @@ public void write(Properties props, Config config) throws IOException { } }; - private final String inputName; + private final String externalName; - private ResultWriter(String inputName) { - this.inputName = inputName; + private ResultWriter(Ids.ResultWriterName externalName) { + this.externalName = externalName.externalName; } - public static ResultWriter of(String inputName) { + public static ResultWriter of(String externalName) { for (ResultWriter rw : ResultWriter.values()) { - if (rw.inputName.equals(inputName)) { + if (rw.externalName.equals(externalName)) { return rw; } } - throw OutputException.forIllegalArgument("invalid " + ConfigVariable.RESULT_TYPE + ": " + inputName); + throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + externalName); } public abstract void write(Properties props, Config config) throws IOException; private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " - + ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + ConfigVariable.KEYS + ")")); + + Ids.ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + Ids.ConfigVariable.KEYS + ")")); @SuppressWarnings("java:S3655") // Sonar rule: "Optional value should only be accessed after calling isPresent()". // requiredResultTypeArg() is not empty, so splitArray returns non-empty String[] resultNames = Util.splitArray(config.requiredResultTypeArg(), config.resultNameSeparator()).get(); if (resultNames.length != selectedKeys.size() && resultNames.length != 1) { - throw OutputException.forIllegalArgument(ConfigVariable.RESULT_TYPE + " " + config.resultTypeWithArg() + " has " + throw OutputException.forIllegalArgument(Ids.ConfigVariable.RESULT_TYPE + " " + config.resultTypeWithArg() + " has " + resultNames.length + " arguments, but " + selectedKeys.size() + " keys are selected"); } try (GitHubVariableWriter writer = gitHubOutputFile.open()) { @@ -281,6 +309,10 @@ public void write(String key, String value) throws IOException { } } + public void write(Ids.OutputName key, String value) throws IOException { + write(key.externalName, value); + } + /** * Writes a key-value pair in * multiline @@ -333,7 +365,7 @@ public static String getRequiredEnv(String varName) { return value; } - public static String getRequiredEnv(ConfigVariable varName) { + public static String getRequiredEnv(Ids.ConfigVariable varName) { return getRequiredEnv(varName.name()); } From 95a7589d8663056fc0187fd42bb919e0f707a156 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Fri, 15 May 2026 21:58:02 +0200 Subject: [PATCH 091/190] fix resultType "output": obey selected keys order --- Action.java | 77 +++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 43 deletions(-) diff --git a/Action.java b/Action.java index 3024912..7120520 100644 --- a/Action.java +++ b/Action.java @@ -8,8 +8,7 @@ import java.nio.file.OpenOption; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.AbstractMap; -import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -29,8 +28,9 @@ static void main(String[] args) throws Exception { ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { Properties properties = Util.readProperties(file); - Properties selectedProperties = config.selectedKeys().map(keys -> Util.selectProperties(properties, keys, file)) - .orElse(properties); + Map selectedProperties = config.selectedKeys() + .map(keys -> Util.selectProperties(properties, keys, file)) // + .orElseGet(() -> Util.stringEntries(properties)); resultWriter.write(selectedProperties, config); } } catch (OutputException e) { @@ -155,17 +155,15 @@ public void requireNoArg() { enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - for (Map.Entry entry : Util.stringEntries(props)) { + for (Map.Entry entry : props.entrySet()) { String key = encodeKey(config, config.outputPrefix() + entry.getKey()); lastValue = entry.getValue(); writer.write(key, lastValue); } - // TODO props must be in order of config.selectedKeys() - // Otherwise, this is arbitrary: if (config.selectedKeys().isPresent() && lastValue != null) { writer.write(Ids.OutputName.VALUE, lastValue); } @@ -184,22 +182,22 @@ private static String encodeKey(Config config, String key) { }, OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); } }, ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.ENV); } }, ENV(Ids.ResultWriterName.ENV) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String prefix = config.resultTypeArg(); try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { - for (Map.Entry entry : Util.stringEntries(props)) { + for (Map.Entry entry : props.entrySet()) { writer.write(prefix + entry.getKey(), entry.getValue()); } } @@ -207,7 +205,7 @@ public void write(Properties props, Config config) throws IOException { }, JSON(Ids.ResultWriterName.JSON) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write(Ids.OutputName.JSON, Util.toJson(props)); @@ -216,7 +214,7 @@ public void write(Properties props, Config config) throws IOException { }, JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { @@ -244,9 +242,10 @@ public static ResultWriter of(String externalName) { throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + externalName); } - public abstract void write(Properties props, Config config) throws IOException; + public abstract void write(Map props, Config config) throws IOException; - private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { + private static void writeNamedImpl(Map props, Config config, GitHubOutputFile gitHubOutputFile) + throws IOException { List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " + Ids.ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + Ids.ConfigVariable.KEYS + ")")); @@ -260,7 +259,7 @@ private static void writeNamedImpl(Properties props, Config config, GitHubOutput try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; - String value = props.getProperty(selectedKeys.get(i)); + String value = props.get(selectedKeys.get(i)); writer.write(name, value); } } @@ -385,42 +384,34 @@ public static Properties readProperties(String file) throws IOException { return allProps; } - public static Properties selectProperties(Properties allProps, List selectedKeys, String file) { - Set selectedKeysSet = new LinkedHashSet<>(selectedKeys); - Properties props = new Properties(); - for (Map.Entry entry : stringEntries(allProps)) { - if (selectedKeysSet.contains(entry.getKey())) { - selectedKeysSet.remove(entry.getKey()); - props.setProperty(entry.getKey(), entry.getValue()); + public static Map selectProperties(Properties allProps, List selectedKeys, String file) { + Set unmatchedKeysSet = new LinkedHashSet<>(selectedKeys); + Map results = new LinkedHashMap<>(); + for (String key : selectedKeys) { + String value = allProps.getProperty(key); + if (value != null) { + results.put(key, value); + unmatchedKeysSet.remove(key); } } - for (String key : selectedKeysSet) { + for (String key : unmatchedKeysSet) { System.err.format("Property %s not found in %s%n", key, file); } - return props; + return results; } - public static Iterable> stringEntries(Properties props) { - return () -> new Iterator<>() { - private final Iterator keysIter = props.stringPropertyNames().iterator(); - - @Override - public Map.Entry next() { - String key = keysIter.next(); - return new AbstractMap.SimpleImmutableEntry<>(key, props.getProperty(key)); - } - - @Override - public boolean hasNext() { - return keysIter.hasNext(); - } - }; + public static Map stringEntries(Properties props) { + Map map = new LinkedHashMap<>(); + for (String key : props.stringPropertyNames()) { + map.put(key, props.getProperty(key)); + } + return map; } - public static String toJson(Properties props) { + public static String toJson(Map map) { StringBuilder s = new StringBuilder(50).append('{'); int initialLength = s.length(); - for (Map.Entry entry : stringEntries(props)) { + for (Map.Entry entry : map.entrySet()) { s.append(s.length() == initialLength ? '"' : ", \""); appendJsonString(s, entry.getKey()); s.append("\":\""); From a98e59b50fd3854a3f27448780f5675379b1b110 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 14:42:52 +0200 Subject: [PATCH 092/190] docs --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5330f0a..ae11dc9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,10 @@ Query all properties as action outputs: with: file: gradle.properties ``` -⇒ `${{steps.readProp.outputs._sourceJavaVersion}}` == `21`, `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, `${{steps.readProp.outputs._org.gradle.jvmargs}}` == `-ea -showversion`. +⇒ \ + `${{steps.readProp.outputs._sourceJavaVersion}}` == `21`, \ + `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, \ + `${{steps.readProp.outputs._org-002Egradle-002Ejvmargs}}` == `-ea -showversion`. Query multiple properties as action outputs: ``` @@ -21,7 +24,7 @@ Query multiple properties as action outputs: uses: freenet-actions/read-java-properties@v1 with: file: gradle.properties - keys: sourceJavaVersion,targetJavaVersion + keys: 'sourceJavaVersion targetJavaVersion' ``` or with another key separator: ``` @@ -29,10 +32,13 @@ or with another key separator: uses: freenet-actions/read-java-properties@v1 with: file: gradle.properties - keys: "sourceJavaVersion; targetJavaVersion" - keySeparator: "; " + keys: sourceJavaVersion,targetJavaVersion + keySeparator: , ``` -⇒ `${{steps.readProp.outputs._sourceJavaVersion}}` == `21`, `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, `${{steps.readProp.outputs.value}}` == `17`. +⇒ \ + `${{steps.readProp.outputs._sourceJavaVersion}}` == `21`, \ + `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, \ + `${{steps.readProp.outputs.value}}` == `17`. Query multiple properties as action output of which only one is found: ``` @@ -40,9 +46,11 @@ Query multiple properties as action output of which only one is found: uses: freenet-actions/read-java-properties@v1 with: file: gradle.properties - keys: foo,targetJavaVersion,bar + keys: 'foo targetJavaVersion bar' ``` -⇒ `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, `${{steps.readProp.outputs.value}}` == `17`. +⇒ \ + `${{steps.readProp.outputs._targetJavaVersion}}` == `17`, \ + `${{steps.readProp.outputs.value}}` == `17`. Query a single property as action output: ``` @@ -52,7 +60,19 @@ Query a single property as action output: file: gradle.properties keys: org.gradle.jvmargs ``` -⇒ `${{steps.readProp.outputs.org.gradle.jvmargs}}` == `-ea -showversion`, `${{steps.readProp.outputs.value}}` == `-ea -showversion`. +⇒ \ + `${{steps.readProp.outputs.org.gradle.jvmargs}}` == `-ea -showversion`, \ + `${{steps.readProp.outputs.value}}` == `-ea -showversion`. + +Query multiple (alternative) properties as a single action output. In other words, query a property with a fallback to another property. +``` +- uses: freenet-actions/read-java-properties@v1 + with: + file: gradle.properties + keys: sourceJavaVersion,targetJavaVersion + resultType: 'output-named:_javaVersion' +``` +⇒ `${{steps.readProp.outputs._javaVersion}}` == `17`. Query all properties as environment variables of the same names: ``` @@ -61,7 +81,10 @@ Query all properties as environment variables of the same names: file: gradle.properties resultType: env ``` -⇒ Environment variables `sourceJavaVersion` == `21`, `targetJavaVersion` == `17`, `org.gradle.jvmargs` == `-ea -showversion`. +⇒ Environment variables \ + `sourceJavaVersion` == `21`, \ + `targetJavaVersion` == `17`, \ + `org.gradle.jvmargs` == `-ea -showversion`. Query all properties as environment variables with prefix: ``` @@ -70,7 +93,10 @@ Query all properties as environment variables with prefix: file: gradle.properties resultType: "env:GRADLE_PROP_" ``` -⇒ Environment variables `GRADLE_PROP_sourceJavaVersion` == `21`, `GRADLE_PROP_targetJavaVersion` == `17`, `GRADLE_PROP_org.gradle.jvmargs` == `-ea -showversion`. +⇒ Environment variables \ + `GRADLE_PROP_sourceJavaVersion` == `21`, \ + `GRADLE_PROP_targetJavaVersion` == `17`, \ + `GRADLE_PROP_org.gradle.jvmargs` == `-ea -showversion`. Query multiple properties as environment variables: ``` @@ -80,7 +106,9 @@ Query multiple properties as environment variables: keys: sourceJavaVersion,targetJavaVersion resultType: env ``` -⇒ Environment variables `sourceJavaVersion` == `21`, `targetJavaVersion` == `17`. +⇒ Environment variables \ + `sourceJavaVersion` == `21`, \ + `targetJavaVersion` == `17`. Query multiple properties as environment variables of given names: ``` @@ -90,9 +118,11 @@ Query multiple properties as environment variables of given names: keys: sourceJavaVersion,targetJavaVersion resultType: env-named:SOURCE_JAVA_VERSION,TARGET_JAVA_VERSION ``` -⇒ Environment variables `SOURCE_JAVA_VERSION` == `21`, `TARGET_JAVA_VERSION` == `17`. +⇒ Environment variables \ + `SOURCE_JAVA_VERSION` == `21`, \ + `TARGET_JAVA_VERSION` == `17`. -Query multiple (alternative) properties as a single environment variable: +Query multiple (alternative) properties as a single environment variable. In other words, query a property with a fallback to another property. ``` - uses: freenet-actions/read-java-properties@v1 with: @@ -110,7 +140,8 @@ Query all properties as JSON action output: file: gradle.properties resultType: json ``` -⇒ `${{steps.readProp.outputs.json}}` == `{"sourceJavaVersion": "21", "targetJavaVersion": "17", "org.gradle.jvmargs": "-ea -showversion"}` (or similar; order not guaranteed). +⇒ `${{steps.readProp.outputs.json}}` == `{"sourceJavaVersion": "21", "targetJavaVersion": "17", "org.gradle.jvmargs": "-ea -showversion"}` \ +(or similar; order not guaranteed). Query all properties as JSON file: ``` @@ -120,4 +151,6 @@ Query all properties as JSON file: file: gradle.properties resultType: json-file:/tmp/gradle-properties.json ``` -⇒ File /tmp/gradle-properties.json contains `{"sourceJavaVersion": "21", "targetJavaVersion": "17", "org.gradle.jvmargs": "-ea -showversion"}` (or similar; order and formatting not guaranteed). +⇒ File /tmp/gradle-properties.json contains \ + `{"sourceJavaVersion": "21", "targetJavaVersion": "17", "org.gradle.jvmargs": "-ea -showversion"}` \ +(or similar; order and formatting not guaranteed). From 020a759a126c2f2d2a878c27b4d80569e8e31b66 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 14:44:06 +0200 Subject: [PATCH 093/190] fix resultType "output": obey selected keys order --- Action.java | 80 +++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/Action.java b/Action.java index 3024912..c837f72 100644 --- a/Action.java +++ b/Action.java @@ -8,8 +8,7 @@ import java.nio.file.OpenOption; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.AbstractMap; -import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -29,8 +28,9 @@ static void main(String[] args) throws Exception { ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { Properties properties = Util.readProperties(file); - Properties selectedProperties = config.selectedKeys().map(keys -> Util.selectProperties(properties, keys, file)) - .orElse(properties); + Map selectedProperties = config.selectedKeys() + .map(keys -> Util.selectProperties(properties, keys, file)) // + .orElseGet(() -> Util.stringEntries(properties)); resultWriter.write(selectedProperties, config); } } catch (OutputException e) { @@ -155,17 +155,15 @@ public void requireNoArg() { enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - for (Map.Entry entry : Util.stringEntries(props)) { + for (Map.Entry entry : props.entrySet()) { String key = encodeKey(config, config.outputPrefix() + entry.getKey()); lastValue = entry.getValue(); writer.write(key, lastValue); } - // TODO props must be in order of config.selectedKeys() - // Otherwise, this is arbitrary: if (config.selectedKeys().isPresent() && lastValue != null) { writer.write(Ids.OutputName.VALUE, lastValue); } @@ -184,22 +182,22 @@ private static String encodeKey(Config config, String key) { }, OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); } }, ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { writeNamedImpl(props, config, GitHubOutputFile.ENV); } }, ENV(Ids.ResultWriterName.ENV) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String prefix = config.resultTypeArg(); try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { - for (Map.Entry entry : Util.stringEntries(props)) { + for (Map.Entry entry : props.entrySet()) { writer.write(prefix + entry.getKey(), entry.getValue()); } } @@ -207,7 +205,7 @@ public void write(Properties props, Config config) throws IOException { }, JSON(Ids.ResultWriterName.JSON) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write(Ids.OutputName.JSON, Util.toJson(props)); @@ -216,7 +214,7 @@ public void write(Properties props, Config config) throws IOException { }, JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override - public void write(Properties props, Config config) throws IOException { + public void write(Map props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { @@ -244,9 +242,10 @@ public static ResultWriter of(String externalName) { throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + externalName); } - public abstract void write(Properties props, Config config) throws IOException; + public abstract void write(Map props, Config config) throws IOException; - private static void writeNamedImpl(Properties props, Config config, GitHubOutputFile gitHubOutputFile) throws IOException { + private static void writeNamedImpl(Map props, Config config, GitHubOutputFile gitHubOutputFile) + throws IOException { List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " + Ids.ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + Ids.ConfigVariable.KEYS + ")")); @@ -260,7 +259,7 @@ private static void writeNamedImpl(Properties props, Config config, GitHubOutput try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; - String value = props.getProperty(selectedKeys.get(i)); + String value = props.get(selectedKeys.get(i)); writer.write(name, value); } } @@ -385,42 +384,37 @@ public static Properties readProperties(String file) throws IOException { return allProps; } - public static Properties selectProperties(Properties allProps, List selectedKeys, String file) { - Set selectedKeysSet = new LinkedHashSet<>(selectedKeys); - Properties props = new Properties(); - for (Map.Entry entry : stringEntries(allProps)) { - if (selectedKeysSet.contains(entry.getKey())) { - selectedKeysSet.remove(entry.getKey()); - props.setProperty(entry.getKey(), entry.getValue()); + /** + * Selects the properties with the given keys and returns them as a Map with the corresponding iteration order. + */ + public static Map selectProperties(Properties allProps, List selectedKeys, String file) { + Set unmatchedKeysSet = new LinkedHashSet<>(selectedKeys); + Map results = new LinkedHashMap<>(); + for (String key : selectedKeys) { + String value = allProps.getProperty(key); + if (value != null) { + results.put(key, value); + unmatchedKeysSet.remove(key); } } - for (String key : selectedKeysSet) { + for (String key : unmatchedKeysSet) { System.err.format("Property %s not found in %s%n", key, file); } - return props; + return results; } - public static Iterable> stringEntries(Properties props) { - return () -> new Iterator<>() { - private final Iterator keysIter = props.stringPropertyNames().iterator(); - - @Override - public Map.Entry next() { - String key = keysIter.next(); - return new AbstractMap.SimpleImmutableEntry<>(key, props.getProperty(key)); - } - - @Override - public boolean hasNext() { - return keysIter.hasNext(); - } - }; + public static Map stringEntries(Properties props) { + Map map = new LinkedHashMap<>(); + for (String key : props.stringPropertyNames()) { + map.put(key, props.getProperty(key)); + } + return map; } - public static String toJson(Properties props) { + public static String toJson(Map map) { StringBuilder s = new StringBuilder(50).append('{'); int initialLength = s.length(); - for (Map.Entry entry : stringEntries(props)) { + for (Map.Entry entry : map.entrySet()) { s.append(s.length() == initialLength ? '"' : ", \""); appendJsonString(s, entry.getKey()); s.append("\":\""); From 90c6d168c13e2d88452e9042e97292a59f891678 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 14:56:07 +0200 Subject: [PATCH 094/190] test: add tests for non-default keySeparator, resultNameSeparator --- .github/workflows/test.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cd01e4..998b440 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -344,6 +344,20 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" + ############################################################ + - name: '[act] test: query multiple properties as action outputs (different keySeparator)' + id: multi-output-keySeparator + uses: ./ + with: + file: test/resources/test.properties + keySeparator: ':/ ' + keys: 'sourceJavaVersion:/ org.gradle.jvmargs:/ unicode_escapes:/ colon:in:Key' + - name: '[assert] test: query multiple properties as action outputs (different keySeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: *validateMultiImpl + ############################################################ - name: '[act] test: query multiple properties as action outputs (in another keys order)' id: multi-output-2 @@ -401,6 +415,21 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T + ############################################################ + - name: '[act] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + id: multi-output-named-resultNameSeparator + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultNameSeparator: '- -' + resultType: 'java-version- -jvm_args- -unicode- -colon' + - name: '[assert] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' + run: *validateMultiNamedImpl + ############################################################ - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 From e12c81290e904e41e9606732c1a5f5f64369d4d5 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:05:03 +0200 Subject: [PATCH 095/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 998b440..66b9f84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -350,14 +350,28 @@ jobs: uses: ./ with: file: test/resources/test.properties - keySeparator: ':/ ' - keys: 'sourceJavaVersion:/ org.gradle.jvmargs:/ unicode_escapes:/ colon:in:Key' + keySeparator: '?' # something with a special meaning in regex; should be treated as literal + keys: 'sourceJavaVersion?org.gradle.jvmargs?unicode_escapes?colon:in:Key' - name: '[assert] test: query multiple properties as action outputs (different keySeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl + ############################################################ + - name: '[act] test: query multiple properties as action outputs (different multi-char keySeparator)' + id: multi-output-keySeparatorMC + uses: ./ + with: + file: test/resources/test.properties + keySeparator: ':/ ' + keys: 'sourceJavaVersion:/ org.gradle.jvmargs:/ unicode_escapes:/ colon:in:Key' + - name: '[assert] test: query multiple properties as action outputs (different multi-char keySeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-keySeparatorMC.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: *validateMultiImpl + ############################################################ - name: '[act] test: query multiple properties as action outputs (in another keys order)' id: multi-output-2 @@ -422,14 +436,29 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultNameSeparator: '- -' - resultType: 'java-version- -jvm_args- -unicode- -colon' + resultNameSeparator: '?' # something with a special meaning in regex; should be treated as literal + resultType: 'java-version?jvm_args?unicode?colon' - name: '[assert] test: query multiple properties as action outputs of given names (different resultNameSeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl + ############################################################ + - name: '[act] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + id: multi-output-named-resultNameSeparatorMC + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultNameSeparator: '- -' + resultType: 'output-named:java-version- -jvm_args- -unicode- -colon' + - name: '[assert] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparatorMC.outputs)}} + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' + run: *validateMultiNamedImpl + ############################################################ - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 From 37955cb24cd2c70e17d9044257eeeeb180d44a34 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:11:22 +0200 Subject: [PATCH 096/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66b9f84..8355c3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -437,7 +437,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultNameSeparator: '?' # something with a special meaning in regex; should be treated as literal - resultType: 'java-version?jvm_args?unicode?colon' + resultType: 'output-named:java-version?jvm_args?unicode?colon' - name: '[assert] test: query multiple properties as action outputs of given names (different resultNameSeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} From ee203245bcb66636ade60c6dd86483e34d3212dd Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:15:34 +0200 Subject: [PATCH 097/190] test: add tests for non-default keySeparator, resultNameSeparator --- .github/workflows/test.yml | 58 ++++++++++++++++++++++++++++++++++++++ Action.java | 3 ++ 2 files changed, 61 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cd01e4..8355c3f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -344,6 +344,34 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" + ############################################################ + - name: '[act] test: query multiple properties as action outputs (different keySeparator)' + id: multi-output-keySeparator + uses: ./ + with: + file: test/resources/test.properties + keySeparator: '?' # something with a special meaning in regex; should be treated as literal + keys: 'sourceJavaVersion?org.gradle.jvmargs?unicode_escapes?colon:in:Key' + - name: '[assert] test: query multiple properties as action outputs (different keySeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: *validateMultiImpl + + ############################################################ + - name: '[act] test: query multiple properties as action outputs (different multi-char keySeparator)' + id: multi-output-keySeparatorMC + uses: ./ + with: + file: test/resources/test.properties + keySeparator: ':/ ' + keys: 'sourceJavaVersion:/ org.gradle.jvmargs:/ unicode_escapes:/ colon:in:Key' + - name: '[assert] test: query multiple properties as action outputs (different multi-char keySeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-keySeparatorMC.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: *validateMultiImpl + ############################################################ - name: '[act] test: query multiple properties as action outputs (in another keys order)' id: multi-output-2 @@ -401,6 +429,36 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T + ############################################################ + - name: '[act] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + id: multi-output-named-resultNameSeparator + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultNameSeparator: '?' # something with a special meaning in regex; should be treated as literal + resultType: 'output-named:java-version?jvm_args?unicode?colon' + - name: '[assert] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' + run: *validateMultiNamedImpl + + ############################################################ + - name: '[act] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + id: multi-output-named-resultNameSeparatorMC + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultNameSeparator: '- -' + resultType: 'output-named:java-version- -jvm_args- -unicode- -colon' + - name: '[assert] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + env: + RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparatorMC.outputs)}} + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' + run: *validateMultiNamedImpl + ############################################################ - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 diff --git a/Action.java b/Action.java index 7120520..c837f72 100644 --- a/Action.java +++ b/Action.java @@ -384,6 +384,9 @@ public static Properties readProperties(String file) throws IOException { return allProps; } + /** + * Selects the properties with the given keys and returns them as a Map with the corresponding iteration order. + */ public static Map selectProperties(Properties allProps, List selectedKeys, String file) { Set unmatchedKeysSet = new LinkedHashSet<>(selectedKeys); Map results = new LinkedHashMap<>(); From c18e178456ff44e7fdf9ad610939f92c69752e65 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:25:34 +0200 Subject: [PATCH 098/190] security: remove value logging + minor simplification of GitHubOutputFile ctor --- Action.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Action.java b/Action.java index c837f72..733d687 100644 --- a/Action.java +++ b/Action.java @@ -219,7 +219,7 @@ public void write(Map props, Config config) throws IOException { Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); - System.err.format("writing to %s: %s%n", outputFile, jsonResult); + System.err.format("writing JSON for %s properties to %s%n", props.size(), outputFile); writer.write(jsonResult); writer.write('\n'); writer.flush(); @@ -268,17 +268,17 @@ private static void writeNamedImpl(Map props, Config config, Git enum GitHubOutputFile { - OUTPUT("GITHUB_OUTPUT"), // - ENV("GITHUB_ENV"); + OUTPUT, ENV; final String fileName; - private GitHubOutputFile(String fileNameEnvVar) { + private GitHubOutputFile() { + String fileNameEnvVar = "GITHUB_" + name(); this.fileName = Util.getRequiredEnv(fileNameEnvVar); } public GitHubVariableWriter open() throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName); + return new GitHubVariableWriter(this.toString().toLowerCase(), fileName); } } @@ -295,7 +295,7 @@ public GitHubVariableWriter(String description, String fileName) throws IOExcept } public void write(String key, String value) throws IOException { - System.err.format("%s %s\t:= \"%s\"%n", description, key, value); + System.err.format("setting %s %s%n", description, key); // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { From 88935d90836af93e7815516c3c86ab63387e4232 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:32:13 +0200 Subject: [PATCH 099/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Action.java b/Action.java index 733d687..fd8e64f 100644 --- a/Action.java +++ b/Action.java @@ -268,17 +268,20 @@ private static void writeNamedImpl(Map props, Config config, Git enum GitHubOutputFile { - OUTPUT, ENV; + OUTPUT("output"), // + ENV("environment variable"); - final String fileName; + private final String description; + private final String fileName; - private GitHubOutputFile() { + private GitHubOutputFile(String description) { + this.description = description; String fileNameEnvVar = "GITHUB_" + name(); this.fileName = Util.getRequiredEnv(fileNameEnvVar); } public GitHubVariableWriter open() throws IOException { - return new GitHubVariableWriter(this.toString().toLowerCase(), fileName); + return new GitHubVariableWriter(description, fileName); } } From e700279ab038514d55d2f950595630da6afcff34 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:39:33 +0200 Subject: [PATCH 100/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Action.java b/Action.java index fd8e64f..063ebfd 100644 --- a/Action.java +++ b/Action.java @@ -268,16 +268,15 @@ private static void writeNamedImpl(Map props, Config config, Git enum GitHubOutputFile { - OUTPUT("output"), // - ENV("environment variable"); + OUTPUT("GITHUB_OUTPUT", "output"), // + ENV("GITHUB_ENV", "environment variable"); - private final String description; private final String fileName; + private final String description; - private GitHubOutputFile(String description) { - this.description = description; - String fileNameEnvVar = "GITHUB_" + name(); + private GitHubOutputFile(String fileNameEnvVar, String description) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); + this.description = description; } public GitHubVariableWriter open() throws IOException { From b537b22f256c9d64d6104da66134c961cf727b90 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:40:56 +0200 Subject: [PATCH 101/190] security: remove value logging --- Action.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Action.java b/Action.java index c837f72..063ebfd 100644 --- a/Action.java +++ b/Action.java @@ -219,7 +219,7 @@ public void write(Map props, Config config) throws IOException { Files.createDirectories((Paths.get(outputFile).getParent())); try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); - System.err.format("writing to %s: %s%n", outputFile, jsonResult); + System.err.format("writing JSON for %s properties to %s%n", props.size(), outputFile); writer.write(jsonResult); writer.write('\n'); writer.flush(); @@ -268,17 +268,19 @@ private static void writeNamedImpl(Map props, Config config, Git enum GitHubOutputFile { - OUTPUT("GITHUB_OUTPUT"), // - ENV("GITHUB_ENV"); + OUTPUT("GITHUB_OUTPUT", "output"), // + ENV("GITHUB_ENV", "environment variable"); - final String fileName; + private final String fileName; + private final String description; - private GitHubOutputFile(String fileNameEnvVar) { + private GitHubOutputFile(String fileNameEnvVar, String description) { this.fileName = Util.getRequiredEnv(fileNameEnvVar); + this.description = description; } public GitHubVariableWriter open() throws IOException { - return new GitHubVariableWriter(this.toString().replaceFirst("^GITHUB_", "").toLowerCase(), fileName); + return new GitHubVariableWriter(description, fileName); } } @@ -295,7 +297,7 @@ public GitHubVariableWriter(String description, String fileName) throws IOExcept } public void write(String key, String value) throws IOException { - System.err.format("%s %s\t:= \"%s\"%n", description, key, value); + System.err.format("setting %s %s%n", description, key); // write (very) simple values in format "=": if (SIMPLE_VALUE.matcher(value).matches()) { From 9d6e2d9da095965956a5151acf0a1142a49473d3 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 15:48:38 +0200 Subject: [PATCH 102/190] docs --- action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yml b/action.yml index 412728c..97e5ed6 100644 --- a/action.yml +++ b/action.yml @@ -31,6 +31,7 @@ inputs: - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. - "env": For each property key , set an environment variable . + In order not to pollute the environment with hard to understand variables, this should only be used to set some specifically named variables. I.e. you know that the property file can only contain such properties or you are selecting only such properties with `keys`. - "env:": For each property key , set an environment variable . - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. - "env-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same environment variable; the last found key wins. In this mode, `keys` is required. From 9b97f5050be102d29fa65f51fee1d5d31b015a6a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:04:14 +0200 Subject: [PATCH 103/190] add aggregated test job as a GitHub status check; docs --- .github/workflows/test.yml | 26 ++++++++++++++++++++++++++ action.yml | 1 + 2 files changed, 27 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8355c3f..b86d206 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,7 +123,33 @@ env: } jobs: + # Similar to [https://github.com/orgs/community/discussions/12395#discussioncomment-12970019], this step + # aggregates all job states, so that we need only this job as a status check in the GitHub repository settings. test: + needs: + - test-misc + - test-all-env + - test-multi-env + - test-multi-env-named + - test-multi-env-named1 + - test-single-env + - test-all-env-with-prefix + - test-multi-env-with-prefix + - test-single-env-with-prefix + if: always() + runs-on: ubuntu-latest + steps: + - name: check overall status + env: + JOBS_TO_CHECK_JSON: ${{toJSON(needs)}} + run: |- + printf '$JOBS_TO_CHECK_JSON pretty-printed: %s\n' "$(jq --tab <<<"$JOBS_TO_CHECK_JSON")" + jq --raw-output ' + [to_entries[] | select(.value.result != "success")] + | map((.key + " has status " + (.value.result//"null") + "\n") | halt_error(1))[] + ' <<<"$JOBS_TO_CHECK_JSON" + + test-misc: runs-on: ubuntu-latest steps: - &checkoutStep diff --git a/action.yml b/action.yml index 412728c..97e5ed6 100644 --- a/action.yml +++ b/action.yml @@ -31,6 +31,7 @@ inputs: - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. - "env": For each property key , set an environment variable . + In order not to pollute the environment with hard to understand variables, this should only be used to set some specifically named variables. I.e. you know that the property file can only contain such properties or you are selecting only such properties with `keys`. - "env:": For each property key , set an environment variable . - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. - "env-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same environment variable; the last found key wins. In this mode, `keys` is required. From 2c66655f7bac3adfcdf576bccf3f37b9f3ad3d46 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:06:24 +0200 Subject: [PATCH 104/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b86d206..d40831f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -722,7 +722,7 @@ jobs: resultType: env - name: '[assert] test: query single property as environment variable' env: - RESULT_GETTER: getEnv + RESULT_GETTER: getEnvXXXXXXXXXXXXX run: *validateSingleImpl ############################################################ From 1ce45776de4db3dcc429fee0cf696c7d69ffe1e3 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:09:48 +0200 Subject: [PATCH 105/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d40831f..8505961 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -143,7 +143,7 @@ jobs: env: JOBS_TO_CHECK_JSON: ${{toJSON(needs)}} run: |- - printf '$JOBS_TO_CHECK_JSON pretty-printed: %s\n' "$(jq --tab <<<"$JOBS_TO_CHECK_JSON")" + printf '$JOBS_TO_CHECK_JSON == "%s"\n' "$JOBS_TO_CHECK_JSON" >&2 jq --raw-output ' [to_entries[] | select(.value.result != "success")] | map((.key + " has status " + (.value.result//"null") + "\n") | halt_error(1))[] From 26826ca8f7b263dda8c0f52fd318a74176ec2f75 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:12:16 +0200 Subject: [PATCH 106/190] =?UTF-8?q?Revert=20"=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2c66655f7bac3adfcdf576bccf3f37b9f3ad3d46. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8505961..c310f9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -722,7 +722,7 @@ jobs: resultType: env - name: '[assert] test: query single property as environment variable' env: - RESULT_GETTER: getEnvXXXXXXXXXXXXX + RESULT_GETTER: getEnv run: *validateSingleImpl ############################################################ From 8fd74374498fb4151e520238d59d402a046b2d19 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:12:56 +0200 Subject: [PATCH 107/190] add aggregated test job as a GitHub status check --- .github/workflows/test.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8355c3f..c310f9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,7 +123,33 @@ env: } jobs: + # Similar to [https://github.com/orgs/community/discussions/12395#discussioncomment-12970019], this step + # aggregates all job states, so that we need only this job as a status check in the GitHub repository settings. test: + needs: + - test-misc + - test-all-env + - test-multi-env + - test-multi-env-named + - test-multi-env-named1 + - test-single-env + - test-all-env-with-prefix + - test-multi-env-with-prefix + - test-single-env-with-prefix + if: always() + runs-on: ubuntu-latest + steps: + - name: check overall status + env: + JOBS_TO_CHECK_JSON: ${{toJSON(needs)}} + run: |- + printf '$JOBS_TO_CHECK_JSON == "%s"\n' "$JOBS_TO_CHECK_JSON" >&2 + jq --raw-output ' + [to_entries[] | select(.value.result != "success")] + | map((.key + " has status " + (.value.result//"null") + "\n") | halt_error(1))[] + ' <<<"$JOBS_TO_CHECK_JSON" + + test-misc: runs-on: ubuntu-latest steps: - &checkoutStep From 6660e79222fe853d255b23c8f80f35801260fead Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:40:48 +0200 Subject: [PATCH 108/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 5 ++++- Action.java | 8 ++++---- README.md | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c310f9f..a8efead 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -632,7 +632,10 @@ jobs: # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. run: |- - printf 'file=%s\n' "$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" + # Remove leading "./" ⇒ Also test the case where given file has "no parent" (i.e. Java method + # java.nio.file.Path.getParent() returns null). + file=$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json | sed 's=^\./==') + printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" - name: '[act] test: query all properties as JSON file' uses: ./ with: diff --git a/Action.java b/Action.java index 063ebfd..b81e7bf 100644 --- a/Action.java +++ b/Action.java @@ -159,7 +159,7 @@ public void write(Map props, Config config) throws IOException { String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { for (Map.Entry entry : props.entrySet()) { - String key = encodeKey(config, config.outputPrefix() + entry.getKey()); + String key = encodeKey(config.outputPrefix() + entry.getKey()); lastValue = entry.getValue(); writer.write(key, lastValue); } @@ -170,8 +170,8 @@ public void write(Map props, Config config) throws IOException { } } - private static String encodeKey(Config config, String key) { - StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); + private static String encodeKey(String key) { + StringBuilder result = new StringBuilder(key.length() + 4); Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); while (matcher.find()) { matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); @@ -419,7 +419,7 @@ public static String toJson(Map map) { for (Map.Entry entry : map.entrySet()) { s.append(s.length() == initialLength ? '"' : ", \""); appendJsonString(s, entry.getKey()); - s.append("\":\""); + s.append("\": \""); appendJsonString(s, entry.getValue()); s.append('"'); } diff --git a/README.md b/README.md index ae11dc9..69281a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # read-java-properties -Github Action to read a Java .properties file and output one, multiple, or all properties as plain strings or JSON. +GitHub Action to read a Java .properties file and output one, multiple, or all properties as plain strings or JSON. ## Usage example: @@ -61,7 +61,7 @@ Query a single property as action output: keys: org.gradle.jvmargs ``` ⇒ \ - `${{steps.readProp.outputs.org.gradle.jvmargs}}` == `-ea -showversion`, \ + `${{steps.readProp.outputs.org-002Egradle-002Ejvmargs}}` == `-ea -showversion`, \ `${{steps.readProp.outputs.value}}` == `-ea -showversion`. Query multiple (alternative) properties as a single action output. In other words, query a property with a fallback to another property. From 4204e5cd8a21f5fb6b374c7c4bb8ae6924ab800b Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:46:21 +0200 Subject: [PATCH 109/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Action.java b/Action.java index b81e7bf..bc63285 100644 --- a/Action.java +++ b/Action.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.OpenOption; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.LinkedHashMap; @@ -216,7 +217,10 @@ public void write(Map props, Config config) throws IOException { @Override public void write(Map props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); - Files.createDirectories((Paths.get(outputFile).getParent())); + Path parentDir = Paths.get(outputFile).getParent(); + if (parentDir != null) { + Files.createDirectories(parentDir); + } try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); System.err.format("writing JSON for %s properties to %s%n", props.size(), outputFile); From b038095ee1055468187c18c10a2db88c8da3a033 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:52:05 +0200 Subject: [PATCH 110/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 +++ test/resources/test.properties | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8efead..58b6793 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -290,6 +290,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" 'space in Key' 'sp' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' + evalAndAssertEquals "$RESULT_GETTER" 'back\slash' 'back\slash\value' evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' evalAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' evalAndAssertEquals "$RESULT_GETTER" empty '' @@ -341,6 +342,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue evalAndAssertUndefined "$RESULT_GETTER" empty @@ -534,6 +536,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue evalAndAssertUndefined "$RESULT_GETTER" empty diff --git a/test/resources/test.properties b/test/resources/test.properties index 9bb4b82..3933a50 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -5,6 +5,7 @@ org.gradle.jvmargs: -ea -showversion space\ in\ Key: sp colon\:in\:Key: c colonInValue: cv: +back\\slash: back\\slash\\value dash-in-Key: d dashInValue: d- empty: From 4190a1936515dfe916059f55e50163d50f4df608 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 17:57:32 +0200 Subject: [PATCH 111/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Action.java b/Action.java index bc63285..e489ede 100644 --- a/Action.java +++ b/Action.java @@ -434,6 +434,8 @@ private static void appendJsonString(StringBuilder buffer, String s) { for (char c : s.toCharArray()) { if (c == ' ') { buffer.append(c); + } else if (c == '\\') { + buffer.append('\\').append('\\'); } else if (c == '"') { buffer.append('\\').append('"'); } else if (c == '\t') { From 08f64cb2af2863dea54183da14df380a95bb5e9b Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 18:56:15 +0200 Subject: [PATCH 112/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- action.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Action.java b/Action.java index e489ede..d57e716 100644 --- a/Action.java +++ b/Action.java @@ -264,7 +264,7 @@ private static void writeNamedImpl(Map props, Config config, Git for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; String value = props.get(selectedKeys.get(i)); - writer.write(name, value); + writer.write(name, value != null ? value : ""); } } } diff --git a/action.yml b/action.yml index 97e5ed6..efa1af4 100644 --- a/action.yml +++ b/action.yml @@ -28,8 +28,8 @@ inputs: of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name "a-002Eb-002Dc_d". Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. + - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) An output is set to the empty string if the corresponding selected key does not exist in the properties. + - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. The output is set to the empty string if no selected key exists in the properties. - "env": For each property key , set an environment variable . In order not to pollute the environment with hard to understand variables, this should only be used to set some specifically named variables. I.e. you know that the property file can only contain such properties or you are selecting only such properties with `keys`. - "env:": For each property key , set an environment variable . From 087a30f5648c0462c9e6137bdade473b3cd5b078 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Mon, 18 May 2026 19:01:42 +0200 Subject: [PATCH 113/190] misc fixes (GitHub Copilot review) - fix backslash escaping in JSON - fix misleading StringBuilder initial capacity - fix NPE for missing properties (write empty output / env var) - fix NPE if file is relative (and without leading "./") - JSON formatting: add space before value (as documented) - fix docs: encoded output name --- .github/workflows/test.yml | 8 +++++++- Action.java | 18 ++++++++++++------ README.md | 4 ++-- test/resources/test.properties | 1 + 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c310f9f..58b6793 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -290,6 +290,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" 'space in Key' 'sp' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' evalAndAssertEquals "$RESULT_GETTER" colonInValue 'cv:' + evalAndAssertEquals "$RESULT_GETTER" 'back\slash' 'back\slash\value' evalAndAssertEquals "$RESULT_GETTER" dash-in-Key 'd' evalAndAssertEquals "$RESULT_GETTER" dashInValue 'd-' evalAndAssertEquals "$RESULT_GETTER" empty '' @@ -341,6 +342,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue evalAndAssertUndefined "$RESULT_GETTER" empty @@ -534,6 +536,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key evalAndAssertUndefined "$RESULT_GETTER" dashInValue evalAndAssertUndefined "$RESULT_GETTER" empty @@ -632,7 +635,10 @@ jobs: # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. run: |- - printf 'file=%s\n' "$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json)" | tee -- "$GITHUB_OUTPUT" + # Remove leading "./" ⇒ Also test the case where given file has "no parent" (i.e. Java method + # java.nio.file.Path.getParent() returns null). + file=$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json | sed 's=^\./==') + printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" - name: '[act] test: query all properties as JSON file' uses: ./ with: diff --git a/Action.java b/Action.java index 063ebfd..d57e716 100644 --- a/Action.java +++ b/Action.java @@ -6,6 +6,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.OpenOption; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.LinkedHashMap; @@ -159,7 +160,7 @@ public void write(Map props, Config config) throws IOException { String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { for (Map.Entry entry : props.entrySet()) { - String key = encodeKey(config, config.outputPrefix() + entry.getKey()); + String key = encodeKey(config.outputPrefix() + entry.getKey()); lastValue = entry.getValue(); writer.write(key, lastValue); } @@ -170,8 +171,8 @@ public void write(Map props, Config config) throws IOException { } } - private static String encodeKey(Config config, String key) { - StringBuilder result = new StringBuilder(key.length() + config.outputPrefix().length() + 4); + private static String encodeKey(String key) { + StringBuilder result = new StringBuilder(key.length() + 4); Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); while (matcher.find()) { matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); @@ -216,7 +217,10 @@ public void write(Map props, Config config) throws IOException { @Override public void write(Map props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); - Files.createDirectories((Paths.get(outputFile).getParent())); + Path parentDir = Paths.get(outputFile).getParent(); + if (parentDir != null) { + Files.createDirectories(parentDir); + } try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { String jsonResult = Util.toJson(props); System.err.format("writing JSON for %s properties to %s%n", props.size(), outputFile); @@ -260,7 +264,7 @@ private static void writeNamedImpl(Map props, Config config, Git for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; String value = props.get(selectedKeys.get(i)); - writer.write(name, value); + writer.write(name, value != null ? value : ""); } } } @@ -419,7 +423,7 @@ public static String toJson(Map map) { for (Map.Entry entry : map.entrySet()) { s.append(s.length() == initialLength ? '"' : ", \""); appendJsonString(s, entry.getKey()); - s.append("\":\""); + s.append("\": \""); appendJsonString(s, entry.getValue()); s.append('"'); } @@ -430,6 +434,8 @@ private static void appendJsonString(StringBuilder buffer, String s) { for (char c : s.toCharArray()) { if (c == ' ') { buffer.append(c); + } else if (c == '\\') { + buffer.append('\\').append('\\'); } else if (c == '"') { buffer.append('\\').append('"'); } else if (c == '\t') { diff --git a/README.md b/README.md index ae11dc9..69281a8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # read-java-properties -Github Action to read a Java .properties file and output one, multiple, or all properties as plain strings or JSON. +GitHub Action to read a Java .properties file and output one, multiple, or all properties as plain strings or JSON. ## Usage example: @@ -61,7 +61,7 @@ Query a single property as action output: keys: org.gradle.jvmargs ``` ⇒ \ - `${{steps.readProp.outputs.org.gradle.jvmargs}}` == `-ea -showversion`, \ + `${{steps.readProp.outputs.org-002Egradle-002Ejvmargs}}` == `-ea -showversion`, \ `${{steps.readProp.outputs.value}}` == `-ea -showversion`. Query multiple (alternative) properties as a single action output. In other words, query a property with a fallback to another property. diff --git a/test/resources/test.properties b/test/resources/test.properties index 9bb4b82..3933a50 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -5,6 +5,7 @@ org.gradle.jvmargs: -ea -showversion space\ in\ Key: sp colon\:in\:Key: c colonInValue: cv: +back\\slash: back\\slash\\value dash-in-Key: d dashInValue: d- empty: From 077c092da852797db4c6a06cc5c5a6c2b7e10363 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 12:25:15 +0200 Subject: [PATCH 114/190] test: split into more jobs for readability --- .github/workflows/test.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58b6793..c067648 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -127,7 +127,9 @@ jobs: # aggregates all job states, so that we need only this job as a status check in the GitHub repository settings. test: needs: - - test-misc + - test-invalid-usage + - test-output + - test-json - test-all-env - test-multi-env - test-multi-env-named @@ -149,7 +151,7 @@ jobs: | map((.key + " has status " + (.value.result//"null") + "\n") | halt_error(1))[] ' <<<"$JOBS_TO_CHECK_JSON" - test-misc: + test-invalid-usage: runs-on: ubuntu-latest steps: - &checkoutStep @@ -247,6 +249,10 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' + test-output: + runs-on: ubuntu-latest + steps: + - *checkoutStep ############################################################ # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output # name (instead of re-implemented). @@ -503,7 +509,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" my-prop 'c' + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_SINGLE_NAME" 'c' # The outputs with the default names are not set: evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion @@ -560,6 +566,10 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + test-json: + runs-on: ubuntu-latest + steps: + - *checkoutStep ############################################################ - name: '[act] test: query all properties as JSON action output' id: all-json From e795cd60fb75e2502d7f5f3c39488d744547d620 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 16:15:18 +0200 Subject: [PATCH 115/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 49 +++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c067648..5a1796a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -127,17 +127,18 @@ jobs: # aggregates all job states, so that we need only this job as a status check in the GitHub repository settings. test: needs: - - test-invalid-usage - - test-output - - test-json - - test-all-env - - test-multi-env - - test-multi-env-named - - test-multi-env-named1 - - test-single-env - - test-all-env-with-prefix - - test-multi-env-with-prefix - - test-single-env-with-prefix + - test_invalid-usage + - test_output + - test_json + - test_json-file + - test_env_all + - test_env_multi + - test_env-named_multi + - test_env-named_multi-to-one + - test_env_single + - test_env-with-prefix_all + - test_env-with-prefix_multi + - test_env-with-prefix_single if: always() runs-on: ubuntu-latest steps: @@ -151,7 +152,7 @@ jobs: | map((.key + " has status " + (.value.result//"null") + "\n") | halt_error(1))[] ' <<<"$JOBS_TO_CHECK_JSON" - test-invalid-usage: + test_invalid-usage: runs-on: ubuntu-latest steps: - &checkoutStep @@ -249,7 +250,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' - test-output: + test_output: runs-on: ubuntu-latest steps: - *checkoutStep @@ -566,7 +567,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 - test-json: + test_json: runs-on: ubuntu-latest steps: - *checkoutStep @@ -597,6 +598,10 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiImpl + test_json-file: + runs-on: ubuntu-latest + steps: + - *checkoutStep ############################################################ - name: '[arrange] test: query multiple properties as JSON file (creating file)' id: multiple-json-file-create-arrange @@ -663,7 +668,7 @@ jobs: # Each test which sets environment variables is a separate job to be independant of other tests. ############################################################ - test-all-env: + test_env_all: runs-on: ubuntu-latest steps: - *checkoutStep @@ -678,7 +683,7 @@ jobs: run: *validateAllImpl ############################################################ - test-multi-env: + test_env_multi: runs-on: ubuntu-latest steps: - *checkoutStep @@ -694,7 +699,7 @@ jobs: run: *validateMultiImpl ############################################################ - test-multi-env-named: + test_env-named_multi: runs-on: ubuntu-latest steps: - *checkoutStep @@ -710,7 +715,7 @@ jobs: run: *validateMultiNamedImpl ############################################################ - test-multi-env-named1: + test_env-named_multi-to-one: runs-on: ubuntu-latest steps: - *checkoutStep @@ -726,7 +731,7 @@ jobs: run: *validateMultiNamed1Impl ############################################################ - test-single-env: + test_env_single: runs-on: ubuntu-latest steps: - *checkoutStep @@ -742,7 +747,7 @@ jobs: run: *validateSingleImpl ############################################################ - test-all-env-with-prefix: + test_env-with-prefix_all: runs-on: ubuntu-latest steps: - *checkoutStep @@ -757,7 +762,7 @@ jobs: run: *validateAllImpl ############################################################ - test-multi-env-with-prefix: + test_env-with-prefix_multi: runs-on: ubuntu-latest steps: - *checkoutStep @@ -773,7 +778,7 @@ jobs: run: *validateMultiImpl ############################################################ - test-single-env-with-prefix: + test_env-with-prefix_single: runs-on: ubuntu-latest steps: - *checkoutStep From 9423fc350be4b45bcedf39ba01284b853b053e3b Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 16:35:08 +0200 Subject: [PATCH 116/190] add test for default resultType; shorten step names --- .github/workflows/test.yml | 156 +++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 65 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5a1796a..f0a5cd0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -128,6 +128,7 @@ jobs: test: needs: - test_invalid-usage + - test_default - test_output - test_json - test_json-file @@ -164,14 +165,14 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ############################################################ - - name: '[act] test (should fail): invalid input: resultType' + - name: '[act] (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: rt1 - - name: '[assert] test (should fail): invalid input: resultType' + - name: '[assert] (should fail): invalid input: resultType' env: STATUS: ${{steps.error-input-resultType.outcome}} ERROR: ${{steps.error-input-resultType.outputs.error}} @@ -181,7 +182,7 @@ jobs: assertEquals "$ERROR" 'invalid resultType: rt1' ############################################################ - - name: '[act] test (should fail): resultType "output-named" without names' + - name: '[act] (should fail): resultType "output-named" without names' id: error-resultType-output-without-names continue-on-error: true uses: ./ @@ -189,7 +190,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:' - - name: '[assert] test (should fail): resultType "output-named" without names' + - name: '[assert] (should fail): resultType "output-named" without names' env: STATUS: ${{steps.error-resultType-output-without-names.outcome}} ERROR: ${{steps.error-resultType-output-without-names.outputs.error}} @@ -199,7 +200,7 @@ jobs: assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' ############################################################ - - name: '[act] test (should fail): resultType "env-named" without names' + - name: '[act] (should fail): resultType "env-named" without names' id: error-resultType-env-without-names continue-on-error: true uses: ./ @@ -207,7 +208,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:' - - name: '[assert] test (should fail): resultType "env-named" without names' + - name: '[assert] (should fail): resultType "env-named" without names' env: STATUS: ${{steps.error-resultType-env-without-names.outcome}} ERROR: ${{steps.error-resultType-env-without-names.outputs.error}} @@ -217,14 +218,14 @@ jobs: assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' ############################################################ - - name: '[act] test (should fail): resultType "output-named" without keys' + - name: '[act] (should fail): resultType "output-named" without keys' id: error-resultType-output-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'output-named:a b' - - name: '[assert] test (should fail): resultType "output-named" without keys' + - name: '[assert] (should fail): resultType "output-named" without keys' env: STATUS: ${{steps.error-resultType-output-without-keys.outcome}} ERROR: ${{steps.error-resultType-output-without-keys.outputs.error}} @@ -234,14 +235,14 @@ jobs: assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' ############################################################ - - name: '[act] test (should fail): resultType "env-named" without keys' + - name: '[act] (should fail): resultType "env-named" without keys' id: error-resultType-env-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'env-named:a b' - - name: '[assert] test (should fail): resultType "env-named" without keys' + - name: '[assert] (should fail): resultType "env-named" without keys' env: STATUS: ${{steps.error-resultType-env-without-keys.outcome}} ERROR: ${{steps.error-resultType-env-without-keys.outputs.error}} @@ -250,6 +251,24 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' + test_default: + runs-on: ubuntu-latest + steps: + - *checkoutStep + ############################################################ + - name: '[act] query single property with default resultType' + id: single-default + uses: ./ + with: + file: test/resources/test.properties + keys: sourceJavaVersion + - name: '[assert] query single property with default resultType' + env: + RESULT: ${{steps.single-default.outputs._sourceJavaVersion}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT" '21' + test_output: runs-on: ubuntu-latest steps: @@ -258,13 +277,14 @@ jobs: # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. - - name: '[act] test: query multiple properties as action outputs (simple)' + - name: '[act] query multiple properties as action outputs (simple)' id: multi-output-simple uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}} dash-in-Key' - - name: '[assert] test: query multiple properties as action outputs (simple)' + resultType: output + - name: '[assert] query multiple properties as action outputs (simple)' env: RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} @@ -280,12 +300,13 @@ jobs: assertEquals "$RESULT_UNI" 'a«,»c' ############################################################ - - name: '[act] test: query all properties as action outputs' + - name: '[act] query all properties as action outputs' id: all-output uses: ./ with: file: test/resources/test.properties - - name: '[assert] test: query all properties as action outputs' + resultType: output + - name: '[assert] query all properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' @@ -320,7 +341,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' - - name: '[assert] test: query all properties as action outputs (additional "value" output not set)' + - name: '[assert] query all properties as action outputs (additional "value" output not set)' env: RESULT_VALUE: ${{steps.all-output.outputs.value || 'U_N_S_E_T'}} run: |- @@ -328,14 +349,15 @@ jobs: assertEquals "$RESULT_VALUE" U_N_S_E_T ############################################################ - - name: '[act] test: query multiple properties as action outputs' + - name: '[act] query multiple properties as action outputs' id: multi-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' + resultType: output - - name: '[assert] test: query multiple properties as action outputs' + - name: '[assert] query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' @@ -372,7 +394,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 - - name: '[assert] test: query multiple properties as action outputs (additional "value" output)' + - name: '[assert] query multiple properties as action outputs (additional "value" output)' env: RESULT_VALUE: ${{steps.multi-output.outputs.value}} run: |- @@ -380,46 +402,49 @@ jobs: assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" ############################################################ - - name: '[act] test: query multiple properties as action outputs (different keySeparator)' + - name: '[act] query multiple properties as action outputs (different keySeparator)' id: multi-output-keySeparator uses: ./ with: file: test/resources/test.properties keySeparator: '?' # something with a special meaning in regex; should be treated as literal keys: 'sourceJavaVersion?org.gradle.jvmargs?unicode_escapes?colon:in:Key' - - name: '[assert] test: query multiple properties as action outputs (different keySeparator)' + resultType: output + - name: '[assert] query multiple properties as action outputs (different keySeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl ############################################################ - - name: '[act] test: query multiple properties as action outputs (different multi-char keySeparator)' + - name: '[act] query multiple properties as action outputs (different multi-char keySeparator)' id: multi-output-keySeparatorMC uses: ./ with: file: test/resources/test.properties keySeparator: ':/ ' keys: 'sourceJavaVersion:/ org.gradle.jvmargs:/ unicode_escapes:/ colon:in:Key' - - name: '[assert] test: query multiple properties as action outputs (different multi-char keySeparator)' + resultType: output + - name: '[assert] query multiple properties as action outputs (different multi-char keySeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-keySeparatorMC.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl ############################################################ - - name: '[act] test: query multiple properties as action outputs (in another keys order)' + - name: '[act] query multiple properties as action outputs (in another keys order)' id: multi-output-2 uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS_OTHER_ORDER}}' - - name: '[assert] test: query multiple properties as action outputs (in another keys order)' + resultType: output + - name: '[assert] query multiple properties as action outputs (in another keys order)' env: RESULT_JSON: ${{toJSON(steps.multi-output-2.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - - name: '[assert] test: query multiple properties as action outputs (in another keys order) (additional "value" output)' + - name: '[assert] query multiple properties as action outputs (in another keys order) (additional "value" output)' env: RESULT_VALUE: ${{steps.multi-output-2.outputs.value}} run: |- @@ -427,7 +452,7 @@ jobs: assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" ############################################################ - - name: '[act] test: query multiple properties as action outputs of given names' + - name: '[act] query multiple properties as action outputs of given names' id: multi-output-named uses: ./ with: @@ -435,7 +460,7 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_NAMES}}' - - name: '[assert] test: query multiple properties as action outputs of given names' + - name: '[assert] query multiple properties as action outputs of given names' env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -457,7 +482,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash_1 - - name: '[assert] test: query multiple properties as action outputs (additional "value" output not set)' + - name: '[assert] query multiple properties as action outputs (additional "value" output not set)' env: RESULT_VALUE: ${{steps.multi-output-named.outputs.value || 'U_N_S_E_T'}} run: |- @@ -465,7 +490,7 @@ jobs: assertEquals "$RESULT_VALUE" U_N_S_E_T ############################################################ - - name: '[act] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + - name: '[act] query multiple properties as action outputs of given names (different resultNameSeparator)' id: multi-output-named-resultNameSeparator uses: ./ with: @@ -473,14 +498,14 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultNameSeparator: '?' # something with a special meaning in regex; should be treated as literal resultType: 'output-named:java-version?jvm_args?unicode?colon' - - name: '[assert] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + - name: '[assert] query multiple properties as action outputs of given names (different resultNameSeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl ############################################################ - - name: '[act] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + - name: '[act] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' id: multi-output-named-resultNameSeparatorMC uses: ./ with: @@ -488,14 +513,14 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultNameSeparator: '- -' resultType: 'output-named:java-version- -jvm_args- -unicode- -colon' - - name: '[assert] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + - name: '[assert] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparatorMC.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl ############################################################ - - name: '[act] test: query multiple properties as action outputs of given name' + - name: '[act] query multiple properties as action outputs of given name' id: multi-output-named1 uses: ./ with: @@ -503,7 +528,7 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' - - name: '[assert] test: query multiple properties as action outputs of given name' + - name: '[assert] query multiple properties as action outputs of given name' env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -523,14 +548,15 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_1 ############################################################ - - name: '[act] test: query single property as action outputs' + - name: '[act] query single property as action outputs' id: single-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' + resultType: output - - name: '[assert] test: query multiple properties as action outputs' + - name: '[assert] query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' @@ -572,27 +598,27 @@ jobs: steps: - *checkoutStep ############################################################ - - name: '[act] test: query all properties as JSON action output' + - name: '[act] query all properties as JSON action output' id: all-json uses: ./ with: file: test/resources/test.properties resultType: json - - name: '[assert] test: query all properties as JSON action output' + - name: '[assert] query all properties as JSON action output' env: RESULT_JSON: ${{steps.all-json.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl ############################################################ - - name: '[act] test: query multiple properties as JSON action output' + - name: '[act] query multiple properties as JSON action output' id: multiple-json uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: json - - name: '[assert] test: query multiple properties as JSON action output' + - name: '[assert] query multiple properties as JSON action output' env: RESULT_JSON: ${{steps.multiple-json.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' @@ -603,27 +629,27 @@ jobs: steps: - *checkoutStep ############################################################ - - name: '[arrange] test: query multiple properties as JSON file (creating file)' + - name: '[arrange] query multiple properties as JSON file (creating file)' id: multiple-json-file-create-arrange run: |- parentDir=$(mktemp --directory --dry-run --tmpdir=. test_json-file_multi_create_XXXXXX) file=$parentDir/out/result.json printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" # ⇒ action must create at least two parent dirs - - name: '[act] test: query multiple properties as JSON file (creating file)' + - name: '[act] query multiple properties as JSON file (creating file)' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' - - name: '[assert] test: query multiple properties as JSON file (creating file)' + - name: '[assert] query multiple properties as JSON file (creating file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl ############################################################ - - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' + - name: '[arrange] query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- file=$(mktemp --tmpdir=. test_json-file_multi_overwrite_XXXXXX.json) @@ -632,20 +658,20 @@ jobs: for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" ls -lAF -- "$file" printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" - - name: '[act] test: query multiple properties as JSON file (overwriting file)' + - name: '[act] query multiple properties as JSON file (overwriting file)' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'json-file:${{steps.multiple-json-file-overwrite-arrange.outputs.file}}' - - name: '[assert] test: query multiple properties as JSON file (overwriting file)' + - name: '[assert] query multiple properties as JSON file (overwriting file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl ############################################################ - - name: '[arrange] test: query all properties as JSON file' + - name: '[arrange] query all properties as JSON file' id: all-json-file-arrange # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. @@ -654,12 +680,12 @@ jobs: # java.nio.file.Path.getParent() returns null). file=$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json | sed 's=^\./==') printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" - - name: '[act] test: query all properties as JSON file' + - name: '[act] query all properties as JSON file' uses: ./ with: file: test/resources/test.properties resultType: 'json-file:${{steps.all-json-file-arrange.outputs.file}}' - - name: '[assert] test: query all properties as JSON file' + - name: '[assert] query all properties as JSON file' env: JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' @@ -672,12 +698,12 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query all properties as environment variables' + - name: '[act] query all properties as environment variables' uses: ./ with: file: test/resources/test.properties resultType: env - - name: '[assert] test: query all properties as environment variables' + - name: '[assert] query all properties as environment variables' env: RESULT_GETTER: getEnv run: *validateAllImpl @@ -687,13 +713,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query multiple properties as environment variables' + - name: '[act] query multiple properties as environment variables' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: env - - name: '[assert] test: query multiple properties as environment variables' + - name: '[assert] query multiple properties as environment variables' env: RESULT_GETTER: getEnv run: *validateMultiImpl @@ -703,13 +729,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query multiple properties as environment variables of given names' + - name: '[act] query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_NAMES}}' - - name: '[assert] test: query multiple properties as environment variables of given names' + - name: '[assert] query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamedImpl @@ -719,13 +745,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query multiple properties as environment variables of given names' + - name: '[act] query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_SINGLE_NAME}}' - - name: '[assert] test: query multiple properties as environment variables of given names' + - name: '[assert] query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamed1Impl @@ -735,13 +761,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query single property as environment variable' + - name: '[act] query single property as environment variable' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: env - - name: '[assert] test: query single property as environment variable' + - name: '[assert] query single property as environment variable' env: RESULT_GETTER: getEnv run: *validateSingleImpl @@ -751,12 +777,12 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query all properties as environment variables with prefix' + - name: '[act] query all properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties resultType: 'env:ALL_PROPS__' - - name: '[assert] test: query all properties as environment variables' + - name: '[assert] query all properties as environment variables' env: RESULT_GETTER: 'getEnvWithPrefix ALL_PROPS__' run: *validateAllImpl @@ -766,13 +792,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query multiple properties as environment variables with prefix' + - name: '[act] query multiple properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env:MULTI_PROPS__' - - name: '[assert] test: query multiple properties as environment variables with prefix' + - name: '[assert] query multiple properties as environment variables with prefix' env: RESULT_GETTER: 'getEnvWithPrefix MULTI_PROPS__' run: *validateMultiImpl @@ -782,13 +808,13 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - - name: '[act] test: query single property as environment variable with prefix' + - name: '[act] query single property as environment variable with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: 'env:PROP__' - - name: '[assert] test: query single property as environment variable with prefix' + - name: '[assert] query single property as environment variable with prefix' env: RESULT_GETTER: 'getEnvWithPrefix PROP__' run: *validateSingleImpl From 1e3908d7ccfa8a5db8d9b3bc5865970c01e75331 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 16:27:46 +0200 Subject: [PATCH 117/190] test: split into more jobs for readability --- .github/workflows/test.yml | 140 ++++++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 49 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58b6793..c1ffb57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,19 +123,25 @@ env: } jobs: - # Similar to [https://github.com/orgs/community/discussions/12395#discussioncomment-12970019], this step + ######################################################################### + # Similar to [https://github.com/orgs/community/discussions/12395#discussioncomment-12970019], this # aggregates all job states, so that we need only this job as a status check in the GitHub repository settings. test: needs: - - test-misc - - test-all-env - - test-multi-env - - test-multi-env-named - - test-multi-env-named1 - - test-single-env - - test-all-env-with-prefix - - test-multi-env-with-prefix - - test-single-env-with-prefix + - test_invalid-usage + - test_default + - test_output + - test_output-named + - test_json + - test_json-file + - test_env_all + - test_env_multi + - test_env-named_multi + - test_env-named_multi-to-one + - test_env_single + - test_env-with-prefix_all + - test_env-with-prefix_multi + - test_env-with-prefix_single if: always() runs-on: ubuntu-latest steps: @@ -149,7 +155,8 @@ jobs: | map((.key + " has status " + (.value.result//"null") + "\n") | halt_error(1))[] ' <<<"$JOBS_TO_CHECK_JSON" - test-misc: + ######################################################################### + test_invalid-usage: runs-on: ubuntu-latest steps: - &checkoutStep @@ -160,7 +167,7 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - ############################################################ + # ===================================================================== - name: '[act] test (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true @@ -177,7 +184,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType: rt1' - ############################################################ + # ===================================================================== - name: '[act] test (should fail): resultType "output-named" without names' id: error-resultType-output-without-names continue-on-error: true @@ -195,7 +202,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' - ############################################################ + # ===================================================================== - name: '[act] test (should fail): resultType "env-named" without names' id: error-resultType-env-without-names continue-on-error: true @@ -213,7 +220,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' - ############################################################ + # ===================================================================== - name: '[act] test (should fail): resultType "output-named" without keys' id: error-resultType-output-without-keys continue-on-error: true @@ -230,7 +237,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' - ############################################################ + # ===================================================================== - name: '[act] test (should fail): resultType "env-named" without keys' id: error-resultType-env-without-keys continue-on-error: true @@ -247,7 +254,12 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' - ############################################################ + ######################################################################### + test_output: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. @@ -257,6 +269,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}} dash-in-Key' + resultType: output - name: '[assert] test: query multiple properties as action outputs (simple)' env: RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} @@ -272,12 +285,13 @@ jobs: assertEquals "$RESULT_DASHK" 'd' assertEquals "$RESULT_UNI" 'a«,»c' - ############################################################ + # ===================================================================== - name: '[act] test: query all properties as action outputs' id: all-output uses: ./ with: file: test/resources/test.properties + resultType: output - name: '[assert] test: query all properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} @@ -320,13 +334,14 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as action outputs' id: multi-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' + resultType: output - name: '[assert] test: query multiple properties as action outputs' env: @@ -372,7 +387,7 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as action outputs (different keySeparator)' id: multi-output-keySeparator uses: ./ @@ -380,13 +395,14 @@ jobs: file: test/resources/test.properties keySeparator: '?' # something with a special meaning in regex; should be treated as literal keys: 'sourceJavaVersion?org.gradle.jvmargs?unicode_escapes?colon:in:Key' + resultType: output - name: '[assert] test: query multiple properties as action outputs (different keySeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as action outputs (different multi-char keySeparator)' id: multi-output-keySeparatorMC uses: ./ @@ -394,19 +410,21 @@ jobs: file: test/resources/test.properties keySeparator: ':/ ' keys: 'sourceJavaVersion:/ org.gradle.jvmargs:/ unicode_escapes:/ colon:in:Key' + resultType: output - name: '[assert] test: query multiple properties as action outputs (different multi-char keySeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-keySeparatorMC.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as action outputs (in another keys order)' id: multi-output-2 uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS_OTHER_ORDER}}' + resultType: output - name: '[assert] test: query multiple properties as action outputs (in another keys order)' env: RESULT_JSON: ${{toJSON(steps.multi-output-2.outputs)}} @@ -419,7 +437,12 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" - ############################################################ + ######################################################################### + test_output-named: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== - name: '[act] test: query multiple properties as action outputs of given names' id: multi-output-named uses: ./ @@ -457,7 +480,7 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as action outputs of given names (different resultNameSeparator)' id: multi-output-named-resultNameSeparator uses: ./ @@ -472,7 +495,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' id: multi-output-named-resultNameSeparatorMC uses: ./ @@ -487,7 +510,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as action outputs of given name' id: multi-output-named1 uses: ./ @@ -503,7 +526,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" my-prop 'c' + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_SINGLE_NAME" 'c' # The outputs with the default names are not set: evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion @@ -515,13 +538,14 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash_1 - ############################################################ + # ===================================================================== - name: '[act] test: query single property as action outputs' id: single-output uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' + resultType: output - name: '[assert] test: query multiple properties as action outputs' env: @@ -560,7 +584,12 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 - ############################################################ + ######################################################################### + test_json: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== - name: '[act] test: query all properties as JSON action output' id: all-json uses: ./ @@ -573,7 +602,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl - ############################################################ + # ===================================================================== - name: '[act] test: query multiple properties as JSON action output' id: multiple-json uses: ./ @@ -587,7 +616,12 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiImpl - ############################################################ + ######################################################################### + test_json-file: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== - name: '[arrange] test: query multiple properties as JSON file (creating file)' id: multiple-json-file-create-arrange run: |- @@ -607,7 +641,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- @@ -629,7 +663,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[arrange] test: query all properties as JSON file' id: all-json-file-arrange # Distinction between creating and overwriting need not be tested again. @@ -652,11 +686,12 @@ jobs: # Each test which sets environment variables is a separate job to be independant of other tests. - ############################################################ - test-all-env: + ######################################################################### + test_env_all: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query all properties as environment variables' uses: ./ with: @@ -667,11 +702,12 @@ jobs: RESULT_GETTER: getEnv run: *validateAllImpl - ############################################################ - test-multi-env: + ######################################################################### + test_env_multi: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query multiple properties as environment variables' uses: ./ with: @@ -683,11 +719,12 @@ jobs: RESULT_GETTER: getEnv run: *validateMultiImpl - ############################################################ - test-multi-env-named: + ######################################################################### + test_env-named_multi: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query multiple properties as environment variables of given names' uses: ./ with: @@ -699,11 +736,12 @@ jobs: RESULT_GETTER: getEnv run: *validateMultiNamedImpl - ############################################################ - test-multi-env-named1: + ######################################################################### + test_env-named_multi-to-one: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query multiple properties as environment variables of given names' uses: ./ with: @@ -715,11 +753,12 @@ jobs: RESULT_GETTER: getEnv run: *validateMultiNamed1Impl - ############################################################ - test-single-env: + ######################################################################### + test_env_single: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query single property as environment variable' uses: ./ with: @@ -731,11 +770,12 @@ jobs: RESULT_GETTER: getEnv run: *validateSingleImpl - ############################################################ - test-all-env-with-prefix: + ######################################################################### + test_env-with-prefix_all: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query all properties as environment variables with prefix' uses: ./ with: @@ -746,11 +786,12 @@ jobs: RESULT_GETTER: 'getEnvWithPrefix ALL_PROPS__' run: *validateAllImpl - ############################################################ - test-multi-env-with-prefix: + ######################################################################### + test_env-with-prefix_multi: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query multiple properties as environment variables with prefix' uses: ./ with: @@ -762,11 +803,12 @@ jobs: RESULT_GETTER: 'getEnvWithPrefix MULTI_PROPS__' run: *validateMultiImpl - ############################################################ - test-single-env-with-prefix: + ######################################################################### + test_env-with-prefix_single: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] test: query single property as environment variable with prefix' uses: ./ with: From 8f0e1ca274bb18a656127047c9626501c47b68d5 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 17:17:48 +0200 Subject: [PATCH 118/190] add test for default resultType; shorten step names --- .github/workflows/test.yml | 148 +++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 65 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c1ffb57..f9b07a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -168,14 +168,14 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} # ===================================================================== - - name: '[act] test (should fail): invalid input: resultType' + - name: '[act] (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: rt1 - - name: '[assert] test (should fail): invalid input: resultType' + - name: '[assert] (should fail): invalid input: resultType' env: STATUS: ${{steps.error-input-resultType.outcome}} ERROR: ${{steps.error-input-resultType.outputs.error}} @@ -185,7 +185,7 @@ jobs: assertEquals "$ERROR" 'invalid resultType: rt1' # ===================================================================== - - name: '[act] test (should fail): resultType "output-named" without names' + - name: '[act] (should fail): resultType "output-named" without names' id: error-resultType-output-without-names continue-on-error: true uses: ./ @@ -193,7 +193,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:' - - name: '[assert] test (should fail): resultType "output-named" without names' + - name: '[assert] (should fail): resultType "output-named" without names' env: STATUS: ${{steps.error-resultType-output-without-names.outcome}} ERROR: ${{steps.error-resultType-output-without-names.outputs.error}} @@ -203,7 +203,7 @@ jobs: assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' # ===================================================================== - - name: '[act] test (should fail): resultType "env-named" without names' + - name: '[act] (should fail): resultType "env-named" without names' id: error-resultType-env-without-names continue-on-error: true uses: ./ @@ -211,7 +211,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:' - - name: '[assert] test (should fail): resultType "env-named" without names' + - name: '[assert] (should fail): resultType "env-named" without names' env: STATUS: ${{steps.error-resultType-env-without-names.outcome}} ERROR: ${{steps.error-resultType-env-without-names.outputs.error}} @@ -221,14 +221,14 @@ jobs: assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' # ===================================================================== - - name: '[act] test (should fail): resultType "output-named" without keys' + - name: '[act] (should fail): resultType "output-named" without keys' id: error-resultType-output-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'output-named:a b' - - name: '[assert] test (should fail): resultType "output-named" without keys' + - name: '[assert] (should fail): resultType "output-named" without keys' env: STATUS: ${{steps.error-resultType-output-without-keys.outcome}} ERROR: ${{steps.error-resultType-output-without-keys.outputs.error}} @@ -238,14 +238,14 @@ jobs: assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' # ===================================================================== - - name: '[act] test (should fail): resultType "env-named" without keys' + - name: '[act] (should fail): resultType "env-named" without keys' id: error-resultType-env-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'env-named:a b' - - name: '[assert] test (should fail): resultType "env-named" without keys' + - name: '[assert] (should fail): resultType "env-named" without keys' env: STATUS: ${{steps.error-resultType-env-without-keys.outcome}} ERROR: ${{steps.error-resultType-env-without-keys.outputs.error}} @@ -255,6 +255,24 @@ jobs: assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' ######################################################################### + test_default: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] query single property with default resultType' + id: single-default + uses: ./ + with: + file: test/resources/test.properties + keys: sourceJavaVersion + - name: '[assert] query single property with default resultType' + env: + RESULT: ${{steps.single-default.outputs._sourceJavaVersion}} + run: |- + eval "$SHELL_SETUP" + assertEquals "$RESULT" '21' + test_output: runs-on: ubuntu-latest steps: @@ -263,14 +281,14 @@ jobs: # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. - - name: '[act] test: query multiple properties as action outputs (simple)' + - name: '[act] query multiple properties as action outputs (simple)' id: multi-output-simple uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}} dash-in-Key' resultType: output - - name: '[assert] test: query multiple properties as action outputs (simple)' + - name: '[assert] query multiple properties as action outputs (simple)' env: RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} @@ -286,13 +304,13 @@ jobs: assertEquals "$RESULT_UNI" 'a«,»c' # ===================================================================== - - name: '[act] test: query all properties as action outputs' + - name: '[act] query all properties as action outputs' id: all-output uses: ./ with: file: test/resources/test.properties resultType: output - - name: '[assert] test: query all properties as action outputs' + - name: '[assert] query all properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' @@ -327,7 +345,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' - - name: '[assert] test: query all properties as action outputs (additional "value" output not set)' + - name: '[assert] query all properties as action outputs (additional "value" output not set)' env: RESULT_VALUE: ${{steps.all-output.outputs.value || 'U_N_S_E_T'}} run: |- @@ -335,7 +353,7 @@ jobs: assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== - - name: '[act] test: query multiple properties as action outputs' + - name: '[act] query multiple properties as action outputs' id: multi-output uses: ./ with: @@ -343,7 +361,7 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: output - - name: '[assert] test: query multiple properties as action outputs' + - name: '[assert] query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' @@ -380,7 +398,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 - - name: '[assert] test: query multiple properties as action outputs (additional "value" output)' + - name: '[assert] query multiple properties as action outputs (additional "value" output)' env: RESULT_VALUE: ${{steps.multi-output.outputs.value}} run: |- @@ -388,7 +406,7 @@ jobs: assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" # ===================================================================== - - name: '[act] test: query multiple properties as action outputs (different keySeparator)' + - name: '[act] query multiple properties as action outputs (different keySeparator)' id: multi-output-keySeparator uses: ./ with: @@ -396,14 +414,14 @@ jobs: keySeparator: '?' # something with a special meaning in regex; should be treated as literal keys: 'sourceJavaVersion?org.gradle.jvmargs?unicode_escapes?colon:in:Key' resultType: output - - name: '[assert] test: query multiple properties as action outputs (different keySeparator)' + - name: '[assert] query multiple properties as action outputs (different keySeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl # ===================================================================== - - name: '[act] test: query multiple properties as action outputs (different multi-char keySeparator)' + - name: '[act] query multiple properties as action outputs (different multi-char keySeparator)' id: multi-output-keySeparatorMC uses: ./ with: @@ -411,26 +429,26 @@ jobs: keySeparator: ':/ ' keys: 'sourceJavaVersion:/ org.gradle.jvmargs:/ unicode_escapes:/ colon:in:Key' resultType: output - - name: '[assert] test: query multiple properties as action outputs (different multi-char keySeparator)' + - name: '[assert] query multiple properties as action outputs (different multi-char keySeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-keySeparatorMC.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl # ===================================================================== - - name: '[act] test: query multiple properties as action outputs (in another keys order)' + - name: '[act] query multiple properties as action outputs (in another keys order)' id: multi-output-2 uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS_OTHER_ORDER}}' resultType: output - - name: '[assert] test: query multiple properties as action outputs (in another keys order)' + - name: '[assert] query multiple properties as action outputs (in another keys order)' env: RESULT_JSON: ${{toJSON(steps.multi-output-2.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - - name: '[assert] test: query multiple properties as action outputs (in another keys order) (additional "value" output)' + - name: '[assert] query multiple properties as action outputs (in another keys order) (additional "value" output)' env: RESULT_VALUE: ${{steps.multi-output-2.outputs.value}} run: |- @@ -443,7 +461,7 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query multiple properties as action outputs of given names' + - name: '[act] query multiple properties as action outputs of given names' id: multi-output-named uses: ./ with: @@ -451,7 +469,7 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_NAMES}}' - - name: '[assert] test: query multiple properties as action outputs of given names' + - name: '[assert] query multiple properties as action outputs of given names' env: RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -473,7 +491,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash_1 - - name: '[assert] test: query multiple properties as action outputs (additional "value" output not set)' + - name: '[assert] query multiple properties as action outputs (additional "value" output not set)' env: RESULT_VALUE: ${{steps.multi-output-named.outputs.value || 'U_N_S_E_T'}} run: |- @@ -481,7 +499,7 @@ jobs: assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== - - name: '[act] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + - name: '[act] query multiple properties as action outputs of given names (different resultNameSeparator)' id: multi-output-named-resultNameSeparator uses: ./ with: @@ -489,14 +507,14 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultNameSeparator: '?' # something with a special meaning in regex; should be treated as literal resultType: 'output-named:java-version?jvm_args?unicode?colon' - - name: '[assert] test: query multiple properties as action outputs of given names (different resultNameSeparator)' + - name: '[assert] query multiple properties as action outputs of given names (different resultNameSeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl # ===================================================================== - - name: '[act] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + - name: '[act] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' id: multi-output-named-resultNameSeparatorMC uses: ./ with: @@ -504,14 +522,14 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultNameSeparator: '- -' resultType: 'output-named:java-version- -jvm_args- -unicode- -colon' - - name: '[assert] test: query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' + - name: '[assert] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' env: RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparatorMC.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl # ===================================================================== - - name: '[act] test: query multiple properties as action outputs of given name' + - name: '[act] query multiple properties as action outputs of given name' id: multi-output-named1 uses: ./ with: @@ -519,7 +537,7 @@ jobs: keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' - - name: '[assert] test: query multiple properties as action outputs of given name' + - name: '[assert] query multiple properties as action outputs of given name' env: RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): @@ -539,7 +557,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_1 # ===================================================================== - - name: '[act] test: query single property as action outputs' + - name: '[act] query single property as action outputs' id: single-output uses: ./ with: @@ -547,7 +565,7 @@ jobs: keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: output - - name: '[assert] test: query multiple properties as action outputs' + - name: '[assert] query multiple properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' @@ -590,27 +608,27 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query all properties as JSON action output' + - name: '[act] query all properties as JSON action output' id: all-json uses: ./ with: file: test/resources/test.properties resultType: json - - name: '[assert] test: query all properties as JSON action output' + - name: '[assert] query all properties as JSON action output' env: RESULT_JSON: ${{steps.all-json.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl # ===================================================================== - - name: '[act] test: query multiple properties as JSON action output' + - name: '[act] query multiple properties as JSON action output' id: multiple-json uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: json - - name: '[assert] test: query multiple properties as JSON action output' + - name: '[assert] query multiple properties as JSON action output' env: RESULT_JSON: ${{steps.multiple-json.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' @@ -622,27 +640,27 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[arrange] test: query multiple properties as JSON file (creating file)' + - name: '[arrange] query multiple properties as JSON file (creating file)' id: multiple-json-file-create-arrange run: |- parentDir=$(mktemp --directory --dry-run --tmpdir=. test_json-file_multi_create_XXXXXX) file=$parentDir/out/result.json printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" # ⇒ action must create at least two parent dirs - - name: '[act] test: query multiple properties as JSON file (creating file)' + - name: '[act] query multiple properties as JSON file (creating file)' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' - - name: '[assert] test: query multiple properties as JSON file (creating file)' + - name: '[assert] query multiple properties as JSON file (creating file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl # ===================================================================== - - name: '[arrange] test: query multiple properties as JSON file (overwriting file)' + - name: '[arrange] query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- file=$(mktemp --tmpdir=. test_json-file_multi_overwrite_XXXXXX.json) @@ -651,20 +669,20 @@ jobs: for ((i=0; i<100; i++)); do printf 'This is ¬ JS}ON. '; done >"$file" ls -lAF -- "$file" printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" - - name: '[act] test: query multiple properties as JSON file (overwriting file)' + - name: '[act] query multiple properties as JSON file (overwriting file)' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'json-file:${{steps.multiple-json-file-overwrite-arrange.outputs.file}}' - - name: '[assert] test: query multiple properties as JSON file (overwriting file)' + - name: '[assert] query multiple properties as JSON file (overwriting file)' env: JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl # ===================================================================== - - name: '[arrange] test: query all properties as JSON file' + - name: '[arrange] query all properties as JSON file' id: all-json-file-arrange # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. @@ -673,12 +691,12 @@ jobs: # java.nio.file.Path.getParent() returns null). file=$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json | sed 's=^\./==') printf 'file=%s\n' "$file" | tee -- "$GITHUB_OUTPUT" - - name: '[act] test: query all properties as JSON file' + - name: '[act] query all properties as JSON file' uses: ./ with: file: test/resources/test.properties resultType: 'json-file:${{steps.all-json-file-arrange.outputs.file}}' - - name: '[assert] test: query all properties as JSON file' + - name: '[assert] query all properties as JSON file' env: JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' @@ -692,12 +710,12 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query all properties as environment variables' + - name: '[act] query all properties as environment variables' uses: ./ with: file: test/resources/test.properties resultType: env - - name: '[assert] test: query all properties as environment variables' + - name: '[assert] query all properties as environment variables' env: RESULT_GETTER: getEnv run: *validateAllImpl @@ -708,13 +726,13 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query multiple properties as environment variables' + - name: '[act] query multiple properties as environment variables' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: env - - name: '[assert] test: query multiple properties as environment variables' + - name: '[assert] query multiple properties as environment variables' env: RESULT_GETTER: getEnv run: *validateMultiImpl @@ -725,13 +743,13 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query multiple properties as environment variables of given names' + - name: '[act] query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_NAMES}}' - - name: '[assert] test: query multiple properties as environment variables of given names' + - name: '[assert] query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamedImpl @@ -742,13 +760,13 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query multiple properties as environment variables of given names' + - name: '[act] query multiple properties as environment variables of given names' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:${{env.SELECTED_SINGLE_NAME}}' - - name: '[assert] test: query multiple properties as environment variables of given names' + - name: '[assert] query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv run: *validateMultiNamed1Impl @@ -759,13 +777,13 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query single property as environment variable' + - name: '[act] query single property as environment variable' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: env - - name: '[assert] test: query single property as environment variable' + - name: '[assert] query single property as environment variable' env: RESULT_GETTER: getEnv run: *validateSingleImpl @@ -776,12 +794,12 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query all properties as environment variables with prefix' + - name: '[act] query all properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties resultType: 'env:ALL_PROPS__' - - name: '[assert] test: query all properties as environment variables' + - name: '[assert] query all properties as environment variables' env: RESULT_GETTER: 'getEnvWithPrefix ALL_PROPS__' run: *validateAllImpl @@ -792,13 +810,13 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query multiple properties as environment variables with prefix' + - name: '[act] query multiple properties as environment variables with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env:MULTI_PROPS__' - - name: '[assert] test: query multiple properties as environment variables with prefix' + - name: '[assert] query multiple properties as environment variables with prefix' env: RESULT_GETTER: 'getEnvWithPrefix MULTI_PROPS__' run: *validateMultiImpl @@ -809,13 +827,13 @@ jobs: steps: - *checkoutStep # ===================================================================== - - name: '[act] test: query single property as environment variable with prefix' + - name: '[act] query single property as environment variable with prefix' uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_SINGLE_KEY}}' resultType: 'env:PROP__' - - name: '[assert] test: query single property as environment variable with prefix' + - name: '[assert] query single property as environment variable with prefix' env: RESULT_GETTER: 'getEnvWithPrefix PROP__' run: *validateSingleImpl From 155d3f8b6d39d963dae6d518fd34ba4e5e34d72a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 17:49:46 +0200 Subject: [PATCH 119/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 81 +++++++++++++++---------- Action.java | 120 ++++++++++++++++++++++++------------- action.yml | 10 ++-- 3 files changed, 132 insertions(+), 79 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0a5cd0..f9b07a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,13 +123,15 @@ env: } jobs: - # Similar to [https://github.com/orgs/community/discussions/12395#discussioncomment-12970019], this step + ######################################################################### + # Similar to [https://github.com/orgs/community/discussions/12395#discussioncomment-12970019], this # aggregates all job states, so that we need only this job as a status check in the GitHub repository settings. test: needs: - test_invalid-usage - test_default - test_output + - test_output-named - test_json - test_json-file - test_env_all @@ -153,6 +155,7 @@ jobs: | map((.key + " has status " + (.value.result//"null") + "\n") | halt_error(1))[] ' <<<"$JOBS_TO_CHECK_JSON" + ######################################################################### test_invalid-usage: runs-on: ubuntu-latest steps: @@ -164,7 +167,7 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - ############################################################ + # ===================================================================== - name: '[act] (should fail): invalid input: resultType' id: error-input-resultType continue-on-error: true @@ -181,7 +184,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType: rt1' - ############################################################ + # ===================================================================== - name: '[act] (should fail): resultType "output-named" without names' id: error-resultType-output-without-names continue-on-error: true @@ -199,7 +202,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' - ############################################################ + # ===================================================================== - name: '[act] (should fail): resultType "env-named" without names' id: error-resultType-env-without-names continue-on-error: true @@ -217,7 +220,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' - ############################################################ + # ===================================================================== - name: '[act] (should fail): resultType "output-named" without keys' id: error-resultType-output-without-keys continue-on-error: true @@ -234,7 +237,7 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' - ############################################################ + # ===================================================================== - name: '[act] (should fail): resultType "env-named" without keys' id: error-resultType-env-without-keys continue-on-error: true @@ -251,11 +254,12 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' + ######################################################################### test_default: runs-on: ubuntu-latest steps: - *checkoutStep - ############################################################ + # ===================================================================== - name: '[act] query single property with default resultType' id: single-default uses: ./ @@ -273,7 +277,7 @@ jobs: runs-on: ubuntu-latest steps: - *checkoutStep - ############################################################ + # ===================================================================== # This tests a subset of test case multi-output, but with hard-coded encoding from property key to output # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. @@ -299,7 +303,7 @@ jobs: assertEquals "$RESULT_DASHK" 'd' assertEquals "$RESULT_UNI" 'a«,»c' - ############################################################ + # ===================================================================== - name: '[act] query all properties as action outputs' id: all-output uses: ./ @@ -348,7 +352,7 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as action outputs' id: multi-output uses: ./ @@ -401,7 +405,7 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as action outputs (different keySeparator)' id: multi-output-keySeparator uses: ./ @@ -416,7 +420,7 @@ jobs: RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as action outputs (different multi-char keySeparator)' id: multi-output-keySeparatorMC uses: ./ @@ -431,7 +435,7 @@ jobs: RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as action outputs (in another keys order)' id: multi-output-2 uses: ./ @@ -451,7 +455,12 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" - ############################################################ + ######################################################################### + test_output-named: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== - name: '[act] query multiple properties as action outputs of given names' id: multi-output-named uses: ./ @@ -489,7 +498,7 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as action outputs of given names (different resultNameSeparator)' id: multi-output-named-resultNameSeparator uses: ./ @@ -504,7 +513,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' id: multi-output-named-resultNameSeparatorMC uses: ./ @@ -519,7 +528,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as action outputs of given name' id: multi-output-named1 uses: ./ @@ -547,7 +556,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" colonInValue evalAndAssertUndefined "$RESULT_GETTER" dash_1 - ############################################################ + # ===================================================================== - name: '[act] query single property as action outputs' id: single-output uses: ./ @@ -593,11 +602,12 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + ######################################################################### test_json: runs-on: ubuntu-latest steps: - *checkoutStep - ############################################################ + # ===================================================================== - name: '[act] query all properties as JSON action output' id: all-json uses: ./ @@ -610,7 +620,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl - ############################################################ + # ===================================================================== - name: '[act] query multiple properties as JSON action output' id: multiple-json uses: ./ @@ -624,11 +634,12 @@ jobs: RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiImpl + ######################################################################### test_json-file: runs-on: ubuntu-latest steps: - *checkoutStep - ############################################################ + # ===================================================================== - name: '[arrange] query multiple properties as JSON file (creating file)' id: multiple-json-file-create-arrange run: |- @@ -648,7 +659,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[arrange] query multiple properties as JSON file (overwriting file)' id: multiple-json-file-overwrite-arrange run: |- @@ -670,7 +681,7 @@ jobs: RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl - ############################################################ + # ===================================================================== - name: '[arrange] query all properties as JSON file' id: all-json-file-arrange # Distinction between creating and overwriting need not be tested again. @@ -693,11 +704,12 @@ jobs: # Each test which sets environment variables is a separate job to be independant of other tests. - ############################################################ + ######################################################################### test_env_all: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query all properties as environment variables' uses: ./ with: @@ -708,11 +720,12 @@ jobs: RESULT_GETTER: getEnv run: *validateAllImpl - ############################################################ + ######################################################################### test_env_multi: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query multiple properties as environment variables' uses: ./ with: @@ -724,11 +737,12 @@ jobs: RESULT_GETTER: getEnv run: *validateMultiImpl - ############################################################ + ######################################################################### test_env-named_multi: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query multiple properties as environment variables of given names' uses: ./ with: @@ -740,11 +754,12 @@ jobs: RESULT_GETTER: getEnv run: *validateMultiNamedImpl - ############################################################ + ######################################################################### test_env-named_multi-to-one: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query multiple properties as environment variables of given names' uses: ./ with: @@ -756,11 +771,12 @@ jobs: RESULT_GETTER: getEnv run: *validateMultiNamed1Impl - ############################################################ + ######################################################################### test_env_single: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query single property as environment variable' uses: ./ with: @@ -772,11 +788,12 @@ jobs: RESULT_GETTER: getEnv run: *validateSingleImpl - ############################################################ + ######################################################################### test_env-with-prefix_all: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query all properties as environment variables with prefix' uses: ./ with: @@ -787,11 +804,12 @@ jobs: RESULT_GETTER: 'getEnvWithPrefix ALL_PROPS__' run: *validateAllImpl - ############################################################ + ######################################################################### test_env-with-prefix_multi: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query multiple properties as environment variables with prefix' uses: ./ with: @@ -803,11 +821,12 @@ jobs: RESULT_GETTER: 'getEnvWithPrefix MULTI_PROPS__' run: *validateMultiImpl - ############################################################ + ######################################################################### test_env-with-prefix_single: runs-on: ubuntu-latest steps: - *checkoutStep + # ===================================================================== - name: '[act] query single property as environment variable with prefix' uses: ./ with: diff --git a/Action.java b/Action.java index d57e716..6d55a48 100644 --- a/Action.java +++ b/Action.java @@ -29,11 +29,16 @@ static void main(String[] args) throws Exception { ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { Properties properties = Util.readProperties(file); - Map selectedProperties = config.selectedKeys() + // If selectedKeys is set, this Map will have exactly those keys and values may be empty (where selected key is + // not found in properties). + // Otherwise, this Map will have the same keys as the properties file, and "not found" is not possible. + Map> selectedProperties = config.selectedKeys() .map(keys -> Util.selectProperties(properties, keys, file)) // .orElseGet(() -> Util.stringEntries(properties)); resultWriter.write(selectedProperties, config); } + } catch (IoRuntimeException e) { + throw e.getCause(); } catch (OutputException e) { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write(Ids.OutputName.ERROR, e.getMessage()); @@ -97,6 +102,18 @@ enum OutputName { } +class IoRuntimeException extends RuntimeException { + public IoRuntimeException(IOException cause) { + super(cause); + } + + @Override + public synchronized IOException getCause() { + return (IOException) super.getCause(); + } +} + + /** * An exception for which an "error" output should be set. */ @@ -156,17 +173,17 @@ public void requireNoArg() { enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - for (Map.Entry entry : props.entrySet()) { + for (Map.Entry> entry : props.entrySet()) { String key = encodeKey(config.outputPrefix() + entry.getKey()); - lastValue = entry.getValue(); + lastValue = entry.getValue().orElse(""); writer.write(key, lastValue); } - if (config.selectedKeys().isPresent() && lastValue != null) { - writer.write(Ids.OutputName.VALUE, lastValue); + if (config.selectedKeys().isPresent()) { + writer.write(Ids.OutputName.VALUE, lastValue != null ? lastValue : ""); } } } @@ -183,30 +200,31 @@ private static String encodeKey(String key) { }, OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override - public void write(Map props, Config config) throws IOException { - writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); + public void write(Map> props, Config config) throws IOException { + writeNamedImpl(props, config, true, GitHubOutputFile.OUTPUT); } }, ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override - public void write(Map props, Config config) throws IOException { - writeNamedImpl(props, config, GitHubOutputFile.ENV); + public void write(Map> props, Config config) throws IOException { + writeNamedImpl(props, config, false, GitHubOutputFile.ENV); } }, ENV(Ids.ResultWriterName.ENV) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { String prefix = config.resultTypeArg(); try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { - for (Map.Entry entry : props.entrySet()) { - writer.write(prefix + entry.getKey(), entry.getValue()); + for (Map.Entry> entry : props.entrySet()) { + Optional value = entry.getValue(); + value.ifPresent(v -> writer.write(prefix + entry.getKey(), v)); } } } }, JSON(Ids.ResultWriterName.JSON) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write(Ids.OutputName.JSON, Util.toJson(props)); @@ -215,7 +233,7 @@ public void write(Map props, Config config) throws IOException { }, JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Path parentDir = Paths.get(outputFile).getParent(); if (parentDir != null) { @@ -246,10 +264,10 @@ public static ResultWriter of(String externalName) { throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + externalName); } - public abstract void write(Map props, Config config) throws IOException; + public abstract void write(Map> props, Config config) throws IOException; - private static void writeNamedImpl(Map props, Config config, GitHubOutputFile gitHubOutputFile) - throws IOException { + private static void writeNamedImpl(Map> props, Config config, boolean includeMissing, + GitHubOutputFile gitHubOutputFile) throws IOException { List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " + Ids.ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + Ids.ConfigVariable.KEYS + ")")); @@ -263,8 +281,14 @@ private static void writeNamedImpl(Map props, Config config, Git try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; - String value = props.get(selectedKeys.get(i)); - writer.write(name, value != null ? value : ""); + Optional value = props.get(selectedKeys.get(i)); + value.ifPresentOrElse(v -> writer.write(name, v), () -> { + if (includeMissing) { + writer.write(name, ""); + } else { + // Nothing to do. "not found" has already been logged. + } + }); } } } @@ -300,21 +324,25 @@ public GitHubVariableWriter(String description, String fileName) throws IOExcept this.writer = Util.openFile(fileName, StandardOpenOption.CREATE, StandardOpenOption.APPEND); } - public void write(String key, String value) throws IOException { - System.err.format("setting %s %s%n", description, key); - - // write (very) simple values in format "=": - if (SIMPLE_VALUE.matcher(value).matches()) { - this.writer.write(key); - this.writer.write('='); - this.writer.write(value); - this.writer.write('\n'); - } else { - writeMultiLine(key, value); + public void write(String key, String value) { + try { + System.err.format("setting %s %s%n", description, key); + + // write (very) simple values in format "=": + if (SIMPLE_VALUE.matcher(value).matches()) { + this.writer.write(key); + this.writer.write('='); + this.writer.write(value); + this.writer.write('\n'); + } else { + writeMultiLine(key, value); + } + } catch (IOException e) { + throw new IoRuntimeException(e); } } - public void write(Ids.OutputName key, String value) throws IOException { + public void write(Ids.OutputName key, String value) { write(key.externalName, value); } @@ -393,13 +421,13 @@ public static Properties readProperties(String file) throws IOException { /** * Selects the properties with the given keys and returns them as a Map with the corresponding iteration order. */ - public static Map selectProperties(Properties allProps, List selectedKeys, String file) { + public static Map> selectProperties(Properties allProps, List selectedKeys, String file) { Set unmatchedKeysSet = new LinkedHashSet<>(selectedKeys); - Map results = new LinkedHashMap<>(); + Map> results = new LinkedHashMap<>(); for (String key : selectedKeys) { String value = allProps.getProperty(key); + results.put(key, Optional.ofNullable(value)); if (value != null) { - results.put(key, value); unmatchedKeysSet.remove(key); } } @@ -409,23 +437,29 @@ public static Map selectProperties(Properties allProps, List stringEntries(Properties props) { - Map map = new LinkedHashMap<>(); + /** + * @return the Properties as a Map with each value as a non-empty Optional (strange, but useful for our use case) + */ + public static Map> stringEntries(Properties props) { + Map> map = new LinkedHashMap<>(); for (String key : props.stringPropertyNames()) { - map.put(key, props.getProperty(key)); + map.put(key, Optional.of(props.getProperty(key))); } return map; } - public static String toJson(Map map) { + public static String toJson(Map> map) { StringBuilder s = new StringBuilder(50).append('{'); int initialLength = s.length(); - for (Map.Entry entry : map.entrySet()) { + for (Map.Entry> entry : map.entrySet()) { s.append(s.length() == initialLength ? '"' : ", \""); appendJsonString(s, entry.getKey()); - s.append("\": \""); - appendJsonString(s, entry.getValue()); - s.append('"'); + s.append("\": "); + entry.getValue().ifPresentOrElse(value -> { + s.append('"'); + appendJsonString(s, value); + s.append('"'); + }, () -> s.append("null")); } return s.append('}').toString(); } diff --git a/action.yml b/action.yml index efa1af4..87d5ccc 100644 --- a/action.yml +++ b/action.yml @@ -27,16 +27,16 @@ inputs: Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name "a-002Eb-002Dc_d". - Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) An output is set to the empty string if the corresponding selected key does not exist in the properties. - - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. The output is set to the empty string if no selected key exists in the properties. + Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. If `keys` is given, but none is found, "value" is set to "". (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) + - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) + - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. If none is found, the output is set to "". - "env": For each property key , set an environment variable . In order not to pollute the environment with hard to understand variables, this should only be used to set some specifically named variables. I.e. you know that the property file can only contain such properties or you are selecting only such properties with `keys`. - "env:": For each property key , set an environment variable . - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. - "env-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same environment variable; the last found key wins. In this mode, `keys` is required. - - "json": Set an action output "json" to all properties as a JSON object, formatted as a single-line string. - - "json-file:": Write a file with name with contents: all properties as a JSON object (formatting not specified). + - "json": Set an action output "json" to all (selected, if keys is set) properties as a JSON object, formatted as a single-line string. Selected keys for which no property is found are included with value null. + - "json-file:": Write a file with name with contents: all (selected, if keys is set) properties as a JSON object (formatting not specified). Selected keys for which no property is found are included with value null. required: false default: 'output' From 38bff421137cb86eee70ee141072467e9253e216 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:28:13 +0200 Subject: [PATCH 120/190] missing property handling - outputs: set to empty - environment variables: do not set - json: set key with value null --- .github/workflows/test.yml | 243 +++++++++++++++++++++++++------------ 1 file changed, 168 insertions(+), 75 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9b07a3..0b56c8f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,10 +4,12 @@ defaults: run: shell: bash env: - SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' + SELECTED_KEY: 'sourceJavaVersion' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' SELECTED_KEYS_OTHER_ORDER: 'colon:in:Key sourceJavaVersion org.gradle.jvmargs unicode_escapes' - SELECTED_SINGLE_NAME: 'my-prop' + SELECTED_NON_EX_KEY: 'nonExistingKey1' + SELECTED_NON_EX_KEY2: 'nonExistingKey2' + SELECTED_NAME: 'my-prop' SELECTED_NAMES: 'java-version jvm_args unicode colon' NOT_FOUND_STATUS: 54 @@ -91,6 +93,19 @@ env: jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" } + assertJsonDoesNotContainKey() { + if assertJsonContainsKey "$@"; then return 1; else return 0; fi + } + + assertJsonContainsKey() { + local json=$1; shift + local key=$1; shift + debugPrintf 'assertJsonDoesNotContainKey("%s", "%s")\n' "$json" "$key" + local contains + contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null') + assertEquals "$contains" true + } + # Extracts the value for key from JSON $2. encodeKeyAndGetJsonValue() { local json=$1; shift @@ -137,7 +152,7 @@ jobs: - test_env_all - test_env_multi - test_env-named_multi - - test_env-named_multi-to-one + - test_env-named_multi-to-single - test_env_single - test_env-with-prefix_all - test_env-with-prefix_multi @@ -261,14 +276,14 @@ jobs: - *checkoutStep # ===================================================================== - name: '[act] query single property with default resultType' - id: single-default + id: default_single uses: ./ with: file: test/resources/test.properties keys: sourceJavaVersion - name: '[assert] query single property with default resultType' env: - RESULT: ${{steps.single-default.outputs._sourceJavaVersion}} + RESULT: ${{steps.default_single.outputs._sourceJavaVersion}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT" '21' @@ -282,7 +297,7 @@ jobs: # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. - name: '[act] query multiple properties as action outputs (simple)' - id: multi-output-simple + id: output_multi_simple uses: ./ with: file: test/resources/test.properties @@ -290,11 +305,11 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (simple)' env: - RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} - RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} - RESULT_COLONK: ${{steps.multi-output-simple.outputs._colon-003Ain-003AKey}} - RESULT_DASHK: ${{steps.multi-output-simple.outputs._dash-002Din-002DKey}} - RESULT_UNI: ${{steps.multi-output-simple.outputs._unicode_escapes}} + RESULT_SJV: ${{steps.output_multi_simple.outputs._sourceJavaVersion}} + RESULT_JVMARGS: ${{steps.output_multi_simple.outputs._org-002Egradle-002Ejvmargs}} + RESULT_COLONK: ${{steps.output_multi_simple.outputs._colon-003Ain-003AKey}} + RESULT_DASHK: ${{steps.output_multi_simple.outputs._dash-002Din-002DKey}} + RESULT_UNI: ${{steps.output_multi_simple.outputs._unicode_escapes}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_SJV" '21' @@ -305,14 +320,14 @@ jobs: # ===================================================================== - name: '[act] query all properties as action outputs' - id: all-output + id: output_all uses: ./ with: file: test/resources/test.properties resultType: output - name: '[assert] query all properties as action outputs' env: - RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_all.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- eval "$SHELL_SETUP" @@ -354,7 +369,7 @@ jobs: # ===================================================================== - name: '[act] query multiple properties as action outputs' - id: multi-output + id: output_multi uses: ./ with: file: test/resources/test.properties @@ -363,7 +378,7 @@ jobs: - name: '[assert] query multiple properties as action outputs' env: - RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateMultiImpl |- eval "$SHELL_SETUP" @@ -407,7 +422,7 @@ jobs: # ===================================================================== - name: '[act] query multiple properties as action outputs (different keySeparator)' - id: multi-output-keySeparator + id: output_multi_keySeparator uses: ./ with: file: test/resources/test.properties @@ -416,13 +431,13 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (different keySeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi_keySeparator.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl # ===================================================================== - name: '[act] query multiple properties as action outputs (different multi-char keySeparator)' - id: multi-output-keySeparatorMC + id: output_multi_keySeparatorMC uses: ./ with: file: test/resources/test.properties @@ -431,13 +446,13 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (different multi-char keySeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-keySeparatorMC.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi_keySeparatorMC.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl # ===================================================================== - name: '[act] query multiple properties as action outputs (in another keys order)' - id: multi-output-2 + id: output_multi2 uses: ./ with: file: test/resources/test.properties @@ -445,7 +460,7 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (in another keys order)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-2.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi2.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - name: '[assert] query multiple properties as action outputs (in another keys order) (additional "value" output)' @@ -455,6 +470,99 @@ jobs: eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" + # ===================================================================== + - name: '[act] query single property as action outputs' + id: output_single + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEY}}' + resultType: output + + - name: '[assert] query multiple properties as action outputs' + env: + RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: &validateSingleImpl |- + eval "$SHELL_SETUP" + evalAndAssertEquals "$RESULT_GETTER" "SELECTED_KEY" '21' + + evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' + evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key + evalAndAssertUndefined "$RESULT_GETTER" dashInValue + evalAndAssertUndefined "$RESULT_GETTER" empty + evalAndAssertUndefined "$RESULT_GETTER" null + evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_3 + evalAndAssertUndefined "$RESULT_GETTER" dash_4 + evalAndAssertUndefined "$RESULT_GETTER" dash_5 + evalAndAssertUndefined "$RESULT_GETTER" dash_6 + evalAndAssertUndefined "$RESULT_GETTER" dash_7 + evalAndAssertUndefined "$RESULT_GETTER" dash_8 + evalAndAssertUndefined "$RESULT_GETTER" dash_9 + evalAndAssertUndefined "$RESULT_GETTER" dash_10 + evalAndAssertUndefined "$RESULT_GETTER" dash_11 + evalAndAssertUndefined "$RESULT_GETTER" dash_12 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + + # ===================================================================== + - name: '[act] query single missing property as action output' + id: output_single_missing + uses: ./ + with: + file: test/resources/test.properties + keys: ${{env.SELECTED_NON_EX_KEY}} + resultType: output + - name: '[assert] query single missing property as action output' + env: + RESULTS_JSON: ${{toJSON(steps.output_single_missing.outputs)}} + RESULT: ${{toJSON(steps.output_single_missing.outputs[env.SELECTED_NON_EX_KEY])}} + RESULT_VALUE: ${{toJSON(steps.output_single_missing.outputs.value)}} + run: &validateSingleMissingOutput |- + eval "$SHELL_SETUP" + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT" '' + assertEquals "$RESULT_VALUE" '' + # Assert set: + assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" + assertJsonContainsKey "$RESULTS_JSON" value + + # ===================================================================== + - name: '[act] query multiple missing properties as action outputs' + id: output_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: output + - name: '[assert] query multiple missing properties as action outputs' + env: + RESULTS_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} + RESULT1: ${{toJSON(steps.output_multi_missing.outputs[env.SELECTED_NON_EX_KEY])}} + RESULT2: ${{toJSON(steps.output_multi_missing.outputs[env.SELECTED_NON_EX_KEY2])}} + RESULT_VALUE: ${{toJSON(steps.output_multi_missing.outputs.value)}} + run: &validateMultiMissingOutput |- + eval "$SHELL_SETUP" + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT1" '' + assertEquals "$RESULT2" '' + assertEquals "$RESULT_VALUE" '' + # Assert not set: + assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" + assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY2")" + assertJsonContainsKey "$RESULTS_JSON" value + ######################################################################### test_output-named: runs-on: ubuntu-latest @@ -462,7 +570,7 @@ jobs: - *checkoutStep # ===================================================================== - name: '[act] query multiple properties as action outputs of given names' - id: multi-output-named + id: output-named_multi uses: ./ with: file: test/resources/test.properties @@ -471,7 +579,7 @@ jobs: - name: '[assert] query multiple properties as action outputs of given names' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamedImpl |- @@ -493,14 +601,14 @@ jobs: - name: '[assert] query multiple properties as action outputs (additional "value" output not set)' env: - RESULT_VALUE: ${{steps.multi-output-named.outputs.value || 'U_N_S_E_T'}} + RESULT_VALUE: ${{steps.output-named_multi.outputs.value || 'U_N_S_E_T'}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== - name: '[act] query multiple properties as action outputs of given names (different resultNameSeparator)' - id: multi-output-named-resultNameSeparator + id: output-named_multi_resultNameSeparator uses: ./ with: file: test/resources/test.properties @@ -509,13 +617,13 @@ jobs: resultType: 'output-named:java-version?jvm_args?unicode?colon' - name: '[assert] query multiple properties as action outputs of given names (different resultNameSeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi_resultNameSeparator.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl # ===================================================================== - name: '[act] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' - id: multi-output-named-resultNameSeparatorMC + id: output-named_multi_resultNameSeparatorMC uses: ./ with: file: test/resources/test.properties @@ -524,27 +632,27 @@ jobs: resultType: 'output-named:java-version- -jvm_args- -unicode- -colon' - name: '[assert] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparatorMC.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi_resultNameSeparatorMC.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl # ===================================================================== - name: '[act] query multiple properties as action outputs of given name' - id: multi-output-named1 + id: output-named_multi-to-single uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' + resultType: 'output-named:${{env.SELECTED_NAME}}' - name: '[assert] query multiple properties as action outputs of given name' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi-to-single.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_SINGLE_NAME" 'c' + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NAME" 'c' # The outputs with the default names are not set: evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion @@ -557,50 +665,35 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_1 # ===================================================================== - - name: '[act] query single property as action outputs' - id: single-output + - name: '[act] query single missing property as action output of given names' + id: output-named_single_missing uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_SINGLE_KEY}}' - resultType: output - - - name: '[assert] query multiple properties as action outputs' + keys: ${{env.SELECTED_NON_EX_KEY}} + resultType: 'output-named:${{env.SELECTED_NAME}}' + - name: '[assert] query single missing property as action output of given names' env: - RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} - RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' - run: &validateSingleImpl |- - eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + RESULTS_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} + RESULT: ${{toJSON(steps.output-named_single_missing.outputs[env.SELECTED_NON_EX_KEY])}} + RESULT_VALUE: ${{toJSON(steps.output-named_single_missing.outputs.value)}} + run: *validateSingleMissingOutput - evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion - evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key - evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' - evalAndAssertUndefined "$RESULT_GETTER" colonInValue - evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' - evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key - evalAndAssertUndefined "$RESULT_GETTER" dashInValue - evalAndAssertUndefined "$RESULT_GETTER" empty - evalAndAssertUndefined "$RESULT_GETTER" null - evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes - evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 - evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 - evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes - evalAndAssertUndefined "$RESULT_GETTER" dash_1 - evalAndAssertUndefined "$RESULT_GETTER" dash_2 - evalAndAssertUndefined "$RESULT_GETTER" dash_3 - evalAndAssertUndefined "$RESULT_GETTER" dash_4 - evalAndAssertUndefined "$RESULT_GETTER" dash_5 - evalAndAssertUndefined "$RESULT_GETTER" dash_6 - evalAndAssertUndefined "$RESULT_GETTER" dash_7 - evalAndAssertUndefined "$RESULT_GETTER" dash_8 - evalAndAssertUndefined "$RESULT_GETTER" dash_9 - evalAndAssertUndefined "$RESULT_GETTER" dash_10 - evalAndAssertUndefined "$RESULT_GETTER" dash_11 - evalAndAssertUndefined "$RESULT_GETTER" dash_12 - evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 - evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + # ===================================================================== + - name: '[act] query multiple missing properties as action outputs of given names' + id: output-named_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: output + - name: '[assert] query multiple missing properties as action outputs of given names' + env: + RESULTS_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} + RESULT1: ${{toJSON(steps.output-named_multi_missing.outputs[env.SELECTED_NON_EX_KEY])}} + RESULT2: ${{toJSON(steps.output-named_multi_missing.outputs[env.SELECTED_NON_EX_KEY2])}} + RESULT_VALUE: ${{toJSON(steps.output-named_multi_missing.outputs.value)}} + run: *validateMultiMissingOutput ######################################################################### test_json: @@ -755,7 +848,7 @@ jobs: run: *validateMultiNamedImpl ######################################################################### - test_env-named_multi-to-one: + test_env-named_multi-to-single: runs-on: ubuntu-latest steps: - *checkoutStep @@ -765,7 +858,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'env-named:${{env.SELECTED_SINGLE_NAME}}' + resultType: 'env-named:${{env.SELECTED_NAME}}' - name: '[assert] query multiple properties as environment variables of given names' env: RESULT_GETTER: getEnv @@ -781,7 +874,7 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_SINGLE_KEY}}' + keys: '${{env.SELECTED_KEY}}' resultType: env - name: '[assert] query single property as environment variable' env: @@ -831,7 +924,7 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_SINGLE_KEY}}' + keys: '${{env.SELECTED_KEY}}' resultType: 'env:PROP__' - name: '[assert] query single property as environment variable with prefix' env: From 60640908c4f5745ba16ceb35951451a27708e2ca Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:31:00 +0200 Subject: [PATCH 121/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b56c8f..f03b31b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -362,7 +362,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' - name: '[assert] query all properties as action outputs (additional "value" output not set)' env: - RESULT_VALUE: ${{steps.all-output.outputs.value || 'U_N_S_E_T'}} + RESULT_VALUE: ${{steps.output_all.outputs.value || 'U_N_S_E_T'}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T @@ -415,7 +415,7 @@ jobs: - name: '[assert] query multiple properties as action outputs (additional "value" output)' env: - RESULT_VALUE: ${{steps.multi-output.outputs.value}} + RESULT_VALUE: ${{steps.output_multi.outputs.value}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" @@ -465,7 +465,7 @@ jobs: run: *validateMultiImpl - name: '[assert] query multiple properties as action outputs (in another keys order) (additional "value" output)' env: - RESULT_VALUE: ${{steps.multi-output-2.outputs.value}} + RESULT_VALUE: ${{steps.output_multi2.outputs.value}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" @@ -481,7 +481,7 @@ jobs: - name: '[assert] query multiple properties as action outputs' env: - RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_single.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateSingleImpl |- eval "$SHELL_SETUP" From bbaee91dbae1ce037a7d841f2c17f01ae45a8c07 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:33:38 +0200 Subject: [PATCH 122/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f03b31b..db6c0b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -479,13 +479,13 @@ jobs: keys: '${{env.SELECTED_KEY}}' resultType: output - - name: '[assert] query multiple properties as action outputs' + - name: '[assert] query single properties as action outputs' env: RESULT_JSON: ${{toJSON(steps.output_single.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateSingleImpl |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" "SELECTED_KEY" '21' + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_KEY" '21' evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs From 111a5bcb8bfb1cbf69add8e565927eed4604d5b7 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:35:59 +0200 Subject: [PATCH 123/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db6c0b4..66a2f18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -487,7 +487,7 @@ jobs: eval "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_KEY" '21' - evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' From 4f47e83f51726cfe7c93c596f8bb5a5ec87fcfd8 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:39:37 +0200 Subject: [PATCH 124/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66a2f18..64b24f1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -527,7 +527,7 @@ jobs: - name: '[assert] query single missing property as action output' env: RESULTS_JSON: ${{toJSON(steps.output_single_missing.outputs)}} - RESULT: ${{toJSON(steps.output_single_missing.outputs[env.SELECTED_NON_EX_KEY])}} + RESULT: ${{toJSON(steps.output_single_missing.outputs.nonExistingKey1)}} RESULT_VALUE: ${{toJSON(steps.output_single_missing.outputs.value)}} run: &validateSingleMissingOutput |- eval "$SHELL_SETUP" @@ -549,8 +549,8 @@ jobs: - name: '[assert] query multiple missing properties as action outputs' env: RESULTS_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} - RESULT1: ${{toJSON(steps.output_multi_missing.outputs[env.SELECTED_NON_EX_KEY])}} - RESULT2: ${{toJSON(steps.output_multi_missing.outputs[env.SELECTED_NON_EX_KEY2])}} + RESULT1: ${{toJSON(steps.output_multi_missing.outputs.nonExistingKey1)}} + RESULT2: ${{toJSON(steps.output_multi_missing.outputs.nonExistingKey2)}} RESULT_VALUE: ${{toJSON(steps.output_multi_missing.outputs.value)}} run: &validateMultiMissingOutput |- eval "$SHELL_SETUP" @@ -665,17 +665,17 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_1 # ===================================================================== - - name: '[act] query single missing property as action output of given names' + - name: '[act] query single missing property as action output of given name' id: output-named_single_missing uses: ./ with: file: test/resources/test.properties keys: ${{env.SELECTED_NON_EX_KEY}} resultType: 'output-named:${{env.SELECTED_NAME}}' - - name: '[assert] query single missing property as action output of given names' + - name: '[assert] query single missing property as action output of given name' env: RESULTS_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} - RESULT: ${{toJSON(steps.output-named_single_missing.outputs[env.SELECTED_NON_EX_KEY])}} + RESULT: ${{toJSON(steps.output-named_single_missing.outputs.nonExistingKey1)}} RESULT_VALUE: ${{toJSON(steps.output-named_single_missing.outputs.value)}} run: *validateSingleMissingOutput @@ -690,8 +690,8 @@ jobs: - name: '[assert] query multiple missing properties as action outputs of given names' env: RESULTS_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} - RESULT1: ${{toJSON(steps.output-named_multi_missing.outputs[env.SELECTED_NON_EX_KEY])}} - RESULT2: ${{toJSON(steps.output-named_multi_missing.outputs[env.SELECTED_NON_EX_KEY2])}} + RESULT1: ${{toJSON(steps.output-named_multi_missing.outputs.nonExistingKey1)}} + RESULT2: ${{toJSON(steps.output-named_multi_missing.outputs.nonExistingKey2)}} RESULT_VALUE: ${{toJSON(steps.output-named_multi_missing.outputs.value)}} run: *validateMultiMissingOutput From b34b3229fdcd684e047418e8344f9d7655f6f4b6 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:43:46 +0200 Subject: [PATCH 125/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64b24f1..c2abd19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -527,7 +527,7 @@ jobs: - name: '[assert] query single missing property as action output' env: RESULTS_JSON: ${{toJSON(steps.output_single_missing.outputs)}} - RESULT: ${{toJSON(steps.output_single_missing.outputs.nonExistingKey1)}} + RESULT: ${{toJSON(steps.output_single_missing.outputs._nonExistingKey1)}} RESULT_VALUE: ${{toJSON(steps.output_single_missing.outputs.value)}} run: &validateSingleMissingOutput |- eval "$SHELL_SETUP" @@ -549,8 +549,8 @@ jobs: - name: '[assert] query multiple missing properties as action outputs' env: RESULTS_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} - RESULT1: ${{toJSON(steps.output_multi_missing.outputs.nonExistingKey1)}} - RESULT2: ${{toJSON(steps.output_multi_missing.outputs.nonExistingKey2)}} + RESULT1: ${{toJSON(steps.output_multi_missing.outputs._nonExistingKey1)}} + RESULT2: ${{toJSON(steps.output_multi_missing.outputs._nonExistingKey2)}} RESULT_VALUE: ${{toJSON(steps.output_multi_missing.outputs.value)}} run: &validateMultiMissingOutput |- eval "$SHELL_SETUP" From ad449ce7bb9521e68a7676dc09231507a6fbc15d Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:52:33 +0200 Subject: [PATCH 126/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2abd19..166dfa9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -471,7 +471,7 @@ jobs: assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" # ===================================================================== - - name: '[act] query single property as action outputs' + - name: '[act] query single property as action output' id: output_single uses: ./ with: @@ -479,7 +479,7 @@ jobs: keys: '${{env.SELECTED_KEY}}' resultType: output - - name: '[assert] query single properties as action outputs' + - name: '[assert] query single property as action output' env: RESULT_JSON: ${{toJSON(steps.output_single.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' @@ -552,13 +552,14 @@ jobs: RESULT1: ${{toJSON(steps.output_multi_missing.outputs._nonExistingKey1)}} RESULT2: ${{toJSON(steps.output_multi_missing.outputs._nonExistingKey2)}} RESULT_VALUE: ${{toJSON(steps.output_multi_missing.outputs.value)}} + # This is only reusable for output… resultTypes, because it calls encodeKey. run: &validateMultiMissingOutput |- eval "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT1" '' assertEquals "$RESULT2" '' assertEquals "$RESULT_VALUE" '' - # Assert not set: + # Assert set: assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY2")" assertJsonContainsKey "$RESULTS_JSON" value @@ -675,23 +676,23 @@ jobs: - name: '[assert] query single missing property as action output of given name' env: RESULTS_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} - RESULT: ${{toJSON(steps.output-named_single_missing.outputs.nonExistingKey1)}} + RESULT: ${{toJSON(steps.output-named_single_missing.outputs.my-prop)}} RESULT_VALUE: ${{toJSON(steps.output-named_single_missing.outputs.value)}} run: *validateSingleMissingOutput # ===================================================================== - - name: '[act] query multiple missing properties as action outputs of given names' + - name: '[act] query multiple missing properties as action outputs of given name' id: output-named_multi_missing uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' - resultType: output + resultType: 'output-named:${{env.SELECTED_NAME}}' - name: '[assert] query multiple missing properties as action outputs of given names' env: RESULTS_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} RESULT1: ${{toJSON(steps.output-named_multi_missing.outputs.nonExistingKey1)}} - RESULT2: ${{toJSON(steps.output-named_multi_missing.outputs.nonExistingKey2)}} + RESULT2: '' RESULT_VALUE: ${{toJSON(steps.output-named_multi_missing.outputs.value)}} run: *validateMultiMissingOutput From 1d401561e065f4f85eff0991698e3c4974c7142c Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 18:58:34 +0200 Subject: [PATCH 127/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 166dfa9..3f9b072 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -526,9 +526,9 @@ jobs: resultType: output - name: '[assert] query single missing property as action output' env: + RESULT: ${{steps.output_single_missing.outputs._nonExistingKey1}} + RESULT_VALUE: ${{steps.output_single_missing.outputs.value}} RESULTS_JSON: ${{toJSON(steps.output_single_missing.outputs)}} - RESULT: ${{toJSON(steps.output_single_missing.outputs._nonExistingKey1)}} - RESULT_VALUE: ${{toJSON(steps.output_single_missing.outputs.value)}} run: &validateSingleMissingOutput |- eval "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: @@ -548,10 +548,10 @@ jobs: resultType: output - name: '[assert] query multiple missing properties as action outputs' env: + RESULT1: ${{steps.output_multi_missing.outputs._nonExistingKey1}} + RESULT2: ${{steps.output_multi_missing.outputs._nonExistingKey2}} + RESULT_VALUE: ${{steps.output_multi_missing.outputs.value}} RESULTS_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} - RESULT1: ${{toJSON(steps.output_multi_missing.outputs._nonExistingKey1)}} - RESULT2: ${{toJSON(steps.output_multi_missing.outputs._nonExistingKey2)}} - RESULT_VALUE: ${{toJSON(steps.output_multi_missing.outputs.value)}} # This is only reusable for output… resultTypes, because it calls encodeKey. run: &validateMultiMissingOutput |- eval "$SHELL_SETUP" @@ -675,9 +675,9 @@ jobs: resultType: 'output-named:${{env.SELECTED_NAME}}' - name: '[assert] query single missing property as action output of given name' env: + RESULT: ${{steps.output-named_single_missing.outputs.my-prop}} + RESULT_VALUE: ${{steps.output-named_single_missing.outputs.value}} RESULTS_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} - RESULT: ${{toJSON(steps.output-named_single_missing.outputs.my-prop)}} - RESULT_VALUE: ${{toJSON(steps.output-named_single_missing.outputs.value)}} run: *validateSingleMissingOutput # ===================================================================== @@ -690,10 +690,10 @@ jobs: resultType: 'output-named:${{env.SELECTED_NAME}}' - name: '[assert] query multiple missing properties as action outputs of given names' env: - RESULTS_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} - RESULT1: ${{toJSON(steps.output-named_multi_missing.outputs.nonExistingKey1)}} + RESULT1: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} RESULT2: '' - RESULT_VALUE: ${{toJSON(steps.output-named_multi_missing.outputs.value)}} + RESULT_VALUE: ${{steps.output-named_multi_missing.outputs.value}} + RESULTS_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} run: *validateMultiMissingOutput ######################################################################### From 553ce0e06dbbc41b38f8317fa1a136c8276e2392 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 19:03:05 +0200 Subject: [PATCH 128/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f9b072..e4e6ef4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -106,12 +106,18 @@ env: assertEquals "$contains" true } + # Encodes a key $1 in the output-style encoding (property key to output name). + encodeKey() { + local key=$1; shift + perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" + } + # Extracts the value for key from JSON $2. encodeKeyAndGetJsonValue() { local json=$1; shift local key=$1; shift local encodedKey - encodedKey="_$(perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge' <<<"$key")" + encodedKey=$(encodeKey "$key") getJsonValue "$json" "$encodedKey" "$@" } From 17a2292c61bf09520936d7be2d1c25afe2bc2c50 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 19:18:17 +0200 Subject: [PATCH 129/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4e6ef4..72e839e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,16 +94,21 @@ env: } assertJsonDoesNotContainKey() { - if assertJsonContainsKey "$@"; then return 1; else return 0; fi + assertJsonContainsKeyEquals "$@" false } assertJsonContainsKey() { + assertJsonContainsKeyEquals "$@" true + } + + assertJsonContainsKeyEquals() { local json=$1; shift local key=$1; shift - debugPrintf 'assertJsonDoesNotContainKey("%s", "%s")\n' "$json" "$key" + local expected=$1; shift + debugPrintf 'assertJsonContainsKeyEquals("%s", "%s", "%s")\n' "$json" "$key" "$expected" local contains - contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null') - assertEquals "$contains" true + contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null' <<<"$json") + assertEquals "$contains" "$expected" } # Encodes a key $1 in the output-style encoding (property key to output name). @@ -534,15 +539,15 @@ jobs: env: RESULT: ${{steps.output_single_missing.outputs._nonExistingKey1}} RESULT_VALUE: ${{steps.output_single_missing.outputs.value}} - RESULTS_JSON: ${{toJSON(steps.output_single_missing.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_single_missing.outputs)}} run: &validateSingleMissingOutput |- eval "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' assertEquals "$RESULT_VALUE" '' # Assert set: - assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" - assertJsonContainsKey "$RESULTS_JSON" value + assertJsonContainsKey "$RESULT_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" + assertJsonContainsKey "$RESULT_JSON" value # ===================================================================== - name: '[act] query multiple missing properties as action outputs' @@ -557,7 +562,7 @@ jobs: RESULT1: ${{steps.output_multi_missing.outputs._nonExistingKey1}} RESULT2: ${{steps.output_multi_missing.outputs._nonExistingKey2}} RESULT_VALUE: ${{steps.output_multi_missing.outputs.value}} - RESULTS_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} # This is only reusable for output… resultTypes, because it calls encodeKey. run: &validateMultiMissingOutput |- eval "$SHELL_SETUP" @@ -566,9 +571,9 @@ jobs: assertEquals "$RESULT2" '' assertEquals "$RESULT_VALUE" '' # Assert set: - assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" - assertJsonContainsKey "$RESULTS_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY2")" - assertJsonContainsKey "$RESULTS_JSON" value + assertJsonContainsKey "$RESULT_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" + assertJsonContainsKey "$RESULT_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY2")" + assertJsonContainsKey "$RESULT_JSON" value ######################################################################### test_output-named: @@ -683,7 +688,7 @@ jobs: env: RESULT: ${{steps.output-named_single_missing.outputs.my-prop}} RESULT_VALUE: ${{steps.output-named_single_missing.outputs.value}} - RESULTS_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} run: *validateSingleMissingOutput # ===================================================================== @@ -699,7 +704,7 @@ jobs: RESULT1: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} RESULT2: '' RESULT_VALUE: ${{steps.output-named_multi_missing.outputs.value}} - RESULTS_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} run: *validateMultiMissingOutput ######################################################################### From 5678a68a9dc8ff3c6fb77d1bc16821a97fcd6411 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 19:26:59 +0200 Subject: [PATCH 130/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 72e839e..d9fa877 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -540,7 +540,7 @@ jobs: RESULT: ${{steps.output_single_missing.outputs._nonExistingKey1}} RESULT_VALUE: ${{steps.output_single_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output_single_missing.outputs)}} - run: &validateSingleMissingOutput |- + run: |- eval "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' @@ -563,8 +563,7 @@ jobs: RESULT2: ${{steps.output_multi_missing.outputs._nonExistingKey2}} RESULT_VALUE: ${{steps.output_multi_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} - # This is only reusable for output… resultTypes, because it calls encodeKey. - run: &validateMultiMissingOutput |- + run: |- eval "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT1" '' @@ -689,7 +688,14 @@ jobs: RESULT: ${{steps.output-named_single_missing.outputs.my-prop}} RESULT_VALUE: ${{steps.output-named_single_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} - run: *validateSingleMissingOutput + run: |- + eval "$SHELL_SETUP" + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT" '' + assertEquals "$RESULT_VALUE" '' + # Assert set: + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NAME" + assertJsonContainsKey "$RESULT_JSON" value # ===================================================================== - name: '[act] query multiple missing properties as action outputs of given name' @@ -701,11 +707,17 @@ jobs: resultType: 'output-named:${{env.SELECTED_NAME}}' - name: '[assert] query multiple missing properties as action outputs of given names' env: - RESULT1: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} - RESULT2: '' + RESULT: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} RESULT_VALUE: ${{steps.output-named_multi_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} - run: *validateMultiMissingOutput + run: |- + eval "$SHELL_SETUP" + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT" '' + assertEquals "$RESULT_VALUE" '' + # Assert set: + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NAME" + assertJsonContainsKey "$RESULT_JSON" value ######################################################################### test_json: From c7ff58cd80726fc0130f94741f78aaec1dbd8fd1 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 19:33:27 +0200 Subject: [PATCH 131/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9fa877..3211950 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -686,16 +686,13 @@ jobs: - name: '[assert] query single missing property as action output of given name' env: RESULT: ${{steps.output-named_single_missing.outputs.my-prop}} - RESULT_VALUE: ${{steps.output-named_single_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} run: |- eval "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' - assertEquals "$RESULT_VALUE" '' # Assert set: assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NAME" - assertJsonContainsKey "$RESULT_JSON" value # ===================================================================== - name: '[act] query multiple missing properties as action outputs of given name' @@ -708,16 +705,13 @@ jobs: - name: '[assert] query multiple missing properties as action outputs of given names' env: RESULT: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} - RESULT_VALUE: ${{steps.output-named_multi_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} run: |- eval "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' - assertEquals "$RESULT_VALUE" '' # Assert set: assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NAME" - assertJsonContainsKey "$RESULT_JSON" value ######################################################################### test_json: From 4f424462803e5c878b7b5b047bb7aba17435ead8 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 19:46:22 +0200 Subject: [PATCH 132/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 79 ++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3211950..755d490 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -195,7 +195,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): invalid input: resultType' - id: error-input-resultType + id: input-error_resultType continue-on-error: true uses: ./ with: @@ -203,8 +203,8 @@ jobs: resultType: rt1 - name: '[assert] (should fail): invalid input: resultType' env: - STATUS: ${{steps.error-input-resultType.outcome}} - ERROR: ${{steps.error-input-resultType.outputs.error}} + STATUS: ${{steps.input-error_resultType.outcome}} + ERROR: ${{steps.input-error_resultType.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -212,7 +212,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "output-named" without names' - id: error-resultType-output-without-names + id: input-error_resultType-output-without-names continue-on-error: true uses: ./ with: @@ -221,8 +221,8 @@ jobs: resultType: 'output-named:' - name: '[assert] (should fail): resultType "output-named" without names' env: - STATUS: ${{steps.error-resultType-output-without-names.outcome}} - ERROR: ${{steps.error-resultType-output-without-names.outputs.error}} + STATUS: ${{steps.input-error_resultType-output-without-names.outcome}} + ERROR: ${{steps.input-error_resultType-output-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -230,7 +230,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "env-named" without names' - id: error-resultType-env-without-names + id: input-error_resultType-env-without-names continue-on-error: true uses: ./ with: @@ -239,8 +239,8 @@ jobs: resultType: 'env-named:' - name: '[assert] (should fail): resultType "env-named" without names' env: - STATUS: ${{steps.error-resultType-env-without-names.outcome}} - ERROR: ${{steps.error-resultType-env-without-names.outputs.error}} + STATUS: ${{steps.input-error_resultType-env-without-names.outcome}} + ERROR: ${{steps.input-error_resultType-env-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -248,7 +248,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "output-named" without keys' - id: error-resultType-output-without-keys + id: input-error_resultType-output-without-keys continue-on-error: true uses: ./ with: @@ -256,8 +256,8 @@ jobs: resultType: 'output-named:a b' - name: '[assert] (should fail): resultType "output-named" without keys' env: - STATUS: ${{steps.error-resultType-output-without-keys.outcome}} - ERROR: ${{steps.error-resultType-output-without-keys.outputs.error}} + STATUS: ${{steps.input-error_resultType-output-without-keys.outcome}} + ERROR: ${{steps.input-error_resultType-output-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -265,7 +265,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "env-named" without keys' - id: error-resultType-env-without-keys + id: input-error_resultType-env-without-keys continue-on-error: true uses: ./ with: @@ -273,8 +273,8 @@ jobs: resultType: 'env-named:a b' - name: '[assert] (should fail): resultType "env-named" without keys' env: - STATUS: ${{steps.error-resultType-env-without-keys.outcome}} - ERROR: ${{steps.error-resultType-env-without-keys.outputs.error}} + STATUS: ${{steps.input-error_resultType-env-without-keys.outcome}} + ERROR: ${{steps.input-error_resultType-env-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -720,20 +720,20 @@ jobs: - *checkoutStep # ===================================================================== - name: '[act] query all properties as JSON action output' - id: all-json + id: json_all uses: ./ with: file: test/resources/test.properties resultType: json - name: '[assert] query all properties as JSON action output' env: - RESULT_JSON: ${{steps.all-json.outputs.json}} + RESULT_JSON: ${{steps.json_all.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl # ===================================================================== - name: '[act] query multiple properties as JSON action output' - id: multiple-json + id: json_multi uses: ./ with: file: test/resources/test.properties @@ -741,10 +741,33 @@ jobs: resultType: json - name: '[assert] query multiple properties as JSON action output' env: - RESULT_JSON: ${{steps.multiple-json.outputs.json}} + RESULT_JSON: ${{steps.json_multi.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiImpl + # ===================================================================== + - name: '[act] query multiple missing properties as JSON action output' + id: json_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEYS}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: json + - name: '[assert] query multiple missing properties as JSON action output' + env: + RESULT_JSON: ${{toJSON(steps.json_multi_missing.outputs)}} + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' + run: |- + eval "$SHELL_SETUP" + evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' + evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' + evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NON_EX_KEY" '' + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NON_EX_KEY2" '' + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NON_EX_KEY" + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NON_EX_KEY2" + ######################################################################### test_json-file: runs-on: ubuntu-latest @@ -752,7 +775,7 @@ jobs: - *checkoutStep # ===================================================================== - name: '[arrange] query multiple properties as JSON file (creating file)' - id: multiple-json-file-create-arrange + id: json-file_multi_create_arrange run: |- parentDir=$(mktemp --directory --dry-run --tmpdir=. test_json-file_multi_create_XXXXXX) file=$parentDir/out/result.json @@ -763,16 +786,16 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' + resultType: 'json-file:${{steps.json-file_multi_create_arrange.outputs.file}}' - name: '[assert] query multiple properties as JSON file (creating file)' env: - JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} + JSON_OUTPUT_FILE: ${{steps.json-file_multi_create_arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl # ===================================================================== - name: '[arrange] query multiple properties as JSON file (overwriting file)' - id: multiple-json-file-overwrite-arrange + id: json-file_multi_overwrite_arrange run: |- file=$(mktemp --tmpdir=. test_json-file_multi_overwrite_XXXXXX.json) # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be @@ -785,16 +808,16 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'json-file:${{steps.multiple-json-file-overwrite-arrange.outputs.file}}' + resultType: 'json-file:${{steps.json-file_multi_overwrite_arrange.outputs.file}}' - name: '[assert] query multiple properties as JSON file (overwriting file)' env: - JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} + JSON_OUTPUT_FILE: ${{steps.json-file_multi_overwrite_arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl # ===================================================================== - name: '[arrange] query all properties as JSON file' - id: all-json-file-arrange + id: json-file_all_arrange # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. run: |- @@ -806,10 +829,10 @@ jobs: uses: ./ with: file: test/resources/test.properties - resultType: 'json-file:${{steps.all-json-file-arrange.outputs.file}}' + resultType: 'json-file:${{steps.json-file_all_arrange.outputs.file}}' - name: '[assert] query all properties as JSON file' env: - JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} + JSON_OUTPUT_FILE: ${{steps.json-file_all_arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateAllImpl From fdc20ad43b6b9025edc86cbf9e256d6b67c99c82 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:06:25 +0200 Subject: [PATCH 133/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 755d490..57720c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -62,7 +62,13 @@ env: local quotedArgs value quotedArgs=$(printf ' %q' "${args[@]}") + + # Call $getterFunction once to check its status only before the call to capture its output. + # Otherwise, we'd get a misleading error from replaceLastLF if $getterFunction prints nothing and exits + # with $NOT_FOUND_STATUS. + eval "${getterFunction}${quotedArgs}" >/dev/null eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" + expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" assertEquals "$value" "$expectedValue" } @@ -90,7 +96,7 @@ env: local json=$1; shift local key=$1; shift debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" - jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" + jq --arg k "$key" --raw-output '.[$k] // (("key " + $key + " not found") | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" } assertJsonDoesNotContainKey() { @@ -134,6 +140,7 @@ env: if printenv -- "$key"; then : # OK elif [ ${st-$?} -eq 1 ]; then + printf 'environment variable %s not set\n' "$key" >&2 return $NOT_FOUND_STATUS else # no error message, assuming printenv has already written one @@ -757,7 +764,7 @@ jobs: env: RESULT_JSON: ${{toJSON(steps.json_multi_missing.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' - run: |- + run: &validateMultiMissingJsonImpl |- eval "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' @@ -836,6 +843,19 @@ jobs: RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateAllImpl + # ===================================================================== + - name: '[act] query multiple missing properties as JSON file' + id: json-file_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEYS}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: json + - name: '[assert] query multiple missing properties as JSON file' + env: + RESULT_JSON: ${{toJSON(steps.json-file_multi_missing.outputs)}} + RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' + run: *validateMultiMissingJsonImpl # Each test which sets environment variables is a separate job to be independant of other tests. ######################################################################### From 9abe6f4db75ee512aefab499bf838181a8e6cf8a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:07:24 +0200 Subject: [PATCH 134/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57720c2..cbb90c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ env: local json=$1; shift local key=$1; shift debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" - jq --arg k "$key" --raw-output '.[$k] // (("key " + $key + " not found") | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" + jq --arg k "$key" --raw-output '.[$k] // (("key " + $k + " not found") | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" } assertJsonDoesNotContainKey() { From 0ca3bd2be651185df1f0df0ead0cd7686455fe0e Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:13:08 +0200 Subject: [PATCH 135/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cbb90c3..ab78c88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,7 @@ env: local json=$1; shift local key=$1; shift debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" - jq --arg k "$key" --raw-output '.[$k] // (("key " + $k + " not found") | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" + jq --arg k "$key" --raw-output '.[$k] // (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" } assertJsonDoesNotContainKey() { @@ -762,7 +762,7 @@ jobs: resultType: json - name: '[assert] query multiple missing properties as JSON action output' env: - RESULT_JSON: ${{toJSON(steps.json_multi_missing.outputs)}} + RESULT_JSON: ${{toJSON(steps.json_multi_missing.outputs.json)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiMissingJsonImpl |- eval "$SHELL_SETUP" @@ -827,7 +827,7 @@ jobs: id: json-file_all_arrange # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. - run: |- + run: &createEmptyJsonFile |- # Remove leading "./" ⇒ Also test the case where given file has "no parent" (i.e. Java method # java.nio.file.Path.getParent() returns null). file=$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json | sed 's=^\./==') @@ -844,6 +844,9 @@ jobs: run: *validateAllImpl # ===================================================================== + - name: '[arrange] query multiple missing properties as JSON file' + id: json-file_multi_missing_arrange + run: *createEmptyJsonFile - name: '[act] query multiple missing properties as JSON file' id: json-file_multi_missing uses: ./ @@ -853,8 +856,8 @@ jobs: resultType: json - name: '[assert] query multiple missing properties as JSON file' env: - RESULT_JSON: ${{toJSON(steps.json-file_multi_missing.outputs)}} - RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' + JSON_OUTPUT_FILE: ${{steps.json-file_multi_missing_arrange.outputs.file}} + RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiMissingJsonImpl # Each test which sets environment variables is a separate job to be independant of other tests. From 19d4ea70fb241154b61c27fb6e78438351bfa407 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:14:42 +0200 Subject: [PATCH 136/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab78c88..7cc8932 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -853,7 +853,7 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEYS}} ${{env.SELECTED_NON_EX_KEY2}}' - resultType: json + resultType: 'json-file:${{steps.json-file_multi_missing_arrange.outputs.file}}' - name: '[assert] query multiple missing properties as JSON file' env: JSON_OUTPUT_FILE: ${{steps.json-file_multi_missing_arrange.outputs.file}} From 5e47ba257d79b0265de30cfa22b9fc9ec7b32e25 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:16:44 +0200 Subject: [PATCH 137/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7cc8932..0cf66c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -762,7 +762,7 @@ jobs: resultType: json - name: '[assert] query multiple missing properties as JSON action output' env: - RESULT_JSON: ${{toJSON(steps.json_multi_missing.outputs.json)}} + RESULT_JSON: ${{steps.json_multi_missing.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiMissingJsonImpl |- eval "$SHELL_SETUP" From 0da279a45cfe55fd2bfdd1b9f830359de165a090 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:38:30 +0200 Subject: [PATCH 138/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 10 +++++++++- Action.java | 28 +++++++++++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0cf66c0..1bef274 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,7 +96,15 @@ env: local json=$1; shift local key=$1; shift debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" - jq --arg k "$key" --raw-output '.[$k] // (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" + jq --arg k "$key" --raw-output ' + .[$k] as $v + | if $v != null then + $v + elif (keys | index($k) != null) then #contains key $k (with a null or false value) + "" + else + (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber)) + end' <<<"$json" } assertJsonDoesNotContainKey() { diff --git a/Action.java b/Action.java index 6d55a48..4759a0e 100644 --- a/Action.java +++ b/Action.java @@ -16,6 +16,7 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -227,7 +228,7 @@ public void write(Map> props, Config config) throws IOE public void write(Map> props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - writer.write(Ids.OutputName.JSON, Util.toJson(props)); + writer.write(Ids.OutputName.JSON, Util.toJson(props).s()); } } }, @@ -240,9 +241,9 @@ public void write(Map> props, Config config) throws IOE Files.createDirectories(parentDir); } try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - String jsonResult = Util.toJson(props); - System.err.format("writing JSON for %s properties to %s%n", props.size(), outputFile); - writer.write(jsonResult); + StringIntPair jsonResult = Util.toJson(props); + System.err.format("writing JSON for %s properties to %s%n", jsonResult.i(), outputFile); + writer.write(jsonResult.s()); writer.write('\n'); writer.flush(); } @@ -448,20 +449,29 @@ public static Map> stringEntries(Properties props) { return map; } - public static String toJson(Map> map) { + /** + * @return the JSON string and the number of entries with non-empty value + */ + public static StringIntPair toJson(Map> map) { + AtomicInteger size = new AtomicInteger(0); StringBuilder s = new StringBuilder(50).append('{'); int initialLength = s.length(); for (Map.Entry> entry : map.entrySet()) { - s.append(s.length() == initialLength ? '"' : ", \""); + if (s.length() != initialLength) { + s.append(", "); + } + s.append('"'); appendJsonString(s, entry.getKey()); s.append("\": "); entry.getValue().ifPresentOrElse(value -> { + size.incrementAndGet(); s.append('"'); appendJsonString(s, value); s.append('"'); }, () -> s.append("null")); } - return s.append('}').toString(); + s.append('}'); + return new StringIntPair(s.toString(), size.get()); } private static void appendJsonString(StringBuilder buffer, String s) { @@ -490,3 +500,7 @@ private static void appendJsonString(StringBuilder buffer, String s) { } } } + + +record StringIntPair(String s, int i) { +} From 22a6273d2f34983a12cc971a6c71badaa77189d7 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:46:00 +0200 Subject: [PATCH 139/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1bef274..6abca09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -771,15 +771,15 @@ jobs: - name: '[assert] query multiple missing properties as JSON action output' env: RESULT_JSON: ${{steps.json_multi_missing.outputs.json}} - RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' - run: &validateMultiMissingJsonImpl |- + run: |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' - evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' - evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' - evalAndAssertEquals "$RESULT_GETTER" unicode_escapes 'a«,»c' - evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NON_EX_KEY" '' - evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NON_EX_KEY2" '' + resultGetter='getJsonValue "$RESULT_JSON"' + evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' + evalAndAssertEquals "$resultGetter" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$resultGetter" colon:in:Key 'c' + evalAndAssertEquals "$resultGetter" unicode_escapes 'a«,»c' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY" '' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY2" '' assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NON_EX_KEY" assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NON_EX_KEY2" @@ -865,8 +865,18 @@ jobs: - name: '[assert] query multiple missing properties as JSON file' env: JSON_OUTPUT_FILE: ${{steps.json-file_multi_missing_arrange.outputs.file}} - RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' - run: *validateMultiMissingJsonImpl + run: |- + eval "$SHELL_SETUP" + resultJson=$(<"$JSON_OUTPUT_FILE") + resultGetter='"getJsonValue "$resultJson"' + evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' + evalAndAssertEquals "$resultGetter" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$resultGetter" colon:in:Key 'c' + evalAndAssertEquals "$resultGetter" unicode_escapes 'a«,»c' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY" '' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY2" '' + assertJsonContainsKey "$resultJson" "$SELECTED_NON_EX_KEY" + assertJsonContainsKey "$resultJson" "$SELECTED_NON_EX_KEY2" # Each test which sets environment variables is a separate job to be independant of other tests. ######################################################################### From c087e2f2c4ac419f94f91a36ee2af4e650254eec Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Tue, 19 May 2026 20:47:56 +0200 Subject: [PATCH 140/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6abca09..d73cdec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -868,7 +868,7 @@ jobs: run: |- eval "$SHELL_SETUP" resultJson=$(<"$JSON_OUTPUT_FILE") - resultGetter='"getJsonValue "$resultJson"' + resultGetter='getJsonValue "$resultJson"' evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' evalAndAssertEquals "$resultGetter" org.gradle.jvmargs '-ea -showversion' evalAndAssertEquals "$resultGetter" colon:in:Key 'c' From 1c05d49eacae50d698248fbed530e78ea0cc8564 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 10:10:39 +0200 Subject: [PATCH 141/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 90 +++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d73cdec..6696db3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -913,38 +913,24 @@ jobs: run: *validateMultiImpl ######################################################################### - test_env-named_multi: + test_env_multi_missing: runs-on: ubuntu-latest steps: - *checkoutStep # ===================================================================== - - name: '[act] query multiple properties as environment variables of given names' - uses: ./ - with: - file: test/resources/test.properties - keys: '${{env.SELECTED_KEYS}}' - resultType: 'env-named:${{env.SELECTED_NAMES}}' - - name: '[assert] query multiple properties as environment variables of given names' - env: - RESULT_GETTER: getEnv - run: *validateMultiNamedImpl - - ######################################################################### - test_env-named_multi-to-single: - runs-on: ubuntu-latest - steps: - - *checkoutStep - # ===================================================================== - - name: '[act] query multiple properties as environment variables of given names' + - name: '[act] query multiple missing properties as environment variables' uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_KEYS}}' - resultType: 'env-named:${{env.SELECTED_NAME}}' - - name: '[assert] query multiple properties as environment variables of given names' - env: - RESULT_GETTER: getEnv - run: *validateMultiNamed1Impl + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: env + - name: '[assert] query multiple missing properties as environment variables' + run: |- + eval "$SHELL_SETUP" + # Assert not set: + assertEquals "${SELECTED_NON_EX_KEY-undef}" 'undef' + assertEquals "${SELECTED_NON_EX_KEY2-undef}" 'undef' + assertEquals "${SELECTED_KEY}" '21' ######################################################################### test_env_single: @@ -1012,3 +998,57 @@ jobs: env: RESULT_GETTER: 'getEnvWithPrefix PROP__' run: *validateSingleImpl + + ######################################################################### + test_env-named_multi: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] query multiple properties as environment variables of given names' + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'env-named:${{env.SELECTED_NAMES}}' + - name: '[assert] query multiple properties as environment variables of given names' + env: + RESULT_GETTER: getEnv + run: *validateMultiNamedImpl + + ######################################################################### + test_env-named_multi_missing: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] query multiple missing properties as environment variables' + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: 'env-named:NE1 E1 NE2' + - name: '[assert] query multiple missing properties as environment variables' + run: |- + eval "$SHELL_SETUP" + # Assert not set: + assertEquals "${NE1-undef}" 'undef' + assertEquals "${NE2-undef}" 'undef' + assertEquals "${E1}" '21' + + ######################################################################### + test_env-named_multi-to-single: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] query multiple properties as environment variables of given names' + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'env-named:${{env.SELECTED_NAME}}' + - name: '[assert] query multiple properties as environment variables of given names' + env: + RESULT_GETTER: getEnv + run: *validateMultiNamed1Impl From 00149d4164b9a7057362f14877237eddcfe5f85c Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 10:40:00 +0200 Subject: [PATCH 142/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6696db3..bf3dfbd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,11 @@ env: perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' } + assertVariableUnset() { + local varName=$1; shift + eval "assertEquals \"\${${varName}-undefined}\" undefined" + } + assertEquals() { debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then @@ -927,10 +932,9 @@ jobs: - name: '[assert] query multiple missing properties as environment variables' run: |- eval "$SHELL_SETUP" - # Assert not set: - assertEquals "${SELECTED_NON_EX_KEY-undef}" 'undef' - assertEquals "${SELECTED_NON_EX_KEY2-undef}" 'undef' - assertEquals "${SELECTED_KEY}" '21' + assertVariableUnset "$SELECTED_NON_EX_KEY" + assertVariableUnset "$SELECTED_NON_EX_KEY2" + assertEquals "$SELECTED_KEY" '21' ######################################################################### test_env_single: @@ -1031,9 +1035,8 @@ jobs: - name: '[assert] query multiple missing properties as environment variables' run: |- eval "$SHELL_SETUP" - # Assert not set: - assertEquals "${NE1-undef}" 'undef' - assertEquals "${NE2-undef}" 'undef' + assertVariableUnset "$NE1" + assertVariableUnset "$NE2" assertEquals "${E1}" '21' ######################################################################### From bfcda4fe4ef10673a0dc52071799df670f52b8a6 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 13:15:48 +0200 Subject: [PATCH 143/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf3dfbd..57ff2bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -934,7 +934,7 @@ jobs: eval "$SHELL_SETUP" assertVariableUnset "$SELECTED_NON_EX_KEY" assertVariableUnset "$SELECTED_NON_EX_KEY2" - assertEquals "$SELECTED_KEY" '21' + assertEquals "${!SELECTED_KEY}" '21' ######################################################################### test_env_single: @@ -1035,9 +1035,9 @@ jobs: - name: '[assert] query multiple missing properties as environment variables' run: |- eval "$SHELL_SETUP" - assertVariableUnset "$NE1" - assertVariableUnset "$NE2" - assertEquals "${E1}" '21' + assertVariableUnset NE1 + assertVariableUnset NE2 + assertEquals "$E1" '21' ######################################################################### test_env-named_multi-to-single: From 6c2a8fbc5f34bb9983d49258da231154207725ab Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 13:21:17 +0200 Subject: [PATCH 144/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57ff2bd..d57fe5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,7 @@ env: assertVariableUnset() { local varName=$1; shift + printf '$%s: ' "$varName" >&2 eval "assertEquals \"\${${varName}-undefined}\" undefined" } From 4e2b558e3938279766016c751854142600f19672 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 13:55:57 +0200 Subject: [PATCH 145/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d57fe5f..580a1fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,9 +31,26 @@ env: assertVariableUnset() { local varName=$1; shift - printf '$%s: ' "$varName" >&2 - eval "assertEquals \"\${${varName}-undefined}\" undefined" + if [ "${!varName+defined}" = defined ]; then + printf 'assertion error: %s (== "%s") is set\n' "$varName" "${!varName}" >&2 + return 1 + else + printf 'ok: %s is not set\n' "$varName" >&2 + fi } + # test this function: + ( + set +e + unset x; assertVariableUnset x >&/dev/null; statusUnset=$? + x=''; assertVariableUnset x >&/dev/null; statusEmpty=$? + x=foo; assertVariableUnset x >&/dev/null; statusNonEmpty1=$? + x=defined; assertVariableUnset x >&/dev/null; statusNonEmpty2=$? + set -e + [ $statusUnset -eq 0 ] + [ $statusEmpty -ne 0 ] + [ $statusNonEmpty1 -ne 0 ] + [ $statusNonEmpty2 -ne 0 ] + ) assertEquals() { debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" From 53fd99c8759aeac6578f1d89f61c39061473a613 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 14:45:59 +0200 Subject: [PATCH 146/190] better missing property handling - outputs: set to empty - environment variables: do not set - json: set key with value null --- .github/workflows/test.yml | 498 +++++++++++++++++++++++++++---------- Action.java | 146 +++++++---- action.yml | 12 +- 3 files changed, 470 insertions(+), 186 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9b07a3..580a1fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,10 +4,12 @@ defaults: run: shell: bash env: - SELECTED_SINGLE_KEY: 'org.gradle.jvmargs' + SELECTED_KEY: 'sourceJavaVersion' SELECTED_KEYS: 'sourceJavaVersion org.gradle.jvmargs unicode_escapes colon:in:Key' SELECTED_KEYS_OTHER_ORDER: 'colon:in:Key sourceJavaVersion org.gradle.jvmargs unicode_escapes' - SELECTED_SINGLE_NAME: 'my-prop' + SELECTED_NON_EX_KEY: 'nonExistingKey1' + SELECTED_NON_EX_KEY2: 'nonExistingKey2' + SELECTED_NAME: 'my-prop' SELECTED_NAMES: 'java-version jvm_args unicode colon' NOT_FOUND_STATUS: 54 @@ -27,6 +29,29 @@ env: perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' } + assertVariableUnset() { + local varName=$1; shift + if [ "${!varName+defined}" = defined ]; then + printf 'assertion error: %s (== "%s") is set\n' "$varName" "${!varName}" >&2 + return 1 + else + printf 'ok: %s is not set\n' "$varName" >&2 + fi + } + # test this function: + ( + set +e + unset x; assertVariableUnset x >&/dev/null; statusUnset=$? + x=''; assertVariableUnset x >&/dev/null; statusEmpty=$? + x=foo; assertVariableUnset x >&/dev/null; statusNonEmpty1=$? + x=defined; assertVariableUnset x >&/dev/null; statusNonEmpty2=$? + set -e + [ $statusUnset -eq 0 ] + [ $statusEmpty -ne 0 ] + [ $statusNonEmpty1 -ne 0 ] + [ $statusNonEmpty2 -ne 0 ] + ) + assertEquals() { debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" if [ "$1" = "$2" ]; then @@ -60,7 +85,13 @@ env: local quotedArgs value quotedArgs=$(printf ' %q' "${args[@]}") + + # Call $getterFunction once to check its status only before the call to capture its output. + # Otherwise, we'd get a misleading error from replaceLastLF if $getterFunction prints nothing and exits + # with $NOT_FOUND_STATUS. + eval "${getterFunction}${quotedArgs}" >/dev/null eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" + expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" assertEquals "$value" "$expectedValue" } @@ -88,7 +119,39 @@ env: local json=$1; shift local key=$1; shift debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" - jq --arg k "$key" --raw-output '.[$k] // ("" | halt_error(env.NOT_FOUND_STATUS | tonumber))' <<<"$json" + jq --arg k "$key" --raw-output ' + .[$k] as $v + | if $v != null then + $v + elif (keys | index($k) != null) then #contains key $k (with a null or false value) + "" + else + (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber)) + end' <<<"$json" + } + + assertJsonDoesNotContainKey() { + assertJsonContainsKeyEquals "$@" false + } + + assertJsonContainsKey() { + assertJsonContainsKeyEquals "$@" true + } + + assertJsonContainsKeyEquals() { + local json=$1; shift + local key=$1; shift + local expected=$1; shift + debugPrintf 'assertJsonContainsKeyEquals("%s", "%s", "%s")\n' "$json" "$key" "$expected" + local contains + contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null' <<<"$json") + assertEquals "$contains" "$expected" + } + + # Encodes a key $1 in the output-style encoding (property key to output name). + encodeKey() { + local key=$1; shift + perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" } # Extracts the value for key from JSON $2. @@ -96,7 +159,7 @@ env: local json=$1; shift local key=$1; shift local encodedKey - encodedKey="_$(perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge' <<<"$key")" + encodedKey=$(encodeKey "$key") getJsonValue "$json" "$encodedKey" "$@" } @@ -108,6 +171,7 @@ env: if printenv -- "$key"; then : # OK elif [ ${st-$?} -eq 1 ]; then + printf 'environment variable %s not set\n' "$key" >&2 return $NOT_FOUND_STATUS else # no error message, assuming printenv has already written one @@ -137,7 +201,7 @@ jobs: - test_env_all - test_env_multi - test_env-named_multi - - test_env-named_multi-to-one + - test_env-named_multi-to-single - test_env_single - test_env-with-prefix_all - test_env-with-prefix_multi @@ -169,7 +233,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): invalid input: resultType' - id: error-input-resultType + id: input-error_resultType continue-on-error: true uses: ./ with: @@ -177,8 +241,8 @@ jobs: resultType: rt1 - name: '[assert] (should fail): invalid input: resultType' env: - STATUS: ${{steps.error-input-resultType.outcome}} - ERROR: ${{steps.error-input-resultType.outputs.error}} + STATUS: ${{steps.input-error_resultType.outcome}} + ERROR: ${{steps.input-error_resultType.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -186,7 +250,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "output-named" without names' - id: error-resultType-output-without-names + id: input-error_resultType-output-without-names continue-on-error: true uses: ./ with: @@ -195,8 +259,8 @@ jobs: resultType: 'output-named:' - name: '[assert] (should fail): resultType "output-named" without names' env: - STATUS: ${{steps.error-resultType-output-without-names.outcome}} - ERROR: ${{steps.error-resultType-output-without-names.outputs.error}} + STATUS: ${{steps.input-error_resultType-output-without-names.outcome}} + ERROR: ${{steps.input-error_resultType-output-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -204,7 +268,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "env-named" without names' - id: error-resultType-env-without-names + id: input-error_resultType-env-without-names continue-on-error: true uses: ./ with: @@ -213,8 +277,8 @@ jobs: resultType: 'env-named:' - name: '[assert] (should fail): resultType "env-named" without names' env: - STATUS: ${{steps.error-resultType-env-without-names.outcome}} - ERROR: ${{steps.error-resultType-env-without-names.outputs.error}} + STATUS: ${{steps.input-error_resultType-env-without-names.outcome}} + ERROR: ${{steps.input-error_resultType-env-without-names.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -222,7 +286,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "output-named" without keys' - id: error-resultType-output-without-keys + id: input-error_resultType-output-without-keys continue-on-error: true uses: ./ with: @@ -230,8 +294,8 @@ jobs: resultType: 'output-named:a b' - name: '[assert] (should fail): resultType "output-named" without keys' env: - STATUS: ${{steps.error-resultType-output-without-keys.outcome}} - ERROR: ${{steps.error-resultType-output-without-keys.outputs.error}} + STATUS: ${{steps.input-error_resultType-output-without-keys.outcome}} + ERROR: ${{steps.input-error_resultType-output-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -239,7 +303,7 @@ jobs: # ===================================================================== - name: '[act] (should fail): resultType "env-named" without keys' - id: error-resultType-env-without-keys + id: input-error_resultType-env-without-keys continue-on-error: true uses: ./ with: @@ -247,8 +311,8 @@ jobs: resultType: 'env-named:a b' - name: '[assert] (should fail): resultType "env-named" without keys' env: - STATUS: ${{steps.error-resultType-env-without-keys.outcome}} - ERROR: ${{steps.error-resultType-env-without-keys.outputs.error}} + STATUS: ${{steps.input-error_resultType-env-without-keys.outcome}} + ERROR: ${{steps.input-error_resultType-env-without-keys.outputs.error}} run: |- eval "$SHELL_SETUP" assertEquals "$STATUS" failure @@ -261,14 +325,14 @@ jobs: - *checkoutStep # ===================================================================== - name: '[act] query single property with default resultType' - id: single-default + id: default_single uses: ./ with: file: test/resources/test.properties keys: sourceJavaVersion - name: '[assert] query single property with default resultType' env: - RESULT: ${{steps.single-default.outputs._sourceJavaVersion}} + RESULT: ${{steps.default_single.outputs._sourceJavaVersion}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT" '21' @@ -282,7 +346,7 @@ jobs: # name (instead of re-implemented). # It also demonstrates how you'd usually use outputs with one environment variable per output. - name: '[act] query multiple properties as action outputs (simple)' - id: multi-output-simple + id: output_multi_simple uses: ./ with: file: test/resources/test.properties @@ -290,11 +354,11 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (simple)' env: - RESULT_SJV: ${{steps.multi-output-simple.outputs._sourceJavaVersion}} - RESULT_JVMARGS: ${{steps.multi-output-simple.outputs._org-002Egradle-002Ejvmargs}} - RESULT_COLONK: ${{steps.multi-output-simple.outputs._colon-003Ain-003AKey}} - RESULT_DASHK: ${{steps.multi-output-simple.outputs._dash-002Din-002DKey}} - RESULT_UNI: ${{steps.multi-output-simple.outputs._unicode_escapes}} + RESULT_SJV: ${{steps.output_multi_simple.outputs._sourceJavaVersion}} + RESULT_JVMARGS: ${{steps.output_multi_simple.outputs._org-002Egradle-002Ejvmargs}} + RESULT_COLONK: ${{steps.output_multi_simple.outputs._colon-003Ain-003AKey}} + RESULT_DASHK: ${{steps.output_multi_simple.outputs._dash-002Din-002DKey}} + RESULT_UNI: ${{steps.output_multi_simple.outputs._unicode_escapes}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_SJV" '21' @@ -305,14 +369,14 @@ jobs: # ===================================================================== - name: '[act] query all properties as action outputs' - id: all-output + id: output_all uses: ./ with: file: test/resources/test.properties resultType: output - name: '[assert] query all properties as action outputs' env: - RESULT_JSON: ${{toJSON(steps.all-output.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_all.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- eval "$SHELL_SETUP" @@ -347,14 +411,14 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' - name: '[assert] query all properties as action outputs (additional "value" output not set)' env: - RESULT_VALUE: ${{steps.all-output.outputs.value || 'U_N_S_E_T'}} + RESULT_VALUE: ${{steps.output_all.outputs.value || 'U_N_S_E_T'}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== - name: '[act] query multiple properties as action outputs' - id: multi-output + id: output_multi uses: ./ with: file: test/resources/test.properties @@ -363,7 +427,7 @@ jobs: - name: '[assert] query multiple properties as action outputs' env: - RESULT_JSON: ${{toJSON(steps.multi-output.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateMultiImpl |- eval "$SHELL_SETUP" @@ -400,14 +464,14 @@ jobs: - name: '[assert] query multiple properties as action outputs (additional "value" output)' env: - RESULT_VALUE: ${{steps.multi-output.outputs.value}} + RESULT_VALUE: ${{steps.output_multi.outputs.value}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" # ===================================================================== - name: '[act] query multiple properties as action outputs (different keySeparator)' - id: multi-output-keySeparator + id: output_multi_keySeparator uses: ./ with: file: test/resources/test.properties @@ -416,13 +480,13 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (different keySeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-keySeparator.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi_keySeparator.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl # ===================================================================== - name: '[act] query multiple properties as action outputs (different multi-char keySeparator)' - id: multi-output-keySeparatorMC + id: output_multi_keySeparatorMC uses: ./ with: file: test/resources/test.properties @@ -431,13 +495,13 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (different multi-char keySeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-keySeparatorMC.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi_keySeparatorMC.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl # ===================================================================== - name: '[act] query multiple properties as action outputs (in another keys order)' - id: multi-output-2 + id: output_multi2 uses: ./ with: file: test/resources/test.properties @@ -445,16 +509,109 @@ jobs: resultType: output - name: '[assert] query multiple properties as action outputs (in another keys order)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-2.outputs)}} + RESULT_JSON: ${{toJSON(steps.output_multi2.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: *validateMultiImpl - name: '[assert] query multiple properties as action outputs (in another keys order) (additional "value" output)' env: - RESULT_VALUE: ${{steps.multi-output-2.outputs.value}} + RESULT_VALUE: ${{steps.output_multi2.outputs.value}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" + # ===================================================================== + - name: '[act] query single property as action output' + id: output_single + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEY}}' + resultType: output + + - name: '[assert] query single property as action output' + env: + RESULT_JSON: ${{toJSON(steps.output_single.outputs)}} + RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' + run: &validateSingleImpl |- + eval "$SHELL_SETUP" + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_KEY" '21' + + evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion + evalAndAssertUndefined "$RESULT_GETTER" org.gradle.jvmargs + evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key + evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' + evalAndAssertUndefined "$RESULT_GETTER" colonInValue + evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' + evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key + evalAndAssertUndefined "$RESULT_GETTER" dashInValue + evalAndAssertUndefined "$RESULT_GETTER" empty + evalAndAssertUndefined "$RESULT_GETTER" null + evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 + evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 + evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes + evalAndAssertUndefined "$RESULT_GETTER" dash_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_2 + evalAndAssertUndefined "$RESULT_GETTER" dash_3 + evalAndAssertUndefined "$RESULT_GETTER" dash_4 + evalAndAssertUndefined "$RESULT_GETTER" dash_5 + evalAndAssertUndefined "$RESULT_GETTER" dash_6 + evalAndAssertUndefined "$RESULT_GETTER" dash_7 + evalAndAssertUndefined "$RESULT_GETTER" dash_8 + evalAndAssertUndefined "$RESULT_GETTER" dash_9 + evalAndAssertUndefined "$RESULT_GETTER" dash_10 + evalAndAssertUndefined "$RESULT_GETTER" dash_11 + evalAndAssertUndefined "$RESULT_GETTER" dash_12 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + + # ===================================================================== + - name: '[act] query single missing property as action output' + id: output_single_missing + uses: ./ + with: + file: test/resources/test.properties + keys: ${{env.SELECTED_NON_EX_KEY}} + resultType: output + - name: '[assert] query single missing property as action output' + env: + RESULT: ${{steps.output_single_missing.outputs._nonExistingKey1}} + RESULT_VALUE: ${{steps.output_single_missing.outputs.value}} + RESULT_JSON: ${{toJSON(steps.output_single_missing.outputs)}} + run: |- + eval "$SHELL_SETUP" + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT" '' + assertEquals "$RESULT_VALUE" '' + # Assert set: + assertJsonContainsKey "$RESULT_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" + assertJsonContainsKey "$RESULT_JSON" value + + # ===================================================================== + - name: '[act] query multiple missing properties as action outputs' + id: output_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: output + - name: '[assert] query multiple missing properties as action outputs' + env: + RESULT1: ${{steps.output_multi_missing.outputs._nonExistingKey1}} + RESULT2: ${{steps.output_multi_missing.outputs._nonExistingKey2}} + RESULT_VALUE: ${{steps.output_multi_missing.outputs.value}} + RESULT_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} + run: |- + eval "$SHELL_SETUP" + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT1" '' + assertEquals "$RESULT2" '' + assertEquals "$RESULT_VALUE" '' + # Assert set: + assertJsonContainsKey "$RESULT_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY")" + assertJsonContainsKey "$RESULT_JSON" "$(encodeKey "$SELECTED_NON_EX_KEY2")" + assertJsonContainsKey "$RESULT_JSON" value + ######################################################################### test_output-named: runs-on: ubuntu-latest @@ -462,7 +619,7 @@ jobs: - *checkoutStep # ===================================================================== - name: '[act] query multiple properties as action outputs of given names' - id: multi-output-named + id: output-named_multi uses: ./ with: file: test/resources/test.properties @@ -471,7 +628,7 @@ jobs: - name: '[assert] query multiple properties as action outputs of given names' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamedImpl |- @@ -493,14 +650,14 @@ jobs: - name: '[assert] query multiple properties as action outputs (additional "value" output not set)' env: - RESULT_VALUE: ${{steps.multi-output-named.outputs.value || 'U_N_S_E_T'}} + RESULT_VALUE: ${{steps.output-named_multi.outputs.value || 'U_N_S_E_T'}} run: |- eval "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== - name: '[act] query multiple properties as action outputs of given names (different resultNameSeparator)' - id: multi-output-named-resultNameSeparator + id: output-named_multi_resultNameSeparator uses: ./ with: file: test/resources/test.properties @@ -509,13 +666,13 @@ jobs: resultType: 'output-named:java-version?jvm_args?unicode?colon' - name: '[assert] query multiple properties as action outputs of given names (different resultNameSeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparator.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi_resultNameSeparator.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl # ===================================================================== - name: '[act] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' - id: multi-output-named-resultNameSeparatorMC + id: output-named_multi_resultNameSeparatorMC uses: ./ with: file: test/resources/test.properties @@ -524,27 +681,27 @@ jobs: resultType: 'output-named:java-version- -jvm_args- -unicode- -colon' - name: '[assert] query multiple properties as action outputs of given names (different multi-char resultNameSeparator)' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named-resultNameSeparatorMC.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi_resultNameSeparatorMC.outputs)}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiNamedImpl # ===================================================================== - name: '[act] query multiple properties as action outputs of given name' - id: multi-output-named1 + id: output-named_multi-to-single uses: ./ with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'output-named:${{env.SELECTED_SINGLE_NAME}}' + resultType: 'output-named:${{env.SELECTED_NAME}}' - name: '[assert] query multiple properties as action outputs of given name' env: - RESULT_JSON: ${{toJSON(steps.multi-output-named1.outputs)}} + RESULT_JSON: ${{toJSON(steps.output-named_multi-to-single.outputs)}} # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_SINGLE_NAME" 'c' + evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NAME" 'c' # The outputs with the default names are not set: evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion @@ -557,50 +714,42 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_1 # ===================================================================== - - name: '[act] query single property as action outputs' - id: single-output + - name: '[act] query single missing property as action output of given name' + id: output-named_single_missing uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_SINGLE_KEY}}' - resultType: output - - - name: '[assert] query multiple properties as action outputs' + keys: ${{env.SELECTED_NON_EX_KEY}} + resultType: 'output-named:${{env.SELECTED_NAME}}' + - name: '[assert] query single missing property as action output of given name' env: - RESULT_JSON: ${{toJSON(steps.single-output.outputs)}} - RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' - run: &validateSingleImpl |- + RESULT: ${{steps.output-named_single_missing.outputs.my-prop}} + RESULT_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} + run: |- eval "$SHELL_SETUP" - evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT" '' + # Assert set: + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NAME" - evalAndAssertUndefined "$RESULT_GETTER" sourceJavaVersion - evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion - evalAndAssertUndefined "$RESULT_GETTER" colon:in:Key - evalAndAssertUndefined "$RESULT_GETTER" 'space in Key' - evalAndAssertUndefined "$RESULT_GETTER" colonInValue - evalAndAssertUndefined "$RESULT_GETTER" 'back\slash' - evalAndAssertUndefined "$RESULT_GETTER" dash-in-Key - evalAndAssertUndefined "$RESULT_GETTER" dashInValue - evalAndAssertUndefined "$RESULT_GETTER" empty - evalAndAssertUndefined "$RESULT_GETTER" null - evalAndAssertUndefined "$RESULT_GETTER" whitespace_escapes - evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_1 - evalAndAssertUndefined "$RESULT_GETTER" trailing_linefeed_2 - evalAndAssertUndefined "$RESULT_GETTER" unicode_escapes - evalAndAssertUndefined "$RESULT_GETTER" dash_1 - evalAndAssertUndefined "$RESULT_GETTER" dash_2 - evalAndAssertUndefined "$RESULT_GETTER" dash_3 - evalAndAssertUndefined "$RESULT_GETTER" dash_4 - evalAndAssertUndefined "$RESULT_GETTER" dash_5 - evalAndAssertUndefined "$RESULT_GETTER" dash_6 - evalAndAssertUndefined "$RESULT_GETTER" dash_7 - evalAndAssertUndefined "$RESULT_GETTER" dash_8 - evalAndAssertUndefined "$RESULT_GETTER" dash_9 - evalAndAssertUndefined "$RESULT_GETTER" dash_10 - evalAndAssertUndefined "$RESULT_GETTER" dash_11 - evalAndAssertUndefined "$RESULT_GETTER" dash_12 - evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 - evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 + # ===================================================================== + - name: '[act] query multiple missing properties as action outputs of given name' + id: output-named_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: 'output-named:${{env.SELECTED_NAME}}' + - name: '[assert] query multiple missing properties as action outputs of given names' + env: + RESULT: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} + RESULT_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} + run: |- + eval "$SHELL_SETUP" + # Like this, the expected empty value is not distinguishable from unset: + assertEquals "$RESULT" '' + # Assert set: + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NAME" ######################################################################### test_json: @@ -609,20 +758,20 @@ jobs: - *checkoutStep # ===================================================================== - name: '[act] query all properties as JSON action output' - id: all-json + id: json_all uses: ./ with: file: test/resources/test.properties resultType: json - name: '[assert] query all properties as JSON action output' env: - RESULT_JSON: ${{steps.all-json.outputs.json}} + RESULT_JSON: ${{steps.json_all.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateAllImpl # ===================================================================== - name: '[act] query multiple properties as JSON action output' - id: multiple-json + id: json_multi uses: ./ with: file: test/resources/test.properties @@ -630,10 +779,33 @@ jobs: resultType: json - name: '[assert] query multiple properties as JSON action output' env: - RESULT_JSON: ${{steps.multiple-json.outputs.json}} + RESULT_JSON: ${{steps.json_multi.outputs.json}} RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: *validateMultiImpl + # ===================================================================== + - name: '[act] query multiple missing properties as JSON action output' + id: json_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEYS}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: json + - name: '[assert] query multiple missing properties as JSON action output' + env: + RESULT_JSON: ${{steps.json_multi_missing.outputs.json}} + run: |- + eval "$SHELL_SETUP" + resultGetter='getJsonValue "$RESULT_JSON"' + evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' + evalAndAssertEquals "$resultGetter" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$resultGetter" colon:in:Key 'c' + evalAndAssertEquals "$resultGetter" unicode_escapes 'a«,»c' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY" '' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY2" '' + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NON_EX_KEY" + assertJsonContainsKey "$RESULT_JSON" "$SELECTED_NON_EX_KEY2" + ######################################################################### test_json-file: runs-on: ubuntu-latest @@ -641,7 +813,7 @@ jobs: - *checkoutStep # ===================================================================== - name: '[arrange] query multiple properties as JSON file (creating file)' - id: multiple-json-file-create-arrange + id: json-file_multi_create_arrange run: |- parentDir=$(mktemp --directory --dry-run --tmpdir=. test_json-file_multi_create_XXXXXX) file=$parentDir/out/result.json @@ -652,16 +824,16 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'json-file:${{steps.multiple-json-file-create-arrange.outputs.file}}' + resultType: 'json-file:${{steps.json-file_multi_create_arrange.outputs.file}}' - name: '[assert] query multiple properties as JSON file (creating file)' env: - JSON_OUTPUT_FILE: ${{steps.multiple-json-file-create-arrange.outputs.file}} + JSON_OUTPUT_FILE: ${{steps.json-file_multi_create_arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl # ===================================================================== - name: '[arrange] query multiple properties as JSON file (overwriting file)' - id: multiple-json-file-overwrite-arrange + id: json-file_multi_overwrite_arrange run: |- file=$(mktemp --tmpdir=. test_json-file_multi_overwrite_XXXXXX.json) # Write longer contents than the expected output. ⇒ If the action does not truncate, there will be @@ -674,19 +846,19 @@ jobs: with: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' - resultType: 'json-file:${{steps.multiple-json-file-overwrite-arrange.outputs.file}}' + resultType: 'json-file:${{steps.json-file_multi_overwrite_arrange.outputs.file}}' - name: '[assert] query multiple properties as JSON file (overwriting file)' env: - JSON_OUTPUT_FILE: ${{steps.multiple-json-file-overwrite-arrange.outputs.file}} + JSON_OUTPUT_FILE: ${{steps.json-file_multi_overwrite_arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateMultiImpl # ===================================================================== - name: '[arrange] query all properties as JSON file' - id: all-json-file-arrange + id: json-file_all_arrange # Distinction between creating and overwriting need not be tested again. # This test just uses an existing empty file. - run: |- + run: &createEmptyJsonFile |- # Remove leading "./" ⇒ Also test the case where given file has "no parent" (i.e. Java method # java.nio.file.Path.getParent() returns null). file=$(mktemp --tmpdir=. test_json-file_all_XXXXXX.json | sed 's=^\./==') @@ -695,13 +867,39 @@ jobs: uses: ./ with: file: test/resources/test.properties - resultType: 'json-file:${{steps.all-json-file-arrange.outputs.file}}' + resultType: 'json-file:${{steps.json-file_all_arrange.outputs.file}}' - name: '[assert] query all properties as JSON file' env: - JSON_OUTPUT_FILE: ${{steps.all-json-file-arrange.outputs.file}} + JSON_OUTPUT_FILE: ${{steps.json-file_all_arrange.outputs.file}} RESULT_GETTER: 'getJsonValue "$(<"$JSON_OUTPUT_FILE")"' run: *validateAllImpl + # ===================================================================== + - name: '[arrange] query multiple missing properties as JSON file' + id: json-file_multi_missing_arrange + run: *createEmptyJsonFile + - name: '[act] query multiple missing properties as JSON file' + id: json-file_multi_missing + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEYS}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: 'json-file:${{steps.json-file_multi_missing_arrange.outputs.file}}' + - name: '[assert] query multiple missing properties as JSON file' + env: + JSON_OUTPUT_FILE: ${{steps.json-file_multi_missing_arrange.outputs.file}} + run: |- + eval "$SHELL_SETUP" + resultJson=$(<"$JSON_OUTPUT_FILE") + resultGetter='getJsonValue "$resultJson"' + evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' + evalAndAssertEquals "$resultGetter" org.gradle.jvmargs '-ea -showversion' + evalAndAssertEquals "$resultGetter" colon:in:Key 'c' + evalAndAssertEquals "$resultGetter" unicode_escapes 'a«,»c' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY" '' + evalAndAssertEquals "$resultGetter" "$SELECTED_NON_EX_KEY2" '' + assertJsonContainsKey "$resultJson" "$SELECTED_NON_EX_KEY" + assertJsonContainsKey "$resultJson" "$SELECTED_NON_EX_KEY2" # Each test which sets environment variables is a separate job to be independant of other tests. ######################################################################### @@ -738,38 +936,23 @@ jobs: run: *validateMultiImpl ######################################################################### - test_env-named_multi: + test_env_multi_missing: runs-on: ubuntu-latest steps: - *checkoutStep # ===================================================================== - - name: '[act] query multiple properties as environment variables of given names' - uses: ./ - with: - file: test/resources/test.properties - keys: '${{env.SELECTED_KEYS}}' - resultType: 'env-named:${{env.SELECTED_NAMES}}' - - name: '[assert] query multiple properties as environment variables of given names' - env: - RESULT_GETTER: getEnv - run: *validateMultiNamedImpl - - ######################################################################### - test_env-named_multi-to-one: - runs-on: ubuntu-latest - steps: - - *checkoutStep - # ===================================================================== - - name: '[act] query multiple properties as environment variables of given names' + - name: '[act] query multiple missing properties as environment variables' uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_KEYS}}' - resultType: 'env-named:${{env.SELECTED_SINGLE_NAME}}' - - name: '[assert] query multiple properties as environment variables of given names' - env: - RESULT_GETTER: getEnv - run: *validateMultiNamed1Impl + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: env + - name: '[assert] query multiple missing properties as environment variables' + run: |- + eval "$SHELL_SETUP" + assertVariableUnset "$SELECTED_NON_EX_KEY" + assertVariableUnset "$SELECTED_NON_EX_KEY2" + assertEquals "${!SELECTED_KEY}" '21' ######################################################################### test_env_single: @@ -781,7 +964,7 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_SINGLE_KEY}}' + keys: '${{env.SELECTED_KEY}}' resultType: env - name: '[assert] query single property as environment variable' env: @@ -831,9 +1014,62 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_SINGLE_KEY}}' + keys: '${{env.SELECTED_KEY}}' resultType: 'env:PROP__' - name: '[assert] query single property as environment variable with prefix' env: RESULT_GETTER: 'getEnvWithPrefix PROP__' run: *validateSingleImpl + + ######################################################################### + test_env-named_multi: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] query multiple properties as environment variables of given names' + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'env-named:${{env.SELECTED_NAMES}}' + - name: '[assert] query multiple properties as environment variables of given names' + env: + RESULT_GETTER: getEnv + run: *validateMultiNamedImpl + + ######################################################################### + test_env-named_multi_missing: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] query multiple missing properties as environment variables' + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_NON_EX_KEY}} ${{env.SELECTED_KEY}} ${{env.SELECTED_NON_EX_KEY2}}' + resultType: 'env-named:NE1 E1 NE2' + - name: '[assert] query multiple missing properties as environment variables' + run: |- + eval "$SHELL_SETUP" + assertVariableUnset NE1 + assertVariableUnset NE2 + assertEquals "$E1" '21' + + ######################################################################### + test_env-named_multi-to-single: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] query multiple properties as environment variables of given names' + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'env-named:${{env.SELECTED_NAME}}' + - name: '[assert] query multiple properties as environment variables of given names' + env: + RESULT_GETTER: getEnv + run: *validateMultiNamed1Impl diff --git a/Action.java b/Action.java index d57e716..4759a0e 100644 --- a/Action.java +++ b/Action.java @@ -16,6 +16,7 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -29,11 +30,16 @@ static void main(String[] args) throws Exception { ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { Properties properties = Util.readProperties(file); - Map selectedProperties = config.selectedKeys() + // If selectedKeys is set, this Map will have exactly those keys and values may be empty (where selected key is + // not found in properties). + // Otherwise, this Map will have the same keys as the properties file, and "not found" is not possible. + Map> selectedProperties = config.selectedKeys() .map(keys -> Util.selectProperties(properties, keys, file)) // .orElseGet(() -> Util.stringEntries(properties)); resultWriter.write(selectedProperties, config); } + } catch (IoRuntimeException e) { + throw e.getCause(); } catch (OutputException e) { try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { writer.write(Ids.OutputName.ERROR, e.getMessage()); @@ -97,6 +103,18 @@ enum OutputName { } +class IoRuntimeException extends RuntimeException { + public IoRuntimeException(IOException cause) { + super(cause); + } + + @Override + public synchronized IOException getCause() { + return (IOException) super.getCause(); + } +} + + /** * An exception for which an "error" output should be set. */ @@ -156,17 +174,17 @@ public void requireNoArg() { enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - for (Map.Entry entry : props.entrySet()) { + for (Map.Entry> entry : props.entrySet()) { String key = encodeKey(config.outputPrefix() + entry.getKey()); - lastValue = entry.getValue(); + lastValue = entry.getValue().orElse(""); writer.write(key, lastValue); } - if (config.selectedKeys().isPresent() && lastValue != null) { - writer.write(Ids.OutputName.VALUE, lastValue); + if (config.selectedKeys().isPresent()) { + writer.write(Ids.OutputName.VALUE, lastValue != null ? lastValue : ""); } } } @@ -183,48 +201,49 @@ private static String encodeKey(String key) { }, OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override - public void write(Map props, Config config) throws IOException { - writeNamedImpl(props, config, GitHubOutputFile.OUTPUT); + public void write(Map> props, Config config) throws IOException { + writeNamedImpl(props, config, true, GitHubOutputFile.OUTPUT); } }, ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override - public void write(Map props, Config config) throws IOException { - writeNamedImpl(props, config, GitHubOutputFile.ENV); + public void write(Map> props, Config config) throws IOException { + writeNamedImpl(props, config, false, GitHubOutputFile.ENV); } }, ENV(Ids.ResultWriterName.ENV) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { String prefix = config.resultTypeArg(); try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { - for (Map.Entry entry : props.entrySet()) { - writer.write(prefix + entry.getKey(), entry.getValue()); + for (Map.Entry> entry : props.entrySet()) { + Optional value = entry.getValue(); + value.ifPresent(v -> writer.write(prefix + entry.getKey(), v)); } } } }, JSON(Ids.ResultWriterName.JSON) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { config.requireNoArg(); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - writer.write(Ids.OutputName.JSON, Util.toJson(props)); + writer.write(Ids.OutputName.JSON, Util.toJson(props).s()); } } }, JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override - public void write(Map props, Config config) throws IOException { + public void write(Map> props, Config config) throws IOException { String outputFile = config.requiredResultTypeArg(); Path parentDir = Paths.get(outputFile).getParent(); if (parentDir != null) { Files.createDirectories(parentDir); } try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - String jsonResult = Util.toJson(props); - System.err.format("writing JSON for %s properties to %s%n", props.size(), outputFile); - writer.write(jsonResult); + StringIntPair jsonResult = Util.toJson(props); + System.err.format("writing JSON for %s properties to %s%n", jsonResult.i(), outputFile); + writer.write(jsonResult.s()); writer.write('\n'); writer.flush(); } @@ -246,10 +265,10 @@ public static ResultWriter of(String externalName) { throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.RESULT_TYPE + ": " + externalName); } - public abstract void write(Map props, Config config) throws IOException; + public abstract void write(Map> props, Config config) throws IOException; - private static void writeNamedImpl(Map props, Config config, GitHubOutputFile gitHubOutputFile) - throws IOException { + private static void writeNamedImpl(Map> props, Config config, boolean includeMissing, + GitHubOutputFile gitHubOutputFile) throws IOException { List selectedKeys = config.selectedKeys().orElseThrow(() -> OutputException.forIllegalArgument("invalid use of " + Ids.ConfigVariable.RESULT_TYPE + " " + config.resultType() + " (missing " + Ids.ConfigVariable.KEYS + ")")); @@ -263,8 +282,14 @@ private static void writeNamedImpl(Map props, Config config, Git try (GitHubVariableWriter writer = gitHubOutputFile.open()) { for (int i = 0; i < selectedKeys.size(); i++) { String name = resultNames[resultNames.length == 1 ? 0 : i]; - String value = props.get(selectedKeys.get(i)); - writer.write(name, value != null ? value : ""); + Optional value = props.get(selectedKeys.get(i)); + value.ifPresentOrElse(v -> writer.write(name, v), () -> { + if (includeMissing) { + writer.write(name, ""); + } else { + // Nothing to do. "not found" has already been logged. + } + }); } } } @@ -300,21 +325,25 @@ public GitHubVariableWriter(String description, String fileName) throws IOExcept this.writer = Util.openFile(fileName, StandardOpenOption.CREATE, StandardOpenOption.APPEND); } - public void write(String key, String value) throws IOException { - System.err.format("setting %s %s%n", description, key); - - // write (very) simple values in format "=": - if (SIMPLE_VALUE.matcher(value).matches()) { - this.writer.write(key); - this.writer.write('='); - this.writer.write(value); - this.writer.write('\n'); - } else { - writeMultiLine(key, value); + public void write(String key, String value) { + try { + System.err.format("setting %s %s%n", description, key); + + // write (very) simple values in format "=": + if (SIMPLE_VALUE.matcher(value).matches()) { + this.writer.write(key); + this.writer.write('='); + this.writer.write(value); + this.writer.write('\n'); + } else { + writeMultiLine(key, value); + } + } catch (IOException e) { + throw new IoRuntimeException(e); } } - public void write(Ids.OutputName key, String value) throws IOException { + public void write(Ids.OutputName key, String value) { write(key.externalName, value); } @@ -393,13 +422,13 @@ public static Properties readProperties(String file) throws IOException { /** * Selects the properties with the given keys and returns them as a Map with the corresponding iteration order. */ - public static Map selectProperties(Properties allProps, List selectedKeys, String file) { + public static Map> selectProperties(Properties allProps, List selectedKeys, String file) { Set unmatchedKeysSet = new LinkedHashSet<>(selectedKeys); - Map results = new LinkedHashMap<>(); + Map> results = new LinkedHashMap<>(); for (String key : selectedKeys) { String value = allProps.getProperty(key); + results.put(key, Optional.ofNullable(value)); if (value != null) { - results.put(key, value); unmatchedKeysSet.remove(key); } } @@ -409,25 +438,40 @@ public static Map selectProperties(Properties allProps, List stringEntries(Properties props) { - Map map = new LinkedHashMap<>(); + /** + * @return the Properties as a Map with each value as a non-empty Optional (strange, but useful for our use case) + */ + public static Map> stringEntries(Properties props) { + Map> map = new LinkedHashMap<>(); for (String key : props.stringPropertyNames()) { - map.put(key, props.getProperty(key)); + map.put(key, Optional.of(props.getProperty(key))); } return map; } - public static String toJson(Map map) { + /** + * @return the JSON string and the number of entries with non-empty value + */ + public static StringIntPair toJson(Map> map) { + AtomicInteger size = new AtomicInteger(0); StringBuilder s = new StringBuilder(50).append('{'); int initialLength = s.length(); - for (Map.Entry entry : map.entrySet()) { - s.append(s.length() == initialLength ? '"' : ", \""); - appendJsonString(s, entry.getKey()); - s.append("\": \""); - appendJsonString(s, entry.getValue()); + for (Map.Entry> entry : map.entrySet()) { + if (s.length() != initialLength) { + s.append(", "); + } s.append('"'); + appendJsonString(s, entry.getKey()); + s.append("\": "); + entry.getValue().ifPresentOrElse(value -> { + size.incrementAndGet(); + s.append('"'); + appendJsonString(s, value); + s.append('"'); + }, () -> s.append("null")); } - return s.append('}').toString(); + s.append('}'); + return new StringIntPair(s.toString(), size.get()); } private static void appendJsonString(StringBuilder buffer, String s) { @@ -456,3 +500,7 @@ private static void appendJsonString(StringBuilder buffer, String s) { } } } + + +record StringIntPair(String s, int i) { +} diff --git a/action.yml b/action.yml index 97e5ed6..d771ef7 100644 --- a/action.yml +++ b/action.yml @@ -27,16 +27,16 @@ inputs: Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name "a-002Eb-002Dc_d". - Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. + Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. If `keys` is given, but none is found, "value" is set to empty. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) + - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) The output for a missing property is set to empty. If you need to distinguish between empty an undefined properties, resultType "json" or "json-file" is recommended. + - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. If none is found, the output is set to empty. - "env": For each property key , set an environment variable . In order not to pollute the environment with hard to understand variables, this should only be used to set some specifically named variables. I.e. you know that the property file can only contain such properties or you are selecting only such properties with `keys`. - "env:": For each property key , set an environment variable . - - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. + - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. The environment variable for a missing property is not set. - "env-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same environment variable; the last found key wins. In this mode, `keys` is required. - - "json": Set an action output "json" to all properties as a JSON object, formatted as a single-line string. - - "json-file:": Write a file with name with contents: all properties as a JSON object (formatting not specified). + - "json": Set an action output "json" to all (selected, if keys is set) properties as a JSON object, formatted as a single-line string. Selected keys for missing properties are included with value null. + - "json-file:": Write a file with name with contents: all (selected, if keys is set) properties as a JSON object (formatting not specified). Selected keys for missing properties are included with value null. required: false default: 'output' From 02666ad6bc0d1dcb83a5d42acbb1e5b9a659e724 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:03:34 +0200 Subject: [PATCH 147/190] refactor test: move shell setup into separate file --- .github/workflows/test.yml | 221 +++++-------------------------------- action.yml | 12 +- test/test-setup.sh | 171 ++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 202 deletions(-) create mode 100644 test/test-setup.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 580a1fa..be75c65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,179 +12,8 @@ env: SELECTED_NAME: 'my-prop' SELECTED_NAMES: 'java-version jvm_args unicode colon' + SHELL_SETUP: test/test-setup.sh NOT_FOUND_STATUS: 54 - SHELL_SETUP: |- - debugPrintf() { - printf "$@" | sed 's/^/::debug::/g' >&2 - } - - # Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is - # missing. - # Piping a command to this function works around the removal of trailing LFs in command substitution - # [https://stackoverflow.com/a/15184414] and preserves meaningful LFs. - # For example, when the command is `jq --raw-output …` and the result is a string with ends with LF, that LF is - # preserved, and the LF added by jq (like line-based commands generally do) is replaced. - # This enables precise comparisons. - replaceLastLF() { - perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' - } - - assertVariableUnset() { - local varName=$1; shift - if [ "${!varName+defined}" = defined ]; then - printf 'assertion error: %s (== "%s") is set\n' "$varName" "${!varName}" >&2 - return 1 - else - printf 'ok: %s is not set\n' "$varName" >&2 - fi - } - # test this function: - ( - set +e - unset x; assertVariableUnset x >&/dev/null; statusUnset=$? - x=''; assertVariableUnset x >&/dev/null; statusEmpty=$? - x=foo; assertVariableUnset x >&/dev/null; statusNonEmpty1=$? - x=defined; assertVariableUnset x >&/dev/null; statusNonEmpty2=$? - set -e - [ $statusUnset -eq 0 ] - [ $statusEmpty -ne 0 ] - [ $statusNonEmpty1 -ne 0 ] - [ $statusNonEmpty2 -ne 0 ] - ) - - assertEquals() { - debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" - if [ "$1" = "$2" ]; then - printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 - else - printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 - return 1 - fi - } - - # The last argument is the expected result. All other arguments are treated as a command (with arguments) - # to be evaluated which outputs the actual result. Both results are then asserted to be equal. - # - # Also, unlike directly using the form - # assertEquals "$(eval "$RESULT_GETTER 'key with spaces')" 'expected value' - # this function does not swallow a non-zero exit status in $RESULT_GETTER (but returns with that error - # status and skips the assertion). - # - # Of the arguments which form the command, only the first is evaled, the rest is treated literally. - # That odd behavior is useful for how the steps for similar test cases are written: They require - # different ways to get a result; for example, one needs `getJsonValue "$RESULT"` and another one - # `getJsonValue "$(<"$RESULT_FILE")`; this difference is moved to environment variables in order to keep - # the script identical, so it can be reused with a YAML anchor. - # Example usage: evalAndAssertEquals "$RESULT_GETTER" 'key with spaces' 'expected value' - # with RESULT_GETTER set to 'getJsonValue "$RESULT"'. - evalAndAssertEquals() { - local getterFunction=$1; shift - local args=("$@") - local expectedValue="${args[@]:${#args[@]}-1}" - args=("${args[@]:0:${#args[@]}-1}") - - local quotedArgs value - quotedArgs=$(printf ' %q' "${args[@]}") - - # Call $getterFunction once to check its status only before the call to capture its output. - # Otherwise, we'd get a misleading error from replaceLastLF if $getterFunction prints nothing and exits - # with $NOT_FOUND_STATUS. - eval "${getterFunction}${quotedArgs}" >/dev/null - eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" - - expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" - assertEquals "$value" "$expectedValue" - } - - # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. - # The command is handled like in →evalAndAssertEquals (except that there is no expected value as last argument which - # is not passed to the command). - evalAndAssertUndefined() { - local getterFunction=$1; shift - local quotedArgs value st - quotedArgs=$(printf ' %q' "$@") - if eval "value=\$(${getterFunction}${quotedArgs})"; then - printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 - return 1 - elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then - printf 'ok: %s is undefined\n' "$*" >&2 - else - # no error message, assuming failed $getterFunction has already written one - return $st - fi - } - - # Extracts the value for key $2 from JSON $1. - getJsonValue() { - local json=$1; shift - local key=$1; shift - debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" - jq --arg k "$key" --raw-output ' - .[$k] as $v - | if $v != null then - $v - elif (keys | index($k) != null) then #contains key $k (with a null or false value) - "" - else - (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber)) - end' <<<"$json" - } - - assertJsonDoesNotContainKey() { - assertJsonContainsKeyEquals "$@" false - } - - assertJsonContainsKey() { - assertJsonContainsKeyEquals "$@" true - } - - assertJsonContainsKeyEquals() { - local json=$1; shift - local key=$1; shift - local expected=$1; shift - debugPrintf 'assertJsonContainsKeyEquals("%s", "%s", "%s")\n' "$json" "$key" "$expected" - local contains - contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null' <<<"$json") - assertEquals "$contains" "$expected" - } - - # Encodes a key $1 in the output-style encoding (property key to output name). - encodeKey() { - local key=$1; shift - perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" - } - - # Extracts the value for key from JSON $2. - encodeKeyAndGetJsonValue() { - local json=$1; shift - local key=$1; shift - local encodedKey - encodedKey=$(encodeKey "$key") - getJsonValue "$json" "$encodedKey" "$@" - } - - # Prints the value of the environment variable set for key $1 (when not using a prefix). - getEnv() { - local key=$1; shift - debugPrintf 'getEnv("%s")\n' "$key" - local st - if printenv -- "$key"; then - : # OK - elif [ ${st-$?} -eq 1 ]; then - printf 'environment variable %s not set\n' "$key" >&2 - return $NOT_FOUND_STATUS - else - # no error message, assuming printenv has already written one - return $st - fi - } - - # Prints the value of the environment variable set for key $2 when using a prefix $1. - getEnvWithPrefix() { - local prefix=$1; shift - local key=$1; shift - getEnv "${prefix}${key}" "$@" - } jobs: ######################################################################### @@ -244,7 +73,7 @@ jobs: STATUS: ${{steps.input-error_resultType.outcome}} ERROR: ${{steps.input-error_resultType.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType: rt1' @@ -262,7 +91,7 @@ jobs: STATUS: ${{steps.input-error_resultType-output-without-names.outcome}} ERROR: ${{steps.input-error_resultType-output-without-names.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' @@ -280,7 +109,7 @@ jobs: STATUS: ${{steps.input-error_resultType-env-without-names.outcome}} ERROR: ${{steps.input-error_resultType-env-without-names.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' @@ -297,7 +126,7 @@ jobs: STATUS: ${{steps.input-error_resultType-output-without-keys.outcome}} ERROR: ${{steps.input-error_resultType-output-without-keys.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' @@ -314,7 +143,7 @@ jobs: STATUS: ${{steps.input-error_resultType-env-without-keys.outcome}} ERROR: ${{steps.input-error_resultType-env-without-keys.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' @@ -334,7 +163,7 @@ jobs: env: RESULT: ${{steps.default_single.outputs._sourceJavaVersion}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT" '21' test_output: @@ -360,7 +189,7 @@ jobs: RESULT_DASHK: ${{steps.output_multi_simple.outputs._dash-002Din-002DKey}} RESULT_UNI: ${{steps.output_multi_simple.outputs._unicode_escapes}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_SJV" '21' assertEquals "$RESULT_JVMARGS" '-ea -showversion' assertEquals "$RESULT_COLONK" 'c' @@ -379,7 +208,7 @@ jobs: RESULT_JSON: ${{toJSON(steps.output_all.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' @@ -413,7 +242,7 @@ jobs: env: RESULT_VALUE: ${{steps.output_all.outputs.value || 'U_N_S_E_T'}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== @@ -430,7 +259,7 @@ jobs: RESULT_JSON: ${{toJSON(steps.output_multi.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateMultiImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' @@ -466,7 +295,7 @@ jobs: env: RESULT_VALUE: ${{steps.output_multi.outputs.value}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" # ===================================================================== @@ -516,7 +345,7 @@ jobs: env: RESULT_VALUE: ${{steps.output_multi2.outputs.value}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" # ===================================================================== @@ -533,7 +362,7 @@ jobs: RESULT_JSON: ${{toJSON(steps.output_single.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateSingleImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_KEY" '21' evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion @@ -579,7 +408,7 @@ jobs: RESULT_VALUE: ${{steps.output_single_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output_single_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' assertEquals "$RESULT_VALUE" '' @@ -602,7 +431,7 @@ jobs: RESULT_VALUE: ${{steps.output_multi_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT1" '' assertEquals "$RESULT2" '' @@ -632,7 +461,7 @@ jobs: # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamedImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" java-version '21' evalAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' evalAndAssertEquals "$RESULT_GETTER" colon 'c' @@ -652,7 +481,7 @@ jobs: env: RESULT_VALUE: ${{steps.output-named_multi.outputs.value || 'U_N_S_E_T'}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== @@ -700,7 +529,7 @@ jobs: # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NAME" 'c' # The outputs with the default names are not set: @@ -726,7 +555,7 @@ jobs: RESULT: ${{steps.output-named_single_missing.outputs.my-prop}} RESULT_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' # Assert set: @@ -745,7 +574,7 @@ jobs: RESULT: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} RESULT_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' # Assert set: @@ -795,7 +624,7 @@ jobs: env: RESULT_JSON: ${{steps.json_multi_missing.outputs.json}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" resultGetter='getJsonValue "$RESULT_JSON"' evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' evalAndAssertEquals "$resultGetter" org.gradle.jvmargs '-ea -showversion' @@ -889,7 +718,7 @@ jobs: env: JSON_OUTPUT_FILE: ${{steps.json-file_multi_missing_arrange.outputs.file}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" resultJson=$(<"$JSON_OUTPUT_FILE") resultGetter='getJsonValue "$resultJson"' evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' @@ -949,7 +778,7 @@ jobs: resultType: env - name: '[assert] query multiple missing properties as environment variables' run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertVariableUnset "$SELECTED_NON_EX_KEY" assertVariableUnset "$SELECTED_NON_EX_KEY2" assertEquals "${!SELECTED_KEY}" '21' @@ -1052,7 +881,7 @@ jobs: resultType: 'env-named:NE1 E1 NE2' - name: '[assert] query multiple missing properties as environment variables' run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertVariableUnset NE1 assertVariableUnset NE2 assertEquals "$E1" '21' diff --git a/action.yml b/action.yml index 87d5ccc..d771ef7 100644 --- a/action.yml +++ b/action.yml @@ -27,16 +27,16 @@ inputs: Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name "a-002Eb-002Dc_d". - Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. If `keys` is given, but none is found, "value" is set to "". (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) - - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. If none is found, the output is set to "". + Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. If `keys` is given, but none is found, "value" is set to empty. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) + - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) The output for a missing property is set to empty. If you need to distinguish between empty an undefined properties, resultType "json" or "json-file" is recommended. + - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. If none is found, the output is set to empty. - "env": For each property key , set an environment variable . In order not to pollute the environment with hard to understand variables, this should only be used to set some specifically named variables. I.e. you know that the property file can only contain such properties or you are selecting only such properties with `keys`. - "env:": For each property key , set an environment variable . - - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. + - "env-named:": is a `resultNameSeparator`-separated list of variable names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as environment variable `[i]`. In this mode, `keys` is required. The environment variable for a missing property is not set. - "env-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same environment variable; the last found key wins. In this mode, `keys` is required. - - "json": Set an action output "json" to all (selected, if keys is set) properties as a JSON object, formatted as a single-line string. Selected keys for which no property is found are included with value null. - - "json-file:": Write a file with name with contents: all (selected, if keys is set) properties as a JSON object (formatting not specified). Selected keys for which no property is found are included with value null. + - "json": Set an action output "json" to all (selected, if keys is set) properties as a JSON object, formatted as a single-line string. Selected keys for missing properties are included with value null. + - "json-file:": Write a file with name with contents: all (selected, if keys is set) properties as a JSON object (formatting not specified). Selected keys for missing properties are included with value null. required: false default: 'output' diff --git a/test/test-setup.sh b/test/test-setup.sh new file mode 100644 index 0000000..1b45b11 --- /dev/null +++ b/test/test-setup.sh @@ -0,0 +1,171 @@ +debugPrintf() { + printf "$@" | sed 's/^/::debug::/g' >&2 +} + +# Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is +# missing. +# Piping a command to this function works around the removal of trailing LFs in command substitution +# [https://stackoverflow.com/a/15184414] and preserves meaningful LFs. +# For example, when the command is `jq --raw-output …` and the result is a string with ends with LF, that LF is +# preserved, and the LF added by jq (like line-based commands generally do) is replaced. +# This enables precise comparisons. +replaceLastLF() { + perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' +} + +assertVariableUnset() { + local varName=$1; shift + if [ "${!varName+defined}" = defined ]; then + printf 'assertion error: %s (== "%s") is set\n' "$varName" "${!varName}" >&2 + return 1 + else + printf 'ok: %s is not set\n' "$varName" >&2 + fi +} +# test this function: +( + set +e + unset x; assertVariableUnset x >&/dev/null; statusUnset=$? + x=''; assertVariableUnset x >&/dev/null; statusEmpty=$? + x=foo; assertVariableUnset x >&/dev/null; statusNonEmpty1=$? + x=defined; assertVariableUnset x >&/dev/null; statusNonEmpty2=$? + set -e + [ $statusUnset -eq 0 ] + [ $statusEmpty -ne 0 ] + [ $statusNonEmpty1 -ne 0 ] + [ $statusNonEmpty2 -ne 0 ] +) + +assertEquals() { + debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" + if [ "$1" = "$2" ]; then + printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 + else + printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 + return 1 + fi +} + +# The last argument is the expected result. All other arguments are treated as a command (with arguments) +# to be evaluated which outputs the actual result. Both results are then asserted to be equal. +# +# Also, unlike directly using the form +# assertEquals "$(eval "$RESULT_GETTER 'key with spaces')" 'expected value' +# this function does not swallow a non-zero exit status in $RESULT_GETTER (but returns with that error +# status and skips the assertion). +# +# Of the arguments which form the command, only the first is evaled, the rest is treated literally. +# That odd behavior is useful for how the steps for similar test cases are written: They require +# different ways to get a result; for example, one needs `getJsonValue "$RESULT"` and another one +# `getJsonValue "$(<"$RESULT_FILE")`; this difference is moved to environment variables in order to keep +# the script identical, so it can be reused with a YAML anchor. +# Example usage: evalAndAssertEquals "$RESULT_GETTER" 'key with spaces' 'expected value' +# with RESULT_GETTER set to 'getJsonValue "$RESULT"'. +evalAndAssertEquals() { + local getterFunction=$1; shift + local args=("$@") + local expectedValue="${args[@]:${#args[@]}-1}" + args=("${args[@]:0:${#args[@]}-1}") + + local quotedArgs value + quotedArgs=$(printf ' %q' "${args[@]}") + + # Call $getterFunction once to check its status only before the call to capture its output. + # Otherwise, we'd get a misleading error from replaceLastLF if $getterFunction prints nothing and exits + # with $NOT_FOUND_STATUS. + eval "${getterFunction}${quotedArgs}" >/dev/null + eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" + + expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" + assertEquals "$value" "$expectedValue" +} + +# Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. +# The command is handled like in →evalAndAssertEquals (except that there is no expected value as last argument which +# is not passed to the command). +evalAndAssertUndefined() { + local getterFunction=$1; shift + local quotedArgs value st + quotedArgs=$(printf ' %q' "$@") + if eval "value=\$(${getterFunction}${quotedArgs})"; then + printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 + return 1 + elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then + printf 'ok: %s is undefined\n' "$*" >&2 + else + # no error message, assuming failed $getterFunction has already written one + return $st + fi +} + +# Extracts the value for key $2 from JSON $1. +getJsonValue() { + local json=$1; shift + local key=$1; shift + debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" + jq --arg k "$key" --raw-output ' + .[$k] as $v + | if $v != null then + $v + elif (keys | index($k) != null) then #contains key $k (with a null or false value) + "" + else + (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber)) + end' <<<"$json" +} + +assertJsonDoesNotContainKey() { + assertJsonContainsKeyEquals "$@" false +} + +assertJsonContainsKey() { + assertJsonContainsKeyEquals "$@" true +} + +assertJsonContainsKeyEquals() { + local json=$1; shift + local key=$1; shift + local expected=$1; shift + debugPrintf 'assertJsonContainsKeyEquals("%s", "%s", "%s")\n' "$json" "$key" "$expected" + local contains + contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null' <<<"$json") + assertEquals "$contains" "$expected" +} + +# Encodes a key $1 in the output-style encoding (property key to output name). +encodeKey() { + local key=$1; shift + perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" +} + +# Extracts the value for key from JSON $2. +encodeKeyAndGetJsonValue() { + local json=$1; shift + local key=$1; shift + local encodedKey + encodedKey=$(encodeKey "$key") + getJsonValue "$json" "$encodedKey" "$@" +} + +# Prints the value of the environment variable set for key $1 (when not using a prefix). +getEnv() { + local key=$1; shift + debugPrintf 'getEnv("%s")\n' "$key" + local st + if printenv -- "$key"; then + : # OK + elif [ ${st-$?} -eq 1 ]; then + printf 'environment variable %s not set\n' "$key" >&2 + return $NOT_FOUND_STATUS + else + # no error message, assuming printenv has already written one + return $st + fi +} + +# Prints the value of the environment variable set for key $2 when using a prefix $1. +getEnvWithPrefix() { + local prefix=$1; shift + local key=$1; shift + getEnv "${prefix}${key}" "$@" +} From 381ae3c6c7aa01ebaac0a1b0bf40e28aec7b813b Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:05:30 +0200 Subject: [PATCH 148/190] refactor test: move shell setup into separate file --- .github/workflows/test.yml | 221 +++++-------------------------------- test/test-setup.sh | 171 ++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 196 deletions(-) create mode 100644 test/test-setup.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 580a1fa..be75c65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,179 +12,8 @@ env: SELECTED_NAME: 'my-prop' SELECTED_NAMES: 'java-version jvm_args unicode colon' + SHELL_SETUP: test/test-setup.sh NOT_FOUND_STATUS: 54 - SHELL_SETUP: |- - debugPrintf() { - printf "$@" | sed 's/^/::debug::/g' >&2 - } - - # Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is - # missing. - # Piping a command to this function works around the removal of trailing LFs in command substitution - # [https://stackoverflow.com/a/15184414] and preserves meaningful LFs. - # For example, when the command is `jq --raw-output …` and the result is a string with ends with LF, that LF is - # preserved, and the LF added by jq (like line-based commands generally do) is replaced. - # This enables precise comparisons. - replaceLastLF() { - perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' - } - - assertVariableUnset() { - local varName=$1; shift - if [ "${!varName+defined}" = defined ]; then - printf 'assertion error: %s (== "%s") is set\n' "$varName" "${!varName}" >&2 - return 1 - else - printf 'ok: %s is not set\n' "$varName" >&2 - fi - } - # test this function: - ( - set +e - unset x; assertVariableUnset x >&/dev/null; statusUnset=$? - x=''; assertVariableUnset x >&/dev/null; statusEmpty=$? - x=foo; assertVariableUnset x >&/dev/null; statusNonEmpty1=$? - x=defined; assertVariableUnset x >&/dev/null; statusNonEmpty2=$? - set -e - [ $statusUnset -eq 0 ] - [ $statusEmpty -ne 0 ] - [ $statusNonEmpty1 -ne 0 ] - [ $statusNonEmpty2 -ne 0 ] - ) - - assertEquals() { - debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" - if [ "$1" = "$2" ]; then - printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 - else - printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 - return 1 - fi - } - - # The last argument is the expected result. All other arguments are treated as a command (with arguments) - # to be evaluated which outputs the actual result. Both results are then asserted to be equal. - # - # Also, unlike directly using the form - # assertEquals "$(eval "$RESULT_GETTER 'key with spaces')" 'expected value' - # this function does not swallow a non-zero exit status in $RESULT_GETTER (but returns with that error - # status and skips the assertion). - # - # Of the arguments which form the command, only the first is evaled, the rest is treated literally. - # That odd behavior is useful for how the steps for similar test cases are written: They require - # different ways to get a result; for example, one needs `getJsonValue "$RESULT"` and another one - # `getJsonValue "$(<"$RESULT_FILE")`; this difference is moved to environment variables in order to keep - # the script identical, so it can be reused with a YAML anchor. - # Example usage: evalAndAssertEquals "$RESULT_GETTER" 'key with spaces' 'expected value' - # with RESULT_GETTER set to 'getJsonValue "$RESULT"'. - evalAndAssertEquals() { - local getterFunction=$1; shift - local args=("$@") - local expectedValue="${args[@]:${#args[@]}-1}" - args=("${args[@]:0:${#args[@]}-1}") - - local quotedArgs value - quotedArgs=$(printf ' %q' "${args[@]}") - - # Call $getterFunction once to check its status only before the call to capture its output. - # Otherwise, we'd get a misleading error from replaceLastLF if $getterFunction prints nothing and exits - # with $NOT_FOUND_STATUS. - eval "${getterFunction}${quotedArgs}" >/dev/null - eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" - - expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" - assertEquals "$value" "$expectedValue" - } - - # Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. - # The command is handled like in →evalAndAssertEquals (except that there is no expected value as last argument which - # is not passed to the command). - evalAndAssertUndefined() { - local getterFunction=$1; shift - local quotedArgs value st - quotedArgs=$(printf ' %q' "$@") - if eval "value=\$(${getterFunction}${quotedArgs})"; then - printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 - return 1 - elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then - printf 'ok: %s is undefined\n' "$*" >&2 - else - # no error message, assuming failed $getterFunction has already written one - return $st - fi - } - - # Extracts the value for key $2 from JSON $1. - getJsonValue() { - local json=$1; shift - local key=$1; shift - debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" - jq --arg k "$key" --raw-output ' - .[$k] as $v - | if $v != null then - $v - elif (keys | index($k) != null) then #contains key $k (with a null or false value) - "" - else - (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber)) - end' <<<"$json" - } - - assertJsonDoesNotContainKey() { - assertJsonContainsKeyEquals "$@" false - } - - assertJsonContainsKey() { - assertJsonContainsKeyEquals "$@" true - } - - assertJsonContainsKeyEquals() { - local json=$1; shift - local key=$1; shift - local expected=$1; shift - debugPrintf 'assertJsonContainsKeyEquals("%s", "%s", "%s")\n' "$json" "$key" "$expected" - local contains - contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null' <<<"$json") - assertEquals "$contains" "$expected" - } - - # Encodes a key $1 in the output-style encoding (property key to output name). - encodeKey() { - local key=$1; shift - perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" - } - - # Extracts the value for key from JSON $2. - encodeKeyAndGetJsonValue() { - local json=$1; shift - local key=$1; shift - local encodedKey - encodedKey=$(encodeKey "$key") - getJsonValue "$json" "$encodedKey" "$@" - } - - # Prints the value of the environment variable set for key $1 (when not using a prefix). - getEnv() { - local key=$1; shift - debugPrintf 'getEnv("%s")\n' "$key" - local st - if printenv -- "$key"; then - : # OK - elif [ ${st-$?} -eq 1 ]; then - printf 'environment variable %s not set\n' "$key" >&2 - return $NOT_FOUND_STATUS - else - # no error message, assuming printenv has already written one - return $st - fi - } - - # Prints the value of the environment variable set for key $2 when using a prefix $1. - getEnvWithPrefix() { - local prefix=$1; shift - local key=$1; shift - getEnv "${prefix}${key}" "$@" - } jobs: ######################################################################### @@ -244,7 +73,7 @@ jobs: STATUS: ${{steps.input-error_resultType.outcome}} ERROR: ${{steps.input-error_resultType.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType: rt1' @@ -262,7 +91,7 @@ jobs: STATUS: ${{steps.input-error_resultType-output-without-names.outcome}} ERROR: ${{steps.input-error_resultType-output-without-names.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' @@ -280,7 +109,7 @@ jobs: STATUS: ${{steps.input-error_resultType-env-without-names.outcome}} ERROR: ${{steps.input-error_resultType-env-without-names.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' @@ -297,7 +126,7 @@ jobs: STATUS: ${{steps.input-error_resultType-output-without-keys.outcome}} ERROR: ${{steps.input-error_resultType-output-without-keys.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' @@ -314,7 +143,7 @@ jobs: STATUS: ${{steps.input-error_resultType-env-without-keys.outcome}} ERROR: ${{steps.input-error_resultType-env-without-keys.outputs.error}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' @@ -334,7 +163,7 @@ jobs: env: RESULT: ${{steps.default_single.outputs._sourceJavaVersion}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT" '21' test_output: @@ -360,7 +189,7 @@ jobs: RESULT_DASHK: ${{steps.output_multi_simple.outputs._dash-002Din-002DKey}} RESULT_UNI: ${{steps.output_multi_simple.outputs._unicode_escapes}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_SJV" '21' assertEquals "$RESULT_JVMARGS" '-ea -showversion' assertEquals "$RESULT_COLONK" 'c' @@ -379,7 +208,7 @@ jobs: RESULT_JSON: ${{toJSON(steps.output_all.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateAllImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" targetJavaVersion '17' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' @@ -413,7 +242,7 @@ jobs: env: RESULT_VALUE: ${{steps.output_all.outputs.value || 'U_N_S_E_T'}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== @@ -430,7 +259,7 @@ jobs: RESULT_JSON: ${{toJSON(steps.output_multi.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateMultiImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" sourceJavaVersion '21' evalAndAssertEquals "$RESULT_GETTER" org.gradle.jvmargs '-ea -showversion' evalAndAssertEquals "$RESULT_GETTER" colon:in:Key 'c' @@ -466,7 +295,7 @@ jobs: env: RESULT_VALUE: ${{steps.output_multi.outputs.value}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'c' # from last key "colon:in:Key" # ===================================================================== @@ -516,7 +345,7 @@ jobs: env: RESULT_VALUE: ${{steps.output_multi2.outputs.value}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" 'a«,»c' # from last key "unicode_escapes" # ===================================================================== @@ -533,7 +362,7 @@ jobs: RESULT_JSON: ${{toJSON(steps.output_single.outputs)}} RESULT_GETTER: 'encodeKeyAndGetJsonValue "$RESULT_JSON"' run: &validateSingleImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_KEY" '21' evalAndAssertUndefined "$RESULT_GETTER" targetJavaVersion @@ -579,7 +408,7 @@ jobs: RESULT_VALUE: ${{steps.output_single_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output_single_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' assertEquals "$RESULT_VALUE" '' @@ -602,7 +431,7 @@ jobs: RESULT_VALUE: ${{steps.output_multi_missing.outputs.value}} RESULT_JSON: ${{toJSON(steps.output_multi_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT1" '' assertEquals "$RESULT2" '' @@ -632,7 +461,7 @@ jobs: # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamedImpl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" java-version '21' evalAndAssertEquals "$RESULT_GETTER" jvm_args '-ea -showversion' evalAndAssertEquals "$RESULT_GETTER" colon 'c' @@ -652,7 +481,7 @@ jobs: env: RESULT_VALUE: ${{steps.output-named_multi.outputs.value || 'U_N_S_E_T'}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertEquals "$RESULT_VALUE" U_N_S_E_T # ===================================================================== @@ -700,7 +529,7 @@ jobs: # Output names specified as input are not subject to encoding (prefix "_", replace punctuation except "_"): RESULT_GETTER: 'getJsonValue "$RESULT_JSON"' run: &validateMultiNamed1Impl |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" evalAndAssertEquals "$RESULT_GETTER" "$SELECTED_NAME" 'c' # The outputs with the default names are not set: @@ -726,7 +555,7 @@ jobs: RESULT: ${{steps.output-named_single_missing.outputs.my-prop}} RESULT_JSON: ${{toJSON(steps.output-named_single_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' # Assert set: @@ -745,7 +574,7 @@ jobs: RESULT: ${{steps.output-named_multi_missing.outputs.nonExistingKey1}} RESULT_JSON: ${{toJSON(steps.output-named_multi_missing.outputs)}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" # Like this, the expected empty value is not distinguishable from unset: assertEquals "$RESULT" '' # Assert set: @@ -795,7 +624,7 @@ jobs: env: RESULT_JSON: ${{steps.json_multi_missing.outputs.json}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" resultGetter='getJsonValue "$RESULT_JSON"' evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' evalAndAssertEquals "$resultGetter" org.gradle.jvmargs '-ea -showversion' @@ -889,7 +718,7 @@ jobs: env: JSON_OUTPUT_FILE: ${{steps.json-file_multi_missing_arrange.outputs.file}} run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" resultJson=$(<"$JSON_OUTPUT_FILE") resultGetter='getJsonValue "$resultJson"' evalAndAssertEquals "$resultGetter" sourceJavaVersion '21' @@ -949,7 +778,7 @@ jobs: resultType: env - name: '[assert] query multiple missing properties as environment variables' run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertVariableUnset "$SELECTED_NON_EX_KEY" assertVariableUnset "$SELECTED_NON_EX_KEY2" assertEquals "${!SELECTED_KEY}" '21' @@ -1052,7 +881,7 @@ jobs: resultType: 'env-named:NE1 E1 NE2' - name: '[assert] query multiple missing properties as environment variables' run: |- - eval "$SHELL_SETUP" + . "$SHELL_SETUP" assertVariableUnset NE1 assertVariableUnset NE2 assertEquals "$E1" '21' diff --git a/test/test-setup.sh b/test/test-setup.sh new file mode 100644 index 0000000..1b45b11 --- /dev/null +++ b/test/test-setup.sh @@ -0,0 +1,171 @@ +debugPrintf() { + printf "$@" | sed 's/^/::debug::/g' >&2 +} + +# Reads stdin and replaces one linefeed at the end of the input with "¶". Exits with non-zero status if that LF is +# missing. +# Piping a command to this function works around the removal of trailing LFs in command substitution +# [https://stackoverflow.com/a/15184414] and preserves meaningful LFs. +# For example, when the command is `jq --raw-output …` and the result is a string with ends with LF, that LF is +# preserved, and the LF added by jq (like line-based commands generally do) is replaced. +# This enables precise comparisons. +replaceLastLF() { + perl -e '$/ = undef; $_ = <>; ($r = $_) =~ s{\n$}{}; die "missing trailing LF\n" if $r eq $_; printf "%s¶", $r' +} + +assertVariableUnset() { + local varName=$1; shift + if [ "${!varName+defined}" = defined ]; then + printf 'assertion error: %s (== "%s") is set\n' "$varName" "${!varName}" >&2 + return 1 + else + printf 'ok: %s is not set\n' "$varName" >&2 + fi +} +# test this function: +( + set +e + unset x; assertVariableUnset x >&/dev/null; statusUnset=$? + x=''; assertVariableUnset x >&/dev/null; statusEmpty=$? + x=foo; assertVariableUnset x >&/dev/null; statusNonEmpty1=$? + x=defined; assertVariableUnset x >&/dev/null; statusNonEmpty2=$? + set -e + [ $statusUnset -eq 0 ] + [ $statusEmpty -ne 0 ] + [ $statusNonEmpty1 -ne 0 ] + [ $statusNonEmpty2 -ne 0 ] +) + +assertEquals() { + debugPrintf 'assertEquals("%s", "%s")\n' "$1" "$2" + if [ "$1" = "$2" ]; then + printf 'ok: "%s" == "%s"\n' "$1" "$2" >&2 + else + printf 'assertion error: "%s" != "%s"\n' "$1" "$2" >&2 + return 1 + fi +} + +# The last argument is the expected result. All other arguments are treated as a command (with arguments) +# to be evaluated which outputs the actual result. Both results are then asserted to be equal. +# +# Also, unlike directly using the form +# assertEquals "$(eval "$RESULT_GETTER 'key with spaces')" 'expected value' +# this function does not swallow a non-zero exit status in $RESULT_GETTER (but returns with that error +# status and skips the assertion). +# +# Of the arguments which form the command, only the first is evaled, the rest is treated literally. +# That odd behavior is useful for how the steps for similar test cases are written: They require +# different ways to get a result; for example, one needs `getJsonValue "$RESULT"` and another one +# `getJsonValue "$(<"$RESULT_FILE")`; this difference is moved to environment variables in order to keep +# the script identical, so it can be reused with a YAML anchor. +# Example usage: evalAndAssertEquals "$RESULT_GETTER" 'key with spaces' 'expected value' +# with RESULT_GETTER set to 'getJsonValue "$RESULT"'. +evalAndAssertEquals() { + local getterFunction=$1; shift + local args=("$@") + local expectedValue="${args[@]:${#args[@]}-1}" + args=("${args[@]:0:${#args[@]}-1}") + + local quotedArgs value + quotedArgs=$(printf ' %q' "${args[@]}") + + # Call $getterFunction once to check its status only before the call to capture its output. + # Otherwise, we'd get a misleading error from replaceLastLF if $getterFunction prints nothing and exits + # with $NOT_FOUND_STATUS. + eval "${getterFunction}${quotedArgs}" >/dev/null + eval "value=\$(set -o pipefail; ${getterFunction}${quotedArgs} | replaceLastLF)" + + expectedValue="$(printf '%s\n' "$expectedValue" | replaceLastLF)" + assertEquals "$value" "$expectedValue" +} + +# Queries a value by calling command $1 with the remaining args and expects it to exit with status $NOT_FOUND_STATUS. +# The command is handled like in →evalAndAssertEquals (except that there is no expected value as last argument which +# is not passed to the command). +evalAndAssertUndefined() { + local getterFunction=$1; shift + local quotedArgs value st + quotedArgs=$(printf ' %q' "$@") + if eval "value=\$(${getterFunction}${quotedArgs})"; then + printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 + return 1 + elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then + printf 'ok: %s is undefined\n' "$*" >&2 + else + # no error message, assuming failed $getterFunction has already written one + return $st + fi +} + +# Extracts the value for key $2 from JSON $1. +getJsonValue() { + local json=$1; shift + local key=$1; shift + debugPrintf 'getJsonValue("%s", "%s")\n' "$json" "$key" + jq --arg k "$key" --raw-output ' + .[$k] as $v + | if $v != null then + $v + elif (keys | index($k) != null) then #contains key $k (with a null or false value) + "" + else + (("key " + $k + " not found\n") | halt_error(env.NOT_FOUND_STATUS | tonumber)) + end' <<<"$json" +} + +assertJsonDoesNotContainKey() { + assertJsonContainsKeyEquals "$@" false +} + +assertJsonContainsKey() { + assertJsonContainsKeyEquals "$@" true +} + +assertJsonContainsKeyEquals() { + local json=$1; shift + local key=$1; shift + local expected=$1; shift + debugPrintf 'assertJsonContainsKeyEquals("%s", "%s", "%s")\n' "$json" "$key" "$expected" + local contains + contains=$(jq --arg k "$key" --raw-output 'keys | index($k) != null' <<<"$json") + assertEquals "$contains" "$expected" +} + +# Encodes a key $1 in the output-style encoding (property key to output name). +encodeKey() { + local key=$1; shift + perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" +} + +# Extracts the value for key from JSON $2. +encodeKeyAndGetJsonValue() { + local json=$1; shift + local key=$1; shift + local encodedKey + encodedKey=$(encodeKey "$key") + getJsonValue "$json" "$encodedKey" "$@" +} + +# Prints the value of the environment variable set for key $1 (when not using a prefix). +getEnv() { + local key=$1; shift + debugPrintf 'getEnv("%s")\n' "$key" + local st + if printenv -- "$key"; then + : # OK + elif [ ${st-$?} -eq 1 ]; then + printf 'environment variable %s not set\n' "$key" >&2 + return $NOT_FOUND_STATUS + else + # no error message, assuming printenv has already written one + return $st + fi +} + +# Prints the value of the environment variable set for key $2 when using a prefix $1. +getEnvWithPrefix() { + local prefix=$1; shift + local key=$1; shift + getEnv "${prefix}${key}" "$@" +} From b621ad4f7ce57858557f03fd6f51da6e951ffc5b Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:20:47 +0200 Subject: [PATCH 149/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be75c65..f3e05f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,9 +35,23 @@ jobs: - test_env-with-prefix_all - test_env-with-prefix_multi - test_env-with-prefix_single + - foo if: always() runs-on: ubuntu-latest steps: + - &checkoutStep + name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: "check that this job's `needs` is set to exactly all other jobs" + run: |- + diff --minimal \ + <(yq -r '.jobs | keys() | .[]' .github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match test) \ + <(yq -r '.jobs.test.needs[]' < .github/workflows/test.yml | sort) + - name: check overall status env: JOBS_TO_CHECK_JSON: ${{toJSON(needs)}} @@ -52,13 +66,7 @@ jobs: test_invalid-usage: runs-on: ubuntu-latest steps: - - &checkoutStep - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + - *checkoutStep # ===================================================================== - name: '[act] (should fail): invalid input: resultType' From 6a5f13e4ac9dad69645cfc8dbd3f462765a5af8f Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:22:32 +0200 Subject: [PATCH 150/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3e05f5..30fdfc7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,6 @@ jobs: - test_env-with-prefix_all - test_env-with-prefix_multi - test_env-with-prefix_single - - foo if: always() runs-on: ubuntu-latest steps: From eb9784b3e9011290e1784db18063ddf66abd263c Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:24:19 +0200 Subject: [PATCH 151/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30fdfc7..f3464de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,9 +29,11 @@ jobs: - test_json-file - test_env_all - test_env_multi + - test_env_multi_missing + - test_env_single - test_env-named_multi - test_env-named_multi-to-single - - test_env_single + - test_env-named_multi_missing - test_env-with-prefix_all - test_env-with-prefix_multi - test_env-with-prefix_single From 45deb2bb8ed3c5f1f370fc480d030a1f79b77d08 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:25:59 +0200 Subject: [PATCH 152/190] fix test: add missing jobs to meta job "test"; add step to check completeness --- .github/workflows/test.yml | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be75c65..f3464de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,15 +29,30 @@ jobs: - test_json-file - test_env_all - test_env_multi + - test_env_multi_missing + - test_env_single - test_env-named_multi - test_env-named_multi-to-single - - test_env_single + - test_env-named_multi_missing - test_env-with-prefix_all - test_env-with-prefix_multi - test_env-with-prefix_single if: always() runs-on: ubuntu-latest steps: + - &checkoutStep + name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: "check that this job's `needs` is set to exactly all other jobs" + run: |- + diff --minimal \ + <(yq -r '.jobs | keys() | .[]' .github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match test) \ + <(yq -r '.jobs.test.needs[]' < .github/workflows/test.yml | sort) + - name: check overall status env: JOBS_TO_CHECK_JSON: ${{toJSON(needs)}} @@ -52,13 +67,7 @@ jobs: test_invalid-usage: runs-on: ubuntu-latest steps: - - &checkoutStep - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ github.head_ref }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + - *checkoutStep # ===================================================================== - name: '[act] (should fail): invalid input: resultType' From 2dafa04f348c11bc9a3e416630d93d7feaf06a84 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:29:55 +0200 Subject: [PATCH 153/190] fix test: add missing jobs to meta job "test"; add step to check completeness --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3464de..2d0b6a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: "check that this job's `needs` is set to exactly all other jobs" run: |- - diff --minimal \ + diff --minimal --report-identical-files \ <(yq -r '.jobs | keys() | .[]' .github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match test) \ <(yq -r '.jobs.test.needs[]' < .github/workflows/test.yml | sort) From f074ea5432a24bdec9ea5616597a7c22b79b28d4 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 15:51:25 +0200 Subject: [PATCH 154/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 11 +++++------ Action.java | 2 +- test/test-setup.sh | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3464de..c77af2e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: "check that this job's `needs` is set to exactly all other jobs" run: |- - diff --minimal \ + diff --minimal --report-identical-files \ <(yq -r '.jobs | keys() | .[]' .github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match test) \ <(yq -r '.jobs.test.needs[]' < .github/workflows/test.yml | sort) @@ -188,22 +188,21 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_KEYS}} dash-in-Key' + keySeparator: ', ' + keys: 'sourceJavaVersion, org.gradle.jvmargs, space in Key, dash-in-Key' resultType: output - name: '[assert] query multiple properties as action outputs (simple)' env: RESULT_SJV: ${{steps.output_multi_simple.outputs._sourceJavaVersion}} RESULT_JVMARGS: ${{steps.output_multi_simple.outputs._org-002Egradle-002Ejvmargs}} - RESULT_COLONK: ${{steps.output_multi_simple.outputs._colon-003Ain-003AKey}} + RESULT_SPACEK: ${{steps.output_multi_simple.outputs._space-0020in-0020Key}} RESULT_DASHK: ${{steps.output_multi_simple.outputs._dash-002Din-002DKey}} - RESULT_UNI: ${{steps.output_multi_simple.outputs._unicode_escapes}} run: |- . "$SHELL_SETUP" assertEquals "$RESULT_SJV" '21' assertEquals "$RESULT_JVMARGS" '-ea -showversion' - assertEquals "$RESULT_COLONK" 'c' + assertEquals "$RESULT_SPACEK" 'sp' assertEquals "$RESULT_DASHK" 'd' - assertEquals "$RESULT_UNI" 'a«,»c' # ===================================================================== - name: '[act] query all properties as action outputs' diff --git a/Action.java b/Action.java index 4759a0e..2e3306f 100644 --- a/Action.java +++ b/Action.java @@ -191,7 +191,7 @@ public void write(Map> props, Config config) throws IOE private static String encodeKey(String key) { StringBuilder result = new StringBuilder(key.length() + 4); - Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); + Matcher matcher = Pattern.compile("([\\s\\p{Punct}&&[^_]])").matcher(key); while (matcher.find()) { matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); } diff --git a/test/test-setup.sh b/test/test-setup.sh index 1b45b11..deb6ac7 100644 --- a/test/test-setup.sh +++ b/test/test-setup.sh @@ -135,7 +135,7 @@ assertJsonContainsKeyEquals() { # Encodes a key $1 in the output-style encoding (property key to output name). encodeKey() { local key=$1; shift - perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" + perl -pe 's=((?!_)[[:space:][:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" } # Extracts the value for key from JSON $2. From b7d2725dc64c67b87d633dba1b73d2b3009ea4cf Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:02:43 +0200 Subject: [PATCH 155/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test-setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-setup.sh b/test/test-setup.sh index deb6ac7..6a2f9bd 100644 --- a/test/test-setup.sh +++ b/test/test-setup.sh @@ -135,7 +135,7 @@ assertJsonContainsKeyEquals() { # Encodes a key $1 in the output-style encoding (property key to output name). encodeKey() { local key=$1; shift - perl -pe 's=((?!_)[[:space:][:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" + perl -ple 's=((?!_)[[:space:][:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" } # Extracts the value for key from JSON $2. From df35bb2ed8d42e4e42cc05f0a7550de3f63afa51 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:10:11 +0200 Subject: [PATCH 156/190] output name encoding also for whitespace characters (as was already documented) --- .github/workflows/test.yml | 9 ++++----- Action.java | 2 +- test/test-setup.sh | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d0b6a3..c77af2e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -188,22 +188,21 @@ jobs: uses: ./ with: file: test/resources/test.properties - keys: '${{env.SELECTED_KEYS}} dash-in-Key' + keySeparator: ', ' + keys: 'sourceJavaVersion, org.gradle.jvmargs, space in Key, dash-in-Key' resultType: output - name: '[assert] query multiple properties as action outputs (simple)' env: RESULT_SJV: ${{steps.output_multi_simple.outputs._sourceJavaVersion}} RESULT_JVMARGS: ${{steps.output_multi_simple.outputs._org-002Egradle-002Ejvmargs}} - RESULT_COLONK: ${{steps.output_multi_simple.outputs._colon-003Ain-003AKey}} + RESULT_SPACEK: ${{steps.output_multi_simple.outputs._space-0020in-0020Key}} RESULT_DASHK: ${{steps.output_multi_simple.outputs._dash-002Din-002DKey}} - RESULT_UNI: ${{steps.output_multi_simple.outputs._unicode_escapes}} run: |- . "$SHELL_SETUP" assertEquals "$RESULT_SJV" '21' assertEquals "$RESULT_JVMARGS" '-ea -showversion' - assertEquals "$RESULT_COLONK" 'c' + assertEquals "$RESULT_SPACEK" 'sp' assertEquals "$RESULT_DASHK" 'd' - assertEquals "$RESULT_UNI" 'a«,»c' # ===================================================================== - name: '[act] query all properties as action outputs' diff --git a/Action.java b/Action.java index 4759a0e..2e3306f 100644 --- a/Action.java +++ b/Action.java @@ -191,7 +191,7 @@ public void write(Map> props, Config config) throws IOE private static String encodeKey(String key) { StringBuilder result = new StringBuilder(key.length() + 4); - Matcher matcher = Pattern.compile("([\\p{Punct}&&[^_]])").matcher(key); + Matcher matcher = Pattern.compile("([\\s\\p{Punct}&&[^_]])").matcher(key); while (matcher.find()) { matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); } diff --git a/test/test-setup.sh b/test/test-setup.sh index 1b45b11..6a2f9bd 100644 --- a/test/test-setup.sh +++ b/test/test-setup.sh @@ -135,7 +135,7 @@ assertJsonContainsKeyEquals() { # Encodes a key $1 in the output-style encoding (property key to output name). encodeKey() { local key=$1; shift - perl -pe 's=((?!_)[[:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" + perl -ple 's=((?!_)[[:space:][:punct:]])= sprintf("-%04X", ord($1)) =ge; s=^=_=;' <<<"$key" } # Extracts the value for key from JSON $2. From b09a25555ca32f76d679230f7c5f0f74ffbb84ac Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:18:47 +0200 Subject: [PATCH 157/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4a43212..36a22b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,5 @@ LABEL "maintainer"="Team MCBS Core " WORKDIR /action COPY *.java . - -ENTRYPOINT ["java", "Action.java"] +RUN ["javac", "Action.java"] +ENTRYPOINT ["java", "Action"] From 43141175e5cea77057ffddfa13964a447028f170 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:24:50 +0200 Subject: [PATCH 158/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 36a22b4..f1c1508 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,4 @@ LABEL "maintainer"="Team MCBS Core " WORKDIR /action COPY *.java . RUN ["javac", "Action.java"] -ENTRYPOINT ["java", "Action"] +ENTRYPOINT ["java", "--class-path", ".", "Action"] From 02b85f8e08421c36edaf6c846255d996c5d3a97d Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:28:55 +0200 Subject: [PATCH 159/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Action.java b/Action.java index 2e3306f..0f70239 100644 --- a/Action.java +++ b/Action.java @@ -24,7 +24,7 @@ public class Action { private Action() {} - static void main(String[] args) throws Exception { + public static void main(String[] args) throws Exception { try { Config config = Config.fromEnv(); ResultWriter resultWriter = ResultWriter.of(config.resultType()); From ef467455fe0aadd455a2462c6e9cf3886327f394 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:30:59 +0200 Subject: [PATCH 160/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Action.java b/Action.java index 0f70239..2e3306f 100644 --- a/Action.java +++ b/Action.java @@ -24,7 +24,7 @@ public class Action { private Action() {} - public static void main(String[] args) throws Exception { + static void main(String[] args) throws Exception { try { Config config = Config.fromEnv(); ResultWriter resultWriter = ResultWriter.of(config.resultType()); diff --git a/Dockerfile b/Dockerfile index f1c1508..1f3f8d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,4 @@ LABEL "maintainer"="Team MCBS Core " WORKDIR /action COPY *.java . RUN ["javac", "Action.java"] -ENTRYPOINT ["java", "--class-path", ".", "Action"] +ENTRYPOINT ["java", "--class-path", "/action", "Action"] From 43223048cdd0e9b93c578354668eb2983f451149 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:35:04 +0200 Subject: [PATCH 161/190] optimization: compile Java at Docker build time --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4a43212..1f3f8d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,5 @@ LABEL "maintainer"="Team MCBS Core " WORKDIR /action COPY *.java . - -ENTRYPOINT ["java", "Action.java"] +RUN ["javac", "Action.java"] +ENTRYPOINT ["java", "--class-path", "/action", "Action"] From 2377f27603582d12fd8d3aeaf8765915e2625c6b Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:43:07 +0200 Subject: [PATCH 162/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 +++ test/resources/test.properties | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c77af2e..3267f13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -245,6 +245,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' + evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1_4_2 $'---\n----\n-\n----\n--' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' - name: '[assert] query all properties as action outputs (additional "value" output not set)' env: @@ -297,6 +298,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_11 evalAndAssertUndefined "$RESULT_GETTER" dash_12 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1_4_2 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 - name: '[assert] query multiple properties as action outputs (additional "value" output)' @@ -400,6 +402,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_11 evalAndAssertUndefined "$RESULT_GETTER" dash_12 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1_4_2 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 # ===================================================================== diff --git a/test/resources/test.properties b/test/resources/test.properties index 3933a50..5e7d695 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -31,4 +31,5 @@ dash_10 = ---------- dash_11 = ----------- dash_12 = ------------ dash_3_4_1 = ---\n----\n- +dash_3_4_1_4_2 = ---\n----\n-\n----\n-- dash_3_4_8_1 = ---\n----\n--------\n- From 1846b11f28bc070b6f09cff923d9288e3b924f84 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:46:07 +0200 Subject: [PATCH 163/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Action.java b/Action.java index 2e3306f..19b6571 100644 --- a/Action.java +++ b/Action.java @@ -9,6 +9,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -371,7 +372,7 @@ private void writeMultiLine(String key, String value) throws IOException { @SuppressWarnings("java:S1643") // Sonar rule: "Strings should not be concatenated using '+' in a loop". // False positive: We would need that StringBuilder's toString for each iteration for the contains check anyway. private static String computeSeparator(String value) { - Set valueLines = Set.of(value.split("(?s)\n")); + Set valueLines = new HashSet<>(List.of(value.split("(?s)\n"))); String separatorPart = "----"; @SuppressWarnings("java:S1643") String separator = separatorPart; From 38154fbb5ca887c2df0f272275945001e062ab7a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:50:23 +0200 Subject: [PATCH 164/190] fix IllegalArgumentException when writing values with duplicate lines --- .github/workflows/test.yml | 3 +++ Action.java | 3 ++- test/resources/test.properties | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c77af2e..3267f13 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -245,6 +245,7 @@ jobs: evalAndAssertEquals "$RESULT_GETTER" dash_11 '-----------' evalAndAssertEquals "$RESULT_GETTER" dash_12 '------------' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1 $'---\n----\n-' + evalAndAssertEquals "$RESULT_GETTER" dash_3_4_1_4_2 $'---\n----\n-\n----\n--' evalAndAssertEquals "$RESULT_GETTER" dash_3_4_8_1 $'---\n----\n--------\n-' - name: '[assert] query all properties as action outputs (additional "value" output not set)' env: @@ -297,6 +298,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_11 evalAndAssertUndefined "$RESULT_GETTER" dash_12 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1_4_2 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 - name: '[assert] query multiple properties as action outputs (additional "value" output)' @@ -400,6 +402,7 @@ jobs: evalAndAssertUndefined "$RESULT_GETTER" dash_11 evalAndAssertUndefined "$RESULT_GETTER" dash_12 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1 + evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_1_4_2 evalAndAssertUndefined "$RESULT_GETTER" dash_3_4_8_1 # ===================================================================== diff --git a/Action.java b/Action.java index 2e3306f..19b6571 100644 --- a/Action.java +++ b/Action.java @@ -9,6 +9,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -371,7 +372,7 @@ private void writeMultiLine(String key, String value) throws IOException { @SuppressWarnings("java:S1643") // Sonar rule: "Strings should not be concatenated using '+' in a loop". // False positive: We would need that StringBuilder's toString for each iteration for the contains check anyway. private static String computeSeparator(String value) { - Set valueLines = Set.of(value.split("(?s)\n")); + Set valueLines = new HashSet<>(List.of(value.split("(?s)\n"))); String separatorPart = "----"; @SuppressWarnings("java:S1643") String separator = separatorPart; diff --git a/test/resources/test.properties b/test/resources/test.properties index 3933a50..5e7d695 100644 --- a/test/resources/test.properties +++ b/test/resources/test.properties @@ -31,4 +31,5 @@ dash_10 = ---------- dash_11 = ----------- dash_12 = ------------ dash_3_4_1 = ---\n----\n- +dash_3_4_1_4_2 = ---\n----\n-\n----\n-- dash_3_4_8_1 = ---\n----\n--------\n- From 46d8b55edae85cb337d71cee23397507cc197148 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 16:57:44 +0200 Subject: [PATCH 165/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 18 ++++++++++++++++++ Action.java | 1 + 2 files changed, 19 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3267f13..520f8ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,6 +86,24 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType: rt1' + # ===================================================================== + - name: '[act] (should fail): resultType "output" with argument' + id: input-error_resultType-output-with-arg + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output:x' + - name: '[assert] (should fail): resultType "output-named" with argument' + env: + STATUS: ${{steps.input-error_resultType-output-with-arg.outcome}} + ERROR: ${{steps.input-error_resultType-output-with-arg.outputs.error}} + run: |- + . "$SHELL_SETUP" + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType output:x (non-empty argument)' + # ===================================================================== - name: '[act] (should fail): resultType "output-named" without names' id: input-error_resultType-output-without-names diff --git a/Action.java b/Action.java index 19b6571..dca7ec6 100644 --- a/Action.java +++ b/Action.java @@ -176,6 +176,7 @@ enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override public void write(Map> props, Config config) throws IOException { + config.requireNoArg(); String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { for (Map.Entry> entry : props.entrySet()) { From c64638ab8e381f87d1bd8f2796951b9a0e255969 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 17:03:36 +0200 Subject: [PATCH 166/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test-setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-setup.sh b/test/test-setup.sh index 6a2f9bd..7c99b59 100644 --- a/test/test-setup.sh +++ b/test/test-setup.sh @@ -90,7 +90,7 @@ evalAndAssertUndefined() { if eval "value=\$(${getterFunction}${quotedArgs})"; then printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 return 1 - elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then + elif [ "${st=$?}" -eq "$NOT_FOUND_STATUS" ]; then printf 'ok: %s is undefined\n' "$*" >&2 else # no error message, assuming failed $getterFunction has already written one From 086d62254630a757fb09f6fb0ecb288e0ff7a74e Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 17:05:14 +0200 Subject: [PATCH 167/190] resultType "output": forbid argument --- .github/workflows/test.yml | 18 ++++++++++++++++++ Action.java | 1 + 2 files changed, 19 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3267f13..520f8ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,6 +86,24 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid resultType: rt1' + # ===================================================================== + - name: '[act] (should fail): resultType "output" with argument' + id: input-error_resultType-output-with-arg + continue-on-error: true + uses: ./ + with: + file: test/resources/test.properties + keys: '${{env.SELECTED_KEYS}}' + resultType: 'output:x' + - name: '[assert] (should fail): resultType "output-named" with argument' + env: + STATUS: ${{steps.input-error_resultType-output-with-arg.outcome}} + ERROR: ${{steps.input-error_resultType-output-with-arg.outputs.error}} + run: |- + . "$SHELL_SETUP" + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'invalid resultType output:x (non-empty argument)' + # ===================================================================== - name: '[act] (should fail): resultType "output-named" without names' id: input-error_resultType-output-without-names diff --git a/Action.java b/Action.java index 19b6571..dca7ec6 100644 --- a/Action.java +++ b/Action.java @@ -176,6 +176,7 @@ enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override public void write(Map> props, Config config) throws IOException { + config.requireNoArg(); String lastValue = null; try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { for (Map.Entry> entry : props.entrySet()) { From 4440b3b9d653911e55b3444bb8745721e25f674d Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 17:06:49 +0200 Subject: [PATCH 168/190] fix test: evalAndAssertUndefined unexpected $getterFunction status (did not happen, but was noticed by GitHub Copilot review) --- test/test-setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-setup.sh b/test/test-setup.sh index 6a2f9bd..7c99b59 100644 --- a/test/test-setup.sh +++ b/test/test-setup.sh @@ -90,7 +90,7 @@ evalAndAssertUndefined() { if eval "value=\$(${getterFunction}${quotedArgs})"; then printf 'assertion error: %s == "%s", expected undefined\n' "$*" "$value" >&2 return 1 - elif [ ${st-$?} -eq "$NOT_FOUND_STATUS" ]; then + elif [ "${st=$?}" -eq "$NOT_FOUND_STATUS" ]; then printf 'ok: %s is undefined\n' "$*" >&2 else # no error message, assuming failed $getterFunction has already written one From 37b99a14bc1947b4ac4bbcee835c71a120f08fb5 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 17:12:54 +0200 Subject: [PATCH 169/190] fix docs --- README.md | 2 +- action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 69281a8..b458e39 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Query a single property as action output: keys: org.gradle.jvmargs ``` ⇒ \ - `${{steps.readProp.outputs.org-002Egradle-002Ejvmargs}}` == `-ea -showversion`, \ + `${{steps.readProp.outputs._org-002Egradle-002Ejvmargs}}` == `-ea -showversion`, \ `${{steps.readProp.outputs.value}}` == `-ea -showversion`. Query multiple (alternative) properties as a single action output. In other words, query a property with a fallback to another property. diff --git a/action.yml b/action.yml index d771ef7..778c6c1 100644 --- a/action.yml +++ b/action.yml @@ -26,7 +26,7 @@ inputs: - "output": For each property key , set an output named "_", but with special characters encoded. (The leading underscore serves to avoid conflicts with future output names used by this action.) Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name - "a-002Eb-002Dc_d". + "_a-002Eb-002Dc_d". Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. If `keys` is given, but none is found, "value" is set to empty. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) The output for a missing property is set to empty. If you need to distinguish between empty an undefined properties, resultType "json" or "json-file" is recommended. - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. If none is found, the output is set to empty. From 0c23a83dc04d7f2847bc2ef8c5e2f5e5704bcec0 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 17:38:06 +0200 Subject: [PATCH 170/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 6 ++++-- README.md | 2 +- action.yml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 520f8ad..613d113 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,10 +48,12 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: "check that this job's `needs` is set to exactly all other jobs" + env: + THIS_JOB_NAME: test run: |- diff --minimal --report-identical-files \ - <(yq -r '.jobs | keys() | .[]' .github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match test) \ - <(yq -r '.jobs.test.needs[]' < .github/workflows/test.yml | sort) + <(yq -r '.jobs | keys() | .[]' <.github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match -- "$THIS_JOB_NAME") \ + <(yq -r '.jobs.test.needs[]' <.github/workflows/test.yml | sort) - name: check overall status env: diff --git a/README.md b/README.md index 69281a8..b458e39 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Query a single property as action output: keys: org.gradle.jvmargs ``` ⇒ \ - `${{steps.readProp.outputs.org-002Egradle-002Ejvmargs}}` == `-ea -showversion`, \ + `${{steps.readProp.outputs._org-002Egradle-002Ejvmargs}}` == `-ea -showversion`, \ `${{steps.readProp.outputs.value}}` == `-ea -showversion`. Query multiple (alternative) properties as a single action output. In other words, query a property with a fallback to another property. diff --git a/action.yml b/action.yml index d771ef7..778c6c1 100644 --- a/action.yml +++ b/action.yml @@ -26,7 +26,7 @@ inputs: - "output": For each property key , set an output named "_", but with special characters encoded. (The leading underscore serves to avoid conflicts with future output names used by this action.) Encoding replaces all punctuation or whitespace characters except the underscore ("_") with "-" followed by four hex digits of its unicode code point. For example, for a property key "a.b-c_d", resultType "output" sets an output with name - "a-002Eb-002Dc_d". + "_a-002Eb-002Dc_d". Additionally, set output "value" to the last found value, unless `keys` is empty. I.e. last given key wins. If `keys` is given, but none is found, "value" is set to empty. (Without keys, this output is not set, because it would be arbitrary due to the "random" iteration order of the used Java class java.util.Properties.) - "output-named:": is a `resultNameSeparator`-separated list of output names of the same length as (the `keySeparator`-separated list) `keys`. The value for `keys[i]` is set as output `[i]`. In order not to hide any future builtin outputs of this action, it is recommended to prefix each name with an underscore ("_"). In this mode, `keys` is required. Unlike "output", names are taken as-is. (This is because no official output naming rules seem to exist yet; so maybe a future user will know better how to choose valid characters than we would implement now.) The output for a missing property is set to empty. If you need to distinguish between empty an undefined properties, resultType "json" or "json-file" is recommended. - "output-named:": Special case: A single name is supported even with multiple `keys`. All found values for the keys are set as the same output; the last found key wins. In this mode, `keys` is required. If none is found, the output is set to empty. From 9921013a9cafaadbcf0132d786edd4940fa3f8a7 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 17:42:03 +0200 Subject: [PATCH 171/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 613d113..89c6b80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,11 +49,12 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} - name: "check that this job's `needs` is set to exactly all other jobs" env: + THIS_WORKFLOW_FILE: '.github/workflows/test.yml' THIS_JOB_NAME: test run: |- diff --minimal --report-identical-files \ - <(yq -r '.jobs | keys() | .[]' <.github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match -- "$THIS_JOB_NAME") \ - <(yq -r '.jobs.test.needs[]' <.github/workflows/test.yml | sort) + <(yq -r '.jobs | keys() | .[]' <"$THIS_WORKFLOW_FILE" | sort | grep --line-regexp --fixed-strings --invert-match -- "$THIS_JOB_NAME") \ + <(yq -r '.jobs.test.needs[]' <"$THIS_WORKFLOW_FILE" | sort) - name: check overall status env: From f692bcb994c3175677b69fd6d68f76513d5b826f Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 17:51:11 +0200 Subject: [PATCH 172/190] test: minor cleanup - consistency: both yq invocations with redirection - move job and workflow file names into `env` to make the assumptions more visible --- .github/workflows/test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 520f8ad..89c6b80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,10 +48,13 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: "check that this job's `needs` is set to exactly all other jobs" + env: + THIS_WORKFLOW_FILE: '.github/workflows/test.yml' + THIS_JOB_NAME: test run: |- diff --minimal --report-identical-files \ - <(yq -r '.jobs | keys() | .[]' .github/workflows/test.yml | sort | grep --line-regexp --fixed-strings --invert-match test) \ - <(yq -r '.jobs.test.needs[]' < .github/workflows/test.yml | sort) + <(yq -r '.jobs | keys() | .[]' <"$THIS_WORKFLOW_FILE" | sort | grep --line-regexp --fixed-strings --invert-match -- "$THIS_JOB_NAME") \ + <(yq -r '.jobs.test.needs[]' <"$THIS_WORKFLOW_FILE" | sort) - name: check overall status env: From b6f320c944532ee27b5b3a2fd638a1785b59d5d8 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 18:04:37 +0200 Subject: [PATCH 173/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1f3f8d6..c17bba3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:25-latest +FROM azul/zulu-openjdk:25.0.2-25.32@sha256:634efad18fb8fbf1dcc4c2d5a43c663435a77f078a912345f23f332e7abd27be LABEL "com.github.actions.name"="read Java properties" LABEL "com.github.actions.description"="read Java properties file and return one or more as property values as plain text or JSON" From 719fdc9a61a3e101e59c49bbdd085a4f0e728147 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 18:06:32 +0200 Subject: [PATCH 174/190] pin Docker base image version --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1f3f8d6..c17bba3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM azul/zulu-openjdk:25-latest +FROM azul/zulu-openjdk:25.0.2-25.32@sha256:634efad18fb8fbf1dcc4c2d5a43c663435a77f078a912345f23f332e7abd27be LABEL "com.github.actions.name"="read Java properties" LABEL "com.github.actions.description"="read Java properties file and return one or more as property values as plain text or JSON" From 6d18c2ab143088ce730c350fcc0b75d75aac268f Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 19:54:48 +0200 Subject: [PATCH 175/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 72 +++++++++++++++++++++++++++++++++ Action.java | 83 +++++++++++++++++++++++++++++++++----- action.yml | 9 +++++ test/test-setup.sh | 1 + 4 files changed, 156 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89c6b80..0128a94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: test: needs: - test_invalid-usage + - test_missing-file - test_default - test_output - test_output-named @@ -177,6 +178,77 @@ jobs: assertEquals "$STATUS" failure assertEquals "$ERROR" 'invalid use of resultType env-named (missing keys)' + ######################################################################### + test_missing-file: + runs-on: ubuntu-latest + steps: + - *checkoutStep + + # ===================================================================== + - name: '[act] missing file with default onMissingFile "notice-message"' + id: file-error_onMissingFile-notice-message + uses: ./ + with: + file: test/resources/DOES_NOT_EXIST.properties + onMissingFile: notice-message + keys: ${{env.SELECTED_KEY}} + resultType: json + - name: '[assert] missing file with onMissingFile "notice-message"' + env: + RESULT_JSON: ${{steps.file-error_onMissingFile-notice-message.outputs.json}} + run: |- + . "$SHELL_SETUP" + assertEquals "$(getJsonValue "$RESULT_JSON" "$SELECTED_KEY")" '' + + # ===================================================================== + - name: '[act] missing file with default onMissingFile "warning-message"' + id: file-error_onMissingFile-warning-message + uses: ./ + with: + file: test/resources/DOES_NOT_EXIST.properties + onMissingFile: warning-message + keys: ${{env.SELECTED_KEY}} + resultType: json + - name: '[assert] missing file with onMissingFile "warning-message"' + env: + RESULT_JSON: ${{steps.file-error_onMissingFile-warning-message.outputs.json}} + run: |- + . "$SHELL_SETUP" + assertEquals "$(getJsonValue "$RESULT_JSON" "$SELECTED_KEY")" '' + + # ===================================================================== + - name: '[act] (should fail): missing file with default onMissingFile "error"' + id: file-error_onMissingFile-error + continue-on-error: true + uses: ./ + with: + file: test/resources/DOES_NOT_EXIST.properties + onMissingFile: error + keys: ${{env.SELECTED_KEY}} + resultType: json + - name: '[assert] (should fail): missing file with onMissingFile "error' + env: + STATUS: ${{steps.file-error_onMissingFile-error.outcome}} + RESULT_JSON: ${{steps.file-error_onMissingFile-error.outputs.json}} + run: &validateOnMissingFileErrorImpl |- + . "$SHELL_SETUP" + assertEquals "$STATUS" failure + assertEquals "$RESULT_JSON" '' + + # ===================================================================== + - name: '[act] (should fail): missing file with default onMissingFile' + id: file-error_onMissingFile-default + continue-on-error: true + uses: ./ + with: + file: test/resources/DOES_NOT_EXIST.properties + keys: ${{env.SELECTED_KEY}} + resultType: json + - name: '[assert] (should fail): missing file with default onMissingFile' + env: + RESULT_JSON: ${{steps.file-error_onMissingFile-default.outputs.json}} + run: *validateOnMissingFileErrorImpl + ######################################################################### test_default: runs-on: ubuntu-latest diff --git a/Action.java b/Action.java index dca7ec6..9545808 100644 --- a/Action.java +++ b/Action.java @@ -30,7 +30,7 @@ static void main(String[] args) throws Exception { Config config = Config.fromEnv(); ResultWriter resultWriter = ResultWriter.of(config.resultType()); for (String file : args) { - Properties properties = Util.readProperties(file); + Properties properties = Util.readProperties(file, config.missingFileHandler()); // If selectedKeys is set, this Map will have exactly those keys and values may be empty (where selected key is // not found in properties). // Otherwise, this Map will have the same keys as the properties file, and "not found" is not possible. @@ -39,6 +39,8 @@ static void main(String[] args) throws Exception { .orElseGet(() -> Util.stringEntries(properties)); resultWriter.write(selectedProperties, config); } + } catch (ExitSilentlyException e) { + System.exit(e.status); } catch (IoRuntimeException e) { throw e.getCause(); } catch (OutputException e) { @@ -62,6 +64,7 @@ private Ids() {} */ enum ConfigVariable { FILE("file"), // + ON_MISSING_FILE("onMissingFile"), // KEYS("keys"), // RESULT_TYPE("resultType"), // KEY_SEPARATOR("keySeparator"), // @@ -80,6 +83,18 @@ public String toString() { } } + enum MissingFileHandlerName { + NOTICE_MESSAGE("notice-message"), // + WARNING_MESSAGE("warning-message"), // + ERROR("error"); + + public final String externalName; + + private MissingFileHandlerName(String externalName) { + this.externalName = externalName; + } + } + enum ResultWriterName { OUTPUT("output"), // OUTPUT_NAMED("output-named"), // @@ -130,8 +145,18 @@ public static OutputException forIllegalArgument(String message) { } -record Config(Optional> selectedKeys, String keySeparator, String resultTypeWithArg, String resultType, - String resultTypeArg, String resultNameSeparator, String outputPrefix) { +class ExitSilentlyException extends RuntimeException { + public final int status; + + public ExitSilentlyException(int status) { + super(); + this.status = status; + } +} + + +record Config(MissingFileHandler missingFileHandler, Optional> selectedKeys, String keySeparator, + String resultTypeWithArg, String resultType, String resultTypeArg, String resultNameSeparator, String outputPrefix) { public static Config fromEnv() { // In order to keep the defaults DRY (in action.yml), the environment variables are all mandatory. String keySeparator = Util.getRequiredEnv(Ids.ConfigVariable.KEY_SEPARATOR); @@ -150,8 +175,8 @@ public static Config fromEnv() { String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); String outputPrefix = Util.getRequiredEnv(Ids.ConfigVariable.OUTPUT_PREFIX); - return new Config(keys.map(List::of), keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, - outputPrefix); + return new Config(MissingFileHandler.valueOf(Util.getRequiredEnv(Ids.ConfigVariable.ON_MISSING_FILE)), keys.map(List::of), + keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); } public String requiredResultTypeArg() { @@ -172,6 +197,43 @@ public void requireNoArg() { } +enum MissingFileHandler { + NOTICE_MESSAGE(Ids.MissingFileHandlerName.NOTICE_MESSAGE, "notice"), // + WARNING_MESSAGE(Ids.MissingFileHandlerName.WARNING_MESSAGE, "warning"), // + ERROR(Ids.MissingFileHandlerName.ERROR, "error") { + @Override + public void handleMissingFile(Path file, String message) { + super.handleMissingFile(file, message); + throw new ExitSilentlyException(2); + } + }; + + private final String externalName; + private final String output; + + private MissingFileHandler(Ids.MissingFileHandlerName externalName, String output) { + this.externalName = externalName.externalName; + this.output = output; + } + + public static MissingFileHandler of(String externalName) { + for (MissingFileHandler o : MissingFileHandler.values()) { + if (o.externalName.equals(externalName)) { + return o; + } + } + throw OutputException.forIllegalArgument("invalid " + Ids.ConfigVariable.ON_MISSING_FILE + ": " + externalName); + } + + @SuppressWarnings("java:S3457") // Sonar rule suggests %n instead of \n, but that would not strictly be covered by the docs + // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-a-notice-message], so the \r + // might be considered part of the message. + public void handleMissingFile(Path file, String message) { + System.out.format("::%s::error opening file %s: %s\n", output, file, message); + } +} + + enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override @@ -375,7 +437,6 @@ private void writeMultiLine(String key, String value) throws IOException { private static String computeSeparator(String value) { Set valueLines = new HashSet<>(List.of(value.split("(?s)\n"))); String separatorPart = "----"; - @SuppressWarnings("java:S1643") String separator = separatorPart; while (valueLines.contains(separator)) { separator = separator + separatorPart; @@ -413,12 +474,16 @@ public static Writer openFile(String name, OpenOption... options) throws IOExcep return new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(Paths.get(name), options), StandardCharsets.UTF_8)); } - public static Properties readProperties(String file) throws IOException { + public static Properties readProperties(String file, MissingFileHandler missingFileHandler) { Properties allProps = new Properties(); - try (InputStream in = Files.newInputStream(Paths.get(file))) { + Path path = Paths.get(file); + try (InputStream in = Files.newInputStream(path)) { allProps.load(in); + return allProps; + } catch (IOException e) { + missingFileHandler.handleMissingFile(path, e.getMessage()); + return new Properties(); } - return allProps; } /** diff --git a/action.yml b/action.yml index 778c6c1..89f1baa 100644 --- a/action.yml +++ b/action.yml @@ -7,6 +7,14 @@ inputs: file: description: the properties file name required: true + onMissingFile: + required: false + default: error + description: |- + What to do if `file` does not exist or is not readable. One of: + - "notice-message": write a notice message and continue with an empty properties map, + - "warning-message": write a warning message and continue with an empty properties map, + - "error": write an error message and exit with non-zero status. keys: description: |- Selects the property keys (names). Format: `keySeparator`-separated list. @@ -54,6 +62,7 @@ runs: args: - ${{inputs.file}} env: + ON_MISSING_FILE: ${{inputs.onMissingFile}} KEYS: ${{inputs.keys}} KEY_SEPARATOR: ${{inputs.keySeparator}} RESULT_NAME_SEPARATOR: ${{inputs.resultNameSeparator}} diff --git a/test/test-setup.sh b/test/test-setup.sh index 7c99b59..efc727d 100644 --- a/test/test-setup.sh +++ b/test/test-setup.sh @@ -99,6 +99,7 @@ evalAndAssertUndefined() { } # Extracts the value for key $2 from JSON $1. +# Successfully prints nothing if the value is null or empty, but fails if the key is not present. getJsonValue() { local json=$1; shift local key=$1; shift From ab75ce7fdef84818b0ccbf3393a8dba07bf9a8d4 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 19:58:49 +0200 Subject: [PATCH 176/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Action.java b/Action.java index 9545808..d01ded8 100644 --- a/Action.java +++ b/Action.java @@ -28,7 +28,7 @@ private Action() {} static void main(String[] args) throws Exception { try { Config config = Config.fromEnv(); - ResultWriter resultWriter = ResultWriter.of(config.resultType()); + ResultWriter resultWriter = ResultWriter.ofExternalName(config.resultType()); for (String file : args) { Properties properties = Util.readProperties(file, config.missingFileHandler()); // If selectedKeys is set, this Map will have exactly those keys and values may be empty (where selected key is @@ -175,8 +175,14 @@ public static Config fromEnv() { String resultType = matcher.group(1); String resultTypeArg = Optional.ofNullable(matcher.group(2)).orElse(""); String outputPrefix = Util.getRequiredEnv(Ids.ConfigVariable.OUTPUT_PREFIX); - return new Config(MissingFileHandler.valueOf(Util.getRequiredEnv(Ids.ConfigVariable.ON_MISSING_FILE)), keys.map(List::of), - keySeparator, resultTypeWithArg, resultType, resultTypeArg, resultNameSeparator, outputPrefix); + return new Config( // + MissingFileHandler.ofExternalName(Util.getRequiredEnv(Ids.ConfigVariable.ON_MISSING_FILE)), // + keys.map(List::of), keySeparator, // + resultTypeWithArg, // + resultType, // + resultTypeArg, // + resultNameSeparator, // + outputPrefix); } public String requiredResultTypeArg() { @@ -216,7 +222,7 @@ private MissingFileHandler(Ids.MissingFileHandlerName externalName, String outpu this.output = output; } - public static MissingFileHandler of(String externalName) { + public static MissingFileHandler ofExternalName(String externalName) { for (MissingFileHandler o : MissingFileHandler.values()) { if (o.externalName.equals(externalName)) { return o; @@ -320,7 +326,7 @@ private ResultWriter(Ids.ResultWriterName externalName) { this.externalName = externalName.externalName; } - public static ResultWriter of(String externalName) { + public static ResultWriter ofExternalName(String externalName) { for (ResultWriter rw : ResultWriter.values()) { if (rw.externalName.equals(externalName)) { return rw; From 5af3cacd88ae0953952540386a75bccd25a73fc7 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 20:09:41 +0200 Subject: [PATCH 177/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 3 ++- Action.java | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0128a94..98121fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -226,7 +226,7 @@ jobs: onMissingFile: error keys: ${{env.SELECTED_KEY}} resultType: json - - name: '[assert] (should fail): missing file with onMissingFile "error' + - name: '[assert] (should fail): missing file with onMissingFile "error"' env: STATUS: ${{steps.file-error_onMissingFile-error.outcome}} RESULT_JSON: ${{steps.file-error_onMissingFile-error.outputs.json}} @@ -246,6 +246,7 @@ jobs: resultType: json - name: '[assert] (should fail): missing file with default onMissingFile' env: + STATUS: ${{steps.file-error_onMissingFile-default.outcome}} RESULT_JSON: ${{steps.file-error_onMissingFile-default.outputs.json}} run: *validateOnMissingFileErrorImpl diff --git a/Action.java b/Action.java index d01ded8..22f8cb0 100644 --- a/Action.java +++ b/Action.java @@ -1,4 +1,5 @@ import java.io.BufferedWriter; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; @@ -487,7 +488,10 @@ public static Properties readProperties(String file, MissingFileHandler missingF allProps.load(in); return allProps; } catch (IOException e) { - missingFileHandler.handleMissingFile(path, e.getMessage()); + // Nice message (instead of catching FileNotFoundExeption which is also thrown on other problems and just contains the + // filename, not "does not exist" or similar): + String message = Files.exists(path) ? ("error opening file: " + e.getMessage()) : ("file " + path + " does not exist"); + missingFileHandler.handleMissingFile(path, message); return new Properties(); } } From 9f5b55ab927098ce1b31a32ba3610d3df98ae910 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 20:14:50 +0200 Subject: [PATCH 178/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Action.java b/Action.java index 22f8cb0..d7a8876 100644 --- a/Action.java +++ b/Action.java @@ -1,5 +1,4 @@ import java.io.BufferedWriter; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; @@ -209,8 +208,8 @@ enum MissingFileHandler { WARNING_MESSAGE(Ids.MissingFileHandlerName.WARNING_MESSAGE, "warning"), // ERROR(Ids.MissingFileHandlerName.ERROR, "error") { @Override - public void handleMissingFile(Path file, String message) { - super.handleMissingFile(file, message); + public void handleMissingFile(String message) { + super.handleMissingFile(message); throw new ExitSilentlyException(2); } }; @@ -235,8 +234,8 @@ public static MissingFileHandler ofExternalName(String externalName) { @SuppressWarnings("java:S3457") // Sonar rule suggests %n instead of \n, but that would not strictly be covered by the docs // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-a-notice-message], so the \r // might be considered part of the message. - public void handleMissingFile(Path file, String message) { - System.out.format("::%s::error opening file %s: %s\n", output, file, message); + public void handleMissingFile(String message) { + System.out.format("::%s::%s\n", output, message); } } From 7c3209b5e0184f935f19e0fc46b6205b993f10a3 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Wed, 20 May 2026 20:16:36 +0200 Subject: [PATCH 179/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Action.java b/Action.java index d7a8876..e628d00 100644 --- a/Action.java +++ b/Action.java @@ -490,7 +490,7 @@ public static Properties readProperties(String file, MissingFileHandler missingF // Nice message (instead of catching FileNotFoundExeption which is also thrown on other problems and just contains the // filename, not "does not exist" or similar): String message = Files.exists(path) ? ("error opening file: " + e.getMessage()) : ("file " + path + " does not exist"); - missingFileHandler.handleMissingFile(path, message); + missingFileHandler.handleMissingFile(message); return new Properties(); } } From 65cfa7707bc2db192fc737f63825523e746347de Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 10:32:19 +0200 Subject: [PATCH 180/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 94 +++++++++++++++++++++++++++----------- Action.java | 41 +++++++++++++---- 2 files changed, 101 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98121fa..74e4507 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,7 @@ jobs: needs: - test_invalid-usage - test_missing-file + - test_invalid-file - test_default - test_output - test_output-named @@ -74,14 +75,14 @@ jobs: - *checkoutStep # ===================================================================== - - name: '[act] (should fail): invalid input: resultType' + - name: '[act] (should fail) invalid input: resultType' id: input-error_resultType continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: rt1 - - name: '[assert] (should fail): invalid input: resultType' + - name: '[assert] (should fail) invalid input: resultType' env: STATUS: ${{steps.input-error_resultType.outcome}} ERROR: ${{steps.input-error_resultType.outputs.error}} @@ -91,7 +92,7 @@ jobs: assertEquals "$ERROR" 'invalid resultType: rt1' # ===================================================================== - - name: '[act] (should fail): resultType "output" with argument' + - name: '[act] (should fail) resultType "output" with argument' id: input-error_resultType-output-with-arg continue-on-error: true uses: ./ @@ -99,7 +100,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output:x' - - name: '[assert] (should fail): resultType "output-named" with argument' + - name: '[assert] (should fail) resultType "output-named" with argument' env: STATUS: ${{steps.input-error_resultType-output-with-arg.outcome}} ERROR: ${{steps.input-error_resultType-output-with-arg.outputs.error}} @@ -109,7 +110,7 @@ jobs: assertEquals "$ERROR" 'invalid resultType output:x (non-empty argument)' # ===================================================================== - - name: '[act] (should fail): resultType "output-named" without names' + - name: '[act] (should fail) resultType "output-named" without names' id: input-error_resultType-output-without-names continue-on-error: true uses: ./ @@ -117,7 +118,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'output-named:' - - name: '[assert] (should fail): resultType "output-named" without names' + - name: '[assert] (should fail) resultType "output-named" without names' env: STATUS: ${{steps.input-error_resultType-output-without-names.outcome}} ERROR: ${{steps.input-error_resultType-output-without-names.outputs.error}} @@ -127,7 +128,7 @@ jobs: assertEquals "$ERROR" 'invalid resultType output-named: (missing argument)' # ===================================================================== - - name: '[act] (should fail): resultType "env-named" without names' + - name: '[act] (should fail) resultType "env-named" without names' id: input-error_resultType-env-without-names continue-on-error: true uses: ./ @@ -135,7 +136,7 @@ jobs: file: test/resources/test.properties keys: '${{env.SELECTED_KEYS}}' resultType: 'env-named:' - - name: '[assert] (should fail): resultType "env-named" without names' + - name: '[assert] (should fail) resultType "env-named" without names' env: STATUS: ${{steps.input-error_resultType-env-without-names.outcome}} ERROR: ${{steps.input-error_resultType-env-without-names.outputs.error}} @@ -145,14 +146,14 @@ jobs: assertEquals "$ERROR" 'invalid resultType env-named: (missing argument)' # ===================================================================== - - name: '[act] (should fail): resultType "output-named" without keys' + - name: '[act] (should fail) resultType "output-named" without keys' id: input-error_resultType-output-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'output-named:a b' - - name: '[assert] (should fail): resultType "output-named" without keys' + - name: '[assert] (should fail) resultType "output-named" without keys' env: STATUS: ${{steps.input-error_resultType-output-without-keys.outcome}} ERROR: ${{steps.input-error_resultType-output-without-keys.outputs.error}} @@ -162,14 +163,14 @@ jobs: assertEquals "$ERROR" 'invalid use of resultType output-named (missing keys)' # ===================================================================== - - name: '[act] (should fail): resultType "env-named" without keys' + - name: '[act] (should fail) resultType "env-named" without keys' id: input-error_resultType-env-without-keys continue-on-error: true uses: ./ with: file: test/resources/test.properties resultType: 'env-named:a b' - - name: '[assert] (should fail): resultType "env-named" without keys' + - name: '[assert] (should fail) resultType "env-named" without keys' env: STATUS: ${{steps.input-error_resultType-env-without-keys.outcome}} ERROR: ${{steps.input-error_resultType-env-without-keys.outputs.error}} @@ -186,7 +187,7 @@ jobs: # ===================================================================== - name: '[act] missing file with default onMissingFile "notice-message"' - id: file-error_onMissingFile-notice-message + id: missing-file-error_onMissingFile-notice-message uses: ./ with: file: test/resources/DOES_NOT_EXIST.properties @@ -195,14 +196,14 @@ jobs: resultType: json - name: '[assert] missing file with onMissingFile "notice-message"' env: - RESULT_JSON: ${{steps.file-error_onMissingFile-notice-message.outputs.json}} + RESULT_JSON: ${{steps.missing-file-error_onMissingFile-notice-message.outputs.json}} run: |- . "$SHELL_SETUP" assertEquals "$(getJsonValue "$RESULT_JSON" "$SELECTED_KEY")" '' # ===================================================================== - name: '[act] missing file with default onMissingFile "warning-message"' - id: file-error_onMissingFile-warning-message + id: missing-file-error_onMissingFile-warning-message uses: ./ with: file: test/resources/DOES_NOT_EXIST.properties @@ -211,14 +212,14 @@ jobs: resultType: json - name: '[assert] missing file with onMissingFile "warning-message"' env: - RESULT_JSON: ${{steps.file-error_onMissingFile-warning-message.outputs.json}} + RESULT_JSON: ${{steps.missing-file-error_onMissingFile-warning-message.outputs.json}} run: |- . "$SHELL_SETUP" assertEquals "$(getJsonValue "$RESULT_JSON" "$SELECTED_KEY")" '' # ===================================================================== - - name: '[act] (should fail): missing file with default onMissingFile "error"' - id: file-error_onMissingFile-error + - name: '[act] (should fail) missing file with default onMissingFile "error"' + id: missing-file-error_onMissingFile-error continue-on-error: true uses: ./ with: @@ -226,30 +227,71 @@ jobs: onMissingFile: error keys: ${{env.SELECTED_KEY}} resultType: json - - name: '[assert] (should fail): missing file with onMissingFile "error"' + - name: '[assert] (should fail) missing file with onMissingFile "error"' env: - STATUS: ${{steps.file-error_onMissingFile-error.outcome}} - RESULT_JSON: ${{steps.file-error_onMissingFile-error.outputs.json}} + STATUS: ${{steps.missing-file-error_onMissingFile-error.outcome}} + RESULT_JSON: ${{steps.missing-file-error_onMissingFile-error.outputs.json}} run: &validateOnMissingFileErrorImpl |- . "$SHELL_SETUP" assertEquals "$STATUS" failure assertEquals "$RESULT_JSON" '' # ===================================================================== - - name: '[act] (should fail): missing file with default onMissingFile' - id: file-error_onMissingFile-default + - name: '[act] (should fail) missing file with default onMissingFile' + id: missing-file-error_onMissingFile-default continue-on-error: true uses: ./ with: file: test/resources/DOES_NOT_EXIST.properties keys: ${{env.SELECTED_KEY}} resultType: json - - name: '[assert] (should fail): missing file with default onMissingFile' + - name: '[assert] (should fail) missing file with default onMissingFile' env: - STATUS: ${{steps.file-error_onMissingFile-default.outcome}} - RESULT_JSON: ${{steps.file-error_onMissingFile-default.outputs.json}} + STATUS: ${{steps.missing-file-error_onMissingFile-default.outcome}} + RESULT_JSON: ${{steps.missing-file-error_onMissingFile-default.outputs.json}} run: *validateOnMissingFileErrorImpl + ######################################################################### + test_invalid-file: + runs-on: ubuntu-latest + steps: + - *checkoutStep + # ===================================================================== + - name: '[act] (should fail) file is a directory' + id: invalid-file-error_directory + uses: ./ + with: + file: test + onMissingFile: 'notice-message' # should fail with any onMissingFile, even the most lenient one + keys: ${{env.SELECTED_KEY}} + resultType: json + - name: '[assert] (should fail) file is a directory' + env: + STATUS: ${{steps.invalid-file-error_directory.outcome}} + ERROR: ${{steps.invalid-file-error_directory.outputs.error}} + run: |- + . "$SHELL_SETUP" + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'TODO is a directory or something' + + # ===================================================================== + - name: '[act] (should fail) invalid file format' + id: invalid-file-error_format + uses: ./ + with: + file: action.yml + onMissingFile: 'notice-message' # should fail with any onMissingFile, even the most lenient one + keys: ${{env.SELECTED_KEY}} + resultType: json + - name: '[assert] (should fail) invalid file format' + env: + STATUS: ${{steps.invalid-file-error_format.outcome}} + ERROR: ${{steps.invalid-file-error_format.outputs.error}} + run: |- + . "$SHELL_SETUP" + assertEquals "$STATUS" failure + assertEquals "$ERROR" 'TODO parse error or something' + ######################################################################### test_default: runs-on: ubuntu-latest diff --git a/Action.java b/Action.java index e628d00..0a516d8 100644 --- a/Action.java +++ b/Action.java @@ -132,7 +132,7 @@ public synchronized IOException getCause() { /** - * An exception for which an "error" output should be set. + * An exception for which an {@link Ids.OutputName#ERROR} output should be set. */ class OutputException extends RuntimeException { public OutputException(String message, Throwable cause) { @@ -203,10 +203,29 @@ public void requireNoArg() { } +enum GithubMessageType { + DEBUG("debug"), // + NOTICE("notice"), // + WARNING("warning"), // + ERROR("error"); + + public final String externalName; + + private GithubMessageType(String externalName) { + this.externalName = externalName; + } + + public void format(String message, Object... args) { + String format = String.format("::%s::" + message + "\n", externalName); + System.out.format(format, args); + } +} + + enum MissingFileHandler { - NOTICE_MESSAGE(Ids.MissingFileHandlerName.NOTICE_MESSAGE, "notice"), // - WARNING_MESSAGE(Ids.MissingFileHandlerName.WARNING_MESSAGE, "warning"), // - ERROR(Ids.MissingFileHandlerName.ERROR, "error") { + NOTICE_MESSAGE(Ids.MissingFileHandlerName.NOTICE_MESSAGE, GithubMessageType.NOTICE), // + WARNING_MESSAGE(Ids.MissingFileHandlerName.WARNING_MESSAGE, GithubMessageType.WARNING), // + ERROR(Ids.MissingFileHandlerName.ERROR, GithubMessageType.ERROR) { @Override public void handleMissingFile(String message) { super.handleMissingFile(message); @@ -215,11 +234,11 @@ public void handleMissingFile(String message) { }; private final String externalName; - private final String output; + private final GithubMessageType githubMessageType; - private MissingFileHandler(Ids.MissingFileHandlerName externalName, String output) { + private MissingFileHandler(Ids.MissingFileHandlerName externalName, GithubMessageType githubMessageType) { this.externalName = externalName.externalName; - this.output = output; + this.githubMessageType = githubMessageType; } public static MissingFileHandler ofExternalName(String externalName) { @@ -235,7 +254,13 @@ public static MissingFileHandler ofExternalName(String externalName) { // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-a-notice-message], so the \r // might be considered part of the message. public void handleMissingFile(String message) { - System.out.format("::%s::%s\n", output, message); + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + writer.write(Ids.OutputName.ERROR, message); + } catch (IOException e) { + // This is an optional output. ⇒ don't throw + GithubMessageType.DEBUG.format("failed to set output %s = \"%s\"", Ids.OutputName.ERROR, message); + } + githubMessageType.format("%s", message); } } From 1b1e7edafe69e31481700d7250c42e47e0fca39a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 10:49:20 +0200 Subject: [PATCH 181/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Action.java b/Action.java index 0a516d8..bb6cecf 100644 --- a/Action.java +++ b/Action.java @@ -216,7 +216,7 @@ private GithubMessageType(String externalName) { } public void format(String message, Object... args) { - String format = String.format("::%s::" + message + "\n", externalName); + String format = String.format("::%s::%s\n", externalName, message); System.out.format(format, args); } } From fb6f7a57cdc872eae1770448cac511e7ac8e63cf Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 11:06:34 +0200 Subject: [PATCH 182/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- Action.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74e4507..93c6f23 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -272,7 +272,7 @@ jobs: run: |- . "$SHELL_SETUP" assertEquals "$STATUS" failure - assertEquals "$ERROR" 'TODO is a directory or something' + assertEquals "$ERROR" '. is a directory' # ===================================================================== - name: '[act] (should fail) invalid file format' diff --git a/Action.java b/Action.java index bb6cecf..af85687 100644 --- a/Action.java +++ b/Action.java @@ -514,7 +514,9 @@ public static Properties readProperties(String file, MissingFileHandler missingF } catch (IOException e) { // Nice message (instead of catching FileNotFoundExeption which is also thrown on other problems and just contains the // filename, not "does not exist" or similar): - String message = Files.exists(path) ? ("error opening file: " + e.getMessage()) : ("file " + path + " does not exist"); + String message = Files.isDirectory(path) ? (path + " is a directory") // + : !Files.exists(path) ? ("file " + path + " does not exist") // + : ("error opening file: " + e.getMessage()); missingFileHandler.handleMissingFile(message); return new Properties(); } From c4518875d625b2188a1bc9f41fd6984dbb23c24e Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 11:18:02 +0200 Subject: [PATCH 183/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 27 ++++++++++++++++----------- action.yml | 4 ++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Action.java b/Action.java index af85687..0cfa358 100644 --- a/Action.java +++ b/Action.java @@ -84,6 +84,7 @@ public String toString() { } enum MissingFileHandlerName { + DEBUG_MESSAGE("debug-message"), // NOTICE_MESSAGE("notice-message"), // WARNING_MESSAGE("warning-message"), // ERROR("error"); @@ -223,6 +224,7 @@ public void format(String message, Object... args) { enum MissingFileHandler { + DEBUG_MESSAGE(Ids.MissingFileHandlerName.DEBUG_MESSAGE, GithubMessageType.DEBUG), // NOTICE_MESSAGE(Ids.MissingFileHandlerName.NOTICE_MESSAGE, GithubMessageType.NOTICE), // WARNING_MESSAGE(Ids.MissingFileHandlerName.WARNING_MESSAGE, GithubMessageType.WARNING), // ERROR(Ids.MissingFileHandlerName.ERROR, GithubMessageType.ERROR) { @@ -508,18 +510,21 @@ public static Writer openFile(String name, OpenOption... options) throws IOExcep public static Properties readProperties(String file, MissingFileHandler missingFileHandler) { Properties allProps = new Properties(); Path path = Paths.get(file); - try (InputStream in = Files.newInputStream(path)) { - allProps.load(in); - return allProps; - } catch (IOException e) { - // Nice message (instead of catching FileNotFoundExeption which is also thrown on other problems and just contains the - // filename, not "does not exist" or similar): - String message = Files.isDirectory(path) ? (path + " is a directory") // - : !Files.exists(path) ? ("file " + path + " does not exist") // - : ("error opening file: " + e.getMessage()); - missingFileHandler.handleMissingFile(message); - return new Properties(); + if (!Files.exists(path)) { + missingFileHandler.handleMissingFile("file " + path + " does not exist"); + } else if (Files.isDirectory(path)) { + MissingFileHandler.ERROR.handleMissingFile(path + " is a directory"); + } else { + try (InputStream in = Files.newInputStream(path)) { + allProps.load(in); + return allProps; + } catch (IOException e) { + missingFileHandler.handleMissingFile("error opening file: " + e.getMessage()); + } catch (Exception e) { + MissingFileHandler.ERROR.handleMissingFile("error opening file: " + e.getMessage()); + } } + return new Properties(); } /** diff --git a/action.yml b/action.yml index 89f1baa..e51d979 100644 --- a/action.yml +++ b/action.yml @@ -12,9 +12,13 @@ inputs: default: error description: |- What to do if `file` does not exist or is not readable. One of: + - "debug-message": write a debug message and continue with an empty properties map, - "notice-message": write a notice message and continue with an empty properties map, - "warning-message": write a warning message and continue with an empty properties map, - "error": write an error message and exit with non-zero status. + Note that these situations are always treated as an error: + - `file` is a directory, + - `file` is readable, but not in the correct format. keys: description: |- Selects the property keys (names). Format: `keySeparator`-separated list. From d209339c5c5a404b4a26994286ec8cd1f0059616 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 11:19:35 +0200 Subject: [PATCH 184/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93c6f23..5ec5430 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -259,6 +259,7 @@ jobs: # ===================================================================== - name: '[act] (should fail) file is a directory' id: invalid-file-error_directory + continue-on-error: true uses: ./ with: file: test @@ -277,6 +278,7 @@ jobs: # ===================================================================== - name: '[act] (should fail) invalid file format' id: invalid-file-error_format + continue-on-error: true uses: ./ with: file: action.yml From 013deff5b6d3056d6229e696494dc19ea788cbd2 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 11:21:03 +0200 Subject: [PATCH 185/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ec5430..d7d03ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -273,7 +273,7 @@ jobs: run: |- . "$SHELL_SETUP" assertEquals "$STATUS" failure - assertEquals "$ERROR" '. is a directory' + assertEquals "$ERROR" 'test is a directory' # ===================================================================== - name: '[act] (should fail) invalid file format' From 4a4fa6cbe7344213ae28f9f532a4d583790d3d4a Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 11:59:51 +0200 Subject: [PATCH 186/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- Action.java | 8 ++++---- Dockerfile | 11 +++++++++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7d03ec..8a66915 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -281,7 +281,7 @@ jobs: continue-on-error: true uses: ./ with: - file: action.yml + file: test/resources/test-invalid.properties onMissingFile: 'notice-message' # should fail with any onMissingFile, even the most lenient one keys: ${{env.SELECTED_KEY}} resultType: json diff --git a/Action.java b/Action.java index 0cfa358..4f4cb4d 100644 --- a/Action.java +++ b/Action.java @@ -216,9 +216,9 @@ private GithubMessageType(String externalName) { this.externalName = externalName; } - public void format(String message, Object... args) { - String format = String.format("::%s::%s\n", externalName, message); - System.out.format(format, args); + public void format(String format, Object... args) { + String formatWithPrefix = String.format("::%s::%s\n", externalName, format); + System.out.format(formatWithPrefix, args); } } @@ -260,7 +260,7 @@ public void handleMissingFile(String message) { writer.write(Ids.OutputName.ERROR, message); } catch (IOException e) { // This is an optional output. ⇒ don't throw - GithubMessageType.DEBUG.format("failed to set output %s = \"%s\"", Ids.OutputName.ERROR, message); + GithubMessageType.DEBUG.format("failed to set output %s = \"%s\": %s", Ids.OutputName.ERROR, message, e.toString()); } githubMessageType.format("%s", message); } diff --git a/Dockerfile b/Dockerfile index c17bba3..311cca1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,12 @@ LABEL "maintainer"="Team MCBS Core " WORKDIR /action COPY *.java . -RUN ["javac", "Action.java"] -ENTRYPOINT ["java", "--class-path", "/action", "Action"] +RUN ["chmod", "a-wx", "Action.java"] + +# When re-building like this and using `java`, we may get an image with up-to-date Action.java, +# but outdated *.class. Don't know how GitHub actions handles this exactly, but this would break +# local Docker builds at least. +# # RUN ["javac", "Action.java"] +# # ENTRYPOINT ["java", "--class-path", "/action", "Action"] +# So instead: +ENTRYPOINT ["java", "Action.java"] From 4616d60cac6ce966893a8ac1122da42f27a59e7f Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 12:17:02 +0200 Subject: [PATCH 187/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 8 ++-- Action.java | 93 ++++++++++---------------------------- 2 files changed, 29 insertions(+), 72 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a66915..87e1360 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -186,7 +186,7 @@ jobs: - *checkoutStep # ===================================================================== - - name: '[act] missing file with default onMissingFile "notice-message"' + - name: '[act] missing file with onMissingFile "notice-message"' id: missing-file-error_onMissingFile-notice-message uses: ./ with: @@ -202,7 +202,7 @@ jobs: assertEquals "$(getJsonValue "$RESULT_JSON" "$SELECTED_KEY")" '' # ===================================================================== - - name: '[act] missing file with default onMissingFile "warning-message"' + - name: '[act] missing file with onMissingFile "warning-message"' id: missing-file-error_onMissingFile-warning-message uses: ./ with: @@ -218,7 +218,7 @@ jobs: assertEquals "$(getJsonValue "$RESULT_JSON" "$SELECTED_KEY")" '' # ===================================================================== - - name: '[act] (should fail) missing file with default onMissingFile "error"' + - name: '[act] (should fail) missing file with onMissingFile "error"' id: missing-file-error_onMissingFile-error continue-on-error: true uses: ./ @@ -292,7 +292,7 @@ jobs: run: |- . "$SHELL_SETUP" assertEquals "$STATUS" failure - assertEquals "$ERROR" 'TODO parse error or something' + assertEquals "$ERROR" 'error in file test/resources/test-invalid.properties: Malformed \uxxxx encoding.' ######################################################################### test_default: diff --git a/Action.java b/Action.java index 4f4cb4d..fe9433b 100644 --- a/Action.java +++ b/Action.java @@ -229,10 +229,8 @@ enum MissingFileHandler { WARNING_MESSAGE(Ids.MissingFileHandlerName.WARNING_MESSAGE, GithubMessageType.WARNING), // ERROR(Ids.MissingFileHandlerName.ERROR, GithubMessageType.ERROR) { @Override - public void handleMissingFile(String message) { - super.handleMissingFile(message); - throw new ExitSilentlyException(2); - } + public void handleMissingFile(String messageFormat, + Object... messageArgs) {super.handleMissingFile(messageFormat,messageArgs);throw new ExitSilentlyException(2);} }; private final String externalName; @@ -255,14 +253,15 @@ public static MissingFileHandler ofExternalName(String externalName) { @SuppressWarnings("java:S3457") // Sonar rule suggests %n instead of \n, but that would not strictly be covered by the docs // [https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-a-notice-message], so the \r // might be considered part of the message. - public void handleMissingFile(String message) { + public void handleMissingFile(String messageFormat, Object... messageArgs) { + String messageStr = String.format(messageFormat, messageArgs); try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - writer.write(Ids.OutputName.ERROR, message); + writer.write(Ids.OutputName.ERROR, messageStr); } catch (IOException e) { // This is an optional output. ⇒ don't throw - GithubMessageType.DEBUG.format("failed to set output %s = \"%s\": %s", Ids.OutputName.ERROR, message, e.toString()); + GithubMessageType.DEBUG.format("failed to set output %s = \"%s\": %s", Ids.OutputName.ERROR, messageStr, e.toString()); } - githubMessageType.format("%s", message); + githubMessageType.format(messageFormat, messageArgs); } } @@ -271,80 +270,38 @@ enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override public void write(Map> props, Config config) throws IOException { - config.requireNoArg(); - String lastValue = null; - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - for (Map.Entry> entry : props.entrySet()) { - String key = encodeKey(config.outputPrefix() + entry.getKey()); - lastValue = entry.getValue().orElse(""); - writer.write(key, lastValue); - } - - if (config.selectedKeys().isPresent()) { - writer.write(Ids.OutputName.VALUE, lastValue != null ? lastValue : ""); - } - } - } + config.requireNoArg();String lastValue=null;try(GitHubVariableWriter writer=GitHubOutputFile.OUTPUT.open()){for(Map.Entry>entry:props.entrySet()){String key=encodeKey(config.outputPrefix()+entry.getKey());lastValue=entry.getValue().orElse("");writer.write(key,lastValue);} - private static String encodeKey(String key) { - StringBuilder result = new StringBuilder(key.length() + 4); - Matcher matcher = Pattern.compile("([\\s\\p{Punct}&&[^_]])").matcher(key); - while (matcher.find()) { - matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); - } - matcher.appendTail(result); - return result.toString(); + if(config.selectedKeys().isPresent()){writer.write(Ids.OutputName.VALUE,lastValue!=null?lastValue:"");}} } + + private static String encodeKey( + String key) {StringBuilder result=new StringBuilder(key.length()+4);Matcher matcher=Pattern.compile("([\\s\\p{Punct}&&[^_]])").matcher(key);while(matcher.find()){matcher.appendReplacement(result,String.format("-%04X",(int)matcher.group(1).charAt(0)));}matcher.appendTail(result);return result.toString();} }, OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override - public void write(Map> props, Config config) throws IOException { - writeNamedImpl(props, config, true, GitHubOutputFile.OUTPUT); - } + public void write(Map> props, Config config) + throws IOException {writeNamedImpl(props,config,true,GitHubOutputFile.OUTPUT);} }, ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override - public void write(Map> props, Config config) throws IOException { - writeNamedImpl(props, config, false, GitHubOutputFile.ENV); - } + public void write(Map> props, Config config) + throws IOException {writeNamedImpl(props,config,false,GitHubOutputFile.ENV);} }, ENV(Ids.ResultWriterName.ENV) { @Override - public void write(Map> props, Config config) throws IOException { - String prefix = config.resultTypeArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { - for (Map.Entry> entry : props.entrySet()) { - Optional value = entry.getValue(); - value.ifPresent(v -> writer.write(prefix + entry.getKey(), v)); - } - } - } + public void write(Map> props, Config config) + throws IOException {String prefix=config.resultTypeArg();try(GitHubVariableWriter writer=GitHubOutputFile.ENV.open()){for(Map.Entry>entry:props.entrySet()){Optionalvalue=entry.getValue();value.ifPresent(v->writer.write(prefix+entry.getKey(),v));}}} }, JSON(Ids.ResultWriterName.JSON) { @Override - public void write(Map> props, Config config) throws IOException { - config.requireNoArg(); - try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { - writer.write(Ids.OutputName.JSON, Util.toJson(props).s()); - } - } + public void write(Map> props, Config config) + throws IOException {config.requireNoArg();try(GitHubVariableWriter writer=GitHubOutputFile.OUTPUT.open()){writer.write(Ids.OutputName.JSON,Util.toJson(props).s());}} }, JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override - public void write(Map> props, Config config) throws IOException { - String outputFile = config.requiredResultTypeArg(); - Path parentDir = Paths.get(outputFile).getParent(); - if (parentDir != null) { - Files.createDirectories(parentDir); - } - try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - StringIntPair jsonResult = Util.toJson(props); - System.err.format("writing JSON for %s properties to %s%n", jsonResult.i(), outputFile); - writer.write(jsonResult.s()); - writer.write('\n'); - writer.flush(); - } - } + public void write(Map> props, Config config) + throws IOException {String outputFile=config.requiredResultTypeArg();Path parentDir=Paths.get(outputFile).getParent();if(parentDir!=null){Files.createDirectories(parentDir);}try(Writer writer=Util.openFile(outputFile,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){StringIntPair jsonResult=Util.toJson(props);System.err.format("writing JSON for %s properties to %s%n",jsonResult.i(),outputFile);writer.write(jsonResult.s());writer.write('\n');writer.flush();}} }; private final String externalName; @@ -519,9 +476,9 @@ public static Properties readProperties(String file, MissingFileHandler missingF allProps.load(in); return allProps; } catch (IOException e) { - missingFileHandler.handleMissingFile("error opening file: " + e.getMessage()); - } catch (Exception e) { - MissingFileHandler.ERROR.handleMissingFile("error opening file: " + e.getMessage()); + missingFileHandler.handleMissingFile("error opening file %s: %s", path, e.getMessage()); + } catch (Exception e) { // e.g. IllegalArgumentException: "Malformed \\uxxxx encoding." on invalid contents + MissingFileHandler.ERROR.handleMissingFile("error in file %s: %s", path, e.getMessage()); } } return new Properties(); From f56a702d9fe93da3075b77678a28ae455ab27bf9 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 12:17:25 +0200 Subject: [PATCH 188/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/resources/test-invalid.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/resources/test-invalid.properties diff --git a/test/resources/test-invalid.properties b/test/resources/test-invalid.properties new file mode 100644 index 0000000..c62f03e --- /dev/null +++ b/test/resources/test-invalid.properties @@ -0,0 +1 @@ +foo = \u From 58e72e7a3c47f6c2de1dea7b73eb2027c8010f70 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 16:32:59 +0200 Subject: [PATCH 189/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Action.java | 78 +++++++++++++++++++++++++++++++++++++++++------------ Dockerfile | 1 - 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/Action.java b/Action.java index fe9433b..40df412 100644 --- a/Action.java +++ b/Action.java @@ -229,8 +229,10 @@ enum MissingFileHandler { WARNING_MESSAGE(Ids.MissingFileHandlerName.WARNING_MESSAGE, GithubMessageType.WARNING), // ERROR(Ids.MissingFileHandlerName.ERROR, GithubMessageType.ERROR) { @Override - public void handleMissingFile(String messageFormat, - Object... messageArgs) {super.handleMissingFile(messageFormat,messageArgs);throw new ExitSilentlyException(2);} + public void handleMissingFile(String messageFormat, Object... messageArgs) { + super.handleMissingFile(messageFormat, messageArgs); + throw new ExitSilentlyException(2); + } }; private final String externalName; @@ -270,38 +272,80 @@ enum ResultWriter { OUTPUT(Ids.ResultWriterName.OUTPUT) { @Override public void write(Map> props, Config config) throws IOException { - config.requireNoArg();String lastValue=null;try(GitHubVariableWriter writer=GitHubOutputFile.OUTPUT.open()){for(Map.Entry>entry:props.entrySet()){String key=encodeKey(config.outputPrefix()+entry.getKey());lastValue=entry.getValue().orElse("");writer.write(key,lastValue);} - - if(config.selectedKeys().isPresent()){writer.write(Ids.OutputName.VALUE,lastValue!=null?lastValue:"");}} + config.requireNoArg(); + String lastValue = null; + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + for (Map.Entry> entry : props.entrySet()) { + String key = encodeKey(config.outputPrefix() + entry.getKey()); + lastValue = entry.getValue().orElse(""); + writer.write(key, lastValue); + } + + if (config.selectedKeys().isPresent()) { + writer.write(Ids.OutputName.VALUE, lastValue != null ? lastValue : ""); + } + } } - private static String encodeKey( - String key) {StringBuilder result=new StringBuilder(key.length()+4);Matcher matcher=Pattern.compile("([\\s\\p{Punct}&&[^_]])").matcher(key);while(matcher.find()){matcher.appendReplacement(result,String.format("-%04X",(int)matcher.group(1).charAt(0)));}matcher.appendTail(result);return result.toString();} + private static String encodeKey(String key) { + StringBuilder result = new StringBuilder(key.length() + 4); + Matcher matcher = Pattern.compile("([\\s\\p{Punct}&&[^_]])").matcher(key); + while (matcher.find()) { + matcher.appendReplacement(result, String.format("-%04X", (int) matcher.group(1).charAt(0))); + } + matcher.appendTail(result); + return result.toString(); + } }, OUTPUT_NAMED(Ids.ResultWriterName.OUTPUT_NAMED) { @Override - public void write(Map> props, Config config) - throws IOException {writeNamedImpl(props,config,true,GitHubOutputFile.OUTPUT);} + public void write(Map> props, Config config) throws IOException { + writeNamedImpl(props, config, true, GitHubOutputFile.OUTPUT); + } }, ENV_NAMED(Ids.ResultWriterName.ENV_NAMED) { @Override - public void write(Map> props, Config config) - throws IOException {writeNamedImpl(props,config,false,GitHubOutputFile.ENV);} + public void write(Map> props, Config config) throws IOException { + writeNamedImpl(props, config, false, GitHubOutputFile.ENV); + } }, ENV(Ids.ResultWriterName.ENV) { @Override - public void write(Map> props, Config config) - throws IOException {String prefix=config.resultTypeArg();try(GitHubVariableWriter writer=GitHubOutputFile.ENV.open()){for(Map.Entry>entry:props.entrySet()){Optionalvalue=entry.getValue();value.ifPresent(v->writer.write(prefix+entry.getKey(),v));}}} + public void write(Map> props, Config config) throws IOException { + String prefix = config.resultTypeArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.ENV.open()) { + for (Map.Entry> entry : props.entrySet()) { + Optional value = entry.getValue(); + value.ifPresent(v -> writer.write(prefix + entry.getKey(), v)); + } + } + } }, JSON(Ids.ResultWriterName.JSON) { @Override - public void write(Map> props, Config config) - throws IOException {config.requireNoArg();try(GitHubVariableWriter writer=GitHubOutputFile.OUTPUT.open()){writer.write(Ids.OutputName.JSON,Util.toJson(props).s());}} + public void write(Map> props, Config config) throws IOException { + config.requireNoArg(); + try (GitHubVariableWriter writer = GitHubOutputFile.OUTPUT.open()) { + writer.write(Ids.OutputName.JSON, Util.toJson(props).s()); + } + } }, JSON_FILE(Ids.ResultWriterName.JSON_FILE) { @Override - public void write(Map> props, Config config) - throws IOException {String outputFile=config.requiredResultTypeArg();Path parentDir=Paths.get(outputFile).getParent();if(parentDir!=null){Files.createDirectories(parentDir);}try(Writer writer=Util.openFile(outputFile,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){StringIntPair jsonResult=Util.toJson(props);System.err.format("writing JSON for %s properties to %s%n",jsonResult.i(),outputFile);writer.write(jsonResult.s());writer.write('\n');writer.flush();}} + public void write(Map> props, Config config) throws IOException { + String outputFile = config.requiredResultTypeArg(); + Path parentDir = Paths.get(outputFile).getParent(); + if (parentDir != null) { + Files.createDirectories(parentDir); + } + try (Writer writer = Util.openFile(outputFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + StringIntPair jsonResult = Util.toJson(props); + System.err.format("writing JSON for %s properties to %s%n", jsonResult.i(), outputFile); + writer.write(jsonResult.s()); + writer.write('\n'); + writer.flush(); + } + } }; private final String externalName; diff --git a/Dockerfile b/Dockerfile index 311cca1..05b49f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,6 @@ LABEL "com.github.actions.name"="read Java properties" LABEL "com.github.actions.description"="read Java properties file and return one or more as property values as plain text or JSON" LABEL "repository"="https://github.com/freenet-actions/read-java-properties" LABEL "homepage"="https://github.com/freenet-actions" -LABEL "maintainer"="Team MCBS Core " WORKDIR /action COPY *.java . From b05a3c25f92626141a8584d6417849a3a9f66f90 Mon Sep 17 00:00:00 2001 From: Christoph Strebin Date: Thu, 21 May 2026 17:39:44 +0200 Subject: [PATCH 190/190] =?UTF-8?q?=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 05b49f0..fddfed7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,10 +9,5 @@ WORKDIR /action COPY *.java . RUN ["chmod", "a-wx", "Action.java"] -# When re-building like this and using `java`, we may get an image with up-to-date Action.java, -# but outdated *.class. Don't know how GitHub actions handles this exactly, but this would break -# local Docker builds at least. -# # RUN ["javac", "Action.java"] -# # ENTRYPOINT ["java", "--class-path", "/action", "Action"] -# So instead: -ENTRYPOINT ["java", "Action.java"] +RUN ["javac", "Action.java"] +ENTRYPOINT ["java", "--class-path", "/action", "Action"]