Repository navigation
Read the Xcode version without a pipe, and guard the shape that aborted it - #499
Merged
Merged
Conversation
`have="$(xcodebuild -version | head -n 1)"` killed the release run on 2ca67ad. `head` exits once it has the line it wanted, the next write finds the pipe closed, and `xcodebuild` answers that by raising `NSFileHandleOperationException`, printing a backtrace and aborting with 134. `pipefail` makes that the step's status, so "Select Xcode 26 or newer" failed before it had selected anything, the macOS and iOS builds never ran, and `Assert the release published` failed behind them. Android, Linux and Windows were green on that same push. It is a race, not a constant break, and it had passed six times before it fired - every push since the step shipped in #490. Counting green runs understates that: the step also passed on d10fe8a and 2bfa773, whose runs failed later at `Build macOS release` on a stale committed Podfile.lock, which #495 then fixed. Checked by grepping each run log for the step's own closing line; the six print `Xcode 26.6 from /Applications/Xcode_26.6.app, of 14 candidate(s).` and 2ca67ad prints only the echoed script. `xcodebuild -version` prints two lines, and whether its second write lands before `head`'s first read is the runner's business. Taking the first line in the shell removes the pipe instead of hardening around it. No behaviour change otherwise: the six version strings that decided the old `case` decide the new one the same way - 26, 26.1, 26.10.1 accepted, 30.0 and 16.4 rejected - and so do a further twelve inputs, empty output and CRLF among them. This repository warns about the construct in `derive_version.sh`, `check_arb_locales.sh` and `summarize_l10n_failure.sh`, and that is now four warnings and no guard. A six-line grep appended to `lint_workflows.sh:50`, in the shape of the `cond && 0 || 1` guard already there, is green across the workflows at HEAD and red on this step at 2ca67ad. Deliberately not in this commit, which is a five-minute fix to an unpublishable release; it is the obvious next one. Also rewrites the `shell: bash` paragraph, which was wrong before this branch and which fixing a line number would have left wrong. `shell: bash` supplies `-e` and `-o pipefail`, not `-u` - the failed run prints the resolved `/bin/bash --noprofile --norc -e -o pipefail {0}` - and the `-u` comes from the step's own `set` line. Its cross-reference was a bare line number into this file, the only one of its kind here and never once correct: written as `:1014` when the paragraph sat at 1057, found at 1111, and arithmetically 1130 today. It now names the step it means, and the authority is `ci.yml`'s note, which is the one that actually argues for `shell: bash`. With no pipeline left in the step, `pipefail` here is belt-and-braces; what `shell: bash` is now load-bearing for is `$'\n'`, which a POSIX `sh` would not parse. No changelog entry. docs/releasing.md names this case directly - "Most should not - a CI fix, a Crowdin sync, a submodule bump or a test-only branch has nothing a user would read" - and #495 is the precedent beside it. Nothing here ran on a macOS runner. `actionlint` does run on every pull request through `lint_workflows.sh`, so the YAML is checked, but shellcheck is absent from the runner and skipped, and no job on a pull request compiles iOS or macOS - the step's own last paragraph has said so since it was written. The first evidence is the next push to main.
The previous commit removed one `| head -n 1` and left a fourth prose warning about the shape. ADR 0004's argument is that a recurring mistake earns a ratchet instead, so this is the ratchet plus the rest of the instances. The guard goes in `lint_workflows.sh`, beside the `cond && 0 || 1` grep added for #292 - also added because nothing but a publish run could find that mistake. Same shape, same file, no new script and no new ci.yml step, so it runs on every pull request through the `actionlint` step that is already there. It reads the workflows, the composite actions and `.github/scripts`, and it catches more than one spelling: `| head`, `| grep -m`, and `| sed -n 1q`. Checked against a fixture of all four real pre-fix lines and three other early-exit spellings - all seven caught - and against prose quoting the forbidden shape, a `| cut | paste` chain, the correct `sed -n '1,60p'`, the `${v%%$'\n'*}` idiom, and a line of code whose trailing comment mentions `| head`: none of those match. `^[^#]*` is what keeps it off the prose, including the comment directly above it. Its bound is stated where it is written: it is textual, so a `while read … break` is the same trap with no keyword to grep, and the comment says to add a case rather than an exemption. The five remaining instances, all now pipe-free: - `build/{android,ios,linux,macos}.sh` each took the version from pubspec with `sed … | head -n 1`. The producer is a `sed` over a small file, so this was latent, not live - `derive_version.sh` calls it "a silent exit waiting for a longer file". Verified the extraction is unchanged against the real pubspec and against a synthetic one carrying build metadata and a second `version:` line: `0.17.3` and `1.2.3` either way. - `upload_r8_mapping.sh`'s `read_slug` captured through a herestring and then piped to `head`. Now captures and narrows in the shell. - `sentry_proguard_uuid.sh` ended `unzip | sed | head -n 1 || true`. The `|| true` stays, because `unzip -p` exits non-zero on an absent entry and that is one of the two absences this function answers with silence - but its comment claimed the `|| true` was there for the SIGPIPE, which is no longer a reason, so it now says which of the two it is. Gates: `upload_r8_mapping_test.sh`, `verify_apks_test.sh` and `derive_version_test.sh` all pass - the first two cover the two files with behaviour in them. `bash -n` on all seven. `check_format.sh` clean, `check_changelog.sh` OK, and the guard itself is green across `.github`. No changelog entry, for the same reason as the commit before it. Not verified on a runner, and the guard is the part that cannot be: it runs in `lint_workflows.sh`, which downloads a checksum-pinned `actionlint` before reaching it, so the first full execution is this pull request's own `lint-workflows` job. The grep was run directly here instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
have="$(xcodebuild -version | head -n 1)"killed the release run on2ca67ade.headexits once it has the line it wanted, the next write findsthe pipe closed, and
xcodebuildanswers that by raisingNSFileHandleOperationException, printing a backtrace and aborting with 134.pipefailmakes that the step's status, so Select Xcode 26 or newer failedbefore it had selected anything, the macOS and iOS builds never ran, and
Assert the release publishedfailed behind them. Android, Linux and Windowswere green on that same push, so the win32 chain merged a push earlier does
have three platforms' worth of evidence - just not Apple's.
Two commits: the fix, then the ratchet and the five other instances of the
same shape.
The fix
It is a race, and that is the argument for deleting the pipe rather than
hardening around it. It had passed six times before it fired - every push
since the step shipped in #490. Counting green runs understates it: the step
also passed on
d10fe8aband2bfa773c, whose runs failed later atBuild macOS releaseon a stale committedPodfile.lock, which #495 thenfixed. Checked by grepping each run log for the step's own closing line - the
six print
Xcode 26.6 from /Applications/Xcode_26.6.app, of 14 candidate(s).and
2ca67adeprints only the echoed script.xcodebuild -versionprints two lines, and whether its second write landsbefore
head's first read is the runner's business, not ours.No behaviour change otherwise. The six version strings that decided the old
casedecide the new one identically - 26, 26.1, 26.10.1 accepted, 30.0 and16.4 rejected - and so do a further twelve inputs, empty output and CRLF among
them.
The ratchet, because this was the fourth warning and the first cost
The repository had already warned about the shape in prose four times -
derive_version.sh,check_arb_locales.sh,summarize_l10n_failure.shandthe step itself. ADR 0004's argument is that a recurring mistake earns a
ratchet, so the second commit adds one and retires the rest of the instances.
The guard sits in
lint_workflows.shbeside thecond && 0 || 1grep addedfor #292 - also added because nothing but a publish run could find that
mistake. Same shape, same file, no new script and no new
ci.ymlstep, so itruns on every pull request through the
actionlintstep already there.It reads the workflows, the composite actions and
.github/scripts, andcatches
| head,| grep -mand| sed -n 1q. Measured against a fixture:all four real pre-fix lines and three other early-exit spellings are caught;
prose quoting the forbidden shape, a
| cut | pastechain, the correctsed -n '1,60p', the${v%%$'\n'*}idiom, and code whose trailing commentmentions
| headare not.^[^#]*is what keeps it off the prose, includingthe comment directly above it.
Its bound is stated where it is written: it is textual, so a
while read … breakis the same trap with no keyword to grep, and the commentsays to add a case rather than an exemption.
The five siblings, now pipe-free
build/{android,ios,linux,macos}.sheach read the version from pubspecthrough
sed … | head -n 1. Latent rather than live - the producer is asedover a small file - butderive_version.shcalls exactly that "asilent exit waiting for a longer file". Extraction verified unchanged
against the real pubspec and a synthetic one with build metadata and a
second
version:line:0.17.3and1.2.3either way.upload_r8_mapping.sh'sread_slugcaptured through a herestring andpiped to
head. Now captures and narrows in the shell.sentry_proguard_uuid.shendedunzip | sed | head -n 1 || true. The|| truestays -unzip -pexits non-zero on an absent entry, and thatis one of the two absences this function answers with silence - but its
comment claimed the
|| truewas for the SIGPIPE, which is no longer areason, so it now says which of the two it is.
The
shell: bashparagraph was wrong before this branchFixing only its line number would have left the claim wrong, so both are
rewritten.
shell: bashsupplies-eand-o pipefail, not-u- the failed runprints the resolved
/bin/bash --noprofile --norc -e -o pipefail {0}- andthe
-ucomes from the step's ownsetline. Its cross-reference was a bareline number into this same file, the only one of its kind here, and it was
never once correct: written as
`:1014`when the paragraph sat at 1057,found at 1111 on
main, arithmetically 1130 today. It now names the step itmeans, and cites
ci.yml's note, which is the one that actually argues forshell: bash.With no pipeline left anywhere in the step,
pipefailthere isbelt-and-braces. What
shell: bashis now load-bearing for is$'\n', whicha POSIX
shwould not parse - new with this change, so it is stated where afuture editor will see it.
No changelog entry
docs/releasing.mdnames this case directly - "Most should not - a CI fix, aCrowdin sync, a submodule bump or a test-only branch has nothing a user would
read" - and #495 is the precedent beside it.
Gates
upload_r8_mapping_test.sh,verify_apks_test.shandderive_version_test.shpass; the first two cover the two edited files thathave behaviour in them.
bash -non all seven scripts,check_format.shclean,
check_changelog.shOK, the workflow YAML parses, and the guard isgreen across
.github.What this does not prove
Nothing here ran on a macOS runner, and the guard is the one part that
could not be run end to end locally:
lint_workflows.shdownloads achecksum-pinned
actionlintbefore reaching it, so its first full executionis this pull request's own
lint-workflowsjob. The grep was run directlyinstead.
actionlintdoes check the YAML on every pull request, but shellcheck isabsent from the runner and skipped, and no job on a pull request compiles iOS
or macOS. The step's own last paragraph has said so since it was written. The
first evidence for the Xcode fix is the next push to
main.Also found, not fixed here
brew install automake libtoolin the Apple job installs four formulae thatnothing in the tree uses - no
configure.ac,Makefile.amorautogen.shanywhere, and no autotools line in a full release log. About 7s per release
run. Its verification is a release, which is why it is not in a fix to an
unpublishable one.
check_apple_sdk.shwith a stubbedxcodebuildprovably could not havecaught this - a stub's single
printfcompletes beforeheadis scheduled,so the shipped code passes green against it. The extraction buys the glob
shapes, the beta-sorts-first case, the zero-candidate branch and the three
copies of the
2[6-9]bound. Worth doing, on its own merits.actionlint's embedded-bash pass runs would oweactionlint's own pin-and-checksum ceremony, and has no rule forpipefailplus an early-exit consumer - so it would not have caught this either. Its
own issue, on its own motivation.