Deployment fails in a custom code-only guard, followed by “Matching delimiter not found 'EOF'” #212388
Replies: 7 comments 29 replies
|
Short answer to (1): yes, the Why it happens
echo 'stdout<<EOF' >> "$GITHUB_OUTPUT"
drone-ssh ... | tee -a "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"If the remote script exits non-zero, So the real failure is the one before it: the Preserving the original error and exit status
set +e
ssh deploy@"$HOST" 'bash -s' < scripts/deploy.sh 2>&1 | tee deploy.log
rc=${PIPESTATUS[0]}
delim="EOF_$(openssl rand -hex 8)" # random delimiter so log content can't end the block early
{ echo "rc=$rc"; echo "log<<$delim"; cat deploy.log; echo "$delim"; } >> "$GITHUB_OUTPUT"
exit "$rc"
(2) Minimal credential-free reproduction Step 1 reproduces the exact message without SSH at all: name: eof-repro
on: workflow_dispatch
jobs:
repro:
runs-on: ubuntu-latest
steps:
- shell: bash
run: |
set -euo pipefail
echo 'stdout<<EOF' >> "$GITHUB_OUTPUT"
(echo "guard failed"; exit 3) | tee -a "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"Step 2 isolates the action: run Step 3 isolates your script: run the guard directly on a copy of the host layout (container or VM) with (3) Recovering from a partial Docker Compose deployment
Happy to look at a sanitized excerpt of the workflow and guard script if you post one. |
|
Thanks, that helps. From what you describe, production is in a split state: the containers and database are still on the earlier revision, but the source checkout and Step 1: Freeze and snapshot (read-only) Until this is resolved, don't run Record the current state: cat /path/to/unfinished.json
git -C /path/to/app rev-parse HEAD
git -C /path/to/app status --short
docker compose ps
docker inspect --format '{{.Name}} running={{.Image}} tag={{.Config.Image}}' $(docker compose ps -q)
docker image inspect --format '{{.Id}}' <image tag used in compose>Compare the last two outputs. If the candidate build reused the tag the service runs, that tag now points to the candidate image, and any container recreate would start it. Then take a database backup now, since the deploy never reached its own backup step (e.g. Step 2: Identify the failed check Run the guard outside the workflow, in an isolated copy, with the same inputs as the failed run: the running revision, the candidate revision and the env file. Read the guard first to confirm it is read-only (no writes, no marker changes, no docker commands) before running it anywhere near production. Make it name the cause while still failing closed. If it's Python with one broad import sys, traceback
for name, check in CHECKS: # e.g. [("compat_fingerprint", check_fingerprint), ...]
try:
check()
except Exception:
print(f"GUARD_FAIL check={name}", file=sys.stderr)
traceback.print_exc()
sys.exit(1)For a shell guard, Given your earlier note that the candidate doesn't match the compatibility fingerprint in CI, that's the first check I'd suspect, but confirm it with the instrumented run rather than assume it. Step 3: Bring the host files back in line with what's running The safer direction is to make the files match the containers, not the other way round:
Step 4: Resolve the marker with a record Only after Step 3 checks out. Don't delete the marker; archive it (e.g. Step 5: Retry through the normal path
If you post the guard's list of checks and its |
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
|
Thanks for providing the context. I would investigate this as two separate issues rather than treating both messages as one failure. 1. Understanding the EOF errorYes, the EOF error can be a secondary GitHub Actions output-capture failure. When If the remote command exits with a non-zero status,
This behavior is documented in [appleboy/ssh-action issue #416](appleboy/ssh-action#416). A temporary workaround is to disable The permanent fix should ensure that the output delimiter is always closed while preserving the original command's exit status. For example: if [[ "$INPUT_CAPTURE_STDOUT" == "true" ]]; then
echo 'stdout<<EOF' >> "$GITHUB_OUTPUT"
set +e
"$TARGET" "$@" | tee -a "$GITHUB_OUTPUT"
status="${PIPESTATUS[0]}"
set -e
echo 'EOF' >> "$GITHUB_OUTPUT"
exit "$status"
else
"$TARGET" "$@"
fiThis allows the output to be finalized before returning the original failure code. 2. Minimal reproductionThis can be reproduced without an SSH connection or production credentials: output=$(mktemp)
GITHUB_OUTPUT="$output" bash -c '
set -euo pipefail
echo "stdout<<EOF" >> "$GITHUB_OUTPUT"
(echo "Simulated remote failure"; exit 23) |
tee -a "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
'
echo "Captured output file:"
cat "$output"The script exits before writing the closing delimiter. This demonstrates the output-capture failure independently of SSH. For an end-to-end test, use a disposable SSH container with a temporary key and a pinned action version. Run a harmless command that prints a message and exits with status 23, first with This will help distinguish an action-level output problem from an actual failure in the remote deployment script. 3. Safe recovery for partial Docker Compose deploymentsI would recommend treating deployment as a controlled state machine:
For recovery, inspect the actual running containers, image digests, service health and database state. Do not assume that the source checkout represents the running application version. Rollback should use a known-good deployment configuration and compatible images. Persistent volumes and database data must remain untouched. Database migrations should also be designed to support safe rollback or forward recovery. Most importantly, the incomplete marker should only be cleared after a recovery or reconciliation procedure confirms the actual system state. It should not be removed simply to allow another deployment. Additional recommendationI would also make the custom code-only guard report the exact failed condition rather than a generic failure message. Log the check identifier and sanitized expected-versus-actual values. The compatibility fingerprint mismatch should remain a separate preflight failure. It should not be bypassed as part of recovering the original deployment. This approach preserves the original failure information, makes the SSH output issue independently testable, and avoids accidentally turning a partially completed deployment into a data-loss incident. |
|
One extra recovery test: exercise the missing-image case. In the isolated fixture, remove the local baseline image and make the registry unavailable before recovery. An image ID or digest in the journal identifies the baseline, but does not make its bytes available. If there is no verified copy, recovery should leave the interlock in place and report which image is missing, rather than falling back to the current tag. Then restore the exact baseline image from a retained copy and rerun recovery. This tests whether the rollback dependency is actually available, not just whether the journal has the right identifiers. |
|
Based on the symptoms, I would investigate the remote deployment failure separately from the EOF output-capture error. 1. Preserving the original SSH error Yes, the I recommend:
Do not assume the EOF message is the root cause until a minimal reproduction confirms it. 2. Minimal reproduction Create a separate test repository with:
Compare the remote exit status, stdout, stderr, and GitHub Actions logs. This should help isolate an action-level output issue from custom deployment-script behavior. 3. Safe Docker Compose recovery I recommend implementing a deployment state machine rather than treating deployment as a single shell operation. Suggested states:
Any failure should transition to:
Before another deployment is allowed:
Avoid automatically running One additional recommendation: Keep source checkout, deployed image digest, and running container version as separate records. A successful Git checkout does not prove that the corresponding application version is running. I would also investigate issue #375 against the exact action version in use before changing the deployment logic. The safest approach is to reproduce the failure in an isolated environment, identify the original failing command, and test recovery independently before considering any production deployment. |
Uh oh!
There was an error while loading. Please reload this page.
🏷️ Discussion Type
Question
💬 Feature/Topic Area
Workflow Deployment
Discussion Details
Hello GitHub Community,
We deploy a self-hosted business application using GitHub Actions, appleboy/ssh-action, and Docker Compose.
The existing website remains accessible, but a deployment failed with these messages, in this order:
Code-only guard failed; stop and review. No full-deploy fallback.
Staged deployment incomplete
Operation incomplete; persistent interlock retained
A subsequent error was:
Invalid value. Matching delimiter not found 'EOF'
Important context:
Action fails to set stdout when one of the commands has non 0 exit code appleboy/ssh-action#375
We would appreciate advice on:
Could the EOF message be a secondary output-capture error after the original remote command failure? How should the workflow preserve the original error and exit status reliably?
What minimal, credential-free reproduction would help distinguish an ssh-action output issue from a failure in our custom deployment script?
How should we design and test recovery for a partially completed Docker Compose deployment, preserving data and recording the outcome before allowing another deployment?
We are seeking source-level review and isolated testing guidance. We do not want to remove the persistent marker blindly, weaken compatibility checks, or force a production deployment.
We can provide sanitized workflow excerpts and a minimal reproduction if needed. No credentials or production business data will be shared.
Thank you.
All reactions