Skip to content

[BRE-769] Add GitHub Actions workflow for publishing mobile releases #401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
288 changes: 288 additions & 0 deletions .github/workflows/_publish-mobile-github-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
name: _publish-mobile-github-release

on:
workflow_call:
inputs:
release_name:
description: 'Name prefix of the release to publish (e.g. "Password Manager")'
type: string
default: ""
workflow_name:
description: 'Name of the workflow to check for previous runs (e.g. publish-github-release.yml)'
type: string
required: true
credentials_filename:
description: 'Name of the credentials file to download from Azure Blob Storage (e.g. "google-play-credentials.json")'
type: string
required: true
check_release_command:
description: >
Shell command to check if a release is already published.
Use $CREDENTIALS_PATH for the path to credentials file.
Example: 'bundle exec fastlane android getLatestVersion serviceCredentialsFile:$CREDENTIALS_PATH'
type: string
required: true
project_type:
description: 'Type of the project (e.g. "android" or "ios")'
type: string
default: "android"
required: true



jobs:
publish-release:
name: Publish GitHub Release ${{ inputs.release_name }}
runs-on: ubuntu-24.04
permissions:
contents: write
actions: read

steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

- name: Get latest draft name
id: get_latest_draft
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
latest_release=$(gh release list --json name,tagName,isDraft,isPrerelease -L 10 --jq 'first(.[] | select((.name | test("${{ inputs.release_name }}"; "i")) and (.isDraft == true)))')

is_latest_draft="false"

if [ "$latest_release" != "null" ] && [ -n "$latest_release" ]; then
is_latest_draft=$(jq -r '.isDraft' <<< $latest_release)
fi

echo "is_latest_draft=$is_latest_draft" >> $GITHUB_OUTPUT

if [ "$is_latest_draft" != "true" ]; then
echo "No draft found"
exit 0
fi

