Skip to content

Shippable Build & Signing #10

Shippable Build & Signing

Shippable Build & Signing #10

---
name: Shippable Build & Signing
on:
workflow_call:
workflow_dispatch:
inputs:
skipThunderbird:
type: boolean
description: Skip building Thunderbird
skipK9Mail:
type: boolean
description: Skip building K-9 Mail
skipTests:
type: boolean
description: Skip running tests
skipBetaBump:
type: boolean
description: Skip version bump (beta)
skipGooglePlay:
type: boolean
description: Skip Google Play publish
draftGooglePlay:
type: boolean
description: Leave Play Store version in draft state
jobs:
get_environment:
name: Determine Release Environment
runs-on: ubuntu-latest
outputs:
releaseEnv: ${{ steps.releaseEnv.outputs.result }}
steps:
- uses: actions/github-script@v7
id: releaseEnv
with:
result-encoding: string
script: |
const RELEASE_ENVS = {
"refs/heads/main": "thunderbird_daily",
"refs/heads/beta": "thunderbird_beta",
"refs/heads/release": "thunderbird_release",
};
if (context.ref in RELEASE_ENVS) {
return RELEASE_ENVS[context.ref];
} else {
core.setFailed(`Unknown branch ${context.ref} for shippable builds!`)
return "";
}
dump_config:
name: Show Release Environment
runs-on: ubuntu-latest
needs: get_environment
environment: ${{ needs.get_environment.outputs.releaseEnv }}
outputs:
matrixInclude: ${{ steps.dump.outputs.matrixInclude }}
releaseType: ${{ vars.RELEASE_TYPE }}
steps:
- name: Show Environment
uses: actions/github-script@v7
id: dump
env:
matrixInclude: ${{ vars.MATRIX_INCLUDE }}
releaseType: ${{ vars.RELEASE_TYPE }}
skipThunderbird: ${{ inputs.skipThunderbird }}
skipK9Mail: ${{ inputs.skipK9Mail }}
skipTests: ${{ inputs.skipTests }}
skipBetaBump: ${{ inputs.skipBetaBump }}
skipGooglePlay: ${{ inputs.skipGooglePlay }}
draftGooglePlay: ${{ inputs.draftGooglePlay }}
with:
script: |
let matrix = JSON.parse(process.env.matrixInclude);
let skipThunderbird = process.env.skipThunderbird == "true";
let skipK9Mail = process.env.skipK9Mail == "true";
if (!matrix.every(item => !!item.appName && !!item.packageFormat)) {
core.setFailed("MATRIX_INCLUDE is missing appName or packageFormat");
}
let matrixFull = matrix.filter(item => {
return !((item.appName == "k9mail" && skipK9Mail) ||
(item.appName == "thunderbird" && skipThunderbird));
});
if (!matrixFull.length) {
core.setFailed("There are no builds to run");
return;
}
core.setOutput("matrixInclude", matrixFull);
await core.summary
.addRaw(`Beginning a <b>${process.env.releaseType}</b> build with the following configurations:`, true)
.addTable([
[
{ data: "App Name", header: true },
{ data: "Flavor", header: true },
{ data: "Format", header: true },
{ data: "Release Target", header: true },
{ data: "Play Store Track", header: true },
],
...matrixFull.map(item => [
{ data: item.appName },
{ data: item.packageFlavor || "default" },
{ data: item.packageFormat },
{ data: item.releaseTarget?.replace(/\|/g, ", ") || "artifact only" },
{ data: item.playTargetTrack || "none" },
])
])
.write();
if (skipThunderbird) {
await core.summary.addList(["Thunderbird build is being skipped"]).write();
}
if (skipK9Mail) {
await core.summary.addList(["K-9 Mail build is being skipped"]).write();
}
if (process.env.skipTests == "true") {
await core.summary.addList(["Tests are being skipped"]).write();
}
if (process.env.skipBetaBump == "true" && process.env.releaseType == "beta") {
await core.summary.addList(["Beta bump is being skipped"]).write();
}
if (process.env.skipGooglePlay == "true") {
await core.summary.addList(["Play Store upload is being skipped"]).write();
}
if (process.env.skipGooglePlay != "true" && process.env.draftGooglePlay == "true") {
await core.summary.addList(["Play Store upload is being kept in the draft state"]).write();
}
notify_build_start:
name: Notify Build Start
runs-on: ubuntu-latest
needs: [dump_config]
if: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }}
environment: notify_matrix
outputs:
actorLink: ${{ steps.actorLink.outputs.actorLink }}
steps:
- name: Triggering Actor Link
id: actorLink
uses: actions/github-script@v7
env:
userMap: ${{ vars.MATRIX_NOTIFY_USER_MAP }}
with:
script: |
let userMap = JSON.parse(process.env.userMap || "{}");
if (Object.hasOwn(userMap, context.actor)) {
let mxid = userMap[context.actor];
core.setOutput("actorLink", `[${mxid}](https://matrix.to/#/${mxid})`);
} else {
core.setOutput("actorLink", `[@${context.actor}](https://github.com/${context.actor})`);
}
- name: Notify Build Start
if: ${{ vars.MATRIX_NOTIFY_ROOM }}
uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
🔵 [${{ vars.RELEASE_TYPE }} build ${{ github.run_number}}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
was started by ${{ steps.actorLink.outputs.actorLink }}
release_commit:
name: Release Bumps
runs-on: ubuntu-latest
needs: [dump_config, get_environment]
if: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }}
environment: ${{ needs.get_environment.outputs.releaseEnv }}
strategy:
matrix:
include: "${{ fromJSON(needs.dump_config.outputs.matrixInclude) }}"
permissions:
contents: write
outputs:
k9mail_sha: ${{ steps.commit.outputs.k9mail_sha }}
thunderbird_sha: ${{ steps.commit.outputs.thunderbird_sha }}
k9mail_github_notes: ${{ steps.render_notes.outputs.k9mail_github_notes }}
thunderbird_github_notes: ${{ steps.render_notes.outputs.thunderbird_github_notes }}
steps:
- name: Checkout repository
if: ${{ contains(matrix.releaseTarget, 'github') }}
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Copy CI gradle.properties
if: ${{ contains(matrix.releaseTarget, 'github') }}
shell: bash
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- uses: actions/setup-java@v4
if: ${{ contains(matrix.releaseTarget, 'github') }}
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
if: ${{ contains(matrix.releaseTarget, 'github') }}
with:
cache-disabled: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }}
add-job-summary: never
- name: Get application info
id: appinfo
shell: bash
if: ${{ contains(matrix.releaseTarget, 'github') }}
env:
RELEASE_TYPE: ${{ vars.RELEASE_TYPE }}
PACKAGE_FLAVOR: ${{ matrix.packageFlavor }}
APP_NAME: ${{ matrix.appName }}
run: |
if [[ "${APP_NAME}" == "k9mail" && "${RELEASE_TYPE}" == "beta" ]]; then
# k9mail uses release for betas as well. Later on we should align the structures and
# remove this hack
RELEASE_TYPE=release
fi
./gradlew :app-${APP_NAME}:printVersionInfo -PbuildType=${RELEASE_TYPE} -PflavorName=${PACKAGE_FLAVOR} --configure-on-demand
- name: Bump version code
id: bump_version_code
if: ${{ contains(matrix.releaseTarget, 'github') }}
shell: bash
env:
APP_NAME: ${{ matrix.appName }}
OLD_VERSION_CODE: ${{ steps.appinfo.outputs.VERSION_CODE }}
run: |
NEW_VERSION_CODE=$(($OLD_VERSION_CODE + 1))
sed "s/versionCode = $OLD_VERSION_CODE/versionCode = $NEW_VERSION_CODE/" app-${APP_NAME}/build.gradle.kts > tmp_gradle_kts
! diff -u app-${APP_NAME}/build.gradle.kts tmp_gradle_kts # flip return value to force error if no bump
mv tmp_gradle_kts app-${APP_NAME}/build.gradle.kts
echo "CODE=${NEW_VERSION_CODE}" | tee $GITHUB_OUTPUT
- name: Bump version suffix
id: bump_version_suffix
if: ${{ !inputs.skipBetaBump && contains(matrix.releaseTarget, 'github') && vars.RELEASE_TYPE == 'beta' }}
shell: bash
env:
APP_NAME: ${{ matrix.appName }}
OLD_VERSION_SUFFIX: ${{ steps.appinfo.outputs.VERSION_NAME_SUFFIX }}
run: |
NEW_VERSION_SUFFIX=b$((${OLD_VERSION_SUFFIX:1} + 1))
sed "s/versionNameSuffix = \"$OLD_VERSION_SUFFIX\"/versionNameSuffix = \"$NEW_VERSION_SUFFIX\"/" app-${APP_NAME}/build.gradle.kts > tmp_gradle_kts
! diff -u app-${APP_NAME}/build.gradle.kts tmp_gradle_kts # flip return value to force error if no bump
mv tmp_gradle_kts app-${APP_NAME}/build.gradle.kts
echo "SUFFIX=${NEW_VERSION_SUFFIX}" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT
- name: Render Release Notes
id: render_notes
if: ${{ contains(matrix.releaseTarget, 'github') }}
shell: bash
env:
APP_NAME: ${{ matrix.appName }}
APPLICATION_ID: ${{ steps.appinfo.outputs.APPLICATION_ID }}
APPLICATION_LABEL: ${{ steps.appinfo.outputs.APPLICATION_LABEL }}
VERSION_CODE: ${{ steps.bump_version_code.outputs.CODE }}
FULL_VERSION_NAME: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.bump_version_suffix.outputs.SUFFIX || steps.appinfo.outputs.VERSION_NAME_SUFFIX }}
run: |
mkdir -p ./app-metadata/${APPLICATION_ID}/en-US/changelogs
GITHUB_NOTES_FILE="$(mktemp -d)/long-notes.txt"
python ./scripts/render-notes.py ${APPLICATION_ID} ${FULL_VERSION_NAME} ${VERSION_CODE} ${GITHUB_NOTES_FILE}
echo "${APP_NAME}_github_notes<<EOF" >> $GITHUB_OUTPUT
cat $GITHUB_NOTES_FILE >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "<h2>${APPLICATION_LABEL} ${FULL_VERSION_NAME} Release Notes (${VERSION_CODE})</h2>" | tee -a $GITHUB_STEP_SUMMARY
echo -e "Summarized Version Notes\n\n\`\`\`" | tee -a $GITHUB_STEP_SUMMARY
cat ./app-metadata/${APPLICATION_ID}/en-US/changelogs/${VERSION_CODE}.txt | tee -a $GITHUB_STEP_SUMMARY
echo -e "\n\`\`\`\n\nLong Version Notes\n\n\`\`\`" | tee -a $GITHUB_STEP_SUMMARY
cat $GITHUB_NOTES_FILE | tee -a $GITHUB_STEP_SUMMARY
echo -e "\`\`\`" | tee -a $GITHUB_STEP_SUMMARY
- name: Validate Release Notes Length
if: ${{ contains(matrix.releaseTarget, 'github') }}
shell: bash
env:
APPLICATION_ID: ${{ steps.appinfo.outputs.APPLICATION_ID }}
VERSION_CODE: ${{ steps.bump_version_code.outputs.CODE }}
run: |
wc -c ./app-metadata/${APPLICATION_ID}/en-US/changelogs/${VERSION_CODE}.txt
RELNOTES_LENGTH=$(wc -c ./app-metadata/${APPLICATION_ID}/en-US/changelogs/${VERSION_CODE}.txt | awk '{print $1}')
if [[ "${RELNOTES_LENGTH}" -gt 500 ]]; then
echo "Release Notes are too long. Found ${RELNOTES_LENGTH} characters, need a maximum of 500"
exit 1
fi
- name: Release Commits
if: ${{ contains(matrix.releaseTarget, 'github') }}
id: commit
shell: bash
env:
APPLICATION_LABEL: ${{ steps.appinfo.outputs.APPLICATION_LABEL }}
APPLICATION_ID: ${{ steps.appinfo.outputs.APPLICATION_ID }}
APP_NAME: ${{ matrix.appName }}
FULL_VERSION_NAME: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.bump_version_suffix.outputs.SUFFIX || steps.appinfo.outputs.VERSION_NAME_SUFFIX }}
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
# We need the metadata to point to the right application for the release commit
rm metadata
ln -sf app-metadata/${APPLICATION_ID} metadata
# Add changelogs, build version changes and metadata symlink
git add ./app-metadata/${APPLICATION_ID}/en-US/changelogs/*
git add ./app-${APP_NAME}/src/main/res/raw/changelog_master.xml
git add ./app-${APP_NAME}/build.gradle.kts
git add metadata
# Ready to commit. Make sure to pull again to reduce likelihood of race conditions
git status
git pull
git commit -m "Release: ${APPLICATION_LABEL} ${FULL_VERSION_NAME}"
git log -n 5
set +e
git push
GIT_RESULT=$?
set -e
if [ $GIT_RESULT -gt 0 ]; then
echo "Push rejected, trying again once in 5 seconds"
sleep 5
git pull --rebase -X ours
git push
fi
echo "${APP_NAME}_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Summary
if: ${{ contains(matrix.releaseTarget, 'github') }}
uses: actions/github-script@v7
env:
bump_sha: ${{ steps.commit.outputs.sha }}
applicationId: ${{ steps.appinfo.outputs.APPLICATION_ID }}
oldFullVersion: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.appinfo.outputs.VERSION_NAME_SUFFIX }}
oldVersionCode: ${{ steps.appinfo.outputs.VERSION_CODE }}
newFullVersion: ${{ steps.appinfo.outputs.VERSION_NAME }}${{ steps.bump_version_suffix.outputs.SUFFIX || steps.appinfo.outputs.VERSION_NAME_SUFFIX }}
newVersionCode: ${{ steps.bump_version_code.outputs.CODE }}
with:
script: |
let env = process.env;
console.log(env);
await core.summary
.addRaw(`Version for ${env.applicationId} bumped from ${env.oldFullVersion} (${env.oldVersionCode}) to ${env.newFullVersion} (${env.newVersionCode}) in `)
.addLink(process.env.bump_sha, `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${env.bump_sha}`)
.addEOL()
.write();
build_unsigned:
name: Build Unsigned
runs-on: ubuntu-latest
timeout-minutes: 90
if: ${{ !failure() && !cancelled() }} # Run if release_commit is skipped
needs: [dump_config, get_environment, release_commit]
strategy:
matrix:
include: "${{ fromJSON(needs.dump_config.outputs.matrixInclude) }}"
environment: ${{ needs.get_environment.outputs.releaseEnv }}
steps:
- name: Get release sha
id: sha
shell: bash
env:
THUNDERBIRD_SHA: ${{ needs.release_commit.outputs.thunderbird_sha }}
K9MAIL_SHA: ${{ needs.release_commit.outputs.k9mail_sha }}
APP_NAME: ${{ matrix.appName }}
run: |
case "${APP_NAME}" in
thunderbird) APP_SHA=$THUNDERBIRD_SHA ;;
k9mail) APP_SHA=$K9MAIL_SHA ;;
*) APP_SHA=$GITHUB_SHA ;;
esac
echo "app_sha=$APP_SHA" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ steps.sha.outputs.app_sha }}
- name: Copy CI gradle.properties
shell: bash
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-disabled: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }}
add-job-summary: on-failure
- name: Build It
shell: bash
env:
PACKAGE_FORMAT: ${{ matrix.packageFormat }}
PACKAGE_FLAVOR: ${{ matrix.packageFlavor }}
APP_NAME: ${{ matrix.appName }}
RELEASE_TYPE: ${{ vars.RELEASE_TYPE }}
run: |
if [[ "$APP_NAME" = "thunderbird" && "$PACKAGE_FORMAT" = "apk" ]]; then
BUILD_COMMAND="assemble${PACKAGE_FLAVOR^}${RELEASE_TYPE^}"
elif [[ "$APP_NAME" = "thunderbird" && "${PACKAGE_FORMAT}" = "aab" ]]; then
BUILD_COMMAND="bundle${PACKAGE_FLAVOR^}${RELEASE_TYPE^}"
elif [[ "$APP_NAME" = "k9mail" ]]; then
BUILD_COMMAND="assembleRelease"
fi
echo "BUILDING: :app-${APP_NAME}:${BUILD_COMMAND}"
./gradlew clean :app-${APP_NAME}:${BUILD_COMMAND} --no-build-cache --no-configuration-cache
echo "Status: $?"
- name: Test It
if: ${{ !inputs.skipTests }}
shell: bash
run: ./gradlew testsOnCi
- name: Move apps to upload directory
shell: bash
env:
PACKAGE_FORMAT: ${{ matrix.packageFormat }}
PACKAGE_FLAVOR: ${{ matrix.packageFlavor }}
APP_NAME: ${{ matrix.appName }}
RELEASE_TYPE: ${{ vars.RELEASE_TYPE }}
UPLOAD_PATH: "uploads"
run: |
OUT_BASE=app-${APP_NAME}/build/outputs/
if [[ "$APP_NAME" = "thunderbird" && "$PACKAGE_FORMAT" = "apk" ]]; then
OUT_PATH="${OUT_BASE}/apk/${PACKAGE_FLAVOR}/${RELEASE_TYPE}"
OUT_FILE="app-${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}-unsigned.apk"
UPLOAD_FILE="${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.apk"
elif [[ "$APP_NAME" = "thunderbird" && "${PACKAGE_FORMAT}" = "aab" ]]; then
OUT_PATH="${OUT_BASE}/bundle/${PACKAGE_FLAVOR}${RELEASE_TYPE^}"
OUT_FILE="app-${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.aab"
UPLOAD_FILE="${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.aab"
elif [[ "$APP_NAME" = "k9mail" ]]; then
OUT_PATH="${OUT_BASE}/apk/release"
OUT_FILE="app-${APP_NAME}-release-unsigned.apk"
UPLOAD_FILE="${APP_NAME}-default-${RELEASE_TYPE}.apk"
else
echo "PACKAGE_FORMAT $PACKAGE_FORMAT is unknown. Exiting."
exit 23
fi
mkdir -p "${UPLOAD_PATH}"
if [[ -f "${OUT_PATH}/${OUT_FILE}" ]]; then
mv -f "${OUT_PATH}/${OUT_FILE}" "${UPLOAD_PATH}/${UPLOAD_FILE}"
else
echo "Build file ${OUT_PATH}/${OUT_FILE} not found. Exiting."
ls -l ${OUT_PATH}
exit 24
fi
echo "Upload contents:"
ls -l ${UPLOAD_PATH}/
- name: Upload unsigned
uses: actions/upload-artifact@v4
env:
UPLOAD_PATH: "uploads"
with:
name: unsigned-${{ matrix.appName }}-${{ matrix.packageFormat }}-${{ matrix.packageFlavor || 'default' }}
path: ${{ env.UPLOAD_PATH }}/
if-no-files-found: error
sign_mobile:
name: Sign Packages
runs-on: ubuntu-latest
if: ${{ !failure() && !cancelled() }} # Run if previous step is skipped
needs: [build_unsigned, dump_config]
strategy:
matrix:
include: "${{ fromJSON(needs.dump_config.outputs.matrixInclude) }}"
environment: ${{ matrix.appName }}_${{ needs.dump_config.outputs.releaseType }}_${{ matrix.packageFlavor || 'default' }}
env:
RELEASE_TYPE: ${{ needs.dump_config.outputs.releaseType }}
steps:
- uses: actions/download-artifact@v4
with:
name: unsigned-${{ matrix.appName }}-${{ matrix.packageFormat }}-${{ matrix.packageFlavor || 'default' }}
path: uploads/
- uses: noriban/sign-android-release@5f144321d3c7c2233266e78b42360345d8bbe403 # v5.1
name: Sign package
with:
releaseDirectory: uploads/
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
- name: Rename assets
if: ${{ matrix.packageFormat == 'apk' }}
env:
APP_NAME: ${{ matrix.appName }}
PACKAGE_FLAVOR: ${{ matrix.packageFlavor || 'default' }}
run: |
mv uploads/${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}-signed.apk uploads/${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.apk
rm uploads/*-aligned.apk
- name: Remove JKS file
shell: bash
run: |
rm -f uploads/*.jks
- name: Upload signed
uses: actions/upload-artifact@v4
with:
name: signed-${{ matrix.appName }}-${{ matrix.packageFormat }}-${{ matrix.packageFlavor || 'default' }}
if-no-files-found: error
path: |
uploads/*.apk
uploads/*.aab
notify_pre_publish:
name: Notify Publish Approval
needs: [dump_config, sign_mobile, notify_build_start]
if: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }}
runs-on: ubuntu-latest
environment: notify_matrix
steps:
- uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
🟡 [${{ needs.dump_config.outputs.releaseType }} build ${{ github.run_number}}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
waiting for publish approval (triggered by ${{ needs.notify_build_start.outputs.actorLink }})
pre_publish:
# This is a holding job meant to require approval before proceeding with the publishing jobs below
# The environment has a deployment protection rule requiring approval from a set of named reviewers
# before proceeding.
name: Wait for Approval
needs: [dump_config, sign_mobile]
if: ${{ needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release' }}
environment: publish_hold
runs-on: ubuntu-latest
steps:
- name: Approval
shell: bash
run: |
true
publish_release:
name: Publish Release
needs: [pre_publish, dump_config, release_commit]
if: ${{ !failure() && !cancelled() }} # Run if previous step is skipped
runs-on: ubuntu-latest
strategy:
matrix:
include: "${{ fromJSON(needs.dump_config.outputs.matrixInclude) }}"
environment: publish_release
outputs:
thunderbird_release_url: ${{ steps.summary.outputs.thunderbird_release_url }}
k9mail_release_url: ${{ steps.summary.outputs.k9mail_release_url }}
thunderbird_full_version_name: ${{ steps.summary.outputs.thunderbird_full_version_name }}
k9mail_full_version_name: ${{ steps.summary.outputs.k9mail_full_version_name }}
env:
RELEASE_TYPE: ${{ needs.dump_config.outputs.releaseType }}
APP_NAME: ${{ matrix.appName }}
PACKAGE_FLAVOR: ${{ matrix.packageFlavor || 'default' }}
PACKAGE_FORMAT: ${{ matrix.packageFormat }}
steps:
- uses: actions/download-artifact@v4
with:
name: signed-${{ matrix.appName }}-${{ matrix.packageFormat }}-${{ matrix.packageFlavor || 'default' }}
path: "uploads/"
- name: Get Package Info
id: pkginfo
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
PKG_FILE="uploads/${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.${PACKAGE_FORMAT}"
if [[ "${PACKAGE_FORMAT}" == "apk" ]]; then
LATEST_BUILD_TOOLS=$(ls -d ${ANDROID_SDK_ROOT}/build-tools/* | sort -V | tail -n1)
AAPT=${LATEST_BUILD_TOOLS}/aapt
VERSION_NAME=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^package:.*versionName='\([^']*\)'.*$/\1/p")
VERSION_CODE=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^package:.*versionCode='\([^']*\)'.*$/\1/p")
APPLICATION_ID=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^package: name='\([^']*\)'.*$/\1/p")
APPLICATION_LABEL=$(${AAPT} dump badging $PKG_FILE | sed -n "s/^application-label:'\([^']*\)'.*$/\1/p")
elif [[ "${PACKAGE_FORMAT}" == "aab" ]]; then
if [ ! -f bundletool.jar ]; then
gh release download -R google/bundletool -p 'bundletool-all-*.jar' -O bundletool.jar
fi
BUNDLETOOL="java -jar bundletool.jar"
VERSION_NAME=$(${BUNDLETOOL} dump manifest --bundle ${PKG_FILE} --xpath '/manifest/@android:versionName')
VERSION_CODE=$(${BUNDLETOOL} dump manifest --bundle ${PKG_FILE} --xpath '/manifest/@android:versionCode')
APPLICATION_ID=$(${BUNDLETOOL} dump manifest --bundle ${PKG_FILE} --xpath '/manifest/@package')
# Unfortunately no application label in the bundle
case "$APPLICATION_ID" in
net.thunderbird.android) APPLICATION_LABEL="Thunderbird" ;;
net.thunderbird.android.beta) APPLICATION_LABEL="Thunderbird Beta" ;;
net.thunderbird.android.daily) APPLICATION_LABEL="Thunderbird Daily" ;;
com.fsck.k9) APPLICATION_LABEL="K-9 Mail" ;;
esac
fi
echo "TAG_NAME=${APP_NAME^^}_${VERSION_NAME//./_}" >> $GITHUB_OUTPUT
echo "FULL_VERSION_NAME=${APPLICATION_LABEL} ${VERSION_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_NAME=${VERSION_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_CODE=${VERSION_CODE}" >> $GITHUB_OUTPUT
echo "APPLICATION_ID=${APPLICATION_ID}" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT
- name: Rename release assets
id: rename
shell: bash
env:
VERSION_NAME: ${{ steps.pkginfo.outputs.VERSION_NAME }}
run: |
PKG_FILE="${APP_NAME}-${PACKAGE_FLAVOR}-${RELEASE_TYPE}.${PACKAGE_FORMAT}"
PKG_FILE_PRETTY="${APP_NAME}-${VERSION_NAME}.${PACKAGE_FORMAT}"
mv uploads/${PKG_FILE} uploads/${PKG_FILE_PRETTY}
echo "PKG_FILE=${PKG_FILE_PRETTY}" >> $GITHUB_OUTPUT
ls -l uploads/${PKG_FILE_PRETTY}
- name: App Token Generate
uses: actions/create-github-app-token@v1
if: ${{ contains(matrix.releaseTarget, 'github') && vars.RELEASER_APP_CLIENT_ID }}
id: app-token
with:
app-id: ${{ vars.RELEASER_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
- name: Get release sha and notes
id: shanotes
shell: bash
env:
THUNDERBIRD_GITHUB_NOTES: ${{ needs.release_commit.outputs.thunderbird_github_notes }}
K9MAIL_GITHUB_NOTES: ${{ needs.release_commit.outputs.k9mail_github_notes }}
THUNDERBIRD_SHA: ${{ needs.release_commit.outputs.thunderbird_sha }}
K9MAIL_SHA: ${{ needs.release_commit.outputs.k9mail_sha }}
APP_NAME: ${{ matrix.appName }}
run: |
app_sha_name=${APP_NAME^^}_SHA
echo "app_sha=${!app_sha_name}" >> $GITHUB_OUTPUT
app_relnotes_name=${APP_NAME^^}_GITHUB_NOTES
echo "app_github_notes<<EOF" >> $GITHUB_OUTPUT
echo "${!app_relnotes_name}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT
- name: Publish to GitHub Releases
id: publish_gh
if: ${{ contains(matrix.releaseTarget, 'github') }}
uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
with:
token: ${{ steps.app-token.outputs.token || github.token }}
target_commitish: ${{ steps.shanotes.outputs.app_sha }}
tag_name: ${{ steps.pkginfo.outputs.TAG_NAME }}
name: ${{ steps.pkginfo.outputs.FULL_VERSION_NAME }}
body: ${{ steps.shanotes.outputs.app_github_notes }}
prerelease: ${{ env.RELEASE_TYPE != 'release' }}
fail_on_unmatched_files: true
files: |
uploads/${{ steps.rename.outputs.PKG_FILE }}
- name: Adjust release notes for play store upload
if: ${{ !inputs.skipGooglePlay && contains(matrix.releaseTarget, 'play') && matrix.playTargetTrack }}
shell: bash
env:
VERSION_CODE: ${{ steps.pkginfo.outputs.VERSION_CODE }}
APPLICATION_ID: ${{ steps.pkginfo.outputs.APPLICATION_ID }}
REPO: ${{ github.repository }}
APP_SHA: ${{ steps.shanotes.outputs.app_sha }}
run: |
# r0adkll/upload-google-play expects the release notes in a different structure
FILEPATH=app-metadata/${APPLICATION_ID}/en-US/changelogs/${VERSION_CODE}.txt
mkdir whatsnew
wget -O whatsnew/whatsnew-en-US https://raw.githubusercontent.com/${REPO}/${APP_SHA}/${FILEPATH}
- name: Publish to Google Play
id: publish_play
uses: r0adkll/upload-google-play@v1
if: ${{ !inputs.skipGooglePlay && contains(matrix.releaseTarget, 'play') && matrix.playTargetTrack }}
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_ACCOUNT }}
packageName: ${{ steps.pkginfo.outputs.APPLICATION_ID }}
track: ${{ matrix.playTargetTrack }}
releaseName: ${{ steps.pkginfo.outputs.VERSION_NAME }}
status: completed
changesNotSentForReview: ${{ inputs.draftGooglePlay }}
whatsNewDirectory: whatsnew
releaseFiles: |
uploads/${{ steps.rename.outputs.PKG_FILE }}
- name: Summary
uses: actions/github-script@v7
id: summary
env:
tagName: ${{ steps.pkginfo.outputs.TAG_NAME }}
fullVersionName: ${{ steps.pkginfo.outputs.FULL_VERSION_NAME }}
ghReleaseUrl: ${{ steps.publish_gh.outputs.url }}
playTargetTrack: ${{ matrix.playTargetTrack }}
applicationId: ${{ steps.pkginfo.outputs.APPLICATION_ID }}
releaseTarget: ${{ matrix.releaseTarget }}
appSha: ${{ steps.shanotes.outputs.app_sha }}
appName: ${{ matrix.appName }}
skipGooglePlay: ${{ inputs.skipGooglePlay }}
with:
script: |
await core.summary
.addHeading(`${process.env.fullVersionName} (${process.env.applicationId})`, 2)
.write();
core.setOutput(`${process.env.appName}_full_version_name`, process.env.fullVersionName);
if (!process.env.releaseTarget) {
await core.summary
.addRaw(`Artifact-only build at `)
.addLink(process.env.appSha, `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${process.env.appSha}`)
.addEOL()
.write();
} else if (process.env.ghReleaseUrl) {
await core.summary
.addRaw(`Tag ${process.env.tagName} at `)
.addLink(process.env.appSha, `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${process.env.appSha}`)
.addEOL()
.addRaw(`Released to Github at `)
.addLink(process.env.ghReleaseUrl, process.env.ghReleaseUrl)
.addEOL()
.write();
core.setOutput(`${process.env.appName}_release_url`, process.env.ghReleaseUrl);
}
if (process.env.skipGooglePlay != "true" && process.env.playTargetTrack && process.env.releaseTarget.includes("play")) {
await core.summary.addRaw(`Released to the <b>${process.env.playTargetTrack}</b> track on Google Play`, true).write();
}
notify_build_result:
name: Notify Build Result
if: ${{ always() }}
needs: [dump_config, release_commit, build_unsigned, sign_mobile, publish_release, notify_build_start]
runs-on: ubuntu-latest
environment: notify_matrix
steps:
- name: Get previous workflow status
uses: Mercymeilya/last-workflow-status@3418710aefe8556d73b6f173a0564d38bcfd9a43
id: last_status
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Info
uses: actions/github-script@v7
id: info
env:
needs: ${{ toJSON(needs) }}
with:
script: |
let needs = JSON.parse(process.env.needs);
let failures = [];
for (let [job, need] of Object.entries(needs)) {
if (need.result == 'failure') {
failures.push(job.replace(/([-_])/g, "\\$1"));
}
}
core.setOutput("fail_steps", failures.join(`, `));
- name: Notify Failure
if: ${{ vars.MATRIX_NOTIFY_ROOM && contains(needs.*.result, 'failure') }}
uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
🔴 [${{ needs.dump_config.outputs.releaseType }} build ${{ github.run_number}}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
has failed at step ${{ steps.info.outputs.fail_steps }} (triggered by ${{ needs.notify_build_start.outputs.actorLink }})
- name: Notify Cancelled
if: ${{ vars.MATRIX_NOTIFY_ROOM && !contains(needs.*.result, 'failure') && contains(needs.*.result, 'cancelled') }}
uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
⚪ [${{ needs.dump_config.outputs.releaseType }} build ${{ github.run_number}}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
was cancelled
- name: Notify Success (Beta/Release)
if: ${{ vars.MATRIX_NOTIFY_ROOM && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && (needs.dump_config.outputs.releaseType == 'beta' || needs.dump_config.outputs.releaseType == 'release') }}
uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
🟢 [${{ needs.dump_config.outputs.releaseType }} build ${{ github.run_number}}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
has succeeded (triggered by ${{ needs.notify_build_start.outputs.actorLink }})
- name: Thunderbird Publish URL (Beta/Release)
if: ${{ vars.MATRIX_NOTIFY_ROOM && needs.publish_release.outputs.thunderbird_release_url && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
${{ needs.publish_release.outputs.thunderbird_full_version_name }} [is available](${{ needs.publish_release.outputs.thunderbird_release_url }})
- name: K-9 Mail Publish URL (Beta/Release)
if: ${{ vars.MATRIX_NOTIFY_ROOM && needs.publish_release.outputs.k9mail_release_url && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
${{ needs.publish_release.k9mail_full_version_name }} [is available](${{ needs.publish_release.outputs.k9mail_release_url }})
- name: Notify Success (Daily)
if: ${{ vars.MATRIX_NOTIFY_ROOM && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && needs.dump_config.outputs.releaseType == 'daily' && steps.last_status.outputs.last_status == 'failure' }}
uses: kewisch/action-matrix-notify@v1
with:
matrixHomeserver: ${{ vars.MATRIX_NOTIFY_HOMESERVER }}
matrixRoomId: ${{ vars.MATRIX_NOTIFY_ROOM }}
matrixToken: ${{ secrets.MATRIX_NOTIFY_TOKEN }}
message: >-
🟢 [${{ needs.dump_config.outputs.releaseType }} build ${{ github.run_number}}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
has recovered