From c2f934802e7d38d011f2dedf63f17058a9f5349e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Mon, 23 Jun 2025 16:04:31 +0200 Subject: [PATCH] [BRE-769] Add GitHub Actions workflow for publishing mobile releases (#401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GitHub Actions workflow for publishing mobile releases * Fix version comparison logic in GitHub Actions workflow for mobile release * Add artifact name composition step in GitHub Actions workflow * Fix version comparison logic in GitHub Actions workflow for mobile release * Refactor GitHub Actions workflow to enhance version handling and project type input * Enhance version release logging to include version name in GitHub Actions workflow * Update GitHub release command to publish the release * Update .github/workflows/_publish-mobile-github-release.yml Co-authored-by: Álison Fernandes * Update .github/workflows/_publish-mobile-github-release.yml Co-authored-by: Álison Fernandes * fix workflow linter --------- Co-authored-by: Álison Fernandes --- .../_publish-mobile-github-release.yml | 287 ++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 .github/workflows/_publish-mobile-github-release.yml diff --git a/.github/workflows/_publish-mobile-github-release.yml b/.github/workflows/_publish-mobile-github-release.yml new file mode 100644 index 00000000..544789e4 --- /dev/null +++ b/.github/workflows/_publish-mobile-github-release.yml @@ -0,0 +1,287 @@ +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 + 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 "::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: 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_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 + if: steps.check_version.outputs.version_released == 'true' + env: + TAG: ${{ steps.get_latest_draft.outputs.latest_draft_version_name }} + GH_TOKEN: ${{ github.token }} + run: 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