Skip to content

Add GitHub Actions release build workflow#3592

Draft
dannyroberts wants to merge 27 commits into
masterfrom
dmr/release-build
Draft

Add GitHub Actions release build workflow#3592
dannyroberts wants to merge 27 commits into
masterfrom
dmr/release-build

Conversation

@dannyroberts

@dannyroberts dannyroberts commented Mar 5, 2026

Copy link
Copy Markdown
Member

Product Description

No user-facing changes. This adds CI/CD infrastructure for release builds.

Technical Summary

Adds a GitHub Actions workflow to replace the Jenkins release build job. Also refactors the existing PR workflow to share code with the new release workflow.

New release workflow (.github/workflows/release-build.yml):

  • Triggers on commcare_* tag pushes or manual dispatch with a version parameter
  • Parses version from tag (e.g. commcare_2.61.6VERSION=2.61.6)
  • CCCORE_BRANCH is autoselected based on the most up-to-date tag in commcare-core at this version or below (e.g. if VERSION=2.61.6 it will try commcare_2.61.6, then commcare_2.61.5, then commcare_2.61.4, etc. and finally commcare_2.61, but it will never try commcare_2.61.7 or higher)
  • Builds and uploads all release variants: CommCare APK/AAB, CCC Staging APK, LTS APK/AAB
  • Runs BrowserStack instrumentation tests
  • Creates a draft GitHub release with versioned commcare_X.Y.Z.apk and .aab assets
  • Submits the build to CommCare HQ

Shared BrowserStack workflow (.github/workflows/browserstack-tests.yml):

  • Extracted as a reusable workflow_call workflow used by both PR and release workflows
  • Handles artifact download, androidTest APK decryption, and BrowserStack test execution
  • NOTE: after this PR is approved, I'll need to update the required branch logic to reference the new name/path of the browserstack tests.

PR workflow changes (.github/workflows/commcare-android-pr-workflow.yml):

  • Harmonized step names, action versions, and artifact names with the release workflow
  • Updated to use the shared BrowserStack reusable workflow
  • Added versionCode to shared gradle.properties
  • Added androidTest APK encryption for secure artifact transfer

Feature Flag

N/A

Safety Assurance

Safety story

  • The release workflow is new and will only trigger on tag pushes or manual dispatch — no impact on existing PR workflow
  • PR workflow changes are cosmetic (step names, action versions) plus the addition of shared BrowserStack workflow reference, which preserves identical behavior
  • Signing configuration matches the existing PR workflow approach

Automated test coverage

The workflows themselves run the existing testCommcareDebug test suite and BrowserStack instrumentation tests.

QA Plan

  • Trigger a test run via manual workflow dispatch to verify the release build pipeline end-to-end
  • Verify PR workflow continues to function correctly on a test PR

Labels and Review

  • Do we need to enhance the manual QA test coverage ? If yes, the "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, the "Release Note" label is set and a "Release Note" is specified in PR description.
  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

dannyroberts and others added 21 commits March 5, 2026 15:02
Migrate the release build pipeline from Jenkins to GitHub Actions.
The workflow triggers on commcare_* tags and runs tests, builds
release variants (CommCare, LTS, CCC Staging), submits the build
to CommCareHQ, runs BrowserStack tests, and uploads build artifacts.

Co-Authored-By: Claude Code <noreply@anthropic.com>
These variables were carried over from the Jenkins config but are not
used by scripts/submit_build.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extracts the version from the tag name and creates a draft release
with commcare_X.Y.Z.apk and commcare_X.Y.Z.aab attached.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parses commcare_X.Y.Z tag to set VERSION=X.Y.Z and
CCCORE_BRANCH=commcare_X.Y, with validation that the tag
matches the expected format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allows triggering the release build manually with a VERSION parameter.
Version parsing now handles both tag push and dispatch events, and
release steps use VERSION from env instead of re-parsing GITHUB_REF.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removes assembleCommcareReleaseAndroidTest gradle task, encryption
step, and artifact upload for the test APK.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restores the keystore from a base64-encoded secret and writes
signing properties to gradle.properties using a quoted heredoc
to safely handle special characters in secret values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Write full gradle.properties to ~/.gradle/ with JVM args, signing
config, and service API keys, matching the PR workflow configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update shared steps to match PR workflow conventions so that
differences between the two files reflect actual behavioral
differences only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move Python setup right after checkout (version 3.9)
- Move Gradle properties before JDK/Gradle setup
- Move Restore Keystore after tests, before build
- Reorder artifact uploads to match PR sequence
- Remove extra blank lines between steps to match PR style

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Consistently use format "Action Product release FORMAT" for build
and upload step names (e.g. "Upload CommCare release APK").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These settings were already partially in gradle.properties
(org.gradle.parallel=false). Move the remaining flags (no-daemon,
max-workers) there too and simplify the gradle command lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each gradle build step is now immediately followed by its artifact
upload, matching the PR workflow structure for minimal diff.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add androidTest APK build and upload to the build job
- Add browserstack-tests job matching the PR workflow
- Move submit_build.py to a separate job that runs after
  BrowserStack tests pass
