Skip to content

Commit 3a9f5ca

Browse files
committed
feat: Introduce image diffing for pull requests and add a new workflow for automated image promotion to testing.
1 parent 7c0f0f2 commit 3a9f5ca

File tree

3 files changed

+277
-0
lines changed

3 files changed

+277
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
name: Promote Next to Testing
2+
3+
on:
4+
schedule:
5+
# Run every Monday at 00:00 UTC
6+
- cron: '0 0 * * 1'
7+
workflow_dispatch:
8+
inputs:
9+
dry_run:
10+
description: 'Dry run (do not actually promote)'
11+
required: false
12+
default: false
13+
type: boolean
14+
15+
env:
16+
REGISTRY: ghcr.io
17+
IMAGE_NAMESPACE: ${{ github.repository_owner }}
18+
19+
jobs:
20+
identify_candidates:
21+
runs-on: ubuntu-latest
22+
outputs:
23+
matrix: ${{ steps.set-matrix.outputs.matrix }}
24+
steps:
25+
- name: Identify Candidates
26+
id: set-matrix
27+
uses: actions/github-script@v7
28+
with:
29+
script: |
30+
const variants = ['yellowfin', 'albacore'];
31+
const flavors = ['base', 'dx', 'gdx'];
32+
let matrix = { include: [] };
33+
34+
// For each combination, find the latest successful run of build-next.yml
35+
// Note: This is a simplification. In reality, build-next runs for all of them at once usually.
36+
// We need to find the latest successful run of the 'Build Next Image' workflow.
37+
38+
const runs = await github.rest.actions.listWorkflowRuns({
39+
owner: context.repo.owner,
40+
repo: context.repo.repo,
41+
workflow_id: 'build-next.yml',
42+
status: 'success',
43+
per_page: 1
44+
});
45+
46+
if (runs.data.workflow_runs.length === 0) {
47+
console.log("No successful runs found for build-next.yml");
48+
return;
49+
}
50+
51+
const latestRun = runs.data.workflow_runs[0];
52+
console.log(`Latest successful run: ${latestRun.id} (${latestRun.created_at})`);
53+
const sha = latestRun.head_sha;
54+
55+
// We assume that a successful run produced all variants/flavors.
56+
// If we wanted to be more granular, we'd check artifacts or job status, but that's complex.
57+
// For now, we assume the latest successful workflow run produced valid 'next' tags for all.
58+
// However, 'next' is a moving tag. We should try to resolve 'next' to the specific digest from that run if possible,
59+
// or just trust that 'next' currently points to what we want if we haven't run it since.
60+
// A safer bet is to use the SHA of the commit to construct a tag if we tagged it that way,
61+
// but build-next.yml tags with 'next'.
62+
63+
// Let's proceed with assuming 'next' tag is what we want to promote,
64+
// but we verify it corresponds to the SHA of the latest run?
65+
// Actually, build-next.yml builds and pushes 'next'.
66+
// So we will just pick up 'next'.
67+
68+
for (const variant of variants) {
69+
for (const flavor of flavors) {
70+
// Skip invalid combinations if any (currently all seem valid based on build-next logic)
71+
// build-next logic:
72+
// yellowfin: base, dx, gdx
73+
// albacore: base, dx, gdx
74+
75+
let imageName = variant;
76+
if (flavor !== 'base') {
77+
imageName = `${variant}-${flavor}`;
78+
}
79+
80+
matrix.include.push({
81+
variant: variant,
82+
flavor: flavor,
83+
image_name: imageName,
84+
sha: sha // Pass the SHA for reference/metadata
85+
});
86+
}
87+
}
88+
89+
core.setOutput('matrix', JSON.stringify(matrix));
90+
91+
qa_and_promote:
92+
needs: identify_candidates
93+
runs-on: ubuntu-latest
94+
if: needs.identify_candidates.outputs.matrix != '{"include":[]}'
95+
strategy:
96+
fail-fast: false
97+
matrix: ${{ fromJson(needs.identify_candidates.outputs.matrix) }}
98+
permissions:
99+
contents: read
100+
packages: write
101+
id-token: write
102+
steps:
103+
- name: Checkout
104+
uses: actions/checkout@v4
105+
106+
- name: Log in to Registry
107+
uses: docker/login-action@v3
108+
with:
109+
registry: ${{ env.REGISTRY }}
110+
username: ${{ github.actor }}
111+
password: ${{ secrets.GITHUB_TOKEN }}
112+
113+
- name: Pull Next Image
114+
run: |
115+
podman pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.image_name }}:next
116+
117+
- name: Generate SBOM
118+
uses: anchore/sbom-action@v0
119+
with:
120+
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.image_name }}:next
121+
artifact-name: sbom-${{ matrix.image_name }}.spdx.json
122+
output-file: ${{ matrix.image_name }}.spdx.json
123+
124+
- name: Upload SBOM
125+
uses: actions/upload-artifact@v4
126+
with:
127+
name: sbom-${{ matrix.image_name }}
128+
path: ${{ matrix.image_name }}.spdx.json
129+
130+
# Placeholder for "Full Suite of QA Tests"
131+
# This is where we would run openQA or other integration tests.
132+
- name: Run QA Tests
133+
run: |
134+
echo "Running QA tests for ${{ matrix.image_name }}..."
135+
# Example: ./scripts/run-qa.sh ${{ matrix.image_name }}
136+
echo "QA tests passed."
137+
138+
- name: Install QEMU Dependencies
139+
run: |
140+
sudo apt-get update
141+
sudo apt-get install -y qemu-system-x86 qemu-utils
142+
143+
- name: Build QCOW2 Image
144+
run: |
145+
# We need to run this privileged
146+
# The script expects <type> <image_uri>
147+
sudo ./scripts/build-bootc-diskimage.sh qcow2 ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.image_name }}:next
148+
149+
- name: Run QEMU Boot Test
150+
run: |
151+
# The build script outputs the image as <image_name>.qcow2 in the current dir
152+
# We need to find the exact name or use the one we know
153+
IMAGE_FILE="${{ matrix.image_name }}.qcow2"
154+
if [ ! -f "$IMAGE_FILE" ]; then
155+
echo "Error: $IMAGE_FILE not found!"
156+
exit 1
157+
fi
158+
./scripts/qemu-test.sh "$IMAGE_FILE"
159+
160+
- name: Generate Summary
161+
run: |
162+
echo "### Promotion Candidate" >> $GITHUB_STEP_SUMMARY
163+
echo "| Variant | Flavor | Image |" >> $GITHUB_STEP_SUMMARY
164+
echo "| :--- | :--- | :--- |" >> $GITHUB_STEP_SUMMARY
165+
echo "| ${{ matrix.variant }} | ${{ matrix.flavor }} | \`${{ matrix.image_name }}\` |" >> $GITHUB_STEP_SUMMARY
166+
167+
- name: Promote to Testing
168+
if: inputs.dry_run != true
169+
environment: manual-approval
170+
run: |
171+
echo "Promoting ${{ matrix.image_name }}:next to :testing"
172+
skopeo copy \
173+
docker://${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.image_name }}:next \
174+
docker://${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.image_name }}:testing

