Skip to content

Commit cfa5725

Browse files
authored
Move shared workflows to the conventions repo (#11)
* Update actions readmes * Use new setup-gradle action coordinate * Move shared workflows to the conventions repo
1 parent b92e15c commit cfa5725

File tree

7 files changed

+458
-6
lines changed

7 files changed

+458
-6
lines changed

.github/actions/bump-version/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The action reads the version from a file, bumps it, and writes it back:
2525
```yaml
2626
- name: Bump version
2727
id: bump
28-
uses: ./.github/actions/bump-version
28+
uses: GetStream/stream-build-conventions-android/.github/actions/bump-version@main
2929
with:
3030
bump: patch
3131
# file defaults to gradle.properties
@@ -41,7 +41,7 @@ For non-standard file paths or version keys:
4141
```yaml
4242
- name: Bump version
4343
id: bump
44-
uses: ./.github/actions/bump-version
44+
uses: GetStream/stream-build-conventions-android/.github/actions/bump-version@main
4545
with:
4646
bump: minor
4747
file: custom/path/version.properties

.github/actions/setup-gradle/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@ A reusable composite action that sets up Java and Gradle.
1717
uses: actions/checkout@v4
1818

1919
- name: Setup Gradle
20-
uses: ./.github/actions/setup-gradle
20+
uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main
2121
```
2222
2323
### CI Workflow (write cache for main/develop)
2424
```yaml
2525
- name: Setup Gradle
26-
uses: ./.github/actions/setup-gradle
26+
uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main
2727
with:
2828
cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
2929
```
3030
3131
### Release Workflow (write cache)
3232
```yaml
3333
- name: Setup Gradle
34-
uses: ./.github/actions/setup-gradle
34+
uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main
3535
with:
3636
cache-read-only: false
3737
```

.github/actions/setup-gradle/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ runs:
1717
distribution: 'temurin'
1818

1919
- name: Setup Gradle
20-
uses: gradle/gradle-build-action@v3
20+
uses: gradle/actions/setup-gradle@v3
2121
with:
2222
cache-read-only: ${{ inputs.cache-read-only }}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Close stale PRs
2+
3+
on:
4+
workflow_call:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
issues: write
10+
11+
concurrency:
12+
group: close-stale-prs
13+
cancel-in-progress: false
14+
15+
jobs:
16+
stale:
17+
runs-on: ubuntu-latest
18+
19+
steps:
20+
- name: "Mark and close stale PRs"
21+
uses: actions/stale@v9
22+
with:
23+
days-before-pr-stale: 14
24+
days-before-pr-close: 7
25+
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."
26+
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."
27+
exempt-pr-labels: 'pr:keep-open'
28+
repo-token: ${{ secrets.GITHUB_TOKEN }}
29+
days-before-stale: -1 # skip marking issues as stale
30+
days-before-close: -1 # skip closing issues

.github/workflows/pr-quality.yml

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
name: PR checklist
2+
3+
on:
4+
workflow_call:
5+
6+
permissions:
7+
contents: read
8+
pull-requests: write
9+
issues: write
10+
11+
jobs:
12+
pr-checklist:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Validate PR quality
17+
id: validate
18+
env:
19+
TITLE: ${{ github.event.pull_request.title }}
20+
BODY: ${{ github.event.pull_request.body }}
21+
LABELS_CSV: ${{ join(github.event.pull_request.labels.*.name, ',') }}
22+
run: |
23+
set -euo pipefail
24+
25+
failures=""
26+
27+
# ---- Settings ----
28+
MIN_WORDS=5
29+
MAX_WORDS=18
30+
TITLE_BYPASS_LABEL="pr:ignore-for-release"
31+
32+
has_label () {
33+
case ",${LABELS_CSV}," in
34+
*,"$1",*) return 0 ;;
35+
*) return 1 ;;
36+
esac
37+
}
38+
39+
has_any_pr_label () {
40+
IFS=',' read -ra LBL <<< "${LABELS_CSV}"
41+
for l in "${LBL[@]}"; do
42+
l="$(echo "$l" | xargs)"
43+
[[ $l == pr:* ]] && return 0
44+
done
45+
return 1
46+
}
47+
48+
# --- 1) Title check
49+
if ! has_label "$TITLE_BYPASS_LABEL"; then
50+
title_words=$(echo "$TITLE" | tr -s '[:space:]' ' ' | sed -e 's/^ *//' -e 's/ *$//' | wc -w | xargs)
51+
if [ -z "$title_words" ]; then title_words=0; fi
52+
if [ "$title_words" -lt "$MIN_WORDS" ] || [ "$title_words" -gt "$MAX_WORDS" ]; then
53+
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.)"
54+
fi
55+
fi
56+
57+
# --- 2) Has pr:* label
58+
if ! has_any_pr_label; then
59+
failures="${failures}\n- Missing required label: at least one label starting with \`pr:\`."
60+
fi
61+
62+
# --- 3) Sections non-empty
63+
section_nonempty () {
64+
local hdr="$1"
65+
local section
66+
section="$(printf "%s" "$BODY" | awk -v h="^###[[:space:]]*$hdr[[:space:]]*$" '
67+
BEGIN { insec=0 }
68+
$0 ~ h { insec=1; next }
69+
insec && $0 ~ /^##[[:space:]]/ { insec=0 }
70+
insec { print }
71+
')"
72+
section="$(printf "%s" "$section" \
73+
| sed -E 's/<!--(.|\n)*?-->//g' \
74+
| sed -E 's/^[[:space:]]+|[[:space:]]+$//g' \
75+
| sed '/^[[:space:]]*$/d')"
76+
[ -n "$section" ]
77+
}
78+
79+
for hdr in Goal Implementation Testing; do
80+
if ! section_nonempty "$hdr"; then
81+
failures="${failures}\n- Section **${hdr}** is missing or empty."
82+
fi
83+
done
84+
85+
if [ -n "$failures" ]; then
86+
echo "has_failures=true" >> "$GITHUB_OUTPUT"
87+
{
88+
echo 'failures<<EOF'
89+
printf "%b\n" "$failures"
90+
echo 'EOF'
91+
} >> "$GITHUB_OUTPUT"
92+
exit 1
93+
else
94+
echo "has_failures=false" >> "$GITHUB_OUTPUT"
95+
fi
96+
97+
# Compute the latest sticky comment id (by our anchor). If none, output is empty.
98+
- name: Compute sticky comment id
99+
if: always()
100+
id: sticky_comment
101+
uses: actions/github-script@v7
102+
with:
103+
script: |
104+
const anchor = "<!-- pr-quality-anchor -->"; // must match the body below
105+
const { owner, repo } = context.repo;
106+
const issue_number = context.payload.pull_request.number;
107+
108+
// Get up to 100 comments; paginate to be safe.
109+
const comments = await github.paginate(
110+
github.rest.issues.listComments,
111+
{ owner, repo, issue_number, per_page: 100 }
112+
);
113+
114+
const matches = comments.filter(c => (c.body || "").includes(anchor));
115+
if (matches.length === 0) {
116+
core.setOutput("comment_id", "");
117+
return;
118+
}
119+
// Pick the most recently updated one
120+
matches.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
121+
core.setOutput("comment_id", String(matches[0].id));
122+
123+
- name: Sticky comment id
124+
if: always()
125+
run: echo "sticky comment-id=${{ steps.sticky_comment.outputs.comment_id }}"
126+
127+
# Update/create the sticky comment on failure
128+
- name: Create or update failure comment
129+
if: failure()
130+
uses: peter-evans/create-or-update-comment@v4
131+
with:
132+
issue-number: ${{ github.event.pull_request.number }}
133+
comment-id: ${{ steps.sticky_comment.outputs.comment_id }} # empty => creates new
134+
edit-mode: replace
135+
body: |
136+
<!-- pr-quality-anchor -->
137+
### PR checklist ❌
138+
139+
The following issues were detected:
140+
141+
${{ steps.validate.outputs.failures }}
142+
143+
**What we check**
144+
1. Title is concise (5–18 words) unless labeled `pr:ignore-for-release`.
145+
2. At least one `pr:` label exists (e.g., `pr:bug`, `pr:new-feature`).
146+
3. Sections `### Goal`, `### Implementation`, and `### Testing` contain content.
147+
148+
# Flip the same sticky comment to ✅ on success
149+
- name: Create or update success comment
150+
if: success()
151+
uses: peter-evans/create-or-update-comment@v4
152+
with:
153+
issue-number: ${{ github.event.pull_request.number }}
154+
comment-id: ${{ steps.sticky_comment.outputs.comment_id }} # updates if found
155+
edit-mode: replace
156+
body: |
157+
<!-- pr-quality-anchor -->
158+
### PR checklist ✅
159+
160+
All required conditions are satisfied:
161+
- Title length is OK (or ignored by label).
162+
- At least one `pr:` label exists.
163+
- Sections `### Goal`, `### Implementation`, and `### Testing` are filled.
164+
165+
🎉 Great job! This PR is ready for review.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
name: SDK size checks
2+
3+
on:
4+
workflow_call:
5+
6+
inputs:
7+
metrics-project:
8+
required: true
9+
type: string
10+
modules:
11+
required: true
12+
type: string
13+
14+
secrets:
15+
BUILD_CACHE_AWS_REGION:
16+
required: false
17+
BUILD_CACHE_AWS_BUCKET:
18+
required: false
19+
BUILD_CACHE_AWS_ACCESS_KEY_ID:
20+
required: false
21+
BUILD_CACHE_AWS_SECRET_KEY:
22+
required: false
23+
24+
env:
25+
METRICS_PROJECT: ${{ inputs.metrics-project }}
26+
METRICS_FILE: "metrics/size.json"
27+
MODULES: ${{ inputs.modules }}
28+
VARIANTS: "debug release"
29+
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
30+
MAX_TOLERANCE: 500
31+
FINE_TOLERANCE: 250
32+
33+
jobs:
34+
compare-sdk-sizes:
35+
name: Compare SDK sizes
36+
runs-on: ubuntu-latest
37+
38+
steps:
39+
- name: Checkout code
40+
uses: actions/checkout@v4
41+
- name: Setup Gradle
42+
uses: GetStream/stream-build-conventions-android/.github/actions/setup-gradle@main
43+
44+
- name: Assemble release for metrics
45+
run: ./gradlew :metrics:$METRICS_PROJECT:assembleRelease
46+
env:
47+
BUILD_CACHE_AWS_REGION: ${{ secrets.BUILD_CACHE_AWS_REGION }}
48+
BUILD_CACHE_AWS_BUCKET: ${{ secrets.BUILD_CACHE_AWS_BUCKET }}
49+
BUILD_CACHE_AWS_ACCESS_KEY_ID: ${{ secrets.BUILD_CACHE_AWS_ACCESS_KEY_ID }}
50+
BUILD_CACHE_AWS_SECRET_KEY: ${{ secrets.BUILD_CACHE_AWS_SECRET_KEY }}
51+
52+
- name: Get current SDK sizes
53+
run: |
54+
# Reads current SDK sizes from the metrics file
55+
# and define to a variable using a compact JSON format
56+
# so it can be exported for the next job step
57+
CURRENT_SDK_SIZES=$(jq -c .release $METRICS_FILE)
58+
echo "CURRENT_SDK_SIZES=$CURRENT_SDK_SIZES" >> $GITHUB_ENV
59+
60+
- name: Calculate PR branch SDK sizes
61+
run: |
62+
echo '{}' > pr_sdk_sizes.json
63+
64+
# Calculate sizes from the .apk files and save them into a temporary JSON file
65+
# so it can be exported for the next job step
66+
for module in $MODULES; do
67+
baselineFile="metrics/$METRICS_PROJECT/build/outputs/apk/$module-baseline/release/$METRICS_PROJECT-$module-baseline-release.apk"
68+
streamFile="metrics/$METRICS_PROJECT/build/outputs/apk/$module-stream/release/$METRICS_PROJECT-$module-stream-release.apk"
69+
70+
baselineSize=$(du -k "$baselineFile" | awk '{print $1}')
71+
streamSize=$(du -k "$streamFile" | awk '{print $1}')
72+
size=$((streamSize - baselineSize))
73+
74+
jq -c --arg sdk "$module" --arg size "$size" '. + {($sdk): ($size | tonumber)}' pr_sdk_sizes.json > temp.json && mv temp.json pr_sdk_sizes.json
75+
done
76+
77+
echo "PR_SDK_SIZES=$(cat pr_sdk_sizes.json)" >> $GITHUB_ENV
78+
79+
- name: Post a comment or print size comparison
80+
uses: actions/github-script@v7
81+
with:
82+
script: |
83+
const maxTolerance = process.env.MAX_TOLERANCE;
84+
const fineTolerance = process.env.FINE_TOLERANCE;
85+
const currentSdkSizes = process.env.CURRENT_SDK_SIZES ? JSON.parse(process.env.CURRENT_SDK_SIZES) : {};
86+
const prSdkSizes = JSON.parse(process.env.PR_SDK_SIZES);
87+
const commentHeader = '## SDK Size Comparison 📏';
88+
89+
let commentBody = `${commentHeader}\n\n| SDK | Before | After | Difference | Status |\n|-|-|-|-|-|\n`;
90+
91+
Object.keys(prSdkSizes).forEach(sdk => {
92+
const currentSize = currentSdkSizes[sdk] || 0;
93+
const prSize = prSdkSizes[sdk];
94+
const diff = prSize - currentSize;
95+
const currentSizeInMb = (currentSize / 1024).toFixed(2);
96+
const prSizeInMb = (prSize / 1024).toFixed(2);
97+
const diffInMb = (diff / 1024).toFixed(2);
98+
99+
let status = "🟢";
100+
if (diff < 0) { status = "🚀"; }
101+
else if (diff >= maxTolerance) { status = "🔴"; }
102+
else if (diff >= fineTolerance) { status = "🟡"; }
103+
104+
commentBody += `| ${sdk} | ${currentSizeInMb} MB | ${prSizeInMb} MB | ${diffInMb} MB | ${status} |\n`;
105+
});
106+
107+
const isFork = context.payload.pull_request.head.repo.fork;
108+
109+
if (isFork) {
110+
console.log("Pull Request is from a fork. Printing size comparison to the log instead of commenting.");
111+
console.log("------------------------------------------------------------------------------------");
112+
console.log(commentBody);
113+
console.log("------------------------------------------------------------------------------------");
114+
return;
115+
}
116+
117+
const issue_number = context.issue.number;
118+
const { owner, repo } = context.repo;
119+
120+
const { data: comments } = await github.rest.issues.listComments({
121+
owner,
122+
repo,
123+
issue_number,
124+
});
125+
126+
const existingComment = comments.find(c => c.body.includes(commentHeader));
127+
128+
if (existingComment) {
129+
console.log(`Found existing comment with ID ${existingComment.id}. Updating it.`);
130+
await github.rest.issues.updateComment({
131+
owner,
132+
repo,
133+
comment_id: existingComment.id,
134+
body: commentBody,
135+
});
136+
} else {
137+
console.log("No existing comment found. Creating a new one.");
138+
await github.rest.issues.createComment({
139+
owner,
140+
repo,
141+
issue_number,
142+
body: commentBody,
143+
});
144+
}

0 commit comments

Comments
 (0)