latest_draft_version_name=$(echo "$latest_release" | jq -r '.name' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
echo "latest_draft_version_name=$latest_draft_version_name" >> $GITHUB_OUTPUT

latest_draft_version_number=$(echo "$latest_release" | jq -r '.name' | grep -oE '\([0-9]+\)' | sed 's/[()]//g')
echo "latest_draft_version_number=$latest_draft_version_number" >> $GITHUB_OUTPUT

latest_draft_name=$(jq -r '.name' <<< $latest_release)
echo "latest_draft_name=$latest_draft_name" >> $GITHUB_OUTPUT

# Retrieve the previous run ID and run state to determine the status of the last workflow execution.
# This is done to prevent the workflow from publishing a release that was already published,
# but then deleted and reverted to draft for any reason.
# It ensures the workflow does not process the same release multiple times if it was reverted.
- name: Get previous run ID
id: get_previous_run
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW_NAME: ${{ inputs.workflow_name }}
run: |
previous_run_id=$(gh run list --workflow=$WORKFLOW_NAME --status=success --limit 1 --json databaseId --jq '.[0].databaseId // empty')

if [ -n "$previous_run_id" ] && [ "$previous_run_id" != "null" ]; then
echo "Found previous successful scheduled run: $previous_run_id"
echo "previous_run_id=$previous_run_id" >> $GITHUB_OUTPUT
echo "has_previous_run=true" >> $GITHUB_OUTPUT
else
echo "No previous successful scheduled run found"
echo "has_previous_run=false" >> $GITHUB_OUTPUT
fi

- name: Compose artifact name
id: compose_artifact_name
run: |
artifact_name=$(echo "release-info-${{ inputs.release_name }}" | tr '[:upper:]' '[:lower:]' | sed 's/ /-/g')
echo "artifact_name=$artifact_name" >> $GITHUB_OUTPUT

- name: Download previous run state
id: previous_state
if: steps.get_previous_run.outputs.has_previous_run == 'true'
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: ${{ steps.compose_artifact_name.outputs.artifact_name }}
run-id: ${{ steps.get_previous_run.outputs.previous_run_id }}
github-token: ${{ github.token }}

- name: Parse previous state
id: parse_previous_state
if: steps.get_previous_run.outputs.has_previous_run == 'true'
run: |
if [ -f "release-info.json" ]; then

previous_release_tag=$(jq -r '.release_tag // empty' release-info.json)
previous_initial_state=$(jq -r '.initial_state // empty' release-info.json)
previous_changed_to=$(jq -r '.changed_to_state // empty' release-info.json)

echo "previous_release_tag=$previous_release_tag" >> $GITHUB_OUTPUT
echo "previous_initial_state=$previous_initial_state" >> $GITHUB_OUTPUT
echo "previous_changed_to=$previous_changed_to" >> $GITHUB_OUTPUT
echo "previous_timestamp=$previous_timestamp" >> $GITHUB_OUTPUT
echo "has_previous_state=true" >> $GITHUB_OUTPUT

echo "Previous run processed: $previous_release_tag (changed from: $previous_initial_state to: $previous_changed_to)"
else
echo "::error:: No valid release-info.json found in previous artifact"
echo "has_previous_state=false" >> $GITHUB_OUTPUT
fi

- name: Check if release was already processed
id: check_already_processed
env:
CURRENT_RELEASE: ${{ steps.get_latest_draft.outputs.latest_draft_version_name }}
PREVIOUS_RELEASE: ${{ steps.parse_previous_state.outputs.previous_release_tag }}
PREVIOUS_INITIAL_STATE: ${{ steps.parse_previous_state.outputs.previous_initial_state }}
PREVIOUS_CHANGED_TO: ${{ steps.parse_previous_state.outputs.previous_changed_to }}
HAS_PREVIOUS_STATE: ${{ steps.parse_previous_state.outputs.has_previous_state }}
run: |
should_skip=false

if [ "$HAS_PREVIOUS_STATE" == "true" ] && [ "$PREVIOUS_RELEASE" != "" ] && [ "$CURRENT_RELEASE" == "$PREVIOUS_RELEASE" ]; then
if [ "$PREVIOUS_CHANGED_TO" == "published" ] || ([ "$PREVIOUS_INITIAL_STATE" == "published" ] && [ "$PREVIOUS_CHANGED_TO" == "none" ]); then
echo "::error:: Release $CURRENT_RELEASE was already processed and published by this workflow"
echo "This suggests the release was manually reverted to draft after being published"
echo "Skipping to prevent duplicate processing"

echo "## ::error:: Workflow Skipped" >> $GITHUB_STEP_SUMMARY
echo "Release \`$CURRENT_RELEASE\` was already processed by this workflow." >> $GITHUB_STEP_SUMMARY
echo "To force reprocessing, either:" >> $GITHUB_STEP_SUMMARY
echo "- Use manual workflow dispatch" >> $GITHUB_STEP_SUMMARY
echo "- Create a new release version" >> $GITHUB_STEP_SUMMARY

should_skip=true
fi
fi

echo "should_skip=$should_skip" >> $GITHUB_OUTPUT

- name: Log in to Azure
if: steps.check_already_processed.outputs.should_skip == 'false'
uses: Azure/login@cb79c773a3cfa27f31f25eb3f677781210c9ce3d # v1.6.1
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}

- name: Configure Ruby
if: steps.check_already_processed.outputs.should_skip == 'false'
uses: ruby/setup-ruby@ca041f971d66735f3e5ff1e21cc13e2d51e7e535 # v1.233.0
with:
bundler-cache: true

- name: Install Fastlane
if: steps.check_already_processed.outputs.should_skip == 'false'
run: |
gem install bundler:2.2.27

- name: Download Store credentials
if: steps.check_already_processed.outputs.should_skip == 'false'
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: mobile
CREDENTIALS_FILE_NAME: ${{ inputs.credentials_filename }}
run: |
mkdir -p ${{ github.workspace }}/secrets

az storage blob download --account-name $ACCOUNT_NAME --container-name $CONTAINER_NAME \
--name $CREDENTIALS_FILE_NAME --file ${{ github.workspace }}/secrets/$CREDENTIALS_FILE_NAME --output none

- name: Get store versions
if: steps.check_already_processed.outputs.should_skip == 'false' && inputs.check_release_command != ''
id: get_store_versions
env:
CREDENTIALS_PATH: ${{ github.workspace }}/secrets/${{ inputs.credentials_filename }}
run: |
echo "Running custom release check command..."
echo "Command: ${{ inputs.check_release_command }}"

OUTPUT=$(eval "${{ inputs.check_release_command }}")

