Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 0 additions & 46 deletions .github/workflows/mobile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ name: Mobile
on:
push:
branches: [main]
# A release tag also builds the phones, so their files can go on the release (see attach-to-release).
tags: ["v*"]
paths:
- "crates/tinyclient/**"
- "crates/protocol/**"
Expand Down Expand Up @@ -190,47 +188,3 @@ jobs:
name: tinyclient-ios-unsigned
path: mobile/ios/build/Tiny.xcarchive/Products/Applications/Tiny.app
if-no-files-found: error

# On a release tag, put the phone builds on the GitHub release next to the desktop installers.
# The release is created by release.yml in parallel, so this waits for it to exist.
# The Android APK is a debug build; the iOS app is unsigned and only runs once re-signed.
attach-to-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [android-app, ios-app]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v7
with:
name: tinyclient-android-debug
path: phone
- uses: actions/download-artifact@v7
with:
name: tinyclient-ios-unsigned
path: phone/Tiny.app
- name: Name and zip the files
env:
VERSION: ${{ github.ref_name }}
working-directory: phone
run: |
mv app-debug.apk "tinyclient-$VERSION-android-debug.apk"
zip -qr "tinyclient-$VERSION-ios-unsigned.zip" Tiny.app
rm -rf Tiny.app
ls -l
- name: Wait for the release, then attach
env:
GH_TOKEN: ${{ github.token }}
# No checkout in this job, so gh needs the repo named.
GH_REPO: ${{ github.repository }}
VERSION: ${{ github.ref_name }}
run: |
for attempt in $(seq 1 60); do
if gh release view "$VERSION" >/dev/null 2>&1; then
gh release upload "$VERSION" phone/* --clobber
exit 0
fi
sleep 30
done
echo "release $VERSION never appeared" >&2
exit 1
80 changes: 73 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ name: Release
# same files without publishing (`gh workflow run release.yml --ref BRANCH`); they are kept as the
# run's `release-all` artifact.
#
# macOS, Windows, and Linux x86_64 skip compilation when CI already built that commit. The
# programs are packaged and signed here. Linux aarch64 is built here; CI does not cover it.
# A release is promoted from the commit's run on main rather than built from scratch. The tag
# and the main push of a commit start together, so the first job finds the CI run (ci.yml) that
# main's push started and waits for it. If that run fails, nothing is published. If it passes,
# each platform takes its programs from that run's packages and only packages and signs them
# here; a platform whose package the run lacks compiles as before. A commit main's CI never
# looked at (only docs changed, or a branch run by hand) has no run to wait for, and every
# platform compiles.
#
# The phones come from the Mobile run (mobile.yml) of the same commit in the same way. Mobile
# only runs on main when the app's code changed, so when there is no such run this workflow
# starts one on the tag and waits for it. A phone build that fails fails the release.
on:
push:
tags:
Expand Down Expand Up @@ -42,8 +51,59 @@ jobs:
RELEASE_URL: https://github.com/${{ github.repository }}/releases/tag/v${{ inputs.announce }}
run: packaging/release-note.sh

# One job waits for CI, so the four build jobs do not each poll for it. The waits are long
# because macOS runners can queue for over an hour: a dispatched Mobile run took 2h24m.
wait-for-ci:
if: github.event_name == 'push' || inputs.announce == ''
runs-on: ubuntu-latest
timeout-minutes: 240
outputs:
run: ${{ steps.ci.outputs.run }}
steps:
- uses: actions/checkout@v5
- name: Wait for main's CI run of this commit
id: ci
env:
GH_TOKEN: ${{ github.token }}
run: packaging/ci/wait-for-run.sh ci.yml

# The APK is a debug build; the iOS app is unsigned and only runs once re-signed.
phones:
if: github.event_name == 'push' || inputs.announce == ''
runs-on: ubuntu-latest
timeout-minutes: 300
permissions:
contents: read
# To start the Mobile workflow when main never ran it for this commit.
actions: write
steps:
- uses: actions/checkout@v5
- name: Name the files
run: packaging/ci-label.sh >> "$GITHUB_ENV"
- name: Wait for the Mobile run of this commit
id: mobile
env:
GH_TOKEN: ${{ github.token }}
run: packaging/ci/wait-for-run.sh mobile.yml --dispatch
- name: Take, name and zip the files
env:
GH_TOKEN: ${{ github.token }}
RUN: ${{ steps.mobile.outputs.run }}
run: |
gh run download "$RUN" -n tinyclient-android-debug -D phone
gh run download "$RUN" -n tinyclient-ios-unsigned -D phone/Tiny.app
mkdir -p dist
mv phone/app-debug.apk "dist/tinyclient-$LABEL-android-debug.apk"
(cd phone && zip -qr "../dist/tinyclient-$LABEL-ios-unsigned.zip" Tiny.app)
ls -l dist
- uses: actions/upload-artifact@v6
with:
name: release-phones
path: dist/*
if-no-files-found: error

macos:
needs: wait-for-ci
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -53,7 +113,8 @@ jobs:
id: reuse
env:
GH_TOKEN: ${{ github.token }}
run: packaging/ci/reuse-binaries.sh macos
CI_RUN: ${{ needs.wait-for-ci.outputs.run }}
run: packaging/ci/reuse-binaries.sh macos "$CI_RUN"
- uses: ./.github/actions/setup-rust
if: steps.reuse.outputs.found != 'true'
with:
Expand Down Expand Up @@ -95,6 +156,7 @@ jobs:
path: dist/*

linux:
needs: wait-for-ci
strategy:
fail-fast: false
matrix:
Expand All @@ -115,7 +177,8 @@ jobs:
id: reuse
env:
GH_TOKEN: ${{ github.token }}
run: packaging/ci/reuse-binaries.sh ${{ matrix.artifact }}
CI_RUN: ${{ needs.wait-for-ci.outputs.run }}
run: packaging/ci/reuse-binaries.sh ${{ matrix.artifact }} "$CI_RUN"
# Packaging reads the linked libraries even when the programs themselves came from CI.
- name: Install packaging dependencies
if: steps.reuse.outputs.found == 'true'
Expand Down Expand Up @@ -195,6 +258,7 @@ jobs:
run: docker run --rm -v "$PWD:/w" -w /w -e LABEL -e VERSION -e ARCH=${{ matrix.arch }} ${{ matrix.image }} packaging/linux/smoke.sh ${{ matrix.kind }}

windows:
needs: wait-for-ci
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -206,7 +270,8 @@ jobs:
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: packaging/ci/reuse-binaries.sh windows
CI_RUN: ${{ needs.wait-for-ci.outputs.run }}
run: packaging/ci/reuse-binaries.sh windows "$CI_RUN"
- uses: ./.github/actions/setup-rust
if: steps.reuse.outputs.found != 'true'
with:
Expand Down Expand Up @@ -238,7 +303,7 @@ jobs:
path: dist/*

publish:
needs: [macos, linux, linux-smoke, windows]
needs: [macos, linux, linux-smoke, windows, phones]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -253,7 +318,8 @@ jobs:
merge-multiple: true
# Each package also goes up without the version in its name, so a fixed address
# (releases/latest/download/tinyclient-macos.dmg, which tinyclient.io/download/macos points
# at) always serves the newest one. SHA256SUMS covers both names.
# at) always serves the newest one. The phone files get one too, such as
# tinyclient-android-debug.apk. SHA256SUMS covers both names.
- name: Unversioned copies and checksums
working-directory: dist
run: |
Expand Down
16 changes: 8 additions & 8 deletions packaging/ci/reuse-binaries.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#!/bin/sh
# Copies the release programs out of the packages CI already uploaded for this commit.
# Copies the release programs out of the packages a CI run already uploaded for this commit.
# The run is the one wait-for-run.sh found and waited for; release.yml passes its id.
# found=true means target/release holds tinyclient, chatd, mediad and tinyclient-login.
# A commit CI did not build (or whose CI failed) leaves found=false, and the caller compiles.
# With no run id, or a run without this platform's package, found=false and the caller compiles.
set -eu

kind=${1:?usage: reuse-binaries.sh macos|linux-x86_64|windows}
kind=${1:?usage: reuse-binaries.sh macos|linux-x86_64|linux-aarch64|windows [CI run id]}
run=${2:-}
case "$kind" in
macos) artifact=tinyclient-macos ;;
linux-x86_64) artifact=tinyclient-linux-x86_64 ;;
linux-aarch64) artifact=tinyclient-linux-aarch64 ;;
windows) artifact=tinyclient-windows-x86_64 ;;
*)
echo "reuse-binaries.sh: nothing to reuse for $kind"
Expand All @@ -31,7 +34,7 @@ take() {
chmod +x "target/release/$program"
done
;;
linux-x86_64)
linux-x86_64 | linux-aarch64)
archive=$(find "$stage" -name 'tinyclient-*-linux-*.tar.gz' | head -1)
[ -n "$archive" ] || return 1
unpack=$(mktemp -d)
Expand Down Expand Up @@ -64,9 +67,6 @@ take() {
}

found=false
run=$(gh run list --workflow ci.yml --commit "$GITHUB_SHA" --status success --limit 1 \
--json databaseId --jq '.[0].databaseId // empty' || true)

if [ -n "$run" ]; then
stage=$(mktemp -d)
if gh run download "$run" -n "$artifact" -D "$stage" && take "$stage"; then
Expand All @@ -77,7 +77,7 @@ if [ -n "$run" ]; then
fi
rm -rf "$stage"
else
echo "reuse-binaries.sh: no successful CI run for $GITHUB_SHA; compiling"
echo "reuse-binaries.sh: no CI run for $GITHUB_SHA to reuse; compiling"
fi

echo "found=$found" >> "${GITHUB_OUTPUT:?reuse-binaries.sh is for GitHub Actions}"
78 changes: 78 additions & 0 deletions packaging/ci/wait-for-run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/sh
# For the release workflow: finds the run of another workflow (ci.yml or mobile.yml) that main's
# push started for this commit, waits for it to finish, and prints its id as run=<id>.
# A tag and the main push of the same commit start together, so on a tag the search keeps trying
# for a few minutes before it decides there is no such run. Run by hand on a branch, it looks once.
# A run that fails fails this script, so nothing is released from a commit main's CI rejected.
# With no run to wait for, run= is empty and the caller builds the files itself; with --dispatch
# this script starts the workflow on the ref instead and waits for that run.
set -eu

workflow=${1:?usage: wait-for-run.sh ci.yml|mobile.yml [--dispatch]}
dispatch=${2:-}
sha=${GITHUB_SHA:?wait-for-run.sh is for GitHub Actions}
out=${GITHUB_OUTPUT:?wait-for-run.sh is for GitHub Actions}

if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
attempts=10
else
attempts=1
fi

# Prints the id of the newest run matching the arguments, or nothing.
newest() {
gh run list --workflow "$workflow" --commit "$sha" --limit 1 "$@" \
--json databaseId --jq '.[0].databaseId // empty'
}

run=
attempt=1
while :; do
run=$(newest --event push --branch main)
[ -n "$run" ] && break
[ "$attempt" -ge "$attempts" ] && break
echo "wait-for-run.sh: no $workflow run on main for $sha yet (attempt $attempt of $attempts)"
attempt=$((attempt + 1))
sleep 20
done

if [ -z "$run" ] && [ "$dispatch" = --dispatch ]; then
# A minute of slack for the clock difference between this runner and GitHub.
since=$(date -u -d "@$(($(date +%s) - 60))" +%Y-%m-%dT%H:%M:%SZ)
echo "wait-for-run.sh: no $workflow run on main for $sha; starting one on $GITHUB_REF_NAME"
gh workflow run "$workflow" --ref "$GITHUB_REF_NAME"
for attempt in 1 2 3 4 5 6 7 8 9 10; do
sleep 10
run=$(gh run list --workflow "$workflow" --commit "$sha" --event workflow_dispatch --limit 5 \
--json databaseId,createdAt \
--jq "[.[] | select(.createdAt >= \"$since\")] | .[0].databaseId // empty")
[ -n "$run" ] && break
done
if [ -z "$run" ]; then
echo "wait-for-run.sh: the $workflow run started on $GITHUB_REF_NAME never appeared" >&2
exit 1
fi
fi

if [ -z "$run" ]; then
echo "wait-for-run.sh: no $workflow run on main for $sha; the release builds everything itself"
echo "run=" >> "$out"
exit 0
fi

url=$(gh run view "$run" --json url --jq .url)
echo "wait-for-run.sh: waiting for $workflow run $run ($url)"
# The job's timeout bounds this wait, and a minute between looks keeps two waiting jobs well
# inside the token's hourly API allowance. A watch that drops out early (a network error) is
# resumed, and the conclusion is read back from the finished run rather than taken from gh's
# exit status.
until [ "$(gh run view "$run" --json status --jq .status)" = completed ]; do
gh run watch "$run" --interval 60 >/dev/null || sleep 60
done
conclusion=$(gh run view "$run" --json conclusion --jq .conclusion)
if [ "$conclusion" != success ]; then
echo "wait-for-run.sh: $workflow run $run ended $conclusion; $url" >&2
exit 1
fi
echo "wait-for-run.sh: $workflow run $run succeeded"
echo "run=$run" >> "$out"
Loading