Compare commits

...

1 Commits

Author SHA1 Message Date
Zack Pollard 2390ad0fab ci(release): support patch releases from release/* branches
- prepare-release: add `branch` input; validate branch/bump combination;
  skip Weblate merge, mobile build, and APK asset when not on main; point
  checkout, release target, and tag at the selected branch; backport the
  archived-versions.json entry to main via PR.
- build-mobile: gate Android release build and iOS TestFlight upload on
  `environment == production` instead of the branch name, so patch
  releases still produce production artifacts if ever re-enabled.
- docker: build on pushes to release/**; restrict retag-from-main jobs
  to PRs and main-branch pushes.
- docs-build: build on pushes to release/**; include release/** in the
  pre-job force-branches list.
2026-04-24 17:57:29 +01:00
4 changed files with 113 additions and 22 deletions
+6 -6
View File
@@ -143,9 +143,9 @@ jobs:
ALIAS: ${{ secrets.ALIAS }} ALIAS: ${{ secrets.ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }} IS_RELEASE: ${{ inputs.environment == 'production' || github.ref == 'refs/heads/main' }}
run: | run: |
if [[ $IS_MAIN == 'true' ]]; then if [[ $IS_RELEASE == 'true' ]]; then
flutter build apk --release flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64 flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else else
@@ -268,20 +268,20 @@ jobs:
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
ENVIRONMENT: ${{ inputs.environment || 'development' }} ENVIRONMENT: ${{ inputs.environment || 'development' }}
BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }} BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }}
GITHUB_REF: ${{ github.ref }} IS_RELEASE: ${{ inputs.environment == 'production' || github.ref == 'refs/heads/main' }}
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6 FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6
working-directory: ./mobile/ios working-directory: ./mobile/ios
run: | run: |
# Only upload to TestFlight on main branch # Upload to TestFlight on main or when explicitly invoked as a production release.
if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then if [[ "$IS_RELEASE" == "true" ]]; then
if [[ "$ENVIRONMENT" == "development" ]]; then if [[ "$ENVIRONMENT" == "development" ]]; then
bundle exec fastlane gha_testflight_dev bundle exec fastlane gha_testflight_dev
else else
bundle exec fastlane gha_release_prod bundle exec fastlane gha_release_prod
fi fi
else else
# Build only, no TestFlight upload for non-main branches # Build only, no TestFlight upload
bundle exec fastlane gha_build_only bundle exec fastlane gha_build_only
fi fi
+5 -3
View File
@@ -3,7 +3,7 @@ name: Docker
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [main] branches: [main, 'release/**']
pull_request: pull_request:
release: release:
types: [published] types: [published]
@@ -53,7 +53,8 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == false && !github.event.pull_request.head.repo.fork }} # Retag sources from the :main image, so only retag for PRs and main-branch pushes.
if: ${{ fromJSON(needs.pre-job.outputs.should_run).machine-learning == false && !github.event.pull_request.head.repo.fork && (github.event_name == 'pull_request' || github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
@@ -83,7 +84,8 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork }} # Retag sources from the :main image, so only retag for PRs and main-branch pushes.
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == false && !github.event.pull_request.head.repo.fork && (github.event_name == 'pull_request' || github.ref == 'refs/heads/main') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
+2 -2
View File
@@ -1,7 +1,7 @@
name: Docs build name: Docs build
on: on:
push: push:
branches: [main] branches: [main, 'release/**']
pull_request: pull_request:
release: release:
types: [published] types: [published]
@@ -39,7 +39,7 @@ jobs:
force-filters: | force-filters: |
- '.github/workflows/docs-build.yml' - '.github/workflows/docs-build.yml'
force-events: 'release' force-events: 'release'
force-branches: 'main' force-branches: 'main,release/**'
build: build:
name: Docs Build name: Docs Build
+100 -11
View File
@@ -3,8 +3,13 @@ name: Prepare new release
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
branch:
description: 'Branch to release from (must be main or release/*)'
required: true
default: 'main'
type: string
serverBump: serverBump:
description: 'Bump server version' description: 'Bump server version (only patch allowed on release/* branches)'
required: true required: true
default: 'false' default: 'false'
type: choice type: choice
@@ -29,10 +34,31 @@ concurrency:
permissions: {} permissions: {}
jobs: jobs:
validate_inputs:
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Validate branch and bump combination
env:
BRANCH: ${{ inputs.branch }}
SERVER_BUMP: ${{ inputs.serverBump }}
run: |
set -euo pipefail
if [[ "$BRANCH" != "main" && "$BRANCH" != release/* ]]; then
echo "::error::branch must be 'main' or start with 'release/' (got '$BRANCH')"
exit 1
fi
if [[ "$BRANCH" != "main" && "$SERVER_BUMP" != "false" && "$SERVER_BUMP" != "patch" ]]; then
echo "::error::only 'patch' (or 'false') serverBump is allowed on '$BRANCH'"
exit 1
fi
merge_translations: merge_translations:
needs: [validate_inputs]
uses: ./.github/workflows/merge-translations.yml uses: ./.github/workflows/merge-translations.yml
with: with:
skip: ${{ inputs.skipTranslations }} # Weblate tracks main only, so skip translations when releasing from a release/* branch.
skip: ${{ inputs.skipTranslations || inputs.branch != 'main' }}
permissions: permissions:
pull-requests: write pull-requests: write
secrets: secrets:
@@ -60,7 +86,7 @@ jobs:
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true persist-credentials: true
ref: main ref: ${{ inputs.branch }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
@@ -94,6 +120,10 @@ jobs:
push: true push: true
build_mobile: build_mobile:
# Mobile build numbers are monotonic per store; releasing from a release/* branch
# would collide with build numbers already shipped from main. Skip mobile on patch
# releases — handle mobile patches on main instead.
if: ${{ inputs.branch == 'main' }}
uses: ./.github/workflows/build-mobile.yml uses: ./.github/workflows/build-mobile.yml
needs: bump_version needs: bump_version
permissions: permissions:
@@ -118,6 +148,8 @@ jobs:
prepare_release: prepare_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_mobile, bump_version] needs: [build_mobile, bump_version]
# Run even when build_mobile is skipped (patch release from release/* branch).
if: ${{ always() && needs.bump_version.result == 'success' && (needs.build_mobile.result == 'success' || needs.build_mobile.result == 'skipped') }}
permissions: permissions:
actions: read # To download the app artifact actions: read # To download the app artifact
# No content permissions are needed because it uses the app-token # No content permissions are needed because it uses the app-token
@@ -134,26 +166,83 @@ jobs:
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false persist-credentials: false
ref: ${{ needs.bump_version.outputs.ref }}
- name: Download APK - name: Download APK
if: ${{ needs.build_mobile.result == 'success' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
name: release-apk-signed name: release-apk-signed
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
- name: Assemble release assets
id: assets
env:
HAS_APK: ${{ needs.build_mobile.result == 'success' }}
run: |
{
echo 'files<<EOF'
echo 'docker/docker-compose.yml'
echo 'docker/docker-compose.rootless.yml'
echo 'docker/example.env'
echo 'docker/hwaccel.ml.yml'
echo 'docker/hwaccel.transcoding.yml'
echo 'docker/prometheus.yml'
if [[ "$HAS_APK" == "true" ]]; then
echo '*.apk'
fi
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with: with:
draft: true draft: true
tag_name: ${{ needs.bump_version.outputs.version }} tag_name: ${{ needs.bump_version.outputs.version }}
target_commitish: ${{ inputs.branch }}
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
generate_release_notes: true generate_release_notes: true
body_path: misc/release/notes.tmpl body_path: misc/release/notes.tmpl
files: | files: ${{ steps.assets.outputs.files }}
docker/docker-compose.yml
docker/docker-compose.rootless.yml backport_archived_versions:
docker/example.env # When releasing from a release/* branch, the archived-versions.json update
docker/hwaccel.ml.yml # lives on that branch only. Open a PR to mirror the new entry onto main so
docker/hwaccel.transcoding.yml # main's docs keep a complete archive list.
docker/prometheus.yml if: ${{ inputs.branch != 'main' && needs.bump_version.result == 'success' }}
*.apk runs-on: ubuntu-latest
needs: [bump_version, prepare_release]
permissions: {} # uses the app token
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout main
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: false
ref: main
- name: Update archived versions on main
env:
VERSION: ${{ needs.bump_version.outputs.version }}
run: ./misc/release/archive-version.js "${VERSION#v}"
- name: Open backport PR
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ steps.generate-token.outputs.token }}
branch: backport/archived-versions-${{ needs.bump_version.outputs.version }}
base: main
commit-message: 'chore(docs): archive ${{ needs.bump_version.outputs.version }}'
title: 'chore(docs): archive ${{ needs.bump_version.outputs.version }}'
body: |
Backports the `archived-versions.json` entry for ${{ needs.bump_version.outputs.version }},
released from `${{ inputs.branch }}`, so main's docs archive list stays complete.
add-paths: docs/static/archived-versions.json
delete-branch: true