version_name=$(echo "$OUTPUT" | grep 'version_name: ' | cut -d' ' -f3)
version_number=$(echo "$OUTPUT" | grep 'version_number: ' | cut -d' ' -f3)

echo "store_version_name=$version_name" >> $GITHUB_OUTPUT
echo "store_version_number=$version_number" >> $GITHUB_OUTPUT

- name: Check if version is already released
if: steps.check_already_processed.outputs.should_skip == 'false'
id: check_version
env:
LATEST_DRAFT_VERSION_NAME: ${{ steps.get_latest_draft.outputs.latest_draft_version_name }}
LATEST_DRAFT_VERSION_NUMBER: ${{ steps.get_latest_draft.outputs.latest_draft_version_number }}
STORE_VERSION_NAME: ${{ steps.get_store_versions.outputs.store_version_name }}
STORE_VERSION_NUMBER: ${{ steps.get_store_versions.outputs.store_version_number }}
run: |
if [ "${{ inputs.project_type }}" == "ios" ]; then
if [ "$LATEST_DRAFT_VERSION_NAME" == "$STORE_VERSION_NUMBER" ] && [ "$LATEST_DRAFT_VERSION_NUMBER" == "$STORE_VERSION_NUMBER" ]; then
echo "iOS: Version name $LATEST_DRAFT_VERSION_NAME and version number $LATEST_DRAFT_VERSION_NUMBER is already released on store"
echo "version_released=true" >> $GITHUB_OUTPUT
else
echo "iOS: Version $LATEST_DRAFT_VERSION_NAME ($LATEST_DRAFT_VERSION_NUMBER) is not released on store. Latest version in the store is $STORE_VERSION_NAME ($STORE_VERSION_NUMBER)"
echo "version_released=false" >> $GITHUB_OUTPUT
fi
else
if [ "$LATEST_DRAFT_VERSION_NUMBER" == "$STORE_VERSION_NUMBER" ]; then
echo "Version $LATEST_DRAFT_VERSION_NUMBER is already released on store"
echo "version_released=true" >> $GITHUB_OUTPUT
else
echo "Version $LATEST_DRAFT_VERSION_NUMBER is not released on store. Latest version in the store is $STORE_VERSION_NUMBER, with version name: $STORE_VERSION_NAME"
echo "version_released=false" >> $GITHUB_OUTPUT
fi
fi

- name: Make GitHub release latest and non-pre-release
if: steps.check_version.outputs.version_released == 'true'
env:
TAG: ${{ steps.get_latest_draft.outputs.latest_draft_version_name }}
GH_TOKEN: ${{ github.token }}
run: echo "Publish" # gh release edit $TAG --prerelease=false --latest --draft=false

- name: Create workflow state artifact
run: |
if [ -f "release-info.json" ]; then
echo "release-info.json already exists, removing it"
rm -f release-info.json
fi

if [ "${{ steps.get_latest_draft.outputs.is_latest_draft }}" == "true" ]; then
release_tag="${{ steps.get_latest_draft.outputs.latest_draft_version_name }}"
else
release_tag="${{ steps.parse_previous_state.outputs.previous_release_tag }}"
fi

if [ "${{ steps.check_already_processed.outputs.should_skip }}" == "true" ]; then
initial_state="draft"
changed_to_state="none"
elif [ "${{ steps.get_latest_draft.outputs.is_latest_draft }}" == "true" ] && [ "${{ steps.check_already_processed.outputs.should_skip }}" == "false" ]; then
initial_state="draft"
if [ "${{ steps.check_version.outputs.version_released }}" == "true" ]; then
changed_to_state="published"
else
changed_to_state="none"
fi
elif [ "${{ steps.get_latest_draft.outputs.is_latest_draft }}" == "false" ]; then
initial_state="published"
changed_to_state="none"
fi

json=$(jq -n \
--arg release_tag "$release_tag" \
--arg initial_state "$initial_state" \
--arg changed_to_state "$changed_to_state" \
'{release_tag: $release_tag, initial_state: $initial_state, changed_to_state: $changed_to_state}')

echo "$json" > release-info.json

echo '```json' >> $GITHUB_STEP_SUMMARY
echo "$json" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY

- name: Upload workflow state artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ steps.compose_artifact_name.outputs.artifact_name }}
path: release-info.json
Loading