- Expose VERSION as a job output for cross-job access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Combine draft release and submit build into a single "release" job
that runs after BrowserStack tests pass. Downloads artifacts from
the build job to attach to the GitHub release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow Gradle to use default daemon and parallelism settings
for faster builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move versionCode into the main Gradle properties block. Safe for
PR builds since computeVersionCode() only applies it to release
variant outputs and defaults to 1 when absent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Encrypt the instrumentation test APK before uploading as an
artifact and decrypt it in the BrowserStack job before use.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create browserstack-tests.yml as a reusable workflow called by both
PR and release workflows. Accepts an optional pr-number input and
required secrets for decryption and BrowserStack authentication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This change introduces a new GitHub Actions release build workflow and refactors CI/CD automation by extracting BrowserStack testing into a reusable workflow. The release-build.yml workflow orchestrates a multi-stage pipeline triggered on version tags: a build job that validates version format, compiles and signs APK/AAB artifacts, a BrowserStack testing job that validates the builds, and a release job that creates draft GitHub releases and submits builds. The browserstack-tests.yml workflow is extracted as a reusable component for modular test execution. The commcare-android-pr-workflow.yml is refactored to invoke the reusable BrowserStack workflow instead of inline test steps, and step names are updated for clarity.

Sequence Diagram(s)

sequenceDiagram
    actor Trigger as Tag/Dispatch Event
    participant GH as GitHub Actions
    participant Repo as Repository
    participant Build as Build System
    participant BS as BrowserStack
    participant API as GitHub API
    
    Trigger->>GH: Trigger release-build workflow
    GH->>Repo: Checkout commcare-android & commcare-core
    Repo-->>GH: Source code
    GH->>Build: Validate version & build
    Build->>Build: Compile, sign, create APK/AAB
    Build-->>GH: Build artifacts
    GH->>BS: Download artifacts & run tests
    BS-->>GH: Test results
    GH->>API: Create draft GitHub release
    API-->>GH: Release created
    GH->>GH: Submit build with credentials
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and directly summarizes the main change: adding a GitHub Actions release build workflow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering product description, technical summary with detailed implementation, safety assurance with safety story and test coverage, and QA plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dmr/release-build
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release-build.yml:
- Around line 157-162: The release job's needs array incorrectly lists `build`;
update the `release` job's needs to include `build-test-assemble` (so it reads
needs: [build-test-assemble, browserstack-tests]) so that the `release` job can
access `needs.build-test-assemble.outputs.version`; modify the `release` job's
`needs` entry accordingly in the workflow to ensure the output reference
`needs.build-test-assemble.outputs.version` is valid.
- Around line 135-147: The workflow currently runs the Gradle task
bundleLtsRelease (step "Bundle LTS release AAB") but never runs
assembleLtsRelease, so the "Upload LTS release APK" artifact path will not
exist; add a new job step that runs ./gradlew assembleLtsRelease in the
commcare-android working-directory (before the "Upload LTS release APK" step) so
the APK at app/build/outputs/apk/lts/release/app-lts-release.apk is produced,
keeping the existing artifact upload steps unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7721612c-009c-41f0-b2ff-1881a932e28a

📥 Commits

Reviewing files that changed from the base of the PR and between c1d5cc9 and 2a344fb.

📒 Files selected for processing (3)
  • .github/workflows/browserstack-tests.yml
  • .github/workflows/commcare-android-pr-workflow.yml
  • .github/workflows/release-build.yml

Comment on lines +135 to +147
- name: Bundle LTS release AAB
run: ./gradlew bundleLtsRelease
working-directory: commcare-android
- name: Upload LTS release APK
uses: actions/upload-artifact@v6
with:
name: app-lts-release-apk
path: commcare-android/app/build/outputs/apk/lts/release/app-lts-release.apk
- name: Upload LTS release AAB
uses: actions/upload-artifact@v6
with:
name: app-lts-release-aab
path: commcare-android/app/build/outputs/bundle/ltsRelease/app-lts-release.aab

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing assembleLtsRelease step — LTS APK upload will fail.

