# Reference: https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners # Conditions for pushing the image to GHCR: # - Triggered by push to default branch (`main`) # - Triggered by push to a version tag (`v*`) # - Triggered by a scheduled run # - Triggered manually via `workflow_dispatch` with `push: true` # # Conditional expression: # github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push name: Publish Docker image on: workflow_dispatch: inputs: ref: description: 'Git ref (tag or commit SHA) to build' required: true type: string default: 'main' push: description: 'Push the image to container registry' required: false type: boolean default: false push: tags: - 'v*' branches: - main paths-ignore: - 'charts/**' schedule: - cron: '30 1 * * *' env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} permissions: contents: write packages: write jobs: ci: uses: ./.github/workflows/ci.yml build: name: Build Docker image needs: [ ci ] strategy: fail-fast: false matrix: platform: [amd64, arm64] timeout-minutes: 60 runs-on: ${{ matrix.platform == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} outputs: tags: ${{ steps.meta.outputs.tags }} permissions: contents: read packages: write steps: - name: Check out the repo uses: actions/checkout@v4.2.0 with: ref: ${{ github.event.inputs.ref || github.ref }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.10.0 - name: Log in to the container registry uses: docker/login-action@v3.3.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Configure image tags id: tag_config shell: bash run: | BASE_CONFIG="type=sha,format=long" if [[ $GITHUB_EVENT_NAME == "schedule" ]]; then BASE_CONFIG+=$'\n'"type=schedule,pattern=nightly" BASE_CONFIG+=$'\n'"type=schedule,pattern=nightly-{{date 'ddd'}}" elif [[ "$GITHUB_REF" == refs/tags/* ]]; then TAG_NAME="${GITHUB_REF#refs/tags/}" if [[ "$TAG_NAME" == v* ]]; then BASE_CONFIG="type=semver,pattern={{version}}" if [[ "$TAG_NAME" == v*-alpha* ]]; then BASE_CONFIG+=$'\n'"type=raw,value=latest" else BASE_CONFIG+=$'\n'"type=raw,value=stable" BASE_CONFIG+=$'\n'"type=raw,value=latest" fi fi fi { echo 'TAGS_SPEC<> $GITHUB_ENV - name: Get current date (RFC 3339 format) id: date run: echo "date=$(date -Iseconds)" >> $GITHUB_OUTPUT - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5.6.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} flavor: latest=false tags: ${{ env.TAGS_SPEC }} labels: | org.opencontainers.image.version=${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }} org.opencontainers.image.created=${{ steps.date.outputs.date }} org.opencontainers.image.ref.name=${{ github.ref_name }} org.opencontainers.image.vendor=we-promise org.opencontainers.image.title=Sure org.opencontainers.image.description=A multi-arch Docker image for the Sure Rails app - name: Publish 'linux/${{ matrix.platform }}' image by digest uses: docker/build-push-action@v6.16.0 id: build with: context: . build-args: BUILD_COMMIT_SHA=${{ github.sha }} platforms: 'linux/${{ matrix.platform }}' cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform }} cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cache-${{ matrix.platform }},mode=max labels: ${{ steps.meta.outputs.labels }} provenance: false push: true # DO NOT REMOVE `oci-mediatypes=true`, fixes annotation not showing up on job.merge.steps[-1] # ref: https://github.com/docker/build-push-action/discussions/1022 outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},name-canonical=true,push-by-digest=true,oci-mediatypes=true - name: Export the Docker image digest if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }} run: | mkdir -p "${RUNNER_TEMP}"/digests echo "${DIGEST#sha256:}" > "${RUNNER_TEMP}/digests/digest-${PLATFORM}" env: DIGEST: ${{ steps.build.outputs.digest }} PLATFORM: ${{ matrix.platform }} - name: Upload the Docker image digest if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }} uses: actions/upload-artifact@v4.6.2 with: name: digest-${{ matrix.platform }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 merge: name: Merge multi-arch manifest & push multi-arch tag if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule' || github.event.inputs.push }} needs: [build] timeout-minutes: 60 runs-on: 'ubuntu-24.04' permissions: packages: write steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.10.0 - name: Download Docker image digests uses: actions/download-artifact@v4.3.0 with: path: ${{ runner.temp }}/digests pattern: digest-* merge-multiple: true - name: Log in to the container registry uses: docker/login-action@v3.3.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Merge and push Docker image env: TAGS: ${{ needs.build.outputs.tags }} DIGESTS_DIR: ${{ runner.temp }}/digests shell: bash -xeuo pipefail {0} run: | tag_args=() while IFS=$'\n' read -r tag; do [[ -n "${tag}" ]] || continue tag_args+=("--tag=${tag}") done <<< "${TAGS}" image_args=() for PLATFORM in amd64 arm64; do image_args+=("${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:$(<"${DIGESTS_DIR}/digest-${PLATFORM}")") done annotations=( "index:org.opencontainers.image.created=$(date -Iseconds)" 'index:org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}' 'index:org.opencontainers.image.revision=${{ github.sha }}' 'index:org.opencontainers.image.ref.name=${{ github.ref_name }}' 'index:org.opencontainers.image.vendor=we-promise' 'index:org.opencontainers.image.licenses=AGPL-3.0' 'index:org.opencontainers.image.title=Sure' 'index:org.opencontainers.image.description=A multi-arch Docker image for the Sure Rails app' ) annotation_args=() for annotation in "${annotations[@]}"; do annotation_args+=("--annotation=${annotation}") done if [[ $GITHUB_REF_TYPE == "tag" ]]; then annotation_args+=("--annotation=index:org.opencontainers.image.version=$GITHUB_REF_NAME") fi attempts=0 until docker buildx imagetools create \ "${annotation_args[@]}" \ "${tag_args[@]}" \ "${image_args[@]}" \ ; do attempts=$((attempts + 1)) if [[ $attempts -ge 3 ]]; then echo "[$(date -u)] ERROR: Failed after 3 attempts." >&2 exit 1 fi delay=$((2 ** attempts)) if [[ $delay -gt 15 ]]; then delay=15; fi echo "Push failed (attempt $attempts). Retrying in ${delay} seconds..." sleep ${delay} done mobile: name: Build Mobile Apps if: startsWith(github.ref, 'refs/tags/v') uses: ./.github/workflows/flutter-build.yml secrets: inherit release: name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/v') needs: [merge, mobile] runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: write steps: - name: Download Android APK artifact uses: actions/download-artifact@v4.3.0 with: name: app-release-apk path: ${{ runner.temp }}/mobile-artifacts - name: Download iOS build artifact uses: actions/download-artifact@v4.3.0 with: name: ios-build-unsigned path: ${{ runner.temp }}/ios-build - name: Prepare release assets run: | mkdir -p ${{ runner.temp }}/release-assets echo "=== Debugging: List downloaded artifacts ===" echo "Mobile artifacts:" ls -laR "${{ runner.temp }}/mobile-artifacts" || echo "No mobile-artifacts directory" echo "iOS build:" ls -laR "${{ runner.temp }}/ios-build" || echo "No ios-build directory" echo "===========================================" # Copy debug APK if it exists if [ -f "${{ runner.temp }}/mobile-artifacts/app-debug.apk" ]; then cp "${{ runner.temp }}/mobile-artifacts/app-debug.apk" "${{ runner.temp }}/release-assets/sure-${{ github.ref_name }}-debug.apk" echo "✓ Debug APK prepared" fi # Copy release APK if it exists if [ -f "${{ runner.temp }}/mobile-artifacts/app-release.apk" ]; then cp "${{ runner.temp }}/mobile-artifacts/app-release.apk" "${{ runner.temp }}/release-assets/sure-${{ github.ref_name }}.apk" echo "✓ Release APK prepared" fi # Create iOS app archive (zip the .app bundle) # Path preserves directory structure from artifact upload if [ -d "${{ runner.temp }}/ios-build/ios/iphoneos/Runner.app" ]; then cd "${{ runner.temp }}/ios-build/ios/iphoneos" zip -r "${{ runner.temp }}/release-assets/sure-${{ github.ref_name }}-ios-unsigned.zip" Runner.app echo "✓ iOS build archive prepared" fi # Copy iOS build info if [ -f "${{ runner.temp }}/ios-build/ios-build-info.txt" ]; then cp "${{ runner.temp }}/ios-build/ios-build-info.txt" "${{ runner.temp }}/release-assets/" fi echo "Release assets:" ls -la "${{ runner.temp }}/release-assets/" - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} name: ${{ github.ref_name }} draft: false prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} generate_release_notes: true files: | ${{ runner.temp }}/release-assets/* body: | ## Mobile Debug Builds This release includes debug builds of the mobile applications. Download from the `Assets` area below. - **Android APK**: Debug build for testing on Android devices - **iOS Build**: Unsigned iOS build (requires code signing for installation) > **Note**: These are debug builds intended for testing purposes. For production use, please build from source with proper signing credentials. bump-pre_release-version: name: Bump Pre-release Version if: startsWith(github.ref, 'refs/tags/v') && (contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc')) needs: [merge] runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: write steps: - name: Determine source branch for tag id: source_branch run: | # Fetch all branches to find which one contains this tag's commit git init --quiet git remote add origin "https://github.com/${{ github.repository }}.git" git fetch origin --quiet # Find branches containing the tagged commit BRANCHES=$(git branch -r --contains ${{ github.sha }} | grep -v HEAD | sed 's/origin\///' | xargs) echo "Branches containing commit: $BRANCHES" # Prefer non-main branches (release branches) over main SOURCE_BRANCH="main" for branch in $BRANCHES; do if [ "$branch" != "main" ] && [ "$branch" != "master" ]; then SOURCE_BRANCH="$branch" break fi done echo "Selected source branch: $SOURCE_BRANCH" echo "branch=$SOURCE_BRANCH" >> $GITHUB_OUTPUT - name: Check out source branch uses: actions/checkout@v4.2.0 with: ref: ${{ steps.source_branch.outputs.branch }} token: ${{ secrets.GH_PAT }} - name: Bump pre-release version run: | VERSION_FILE="config/initializers/version.rb" CHART_FILE="charts/sure/Chart.yaml" # Ensure version file exists if [ ! -f "$VERSION_FILE" ]; then echo "ERROR: Version file not found: $VERSION_FILE" exit 1 fi # Ensure chart file exists if [ ! -f "$CHART_FILE" ]; then echo "ERROR: Chart file not found: $CHART_FILE" exit 1 fi # Extract current version CURRENT_VERSION=$(grep -oP '"\K[0-9]+\.[0-9]+\.[0-9]+-(alpha|beta|rc)\.[0-9]+' "$VERSION_FILE") if [ -z "$CURRENT_VERSION" ]; then echo "ERROR: Could not extract version from $VERSION_FILE" exit 1 fi echo "Current version: $CURRENT_VERSION" # Extract the pre-release tag and number, then increment it PRE_RELEASE_TAG=$(echo "$CURRENT_VERSION" | grep -oP '(alpha|beta|rc)') if [ -z "$PRE_RELEASE_TAG" ]; then echo "ERROR: Could not extract pre-release tag from $CURRENT_VERSION" exit 1 fi PRE_RELEASE_NUM=$(echo "$CURRENT_VERSION" | grep -oP '(alpha|beta|rc)\.\K[0-9]+') if [ -z "$PRE_RELEASE_NUM" ]; then echo "ERROR: Could not extract pre-release number from $CURRENT_VERSION" exit 1 fi NEW_PRE_RELEASE_NUM=$((PRE_RELEASE_NUM + 1)) # Create new version string BASE_VERSION=$(echo "$CURRENT_VERSION" | grep -oP '^[0-9]+\.[0-9]+\.[0-9]+') if [ -z "$BASE_VERSION" ]; then echo "ERROR: Could not extract base version from $CURRENT_VERSION" exit 1 fi NEW_VERSION="${BASE_VERSION}-${PRE_RELEASE_TAG}.${NEW_PRE_RELEASE_NUM}" echo "New version: $NEW_VERSION" # Update the version file sed -i "s/\"$CURRENT_VERSION\"/\"$NEW_VERSION\"/" "$VERSION_FILE" # Verify the change echo "Updated version.rb:" grep "semver" "$VERSION_FILE" # Update Helm chart version and appVersion sed -i -E "s/^version: .*/version: ${NEW_VERSION}/" "$CHART_FILE" sed -i -E "s/^appVersion: .*/appVersion: \"${NEW_VERSION}\"/" "$CHART_FILE" # Verify the change echo "Updated Chart.yaml:" grep -E "^(version|appVersion):" "$CHART_FILE" - name: Commit and push version bump env: SOURCE_BRANCH: ${{ steps.source_branch.outputs.branch }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add config/initializers/version.rb git add charts/sure/Chart.yaml # Check if there are changes to commit if git diff --cached --quiet; then echo "No changes to commit - version may have already been bumped" exit 0 fi git commit -m "Bump version to next iteration after ${{ github.ref_name }} release" echo "Pushing to branch: $SOURCE_BRANCH" # Push with retry logic attempts=0 until git push origin HEAD:$SOURCE_BRANCH; do attempts=$((attempts + 1)) if [[ $attempts -ge 4 ]]; then echo "ERROR: Failed to push after 4 attempts." >&2 exit 1 fi delay=$((2 ** attempts)) echo "Push failed (attempt $attempts). Retrying in ${delay} seconds..." sleep ${delay} git pull --rebase origin $SOURCE_BRANCH done