Skip to content
Merged
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
108 changes: 108 additions & 0 deletions .github/workflows/publish-r2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Publish release to R2

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Existing published release tag (also refreshes expiring APT metadata)
required: true
type: string

permissions:
contents: read

concurrency:
group: r2-publish
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-24.04
environment: release
env:
TAG: ${{ github.event.release.tag_name || inputs.tag }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_BUCKET: ${{ vars.R2_BUCKET }}
R2_ENDPOINT: ${{ vars.R2_ENDPOINT }}
GNUPGHOME: ${{ runner.temp }}/apt-gnupg
steps:
# Environment rules must restrict publishing to reviewed branches/tags.
- uses: actions/checkout@v4
- name: Test upload safeguards offline
run: python3 scripts/test_r2_upload.py
- name: Validate published stable release and configuration
shell: bash
run: |
[[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Expected stable vX.Y.Z tag'; exit 1; }
gh release view "$TAG" --json isDraft,isPrerelease > release.json
python3 - <<'PY'
import json
release = json.load(open('release.json'))
assert not release['isDraft'] and not release['isPrerelease'], 'Release must be published and stable'
PY
: "${R2_BUCKET:?Set environment variable R2_BUCKET}"
: "${R2_ENDPOINT:?Set environment variable R2_ENDPOINT}"
: "${AWS_ACCESS_KEY_ID:?Missing R2_ACCESS_KEY_ID secret}"
: "${AWS_SECRET_ACCESS_KEY:?Missing R2_SECRET_ACCESS_KEY secret}"
- name: Install repository tools
run: |
sudo apt-get update
sudo apt-get install -y dpkg-dev apt-utils gnupg
command -v aws
- name: Download release assets and retain existing APT pool
run: |
mkdir -p dist/deb "dist/repo/releases/$TAG" dist/repo/apt/pool
gh release download "$TAG" --pattern 'tinline_*.deb' --dir dist/deb
gh release download "$TAG" --pattern "tinline-${TAG}-x86_64-linux.tar.gz" --dir "dist/repo/releases/$TAG"
gh release download "$TAG" --pattern "tinline-${TAG}.apk" --dir "dist/repo/releases/$TAG"
aws s3 sync "s3://$R2_BUCKET/apt/pool/" dist/repo/apt/pool/ --endpoint-url "$R2_ENDPOINT"
# Reject changes to any existing versioned object, including old .deb files.
python3 - <<'PY'
import os, pathlib, subprocess
subprocess.run(['aws', 's3api', 'list-objects-v2', '--bucket', os.environ['R2_BUCKET'],
'--endpoint-url', os.environ['R2_ENDPOINT'], '--output', 'json'],
check=True, stdout=open('objects.json', 'w'))
import json
existing = {x['Key'] for x in json.load(open('objects.json')).get('Contents', [])}
candidates = [(p, f'releases/{os.environ["TAG"]}/{p.name}')
for p in pathlib.Path(f'dist/repo/releases/{os.environ["TAG"]}').iterdir()]
candidates += [(p, f'apt/pool/main/t/tinline/{p.name}') for p in pathlib.Path('dist/deb').glob('*.deb')]
for path, key in candidates:
if key in existing:
subprocess.run(['aws', 's3', 'cp', f's3://{os.environ["R2_BUCKET"]}/{key}',
'existing-object', '--endpoint-url', os.environ['R2_ENDPOINT']], check=True)
if path.read_bytes() != pathlib.Path('existing-object').read_bytes():
raise SystemExit(f'Refusing to overwrite immutable object: {key}')
PY
(cd "dist/repo/releases/$TAG" && sha256sum *.apk *.tar.gz > SHA256SUMS)
- name: Import APT signing key
env:
PRIVATE_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }}
PASSPHRASE: ${{ secrets.APT_GPG_PASSPHRASE }}
FINGERPRINT: ${{ vars.APT_GPG_KEY_ID }}
run: |
: "${PRIVATE_KEY:?Missing APT_GPG_PRIVATE_KEY armored secret}"
: "${FINGERPRINT:?Set APT_GPG_KEY_ID full fingerprint}"
mkdir -m 700 -p "$GNUPGHOME"
printf '%s' "$PRIVATE_KEY" | gpg --batch --import
gpg --list-secret-keys "$FINGERPRINT"
umask 077
printf '%s' "$PASSPHRASE" > "$RUNNER_TEMP/apt-passphrase"
- name: Sign and publish repository
env:
GPG_KEY_ID: ${{ vars.APT_GPG_KEY_ID }}
GPG_PASSPHRASE_FILE: ${{ runner.temp }}/apt-passphrase
run: |
./scripts/build_apt_repo.sh
./scripts/upload_repo_r2.sh
- name: Clean signing material
if: always()
run: |
gpgconf --kill gpg-agent || true
rm -rf "$GNUPGHOME" "$RUNNER_TEMP/apt-passphrase"
166 changes: 166 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
name: Release builds

on:
workflow_dispatch:
push:
branches: ['release/linux-android-packaging']
tags: ['v*']

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
version:
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.version.outputs.version }}
code: ${{ steps.version.outputs.code }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: version
name: Validate release version
shell: bash
run: |
python3 - <<'PY'
import os, re, subprocess, tomllib
with open('Cargo.toml', 'rb') as f:
version = tomllib.load(f)['workspace']['package']['version']
if not re.fullmatch(r'\d+\.\d+\.\d+', version):
raise SystemExit('Only stable X.Y.Z versions supported by this workflow')
if os.environ['GITHUB_REF_TYPE'] == 'tag' and os.environ['GITHUB_REF_NAME'] != f'v{version}':
raise SystemExit('Tag must match Cargo workspace version')
code = subprocess.check_output(['git', 'rev-list', '--count', 'HEAD'], text=True).strip()
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f'version={version}\ncode={code}\n')
PY