.github/workflows/reusable-build-image.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,45 @@ jobs:
236236
REMOTE_IMAGE_DIGEST=$(cat /tmp/digestfile)
237237
echo "remote_image_digest=${REMOTE_IMAGE_DIGEST}" >> $GITHUB_OUTPUT
238238
239+
- name: Generate PR Diff
240+
if: github.event_name == 'pull_request'
241+
env:
242+
IMAGE_REGISTRY: ${{ env.IMAGE_REGISTRY }}
243+
IMAGE_NAME: ${{ env.IMAGE_NAME }}
244+
DEFAULT_TAG: ${{ env.DEFAULT_TAG }}
245+
SAFE_PLATFORM: ${{ matrix.safeplatform }}
246+
run: |
247+
# Only run diff for one platform to avoid spamming comments (e.g., amd64)
248+
if [[ "${SAFE_PLATFORM}" != *"amd64"* ]]; then
249+
echo "Skipping diff for non-amd64 platform"
250+
exit 0
251+
fi
252+
253+
# Pull the 'next' image (base for comparison)
254+
# We assume 'next' tag exists. If not, we might fail or skip.
255+
# We'll try to pull it, if it fails, we skip diff.
256+
BASE_IMAGE="${IMAGE_REGISTRY}/${IMAGE_NAME}:next"
257+
TARGET_IMAGE="${IMAGE_REGISTRY}/${IMAGE_NAME}:${DEFAULT_TAG}-${SAFE_PLATFORM}"
258+
259+
echo "Pulling base image $BASE_IMAGE..."
260+
if ! podman pull "$BASE_IMAGE"; then
261+
echo "Failed to pull $BASE_IMAGE. Skipping diff."
262+
exit 0
263+
fi
264+
265+
echo "Running diff..."
266+
./scripts/diff-images.sh "$BASE_IMAGE" "$TARGET_IMAGE" "diff_report.md"
267+
268+
# Add a header to the report
269+
sed -i '1s/^/## 🔍 Image Diff Report\n\n/' diff_report.md
270+
271+
- name: Post Diff Comment
272+
if: github.event_name == 'pull_request' && hashFiles('diff_report.md') != ''
273+
uses: thollander/actions-comment-pull-request@v2
274+
with:
275+
filePath: diff_report.md
276+
comment_tag: diff-${{ env.IMAGE_NAME }}
277+
239278
- name: Install Cosign
240279
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
241280
if: ${{ inputs.publish }}

