A push fires the web deploy and the app build at the same instant, and on commit
b03fb25 the second SSH session to Gitea came back with "Failed to update public
key" 32 seconds in. The pull succeeded by hand immediately afterwards, so it was
contention between the two runs rather than anything about the code — but the job
had already failed and emailed "App build FAILED".
The pull is now attempted three times with a pause between, and only gives up
after the third. It is the one step here that depends on another machine being
responsive at a moment we do not control, so it is the one worth retrying.
The point is not the build: a false failure alert is what teaches you to ignore
the alert, and an ignored alert is how the off-site backup sat dead for eight days
without anyone noticing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
7.3 KiB
YAML
155 lines
7.3 KiB
YAML
# Push something the apps bundle -> the ingest Mac builds both apps and emails you.
|
|
#
|
|
# WHY THIS STOPS SHORT OF UPLOADING
|
|
# Uploading needs a build number that has never been used before, and Play will never
|
|
# accept a versionCode twice or accept one that goes down. Bumping it means committing
|
|
# the new number back to this repo on every build, which needs write access from the
|
|
# build machine and permanently changes how the apps are numbered. That is a one-way
|
|
# door, so it is left as a deliberate command: ./release.sh --ios --android
|
|
#
|
|
# What this does remove is having to REMEMBER. A change that the apps bundle no longer
|
|
# sits unnoticed: it gets built immediately, you find out straight away if it no longer
|
|
# compiles, and you get an email saying the shipped apps are now behind the website.
|
|
#
|
|
# WHY THE INGEST MAC AND NOT THE NAS
|
|
# An app build needs Xcode, the Android SDK and the signing keys. The NAS runner is
|
|
# Linux in a container and has none of them. This job runs in HOST mode (label
|
|
# "macos:host") directly on the ingest Mac, which has all three.
|
|
|
|
name: Build apps
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
# Only what actually ends up inside an app bundle. Deliberately NOT all of mobile/:
|
|
# release.config.json is excluded so that bumping a build number by hand never
|
|
# triggers a rebuild of the thing that was just built.
|
|
- 'server/public/**'
|
|
- 'mobile/src/**'
|
|
- 'mobile/capacitor.config.json'
|
|
- 'mobile/package.json'
|
|
- '.gitea/workflows/build-apps.yml'
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: macos
|
|
steps:
|
|
- name: Build both apps
|
|
id: build
|
|
run: |
|
|
set -eu
|
|
export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
|
|
APP="$HOME/checklist-app"
|
|
cd "$APP"
|
|
|
|
echo "── syncing the checkout ──"
|
|
# Retried, because this is the one step that talks to another machine. A push
|
|
# fires the web deploy and this build at the same moment, and Gitea has been seen
|
|
# to answer the second SSH session with "Failed to update public key" — a
|
|
# transient contention error that has nothing to do with the code. Without a
|
|
# retry that blip emails "App build FAILED", which is precisely the false alarm
|
|
# that makes an alert worth ignoring.
|
|
for attempt in 1 2 3; do
|
|
if git pull --ff-only; then break; fi
|
|
if [ "$attempt" = 3 ]; then echo "git pull failed three times — giving up"; exit 1; fi
|
|
echo " pull failed (attempt $attempt), retrying in 15s"
|
|
sleep 15
|
|
done
|
|
|
|
echo "── dependencies ──"
|
|
( cd mobile && npm install --no-audit --no-fund >/dev/null )
|
|
|
|
echo "── rebuilding www/ from server/public ──"
|
|
( cd mobile && node scripts/sync-web.js )
|
|
|
|
# Without this the iOS build fails on a missing ios/App/App/public, because that
|
|
# directory is generated rather than committed. release.sh does the same thing.
|
|
echo "── capacitor sync ──"
|
|
( cd mobile && npx cap sync >/dev/null )
|
|
|
|
echo "── android ──"
|
|
( cd mobile/android && . ../scripts/env.sh && ./gradlew :app:bundleRelease --no-daemon -q )
|
|
AAB="$APP/mobile/android/app/build/outputs/bundle/release/app-release.aab"
|
|
test -f "$AAB"
|
|
echo " $AAB ($(du -h "$AAB" | cut -f1))"
|
|
|
|
echo "── ios ──"
|
|
. mobile/ios-signing.config
|
|
rm -rf mobile/build/App.xcarchive mobile/build/ios
|
|
( cd mobile/ios/App && xcodebuild -project App.xcodeproj -scheme App \
|
|
-configuration Release -destination 'generic/platform=iOS' \
|
|
-archivePath "$APP/mobile/build/App.xcarchive" \
|
|
DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" CODE_SIGN_STYLE=Automatic \
|
|
-allowProvisioningUpdates archive -quiet )
|
|
|
|
cat > mobile/ExportOptions.plist <<PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0"><dict>
|
|
<key>method</key><string>app-store-connect</string>
|
|
<key>teamID</key><string>$DEVELOPMENT_TEAM</string>
|
|
<key>destination</key><string>export</string>
|
|
<key>uploadSymbols</key><true/>
|
|
</dict></plist>
|
|
PLIST
|
|
|
|
( cd mobile/ios/App && xcodebuild -exportArchive \
|
|
-archivePath "$APP/mobile/build/App.xcarchive" \
|
|
-exportOptionsPlist "$APP/mobile/ExportOptions.plist" \
|
|
-exportPath "$APP/mobile/build/ios" \
|
|
-allowProvisioningUpdates -quiet )
|
|
IPA="$(find "$APP/mobile/build/ios" -name '*.ipa' | head -1)"
|
|
test -n "$IPA"
|
|
echo " $IPA ($(du -h "$IPA" | cut -f1))"
|
|
|
|
- name: Tell Christian the apps are behind
|
|
if: success()
|
|
run: |
|
|
export PATH="/usr/local/bin:$PATH"
|
|
cd "$HOME/checklist-app"
|
|
V="$(node -e 'const c=require("./mobile/release.config.json");console.log(c.version+" (ios build "+c.iosBuild+", android code "+c.androidVersionCode+")")')"
|
|
# The body goes to a file first. It must NOT be built with $(cat <<EOF ...): macOS
|
|
# ships bash 3.2, which mis-parses a heredoc inside command substitution as soon as
|
|
# the text contains an apostrophe — the word "anyone's" alone was enough to fail the
|
|
# step with "unexpected EOF while looking for matching '".
|
|
BODY_FILE="$(mktemp -t checklist-alert)"
|
|
cat > "$BODY_FILE" <<MSG
|
|
A change landed that the iOS and Android apps bundle, so they were rebuilt on the
|
|
ingest Mac. Both built cleanly and are signed and ready.
|
|
|
|
They are NOT on any phone yet, and will not be until you ship them:
|
|
|
|
cd ~/Documents/recurring-checklist-app
|
|
./release.sh --ios --android
|
|
|
|
Current numbering: $V
|
|
|
|
The website already has this change — it deploys on push. Only the apps, which
|
|
carry their own copy of the UI, are waiting.
|
|
MSG
|
|
node scripts/alert.js "Apps rebuilt and waiting — shipped versions are now behind" "$(cat "$BODY_FILE")" || true
|
|
rm -f "$BODY_FILE"
|
|
|
|
- name: Tell Christian the build broke
|
|
# Scoped to the build step, not the whole job. Previously any failure here sent "the
|
|
# build FAILED" — including a bug in the notification step itself, which is exactly
|
|
# what happened on run 4: both apps built fine and the email said they had not.
|
|
if: failure() && steps.build.outcome == 'failure'
|
|
run: |
|
|
export PATH="/usr/local/bin:$PATH"
|
|
cd "$HOME/checklist-app"
|
|
BODY_FILE="$(mktemp -t checklist-alert)"
|
|
cat > "$BODY_FILE" <<MSG
|
|
A push changed something the apps bundle, and the rebuild did not compile.
|
|
|
|
The website is unaffected — it deploys separately and is already live with this
|
|
change. But the apps cannot be shipped until this builds again.
|
|
|
|
Full log:
|
|
http://chronicles-ab.waterschurch.org:3000/christian/recurring-checklist/actions
|
|
MSG
|
|
node scripts/alert.js "App build FAILED on the ingest Mac" "$(cat "$BODY_FILE")" || true
|
|
rm -f "$BODY_FILE"
|