Skip to content

Commit c7b9a60

Browse files
aantnclaude
andauthored
Add GitHub Action to build and push Docker images on PRs (#1986)
Builds robusta-runner images on every PR and pushes to the temporary-builds registry in GCP. Posts a comment on the PR with the image tag and helm upgrade instructions. Fork PRs will build but not push (no registry access). Co-authored-by: Claude <[email protected]>
1 parent 42ed013 commit c7b9a60

File tree

1 file changed

+282
-0
lines changed

1 file changed

+282
-0
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
name: Docker Build on PR
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
# Cancel in-progress runs for the same PR
8+
concurrency:
9+
group: docker-build-${{ github.head_ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
16+
permissions:
17+
contents: 'read'
18+
id-token: 'write'
19+
pull-requests: 'write'
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- name: Check if PR is from fork
25+
id: fork_check
26+
run: |
27+
HEAD_REPO="${{ github.event.pull_request.head.repo.full_name }}"
28+
BASE_REPO="${{ github.repository }}"
29+
30+
if [ -z "$HEAD_REPO" ]; then
31+
# Head repo data is unavailable (repo may have been deleted)
32+
# Treat as fork to avoid attempting authenticated pushes
33+
echo "⚠️ Head repo data is unavailable - skipping authenticated push"
34+
echo "is_fork=true" >> $GITHUB_OUTPUT
35+
elif [ "$HEAD_REPO" != "$BASE_REPO" ]; then
36+
echo "📌 PR is from a fork ($HEAD_REPO != $BASE_REPO) - will build but not push"
37+
echo "is_fork=true" >> $GITHUB_OUTPUT
38+
else
39+
echo "✅ PR is from the same repo ($HEAD_REPO)"
40+
echo "is_fork=false" >> $GITHUB_OUTPUT
41+
fi
42+
43+
- name: Get short SHA
44+
id: short_sha
45+
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
46+
47+
- name: Record start time
48+
id: start_time
49+
run: echo "start=$(date +%s)" >> $GITHUB_OUTPUT
50+
51+
- name: Comment build started
52+
if: steps.fork_check.outputs.is_fork != 'true'
53+
uses: actions/github-script@v7
54+
with:
55+
script: |
56+
const owner = context.repo.owner;
57+
const repo = context.repo.repo;
58+
const shortSha = `${{ steps.short_sha.outputs.sha_short }}`;
59+
const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }}`;
60+
const marker = '<!-- docker-build-comment -->';
61+
const registryLink = 'https://console.cloud.google.com/artifacts/docker/robusta-development/us-central1/temporary-builds/robusta-runner?project=robusta-development';
62+
63+
const issue_number = context.payload.pull_request.number;
64+
65+
const existingComments = await github.paginate(github.rest.issues.listComments, {
66+
owner,
67+
repo,
68+
issue_number,
69+
});
70+
const existing = existingComments.find(c => c.body?.includes(marker));
71+
72+
// Clean up old-style comments from previous workflow version
73+
const oldMarker = 'Dev Docker images are ready for this commit:';
74+
const oldComments = existingComments.filter(c => c.body?.includes(oldMarker));
75+
for (const old of oldComments) {
76+
await github.rest.issues.deleteComment({ owner, repo, comment_id: old.id });
77+
core.info(`Deleted old-style comment #${old.id}`);
78+
}
79+
80+
// Extract previous image tag from existing comment if present
81+
let previousImageSection = '';
82+
if (existing) {
83+
const tagMatch = existing.body.match(/robusta-runner:([a-f0-9]{7})/);
84+
if (tagMatch && tagMatch[1] !== shortSha) {
85+
const prevSha = tagMatch[1];
86+
const prevTag = `us-central1-docker.pkg.dev/robusta-development/temporary-builds/robusta-runner:${prevSha}`;
87+
previousImageSection = [
88+
'',
89+
'---',
90+
`📦 **Previous image (\`${prevSha}\`):**`,
91+
`- [${prevTag}](${registryLink})`,
92+
'',
93+
'<details>',
94+
'<summary>📋 Copy commands</summary>',
95+
'',
96+
'⚠️ Temporary images are deleted after 30 days. Copy to a permanent registry before using them:',
97+
'```bash',
98+
'gcloud auth configure-docker us-central1-docker.pkg.dev',
99+
`docker pull ${prevTag}`,
100+
`docker tag ${prevTag} me-west1-docker.pkg.dev/robusta-development/development/robusta-runner-dev:${prevSha}`,
101+
`docker push me-west1-docker.pkg.dev/robusta-development/development/robusta-runner-dev:${prevSha}`,
102+
'```',
103+
'',
104+
'Patch Helm values in one line:',
105+
'```bash',
106+
'helm upgrade --install robusta robusta/robusta \\\\',
107+
' --reuse-values \\\\',
108+
` --set runner.image=me-west1-docker.pkg.dev/robusta-development/development/robusta-runner-dev:${prevSha}`,
109+
'```',
110+
'</details>',
111+
].join('\n');
112+
}
113+
}
114+
115+
const message = [
116+
marker,
117+
`🔨 **Building Docker image for \`${shortSha}\`...** (x64 only)`,
118+
'',
119+
`[View build logs](${runUrl})`,
120+
previousImageSection,
121+
].join('\n');
122+
123+
if (existing) {
124+
await github.rest.issues.updateComment({
125+
owner,
126+
repo,
127+
comment_id: existing.id,
128+
body: message,
129+
});
130+
core.info(`Updated existing PR comment #${existing.id}`);
131+
} else {
132+
await github.rest.issues.createComment({
133+
owner,
134+
repo,
135+
issue_number,
136+
body: message,
137+
});
138+
core.info(`Commented on PR #${issue_number}`);
139+
}
140+
141+
# Registry auth - only for non-fork PRs (fork PRs don't have access to push anyway)
142+
- uses: google-github-actions/auth@v2
143+
if: steps.fork_check.outputs.is_fork != 'true'
144+
with:
145+
project_id: 'robusta-development'
146+
workload_identity_provider: 'projects/479654156100/locations/global/workloadIdentityPools/github/providers/robusta-repos'
147+
148+
- name: Set up gcloud CLI
149+
if: steps.fork_check.outputs.is_fork != 'true'
150+
uses: google-github-actions/setup-gcloud@v2
151+
with:
152+
project_id: robusta-development
153+
154+
- name: Configure Docker Registry
155+
if: steps.fork_check.outputs.is_fork != 'true'
156+
run: gcloud auth configure-docker us-central1-docker.pkg.dev
157+
158+
- name: Set up Docker Buildx
159+
uses: docker/setup-buildx-action@v3
160+
161+
# For fork PRs: build only (no push, no registry tags)
162+
# For non-fork PRs: build and push to registry
163+
# Note: PR builds are x64-only for speed. Multi-arch builds happen on release.
164+
- name: Build Docker image (fork PR - no push)
165+
if: steps.fork_check.outputs.is_fork == 'true'
166+
uses: docker/build-push-action@v6
167+
with:
168+
context: .
169+
platforms: linux/amd64
170+
push: false
171+
cache-from: type=gha
172+
cache-to: type=gha,mode=max
173+
174+
- name: Build and push Docker image
175+
if: steps.fork_check.outputs.is_fork != 'true'
176+
uses: docker/build-push-action@v6
177+
with:
178+
context: .
179+
platforms: linux/amd64
180+
push: true
181+
tags: |
182+
us-central1-docker.pkg.dev/robusta-development/temporary-builds/robusta-runner:${{ github.sha }}
183+
us-central1-docker.pkg.dev/robusta-development/temporary-builds/robusta-runner:${{ steps.short_sha.outputs.sha_short }}
184+
cache-from: type=gha
185+
cache-to: type=gha,mode=max
186+
187+
- name: Print image location
188+
if: steps.fork_check.outputs.is_fork != 'true'
189+
run: |
190+
echo "Docker image pushed (x64 only):"
191+
echo " us-central1-docker.pkg.dev/robusta-development/temporary-builds/robusta-runner:${{ github.sha }}"
192+
echo " us-central1-docker.pkg.dev/robusta-development/temporary-builds/robusta-runner:${{ steps.short_sha.outputs.sha_short }}"
193+
194+
- name: Comment with image details
195+
if: always() && steps.fork_check.outputs.is_fork != 'true'
196+
uses: actions/github-script@v7
197+
with:
198+
script: |
199+
const owner = context.repo.owner;
200+
const repo = context.repo.repo;
201+
const shortSha = `${{ steps.short_sha.outputs.sha_short }}`;
202+
const jobStatus = `${{ job.status }}`;
203+
const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }}`;
204+
const marker = '<!-- docker-build-comment -->';
205+
206+
// Calculate build duration
207+
const startTime = parseInt(`${{ steps.start_time.outputs.start }}`);
208+
const endTime = Math.floor(Date.now() / 1000);
209+
const durationSecs = endTime - startTime;
210+
const minutes = Math.floor(durationSecs / 60);
211+
const seconds = durationSecs % 60;
212+
const duration = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
213+
214+
let message;
215+
if (jobStatus === 'success') {
216+
const shortTag = `us-central1-docker.pkg.dev/robusta-development/temporary-builds/robusta-runner:${shortSha}`;
217+
const registryLink = 'https://console.cloud.google.com/artifacts/docker/robusta-development/us-central1/temporary-builds/robusta-runner?project=robusta-development';
218+
219+
message = [
220+
marker,
221+
`✅ **Docker image ready for \`${shortSha}\`** (built in ${duration})`,
222+
'',
223+
`- [${shortTag}](${registryLink})`,
224+
'',
225+
'> ⚠️ **Warning: does not support ARM** (ARM images are built on release only - not on every PR)',
226+
'',
227+
'Use this tag to pull the image for testing.',
228+
'',
229+
'<details>',
230+
'<summary>📋 Copy commands</summary>',
231+
'',
232+
'⚠️ Temporary images are deleted after 30 days. Copy to a permanent registry before using them:',
233+
'```bash',
234+
'gcloud auth configure-docker us-central1-docker.pkg.dev',
235+
`docker pull ${shortTag}`,
236+
`docker tag ${shortTag} me-west1-docker.pkg.dev/robusta-development/development/robusta-runner-dev:${shortSha}`,
237+
`docker push me-west1-docker.pkg.dev/robusta-development/development/robusta-runner-dev:${shortSha}`,
238+
'```',
239+
'',
240+
'Patch Helm values in one line:',
241+
'```bash',
242+
'helm upgrade --install robusta robusta/robusta \\',
243+
' --reuse-values \\',
244+
` --set runner.image=me-west1-docker.pkg.dev/robusta-development/development/robusta-runner-dev:${shortSha}`,
245+
'```',
246+
'</details>',
247+
].join('\n');
248+
} else {
249+
message = [
250+
marker,
251+
`❌ **Docker build failed for \`${shortSha}\`** (after ${duration})`,
252+
'',
253+
`[View build logs](${runUrl})`,
254+
].join('\n');
255+
}
256+
257+
const issue_number = context.payload.pull_request.number;
258+
259+
const existingComments = await github.paginate(github.rest.issues.listComments, {
260+
owner,
261+
repo,
262+
issue_number,
263+
});
264+
const existing = existingComments.find(c => c.body?.includes(marker));
265+
266+
if (existing) {
267+
await github.rest.issues.updateComment({
268+
owner,
269+
repo,
270+
comment_id: existing.id,
271+
body: message,
272+
});
273+
core.info(`Updated existing PR comment #${existing.id}`);
274+
} else {
275+
await github.rest.issues.createComment({
276+
owner,
277+
repo,
278+
issue_number,
279+
body: message,
280+
});
281+
core.info(`Commented on PR #${issue_number}`);
282+
}

0 commit comments

Comments
 (0)