diff --git a/.github/workflows/simcapture-disable-upstream-workflows.yml b/.github/workflows/simcapture-disable-upstream-workflows.yml new file mode 100644 index 00000000000..c6d2517e356 --- /dev/null +++ b/.github/workflows/simcapture-disable-upstream-workflows.yml @@ -0,0 +1,33 @@ +name: Disable non-SimCapture and non-Copilot workflows + +on: + push: + branches: [main] + paths: ['.github/workflows/**'] + workflow_dispatch: + +jobs: + disable-non-prefixed-workflows: + runs-on: ubuntu-latest + steps: + - name: Disable workflows without approved prefixes + env: + GH_TOKEN: ${{ secrets.GH_WORKFLOW_SYNC_PAT_SIMCAPTURE }} + run: | + set -euo pipefail + + gh api "repos/${{ github.repository }}/actions/workflows" \ + --paginate --jq '.workflows[] | select(.state == "active") | [.id, .path] | @tsv' \ + | while IFS=$'\t' read -r wf_id wf_path; do + filename=$(basename "$wf_path") + + case "$filename" in + simcapture-*|copilot-*) + echo "Kept enabled: $filename" + ;; + *) + gh api --method PUT "repos/${{ github.repository }}/actions/workflows/$wf_id/disable" + echo "Disabled: $filename" + ;; + esac + done diff --git a/.github/workflows/simcapture-github-release-signed-apk.yml b/.github/workflows/simcapture-github-release-signed-apk.yml new file mode 100644 index 00000000000..2fda9ea3943 --- /dev/null +++ b/.github/workflows/simcapture-github-release-signed-apk.yml @@ -0,0 +1,130 @@ +name: Build signed SimCapture APK release + +on: + workflow_dispatch: + push: + branches: + - main + +concurrency: + group: simcapture-github-release-signed-apk + cancel-in-progress: false + +env: + MAIN_PROJECT_MODULE: app + +jobs: + simcapture-github-release-signed-apk: + runs-on: ubuntu-latest + permissions: + contents: write + env: + CURRENT_FORK_REPOSITORY: ${{ github.repository }} + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Get current date and time + id: date-time + run: echo "dateTimeUtc=$(date -u +'%Y-%m-%d-%H%M')" >> "$GITHUB_OUTPUT" + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: zulu + java-version: '17' + cache: gradle + + - name: Change wrapper permissions + run: chmod +x ./gradlew + + - name: Read upstream app version + id: read-version + working-directory: ./gradle + run: echo "vName=$(grep '^vName' libs.versions.toml | awk -F' = ' '{print $2}' | tr -d '\"')" >> "$GITHUB_OUTPUT" + + - name: Determine next SimCapture fork release + id: release-info + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + last_fork_number=0 + + while IFS= read -r tag_name; do + [[ "$tag_name" =~ ^SimCapture-DHIS2-v[0-9]+(\.[0-9]+)*-fork-([0-9]+)$ ]] || continue + + candidate_fork_number="${BASH_REMATCH[2]}" + if (( candidate_fork_number > last_fork_number )); then + last_fork_number="$candidate_fork_number" + fi + done < <( + gh api --paginate "repos/$CURRENT_FORK_REPOSITORY/releases?per_page=100" --jq '.[].tag_name' + ) + + next_fork_number=$((last_fork_number + 1)) + release_tag="SimCapture-DHIS2-v${{ steps.read-version.outputs.vName }}-fork-${next_fork_number}" + release_apk_name="${release_tag}-signed-release.apk" + release_apk_path="$RUNNER_TEMP/$release_apk_name" + + { + echo "forkNumber=$next_fork_number" + echo "releaseTag=$release_tag" + echo "releaseApkName=$release_apk_name" + echo "releaseApkPath=$release_apk_path" + } >> "$GITHUB_OUTPUT" + + - name: Decode keystore + id: decode-keystore + # Third-party action - pinned to commit SHA. + uses: timheuer/base64-to-file@604a8926a81a2da120d09b06bb76da9bba5aee6e + with: + fileName: dhis_keystore.jks + encodedString: ${{ secrets.KEYSTORE_BASE64 }} + + - name: Build signed release APK + run: ./gradlew app:assembleDhis2Release + env: + SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + SIGNING_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + SIGNING_KEYSTORE_PATH: ${{ steps.decode-keystore.outputs.filePath }} + + - name: Rename signed release APK for SimCapture release + run: | + set -euo pipefail + + cp \ + "${MAIN_PROJECT_MODULE}/build/outputs/apk/dhis2/release/dhis2-v${{ steps.read-version.outputs.vName }}.apk" \ + "${{ steps.release-info.outputs.releaseApkPath }}" + + - name: Upload signed release APK artifact + uses: actions/upload-artifact@v7.0.0 + with: + name: ${{ steps.release-info.outputs.releaseTag }} + path: ${{ steps.release-info.outputs.releaseApkPath }} + + - name: Create GitHub release with signed APK + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + release_notes=$(cat </dev/null + echo "Force-updated destination $DESTINATION_BRANCH branch head to $source_head_sha." + + post_sync_source_head_sha=$( + gh api "repos/$SOURCE_UPSTREAM_REPOSITORY/branches/$SOURCE_BRANCH" --jq '.commit.sha' + ) + post_sync_destination_head_sha=$( + gh api "repos/$CURRENT_FORK_REPOSITORY/branches/$DESTINATION_BRANCH" --jq '.commit.sha' + ) + + echo "Source $SOURCE_UPSTREAM_REPOSITORY:$SOURCE_BRANCH branch head after sync: $post_sync_source_head_sha." + echo "Destination $CURRENT_FORK_REPOSITORY:$DESTINATION_BRANCH branch head after sync: $post_sync_destination_head_sha." + + if [[ "$post_sync_destination_head_sha" != "$post_sync_source_head_sha" ]]; then + echo "Error: Branch heads $SOURCE_UPSTREAM_REPOSITORY:$SOURCE_BRANCH and $CURRENT_FORK_REPOSITORY:$DESTINATION_BRANCH do not match after sync." >&2 + exit 1 + fi + + echo "Branch sync complete." diff --git a/.github/workflows/simcapture-upstream-release-pr.yml b/.github/workflows/simcapture-upstream-release-pr.yml new file mode 100644 index 00000000000..19e8de26305 --- /dev/null +++ b/.github/workflows/simcapture-upstream-release-pr.yml @@ -0,0 +1,203 @@ +name: Create PRs for upstream releases merged into upstream-main + +on: + workflow_run: + workflows: + - "Sync upstream repo's main into this fork repo's upstream-main" + types: + - completed + workflow_dispatch: + pull_request: + types: + - closed + +concurrency: + group: simcapture-upstream-release-pr + cancel-in-progress: false + +jobs: + simcapture-upstream-release-pr: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + env: + CURRENT_FORK_OWNER: ${{ github.repository_owner }} + CURRENT_FORK_REPOSITORY: ${{ github.repository }} + CURRENT_FORK_MAIN_BRANCH: main + UPSTREAM_REPOSITORY: dhis2/dhis2-android-capture-app + UPSTREAM_TRACKING_MAIN_BRANCH: upstream-main + steps: + - name: Create or update PR branches for new upstream releases + env: + GH_TOKEN: ${{ secrets.GH_WORKFLOW_SYNC_PAT_SIMCAPTURE }} + run: | + set -euo pipefail + + is_commit_reachable_from_ref() { + commit_sha="$1" + ref_name="$2" + + if ! compare_json=$(gh api "repos/$CURRENT_FORK_REPOSITORY/compare/$commit_sha...$ref_name"); then + return 1 + fi + + merge_base_sha=$(jq -r '.merge_base_commit.sha // empty' <<< "$compare_json") + [[ "$merge_base_sha" == "$commit_sha" ]] + } + + create_or_update_branch_ref() { + branch_name="$1" + target_sha="$2" + encoded_branch_name=$(jq -rn --arg value "$branch_name" '$value | @uri') + + if gh api "repos/$CURRENT_FORK_REPOSITORY/git/ref/heads/$encoded_branch_name"; then + gh api --method PATCH "repos/$CURRENT_FORK_REPOSITORY/git/refs/heads/$encoded_branch_name" \ + --raw-field sha="$target_sha" \ + --field force=true + echo "Updated $branch_name branch to $target_sha." + else + gh api --method POST "repos/$CURRENT_FORK_REPOSITORY/git/refs" \ + --raw-field ref="refs/heads/$branch_name" \ + --raw-field sha="$target_sha" + echo "Created $branch_name branch at $target_sha." + fi + } + + main_head_sha=$(gh api "repos/$CURRENT_FORK_REPOSITORY/branches/$CURRENT_FORK_MAIN_BRANCH" --jq '.commit.sha') + main_tree_sha=$(gh api "repos/$CURRENT_FORK_REPOSITORY/git/commits/$main_head_sha" --jq '.tree.sha') + + declare -A release_sha_by_branch=() + declare -a ordered_release_branches=() + release_prs_file=$(mktemp) + trap 'rm -f "$release_prs_file"' EXIT + + gh api --method GET --paginate search/issues \ + -f q="repo:$UPSTREAM_REPOSITORY is:pr is:merged base:main head:release/" \ + -f per_page=100 \ + --jq '.items[].number' \ + | while IFS= read -r pr_number; do + [[ -z "$pr_number" ]] && continue + gh api "repos/$UPSTREAM_REPOSITORY/pulls/$pr_number" \ + --jq '{number: .number, merged_at: .merged_at, merge_commit_sha: .merge_commit_sha, head_ref: .head.ref, title: .title}' + printf '\n' + done > "$release_prs_file" + + if [[ ! -s "$release_prs_file" ]]; then + echo "No upstream release PRs merged into upstream main branch found." + exit 0 + fi + + while IFS= read -r pr_json; do + [[ -z "$pr_json" ]] && continue + + pr_number=$(jq -r '.number' <<< "$pr_json") + release_branch=$(jq -r '.head_ref' <<< "$pr_json") + release_sha=$(jq -r '.merge_commit_sha' <<< "$pr_json") + + echo "Inspecting upstream PR #$pr_number for $release_branch at $release_sha." + + if ! is_commit_reachable_from_ref "$release_sha" "$UPSTREAM_TRACKING_MAIN_BRANCH"; then + echo "Skipping upstream PR #$pr_number because $release_sha is not reachable from $UPSTREAM_TRACKING_MAIN_BRANCH." + continue + fi + + if is_commit_reachable_from_ref "$release_sha" "$CURRENT_FORK_MAIN_BRANCH"; then + echo "Latest upstream release merge already reachable from $CURRENT_FORK_MAIN_BRANCH: $release_sha ($release_branch via upstream PR #$pr_number)." + break + fi + + if [[ -n ${release_sha_by_branch[$release_branch]+x} ]]; then + continue + fi + + release_sha_by_branch["$release_branch"]="$release_sha" + ordered_release_branches+=("$release_branch") + done < <( + jq -sc '. | sort_by(.merged_at) | reverse | .[] | select(.merged_at != null and .merge_commit_sha != null and (.head_ref | startswith("release/")))' "$release_prs_file" + ) + + if [[ ${#ordered_release_branches[@]} -eq 0 ]]; then + echo "No newer upstream release merges require PRs." + exit 0 + fi + + for ((id=${#ordered_release_branches[@]} - 1; id >= 0; id--)); do + release_branch="${ordered_release_branches[$id]}" + release_version="${release_branch#release/}" + pr_branch="upstream-release/$release_version" + release_sha="${release_sha_by_branch[$release_branch]}" + release_tree_sha=$(gh api "repos/$CURRENT_FORK_REPOSITORY/git/commits/$release_sha" --jq '.tree.sha') + + echo "Processing $release_branch at $release_sha." + + if [[ "$release_tree_sha" == "$main_tree_sha" ]]; then + echo "Skipping $pr_branch because its tree already matches $CURRENT_FORK_MAIN_BRANCH." + continue + fi + + existing_prs=$( + gh api --method GET "repos/$CURRENT_FORK_REPOSITORY/pulls" \ + -f state=all \ + -f base="$CURRENT_FORK_MAIN_BRANCH" \ + -f head="$CURRENT_FORK_OWNER:$pr_branch" + ) + + open_pr_number=$(jq -r '.[] | select(.state == "open") | .number' <<< "$existing_prs" | head -n 1) + matching_pr_number=$(jq -r --arg sha "$release_sha" '.[] | select(.head.sha == $sha) | .number' <<< "$existing_prs" | head -n 1) + + if [[ -n "$open_pr_number" && "$open_pr_number" != "null" ]]; then + create_or_update_branch_ref "$pr_branch" "$release_sha" + echo "Updated existing PR branch $pr_branch for open PR #$open_pr_number." + continue + fi + + if [[ -n "$matching_pr_number" && "$matching_pr_number" != "null" ]]; then + echo "Skipping duplicate PR creation for $pr_branch at $release_sha because PR #$matching_pr_number already exists." + continue + fi + + create_or_update_branch_ref "$pr_branch" "$release_sha" + + pr_title="[Automatic] Upstream release $release_version" + pr_body=$(cat <-fork-`. +An example of a release APK name: `SimCapture-DHIS2-v3.3.1-fork-1-signed-release.apk`. + +### GitHub Actions + +The workflows of this fork repo should start with `simcapture-`. + +The upstream `README` is preserved below. + + # README # [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=dhis2_dhis2-android-capture-app&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=dhis2_dhis2-android-capture-app) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 479b5a6139f..baeb1c83b85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,7 +82,7 @@ android { } defaultConfig { - applicationId = "com.dhis2" + applicationId = "com.simprints.simcapture" compileSdk = libs.versions.sdk.get().toInt() targetSdk = libs.versions.sdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()