Conversation
- 📦 deps(npm): pin devDependencies per renovate policy — biome 2.5.7, turbo 2.10.8, @types/react 19.2.18, @types/node 26.1.2, postcss 8.5.25, typescript 7.0.2, @types/react-dom 19.2.4 - 📦 deps(npm): regenerate package-lock.json from scratch (lock file maintenance) - 🐛 fix(docker): bump wolfi-base digest in Dockerfile.release, missed when Dockerfile got the same bump - 🔧 config(biome): migrate config to the 2.5.7 schema; replace the migrator's preset "none" output (which silently disabled every rule) with preset "recommended"
- chore(commits): 11 plain types, no emoji; hook + CI validator updated - fix(ci): release-cut classifies bumps via the full commit header grammar - ci: PR commit check delegates to scripts/validate-commit-msg.sh against the PR base - style(scripts): shfmt-clean validator - docs: convention updated in CONTRIBUTING/AGENTS/RELEASING; BREAKING CHANGE footer caveat
…eps (#85) The lockfile only carried resolved entries for darwin-arm64 platform binaries (npm/cli#4828), so npm ci on Linux builders could not install lightningcss-linux-x64-gnu and every Vercel build failed at the Tailwind step. Full regen restores the complete platform matrix; 23 transitive deps moved within their existing ranges. Verified with fresh npm ci + website build.
* docs(changelog): record v0.9.3 changes * fix(ci): make the release-cut CHANGELOG non-empty check portable mawk, the default awk on the ubuntu runners, has no Perl character classes and reads \S as a literal S. The non-empty check therefore passed only for entries that happened to contain a capital S. The v0.9.3 entry does not, so the cut would have hard-failed on a changelog section that is plainly not empty.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe pull request migrates commit validation and documentation from emoji-prefixed messages to standard Conventional Commits. CI validates commits against the pull request base branch. Release parsing supports valid headers and legacy leading emojis. Changelog filters and validation were updated. The release image digest, Biome configuration, and package version specifications were also updated. Possibly related PRs
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 255-261: Update the commit-validation workflow around the git log
process substitution to resolve and validate origin/${BASE_REF} before
iterating. Write the output of git log --format=%s to a temporary file, fail the
step if that command cannot resolve the ref, then feed the file into the
existing validation loop while preserving its warning and failed-flag behavior.
In `@AGENTS.md`:
- Line 57: Update the Commits guidance in AGENTS.md to state that scope is
optional, showing both accepted formats: <type>: <description> and
<type>(scope): <description>. Keep the existing Conventional Commits and
validation references unchanged.
In `@scripts/validate-commit-msg.sh`:
- Around line 50-52: Update the validation pipeline inside the ctype loop to
avoid grep’s early pipe closure: replace grep -qE with grep -E redirected to
/dev/null, or use a Bash regular-expression match, while preserving the existing
commit-subject pattern and success behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: cf13dc2a-ca76-46f7-b5c2-f524cd26ef07
⛔ Files ignored due to path filters (2)
CHANGELOG.mdis excluded by!CHANGELOG.mdpackage-lock.jsonis excluded by!**/package-lock.json,!**/package-lock.json
📒 Files selected for processing (13)
.github/workflows/ci.yml.github/workflows/release-cut.yml.goreleaser.ymlAGENTS.mdCONTRIBUTING.mdDockerfile.releaseRELEASING.mdbiome.jsondocs/package.jsonlefthook.ymlpackage.jsonscripts/validate-commit-msg.shwebsite/package.json
| while IFS= read -r subject; do | ||
| printf '%s\n' "${subject}" > "${tmp}" | ||
| if ! bash scripts/validate-commit-msg.sh "${tmp}" >/dev/null 2>&1; then | ||
| echo "::warning::Non-conventional commit: ${subject}" | ||
| failed=1 | ||
| fi | ||
| done < <(git log --format=%s origin/main..HEAD) | ||
| done < <(git log --format=%s "origin/${BASE_REF}..HEAD") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
: "${BASE_REF:?Set BASE_REF to the pull request base branch}"
base_ref="origin/${BASE_REF}"
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
git log --format=%s "${base_ref}..HEAD" >/dev/nullRepository: CodesWhat/portwing
Length of output: 225
🏁 Script executed:
#!/usr/bin/env bash
set -u
printf '%s\n' '--- workflow context ---'
sed -n '220,275p' .github/workflows/ci.yml
printf '%s\n' '--- BASE_REF and git setup references ---'
rg -n -C 3 'BASE_REF|origin/|fetch-depth|git fetch|set -e|shell:' .github/workflows/ci.yml
printf '%s\n' '--- isolated process-substitution status probe ---'
probe_dir="$(mktemp -d)"
trap 'rm -rf "$probe_dir"' EXIT
git -C "$probe_dir" init -q
git -C "$probe_dir" config user.email test@example.invalid
git -C "$probe_dir" config user.name test
printf 'x\n' > "$probe_dir/file"
git -C "$probe_dir" add file
git -C "$probe_dir" commit -qm initial
cat > "$probe_dir/probe.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
failed=0
count=0
while IFS= read -r subject; do
count=$((count + 1))
done < <(git log --format=%s "origin/does-not-exist..HEAD")
printf 'loop_count=%s failed=%s status=success\n' "$count" "$failed"
EOF
chmod +x "$probe_dir/probe.sh"
(
cd "$probe_dir"
if ./probe.sh; then
printf 'probe_exit=0\n'
else
printf 'probe_exit=%s\n' "$?"
fi
)Repository: CodesWhat/portwing
Length of output: 3474
🏁 Script executed:
#!/usr/bin/env bash
set -u
printf '%s\n' '--- process substitution with a delayed failing producer ---'
test_dir="$(mktemp -d)"
trap 'rm -rf "$test_dir"' EXIT
git -C "$test_dir" init -q
git -C "$test_dir" config user.email test@example.invalid
git -C "$test_dir" config user.name test
printf 'x\n' > "$test_dir/file"
git -C "$test_dir" add file
git -C "$test_dir" commit -qm initial
cat > "$test_dir/behavior.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
count=0
while IFS= read -r subject; do
count=$((count + 1))
done < <(sleep 0.2; git log --format=%s "origin/does-not-exist..HEAD")
printf 'reached_after_loop count=%s\n' "$count"
EOF
chmod +x "$test_dir/behavior.sh"
(
cd "$test_dir"
if ./behavior.sh; then
printf 'delayed_probe_exit=0\n'
else
printf 'delayed_probe_exit=%s\n' "$?"
fi
)
printf '%s\n' '--- process substitution with producer failure suppressed ---'
(
cd "$test_dir"
if bash -c '
set -euo pipefail
count=0
while IFS= read -r subject; do count=$((count + 1)); done < <(
git log --format=%s "origin/does-not-exist..HEAD" || true
)
printf "reached_after_loop count=%s\n" "$count"
'; then
printf 'suppressed_probe_exit=0\n'
else
printf 'suppressed_probe_exit=%s\n' "$?"
fi
)
printf '%s\n' '--- fail-closed ref check ---'
(
cd "$test_dir"
if bash -c '
set -euo pipefail
base_ref=origin/does-not-exist
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
git log --format=%s "${base_ref}..HEAD" > subjects.tmp
'; then
printf 'fail_closed_probe_exit=0\n'
else
printf 'fail_closed_probe_exit=%s\n' "$?"
fi
)Repository: CodesWhat/portwing
Length of output: 734
Fail closed when origin/${BASE_REF} cannot be resolved.
set -euo pipefail does not propagate the git log status from process substitution. A failed git log can leave the loop with no subjects and failed=0. Validate the ref and write the log to a temporary file before the loop.
Proposed fix
+ subjects_tmp="$(mktemp)"
+ trap 'rm -f "${tmp}" "${subjects_tmp}"' EXIT
+ base_ref="origin/${BASE_REF}"
+ git rev-parse --verify "${base_ref}^{commit}" >/dev/null
+ git log --format=%s "${base_ref}..HEAD" > "${subjects_tmp}"
while IFS= read -r subject; do
...
- done < <(git log --format=%s "origin/${BASE_REF}..HEAD")
+ done < "${subjects_tmp}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| while IFS= read -r subject; do | |
| printf '%s\n' "${subject}" > "${tmp}" | |
| if ! bash scripts/validate-commit-msg.sh "${tmp}" >/dev/null 2>&1; then | |
| echo "::warning::Non-conventional commit: ${subject}" | |
| failed=1 | |
| fi | |
| done < <(git log --format=%s origin/main..HEAD) | |
| done < <(git log --format=%s "origin/${BASE_REF}..HEAD") | |
| subjects_tmp="$(mktemp)" | |
| trap 'rm -f "${tmp}" "${subjects_tmp}"' EXIT | |
| base_ref="origin/${BASE_REF}" | |
| git rev-parse --verify "${base_ref}^{commit}" >/dev/null | |
| git log --format=%s "${base_ref}..HEAD" > "${subjects_tmp}" | |
| while IFS= read -r subject; do | |
| printf '%s\n' "${subject}" > "${tmp}" | |
| if ! bash scripts/validate-commit-msg.sh "${tmp}" >/dev/null 2>&1; then | |
| echo "::warning::Non-conventional commit: ${subject}" | |
| failed=1 | |
| fi | |
| done < "${subjects_tmp}" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 255 - 261, Update the
commit-validation workflow around the git log process substitution to resolve
and validate origin/${BASE_REF} before iterating. Write the output of git log
--format=%s to a temporary file, fail the step if that command cannot resolve
the ref, then feed the file into the existing validation loop while preserving
its warning and failed-flag behavior.
| ## Conventions | ||
|
|
||
| - **Commits:** emoji conventional commits — `<emoji> <type>(scope): <description>` (see CONTRIBUTING.md). Enforced by lefthook via `scripts/validate-commit-msg.sh`. | ||
| - **Commits:** plain Conventional Commits, no emoji — `<type>(scope): <description>` (see CONTRIBUTING.md). Enforced by lefthook via `scripts/validate-commit-msg.sh`. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
State that the scope is optional.
Line 57 presents <type>(scope): <description> as the required form. scripts/validate-commit-msg.sh accepts both scoped and unscoped messages, and CONTRIBUTING.md states that scope is optional. Use <type>: <description> or <type>(scope): <description>.
Proposed fix
-- **Commits:** plain Conventional Commits, no emoji — `<type>(scope): <description>` (see CONTRIBUTING.md). Enforced by lefthook via `scripts/validate-commit-msg.sh`.
+- **Commits:** plain Conventional Commits, no emoji — `<type>: <description>` or `<type>(scope): <description>` (see CONTRIBUTING.md). Enforced by lefthook via `scripts/validate-commit-msg.sh`.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - **Commits:** plain Conventional Commits, no emoji — `<type>(scope): <description>` (see CONTRIBUTING.md). Enforced by lefthook via `scripts/validate-commit-msg.sh`. | |
| - **Commits:** plain Conventional Commits, no emoji — `<type>: <description>` or `<type>(scope): <description>` (see CONTRIBUTING.md). Enforced by lefthook via `scripts/validate-commit-msg.sh`. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@AGENTS.md` at line 57, Update the Commits guidance in AGENTS.md to state that
scope is optional, showing both accepted formats: <type>: <description> and
<type>(scope): <description>. Keep the existing Conventional Commits and
validation references unchanged.
| for ctype in "${types[@]}"; do | ||
| if printf '%s\n' "$first_line" | | ||
| grep -qE "^${emoji} ${ctype}(\([A-Za-z0-9._/-]+\))?!?: .+"; then | ||
| grep -qE "^${ctype}(\([A-Za-z0-9._/-]+\))?!?: .+"; then |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
printf '%s\n' '--- scripts/validate-commit-msg.sh ---'
sed -n '1,100p' scripts/validate-commit-msg.sh
printf '%s\n' '--- relevant shell settings and grep usage ---'
rg -n -C 3 'pipefail|grep -qE|first_line|types=' scripts/validate-commit-msg.shRepository: CodesWhat/portwing
Length of output: 3795
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- shell versions ---'
bash --version | head -n 1
grep --version 2>/dev/null | head -n 1 || true
printf '%s\n' '--- single-run pipeline statuses ---'
bash -c '
set -o pipefail
for n in 100 4096 65536 1048576; do
subject=$(printf "feat: %*s" "$n" "")
subject=${subject// /x}
printf "%s " "$n"
set +e
printf "%s\n" "$subject" | grep -qE "^feat(\([A-Za-z0-9._/-]+\))?!?: .+"
rc=$?
ps0=${PIPESTATUS[0]}
ps1=${PIPESTATUS[1]}
set -e
printf "pipeline=%s printf=%s grep=%s\n" "$rc" "$ps0" "$ps1"
done
'
printf '%s\n' '--- repeated long-subject failure counts ---'
bash -c '
set -o pipefail
q=0
for i in $(seq 1 200); do
subject=$(printf "feat: %*s" 1048576 "")
subject=${subject// /x}
set +e
printf "%s\n" "$subject" | grep -qE "^feat(\([A-Za-z0-9._/-]+\))?!?: .+"
rc=$?
ps0=${PIPESTATUS[0]}
set -e
if [ "$rc" -ne 0 ]; then
q=$((q + 1))
printf "failure iteration=%s pipeline=%s printf=%s\n" "$i" "$rc" "$ps0"
[ "$q" -ge 3 ] && break
fi
done
printf "failures=%s\n" "$q"
'
printf '%s\n' '--- comparison with grep consuming the stream ---'
bash -c '
set -o pipefail
for n in 65536 1048576; do
subject=$(printf "feat: %*s" "$n" "")
subject=${subject// /x}
set +e
printf "%s\n" "$subject" | grep -E "^feat(\([A-Za-z0-9._/-]+\))?!?: .+" >/dev/null
rc=$?
ps0=${PIPESTATUS[0]}
ps1=${PIPESTATUS[1]}
set -e
printf "%s pipeline=%s printf=%s grep=%s\n" "$n" "$rc" "$ps0" "$ps1"
done
'Repository: CodesWhat/portwing
Length of output: 634
🏁 Script executed:
#!/bin/bash
set -u
bash -c '
set -o pipefail
pattern="^feat(\([A-Za-z0-9._/-]+\))?!?: .+"
for n in 100 4096 65536 1048576; do
body=$(printf "%*s" "$n" "")
body=${body// /x}
subject="feat: $body"
set +e
printf "%s\n" "$subject" | grep -qE "$pattern"
ps=("${PIPESTATUS[@]}")
set -e
if [ "${ps[0]}" -ne 0 ] || [ "${ps[1]}" -ne 0 ]; then
pipeline=1
else
pipeline=0
fi
printf "q length=%s pipeline=%s printf=%s grep=%s\n" \
"$n" "$pipeline" "${ps[0]}" "${ps[1]}"
set +e
printf "%s\n" "$subject" | grep -E "$pattern" >/dev/null
ps=("${PIPESTATUS[@]}")
set -e
if [ "${ps[0]}" -ne 0 ] || [ "${ps[1]}" -ne 0 ]; then
pipeline=1
else
pipeline=0
fi
printf "full length=%s pipeline=%s printf=%s grep=%s\n" \
"$n" "$pipeline" "${ps[0]}" "${ps[1]}"
done
failures=0
for i in 1 200; do
body=$(printf "%*s" 1048576 "")
body=${body// /x}
subject="feat: $body"
set +e
printf "%s\n" "$subject" | grep -qE "$pattern"
ps=("${PIPESTATUS[@]}")
set -e
if [ "${ps[0]}" -ne 0 ] || [ "${ps[1]}" -ne 0 ]; then
failures=$((failures + 1))
printf "failure iteration=%s printf=%s grep=%s\n" \
"$i" "${ps[0]}" "${ps[1]}"
[ "$failures" -ge 3 ] && break
fi
done
printf "q_failures=%s\n" "$failures"
'Repository: CodesWhat/portwing
Length of output: 515
🏁 Script executed:
#!/bin/bash
set -u
printf '%s\n' '--- available tracing and shells ---'
command -v strace || true
find /bin /usr/bin -maxdepth 1 -type f \( -name 'bash*' -o -name 'dash' -o -name 'ksh*' \) -print 2>/dev/null | sort || true
printf '%s\n' '--- larger valid subjects with Bash builtin printf ---'
bash -c '
set -o pipefail
pattern="^feat(\([A-Za-z0-9._/-]+\))?!?: .+"
for n in 4194304 16777216 67108864; do
body=$(printf "%*s" "$n" "")
body=${body// /x}
subject="feat: $body"
set +e
printf "%s\n" "$subject" | grep -qE "$pattern"
ps=("${PIPESTATUS[@]}")
set -e
if [ "${ps[0]}" -ne 0 ] || [ "${ps[1]}" -ne 0 ]; then pipeline=1; else pipeline=0; fi
printf "length=%s pipeline=%s printf=%s grep=%s\n" \
"$n" "$pipeline" "${ps[0]}" "${ps[1]}"
done
'
printf '%s\n' '--- external writer comparison ---'
python3 - <<'PY'
import os
import subprocess
pattern = r"^feat(\([A-Za-z0-9._/-]+\))?!?: .+"
for size in (65536, 1048576, 16777216):
code = r'''
import os, sys
data = b"feat: " + b"x" * SIZE + b"\n"
os.write(sys.stdout.fileno(), data)
'''
code = code.replace("SIZE", str(size))
p = subprocess.run(
["bash", "-c", "set -o pipefail; python3 -c \"$1\" | grep -qE \"$2\"", "--", code, pattern],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
print(f"external_writer length={size} pipeline={p.returncode}")
PYRepository: CodesWhat/portwing
Length of output: 148
🌐 Web query:
Bash builtin printf pipefail grep -q SIGPIPE long line
💡 Result:
When using the Bash builtin printf in a pipeline (e.g., printf ... | grep -q ...) with set -o pipefail enabled, you may encounter an intermittent failure where the pipeline exits with a non-zero status, even if the grep command successfully finds a match [1][2]. The root cause is a race condition: 1. grep -q is an "early-exit" command [2]. As soon as it finds a match, it closes its input pipe and exits with status 0 [1][2]. 2. If printf is still attempting to write the remainder of its output, it writes to a pipe that has been closed by grep [1][2]. 3. The operating system sends a SIGPIPE signal to the process writing the data (printf) [1][2]. 4. By default, a process that receives SIGPIPE terminates with a non-zero exit status (typically 141) [3][4][5]. 5. Because set -o pipefail is active, Bash sees this non-zero exit status from printf and marks the entire pipeline as failed, even if grep succeeded [1][2]. This behavior is particularly common with large amounts of data or "long lines" because the larger volume of data makes it more likely that printf will still be writing when grep exits [1][6]. Recommended solutions: - Disable pipefail for the specific pipeline where this occurs by running set +o pipefail locally [2][7]. - If you must use pipefail, you can ignore the specific error code caused by SIGPIPE (141) after the pipeline executes, or ensure the writer handles SIGPIPE gracefully [3][5]. - A robust, non-timing-dependent approach is preferred over trying to "fix" the race condition [1]. For example, instead of relying on early exit within a pipeline, you might process the data in a way that ensures the writer is not dependent on the reader's state [1][2]. Note that if your shell environment has SIGPIPE ignored (SIG_IGN), the write operation will instead return an EPIPE error, which can also trigger the printf builtin to report a write error and cause the pipeline to fail under pipefail [3][8][9].
Citations:
- 1: https://stackoverflow.com/questions/77843350/bash-surprising-behavior-of-fc-list-grep-q-with-pipefail
- 2: https://mywiki.wooledge.org/BashPitfalls
- 3: https://blog.shukebeta.com/2026/08/02/printf-write-error-broken-pipe-on-a-self-hosted-github-runner-check-sigpipe/
- 4: https://www.pixelbeat.org/programming/sigpipe_handling.html
- 5: https://unix.stackexchange.com/questions/732503/how-to-find-out-if-pipe-is-broken
- 6:
pipefailandgrep -qwont get along well koalaman/shellcheck#665 - 7: https://stackoverflow.com/questions/76750683/bash-pipe-to-grep-in-if-statement-return-value
- 8: https://public-inbox.org/bug-gnulib/4942F854.9000107@case.edu/T/
- 9: https://stackoverflow.com/questions/75762893/how-can-i-automatically-suppress-this-broken-pipe-error-when-piping-through-head
🏁 Script executed:
#!/bin/bash
set -u
bash -c '
set -o pipefail
pattern="^feat(\([A-Za-z0-9._/-]+\))?!?: .+"
for n in 1048576 4194304 16777216 33554432; do
subject=$(python3 -c "import sys; sys.stdout.write(\"feat: \" + \"x\" * int(sys.argv[1]))" "$n")
set +e
printf "%s\n" "$subject" | grep -qE "$pattern"
ps=("${PIPESTATUS[@]}")
set -e
if [ "${ps[0]}" -ne 0 ] || [ "${ps[1]}" -ne 0 ]; then pipeline=1; else pipeline=0; fi
printf "q length=%s pipeline=%s printf=%s grep=%s\n" \
"$n" "$pipeline" "${ps[0]}" "${ps[1]}"
set +e
printf "%s\n" "$subject" | grep -E "$pattern" >/dev/null
ps=("${PIPESTATUS[@]}")
set -e
if [ "${ps[0]}" -ne 0 ] || [ "${ps[1]}" -ne 0 ]; then pipeline=1; else pipeline=0; fi
printf "full length=%s pipeline=%s printf=%s grep=%s\n" \
"$n" "$pipeline" "${ps[0]}" "${ps[1]}"
done
'Repository: CodesWhat/portwing
Length of output: 524
🌐 Web query:
Bash 3.2 builtin printf grep -q pipefail SIGPIPE
💡 Result:
In Bash 3.2, interactions between builtins like printf, external commands like grep, and the pipefail option are heavily influenced by how the shell handles SIGPIPE and write errors [1][2]. Key technical aspects include: 1. Pipefail Behavior: When set -o pipefail is enabled, the exit status of a pipeline is the status of the last (rightmost) command that exited with a non-zero status [3]. If a command in the middle of a pipeline exits due to SIGPIPE (a common occurrence when a downstream command like grep -q terminates early), that command's non-zero status will cause the entire pipeline to be considered failed [4][5][6]. 2. SIGPIPE and Early Termination: Commands like grep -q terminate as soon as they find a match, closing their end of the pipe [5][6]. If the upstream command (e.g., printf, echo, or other data producers) is still attempting to write, it receives a SIGPIPE signal [5]. Because SIGPIPE is a terminating signal, the process exits with a non-zero status (typically 141), which triggers the failure behavior in a pipefail-enabled pipeline [4][5]. 3. Bash 3.2 Specific Changes: Bash 3.2 introduced significant changes that made these interactions more visible [1][2]. Specifically, the shell began reporting write errors for builtins like echo and printf that were previously silent [1][2]. In older versions, these errors might not have been reported to the user, but in 3.2, if the shell catches a terminating signal like SIGPIPE, it may report the write error explicitly [1][2]. This behavior was later refined in Bash 4.0 to improve how output builtins handle these situations [1]. 4. Race Conditions: These issues are frequently timing-dependent (race conditions) [4][5][6]. If an upstream producer finishes writing all its data before the downstream consumer exits, no SIGPIPE occurs, and the pipeline may succeed despite the use of pipefail [4][5]. If the output is large or the producer is slow, the downstream consumer will exit first, resulting in a SIGPIPE and a pipeline failure [5][6]. Because of this, combining pipefail with commands that terminate early (like grep -q or head) is widely documented as prone to inconsistent results in Bash [5][6][7].
Citations:
- 1: https://lists.endsoftwarepatents.org/archive/html/bug-bash/2008-12/msg00050.html
- 2: https://public-inbox.org/bug-gnulib/4942F854.9000107@case.edu/T/
- 3: https://man7.org/linux/man-pages/man1/bash.1.html
- 4: https://unix.stackexchange.com/questions/774908/sigpipe-and-bash-pipefail
- 5: https://stackoverflow.com/questions/77843350/bash-surprising-behavior-of-fc-list-grep-q-with-pipefail
- 6:
pipefailandgrep -qwont get along well koalaman/shellcheck#665 - 7: https://www.pixelbeat.org/programming/sigpipe_handling.html
Replace grep -qE in the pipefail pipeline.
When a valid subject is long enough, grep -qE can close the pipe while printf is still writing. Bash 3.2 can report SIGPIPE, and pipefail then rejects the commit. Use grep -E ... >/dev/null or a Bash regular expression.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/validate-commit-msg.sh` around lines 50 - 52, Update the validation
pipeline inside the ctype loop to avoid grep’s early pipe closure: replace grep
-qE with grep -E redirected to /dev/null, or use a Bash regular-expression
match, while preserving the existing commit-subject pattern and success
behavior.
biggest-littlest
left a comment
There was a problem hiding this comment.
Forward-ports the OpenSSF assurance evidence from main and syncs dev/v0.9 for the v0.9.3 cut.
ALARGECOMPANY
left a comment
There was a problem hiding this comment.
Sync looks right: main's evidence files survive the merge and the changelog entry is in place.
Brings
mainlevel withdev/v0.9ahead of the v0.9.3 cut.Three commits: the Conventional Commits migration (#84), the lockfile regeneration that restores the cross-platform optional-dependency matrix (#85), and the v0.9.3 changelog entry plus a release-cut gate fix (#86).
Merging this as a merge commit, matching #83. Squashing would flatten the individual subjects that
release-cut's bump math reads. Verified overv0.9.2..dev/v0.9: nofeatand no!, so the bump computespatchand the cut lands on v0.9.3. The version input will be left blank so the math decides.The gate fix in #86 matters for this cut.
release-cut's "CHANGELOG entry is non-empty" check usedawk '/\S/', and mawk (the default awk on the ubuntu runners) has no Perl character classes, so it read\Sas a literalS. The check only ever passed for entries that happened to contain a capital S. The v0.9.2 section has five; the v0.9.3 section has none, so the cut would have hard-failed on a section that is plainly not empty. Replaced with[^[:space:]].Changelog
release-cutCHANGELOG validation.Concerns