Skip to content
Latchkey LogoLatchkey home

GitHub Actions multiline run script fails on the block scalar

A GitHub Actions multiline run script fails most often because the block scalar chosen for it folds line breaks into spaces, which turns a list of commands into one very long command. The shell is not misbehaving and the runner is not mangling anything: by the time either of them sees the script, YAML has already decided what the text is.

Literal and folded block scalars with the same source lines and the resulting script text
The same four source lines under each style. Folded joins single breaks with spaces and leaves more indented lines alone.

What this error means

A run step with several lines under it behaves as though the lines were joined, or loses a line, or fails with a shell syntax error that quotes a fragment spanning what you thought were two separate commands. A common variant is a command that appears to receive extra arguments it never had, which is the next line having arrived as part of it. Another is a heredoc that never terminates, because the terminator ended up on the same line as its content. Nothing in the log names YAML, so the shell takes the blame.

Illustrative workflow fragment showing the folded result, not a log line
run: >
  echo one
  echo two
# the shell receives: echo one echo two

Three rules decide what the shell receives

The first rule is the choice of style. A literal block scalar keeps every line break exactly as written, which is what a script wants. A folded block scalar replaces a single line break between two ordinary lines with a single space, which is what a paragraph of prose wants. There is no third option and no default that guesses: writing neither indicator means the value is an ordinary scalar, not a block at all.

The second rule is the one that surprises people who already know the first. In a folded block, lines that are indented further than the block indentation are not folded. Their line breaks are preserved. So a folded block containing an indented continuation produces a mixture, some lines joined and some not, which is far harder to diagnose than everything being joined.

The third rule is chomping, which decides what happens to the line breaks at the end. The default keeps a single trailing newline. An added minus strips them all, and an added plus keeps every trailing blank line. This rarely breaks a script outright, and it matters when a value is being written to a file or compared as a string, where an unexpected trailing newline changes the answer.

None of this is specific to GitHub. These are the YAML block scalar rules, and any workflow file is subject to them before any part of Actions reads a single key.

What you writeWhat the line breaks becomeUse it for
run: |kept, every one of themscripts, always
run: >a single break becomes a spaceprose, never scripts
run: > with indented linesthose lines keep their breaksnothing deliberately
run: |-kept, with the trailing ones strippeda value with no trailing newline

Common causes

The folded style was used for a script

The commonest cause. A single line break between two ordinary lines becomes a space, so consecutive commands are concatenated into one, and the failure looks like a command receiving arguments it never had.

A folded block contained more indented lines

Those lines are not folded, so their breaks survive. The result is a script where some joins happened and some did not, which is much harder to read back than a script that was joined uniformly.

A line was indented less than the block indentation

That ends the block. What follows is parsed as something else at the parent level, which is either a confusing key or a syntax error depending on the text.

A heredoc terminator was folded onto another line

A heredoc needs its terminator alone on a line. In our experience this is the version that takes longest to diagnose, because the error is about an unterminated document rather than about the line that moved.

The first command failed and the rest never ran

Not a YAML problem at all, and it presents identically at a glance. A missing line in the output is a line that did not run, and the reason is usually above it.

How to fix it

Use the literal style for every script

Replace the folded indicator with the literal one. There is no situation inside a run key where folding is what you want, so this can be a rule rather than a judgement.

.github/workflows/ci.yml
- run: |
    echo one
    echo two
    ./build.sh

Print the parsed value to see the real script

  1. Load the workflow file with any YAML parser.
  2. Print the value of the run key for the step in question.
  3. Compare what comes out against what you believed you wrote.
shell
python3 -c "import sys,yaml; d=yaml.safe_load(open('.github/workflows/ci.yml')); print(repr(d['jobs']['build']['steps'][0]['run']))"

Check the indentation of every line in the block

Every line must be indented at least as far as the first non empty one. Where leading whitespace is meaningful, pin the block indentation with an explicit indicator rather than relying on what the first line happens to be.

Move anything long into a script file

A file in the repository gets highlighting, linting and a proper review. The workflow step becomes a single line, which removes this whole category of problem rather than avoiding it.

