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 required: true 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 required: true dry_run: type: boolean description: 'Run the workflow in dry-run mode without making any changes' required: false default: false jobs: publish-release: name: Publish GitHub Release ${{ inputs.release_name }} runs-on: ubuntu-24.04 permissions: contents: write id-token: write actions: write env: _DRY_RUN: ${{ inputs.dry_run }} _WORKFLOW_NAME: ${{ inputs.workflow_name }} steps: - name: Check out repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 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" if [ "$_DRY_RUN" != "true" ]; then echo "Disabling workflow to prevent further runs" gh workflow disable "$_WORKFLOW_NAME" fi exit 0 fi latest_draft_tag=$(jq -r '.tagName' <<< $latest_release) echo "latest_draft_tag=$latest_draft_tag" >> $GITHUB_OUTPUT 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 }} 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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.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 "::warning::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: Configure Ruby if: steps.check_already_processed.outputs.should_skip == 'false' uses: ruby/setup-ruby@efbf473cab83af4468e8606cc33eca9281bb213f # v1.256.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: Log in to Azure if: steps.check_already_processed.outputs.should_skip == 'false' uses: bitwarden/gh-actions/azure-login@main with: subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} tenant_id: ${{ secrets.AZURE_TENANT_ID }} client_id: ${{ secrets.AZURE_CLIENT_ID }} - 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: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main - 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_NAME" ] && [ "$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 id: make_release_latest if: steps.check_version.outputs.version_released == 'true' && inputs.dry_run != true env: TAG: ${{ steps.get_latest_draft.outputs.latest_draft_tag }} GH_TOKEN: ${{ github.token }} PRODUCT: ${{ inputs.release_name }} run: | if [ "$PRODUCT" = "Password Manager" ]; then gh release edit $TAG --prerelease=false --latest --draft=false else gh release edit $TAG --prerelease=false --draft=false fi - name: Disable workflow if published if: ${{ steps.make_release_latest.conclusion == 'success' }} env: GH_TOKEN: ${{ github.token }} run: | if [ "$_DRY_RUN" = "true" ]; then echo "Dry run mode - skipping gh workflow disable command" gh workflow list "$_WORKFLOW_NAME" else echo "Disabling workflow to prevent further runs" gh workflow disable "$_WORKFLOW_NAME" fi - 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