actions / actions/toolkit

GITHUB_OUTPUT variables work from workflow echo but not by core API call

Open
#1,906 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
TypeScript
Stars
5.9k
Forks
1.8k
PR merge metrics
No merged PRs in 30d

Description

Description

When I write a key value pair by echo inside a workflow it can be used by other steps as variables and is visible inside the context. But when I use core.setOutput('key1', 'value1') it doesn't.

Also the content inside the $GITHUB_OUTPUT files is completely different.

Steps to reproduce

Set output with echo - works

Here an example which uses the described new way of setting outputs by environment variables. This works and the output file is in an expected way:

 - name: Print output by worfklow
      id: output_test_by_worfklow
      if: always()
      run: |
        echo "scan-trafficlight=RED" >> "$GITHUB_OUTPUT"  # Test Output 1
        echo "scan-findings-count=1" >> "$GITHUB_OUTPUT"  # Test Output 2

- name: Show all github output files
      id: output_file
      if: always()
      run: |
        echo "GITHUB_OUTPUT=$GITHUB_OUTPUT"
        DIRECTORY=$(dirname -- "$GITHUB_OUTPUT")
        # Check if the directory exists
        if [ ! -d "$DIRECTORY" ]; then
          echo "Directory does not exist."
          exit 1
        fi
        # Iterate through each file in the directory
        for FILE in "$DIRECTORY"/*; do
          # Check if the file name starts with "set_output"
          if [[ $(basename "$FILE") == set_output* ]]; then
            echo "----------------------------------------------------------------------------------------------------"
            echo "Contents of $FILE:"
            echo "----------------------------------------------------------------------------------------------------"
            cat "$FILE"
            echo # Add a newline for better readability
          fi
        done
- name: Print Context Information
      if: always()
      env:
        CONTEXT: ${{ toJson(steps) }}
      run: echo "$CONTEXT"

After execution I got following:

----------------------------------------------------------------------------------------------------
Contents of /runner/_work/_temp/_runner_file_commands/set_output_8c665a1b-cb6b-4962-96ca-3075398fe305:
----------------------------------------------------------------------------------------------------
scan-trafficlight=RED
scan-findings-count=1

and the (reduced) context output looks like this

"output_test_by_worfklow": {
    "outputs": {
      "scan-trafficlight": "RED",
      "scan-findings-count": "1"
    },
    "outcome": "success",
    "conclusion": "success"
  }

Means everything is working. Fine.

Set output via API - DOES NOT WORK

But when I write a custom action and use internally there :

core.setOutput('scan-trafficlight','RED')
core.setOutput('scan-findings-count','1')

to define output, it writes the files in a different way and the context does
not contain the two keys:

 - name: Print output by custom action
      id: output_test_by_custom_action
      uses: {orgname}/{customActionWhichSetsOutput}
- name: Show all github output files
      id: output_file
      if: always()
      run: |
        echo "GITHUB_OUTPUT=$GITHUB_OUTPUT"
        DIRECTORY=$(dirname -- "$GITHUB_OUTPUT")
        # Check if the directory exists
        if [ ! -d "$DIRECTORY" ]; then
          echo "Directory does not exist."
          exit 1
        fi
        # Iterate through each file in the directory
        for FILE in "$DIRECTORY"/*; do
          # Check if the file name starts with "set_output"
          if [[ $(basename "$FILE") == set_output* ]]; then
            echo "----------------------------------------------------------------------------------------------------"
            echo "Contents of $FILE:"
            echo "----------------------------------------------------------------------------------------------------"
            cat "$FILE"
            echo # Add a newline for better readability
          fi
        done
- name: Print Context Information
      if: always()
      env:
        CONTEXT: ${{ toJson(steps) }}
      run: echo "$CONTEXT"

After execution I got following:

----------------------------------------------------------------------------------------------------
Contents of /runner/_work/_temp/_runner_file_commands/set_output_9550026e-04b7-4a5e-963b-4d7838e73e4d:
----------------------------------------------------------------------------------------------------
scan-trafficlight<<ghadelimiter_12b579a9-fb20-4e41-8b6d-53dd97e57865
RED
ghadelimiter_12b579a9-fb20-4e41-8b6d-53dd97e57865
scan-findings-count<<ghadelimiter_b1042cbc-4b13-4648-851b-83535f96876c
1
ghadelimiter_b1042cbc-4b13-4648-851b-83535f96876c

and the (reduced) context output looks like this

"output_test_by_custom_action": {
    "outputs": {

    },
    "outcome": "success",
    "conclusion": "success"
  }

Means:

  1. The output is written different to the output by echo!
  2. The context does not contain output information/there is no access to it.
Additional info
Workaround (did not help)

I tried to write the file directly {key}={value}{os.EOL} but the 2 keys were still not
visible in context output.

Analyze
// https://github.com/actions/toolkit/blob/main/packages/core/src/core.ts#L192
export function setOutput(name: string, value: any): void {
  const filePath = process.env['GITHUB_OUTPUT'] || ''
  if (filePath) {
    return issueFileCommand('OUTPUT', prepareKeyValueMessage(name, value))
  }

  process.stdout.write(os.EOL)
  issueCommand('set-output', {name}, toCommandValue(value))
}

this calls

// https://github.com/actions/toolkit/blob/main/packages/core/src/file-command.ts
export function prepareKeyValueMessage(key: string, value: any): string {
  const delimiter = `ghadelimiter_${crypto.randomUUID()}`
  const convertedValue = toCommandValue(value)

  // These should realistically never happen, but just in case someone finds a
  // way to exploit uuid generation let's not allow keys or values that contain
  // the delimiter.
  if (key.includes(delimiter)) {
    throw new Error(
      `Unexpected input: name should not contain the delimiter "${delimiter}"`
    )
  }

  if (convertedValue.includes(delimiter)) {
    throw new Error(
      `Unexpected input: value should not contain the delimiter "${delimiter}"`
    )
  }

  return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`
}

Hm... the usage of ${crypto.randomUUID()} looks odd: What shall here be the purpose of the crypto uuid? Was the intention to ensure keys are unqiue? It is always different. What was the real purpose for this. Sorry I did not get it.

Why does it not appear inside the context? Could it be that the read mechanism which sets the context is not able to read the new format?

Expected behavior

  1. I would expect that the output file structure would be same for echos and for API usages.
  2. using core.setOutput(...) should result in the output listed inside the context

Target

This appeared with core 1.10.1 and newer

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/core/src/core.ts and packages/core/src/file-command.ts, especially setOutput and prepareKeyValueMessage, then reproduce the workflow using the echo and custom-action examples from the issue. Done means core.setOutput outputs are parsed into the step context consistently with direct GITHUB_OUTPUT writes, with coverage for the affected format.

Written by the indexing model from the issue text.

Assessment

Tech stack
github-actions, typescript
Domain
ci-cd, devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.