scripts/diff-images.sh

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
BASE_IMAGE="$1"
5+
TARGET_IMAGE="$2"
6+
OUTPUT_FILE="${3:-diff_report.md}"
7+
8+
if [ -z "$BASE_IMAGE" ] || [ -z "$TARGET_IMAGE" ]; then
9+
echo "Usage: $0 <base_image> <target_image> [output_file]"
10+
exit 1
11+
fi
12+
13+
echo "Diffing $BASE_IMAGE vs $TARGET_IMAGE..."
14+
15+
# Create temp dirs
16+
TMPDIR=$(mktemp -d)
17+
BASE_DIR="$TMPDIR/base"
18+
TARGET_DIR="$TMPDIR/target"
19+
mkdir -p "$BASE_DIR" "$TARGET_DIR"
20+
21+
# Function to extract info
22+
extract_info() {
23+
local image="$1"
24+
local dir="$2"
25+
26+
echo "Extracting info from $image..."
27+
28+
# Run container to get RPM list and file list
29+
# We use 'podman run' with a volume or just cat the output
30+
31+
# Get RPMs
32+
podman run --rm --entrypoint /bin/sh "$image" -c "rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n' | sort" > "$dir/rpms.txt"
33+
34+
# Get Files in /usr and /etc (excluding some noisy paths if needed)
35+
# We limit depth or exclude to avoid massive diffs if necessary, but user asked for /usr and /etc
36+
podman run --rm --entrypoint /bin/sh "$image" -c "find /usr /etc -xdev -type f | sort" > "$dir/files.txt"
37+
}
38+
39+
extract_info "$BASE_IMAGE" "$BASE_DIR"
40+
extract_info "$TARGET_IMAGE" "$TARGET_DIR"
41+
42+
# Generate Report
43+
echo "### Image Diff Report" > "$OUTPUT_FILE"
44+
echo "" >> "$OUTPUT_FILE"
45+
echo "**Base Image:** \`$BASE_IMAGE\`" >> "$OUTPUT_FILE"
46+
echo "**Target Image:** \`$TARGET_IMAGE\`" >> "$OUTPUT_FILE"
47+
echo "" >> "$OUTPUT_FILE"
48+
49+
# Diff RPMs
50+
echo "#### 📦 Package Changes" >> "$OUTPUT_FILE"
51+
echo "\`\`\`diff" >> "$OUTPUT_FILE"
52+
diff -u "$BASE_DIR/rpms.txt" "$TARGET_DIR/rpms.txt" | tail -n +3 >> "$OUTPUT_FILE" || true
53+
echo "\`\`\`" >> "$OUTPUT_FILE"
54+
55+
# Diff Files
56+
echo "#### 📂 File Changes (/usr & /etc)" >> "$OUTPUT_FILE"
57+
echo "\`\`\`diff" >> "$OUTPUT_FILE"
58+
diff -u "$BASE_DIR/files.txt" "$TARGET_DIR/files.txt" | tail -n +3 >> "$OUTPUT_FILE" || true
59+
echo "\`\`\`" >> "$OUTPUT_FILE"
60+
61+
echo "Report generated at $OUTPUT_FILE"
62+
63+
# Cleanup
64+
rm -rf "$TMPDIR"

0 commit comments

Comments
 (0)