.github/workflows/ci.yml
- run: ./scripts/release.sh
  env:
    VERSION: ${{ github.ref_name }}

The cases where folding is not the culprit

Indentation inside the block is the next thing to check. Every line of a block scalar must be indented at least as far as the block indentation established by its first non empty line. A line that is indented less than that ends the block, so the rest of what you thought was your script becomes a sibling key or a syntax error, depending on what it looks like.

A related trap is a first line that begins with a space. Since the first non empty line sets the indentation, a script whose first line is deliberately indented shifts the whole block, and the leading space you wanted disappears. YAML provides an explicit indentation indicator for exactly this, a digit after the style character, which pins the indentation rather than inferring it.

The last one is not a YAML problem at all and gets blamed on YAML constantly. A multi line script runs under a shell that stops at the first failing command, so a script whose second line never runs may have had its first line fail rather than its second line vanish. Reading the log for the output of line one settles it in a second, and our page on a run step exiting with status one covers that behavior.

.github/workflows/ci.yml
# the block indentation is set by the first line, so this loses the leading spaces
run: |
    indented line
  normal line

# pin it explicitly with an indentation indicator
run: |2
    this line keeps two of its leading spaces

Writing multi line scripts that stay readable

Use the literal style for every script, without exception. The folded style has no use inside a run key, and treating the choice as a decision at all is how the wrong one gets made. If a line is genuinely too long, break it with a backslash, which is the shell own continuation and survives the literal style unchanged.

Keep heredocs away from folded blocks entirely. A heredoc depends on its terminator being alone on a line, and folding is precisely the operation that puts it somewhere else. Inside a literal block a heredoc behaves normally.

When a script grows past a handful of lines, move it into a file in the repository and call it. A file gets syntax highlighting, a shell linter, and review that reads it as code rather than as configuration. The step then becomes one line, and none of the rules on this page can affect it.

.github/workflows/ci.yml
- run: |
    set -u
    npm ci
    npm run build \
      --workspace=app

- run: ./scripts/build.sh

Why there is no recorded run on this page

The transformation described here is complete before the runner is involved, so a recorded run would show the consequence and not the cause. The interesting artifact is the script text after YAML has finished with it, and that text does not appear in the log: what appears is whatever the shell made of it.

The rules themselves come from the YAML specification, which is a better source than any run because it is what every parser implements. Checking your own file against them is also something you can do locally in seconds, by loading the file with any YAML parser and printing the value of the run key, which is more direct evidence than any recording we could make.

There is nothing repairable here either. A script that was folded into one line is a script the file asked for, and the runner has no way to know which spaces used to be line breaks.

How to prevent it

  • Allow only the literal block style under a run key, as a review rule.
  • Move any script longer than about ten lines into a file in the repository.
  • Never put a heredoc inside a folded block.
  • When whitespace matters, pin the block indentation explicitly.

Frequently asked questions

What is the difference between the two block scalar styles in a run step?
The literal style keeps every line break as written, which is what a script needs. The folded style replaces a single break between two ordinary lines with a space, which is meant for prose and turns consecutive commands into one command.
Why did only some of my lines get joined?
Because in a folded block, lines indented further than the block indentation are not folded and keep their line breaks. A block with a mixture of indentation levels therefore produces a mixture of joined and unjoined lines.
Why does my heredoc never terminate?
Because the terminator has to be alone on a line and folding moved it. A heredoc inside a folded block is joined to its surroundings, so the shell reads to the end of the script still looking for the terminator. Use the literal style.
How do I keep leading spaces in a multiline run?
Add an explicit indentation indicator after the style character, which pins the block indentation instead of inferring it from the first non empty line. Without it, a first line that is indented sets the indentation for the whole block and those spaces disappear.
My second command never ran. Is that folding?
Not necessarily. A multiline script runs under a shell that stops at the first failing command, so a line that produced no output may never have been reached. Look for the output of the line above it before suspecting YAML.

Related guides

References

The shell never saw the script you wrote. Latchkey runs the one it does see at $0.0025/min. Start free → 30-day trial · No credit card