Skip to content

Commit 10d4e19

Browse files
committed
CI: Add Docker image size testing and PR comment on large increases.
Add test-docker-image-sizes.bash to query compressed image sizes from GHCR and compare them against a baseline. Supports single tutorial, all tutorials, and history modes with text, markdown, and JSON output. Add test-docker-image-sizes.yml workflow triggered after builds to produce a job summary table, and pr-comment-docker-image-sizes.yml to warn on PRs when any image changes by more than 5%.
1 parent 128811b commit 10d4e19

File tree

3 files changed

+765
-0
lines changed

3 files changed

+765
-0
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: PR Comment on Docker Image Sizes
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
workflow_run_id:
7+
description: "Run ID of the test workflow to download artifacts from"
8+
required: true
9+
type: string
10+
11+
# Write permissions for commenting — this workflow never checks out or runs
12+
# untrusted PR code, keeping it separate from the test workflow that does.
13+
permissions:
14+
pull-requests: write
15+
actions: read
16+
17+
jobs:
18+
comment:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Download PR comment data
23+
uses: actions/download-artifact@v4
24+
with:
25+
name: pr-comment-data
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
run-id: ${{ inputs.workflow_run_id }}
28+
29+
- name: Post comment on PR
30+
uses: actions/github-script@v7
31+
with:
32+
script: |
33+
const fs = require('fs');
34+
35+
const prNumber = parseInt(fs.readFileSync('pr_number', 'utf8').trim());
36+
if (!prNumber || isNaN(prNumber)) {
37+
console.log('No valid PR number found, skipping comment.');
38+
return;
39+
}
40+
41+
const warningCount = parseInt(fs.readFileSync('warning_count', 'utf8').trim());
42+
const threshold = parseInt(fs.readFileSync('threshold', 'utf8').trim());
43+
const results = JSON.parse(fs.readFileSync('results.json', 'utf8'));
44+
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${{ inputs.workflow_run_id }}`;
45+
46+
const COMMENT_MARKER = '<!-- docker-image-size-check -->';
47+
48+
// Find existing bot comment
49+
const { data: comments } = await github.rest.issues.listComments({
50+
owner: context.repo.owner,
51+
repo: context.repo.repo,
52+
issue_number: prNumber,
53+
});
54+
const existing = comments.find(c =>
55+
c.user.type === 'Bot' && c.body.includes(COMMENT_MARKER)
56+
);
57+
58+
if (warningCount === 0) {
59+
// No warnings — resolve any existing comment
60+
if (existing) {
61+
const body = [
62+
COMMENT_MARKER,
63+
`## ✅ Docker Image Sizes OK`,
64+
``,
65+
`All tutorial images are within the **${threshold}%** size threshold compared to \`main\`.`,
66+
``,
67+
`[View full size report →](${runUrl})`,
68+
].join('\n');
69+
70+
await github.rest.issues.updateComment({
71+
owner: context.repo.owner,
72+
repo: context.repo.repo,
73+
comment_id: existing.id,
74+
body,
75+
});
76+
console.log(`Updated comment on PR #${prNumber} to resolved.`);
77+
} else {
78+
console.log('No size warnings and no existing comment. Nothing to do.');
79+
}
80+
return;
81+
}
82+
83+
// Build warning table (only tutorials exceeding threshold)
84+
const warnings = results.filter(r =>
85+
!r.error && r.baseline_exists && (r.diff_pct > threshold || r.diff_pct < -threshold)
86+
);
87+
88+
let warningRows = warnings.map(r =>
89+
`| ${r.tutorial} | ${r.current_size_human} | ${r.baseline_size_human} | **${r.change_human}** |`
90+
).join('\n');
91+
92+
// Build full table (all tutorials)
93+
let allRows = results.map(r => {
94+
if (r.error)
95+
return `| ${r.tutorial} | _(error)_ | — | — |`;
96+
if (!r.baseline_exists)
97+
return `| ${r.tutorial} | ${r.current_size_human} | _(not found)_ | — |`;
98+
const bold = (r.diff_pct > threshold || r.diff_pct < -threshold) ? '**' : '';
99+
return `| ${r.tutorial} | ${r.current_size_human} | ${r.baseline_size_human} | ${bold}${r.change_human}${bold} |`;
100+
}).join('\n');
101+
102+
const body = [
103+
COMMENT_MARKER,
104+
`## ⚠️ Docker Image Size Warning`,
105+
``,
106+
`**${warningCount}** tutorial image(s) changed by more than **${threshold}%** compared to \`main\`:`,
107+
``,
108+
`| Tutorial | Current | Main | Change |`,
109+
`|:---------|--------:|-----:|-------:|`,
110+
warningRows,
111+
``,
112+
`<details>`,
113+
`<summary>📊 Full size report for all tutorials</summary>`,
114+
``,
115+
`| Tutorial | Current | Main | Change |`,
116+
`|:---------|--------:|-----:|-------:|`,
117+
allRows,
118+
``,
119+
`</details>`,
120+
``,
121+
`> **Tip:** Review recent Dockerfile changes for unintended dependency additions.`,
122+
``,
123+
`[View full size report →](${runUrl})`,
124+
].join('\n');
125+
126+
if (existing) {
127+
await github.rest.issues.updateComment({
128+
owner: context.repo.owner,
129+
repo: context.repo.repo,
130+
comment_id: existing.id,
131+
body,
132+
});
133+
console.log(`Updated existing comment on PR #${prNumber}`);
134+
} else {
135+
await github.rest.issues.createComment({
136+
owner: context.repo.owner,
137+
repo: context.repo.repo,
138+
issue_number: prNumber,
139+
body,
140+
});
141+
console.log(`Created comment on PR #${prNumber}`);
142+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: Test Docker Image Sizes
2+
3+
on:
4+
workflow_run:
5+
workflows: ["Build and Push Brev Tutorial Docker Images"]
6+
types: [completed]
7+
workflow_dispatch:
8+
inputs:
9+
branch:
10+
description: "Branch name (for tag construction)"
11+
required: false
12+
type: string
13+
sha:
14+
description: "Git SHA (for tag construction)"
15+
required: false
16+
type: string
17+
18+
jobs:
19+
test-image-sizes:
20+
name: test-image-sizes
21+
runs-on: ubuntu-latest
22+
if: >
23+
github.event_name == 'workflow_dispatch' ||
24+
github.event.workflow_run.conclusion == 'success' ||
25+
github.event.workflow_run.conclusion == 'failure'
26+
permissions:
27+
contents: read
28+
packages: read
29+
actions: write
30+
defaults:
31+
run:
32+
working-directory: ${{ github.workspace }}
33+
steps:
34+
- name: Checkout repository
35+
uses: actions/checkout@v4
36+
with:
37+
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
38+
39+
- name: Set variables
40+
run: |
41+
if [ "${{ github.event_name }}" = "workflow_run" ]; then
42+
GIT_BRANCH_NAME="${{ github.event.workflow_run.head_branch }}"
43+
GIT_SHA="${{ github.event.workflow_run.head_sha }}"
44+
elif [ -n "${{ inputs.branch }}" ]; then
45+
GIT_BRANCH_NAME="${{ inputs.branch }}"
46+
GIT_SHA="${{ inputs.sha || github.sha }}"
47+
else
48+
GIT_BRANCH_NAME="${GITHUB_REF#refs/heads/}"
49+
GIT_SHA="${{ github.sha }}"
50+
fi
51+
52+
DOCKER_TAG_BRANCH=$(echo "${GIT_BRANCH_NAME}" | sed 's/[^a-zA-Z0-9._-]/-/g' | tr '[:upper:]' '[:lower:]')
53+
GIT_SHORT_SHA=${GIT_SHA:0:7}
54+
55+
# Extract PR number from pull-request/* branches
56+
PR_NUMBER=""
57+
if [[ "$GIT_BRANCH_NAME" =~ ^pull-request/([0-9]+)$ ]]; then
58+
PR_NUMBER="${BASH_REMATCH[1]}"
59+
fi
60+
61+
echo "GIT_BRANCH_NAME=${GIT_BRANCH_NAME}" >> $GITHUB_ENV
62+
echo "DOCKER_TAG_BRANCH=${DOCKER_TAG_BRANCH}" >> $GITHUB_ENV
63+
echo "GIT_SHA=${GIT_SHA}" >> $GITHUB_ENV
64+
echo "GIT_SHORT_SHA=${GIT_SHORT_SHA}" >> $GITHUB_ENV
65+
echo "OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV
66+
echo "PR_NUMBER=${PR_NUMBER}" >> $GITHUB_ENV
67+
68+
- name: Collect image sizes
69+
id: sizes
70+
run: |
71+
IMAGE_TAG="${DOCKER_TAG_BRANCH}-git-${GIT_SHORT_SHA}"
72+
THRESHOLD=5
73+
74+
RESULTS=$(brev/test-docker-image-sizes.bash compare \
75+
--owner "${OWNER}" \
76+
--tutorial all \
77+
--tag "${IMAGE_TAG}" \
78+
--baseline "main-latest" \
79+
--format json)
80+
81+
# Count tutorials exceeding the threshold
82+
WARNING_COUNT=$(echo "$RESULTS" | jq --argjson t "$THRESHOLD" \
83+
'[.[] | select(.diff_pct != null and ((.diff_pct > $t) or (.diff_pct < -$t)))] | length')
84+
ERRORS=$(echo "$RESULTS" | jq '[.[] | select(.error == true)] | length')
85+
86+
# --- Build job summary ---
87+
{
88+
echo "## 📦 Docker Image Size Report"
89+
echo ""
90+
echo "**Branch:** \`${GIT_BRANCH_NAME}\` (\`${GIT_SHORT_SHA}\`)"
91+
echo "**Baseline:** \`main-latest\`"
92+
echo ""
93+
echo "| Tutorial | Current | Baseline | Change | |"
94+
echo "|:---------|--------:|---------:|-------:|:---:|"
95+
96+
echo "$RESULTS" | jq -r --argjson t "$THRESHOLD" '.[] |
97+
if .error then
98+
"| " + .tutorial + " | _(error)_ | — | — | ❌ |"
99+
elif .baseline_exists == false then
100+
"| " + .tutorial + " | " + .current_size_human + " | _(not found)_ | — | ➖ |"
101+
else
102+
(if ((.diff_pct > $t) or (.diff_pct < (-1 * $t))) then "⚠️" else "✅" end) as $icon |
103+
"| " + .tutorial + " | " + .current_size_human + " | " + .baseline_size_human + " | " + .change_human + " | " + $icon + " |"
104+
end'
105+
106+
echo ""
107+
if [ "$WARNING_COUNT" -gt 0 ]; then
108+
echo "> ⚠️ **${WARNING_COUNT} image(s) changed by more than ${THRESHOLD}%.** Large increases may indicate unnecessary dependencies."
109+
elif [ "$ERRORS" -gt 0 ]; then
110+
echo "> ❌ **${ERRORS} image(s) could not be checked.** This is expected if the build failed for those tutorials."
111+
else
112+
echo "> ✅ All images are within the ${THRESHOLD}% size threshold."
113+
fi
114+
} >> $GITHUB_STEP_SUMMARY
115+
116+
# --- Save PR comment data ---
117+
mkdir -p pr-comment-data
118+
echo "${PR_NUMBER}" > pr-comment-data/pr_number
119+
echo "${WARNING_COUNT}" > pr-comment-data/warning_count
120+
echo "${THRESHOLD}" > pr-comment-data/threshold
121+
echo "$RESULTS" > pr-comment-data/results.json
122+
env:
123+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
124+
125+
- name: Upload PR comment data
126+
if: env.PR_NUMBER != ''
127+
uses: actions/upload-artifact@v4
128+
with:
129+
name: pr-comment-data
130+
path: pr-comment-data/
131+
retention-days: 1
132+
133+
- name: Trigger PR comment workflow
134+
if: env.PR_NUMBER != ''
135+
run: |
136+
echo "Triggering PR comment workflow for PR #${PR_NUMBER}"
137+
gh workflow run pr-comment-docker-image-sizes.yml \
138+
--ref ${GIT_BRANCH_NAME} \
139+
-f workflow_run_id=${{ github.run_id }}
140+
echo "PR comment workflow triggered successfully"
141+
env:
142+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)