The workflow bundles the LTS AAB (line 136) but then attempts to upload an LTS APK (lines 138-142) that was never assembled. The bundleLtsRelease Gradle task produces only the AAB, not the APK.

🐛 Proposed fix: Add LTS APK assembly step
+     - name: Assemble LTS release APK
+       run: ./gradlew assembleLtsRelease
+       working-directory: commcare-android
      - name: Bundle LTS release AAB
        run: ./gradlew bundleLtsRelease
        working-directory: commcare-android
      - name: Upload LTS release APK
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-build.yml around lines 135 - 147, The workflow
currently runs the Gradle task bundleLtsRelease (step "Bundle LTS release AAB")
but never runs assembleLtsRelease, so the "Upload LTS release APK" artifact path
will not exist; add a new job step that runs ./gradlew assembleLtsRelease in the
commcare-android working-directory (before the "Upload LTS release APK" step) so
the APK at app/build/outputs/apk/lts/release/app-lts-release.apk is produced,
keeping the existing artifact upload steps unchanged.

Comment on lines +157 to +162
release:
needs: [build, browserstack-tests]
name: Release
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.build-test-assemble.outputs.version }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix job dependency reference: build does not exist.

The needs array references build, but the actual job name is build-test-assemble. This will cause the workflow to fail. Additionally, line 162 won't be able to access build-test-assemble.outputs.version since only jobs listed in needs expose their outputs.

🐛 Proposed fix
  release:
-   needs: [build, browserstack-tests]
+   needs: [build-test-assemble, browserstack-tests]
    name: Release
    runs-on: ubuntu-latest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
release:
needs: [build, browserstack-tests]
name: Release
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.build-test-assemble.outputs.version }}
release:
needs: [build-test-assemble, browserstack-tests]
name: Release
runs-on: ubuntu-latest
env:
VERSION: ${{ needs.build-test-assemble.outputs.version }}
🧰 Tools
🪛 actionlint (1.7.11)

[error] 157-157: job "release" needs job "build" which does not exist in this workflow

(job-needs)


[error] 162-162: property "build-test-assemble" is not defined in object type {browserstack-tests: {outputs: {}; result: string}}

(expression)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-build.yml around lines 157 - 162, The release
job's needs array incorrectly lists `build`; update the `release` job's needs to
include `build-test-assemble` (so it reads needs: [build-test-assemble,
browserstack-tests]) so that the `release` job can access
`needs.build-test-assemble.outputs.version`; modify the `release` job's `needs`
entry accordingly in the workflow to ensure the output reference
`needs.build-test-assemble.outputs.version` is valid.

dannyroberts and others added 4 commits March 5, 2026 15:12
The build-test-assemble job only runs Gradle tasks and doesn't
need Python.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change print statements to print() function calls and fix
identity comparison (is) to equality (==) for integer check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dannyroberts

Copy link
Copy Markdown
Member Author

I had Claude Code do a somewhat complex rebase-like thing to get a branch based on commcare_2.61.5 that I could do a test workflow run against here https://github.com/dimagi/commcare-android/actions/runs/22975318635/job/66702331956. It's basically commcare_2.61.5 plus all the recent GHA changes plus the changes in this PR. (The way the GHA workflow works, you can't run the workflow from one branch on the code from another branch, so this is the only way to test this PR's new release workflow against a real past release tag.)

My prompt was

I want a dmr/release-build-2.61.5 branch that has the following properties. It is based on refs/tags/commcare_2.61.5. On top of that, it contains every commit in every PR that touches .github/ that is not
already on refs/tags/commcare_2.61.5.

and it appears to have done that. (It also correctly inferred that I wanted dmr/release-build on there.) I confirmed that diff --stat from refs/tags/commcare_2.61.5 dmr/release-build-2.61.5 only had changes in files under .github and scripts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dannyroberts

dannyroberts commented Mar 11, 2026

Copy link
Copy Markdown
Member Author

Seems to be running from the right branch setup, but is failing https://github.com/dimagi/commcare-android/actions/runs/22978879809 with

> Task :app:testCommcareDebugUnitTest

org.commcare.android.tests.formsave.CyclicCasesPurgeTest > testFormSaveResultingIntoCaseCycles_ShouldFail FAILED
    java.lang.NullPointerException at CyclicCasesPurgeTest.java:80

249 tests completed, 1 failed


FAILURE: Build failed with an exception.

Update: I reran this build job and it passed. Ahmad also mentioned that this is a known flaky test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant