From ccc05f7fdfdb8630fb8a57ae05827dbed9fbcf44 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:15:53 +0100 Subject: [PATCH 1/3] Update actions readmes --- .github/actions/bump-version/README.md | 4 ++-- .github/actions/setup-gradle/README.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/bump-version/README.md b/.github/actions/bump-version/README.md index 4a3ebce..ffe5209 100644 --- a/.github/actions/bump-version/README.md +++ b/.github/actions/bump-version/README.md @@ -25,7 +25,7 @@ The action reads the version from a file, bumps it, and writes it back: ```yaml - name: Bump version id: bump - uses: ./.github/actions/bump-version + uses: GetStream/stream-build-conventions-android/.github/actions/bump-version@main with: bump: patch # file defaults to gradle.properties @@ -41,7 +41,7 @@ For non-standard file paths or version keys: ```yaml - name: Bump version id: bump - uses: ./.github/actions/bump-version + uses: GetStream/stream-build-conventions-android/.github/actions/bump-version@main with: bump: minor file: custom/path/version.properties diff --git a/.github/actions/setup-gradle/README.md b/.github/actions/setup-gradle/README.md index d6f7f64..3fbda59 100644 --- a/.github/actions/setup-gradle/README.md +++ b/.github/actions/setup-gradle/README.md @@ -17,13 +17,13 @@ A reusable composite action that sets up Java and Gradle. uses: actions/checkout@v4 - name: Setup Gradle - uses: ./.github/actions/setup-gradle + uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main ``` ### CI Workflow (write cache for main/develop) ```yaml - name: Setup Gradle - uses: ./.github/actions/setup-gradle + uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main with: cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} ``` @@ -31,7 +31,7 @@ A reusable composite action that sets up Java and Gradle. ### Release Workflow (write cache) ```yaml - name: Setup Gradle - uses: ./.github/actions/setup-gradle + uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main with: cache-read-only: false ``` From 90cbdda16929925abeb231094bb126b8c22d4913 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:16:15 +0100 Subject: [PATCH 2/3] Use new setup-gradle action coordinate --- .github/actions/setup-gradle/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-gradle/action.yml b/.github/actions/setup-gradle/action.yml index 3473a62..864bac8 100644 --- a/.github/actions/setup-gradle/action.yml +++ b/.github/actions/setup-gradle/action.yml @@ -17,6 +17,6 @@ runs: distribution: 'temurin' - name: Setup Gradle - uses: gradle/gradle-build-action@v3 + uses: gradle/actions/setup-gradle@v3 with: cache-read-only: ${{ inputs.cache-read-only }} From 5a570f1247d209b423884408d48a046f92d600d4 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:18:04 +0100 Subject: [PATCH 3/3] Move shared workflows to the conventions repo --- .github/workflows/pr-clean-stale.yaml | 30 +++++ .github/workflows/pr-quality.yml | 165 +++++++++++++++++++++++++ .github/workflows/sdk-size-checks.yml | 144 +++++++++++++++++++++ .github/workflows/sdk-size-updates.yml | 113 +++++++++++++++++ 4 files changed, 452 insertions(+) create mode 100644 .github/workflows/pr-clean-stale.yaml create mode 100644 .github/workflows/pr-quality.yml create mode 100644 .github/workflows/sdk-size-checks.yml create mode 100644 .github/workflows/sdk-size-updates.yml diff --git a/.github/workflows/pr-clean-stale.yaml b/.github/workflows/pr-clean-stale.yaml new file mode 100644 index 0000000..0cb9017 --- /dev/null +++ b/.github/workflows/pr-clean-stale.yaml @@ -0,0 +1,30 @@ +name: Close stale PRs + +on: + workflow_call: + +permissions: + contents: write + pull-requests: write + issues: write + +concurrency: + group: close-stale-prs + cancel-in-progress: false + +jobs: + stale: + runs-on: ubuntu-latest + + steps: + - name: "Mark and close stale PRs" + uses: actions/stale@v9 + with: + days-before-pr-stale: 14 + days-before-pr-close: 7 + stale-pr-message: "This pull request has been automatically marked as stale because it has been inactive for 14 days. It will be closed in 7 days if no further activity occurs." + close-pr-message: "This pull request was closed because it has been stalled for 7 days with no activity. Please reopen if you still intend to submit this change." + exempt-pr-labels: 'pr:keep-open' + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: -1 # skip marking issues as stale + days-before-close: -1 # skip closing issues diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml new file mode 100644 index 0000000..5a0eab5 --- /dev/null +++ b/.github/workflows/pr-quality.yml @@ -0,0 +1,165 @@ +name: PR checklist + +on: + workflow_call: + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + pr-checklist: + runs-on: ubuntu-latest + + steps: + - name: Validate PR quality + id: validate + env: + TITLE: ${{ github.event.pull_request.title }} + BODY: ${{ github.event.pull_request.body }} + LABELS_CSV: ${{ join(github.event.pull_request.labels.*.name, ',') }} + run: | + set -euo pipefail + + failures="" + + # ---- Settings ---- + MIN_WORDS=5 + MAX_WORDS=18 + TITLE_BYPASS_LABEL="pr:ignore-for-release" + + has_label () { + case ",${LABELS_CSV}," in + *,"$1",*) return 0 ;; + *) return 1 ;; + esac + } + + has_any_pr_label () { + IFS=',' read -ra LBL <<< "${LABELS_CSV}" + for l in "${LBL[@]}"; do + l="$(echo "$l" | xargs)" + [[ $l == pr:* ]] && return 0 + done + return 1 + } + + # --- 1) Title check + if ! has_label "$TITLE_BYPASS_LABEL"; then + title_words=$(echo "$TITLE" | tr -s '[:space:]' ' ' | sed -e 's/^ *//' -e 's/ *$//' | wc -w | xargs) + if [ -z "$title_words" ]; then title_words=0; fi + if [ "$title_words" -lt "$MIN_WORDS" ] || [ "$title_words" -gt "$MAX_WORDS" ]; then + failures="${failures}\n- **Title** should be ${MIN_WORDS}–${MAX_WORDS} words for release notes. Current: ${title_words} word(s). (Add \`${TITLE_BYPASS_LABEL}\` to bypass.)" + fi + fi + + # --- 2) Has pr:* label + if ! has_any_pr_label; then + failures="${failures}\n- Missing required label: at least one label starting with \`pr:\`." + fi + + # --- 3) Sections non-empty + section_nonempty () { + local hdr="$1" + local section + section="$(printf "%s" "$BODY" | awk -v h="^###[[:space:]]*$hdr[[:space:]]*$" ' + BEGIN { insec=0 } + $0 ~ h { insec=1; next } + insec && $0 ~ /^##[[:space:]]/ { insec=0 } + insec { print } + ')" + section="$(printf "%s" "$section" \ + | sed -E 's///g' \ + | sed -E 's/^[[:space:]]+|[[:space:]]+$//g' \ + | sed '/^[[:space:]]*$/d')" + [ -n "$section" ] + } + + for hdr in Goal Implementation Testing; do + if ! section_nonempty "$hdr"; then + failures="${failures}\n- Section **${hdr}** is missing or empty." + fi + done + + if [ -n "$failures" ]; then + echo "has_failures=true" >> "$GITHUB_OUTPUT" + { + echo 'failures<> "$GITHUB_OUTPUT" + exit 1 + else + echo "has_failures=false" >> "$GITHUB_OUTPUT" + fi + + # Compute the latest sticky comment id (by our anchor). If none, output is empty. + - name: Compute sticky comment id + if: always() + id: sticky_comment + uses: actions/github-script@v7 + with: + script: | + const anchor = ""; // must match the body below + const { owner, repo } = context.repo; + const issue_number = context.payload.pull_request.number; + + // Get up to 100 comments; paginate to be safe. + const comments = await github.paginate( + github.rest.issues.listComments, + { owner, repo, issue_number, per_page: 100 } + ); + + const matches = comments.filter(c => (c.body || "").includes(anchor)); + if (matches.length === 0) { + core.setOutput("comment_id", ""); + return; + } + // Pick the most recently updated one + matches.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)); + core.setOutput("comment_id", String(matches[0].id)); + + - name: Sticky comment id + if: always() + run: echo "sticky comment-id=${{ steps.sticky_comment.outputs.comment_id }}" + + # Update/create the sticky comment on failure + - name: Create or update failure comment + if: failure() + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + comment-id: ${{ steps.sticky_comment.outputs.comment_id }} # empty => creates new + edit-mode: replace + body: | + + ### PR checklist ❌ + + The following issues were detected: + + ${{ steps.validate.outputs.failures }} + + **What we check** + 1. Title is concise (5–18 words) unless labeled `pr:ignore-for-release`. + 2. At least one `pr:` label exists (e.g., `pr:bug`, `pr:new-feature`). + 3. Sections `### Goal`, `### Implementation`, and `### Testing` contain content. + + # Flip the same sticky comment to ✅ on success + - name: Create or update success comment + if: success() + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + comment-id: ${{ steps.sticky_comment.outputs.comment_id }} # updates if found + edit-mode: replace + body: | + + ### PR checklist ✅ + + All required conditions are satisfied: + - Title length is OK (or ignored by label). + - At least one `pr:` label exists. + - Sections `### Goal`, `### Implementation`, and `### Testing` are filled. + + 🎉 Great job! This PR is ready for review. diff --git a/.github/workflows/sdk-size-checks.yml b/.github/workflows/sdk-size-checks.yml new file mode 100644 index 0000000..ef60e17 --- /dev/null +++ b/.github/workflows/sdk-size-checks.yml @@ -0,0 +1,144 @@ +name: SDK size checks + +on: + workflow_call: + + inputs: + metrics-project: + required: true + type: string + modules: + required: true + type: string + + secrets: + BUILD_CACHE_AWS_REGION: + required: false + BUILD_CACHE_AWS_BUCKET: + required: false + BUILD_CACHE_AWS_ACCESS_KEY_ID: + required: false + BUILD_CACHE_AWS_SECRET_KEY: + required: false + +env: + METRICS_PROJECT: ${{ inputs.metrics-project }} + METRICS_FILE: "metrics/size.json" + MODULES: ${{ inputs.modules }} + VARIANTS: "debug release" + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + MAX_TOLERANCE: 500 + FINE_TOLERANCE: 250 + +jobs: + compare-sdk-sizes: + name: Compare SDK sizes + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Gradle + uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main + + - name: Assemble release for metrics + run: ./gradlew :metrics:$METRICS_PROJECT:assembleRelease + env: + BUILD_CACHE_AWS_REGION: ${{ secrets.BUILD_CACHE_AWS_REGION }} + BUILD_CACHE_AWS_BUCKET: ${{ secrets.BUILD_CACHE_AWS_BUCKET }} + BUILD_CACHE_AWS_ACCESS_KEY_ID: ${{ secrets.BUILD_CACHE_AWS_ACCESS_KEY_ID }} + BUILD_CACHE_AWS_SECRET_KEY: ${{ secrets.BUILD_CACHE_AWS_SECRET_KEY }} + + - name: Get current SDK sizes + run: | + # Reads current SDK sizes from the metrics file + # and define to a variable using a compact JSON format + # so it can be exported for the next job step + CURRENT_SDK_SIZES=$(jq -c .release $METRICS_FILE) + echo "CURRENT_SDK_SIZES=$CURRENT_SDK_SIZES" >> $GITHUB_ENV + + - name: Calculate PR branch SDK sizes + run: | + echo '{}' > pr_sdk_sizes.json + + # Calculate sizes from the .apk files and save them into a temporary JSON file + # so it can be exported for the next job step + for module in $MODULES; do + baselineFile="metrics/$METRICS_PROJECT/build/outputs/apk/$module-baseline/release/$METRICS_PROJECT-$module-baseline-release.apk" + streamFile="metrics/$METRICS_PROJECT/build/outputs/apk/$module-stream/release/$METRICS_PROJECT-$module-stream-release.apk" + + baselineSize=$(du -k "$baselineFile" | awk '{print $1}') + streamSize=$(du -k "$streamFile" | awk '{print $1}') + size=$((streamSize - baselineSize)) + + jq -c --arg sdk "$module" --arg size "$size" '. + {($sdk): ($size | tonumber)}' pr_sdk_sizes.json > temp.json && mv temp.json pr_sdk_sizes.json + done + + echo "PR_SDK_SIZES=$(cat pr_sdk_sizes.json)" >> $GITHUB_ENV + + - name: Post a comment or print size comparison + uses: actions/github-script@v7 + with: + script: | + const maxTolerance = process.env.MAX_TOLERANCE; + const fineTolerance = process.env.FINE_TOLERANCE; + const currentSdkSizes = process.env.CURRENT_SDK_SIZES ? JSON.parse(process.env.CURRENT_SDK_SIZES) : {}; + const prSdkSizes = JSON.parse(process.env.PR_SDK_SIZES); + const commentHeader = '## SDK Size Comparison 📏'; + + let commentBody = `${commentHeader}\n\n| SDK | Before | After | Difference | Status |\n|-|-|-|-|-|\n`; + + Object.keys(prSdkSizes).forEach(sdk => { + const currentSize = currentSdkSizes[sdk] || 0; + const prSize = prSdkSizes[sdk]; + const diff = prSize - currentSize; + const currentSizeInMb = (currentSize / 1024).toFixed(2); + const prSizeInMb = (prSize / 1024).toFixed(2); + const diffInMb = (diff / 1024).toFixed(2); + + let status = "🟢"; + if (diff < 0) { status = "🚀"; } + else if (diff >= maxTolerance) { status = "🔴"; } + else if (diff >= fineTolerance) { status = "🟡"; } + + commentBody += `| ${sdk} | ${currentSizeInMb} MB | ${prSizeInMb} MB | ${diffInMb} MB | ${status} |\n`; + }); + + const isFork = context.payload.pull_request.head.repo.fork; + + if (isFork) { + console.log("Pull Request is from a fork. Printing size comparison to the log instead of commenting."); + console.log("------------------------------------------------------------------------------------"); + console.log(commentBody); + console.log("------------------------------------------------------------------------------------"); + return; + } + + const issue_number = context.issue.number; + const { owner, repo } = context.repo; + + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + }); + + const existingComment = comments.find(c => c.body.includes(commentHeader)); + + if (existingComment) { + console.log(`Found existing comment with ID ${existingComment.id}. Updating it.`); + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existingComment.id, + body: commentBody, + }); + } else { + console.log("No existing comment found. Creating a new one."); + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: commentBody, + }); + } diff --git a/.github/workflows/sdk-size-updates.yml b/.github/workflows/sdk-size-updates.yml new file mode 100644 index 0000000..e045dea --- /dev/null +++ b/.github/workflows/sdk-size-updates.yml @@ -0,0 +1,113 @@ +name: SDK size updates + +on: + workflow_call: + + inputs: + metrics-project: + required: true + type: string + modules: + required: true + type: string + + secrets: + GITHUB_PAT: + required: true + BUILD_CACHE_AWS_REGION: + required: false + BUILD_CACHE_AWS_BUCKET: + required: false + BUILD_CACHE_AWS_ACCESS_KEY_ID: + required: false + BUILD_CACHE_AWS_SECRET_KEY: + required: false + +env: + METRICS_PROJECT: ${{ inputs.metrics-project }} + METRICS_FILE: "metrics/size.json" + MODULES: ${{ inputs.modules }} + VARIANTS: "debug release" + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + +jobs: + update-sdk-sizes: + name: Update SDK sizes + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup Gradle + uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main + + - name: Assemble release and debug for metrics + run: ./gradlew :metrics:$METRICS_PROJECT:assemble + env: + BUILD_CACHE_AWS_REGION: ${{ secrets.BUILD_CACHE_AWS_REGION }} + BUILD_CACHE_AWS_BUCKET: ${{ secrets.BUILD_CACHE_AWS_BUCKET }} + BUILD_CACHE_AWS_ACCESS_KEY_ID: ${{ secrets.BUILD_CACHE_AWS_ACCESS_KEY_ID }} + BUILD_CACHE_AWS_SECRET_KEY: ${{ secrets.BUILD_CACHE_AWS_SECRET_KEY }} + + - name: Update size metrics + run: | + # Create temporary JSON file + echo '{}' > metrics.json + + # Calculate sizes + for module in $MODULES; do + for variant in $VARIANTS; do + baselineFile="metrics/$METRICS_PROJECT/build/outputs/apk/$module-baseline/$variant/$METRICS_PROJECT-$module-baseline-$variant.apk" + streamFile="metrics/$METRICS_PROJECT/build/outputs/apk/$module-stream/$variant/$METRICS_PROJECT-$module-stream-$variant.apk" + + # Ensure files exist + if [[ -f "$baselineFile" && -f "$streamFile" ]]; then + baselineSize=$(du -k "$baselineFile" | awk '{print $1}') + streamSize=$(du -k "$streamFile" | awk '{print $1}') + size=$((streamSize - baselineSize)) + else + echo "Warning: $baselineFile or $streamFile not found. Setting size to 0." + size=0 + fi + + # Update JSON + jq --arg module "$module" --arg variant "$variant" --argjson size "$size" \ + ".\"$variant\".\"$module\" = $size" metrics.json > temp.json && mv temp.json metrics.json + done + done + + # Validate Generated JSON + jq . metrics.json + + # Move temporary JSON file to the final file + mv metrics.json $METRICS_FILE + + - name: Update size badges + run: | + for module in $MODULES; do + size=$(jq --arg module "$module" ".release.\"$module\"" $METRICS_FILE) + sizeInMb=$(echo "scale=2; $size / 1024" | bc) + badgeUrl="https://img.shields.io/badge/${module//-/--}-$sizeInMb%20MB-lightgreen" + sed -i "s|!\[$module\](.*)|![$module](${badgeUrl})|" README.md + done + + - name: Commit changes + run: | + git remote set-url origin https://x-access-token:${{ secrets.GITHUB_PAT }}@github.com/${{ github.repository }}.git + + git fetch origin $BRANCH_NAME + git checkout $BRANCH_NAME + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git add $METRICS_FILE README.md + git commit -m "[skip ci] Update SDK sizes" || echo "No changes to commit" + + - name: Push changes + uses: ad-m/github-push-action@1.0.0 + with: + github_token: ${{ secrets.GITHUB_PAT }} + branch: ${{ env.BRANCH_NAME }}