Skip to content

release: publish T4 Code v0.1.13 (#20) #15

release: publish T4 Code v0.1.13 (#20)

release: publish T4 Code v0.1.13 (#20) #15

Workflow file for this run

name: Release app builds
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: Existing version tag to publish
required: true
type: string
permissions:
contents: read
concurrency:
group: release-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
env:
RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }}
jobs:
verify:
if: ${{ github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
outputs:
source_sha: ${{ steps.source.outputs.source_sha }}
version: ${{ steps.source.outputs.version }}
steps:
- name: Check out trusted release-control source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Resolve immutable release source
id: source
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
MAIN_SHA: ${{ github.sha }}
run: |
set -euo pipefail
trusted_version=$(node -p "require('./package.json').version")
expected_tag="v${trusted_version}"
if [[ "$RELEASE_TAG" != "$expected_tag" ]]; then
echo "release tag must be the current package tag ${expected_tag}" >&2
exit 1
fi
git fetch --force --no-tags origin "refs/heads/main:refs/remotes/origin/main"
git fetch --force origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
source_sha=$(git rev-parse "${RELEASE_TAG}^{commit}")
tag_version=$(git show "${source_sha}:package.json" | jq -er '.version')
if [[ "$tag_version" != "$trusted_version" ]]; then
echo "release tag package version ${tag_version} does not match trusted source ${trusted_version}" >&2
exit 1
fi
if ! git merge-base --is-ancestor "$source_sha" refs/remotes/origin/main; then
echo "release tag source is not reachable from main" >&2
exit 1
fi
if [[ "$EVENT_NAME" == "workflow_dispatch" && "$MAIN_SHA" != "$(git rev-parse HEAD)" ]]; then
echo "manual releases must run from the checked-out main commit" >&2
exit 1
fi
printf 'source_sha=%s\nversion=%s\n' "$source_sha" "$tag_version" >> "$GITHUB_OUTPUT"
- name: Check out immutable release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.source.outputs.source_sha }}
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0
- name: Install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1
cache: pnpm
- name: Verify tag, packages, clients, docs, and downloads agree
run: node scripts/check-release-consistency.mjs --tag "$RELEASE_TAG"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install browser for end-to-end tests
run: pnpm exec playwright install --with-deps chromium
- name: Check source and types
run: pnpm check
- name: Run tooling regression tests
run: pnpm test:tooling
- name: Run workspace tests
run: pnpm test
- name: Run built-app end-to-end tests
run: pnpm test:e2e
- name: Build all workspaces
run: pnpm build
- name: Check packaging contract
run: pnpm test:packaging
build-linux:
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 35
steps:
- name: Check out verified release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.verify.outputs.source_sha }}
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0
- name: Install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Linux packages
run: pnpm package:linux
- name: Inspect Linux packages
run: pnpm inspect:package -- release/*.deb release/*.AppImage
- name: Stage Linux artifacts
shell: bash
run: |
mkdir -p artifacts
cp release/T4-Code-*.deb release/T4-Code-*.AppImage artifacts/
(cd artifacts && sha256sum T4-Code-* > SHA256SUMS-linux.txt)
- name: Upload Linux artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: linux-release
path: artifacts/*
if-no-files-found: error
retention-days: 7
build-android:
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 35
steps:
- name: Check out verified release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.verify.outputs.source_sha }}
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0
- name: Install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1
cache: pnpm
- name: Install Java
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
distribution: temurin
java-version: "21"
cache: gradle
- name: Install Android SDK
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
- name: Install pinned Android platform and build tools
run: sdkmanager --install "platforms;android-36" "build-tools;36.0.0"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Restore and validate Android release signing key
shell: bash
env:
T4_ANDROID_KEYSTORE_BASE64: ${{ secrets.T4_ANDROID_KEYSTORE_BASE64 }}
T4_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.T4_ANDROID_KEYSTORE_PASSWORD }}
T4_ANDROID_KEY_ALIAS: ${{ secrets.T4_ANDROID_KEY_ALIAS }}
T4_ANDROID_KEY_PASSWORD: ${{ secrets.T4_ANDROID_KEY_PASSWORD }}
run: |
set -euo pipefail
for name in \
T4_ANDROID_KEYSTORE_BASE64 \
T4_ANDROID_KEYSTORE_PASSWORD \
T4_ANDROID_KEY_ALIAS \
T4_ANDROID_KEY_PASSWORD
do
if [[ -z "${!name:-}" ]]; then
echo "required GitHub Actions secret ${name} is not configured" >&2
exit 1
fi
done
keystore_path="$RUNNER_TEMP/t4-code-release.jks"
printf '%s' "$T4_ANDROID_KEYSTORE_BASE64" | base64 --decode > "$keystore_path"
chmod 600 "$keystore_path"
keytool -list \
-keystore "$keystore_path" \
-storepass "$T4_ANDROID_KEYSTORE_PASSWORD" \
-alias "$T4_ANDROID_KEY_ALIAS" >/dev/null
- name: Build signed Android APK
env:
T4_ANDROID_KEYSTORE_PATH: ${{ runner.temp }}/t4-code-release.jks
T4_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.T4_ANDROID_KEYSTORE_PASSWORD }}
T4_ANDROID_KEY_ALIAS: ${{ secrets.T4_ANDROID_KEY_ALIAS }}
T4_ANDROID_KEY_PASSWORD: ${{ secrets.T4_ANDROID_KEY_PASSWORD }}
run: |
pnpm build:web
pnpm --filter @t4-code/mobile build:android:release
- name: Inspect signed Android APK
shell: bash
env:
VERSION: ${{ needs.verify.outputs.version }}
run: |
set -euo pipefail
apk="apps/mobile/android/app/build/outputs/apk/release/app-release.apk"
test -f "$apk"
build_tools="${ANDROID_SDK_ROOT:-$ANDROID_HOME}/build-tools/36.0.0"
export PATH="$build_tools:$PATH"
apksigner verify --verbose "$apk"
aapt dump badging "$apk" | grep -F "package: name='com.lycaonsolutions.t4code'"
aapt dump badging "$apk" | grep -F "versionName='${VERSION}'"
- name: Stage Android artifact
shell: bash
env:
VERSION: ${{ needs.verify.outputs.version }}
run: |
mkdir -p artifacts
cp \
apps/mobile/android/app/build/outputs/apk/release/app-release.apk \
"artifacts/T4-Code-${VERSION}-android.apk"
(cd artifacts && sha256sum T4-Code-* > SHA256SUMS-android.txt)
- name: Upload Android artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: android-release
path: artifacts/*
if-no-files-found: error
retention-days: 7
build-macos:
needs: verify
runs-on: macos-15
timeout-minutes: 40
steps:
- name: Check out verified release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.verify.outputs.source_sha }}
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
with:
version: 11.10.0
- name: Install Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.13.1
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build unsigned macOS packages
run: pnpm package:mac:unsigned
- name: Inspect macOS packages
run: |
pnpm inspect:package -- release/*.zip
pnpm inspect:dmg -- release/*.dmg
- name: Stage macOS artifacts
shell: bash
run: |
mkdir -p artifacts
cp release/T4-Code-*.dmg release/T4-Code-*.zip artifacts/
(cd artifacts && shasum -a 256 T4-Code-* > SHA256SUMS-macos.txt)
- name: Upload macOS artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: macos-release
path: artifacts/*
if-no-files-found: error
retention-days: 7
publish:
needs: [verify, build-android, build-linux, build-macos]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Check out verified release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.verify.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Confirm the release tag still resolves to the verified source
shell: bash
env:
SOURCE_SHA: ${{ needs.verify.outputs.source_sha }}
run: |
set -euo pipefail
git fetch --force origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
test "$(git rev-parse "${RELEASE_TAG}^{commit}")" = "$SOURCE_SHA"
- name: Download built artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: "*-release"
path: artifacts
merge-multiple: true
- name: Assemble checksums
shell: bash
run: cat artifacts/SHA256SUMS-android.txt artifacts/SHA256SUMS-linux.txt artifacts/SHA256SUMS-macos.txt | sort -k2 > artifacts/SHA256SUMS.txt
- name: Publish GitHub release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: ${{ env.RELEASE_TAG }}
generate_release_notes: true
fail_on_unmatched_files: true
body_path: docs/CURRENT_RELEASE_NOTES.md
files: |
artifacts/T4-Code-*.apk
artifacts/T4-Code-*.deb
artifacts/T4-Code-*.AppImage
artifacts/T4-Code-*.dmg
artifacts/T4-Code-*.zip
artifacts/SHA256SUMS.txt
dispatch-site:
name: Dispatch site deployment after release publication
needs: publish
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
actions: write
contents: read
steps:
- name: Dispatch the production workflow from main with exact release source
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: >-
gh workflow run deploy-site.yml
--ref main
-f release_tag="$RELEASE_TAG"