From 5550e3e0411316f70f671ec763796dde038dae51 Mon Sep 17 00:00:00 2001 From: andresmr Date: Tue, 30 Jun 2026 10:39:29 +0200 Subject: [PATCH 1/4] ci: add maintainer-only /retest workflow for fork PRs Fork pull_request runs do not receive repository secrets, so the secret-dependent required checks (code-quality, run-form-tests, deployment_job) fail and a plain re-run cannot fix them. Add a /retest PR-comment workflow that runs in the base repo context (secrets available), checks out the PR head, runs those tasks, and reports check runs with the exact required names against the PR head SHA so the required checks can turn green. Triggering is restricted to users with write/admin permission. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/retest-fork-pr.yml | 257 +++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 .github/workflows/retest-fork-pr.yml diff --git a/.github/workflows/retest-fork-pr.yml b/.github/workflows/retest-fork-pr.yml new file mode 100644 index 0000000000..8c24b388f0 --- /dev/null +++ b/.github/workflows/retest-fork-pr.yml @@ -0,0 +1,257 @@ +name: Retest Fork PR + +# Manually re-run the secret-dependent required checks on a fork PR. +# +# Fork pull_request runs do not receive repository secrets, so the +# code-quality / run-form-tests / deployment_job checks fail and a plain +# re-run cannot fix them. A maintainer commenting "/retest" on the PR runs +# this workflow in the BASE repo context (secrets available), checks out the +# PR head, runs those tasks, and reports check runs with the exact required +# names against the PR head SHA so the required checks can turn green. +# +# SECURITY: this executes the fork's code (Gradle build scripts, the built +# APK on BrowserStack) with secrets present. Only trigger it on PRs whose +# diff you have reviewed. Triggering is restricted to users with write/admin. + +on: + issue_comment: + types: [created] + +permissions: + checks: write # create/update check runs on the PR head SHA + contents: read + issues: write # add reactions on the PR conversation (issue) comment + pull-requests: read # read PR head SHA/ref + +concurrency: + group: retest-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + authorize: + # Only react to "/retest" comments on pull requests. + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/retest') }} + runs-on: ubuntu-latest + outputs: + authorized: ${{ steps.check.outputs.authorized }} + head_sha: ${{ steps.check.outputs.head_sha }} + head_ref: ${{ steps.check.outputs.head_ref }} + steps: + - name: Authorize and resolve PR head + id: check + uses: actions/github-script@v7 + with: + script: | + const body = (context.payload.comment.body || '').trim(); + if (body !== '/retest') { + core.info(`Comment is not exactly "/retest" (got: "${body}"). Ignoring.`); + core.setOutput('authorized', 'false'); + return; + } + + // Require the commenter to have write or admin permission. + const actor = context.payload.comment.user.login; + const perm = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: actor, + }); + const level = perm.data.permission; // admin | write | read | none + if (level !== 'admin' && level !== 'write') { + core.warning(`@${actor} has '${level}' permission; /retest requires write or admin.`); + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'confused', + }); + core.setOutput('authorized', 'false'); + return; + } + + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + core.setOutput('authorized', 'true'); + core.setOutput('head_sha', pr.data.head.sha); + core.setOutput('head_ref', pr.data.head.ref); + + // Acknowledge the request. + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: 'eyes', + }); + + retest: + needs: authorize + if: ${{ needs.authorize.outputs.authorized == 'true' }} + runs-on: ubuntu-latest + env: + # Required-check names — MUST match the ruleset's required status checks exactly. + CHECK_CODE_QUALITY: code-quality + CHECK_FORM_TESTS: run-form-tests + CHECK_DEPLOYMENT: deployment_job + steps: + - name: Create in-progress check runs + id: checks + uses: actions/github-script@v7 + with: + script: | + const headSha = '${{ needs.authorize.outputs.head_sha }}'; + const detailsUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const names = [ + process.env.CHECK_CODE_QUALITY, + process.env.CHECK_FORM_TESTS, + process.env.CHECK_DEPLOYMENT, + ]; + const ids = {}; + for (const name of names) { + const res = await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name, + head_sha: headSha, + status: 'in_progress', + details_url: detailsUrl, + }); + ids[name] = res.data.id; + } + core.setOutput('ids', JSON.stringify(ids)); + + - name: Checkout PR head + uses: actions/checkout@v7 + with: + ref: ${{ needs.authorize.outputs.head_sha }} + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + + - name: Change wrapper permissions + run: chmod +x ./gradlew + + # ---- code-quality (Sonar) ---- + - name: Code quality (Sonar) + id: code_quality + continue-on-error: true + env: + GIT_BRANCH: ${{ needs.authorize.outputs.head_ref }} + GIT_BRANCH_DEST: ${{ github.event.repository.default_branch }} + PULL_REQUEST: ${{ github.event.issue.number }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + ./gradlew testDebugUnitTest testDhis2DebugUnitTest testAndroidHostTest jacocoReport --stacktrace --no-daemon + chmod +x ./scripts/sonarqube.sh + ./scripts/sonarqube.sh + + # ---- deployment_job (signed debug APK build) ---- + - name: Decode keystore + id: decode_keystore + if: ${{ always() }} + uses: timheuer/base64-to-file@dfa5a5dd38803cb4f5c3a081eeb28e8362d43e9c + with: + fileName: 'debug.keystore' + encodedString: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} + + - name: Build signed debug APK + id: deployment + if: ${{ always() }} + continue-on-error: true + env: + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + DEBUG_KEYSTORE_ALIAS: ${{ secrets.DEBUG_KEYSTORE_ALIAS }} + DEBUG_KEY_PASS: ${{ secrets.DEBUG_KEY_PASS }} + DEBUG_KEYSTORE_PASSWORD: ${{ secrets.DEBUG_KEYSTORE_PASSWORD }} + DEBUG_KEYSTORE_PATH: ${{ steps.decode_keystore.outputs.filePath }} + OPEN_ID_AUTH_SCHEME: ${{ secrets.OPEN_ID_AUTH_SCHEME }} + OPEN_ID_TYPE: ${{ secrets.OPEN_ID_TYPE }} + OPEN_ID_SERVER: ${{ secrets.OPEN_ID_SERVER }} + OPEN_ID_CLIENT: ${{ secrets.OPEN_ID_CLIENT }} + OPEN_ID_REDIRECT_URI: ${{ secrets.OPEN_ID_REDIRECT_URI }} + OPEN_ID_DISCOVERY_URI: ${{ secrets.OPEN_ID_DISCOVERY_URI }} + OPEN_ID_AUTHORIZATION_URL: ${{ secrets.OPEN_ID_AUTHORIZATION_URL }} + OPEN_ID_TOKEN_URL: ${{ secrets.OPEN_ID_TOKEN_URL }} + OPEN_ID_BUTTON_TEXT: ${{ secrets.OPEN_ID_BUTTON_TEXT }} + run: ./gradlew assembleDhis2Debug + + # ---- run-form-tests (BrowserStack) ---- + - name: Build form test APK + id: build_form_apk + if: ${{ always() }} + continue-on-error: true + run: ./gradlew :form:assembleAndroidTest --no-daemon + + - name: Run form tests (BrowserStack) + id: form_tests + if: ${{ always() && steps.build_form_apk.outcome == 'success' }} + continue-on-error: true + env: + BROWSERSTACK_USR: ${{ secrets.BROWSERSTACK_USERNAME }} + BROWSERSTACK_PSW: ${{ secrets.BROWSERSTACK_PASSWORD }} + buildTag: ${{ needs.authorize.outputs.head_ref }} - form (retest) + run: | + form_apk=$(find form/build/outputs -iname "*.apk" | sed -n 1p) + export form_apk_path="${GITHUB_WORKSPACE}/${form_apk}" + cd scripts + chmod +x browserstackJenkinsForm.sh + ./browserstackJenkinsForm.sh + + # ---- Conclude check runs on the PR head SHA ---- + - name: Conclude check runs + if: ${{ always() }} + uses: actions/github-script@v7 + env: + CODE_QUALITY_OUTCOME: ${{ steps.code_quality.outcome }} + DEPLOYMENT_OUTCOME: ${{ steps.deployment.outcome }} + FORM_BUILD_OUTCOME: ${{ steps.build_form_apk.outcome }} + FORM_TESTS_OUTCOME: ${{ steps.form_tests.outcome }} + with: + script: | + const ids = JSON.parse('${{ steps.checks.outputs.ids }}' || '{}'); + const detailsUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const toConclusion = (o) => (o === 'success' ? 'success' : 'failure'); + + const formOutcome = + process.env.FORM_BUILD_OUTCOME === 'success' + ? process.env.FORM_TESTS_OUTCOME + : 'failure'; + + const results = { + [process.env.CHECK_CODE_QUALITY]: toConclusion(process.env.CODE_QUALITY_OUTCOME), + [process.env.CHECK_FORM_TESTS]: toConclusion(formOutcome), + [process.env.CHECK_DEPLOYMENT]: toConclusion(process.env.DEPLOYMENT_OUTCOME), + }; + + let allGreen = true; + for (const [name, conclusion] of Object.entries(results)) { + if (conclusion !== 'success') allGreen = false; + const id = ids[name]; + if (!id) { + core.warning(`No check run id recorded for "${name}"; skipping.`); + continue; + } + await github.rest.checks.update({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: id, + status: 'completed', + conclusion, + details_url: detailsUrl, + }); + } + + await github.rest.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: allGreen ? '+1' : '-1', + }); From 25baffbd613dc5409fed7646837491de8bdb2840 Mon Sep 17 00:00:00 2001 From: andresmr Date: Tue, 30 Jun 2026 10:55:15 +0200 Subject: [PATCH 2/4] ci: extract shared build steps into composite actions Deduplicate the signed-APK build and the BrowserStack form-test run that were duplicated between the retest workflow and ci.yml / continuous-delivery.yml. - Add .github/actions/build-signed-apk (decode keystore + assembleDhis2Debug with signing/OpenID/Sentry inputs), used by deployment_job and retest. - Add .github/actions/run-form-tests (locate form APK + browserstack run), used by run-form-tests and retest. Job names are unchanged, so required-check names are unaffected. Co-Authored-By: Claude Opus 4.8 --- .github/actions/build-signed-apk/action.yml | 78 +++++++++++++++++++++ .github/actions/run-form-tests/action.yml | 31 ++++++++ .github/workflows/ci.yml | 15 ++-- .github/workflows/continuous-delivery.yml | 41 +++++------ .github/workflows/retest-fork-pr.yml | 55 ++++++--------- 5 files changed, 152 insertions(+), 68 deletions(-) create mode 100644 .github/actions/build-signed-apk/action.yml create mode 100644 .github/actions/run-form-tests/action.yml diff --git a/.github/actions/build-signed-apk/action.yml b/.github/actions/build-signed-apk/action.yml new file mode 100644 index 0000000000..34dc28eb99 --- /dev/null +++ b/.github/actions/build-signed-apk/action.yml @@ -0,0 +1,78 @@ +name: Build signed debug APK +description: >- + Decodes the debug keystore and assembles the signed Dhis2 debug APK. + Assumes the repository is checked out and a JDK is already set up. + +inputs: + keystore-base64: + description: Base64-encoded debug keystore. + required: true + keystore-alias: + description: Debug keystore alias. + required: true + key-pass: + description: Debug key password. + required: true + keystore-password: + description: Debug keystore password. + required: true + sentry-dsn: + description: Sentry DSN. + required: false + default: '' + open-id-auth-scheme: + required: false + default: '' + open-id-type: + required: false + default: '' + open-id-server: + required: false + default: '' + open-id-client: + required: false + default: '' + open-id-redirect-uri: + required: false + default: '' + open-id-discovery-uri: + required: false + default: '' + open-id-authorization-url: + required: false + default: '' + open-id-token-url: + required: false + default: '' + open-id-button-text: + required: false + default: '' + +runs: + using: composite + steps: + - name: Decode keystore + id: decode_keystore + uses: timheuer/base64-to-file@dfa5a5dd38803cb4f5c3a081eeb28e8362d43e9c + with: + fileName: 'debug.keystore' + encodedString: ${{ inputs.keystore-base64 }} + + - name: Assemble Dhis2 debug APK + shell: bash + env: + SENTRY_DSN: ${{ inputs.sentry-dsn }} + DEBUG_KEYSTORE_ALIAS: ${{ inputs.keystore-alias }} + DEBUG_KEY_PASS: ${{ inputs.key-pass }} + DEBUG_KEYSTORE_PASSWORD: ${{ inputs.keystore-password }} + DEBUG_KEYSTORE_PATH: ${{ steps.decode_keystore.outputs.filePath }} + OPEN_ID_AUTH_SCHEME: ${{ inputs.open-id-auth-scheme }} + OPEN_ID_TYPE: ${{ inputs.open-id-type }} + OPEN_ID_SERVER: ${{ inputs.open-id-server }} + OPEN_ID_CLIENT: ${{ inputs.open-id-client }} + OPEN_ID_REDIRECT_URI: ${{ inputs.open-id-redirect-uri }} + OPEN_ID_DISCOVERY_URI: ${{ inputs.open-id-discovery-uri }} + OPEN_ID_AUTHORIZATION_URL: ${{ inputs.open-id-authorization-url }} + OPEN_ID_TOKEN_URL: ${{ inputs.open-id-token-url }} + OPEN_ID_BUTTON_TEXT: ${{ inputs.open-id-button-text }} + run: ./gradlew assembleDhis2Debug diff --git a/.github/actions/run-form-tests/action.yml b/.github/actions/run-form-tests/action.yml new file mode 100644 index 0000000000..53d8d92660 --- /dev/null +++ b/.github/actions/run-form-tests/action.yml @@ -0,0 +1,31 @@ +name: Run form tests on BrowserStack +description: >- + Locates the built form androidTest APK and runs the BrowserStack form suite. + Assumes the form APK already exists under form/build/outputs. + +inputs: + browserstack-username: + description: BrowserStack username. + required: true + browserstack-password: + description: BrowserStack access key. + required: true + build-tag: + description: BrowserStack build tag. + required: true + +runs: + using: composite + steps: + - name: Run form tests + shell: bash + env: + BROWSERSTACK_USR: ${{ inputs.browserstack-username }} + BROWSERSTACK_PSW: ${{ inputs.browserstack-password }} + buildTag: ${{ inputs.build-tag }} + run: | + form_apk=$(find form/build/outputs -iname "*.apk" | sed -n 1p) + export form_apk_path="${GITHUB_WORKSPACE}/${form_apk}" + cd scripts + chmod +x browserstackJenkinsForm.sh + ./browserstackJenkinsForm.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e60ab26b7..977ac8962d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,16 +124,11 @@ jobs: path: . - name: Run Form Tests - env: - BROWSERSTACK_USR: ${{ secrets.BROWSERSTACK_USERNAME }} - BROWSERSTACK_PSW: ${{ secrets.BROWSERSTACK_PASSWORD }} - buildTag: ${{ github.head_ref || github.ref_name }} - form - run: | - form_apk=$(find form/build/outputs -iname "*.apk" | sed -n 1p) - export form_apk_path="${GITHUB_WORKSPACE}/${form_apk}" - cd scripts - chmod +x browserstackJenkinsForm.sh - ./browserstackJenkinsForm.sh + uses: ./.github/actions/run-form-tests + with: + browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} + browserstack-password: ${{ secrets.BROWSERSTACK_PASSWORD }} + build-tag: ${{ github.head_ref || github.ref_name }} - form run-ui-portrait: runs-on: ubuntu-latest diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index dd071ced06..8a2d01bd54 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -42,32 +42,25 @@ jobs: - name: Change wrapper permissions run: chmod +x ./gradlew - - name: Decode Keystore - id: decode_keystore - uses: timheuer/base64-to-file@dfa5a5dd38803cb4f5c3a081eeb28e8362d43e9c - with: - fileName: 'debug.keystore' - encodedString: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} - # Create APK Debug - name: Build apk debug project (APK) - ${{ env.main_project_module }} module - run: ./gradlew assembleDhis2Debug - env: - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - DEBUG_KEYSTORE_ALIAS: ${{ secrets.DEBUG_KEYSTORE_ALIAS }} - DEBUG_KEY_PASS: ${{ secrets.DEBUG_KEY_PASS }} - DEBUG_KEYSTORE_PASSWORD: ${{ secrets.DEBUG_KEYSTORE_PASSWORD }} - DEBUG_KEYSTORE_PATH: ${{ steps.decode_keystore.outputs.filePath }} - OPEN_ID_AUTH_SCHEME: ${{ secrets.OPEN_ID_AUTH_SCHEME }} - OPEN_ID_TYPE: ${{ secrets.OPEN_ID_TYPE }} - OPEN_ID_SERVER: ${{ secrets.OPEN_ID_SERVER }} - OPEN_ID_CLIENT: ${{ secrets.OPEN_ID_CLIENT }} - OPEN_ID_REDIRECT_URI: ${{ secrets.OPEN_ID_REDIRECT_URI }} - OPEN_ID_DISCOVERY_URI: ${{ secrets.OPEN_ID_DISCOVERY_URI }} - OPEN_ID_AUTHORIZATION_URL: ${{ secrets.OPEN_ID_AUTHORIZATION_URL }} - OPEN_ID_TOKEN_URL: ${{ secrets.OPEN_ID_TOKEN_URL }} - OPEN_ID_BUTTON_TEXT: ${{ secrets.OPEN_ID_BUTTON_TEXT }} - + uses: ./.github/actions/build-signed-apk + with: + keystore-base64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} + keystore-alias: ${{ secrets.DEBUG_KEYSTORE_ALIAS }} + key-pass: ${{ secrets.DEBUG_KEY_PASS }} + keystore-password: ${{ secrets.DEBUG_KEYSTORE_PASSWORD }} + sentry-dsn: ${{ secrets.SENTRY_DSN }} + open-id-auth-scheme: ${{ secrets.OPEN_ID_AUTH_SCHEME }} + open-id-type: ${{ secrets.OPEN_ID_TYPE }} + open-id-server: ${{ secrets.OPEN_ID_SERVER }} + open-id-client: ${{ secrets.OPEN_ID_CLIENT }} + open-id-redirect-uri: ${{ secrets.OPEN_ID_REDIRECT_URI }} + open-id-discovery-uri: ${{ secrets.OPEN_ID_DISCOVERY_URI }} + open-id-authorization-url: ${{ secrets.OPEN_ID_AUTHORIZATION_URL }} + open-id-token-url: ${{ secrets.OPEN_ID_TOKEN_URL }} + open-id-button-text: ${{ secrets.OPEN_ID_BUTTON_TEXT }} + - name: Locate built APK id: apk-path run: | diff --git a/.github/workflows/retest-fork-pr.yml b/.github/workflows/retest-fork-pr.yml index 8c24b388f0..bd55aa6637 100644 --- a/.github/workflows/retest-fork-pr.yml +++ b/.github/workflows/retest-fork-pr.yml @@ -154,34 +154,26 @@ jobs: ./scripts/sonarqube.sh # ---- deployment_job (signed debug APK build) ---- - - name: Decode keystore - id: decode_keystore - if: ${{ always() }} - uses: timheuer/base64-to-file@dfa5a5dd38803cb4f5c3a081eeb28e8362d43e9c - with: - fileName: 'debug.keystore' - encodedString: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} - - name: Build signed debug APK id: deployment if: ${{ always() }} continue-on-error: true - env: - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - DEBUG_KEYSTORE_ALIAS: ${{ secrets.DEBUG_KEYSTORE_ALIAS }} - DEBUG_KEY_PASS: ${{ secrets.DEBUG_KEY_PASS }} - DEBUG_KEYSTORE_PASSWORD: ${{ secrets.DEBUG_KEYSTORE_PASSWORD }} - DEBUG_KEYSTORE_PATH: ${{ steps.decode_keystore.outputs.filePath }} - OPEN_ID_AUTH_SCHEME: ${{ secrets.OPEN_ID_AUTH_SCHEME }} - OPEN_ID_TYPE: ${{ secrets.OPEN_ID_TYPE }} - OPEN_ID_SERVER: ${{ secrets.OPEN_ID_SERVER }} - OPEN_ID_CLIENT: ${{ secrets.OPEN_ID_CLIENT }} - OPEN_ID_REDIRECT_URI: ${{ secrets.OPEN_ID_REDIRECT_URI }} - OPEN_ID_DISCOVERY_URI: ${{ secrets.OPEN_ID_DISCOVERY_URI }} - OPEN_ID_AUTHORIZATION_URL: ${{ secrets.OPEN_ID_AUTHORIZATION_URL }} - OPEN_ID_TOKEN_URL: ${{ secrets.OPEN_ID_TOKEN_URL }} - OPEN_ID_BUTTON_TEXT: ${{ secrets.OPEN_ID_BUTTON_TEXT }} - run: ./gradlew assembleDhis2Debug + uses: ./.github/actions/build-signed-apk + with: + keystore-base64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }} + keystore-alias: ${{ secrets.DEBUG_KEYSTORE_ALIAS }} + key-pass: ${{ secrets.DEBUG_KEY_PASS }} + keystore-password: ${{ secrets.DEBUG_KEYSTORE_PASSWORD }} + sentry-dsn: ${{ secrets.SENTRY_DSN }} + open-id-auth-scheme: ${{ secrets.OPEN_ID_AUTH_SCHEME }} + open-id-type: ${{ secrets.OPEN_ID_TYPE }} + open-id-server: ${{ secrets.OPEN_ID_SERVER }} + open-id-client: ${{ secrets.OPEN_ID_CLIENT }} + open-id-redirect-uri: ${{ secrets.OPEN_ID_REDIRECT_URI }} + open-id-discovery-uri: ${{ secrets.OPEN_ID_DISCOVERY_URI }} + open-id-authorization-url: ${{ secrets.OPEN_ID_AUTHORIZATION_URL }} + open-id-token-url: ${{ secrets.OPEN_ID_TOKEN_URL }} + open-id-button-text: ${{ secrets.OPEN_ID_BUTTON_TEXT }} # ---- run-form-tests (BrowserStack) ---- - name: Build form test APK @@ -194,16 +186,11 @@ jobs: id: form_tests if: ${{ always() && steps.build_form_apk.outcome == 'success' }} continue-on-error: true - env: - BROWSERSTACK_USR: ${{ secrets.BROWSERSTACK_USERNAME }} - BROWSERSTACK_PSW: ${{ secrets.BROWSERSTACK_PASSWORD }} - buildTag: ${{ needs.authorize.outputs.head_ref }} - form (retest) - run: | - form_apk=$(find form/build/outputs -iname "*.apk" | sed -n 1p) - export form_apk_path="${GITHUB_WORKSPACE}/${form_apk}" - cd scripts - chmod +x browserstackJenkinsForm.sh - ./browserstackJenkinsForm.sh + uses: ./.github/actions/run-form-tests + with: + browserstack-username: ${{ secrets.BROWSERSTACK_USERNAME }} + browserstack-password: ${{ secrets.BROWSERSTACK_PASSWORD }} + build-tag: ${{ needs.authorize.outputs.head_ref }} - form (retest) # ---- Conclude check runs on the PR head SHA ---- - name: Conclude check runs From 269f4a8c1f1079dff3d4395b0c79e859ae4c6211 Mon Sep 17 00:00:00 2001 From: andresmr Date: Tue, 30 Jun 2026 13:18:59 +0200 Subject: [PATCH 3/4] ci: re-run CI on PR title/body edits Add the `edited` activity type to the pull_request trigger so changes to the PR title or body (e.g. adding [skip size] / [skip ci] markers) take effect without needing a new push or a close/reopen. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 977ac8962d..6403ad0554 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,7 @@ name: CI on: pull_request: + types: [opened, synchronize, reopened, edited] push: branches: - main From cb659f2c61f4f596502374231b6f889e74757b95 Mon Sep 17 00:00:00 2001 From: andresmr Date: Tue, 30 Jun 2026 14:46:27 +0200 Subject: [PATCH 4/4] ci: scope retest workflow permissions to job level Move the write permissions (checks, issues) from workflow level to each job so they follow least privilege (resolves SonarCloud warnings): - authorize: issues:write (reaction) + pull-requests:read (read PR head) - retest: checks:write + contents:read + issues:write The authorize job no longer inherits checks:write, which it never used. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/retest-fork-pr.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/retest-fork-pr.yml b/.github/workflows/retest-fork-pr.yml index bd55aa6637..735484fb1e 100644 --- a/.github/workflows/retest-fork-pr.yml +++ b/.github/workflows/retest-fork-pr.yml @@ -17,12 +17,6 @@ on: issue_comment: types: [created] -permissions: - checks: write # create/update check runs on the PR head SHA - contents: read - issues: write # add reactions on the PR conversation (issue) comment - pull-requests: read # read PR head SHA/ref - concurrency: group: retest-${{ github.event.issue.number }} cancel-in-progress: true @@ -32,6 +26,9 @@ jobs: # Only react to "/retest" comments on pull requests. if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/retest') }} runs-on: ubuntu-latest + permissions: + issues: write # add a reaction on the PR conversation (issue) comment + pull-requests: read # read PR head SHA/ref outputs: authorized: ${{ steps.check.outputs.authorized }} head_sha: ${{ steps.check.outputs.head_sha }} @@ -91,6 +88,10 @@ jobs: needs: authorize if: ${{ needs.authorize.outputs.authorized == 'true' }} runs-on: ubuntu-latest + permissions: + checks: write # create/update check runs on the PR head SHA + contents: read # checkout the PR head + issues: write # add the result reaction on the comment env: # Required-check names — MUST match the ruleset's required status checks exactly. CHECK_CODE_QUALITY: code-quality