Make generated command script execution robust #5

Open
opened 2026-05-03 06:21:54 +00:00 by lambadalambda · 2 comments

Problem

Command steps hit a known failure mode where generated shell execution failed with errors like:

/bin/sh: syntax error: unterminated quoted string

This matched upstream reports around command entrypoint/script handling. We had to work around it by overriding the entrypoint and writing CI_SCRIPT to a file before executing it:

printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh

Desired behavior

Woodpecker should execute generated command scripts in a way that is robust for larger or more complex scripts by default. For example:

  • write the decoded script to a file and execute that file
  • avoid piping decoded script directly through shell in a way that can be truncated or quoting-sensitive
  • preserve useful xtrace/error behavior
  • document the exact generated wrapper behavior

Why this matters

Command execution is core CI functionality. Users should not need custom entrypoints across many workflows to avoid intermittent shell quoting/truncation issues.

## Problem Command steps hit a known failure mode where generated shell execution failed with errors like: ```text /bin/sh: syntax error: unterminated quoted string ``` This matched upstream reports around command entrypoint/script handling. We had to work around it by overriding the entrypoint and writing `CI_SCRIPT` to a file before executing it: ```sh printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh ``` ## Desired behavior Woodpecker should execute generated command scripts in a way that is robust for larger or more complex scripts by default. For example: - write the decoded script to a file and execute that file - avoid piping decoded script directly through shell in a way that can be truncated or quoting-sensitive - preserve useful xtrace/error behavior - document the exact generated wrapper behavior ## Why this matters Command execution is core CI functionality. Users should not need custom entrypoints across many workflows to avoid intermittent shell quoting/truncation issues.
Author
Owner

@opencode-bot What do you think about this?

@opencode-bot What do you think about this?
Owner

OpenCode Answer

