Today's theme is failures that report themselves somewhere other than where they happened. A Git hook with the wrong permissions does nothing and says nothing. A heredoc with a trailing space produces an error about a missing zip file, three commands downstream.
One Git task, one AWS task. Install a post-update hook that tags every push to master, then deploy a Python Lambda from the CLI. The tasks come from the KodeKloud Engineer platform.
This is also the last Git task in the track. Days 21 to 34 covered the lot: repository setup, forks, branches, merges, remotes, revert, cherry-pick, pull requests, hard reset, stash, rebase, conflicts, and now hooks.
The hook that runs after it is too late to object
The task was to make every push to master create a release tag automatically. That work belongs on the server, in the bare repository:
cd /opt/apps.git/hooks
vi post-update
#!/bin/bash
for ref in "$@"
do
if [ "$ref" = "refs/heads/master" ]; then
TODAY=$(date +%F)
COMMIT=$(git rev-parse refs/heads/master)
git tag "release-$TODAY" "$COMMIT" 2>/dev/null
fi
done
chmod +x post-update
post-update receives the name of every ref that was just updated, one per argument, which is what "$@" is walking. Two details in that script are worth more than they look. git rev-parse needs no repository path because GIT_DIR is already set when a hook runs. And redirecting the tag error to /dev/null makes the hook idempotent for the day: the second push leaves the existing tag alone instead of failing loudly.
Then the part that decides whether any of this is a good idea. Git's documentation says post-update is meant primarily for notification and cannot affect the outcome of git receive-pack. It runs after the refs have already moved. Exit non-zero, and the push still succeeds.
So if you want a hook that rejects bad pushes, this is the wrong hook. pre-receive and update run before refs move and can fail the push. post-receive and post-update run afterwards and cannot. Pick the wrong one, and you have written a validation rule that logs its objection into the void while the push lands anyway.
And the quiet one:
chmod +x post-update
ls -la | grep post-update
A hook file with perfect contents and no execute bit is skipped in silence. Nothing in the push output mentions it. This is also why Git ships its samples with a .sample suffix, and why hooks never travel: they live in the repository directory, and neither clone nor push carries them. A hook that has to run on the server has to be put on the server.
One last thing, because it looks like the hook failed when it did not:
git fetch --tags
git tag
# release-2026-08-23
The tag was created on the server. Your clone does not know about it until you ask.
The trailing space
The AWS half was Day 33's Lambda again, with a different name and a pre-existing role.
Two useful things before the mistake. aws iam get-role before create-role, because this lab pre-provisions the role and create-role is not idempotent, so a blind create returns EntityAlreadyExists and aborts a chained script. Read before write when a resource might already exist. Worth knowing that attach-role-policy is idempotent, so re-attaching a policy is safe.
Now the mistake:
cat > /root/lambda-build/lambda_function.py <<'EOF'
def lambda_handler(event, context):
...
EOF
There is a space after that final EOF. The shell never recognises its terminator, keeps reading, and drops the prompt to >. Ctrl+C out and the file was never written.
Three commands later:
Unable to load paramfile fileb:///root/function.zip: [Errno 2] No such file or directory
The zip step had been skipped because the source did not exist, so the zip did not exist, so create-function failed. The error names a missing archive. The cause is an invisible character in a command that appeared to succeed three steps earlier.
The fix I have adopted since is to stop pasting heredocs for short files:
printf '%s\n' \
'def lambda_handler(event, context):' \
' return {' \
' "statusCode": 200,' \
' "body": "Welcome to KKE AWS Labs!"' \
' }' \
> /root/lambda-build/lambda_function.py
Each argument becomes a line, single quotes prevent expansion, and there is no terminator to get wrong. Then cat the file rather than assuming.
The same principle applies to the artifact:
cd /root/lambda-build && zip -q /root/function.zip lambda_function.py && unzip -l /root/function.zip
Length Date Time Name
125 2026-08-25 07:44 lambda_function.py
The name column has no directory prefix, which is the only thing that matters. lambda-build/lambda_function.py inside the archive would deploy fine and fail at invoke time with an import error. Check the artifact, not the exit code of the tool that made it.
And one habit worth stealing outright:
aws lambda wait function-active --region us-east-1 --function-name datacenter-lambda-cli
create-function returns with "State": "Pending" and invoking a pending function fails. There is a waiter for that, and there are waiters for far more things than most people expect: ec2 wait instance-running, rds wait db-instance-available, cloudformation wait stack-create-complete. Any sleep 30 in a script is worth one search.
Look upstream of the error
The hook and the heredoc are the same problem wearing different clothes. In both cases, the thing that broke was silent, and the thing that spoke up was downstream of it and blameless.
The habit that helps is separating "this command exited zero" from "this command did what I wanted", and checking the second one deliberately: ls -la on the hook, unzip -l on the archive, git fetch --tags before concluding the tag was never made.
So here is the Day 34 question. When something in your pipeline fails, do you check the step that reported the error, or the last step that anyone actually verified?
Day 34 down. Sixty-six to go.
Top comments (0)