Skip to content

Stop committing the Podfile locks, which no job here can regenerate - #495

Merged
mishamyte merged 1 commit into
mainfrom
fix/podfile-lock
Nov 12, 2026
Merged

mishamyte merged 1 commit into
mainfrom
fix/podfile-lock

Conversation

@mishamyte

Copy link
Copy Markdown
Member

The dev build on main has failed on macOS and iOS since d10fe8a (#492) —
two runs red, and the merge before that one was the last green release run.
Nothing on a pull request could have caught it.

[!] CocoaPods could not find compatible versions for pod "Firebase/CoreOnly":
      In snapshot (Podfile.lock):
        Firebase/CoreOnly (= 12.15.0, ~> 12.15.0)
      In Podfile:
        firebase_core (from `…/plugins/firebase_core/macos`) was resolved to
        4.15.2, which depends on
          Firebase/CoreOnly (~> 12.19.3)

#492 moved firebase_core 4.11.0 → 4.15.2. Its podspec asks for a newer
Firebase pod than the committed lock names, and pod install keeps a locked
version for a transitive pod even when the development pod above it changed.
CocoaPods says so itself, in the second of the two causes it prints:

changed the constraints of dependency Firebase/CoreOnly inside your
development pod firebase_core. You should run pod update Firebase/CoreOnly to apply changes you've made.

The error message blames the wrong thing, and I believed it first

Flutter appends this:

Error: CocoaPods's specs repository is too out-of-date to satisfy dependencies.
To update the CocoaPods specs, run:
  pod repo update

That is not a diagnosis. cocoapods.dart's _diagnosePodInstallFailure does
stdout.contains('out-of-date source repos'), and CocoaPods prints that phrase
for both causes it lists — so Flutter reprints the stale-index remedy
whichever one applies, then throws. It never retries with --repo-update
(pod install --verbose is the only invocation in the file; the string
repo-update does not appear in it at all).

The first version of this branch took that advice and added a pod repo update
step. The verbose log of run 43110342556 shows it would have been a no-op:

Adding spec repo `trunk` with CDN `https://cdn.cocoapods.org/`
…
CDN: trunk Relative path downloaded:
  Specs/0/3/5/Firebase/12.19.3/Firebase.podspec.json, save ETag: …
CDN: trunk Relative path: Specs/0/3/5/Firebase/12.19.3/Firebase.podspec.json
  modified during this run! Returning local

There is no baked index on the image to be stale — the trunk repo is created
during the run
, and 693 podspecs are fetched fresh over the CDN with new
ETags. The exact version the resolver needed, 12.19.3, was downloaded and read
seconds before the failure. The 12.15.0 in the error is labelled In snapshot (Podfile.lock): it is our own committed file, which I had read as a ceiling in
the index.

pod install --repo-update would fail identically — refreshing sources does
not unlock a pinned pod.

So the snapshot is the problem, and the snapshot cannot be maintained here

Committing Podfile.lock is the usual advice, and it assumes somebody can
regenerate it. In this project nobody can:

  • both build scripts refuse a non-Darwin host outright (uname -s → exit 1),
  • ci.yml is analyze plus the BSD script tests and builds no Apple target,
  • the release job is the only thing in the whole project that ever runs
    pod install.

So a plugin bump invalidates two tracked files that no reviewer, no gate and no
local run can notice, and the first sign is a red release. That is t16
exactly: #492's own CI was green because nothing on a pull request compiles.

What is lost is narrow. pubspec.lock still pins every plugin to an exact
version, and each plugin's podspec pins the pods it needs — so what floats is a
third-party pod's patch range inside a ~> it already declared. What is gained
is a release that does not break on every plugin bump.

And it is reversible from real files rather than from a guess. The release
job now uploads the pair it resolved as podfile-locks. Commit that pair and
drop the two .gitignore lines, and the project is back to pinning — with a
lock that is actually correct, which is not true of the one in main today.

Why those steps are last in the job

      - name: Upload the resolved Podfile.lock files
        if: ${{ !cancelled() }}
        continue-on-error: true

Upload macOS and iOS release assets above it carries no if:, so it defaults
to success(). A diagnostic step failing ahead of it would skip the DMG and IPA
upload, skip publish, and lose a release that was already built — the hazard
the Sentry step guards against the same way. !cancelled() rather than
success() because a run where macOS resolved and iOS then failed is exactly
when the resolved lock is worth having, and if-no-files-found: warn rather
than error because a step whose only output is a diagnostic must not redden a
release.

Verified

  • ASSET_RETENTION is defined at workflow level, so it is in scope here; the
    asset upload in the same job already uses the identical expression.
  • Nothing else in the repository reads either lock file (grep over *.sh,
    *.dart, *.yml, *.md, excluding lib/modules/).
  • The workflow parses and the three closing steps are in the intended order,
    with the new one last.
  • Flutter's behaviour was read in
    flutter_tools/lib/src/macos/cocoapods.dart (_runPodInstall line 352,
    _diagnosePodInstallFailure line 392, the reprinted advice line 410), not
    inferred from the console output.
  • The log claims above are quoted from run 43110342556's own verbose output,
    not summarised.

What this does not prove: that the next release run goes green. Nothing
here can build iOS or macOS, so the first evidence will be the dev build after
this merges. If pod install then finds another pin it cannot satisfy, it will
say so with the same clarity.

Not in this PR

The dev build has failed on macOS and iOS since d10fe8a, and nothing on a
pull request could have caught it:

    [!] CocoaPods could not find compatible versions for pod
        "Firebase/CoreOnly":
          In snapshot (Podfile.lock):
            Firebase/CoreOnly (= 12.15.0, ~> 12.15.0)
          In Podfile:
            firebase_core ... was resolved to 4.15.2, which depends on
              Firebase/CoreOnly (~> 12.19.3)

 #492 moved `firebase_core` 4.11.0 -> 4.15.2. Its podspec asks for a newer
Firebase pod than the committed lock names, and `pod install` keeps a locked
version for a transitive pod even when the development pod above it changed -
which is what CocoaPods means by its second suggestion, "changed the
constraints of dependency `Firebase/CoreOnly` inside your development pod
`firebase_core`. You should run `pod update Firebase/CoreOnly`".

**The message Flutter prints on top of that is misleading, and worth knowing
about before it costs somebody a day.** It ends with "CocoaPods's specs
repository is too out-of-date to satisfy dependencies. To update the CocoaPods
specs, run: pod repo update", which is not a diagnosis: `cocoapods.dart`'s
`_diagnosePodInstallFailure` substring-matches `out-of-date source repos` in
CocoaPods' output, and CocoaPods prints that phrase for *both* of the two
causes it lists. The index was not stale and `pod repo update` fixes nothing
here. The verbose log of run 43110342556 settles it: the runner has no baked
index at all - `Adding spec repo 'trunk' with CDN` creates it during the run -
and `Specs/0/3/5/Firebase/12.19.3/Firebase.podspec.json` is downloaded and
read seconds before the error. The version the resolver needed was in front of
it. `pod install --repo-update` would fail identically, because refreshing
sources does not unlock a pinned pod.

So the snapshot is the problem, and the snapshot cannot be maintained here.
Committing `Podfile.lock` is the usual advice and it assumes somebody can
regenerate it. Nobody can: both build scripts refuse a non-Darwin host,
`ci.yml` is `analyze` plus the BSD script tests and builds no Apple target, and
the release job is the only thing in this project that ever runs `pod install`.
A plugin bump therefore invalidates two tracked files that no reviewer, no gate
and no local run can notice, and the first sign is a red release.

What is lost by not committing them is narrow: `pubspec.lock` still pins every
plugin to an exact version and each plugin's podspec pins the pods it needs, so
what floats is a third-party pod's patch range inside a `~>` it already
declared. What is gained is a release that does not break on every plugin bump.

The release job now uploads the pair it resolved as `podfile-locks`, so the
decision is reversible from real files rather than from a guess - commit that
pair and drop the two `.gitignore` lines. Those steps are last in the job and
`continue-on-error`, because the asset upload above them carries no `if:` and so
defaults to `success()`: a diagnostic step failing ahead of it would skip the
DMG and IPA upload, skip `publish`, and lose a release that was already built.
That is the hazard the Sentry step guards against the same way.

This is the shape t16 names. #492's own CI was green because no job on a pull
request compiles anything.
@mishamyte
mishamyte requested a review from apfxtech as a code owner November 11, 2026 23:01
@mishamyte mishamyte added this to the 0.17.3 milestone Nov 11, 2026
@mishamyte mishamyte self-assigned this Nov 11, 2026
@github-actions github-actions Bot added area/platform Android, iOS, Linux, macOS, Windows, web area/ci-build Workflows, scripts and build configuration labels Nov 11, 2026
@mishamyte
mishamyte merged commit 30936ee into main Nov 12, 2026
3 checks passed
@mishamyte
mishamyte deleted the fix/podfile-lock branch November 12, 2026 07:38
mishamyte added a commit that referenced this pull request Nov 14, 2026
…ed it (#499)

* Read the Xcode version without a pipe, which aborted xcodebuild

`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.

* Guard the pipe that aborted xcodebuild, and retire its five siblings

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci-build Workflows, scripts and build configuration area/platform Android, iOS, Linux, macOS, Windows, web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant