From 40f6c9e9c0b9408ae41c02677edf8d9037de0290 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Mon, 20 Jul 2026 12:56:16 +0700 Subject: [PATCH 1/2] fix(ci): piping into grep -q is broken under pipefail The aarch64 release build failed its own verification with dpkg-deb: error: tar subprocess was killed by signal (Broken pipe) ::error::.deb does not ship /usr/bin/thinkutils The file was there. grep -q exits the instant it matches, dpkg-deb takes SIGPIPE while writing the rest of the listing, and pipefail propagates that non-zero status -- so FINDING the file is what made the check report it missing. Failing in that direction is the dangerous one: the same race resolving the other way passes a package nobody verified. Timing-dependent, not architecture-specific. x86_64 passed on the same commit; ARM lost the race. It has been latent in release.yml since before this session and would eventually have failed an x86_64 release too. Verified which formulations actually survive, because the obvious fix does not -- capturing the output first and piping THAT into grep -q fails identically, since printf takes the SIGPIPE instead: producer | grep -q PATTERN MISSING (bug) capture, then pipe into grep -q MISSING (bug) grep -q PATTERN <<<"$contents" found producer | grep PATTERN >/dev/null found (no -q, so grep reads to EOF) Uses the herestring form, and lists the .deb once into a variable rather than twice. Applied to the same shape in publish-copr.yml (copr-cli list) and the two in the launch test, where a false negative would have failed a release for no reason. --- .github/workflows/publish-copr.yml | 7 ++++++- .github/workflows/release.yml | 18 ++++++++++++++++-- scripts/test-gui-packages-docker.sh | 4 ++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-copr.yml b/.github/workflows/publish-copr.yml index 1f49717..3cb3265 100644 --- a/.github/workflows/publish-copr.yml +++ b/.github/workflows/publish-copr.yml @@ -114,7 +114,12 @@ jobs: # `copr-cli list` prints "Name: " plus an indented block, not a # bare name, so grepping for the name alone never matches and the script # tries to create a project that already exists. - if ! copr-cli list 2>/dev/null | grep -qE "^Name:[[:space:]]+thinkutils$"; then + # Herestring, not a pipe into grep -q: grep -q exits on first match, + # the upstream command takes SIGPIPE, and pipefail turns a successful + # match into a failed test. That exact shape broke the aarch64 release + # verification, where finding the file reported it as missing. + projects="$(copr-cli list 2>/dev/null || true)" + if ! grep -qE "^Name:[[:space:]]+thinkutils$" <<<"$projects"; then echo ">> creating COPR project thinkutils" # Separate argv entries. "${CHROOTS[@]/#/--chroot }" looks equivalent # but puts the space INSIDE one argument, so copr-cli sees a single diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93301f2..cb4b7b3 100755 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,10 +144,24 @@ jobs: V="${{ steps.version.outputs.version }}" deb="src-tauri/target/release/bundle/deb/thinkutils_${V}_${{ matrix.deb_arch }}.deb" - dpkg-deb --contents "$deb" | grep -q 'usr/bin/thinkutils' \ + # Listed ONCE into a variable, then matched with a herestring. + # + # `dpkg-deb --contents "$deb" | grep -q ...` is broken under pipefail + # and broken in the worst direction: grep -q exits the instant it + # matches, dpkg-deb takes SIGPIPE writing the rest of the listing, and + # pipefail propagates that non-zero status -- so FINDING the file makes + # the check report it missing. It is timing-dependent, which is why it + # passed on x86_64 and failed the aarch64 release with + # "tar subprocess was killed by signal (Broken pipe)". + # + # A herestring has no upstream process to kill. Piping into plain + # `grep` (no -q) also works, because grep then reads to EOF. + contents="$(dpkg-deb --contents "$deb")" + + grep -q 'usr/bin/thinkutils' <<<"$contents" \ || { echo "::error::.deb does not ship /usr/bin/thinkutils"; exit 1; } - dpkg-deb --contents "$deb" | grep -q 'applications/.*\.desktop' \ + grep -q 'applications/.*\.desktop' <<<"$contents" \ || { echo "::error::.deb ships no .desktop entry - the app would not appear in any launcher"; exit 1; } # The binary's real machine type, not the matrix's claim about it. This diff --git a/scripts/test-gui-packages-docker.sh b/scripts/test-gui-packages-docker.sh index eea9805..ce11f81 100755 --- a/scripts/test-gui-packages-docker.sh +++ b/scripts/test-gui-packages-docker.sh @@ -366,7 +366,7 @@ read -r -d '' VERDICT <<'VEOF' || true # counts. Take the first field and compare in awk, which handles 5.4e+10. DIFF="${RAW%% *}" echo "pixels changed vs the pre-launch display: ${RAW}" - if echo "${DIFF}" | grep -qE "^[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?$"; then + if grep -qE "^[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?$" <<<"${DIFF}"; then awk -v d="${DIFF}" "BEGIN{exit !(d>0)}" \ && echo "OK: ${DIFF} pixels changed after launch" \ || { echo "FAIL: display is pixel-identical to before launch"; fail=1; } @@ -413,7 +413,7 @@ read -r -d '' VERDICT <<'VEOF' || true else # The specific, nameable failure: WebKit rendered index.html's own static # text but templateLoader never ran. Every other check above passes here. - if echo "${OCRTXT}" | grep -qiE "quick settings and overview"; then + if grep -qiE "quick settings and overview" <<<"${OCRTXT}"; then echo "FAIL: only index.html's STATIC text is on screen - templateLoader.js" echo " never injected the sidebar or views. WebKit loaded the page;" echo " the JS did not run." From 6cf5f8151cad3aeeb8bc977cd2be3e452d696953 Mon Sep 17 00:00:00 2001 From: Viet Anh Nguyen Date: Mon, 20 Jul 2026 13:01:13 +0700 Subject: [PATCH 2/2] fix(publish): build the tag, and stop misreporting why a tag check failed Two problems, both found by dry-running the workflows before using them. Neither workflow checked out the tag it was asked to publish. checkout had no ref, so it took the default branch and then labelled the result with the requested version. Publishing main's spec, PKGBUILD and manifests under an older tag's version number is wrong on its own; it surfaced here as COPR fetching a v0.2.0 tarball during a run explicitly asked to publish 0.1.10, because main had already been bumped. The tag check used the 'origin' alias, which depends on the checkout's local git config being readable. Inside a container it is not, so: fatal: 'origin' does not appear to be a git repository ::error::v0.1.10 does not exist on origin - push it before publishing The tag existed. The step reported the wrong cause entirely, and would have sent someone looking for a missing tag rather than a git config problem. Fedora's container tolerated it and Arch's did not, so this would have looked arch-specific too. Both now resolve the tag against an explicit repository URL, which needs no local config, and check out that tag. --- .github/workflows/publish-aur.yml | 14 +++++++++++++- .github/workflows/publish-copr.yml | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml index 29df2b8..577fb33 100644 --- a/.github/workflows/publish-aur.yml +++ b/.github/workflows/publish-aur.yml @@ -28,7 +28,13 @@ jobs: set -euo pipefail pacman -Sy --noconfirm --needed base-devel git openssh pacman-contrib jq + # The TAG, not the default branch. Without this the workflow publishes + # whatever main happens to contain while labelling it with the requested + # version -- so a release cut days ago would ship today's spec, PKGBUILD + # and manifests under the old version number. - uses: actions/checkout@v7 + with: + ref: v${{ inputs.version }} # The tag must exist and be pushed BEFORE publishing: PKGBUILD's source # points at the tag tarball, so a queued build would 404 partway through @@ -37,7 +43,13 @@ jobs: run: | set -euo pipefail tag="v${{ inputs.version }}" - git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null || { + # Explicit URL, not the `origin` alias: inside a container the + # checkout's git config is not reliably readable, and `git ls-remote + # origin` then fails with "'origin' does not appear to be a git + # repository" -- which this step reported as "the tag does not exist". + # A misleading error about the wrong thing entirely. + repo="https://github.com/${{ github.repository }}.git" + git ls-remote --exit-code --tags "${repo}" "refs/tags/${tag}" >/dev/null || { echo "::error::${tag} does not exist on origin - push it before publishing" exit 1 } diff --git a/.github/workflows/publish-copr.yml b/.github/workflows/publish-copr.yml index 3cb3265..b141bc5 100644 --- a/.github/workflows/publish-copr.yml +++ b/.github/workflows/publish-copr.yml @@ -29,7 +29,13 @@ jobs: # Needed for the pushed-tag check below. git config --global --add safe.directory '*' + # The TAG, not the default branch. Without this the workflow publishes + # whatever main happens to contain while labelling it with the requested + # version -- so a release cut days ago would ship today's spec, PKGBUILD + # and manifests under the old version number. - uses: actions/checkout@v7 + with: + ref: v${{ inputs.version }} # Source0 points at the tag tarball, so a queued build 404s partway through # if the tag is not pushed. Cheaper to catch here than in the build log. @@ -37,7 +43,13 @@ jobs: run: | set -euo pipefail tag="v${{ inputs.version }}" - git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null || { + # Explicit URL, not the `origin` alias: inside a container the + # checkout's git config is not reliably readable, and `git ls-remote + # origin` then fails with "'origin' does not appear to be a git + # repository" -- which this step reported as "the tag does not exist". + # A misleading error about the wrong thing entirely. + repo="https://github.com/${{ github.repository }}.git" + git ls-remote --exit-code --tags "${repo}" "refs/tags/${tag}" >/dev/null || { echo "::error::${tag} does not exist on origin - push it before publishing" exit 1 }