kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor(release)!: simplify and streamline release workflow
This commit refactors the entire release process to be simpler, more robust, and less error-prone. The previous complex system of manual version calculation, artifact passing, and dry-run flags has been replaced with a more streamlined, two-stage process driven by distinct GitHub Actions workflows. The new process separates tag creation from the release engine. A new `create-internal-release.yml` workflow is introduced to handle the creation and iteration of internal build tags (e.g., `v1.2.3-internal.1`, `v1.2.3-internal.2`). The main `release.yml` workflow is now triggered solely by the presence of a new version tag. Key changes include: - The `release.yml` workflow no longer takes `dry_run` or `pr_number` inputs, as tag creation is now handled separately. - Version code is now consistently calculated once per build based on Git history and an offset, removing the complex logic for handling hotfixes and reusing version codes via artifacts. - Builds only occur if a release for the given `BASE_TAG` doesn't already exist on the same commit. This makes the process idempotent for promotions. - A new `prepare-release-environment` job intelligently handles superseded releases by deleting old releases and tags if a new tag for the same base version points to a different commit. - The build process is split into separate jobs for Google Play (`release-google`) and F-Droid (`release-fdroid`), allowing them to run in parallel. - Build attestations are now generated for all artifacts to improve supply chain security. - The `create-internal-release.yml` workflow is simplified to only require a `base_version`, automatically calculating the next internal iteration number. It no longer handles complex version bumping logic (`major`, `minor`, `patch`, `hotfix`). - Fastlane scripts have been cleaned up to remove now-unused lanes (`get_internal_track_version_code`). - The `RELEASE_PROCESS.md` documentation has been completely rewritten to reflect the new, simpler workflow. BREAKING CHANGE: The release workflow has been fundamentally changed. - The `release.yml` workflow is no longer triggered with `workflow_dispatch` inputs like `dry_run`. Releases are now initiated by pushing a correctly formatted tag. - The `create-internal-release.yml` workflow has been changed. It no longer supports `release_type` (`auto`, `patch`, etc.) and instead requires a specific `base_version` to create an internal build tag for. - Hotfix handling via special tags like `-hotfix` is removed. Superseding a release is now handled by creating a new internal iteration for the same base version from a newer commit. Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>pull/3309/head
rodzic
a7183cc8ca
commit
b884c83980
|
@ -3,15 +3,9 @@ name: Create Internal Release Tag
|
|||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_type:
|
||||
description: "Type of release (auto|patch|minor|major|hotfix)"
|
||||
base_version:
|
||||
description: "Base version to iterate on (e.g. 2.6.7). The next internal iteration will be created for this version."
|
||||
required: true
|
||||
default: auto
|
||||
type: choice
|
||||
options: [auto, patch, minor, major, hotfix]
|
||||
hotfix_base_version:
|
||||
description: "Base version for hotfix (e.g. 2.5.0) required if release_type=hotfix"
|
||||
required: false
|
||||
dry_run:
|
||||
description: "If true, calculate but do not push tag"
|
||||
required: false
|
||||
|
@ -29,86 +23,21 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine Latest Base Version
|
||||
id: latest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# List base tags (exclude track/hotfix suffixes)
|
||||
BASE_TAGS=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | head -n1 || true)
|
||||
echo "Found latest base tag: $BASE_TAGS"
|
||||
echo "latest_base_tag=$BASE_TAGS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compute Next Version (auto/patch/minor/major)
|
||||
id: compute
|
||||
if: ${{ inputs.release_type != 'hotfix' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RTYPE='${{ inputs.release_type }}'
|
||||
LAST='${{ steps.latest.outputs.latest_base_tag }}'
|
||||
if [ -z "$LAST" ]; then
|
||||
BASE_MAJOR=0; BASE_MINOR=0; BASE_PATCH=0
|
||||
else
|
||||
V=${LAST#v}
|
||||
IFS='.' read -r BASE_MAJOR BASE_MINOR BASE_PATCH <<< "$V"
|
||||
fi
|
||||
if [ "$RTYPE" = 'auto' ]; then
|
||||
echo "Determining bump type from commits since $LAST..."
|
||||
RANGE="$LAST..HEAD"
|
||||
[ -z "$LAST" ] && RANGE="HEAD" # first release
|
||||
LOG=$(git log --format=%s $RANGE || true)
|
||||
BUMP="patch"
|
||||
if echo "$LOG" | grep -Eiq 'BREAKING CHANGE'; then BUMP=major; fi
|
||||
if echo "$LOG" | grep -Eiq '^[a-zA-Z]+!:'; then BUMP=major; fi
|
||||
if [ "$BUMP" != major ] && echo "$LOG" | grep -Eiq '^feat(\(|:)' ; then BUMP=minor; fi
|
||||
RTYPE=$BUMP
|
||||
echo "Auto-detected bump: $RTYPE"
|
||||
fi
|
||||
case "$RTYPE" in
|
||||
major)
|
||||
NEW_MAJOR=$((BASE_MAJOR+1)); NEW_MINOR=0; NEW_PATCH=0;;
|
||||
minor)
|
||||
NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$((BASE_MINOR+1)); NEW_PATCH=0;;
|
||||
patch)
|
||||
NEW_MAJOR=$BASE_MAJOR; NEW_MINOR=$BASE_MINOR; NEW_PATCH=$((BASE_PATCH+1));;
|
||||
*) echo "Unsupported release_type for this step: $RTYPE"; exit 1;;
|
||||
esac
|
||||
NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
echo "base_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compute Hotfix Version
|
||||
id: hotfix
|
||||
if: ${{ inputs.release_type == 'hotfix' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE='${{ inputs.hotfix_base_version }}'
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "hotfix_base_version required for hotfix release_type" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! git tag --list | grep -q "^v$BASE$"; then
|
||||
echo "Base version tag v$BASE not found (production tag required)." >&2
|
||||
exit 1
|
||||
fi
|
||||
EXISTING=$(git tag --list "v${BASE}-hotfix*" | sed -E 's/^v[0-9]+\.[0-9]+\.[0-9]+-hotfix([0-9]+).*$/\1/' | sort -n | tail -1 || true)
|
||||
if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi
|
||||
HOTFIX_VERSION="${BASE}-hotfix${NEXT}"
|
||||
echo "hotfix_version=$HOTFIX_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Decide Internal Tag
|
||||
- name: Compute Tag
|
||||
id: tag
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ '${{ inputs.release_type }}' = 'hotfix' ]; then
|
||||
BASE='${{ steps.hotfix.outputs.hotfix_version }}'
|
||||
else
|
||||
BASE='${{ steps.compute.outputs.base_version }}'
|
||||
fi
|
||||
INTERNAL_TAG="v${BASE}-internal.1"
|
||||
if git tag --list | grep -q "^${INTERNAL_TAG}$"; then
|
||||
echo "Tag ${INTERNAL_TAG} already exists." >&2
|
||||
BASE='${{ inputs.base_version }}'
|
||||
# Find the highest existing internal tag for this base version and increment it.
|
||||
EXISTING=$(git tag --list "v${BASE}-internal.*" | sed -E 's/^v.*-internal\.([0-9]+)$/\1/' | sort -n | tail -1 || true)
|
||||
if [ -z "$EXISTING" ]; then NEXT=1; else NEXT=$((EXISTING+1)); fi
|
||||
FINAL_TAG="v${BASE}-internal.${NEXT}"
|
||||
# Check if the tag already exists for some reason (e.g. race condition).
|
||||
if git tag --list | grep -q "^${FINAL_TAG}$"; then
|
||||
echo "Tag ${FINAL_TAG} already exists." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "internal_tag=$INTERNAL_TAG" >> $GITHUB_OUTPUT
|
||||
echo "internal_tag=$FINAL_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Dry Run Preview
|
||||
if: ${{ inputs.dry_run == 'true' }}
|
||||
|
@ -120,7 +49,7 @@ jobs:
|
|||
if: ${{ inputs.dry_run != 'true' }}
|
||||
run: |
|
||||
TAG='${{ steps.tag.outputs.internal_tag }}'
|
||||
MSG="Initial internal build for ${TAG}"
|
||||
MSG="Internal build iteration for ${TAG}"
|
||||
git tag -a "$TAG" -m "$MSG"
|
||||
git push origin "$TAG"
|
||||
echo "Created and pushed $TAG"
|
||||
|
@ -129,11 +58,5 @@ jobs:
|
|||
run: |
|
||||
echo "### Internal Tag Created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tag: ${{ steps.tag.outputs.internal_tag }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Release Type: ${{ inputs.release_type }}" >> $GITHUB_STEP_SUMMARY
|
||||
if [ '${{ inputs.release_type }}' = 'hotfix' ]; then
|
||||
echo "Base Hotfix Series: ${{ steps.hotfix.outputs.hotfix_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Base Version: ${{ steps.compute.outputs.base_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
echo "Base Version: ${{ inputs.base_version }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Dry Run: ${{ inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
|
|
|
@ -2,16 +2,6 @@ name: Make Release
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: "If true, simulate the release without building, uploading, promoting, or creating a GitHub release"
|
||||
required: false
|
||||
default: "false"
|
||||
type: choice
|
||||
options: ["false", "true"]
|
||||
pr_number:
|
||||
description: "Optional PR number to comment on with dry-run readiness summary"
|
||||
required: false
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
@ -31,10 +21,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }}
|
||||
FINAL_VERSION_CODE: ${{ steps.final_version_code.outputs.FINAL_VERSION_CODE }}
|
||||
HOTFIX_PATCH: ${{ steps.is_hotfix_patch.outputs.hotfix_patch }}
|
||||
APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
BASE_TAG: ${{ steps.get_base_tag.outputs.BASE_TAG }}
|
||||
FULL_TAG: ${{ steps.get_full_tag.outputs.FULL_TAG }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
@ -58,21 +46,16 @@ jobs:
|
|||
id: get_version_name
|
||||
run: echo "APP_VERSION_NAME=$(echo ${GITHUB_REF_NAME#v} | sed 's/-.*//')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Full Tag
|
||||
id: get_full_tag
|
||||
run: echo "FULL_TAG=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Base Tag (for release/artifact naming)
|
||||
id: get_base_tag
|
||||
run: |
|
||||
# Remove track/iteration suffix (e.g., -internal.1, -closed.1, -open.1)
|
||||
BASE_TAG=$(echo ${GITHUB_REF_NAME} | sed 's/\(-internal\.[0-9]\+\|-closed\.[0-9]\+\|-open\.[0-9]\+\)$//')
|
||||
echo "BASE_TAG=$BASE_TAG" >> $GITHUB_OUTPUT
|
||||
VERSION_NAME=$(echo ${GITHUB_REF_NAME#v} | sed 's/-.*//')
|
||||
echo "BASE_TAG=v${VERSION_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract VERSION_CODE_OFFSET from config.properties
|
||||
id: get_version_code_offset
|
||||
run: |
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut -d'=' -f2)
|
||||
OFFSET=$(grep '^VERSION_CODE_OFFSET=' config.properties | cut-d'=' -f2)
|
||||
echo "VERSION_CODE_OFFSET=$OFFSET" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Calculate Version Code from Git Commit Count
|
||||
|
@ -83,91 +66,40 @@ jobs:
|
|||
VERSION_CODE=$((COMMIT_COUNT + OFFSET))
|
||||
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
# This matches the reproducible versionCode strategy: versionCode = git commit count + offset
|
||||
|
||||
- name: Check if Hotfix or Patch
|
||||
id: is_hotfix_patch
|
||||
run: |
|
||||
TAG_LOWER=$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$TAG_LOWER" == *"-hotfix"* || "$TAG_LOWER" == *"-patch"* ]]; then
|
||||
echo "hotfix_patch=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "hotfix_patch=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download Version Code Artifact (if exists)
|
||||
id: try_download_version_code
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: version-code
|
||||
path: .
|
||||
|
||||
- name: Generate and Store Version Code (first build for regular release)
|
||||
if: steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.try_download_version_code.outcome != 'success'
|
||||
id: generate_and_store_version_code
|
||||
run: |
|
||||
VERSION_CODE=${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
echo "$VERSION_CODE" > version_code.txt
|
||||
|
||||
- name: Upload Version Code Artifact (if generated)
|
||||
if: |
|
||||
(steps.is_hotfix_patch.outputs.hotfix_patch == 'true') ||
|
||||
(steps.is_hotfix_patch.outputs.hotfix_patch == 'false' && steps.generate_and_store_version_code.conclusion == 'success')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: version-code
|
||||
path: version_code.txt
|
||||
|
||||
- name: Set Version Code from Artifact (if exists)
|
||||
if: steps.try_download_version_code.outcome == 'success'
|
||||
id: set_version_code_from_artifact
|
||||
run: |
|
||||
VERSION_CODE=$(cat version_code.txt)
|
||||
echo "versionCode=$VERSION_CODE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set Final Version Code Output
|
||||
id: final_version_code
|
||||
run: |
|
||||
if [ -f version_code.txt ]; then
|
||||
FV=$(cat version_code.txt)
|
||||
else
|
||||
FV=${{ steps.calculate_version_code.outputs.versionCode }}
|
||||
fi
|
||||
echo "FINAL_VERSION_CODE=$FV" >> $GITHUB_OUTPUT
|
||||
|
||||
check-internal-release:
|
||||
prepare-release-environment:
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare-build-info
|
||||
outputs:
|
||||
exists: ${{ steps.check_release.outputs.exists }}
|
||||
exists: ${{ steps.check_and_clean.outputs.exists }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for existing GitHub release
|
||||
id: check_release
|
||||
- name: Check for Existing Release and Clean if Superseded
|
||||
id: check_and_clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BASE_TAG=${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
COMMIT_SHA=$(git rev-parse HEAD)
|
||||
EXISTING_RELEASE=$(gh release list --limit 100 --json tagName,targetCommitish | jq -r --arg BASE_TAG "$BASE_TAG" --arg COMMIT_SHA "$COMMIT_SHA" '.[] | select(.tagName == $BASE_TAG and .targetCommitish.oid == $COMMIT_SHA)')
|
||||
if [ -n "$EXISTING_RELEASE" ]; then
|
||||
echo "An existing release with tag '${BASE_TAG}' was found for this commit."
|
||||
RELEASE_INFO=$(gh release view $BASE_TAG --json targetCommitish -q ".targetCommitish.oid" || echo "")
|
||||
if [ -z "$RELEASE_INFO" ]; then
|
||||
echo "No existing release for tag '${BASE_TAG}'. Starting fresh."
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
elif [ "$RELEASE_INFO" == "$COMMIT_SHA" ]; then
|
||||
echo "Existing release for '${BASE_TAG}' found on the current commit. This is a promotion."
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "No existing release found for this commit."
|
||||
echo "Existing release for '${BASE_TAG}' found on a DIFFERENT commit ($RELEASE_INFO)."
|
||||
echo "This new tag supersedes the old one. Deleting old release and tag to restart the process."
|
||||
gh release delete $BASE_TAG --cleanup-tag --yes || echo "Could not delete release. It might have been deleted already."
|
||||
echo "Old release and tag deleted. A new build will be created."
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
release-google:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, check-internal-release]
|
||||
outputs:
|
||||
INTERNAL_VERSION_CODE: ${{ steps.resolve_internal_version_code.outputs.INTERNAL_VERSION_CODE }}
|
||||
needs: [prepare-build-info, prepare-release-environment]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
@ -213,258 +145,129 @@ jobs:
|
|||
ruby-version: '3.2'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Dry Run Sanity Version Code Check
|
||||
if: github.event.inputs.dry_run == 'true'
|
||||
id: dry_run_sanity
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Performing version code sanity check (dry run)..."
|
||||
# Query highest existing version code across tracks
|
||||
bundle exec fastlane get_highest_version_code || true
|
||||
HIGHEST=$(cat highest_version_code.txt 2>/dev/null || echo 0)
|
||||
HOTFIX='${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}'
|
||||
if [ "$HOTFIX" = "true" ]; then
|
||||
PLANNED=$((HIGHEST + 1))
|
||||
echo "Hotfix planned versionCode: $PLANNED (highest existing: $HIGHEST)";
|
||||
STATUS=ok
|
||||
else
|
||||
# Regular base release planned version code (commit count + offset or reused artifact)
|
||||
PLANNED='${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }}'
|
||||
if [ -z "$PLANNED" ]; then PLANNED=0; fi
|
||||
if [ "$PLANNED" -le "$HIGHEST" ]; then
|
||||
echo "ERROR: Planned versionCode $PLANNED is not greater than highest existing $HIGHEST. Adjust VERSION_CODE_OFFSET or convert to hotfix." >&2
|
||||
STATUS=fail
|
||||
else
|
||||
echo "Planned versionCode $PLANNED is greater than existing $HIGHEST: OK";
|
||||
STATUS=ok
|
||||
fi
|
||||
fi
|
||||
echo "sanity_status=$STATUS" >> $GITHUB_OUTPUT
|
||||
echo "sanity_highest=$HIGHEST" >> $GITHUB_OUTPUT
|
||||
echo "sanity_planned=$PLANNED" >> $GITHUB_OUTPUT
|
||||
# Promotion policy validation (if this is a promotion tag in dry run)
|
||||
TAG='${{ github.ref_name }}'
|
||||
if echo "$TAG" | grep -Eq '-(closed|open)$' || [[ "$TAG" != *"-internal"* && "$TAG" != *"-closed"* && "$TAG" != *"-open"* && "$TAG" == v* ]]; then
|
||||
echo "Checking promotion policy (dry run)..."
|
||||
if ! bundle exec fastlane get_internal_track_version_code; then
|
||||
echo "ERROR: Promotion attempted but no internal artifact present." >&2
|
||||
echo "promotion_status=fail" >> $GITHUB_OUTPUT
|
||||
[ "$STATUS" = ok ] || true
|
||||
STATUS=fail
|
||||
else
|
||||
echo "Internal artifact present for promotion.";
|
||||
echo "promotion_status=ok" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "Not a promotion tag (internal build).";
|
||||
echo "promotion_status=na" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ "$STATUS" = fail ]; then
|
||||
echo "Dry run sanity check failed." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Resolve Version Code For Internal Build
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
id: resolve_internal_version_code
|
||||
run: |
|
||||
if [ "${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" = "true" ]; then
|
||||
echo "Hotfix/Patch detected; querying Google Play for highest version code..."
|
||||
bundle exec fastlane get_highest_version_code
|
||||
CODE=$(cat highest_version_code.txt || echo 0)
|
||||
NEXT_CODE=$((CODE + 1))
|
||||
# Race mitigation: re-query to ensure no concurrent allocation
|
||||
bundle exec fastlane get_highest_version_code
|
||||
NEW_HIGHEST=$(cat highest_version_code.txt || echo 0)
|
||||
if [ "$NEW_HIGHEST" -ge "$NEXT_CODE" ]; then
|
||||
echo "Detected race: highest changed from $CODE to $NEW_HIGHEST; bumping again.";
|
||||
NEXT_CODE=$((NEW_HIGHEST + 1))
|
||||
fi
|
||||
echo "Using hotfix version code: $NEXT_CODE (previous highest final: $NEW_HIGHEST)"
|
||||
echo "INTERNAL_VERSION_CODE=$NEXT_CODE" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_CODE=$NEXT_CODE" >> $GITHUB_ENV
|
||||
else
|
||||
BASE_CODE=${{ needs.prepare-build-info.outputs.FINAL_VERSION_CODE }}
|
||||
echo "Using base/internal version code: $BASE_CODE"
|
||||
echo "INTERNAL_VERSION_CODE=$BASE_CODE" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_CODE=$BASE_CODE" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Build and Deploy to Internal Track
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
if: needs.prepare-release-environment.outputs.exists == 'false'
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ env.VERSION_CODE }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane internal
|
||||
|
||||
- name: Build F-Droid (same version code)
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ env.VERSION_CODE }}
|
||||
run: bundle exec fastlane fdroid_build
|
||||
|
||||
- name: Generate Build Metadata & Checksums
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
id: gen_metadata
|
||||
- name: Determine Fastlane Promotion Lane
|
||||
id: fastlane_lane
|
||||
if: "!contains(github.ref_name, '-internal')"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
AAB=app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
APK_GOOGLE=app/build/outputs/apk/google/release/app-google-release.apk
|
||||
APK_FDROID=app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
SHA_AAB=$(sha256sum "$AAB" | cut -d' ' -f1)
|
||||
SHA_APK_GOOGLE=$(sha256sum "$APK_GOOGLE" | cut -d' ' -f1)
|
||||
SHA_APK_FDROID=$(sha256sum "$APK_FDROID" | cut -d' ' -f1)
|
||||
GIT_SHA=$(git rev-parse HEAD)
|
||||
BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
cat > build-metadata.json <<EOF
|
||||
{
|
||||
"baseTag": "${{ needs.prepare-build-info.outputs.BASE_TAG }}",
|
||||
"fullTag": "${{ needs.prepare-build-info.outputs.FULL_TAG }}",
|
||||
"versionName": "${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}",
|
||||
"versionCode": "${{ env.VERSION_CODE }}",
|
||||
"hotfixOrPatch": "${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}",
|
||||
"gitSha": "$GIT_SHA",
|
||||
"buildTimeUtc": "$BUILD_TIME",
|
||||
"artifacts": {
|
||||
"googleAab": { "path": "$AAB", "sha256": "$SHA_AAB" },
|
||||
"googleApk": { "path": "$APK_GOOGLE", "sha256": "$SHA_APK_GOOGLE" },
|
||||
"fdroidApk": { "path": "$APK_FDROID", "sha256": "$SHA_APK_FDROID" }
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Generated build-metadata.json:"; cat build-metadata.json
|
||||
echo "AAB_SHA256=$SHA_AAB" >> $GITHUB_ENV
|
||||
echo "APK_GOOGLE_SHA256=$SHA_APK_GOOGLE" >> $GITHUB_ENV
|
||||
echo "APK_FDROID_SHA256=$SHA_APK_FDROID" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Build Metadata Artifact
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-metadata
|
||||
path: build-metadata.json
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload F-Droid APK artifact
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fdroid-apk
|
||||
path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Promotion Guard - Internal Must Exist
|
||||
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
set -e
|
||||
echo "Validating internal track has an artifact before promotion..."
|
||||
if ! bundle exec fastlane get_internal_track_version_code; then
|
||||
echo "ERROR: No internal artifact found to promote. Ensure an internal tag was built first." >&2
|
||||
exit 1
|
||||
TAG_NAME="${{ github.ref_name }}"
|
||||
if [[ "$TAG_NAME" == *"-closed"* ]]; then
|
||||
echo "lane=closed" >> $GITHUB_OUTPUT
|
||||
elif [[ "$TAG_NAME" == *"-open"* ]]; then
|
||||
echo "lane=open" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "lane=production" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Fetch Internal Track Version Code (for promotion)
|
||||
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
bundle exec fastlane get_internal_track_version_code
|
||||
CODE=$(cat internal_version_code.txt)
|
||||
echo "INTERNAL_VERSION_CODE=$CODE" >> $GITHUB_ENV
|
||||
|
||||
- name: Promote on Google Play
|
||||
if: steps.fastlane_lane.outputs.lane != '' && github.event.inputs.dry_run != 'true'
|
||||
if: "steps.fastlane_lane.outputs.lane != ''"
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ env.INTERNAL_VERSION_CODE }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane ${{ steps.fastlane_lane.outputs.lane }}
|
||||
|
||||
- name: Build Summary (Internal Build)
|
||||
if: needs.check-internal-release.outputs.exists == 'false' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
{
|
||||
echo "### Internal Build Summary"
|
||||
echo "Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}"
|
||||
echo "Version Code: ${{ env.VERSION_CODE }}"
|
||||
echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}"
|
||||
echo "Full Tag: ${{ needs.prepare-build-info.outputs.FULL_TAG }}"
|
||||
echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}"
|
||||
echo "Google AAB SHA256: $AAB_SHA256"
|
||||
echo "Google APK SHA256: $APK_GOOGLE_SHA256"
|
||||
echo "F-Droid APK SHA256: $APK_FDROID_SHA256"
|
||||
} >> $GITHUB_STEP_SUMMARY
|
||||
- name: Upload Google AAB artifact
|
||||
if: needs.prepare-release-environment.outputs.exists == 'false'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: google-aab
|
||||
path: app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
retention-days: 1
|
||||
|
||||
- name: Dry Run Summary
|
||||
if: github.event.inputs.dry_run == 'true'
|
||||
run: |
|
||||
echo "### Release Dry Run" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tag: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Computed Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Planned Version Code Strategy: $([[ '${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}' == 'true' ]] && echo 'highest+1 from Play' || echo 'commit-count+offset (first internal) or reuse')" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Sanity Highest Existing VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_highest }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Sanity Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Promotion Policy Check: ${{ steps.dry_run_sanity.outputs.promotion_status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Would build internal artifacts: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo 'no (already exists)')" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Would promote lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a')" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Would create or update draft GitHub release: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo yes || echo no)" >> $GITHUB_STEP_SUMMARY
|
||||
- name: Upload Google APK artifact
|
||||
if: needs.prepare-release-environment.outputs.exists == 'false'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: google-apk
|
||||
path: app/build/outputs/apk/google/release/app-google-release.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Post Dry Run PR Comment
|
||||
if: github.event.inputs.dry_run == 'true' && github.event.inputs.pr_number != ''
|
||||
- name: Attest Google artifacts provenance
|
||||
if: needs.prepare-release-environment.outputs.exists == 'false'
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-path: |
|
||||
app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
app/build/outputs/apk/google/release/app-google-release.apk
|
||||
|
||||
release-fdroid:
|
||||
if: needs.prepare-release-environment.outputs.exists == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, prepare-release-environment]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: 'recursive'
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'jetbrains'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service'
|
||||
build-scan-terms-of-use-agree: 'yes'
|
||||
|
||||
- name: Load secrets
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
run: |
|
||||
BODY=$(cat <<'EOT'
|
||||
Release Dry Run Summary
|
||||
----------------------
|
||||
Tag: ${{ github.ref_name }}
|
||||
Base Tag: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
Version Name: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
Hotfix/Patch: ${{ needs.prepare-build-info.outputs.HOTFIX_PATCH }}
|
||||
Planned VersionCode: ${{ steps.dry_run_sanity.outputs.sanity_planned }} (highest existing: ${{ steps.dry_run_sanity.outputs.sanity_highest }})
|
||||
Sanity Status: ${{ steps.dry_run_sanity.outputs.sanity_status }}
|
||||
Promotion Policy: ${{ steps.dry_run_sanity.outputs.promotion_status }}
|
||||
Would Promote Lane: $([[ -n '${{ steps.fastlane_lane.outputs.lane }}' ]] && echo '${{ steps.fastlane_lane.outputs.lane }}' || echo 'n/a')
|
||||
Draft Release Action: $([[ '${{ needs.check-internal-release.outputs.exists }}' == 'false' ]] && echo 'would create/update' || echo 'none')
|
||||
EOT
|
||||
)
|
||||
gh pr comment ${{ github.event.inputs.pr_number }} --body "$BODY" || echo "Failed to post PR comment (verify pr_number)"
|
||||
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
|
||||
- name: Setup Fastlane
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Build F-Droid with Fastlane
|
||||
env:
|
||||
VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}
|
||||
VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}
|
||||
run: bundle exec fastlane fdroid_build
|
||||
|
||||
- name: Upload F-Droid APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fdroid-apk
|
||||
path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
retention-days: 1
|
||||
|
||||
- name: Attest F-Droid APK provenance
|
||||
uses: actions/attest-build-provenance@v3
|
||||
with:
|
||||
subject-path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk
|
||||
|
||||
manage-github-release:
|
||||
if: github.event.inputs.dry_run != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-build-info, check-internal-release, release-google]
|
||||
needs: [prepare-build-info, prepare-release-environment, release-google, release-fdroid]
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
if: needs.prepare-release-environment.outputs.exists == 'false'
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
- name: Compute Release Name (Channel Aware)
|
||||
id: release_name
|
||||
run: |
|
||||
BASE='${{ needs.prepare-build-info.outputs.BASE_TAG }}'
|
||||
REF='${{ github.ref_name }}'
|
||||
if [[ "$REF" == *"-internal"* ]]; then
|
||||
NAME="$BASE (internal)"
|
||||
elif [[ "$REF" == *"-closed"* ]]; then
|
||||
NAME="$BASE (closed testing)"
|
||||
elif [[ "$REF" == *"-open"* ]]; then
|
||||
NAME="$BASE (open beta)"
|
||||
else
|
||||
NAME="$BASE"
|
||||
fi
|
||||
echo "Computed release name: $NAME"
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
if: needs.prepare-release-environment.outputs.exists == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
name: ${{ steps.release_name.outputs.name }}
|
||||
name: v${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}-internal
|
||||
generate_release_notes: true
|
||||
files: ./artifacts/*/*
|
||||
draft: true
|
||||
|
@ -493,19 +296,8 @@ jobs:
|
|||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare-build-info.outputs.BASE_TAG }}
|
||||
name: ${{ steps.release_name.outputs.name }}
|
||||
name: ${{ github.ref_name }}
|
||||
draft: ${{ steps.release_properties.outputs.draft }}
|
||||
prerelease: ${{ steps.release_properties.outputs.prerelease }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Append Metadata to Release Notes
|
||||
if: needs.check-internal-release.outputs.exists == 'false'
|
||||
run: |
|
||||
if [ -f artifacts/build-metadata/build-metadata.json ]; then
|
||||
echo "\n---\nBuild Metadata JSON:\n" > appended_notes.txt
|
||||
cat artifacts/build-metadata/build-metadata.json >> appended_notes.txt
|
||||
gh release edit ${{ needs.prepare-build-info.outputs.BASE_TAG }} --notes-file appended_notes.txt
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
@ -1,83 +1,70 @@
|
|||
# Meshtastic-Android Release Process (Condensed)
|
||||
# Release Process
|
||||
|
||||
This guide summarizes the steps for releasing a new version of Meshtastic-Android. The process is automated via GitHub Actions and Fastlane, triggered by pushing a Git tag from a `release/*` branch.
|
||||
This document outlines the process for creating and promoting releases for the Meshtastic Android application. The system is designed to be robust, auditable, and highly automated, using a combination of user-facing GitHub Actions "wizards" and a central, tag-triggered "engine".
|
||||
|
||||
## Overview
|
||||
- **Tagging:** Push a tag (`vX.X.X[-track.Y]`) from a `release/*` branch to start the release workflow.
|
||||
- **CI Automation:** Builds both flavors, uploads to Google Play (correct track), and creates/updates a draft GitHub release.
|
||||
- **Changelog:** Release notes are auto-generated from PR labels via [`.github/release.yml`](.github/release.yml). Label PRs for accurate changelogs.
|
||||
- **Draft Release:** All tags for the same base version (e.g., `v2.3.5`) update the same draft release. The release title uses the full tag (e.g., `v2.3.5-internal.1`).
|
||||
## Philosophy
|
||||
|
||||
## Tagging & Tracks
|
||||
- **Internal:** `vX.X.X-internal.Y`
|
||||
- **Closed:** `vX.X.X-closed.Y`
|
||||
- **Open:** `vX.X.X-open.Y`
|
||||
- **Production:** `vX.X.X`
|
||||
- Increment `.Y` for fixes/iterations.
|
||||
- **Git Tag Driven**: The entire release lifecycle is initiated and controlled by pushing version tags to the repository.
|
||||
- **Automated Engine**: A central workflow (`release.yml`) acts as the engine, listening for new version tags. It handles all the heavy lifting: building, versioning, uploading to Google Play, and managing GitHub Releases.
|
||||
- **User-Friendly Wizards**: Manually creating tags is discouraged. Instead, two "wizard" workflows (`create-internal-release.yml` and `promote-release.yml`) provide a simple UI in the GitHub Actions tab to guide developers through creating and promoting releases safely.
|
||||
|
||||
## Release Steps
|
||||
1. **Branch:** Create `release/X.X.X` from `main`. Only critical fixes allowed.
|
||||
2. **Tag & Push:** Tag the release commit and push (see below).
|
||||
3. **CI:** Wait for CI to finish. Artifacts are uploaded, and a draft GitHub release is created/updated.
|
||||
4. **Verify:** Check Google Play Console and GitHub draft release.
|
||||
5. **Promote:** Tag the same commit for the next track as needed.
|
||||
6. **Finalize:**
|
||||
- **Production:** Publish the GitHub release, then promote in Google Play Console.
|
||||
- **Other tracks:** Verify with testers.
|
||||
7. **Merge:** After production, merge `release/X.X.X` back to `main` and delete the branch.
|
||||
## Versioning Scheme
|
||||
|
||||
## Tagging Example
|
||||
```bash
|
||||
# On release branch
|
||||
git tag v2.3.5-internal.1
|
||||
git push origin v2.3.5-internal.1
|
||||
# For fixes:
|
||||
git tag v2.3.5-internal.2
|
||||
git push origin v2.3.5-internal.2
|
||||
# Promote:
|
||||
git tag v2.3.5-closed.1
|
||||
git push origin v2.3.5-closed.1
|
||||
```
|
||||
Releases follow a semantic versioning scheme, `vX.Y.Z`, with suffixes to denote the release channel and iteration.
|
||||
|
||||
## Hotfixes & Patch Releases
|
||||
- `v2.8.0-internal.1`: An internal build, iteration 1.
|
||||
- `v2.8.0-closed.1`: A closed testing (Alpha) build.
|
||||
- `v2.8.0-open.1`: An open testing (Beta) build.
|
||||
- `v2.8.0`: The final production release.
|
||||
|
||||
If you need to release a hotfix or patch for a previous version (not the latest mainline), follow this process:
|
||||
---
|
||||
|
||||
- **Tagging:** Use a tag with a suffix, such as `vX.X.X-hotfix1` or `vX.X.X-patch1` (e.g., `v2.3.5-hotfix1`).
|
||||
- **Uniqueness:** The release workflow uses the full tag (including suffix) for all artifact and release naming, so each hotfix/patch is uniquely identified.
|
||||
- **Version Code:** The workflow automatically ensures the version code for a hotfix/patch is strictly greater than any previous release, even if the hotfix is created from an older commit. This prevents Play Store upload errors due to version code regressions.
|
||||
- **Multiple Releases:** You can have multiple releases for the same base version (e.g., `v2.3.5`, `v2.3.5-hotfix1`, `v2.3.5-patch2`). Each will be published and promoted independently.
|
||||
## The Release Lifecycle
|
||||
|
||||
### Hotfix Tagging Example
|
||||
```bash
|
||||
# On a release branch or after checking out the commit to hotfix
|
||||
# Tag and push a hotfix release
|
||||
git tag v2.3.5-hotfix1
|
||||
git push origin v2.3.5-hotfix1
|
||||
# For additional hotfixes/patches:
|
||||
git tag v2.3.5-hotfix2
|
||||
git push origin v2.3.5-hotfix2
|
||||
```
|
||||
### Step 1: Creating a New Internal Build
|
||||
|
||||
### Policy
|
||||
- Always use a unique tag for each hotfix/patch.
|
||||
- The version code will always increase, regardless of commit history.
|
||||
- The full tag is used for all release and artifact naming.
|
||||
This is the starting point for any new release, whether it's a brand-new version, a patch, or a hotfix.
|
||||
|
||||
## Manual Checklist
|
||||
- [ ] Verify build in Google Play Console
|
||||
- [ ] Review and publish GitHub draft release (for production)
|
||||
- [ ] Merge release branch to main after production
|
||||
- [ ] Label PRs for changelog accuracy
|
||||
1. Navigate to the **Actions** tab in the GitHub repository.
|
||||
2. Select the **"Create Internal Release Tag"** workflow.
|
||||
3. Click **"Run workflow"**.
|
||||
4. Fill in the `base_version` field with the version you want to create (e.g., `2.8.0`).
|
||||
5. Run the workflow.
|
||||
|
||||
## Build Attestations & Provenance
|
||||
**What Happens Automatically:**
|
||||
|
||||
All release artifacts are accompanied by explicit GitHub build attestations (provenance). After each artifact is uploaded in the release workflow, a provenance attestation is generated using the `actions/attest-build-provenance` action. This provides cryptographic proof that the artifacts were built by our trusted GitHub Actions workflow, ensuring supply chain integrity and allowing users to verify the origin of each release.
|
||||
- The wizard calculates the next iteration number (e.g., `.1`, `.2`, etc.) and pushes a new tag to the commit (e.g., `v2.8.0-internal.1`).
|
||||
- The push triggers the `release.yml` engine, which builds the application, uploads it to the Google Play **Internal** track, and creates a corresponding **draft pre-release** on GitHub.
|
||||
|
||||
- Attestations are generated immediately after each artifact upload in the workflow.
|
||||
- You can view and verify provenance in the GitHub UI under each release asset.
|
||||
- For more details, see [GitHub's documentation on build provenance](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#provenance-attestations).
|
||||
### Step 2: Promoting an Existing Build
|
||||
|
||||
> **Note:** The GitHub release is always attached to the base version tag (e.g., `v2.3.5`). All track tags for the same version update the same draft release. Look for the draft under the base version tag.
|
||||
Once an internal build has been tested and is ready for a wider audience, you promote it.
|
||||
|
||||
> **Best Practice:** Always promote the last verified build from the previous track to the next track. Do not introduce new changes between tracks unless absolutely necessary. This ensures consistency, traceability, and minimizes risk.
|
||||
1. Navigate to the **Actions** tab in the GitHub repository.
|
||||
2. Select the **"Promote Release"** workflow.
|
||||
3. Click **"Run workflow"**.
|
||||
4. Specify the `target_stage` (`closed`, `open`, or `production`). The default, `auto`, will automatically promote to the next logical stage.
|
||||
5. Optionally, specify the `base_version` to promote. If left blank, the wizard will find the latest internal tag and use its base version.
|
||||
6. Run the workflow.
|
||||
|
||||
**What Happens Automatically:**
|
||||
|
||||
- The wizard determines the correct commit from the latest internal tag for that `base_version`.
|
||||
- It pushes a new promotion tag (e.g., `v2.8.0-closed.1`) to that commit.
|
||||
- The push triggers the `release.yml` engine. It intelligently **skips the build steps** and proceeds to:
|
||||
- Promote the build on Google Play to the target track.
|
||||
- Update the existing draft GitHub Release, renaming it and marking it as a non-draft pre-release (or full release for production).
|
||||
|
||||
### Special Case: Hotfixes / Superseding a Release
|
||||
|
||||
The system is designed to handle hotfixes gracefully. If `v2.8.0-internal.1` has been created, but a critical bug is found, the process is simple:
|
||||
|
||||
1. Merge the fix into your main branch.
|
||||
2. Go to the **"Create Internal Release Tag"** workflow again.
|
||||
3. Enter the *same* `base_version`: `2.8.0`.
|
||||
|
||||
**What Happens Automatically:**
|
||||
|
||||
- The wizard creates and pushes a new tag, `v2.8.0-internal.2`, to the **new commit**.
|
||||
- The `release.yml` engine detects that an existing release for `v2.8.0` points to an *older* commit.
|
||||
- It correctly interprets this as a "superseding" event. It **automatically deletes the old GitHub release and its base tag**, effectively restarting the release process for `v2.8.0` from the new, corrected commit. This prevents a broken or outdated build from ever being promoted.
|
||||
|
|
|
@ -104,11 +104,11 @@ platform :android do
|
|||
lane :get_highest_version_code do
|
||||
require 'set'
|
||||
all_codes = Set.new
|
||||
tracks = ['internal', 'closed', 'open', 'production']
|
||||
tracks = ['internal', 'alpha', 'beta', 'production']
|
||||
tracks.each do |track|
|
||||
begin
|
||||
codes = google_play_track_version_codes(track: track, package_name: 'com.geeksville.mesh')
|
||||
all_codes.merge(codes.map(&:to_i))
|
||||
codes = google_play_track_version_codes(track: track)
|
||||
all_codes.merge(codes.map(&:to_i)) if codes
|
||||
rescue => e
|
||||
UI.message("Could not fetch version codes for track #{track}: #{e.message}")
|
||||
end
|
||||
|
@ -118,17 +118,6 @@ platform :android do
|
|||
File.write('highest_version_code.txt', highest.to_s)
|
||||
end
|
||||
|
||||
desc "Get the version code currently on the internal track (max if multiple)"
|
||||
lane :get_internal_track_version_code do
|
||||
codes = google_play_track_version_codes(track: 'internal', package_name: 'com.geeksville.mesh')
|
||||
if codes.nil? || codes.empty?
|
||||
UI.user_error!("No version codes found on internal track. Ensure an internal build has been published before promoting.")
|
||||
end
|
||||
max_code = codes.map(&:to_i).max
|
||||
UI.message("Internal track version code: #{max_code}")
|
||||
File.write('internal_version_code.txt', max_code.to_s)
|
||||
end
|
||||
|
||||
private_lane :build_google_release do
|
||||
gradle(
|
||||
task: "clean bundleGoogleRelease assembleGoogleRelease",
|
||||
|
|
Ładowanie…
Reference in New Issue