Execution completed for pleroma/woodpecker at f87a97893225 (job #53).

Question

What do you think about this?

Answer

Agree with the direction: decoding CI_SCRIPT to a file and executing that file is the right default for POSIX command steps. The current wrapper at pipeline/backend/common/script.go:31 ("echo $CI_SCRIPT | base64 -d | /bin/sh -e"), used by the docker and kubernetes backends, pipes the decoded script through shell stdin, which is quoting/truncation-sensitive, loses file/line context in shell errors, and cannot be re-run or inspected. A file-based wrapper fixes that class of failure, and the project already endorsed this direction on Windows in #6969 ("iex is a poor mechanism..."), with #7065's partial revert showing that passing the payload as an entrypoint argument is equally fragile — env var + file execution avoids both. Two caveats: (1) it does not lift the real size ceiling, since CI_SCRIPT travels as one env var and exec fails E2BIG above ~128KB (MAX_ARG_STRLEN) — verified locally: 127,892B env works, 159,892B fails even for a trivial command, and the reporter's workaround shares this limit; scripts beyond ~96KB need the script mounted as a file, not env-passed. (2) I could not reproduce the exact intermittent 'unterminated quoted string' with dash up to the exec limit, so that root cause remains unconfirmed here and is likely runtime/image-specific — but a file-based wrapper removes it regardless.

Grounding: GenerateContainerConf (pipeline/backend/common/script.go) base64-encodes a generated script into env CI_SCRIPT and overrides the image entrypoint. POSIX: /bin/sh -c 'echo $CI_SCRIPT | base64 -d | /bin/sh -e' (line 31); Windows decodes via [Convert]::FromBase64String | iex (line 27). Docker uses it in convert.go:54-67 and kubernetes in pod.go:260-273; both let step.Entrypoint override it (the workaround users use today). The generated POSIX script (script_posix.go) writes .netrc, unsets CI_NETRC_* and CI_SCRIPT (lines 60-62), mkdir/cd workdir, then per-command 'echo + ' tracing — so error (-e) and tracing behavior live in the script and survive switching to 'sh -e '. Assessment: (1) File execution is a strict improvement — removes echo/pipe fragility, gives named-file error context, and the artifact is debuggable. Suggested wrapper: /bin/sh -c 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/wp-script.sh && /bin/sh -e /tmp/wp-script.sh' (quote $CI_SCRIPT; use mktemp; keep -e and existing echo-tracing rather than adding -x, which would double-print with traceScript; note /tmp writability and the retained base64 dependency in the step image). Apply the analogous WriteAllText change for Windows. (2) Repo history supports it: #6969 (9c018c7) moved Windows off iex citing escaping fragility; #7065 (a1de4fc) partially reverted because -encodedcommand passes the whole script as an argv entry, which hits command-line length limits — argv and stdin-pipe transports are both fragile; env + file is the consistent fix. (3) Hard limit my probe confirmed: with dash, both the current pipe wrapper and the file workaround work up to ~128KB of base64 (script ~96KB); above that exec fails E2BIG before any shell runs (single env string > MAX_ARG_STRLEN 131072), so the issue's 'robust for larger scripts' goal is only partly met — truly large scripts need a different transport (mounted volume/emptyDir). (4) The exact intermittent 'unterminated quoted string' did not reproduce with dash on Linux within the working size range, so that specific root cause is unconfirmed in this checkout; treat it as runtime/image-specific truncation. (5) Compatibility: docs document the wrapper and CI_SCRIPT (docs/docs/20-usage/20-workflow-syntax.md:176, 50-environment.md:155 — which already mislabels CI_SCRIPT as an 'internal script path'); keep CI_SCRIPT as base64 content and custom-entrypoint behavior unchanged, and update these docs alongside the change. The local backend is unaffected (it never uses CI_SCRIPT).

<!-- opencode-review:v2:bbc043191a3bfeb20cc9833f6b9c3cfe --> <!-- opencode-reviewed-head:f87a97893225b95600e12d8922edf005a4beff7d --> ## OpenCode Answer _Execution completed for `pleroma/woodpecker` at `f87a97893225` (job #53)._ ### Question > What do you think about this? ### Answer Agree with the direction: decoding CI_SCRIPT to a file and executing that file is the right default for POSIX command steps. The current wrapper at pipeline/backend/common/script.go:31 ("echo $CI_SCRIPT | base64 -d | /bin/sh -e"), used by the docker and kubernetes backends, pipes the decoded script through shell stdin, which is quoting/truncation-sensitive, loses file/line context in shell errors, and cannot be re-run or inspected. A file-based wrapper fixes that class of failure, and the project already endorsed this direction on Windows in #6969 ("iex is a poor mechanism..."), with #7065's partial revert showing that passing the payload as an entrypoint argument is equally fragile — env var + file execution avoids both. Two caveats: (1) it does not lift the real size ceiling, since CI_SCRIPT travels as one env var and exec fails E2BIG above ~128KB (MAX_ARG_STRLEN) — verified locally: 127,892B env works, 159,892B fails even for a trivial command, and the reporter's workaround shares this limit; scripts beyond ~96KB need the script mounted as a file, not env-passed. (2) I could not reproduce the exact intermittent 'unterminated quoted string' with dash up to the exec limit, so that root cause remains unconfirmed here and is likely runtime/image-specific — but a file-based wrapper removes it regardless. Grounding: GenerateContainerConf (pipeline/backend/common/script.go) base64-encodes a generated script into env CI_SCRIPT and overrides the image entrypoint. POSIX: /bin/sh -c 'echo $CI_SCRIPT | base64 -d | /bin/sh -e' (line 31); Windows decodes via [Convert]::FromBase64String | iex (line 27). Docker uses it in convert.go:54-67 and kubernetes in pod.go:260-273; both let step.Entrypoint override it (the workaround users use today). The generated POSIX script (script_posix.go) writes .netrc, unsets CI_NETRC_* and CI_SCRIPT (lines 60-62), mkdir/cd workdir, then per-command 'echo + <escaped cmd>' tracing — so error (-e) and tracing behavior live in the script and survive switching to 'sh -e <file>'. Assessment: (1) File execution is a strict improvement — removes echo/pipe fragility, gives named-file error context, and the artifact is debuggable. Suggested wrapper: /bin/sh -c 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/wp-script.sh && /bin/sh -e /tmp/wp-script.sh' (quote $CI_SCRIPT; use mktemp; keep -e and existing echo-tracing rather than adding -x, which would double-print with traceScript; note /tmp writability and the retained base64 dependency in the step image). Apply the analogous WriteAllText change for Windows. (2) Repo history supports it: #6969 (9c018c7) moved Windows off iex citing escaping fragility; #7065 (a1de4fc) partially reverted because -encodedcommand passes the whole script as an argv entry, which hits command-line length limits — argv and stdin-pipe transports are both fragile; env + file is the consistent fix. (3) Hard limit my probe confirmed: with dash, both the current pipe wrapper and the file workaround work up to ~128KB of base64 (script ~96KB); above that exec fails E2BIG before any shell runs (single env string > MAX_ARG_STRLEN 131072), so the issue's 'robust for larger scripts' goal is only partly met — truly large scripts need a different transport (mounted volume/emptyDir). (4) The exact intermittent 'unterminated quoted string' did not reproduce with dash on Linux within the working size range, so that specific root cause is unconfirmed in this checkout; treat it as runtime/image-specific truncation. (5) Compatibility: docs document the wrapper and CI_SCRIPT (docs/docs/20-usage/20-workflow-syntax.md:176, 50-environment.md:155 — which already mislabels CI_SCRIPT as an 'internal script path'); keep CI_SCRIPT as base64 content and custom-entrypoint behavior unchanged, and update these docs alongside the change. The local backend is unaffected (it never uses CI_SCRIPT).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
pleroma/woodpecker#5
No description provided.