linux:
needs: version
runs-on: ubuntu-24.04
env:
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Install build and packaging dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential pkg-config cmake meson ninja-build clang libclang-dev \
libgtk-3-dev libasound2-dev libssl-dev libdbus-1-dev libabsl-dev \
libxkbcommon-dev libxcb1-dev dpkg-dev desktop-file-utils
- name: Install Rust
run: rustup toolchain install stable --profile minimal && rustup default stable
- name: Build tarball and Debian package
run: |
desktop-file-validate packaging/linux/tinline.desktop
./scripts/package_linux_tarball.sh
./scripts/package_deb.sh
- name: Inspect and install Debian package on build runner
run: |
dpkg-deb --info dist/deb/*.deb
dpkg-deb --contents dist/deb/*.deb
sudo apt-get install -y ./dist/deb/*.deb
ldd /usr/bin/tinline | tee dist/linux-libraries.txt
! grep -q 'not found' dist/linux-libraries.txt
- uses: actions/upload-artifact@v4
with:
name: linux-release
path: |
dist/deb/*
dist/releases/**/*
dist/linux-libraries.txt
if-no-files-found: error

android:
needs: version
runs-on: ubuntu-24.04
environment: release
env:
VERSION: ${{ needs.version.outputs.version }}
VERSION_CODE: ${{ needs.version.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: android-actions/setup-android@v3
with:
packages: platform-tools
- name: Install native build dependencies and Android SDK
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake ninja-build pkg-config clang libclang-dev
sdkmanager 'platforms;android-36' 'build-tools;36.0.0' 'ndk;28.2.13676358'
- name: Install Rust and cargo-ndk
run: |
rustup toolchain install stable --profile minimal
rustup default stable
rustup target add aarch64-linux-android x86_64-linux-android
cargo install cargo-ndk --version 4.1.2 --locked
- name: Restore release keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
test -n "$KEYSTORE_BASE64" || { echo 'Missing Android release keystore secret'; exit 1; }
umask 077
printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/tinline-release.jks"
- name: Build signed release APK
working-directory: android
env:
ORG_GRADLE_PROJECT_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
ORG_GRADLE_PROJECT_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ORG_GRADLE_PROJECT_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
export ORG_GRADLE_PROJECT_RELEASE_STORE_FILE="$RUNNER_TEMP/tinline-release.jks"
./gradlew --no-daemon --console=plain :app:assembleRelease \
-PrequireReleaseSigning=true -PversionName="$VERSION" -PversionCode="$VERSION_CODE" \
-Pabis=x86_64,arm64-v8a
- name: Verify APK signature and collect artifact
run: |
"$ANDROID_HOME/build-tools/36.0.0/apksigner" verify --verbose --print-certs \
android/app/build/outputs/apk/release/app-release.apk
mkdir -p dist/android
cp android/app/build/outputs/apk/release/app-release.apk "dist/android/tinline-v${VERSION}.apk"
cd dist/android
sha256sum *.apk > SHA256SUMS
- name: Remove keystore
if: always()
run: rm -f "$RUNNER_TEMP/tinline-release.jks"
- uses: actions/upload-artifact@v4
with:
name: android-release
path: dist/android/*
if-no-files-found: error

draft-release:
if: github.ref_type == 'tag'
needs: [version, linux, android]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create draft release and attach packages
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
test "$(gh release view "$TAG" --json isDraft --jq .isDraft)" = true || {
echo 'Refusing to replace artifacts on a published release'; exit 1;
}
else
gh release create "$TAG" --verify-tag --draft --title "Tinline $TAG" \
--notes 'Candidate Linux tarball, Debian package, and signed Android APK. Test before publishing. Linux build baseline: Ubuntu 24.04; older distros are not verified.'
fi
mapfile -d '' files < <(find artifacts -type f -print0)
gh release upload "$TAG" "${files[@]}" --clobber
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
target
dist/
android/.gradle/
android/build/
android/app/build/
Expand Down
Loading
Loading