Skip to content

Commit f30cbe2

Browse files
committed
ci: improve build and artifact performance
Isolate official and pull-request BuildKit caches, reuse commit-addressed native images, publish candidates directly to ACR, reduce artifact processing, and retain measured test sharding. The prerequisite Notation verification topology now lives in the supply-chain branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0abe4d46-be3e-478e-bb36-a58a255110a0
1 parent 995222d commit f30cbe2

22 files changed

Lines changed: 828 additions & 468 deletions

‎.github/actions/upload-image/action.yml‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,14 @@
1414

1515
name: Upload component image
1616
description: >-
17-
Tags a locally built component image with the Git-derived container version,
18-
saves both that tag and the local test tag to a tarball, and uploads it as the
19-
dacs-image-<name>-<arch> artifact the test/scan jobs download.
17+
Uploads the Docker archive produced directly by BuildKit as the
18+
dacs-image-<name>-<arch> artifact consumed by test and scan jobs.
2019
2120
inputs:
2221
name:
2322
description: >-
24-
Component image name. Must match the local docker tag (<name>:local) and
25-
is used as the dacs-image-<name>-<arch> artifact name.
23+
Component image name used to locate dacs-image-<name>.tar and form the
24+
dacs-image-<name>-<arch> artifact name.
2625
required: true
2726
arch:
2827
description: Target architecture suffix for the artifact name (e.g. amd64).
@@ -31,22 +30,11 @@ inputs:
3130
runs:
3231
using: composite
3332
steps:
34-
- name: Save ${{ inputs.name }} image to a tarball
35-
shell: bash
36-
env:
37-
IMAGE_NAME: ${{ inputs.name }}
38-
run: |
39-
version="$(./.pipelines/scripts/get-version.sh container)"
40-
docker tag "${IMAGE_NAME}:local" "${IMAGE_NAME}:${version}"
41-
docker save \
42-
-o "${RUNNER_TEMP}/dacs-image-${IMAGE_NAME}.tar" \
43-
"${IMAGE_NAME}:local" \
44-
"${IMAGE_NAME}:${version}"
45-
4633
- name: Upload ${{ inputs.name }} image
4734
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
4835
with:
4936
name: dacs-image-${{ inputs.name }}-${{ inputs.arch }}
5037
path: ${{ runner.temp }}/dacs-image-${{ inputs.name }}.tar
5138
retention-days: 1
5239
if-no-files-found: error
40+
compression-level: 0

‎.github/workflows/build.yml‎

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ name: Build
1919
# The image builds are split per image so they run on separate runners:
2020
#
2121
# compile --+--> images-cpp (matrix: cache-server, cache-test-client)
22-
# +--> python-package (make python-package) -> dacs-wheel artifact
23-
# +--> rust-package (make rust-package) -> dacs-rust-crate artifact
22+
# +--> packages (Python wheel and Rust crate)
2423
# images-go (matrix: scheduler, controller) # no needs: runs alongside compile
2524
#
2625
# The two Go images compile Go from source inside their own Dockerfiles and do
@@ -52,7 +51,7 @@ on:
5251
permissions: {}
5352

5453
jobs:
55-
# Compile once. Publishes the build tree so images, python-package and the
54+
# Compile once. Publishes the build tree so images, packages, and the
5655
# caller's test jobs can all reuse it. Builds and gha-caches the native-build
5756
# toolchain image so downstream jobs restore it cheaply.
5857
compile:
@@ -106,34 +105,27 @@ jobs:
106105
# private ACR. The tag matches what bldenv.sh looks up (derived from the
107106
# runner arch, so this is correct on amd64 and arm64 runners alike), so
108107
# `make build` reuses this locally loaded image instead of trying to pull
109-
# it. We docker save it to an artifact so the downstream in-container jobs
110-
# (test, pytest, wheel) can docker load the exact same image instead of
111-
# rebuilding it; the gha cache keeps this build step itself fast on re-runs.
108+
# it. BuildKit writes the Docker archive directly so downstream jobs can
109+
# load the exact same image without a separate docker save export.
112110
- name: Resolve native-build image tag
113111
id: image
114112
run: |
115113
arch=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
116114
echo "tag=dacstest.azurecr.io/dacs/native-build:linux-${arch}" >> "$GITHUB_OUTPUT"
117115
118-
- name: Build native-build container image
116+
- name: Build and export native-build container image
119117
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
120118
with:
121119
context: src/images/native-build
122120
file: src/images/native-build/Dockerfile
123121
tags: ${{ steps.image.outputs.tag }}
124-
load: true
125122
push: false # build locally only; never pushed to a registry
126123
cache-from: type=gha,scope=native-build-${{ inputs.build_arch }}
127-
cache-to: type=gha,scope=native-build-${{ inputs.build_arch }},mode=max
124+
cache-to: type=gha,scope=native-build-${{ inputs.build_arch }},mode=min
125+
outputs: type=docker,dest=/tmp/dacs-native-build-image-${{ inputs.build_arch }}.tar
128126

129-
# Publish the toolchain image so the in-container test/pytest/wheel jobs can
130-
# load the identical image (docker save -> artifact -> docker load) rather
131-
# than reconstructing it from the layer cache in every job.
132-
- name: Save native-build image to a tarball
133-
env:
134-
BUILD_ARCH: ${{ inputs.build_arch }}
135-
IMAGE_TAG: ${{ steps.image.outputs.tag }}
136-
run: docker save -o "/tmp/dacs-native-build-image-${BUILD_ARCH}.tar" "$IMAGE_TAG"
127+
- name: Load native-build image
128+
run: docker load -i "/tmp/dacs-native-build-image-${BUILD_ARCH}.tar"
137129

138130
- name: Upload native-build image
139131
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -142,6 +134,7 @@ jobs:
142134
path: /tmp/dacs-native-build-image-${{ inputs.build_arch }}.tar
143135
retention-days: 1
144136
if-no-files-found: error
137+
compression-level: 0
145138

146139
- name: Build
147140
run: make build
@@ -156,9 +149,12 @@ jobs:
156149
# than upload the dirs directly) to preserve the executable bit on the
157150
# compiled binaries, which actions/upload-artifact would otherwise drop.
158151
- name: Pack build tree
159-
env:
160-
BUILD_ARCH: ${{ inputs.build_arch }}
161-
run: tar -cf "/tmp/dacs-build-tree-${BUILD_ARCH}.tar" bin $(find build -name 'CTestTestfile.cmake')
152+
run: |
153+
mapfile -d '' -t ctest_files < <(
154+
find build -name 'CTestTestfile.cmake' -print0
155+
)
156+
tar -cf "/tmp/dacs-build-tree-${BUILD_ARCH}.tar" \
157+
bin "${ctest_files[@]}"
162158
163159
- name: Upload build tree
164160
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
@@ -167,15 +163,16 @@ jobs:
167163
path: /tmp/dacs-build-tree-${{ inputs.build_arch }}.tar
168164
retention-days: 1
169165
if-no-files-found: error
166+
compression-level: 0
170167

171168
# Build the two Go component images (scheduler, controller). They compile Go
172169
# from source inside their own Dockerfiles and do NOT need the C++ build tree,
173170
# so this job has no `needs` and runs concurrently with compile. Each image
174171
# builds on its own runner (matrix) via docker/build-push-action with the
175172
# GitHub Actions layer cache (scoped per image and architecture), so the Go
176173
# module download and build layers persist across runs without amd64 and arm64
177-
# overwriting each other. The image is loaded into the Docker store and
178-
# uploaded as a tarball.
174+
# overwriting each other. BuildKit exports the image directly as the tarball
175+
# uploaded for downstream consumers.
179176
images-go:
180177
name: Go image (${{ matrix.image.name }})
181178
runs-on: ${{ inputs.runner }}
@@ -215,17 +212,23 @@ jobs:
215212
- name: Set up Docker Buildx
216213
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
217214

215+
- name: Resolve image version
216+
id: image
217+
run: echo "version=$(./.pipelines/scripts/get-version.sh container)" >> "$GITHUB_OUTPUT"
218+
218219
- name: Build ${{ matrix.image.name }} image
219220
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
220221
with:
221222
context: ${{ matrix.image.context }}
222223
file: ${{ matrix.image.file }}
223-
tags: ${{ matrix.image.name }}:local
224-
load: true
224+
tags: |
225+
${{ matrix.image.name }}:local
226+
${{ matrix.image.name }}:${{ steps.image.outputs.version }}
227+
outputs: type=docker,dest=${{ runner.temp }}/dacs-image-${{ matrix.image.name }}.tar
225228
cache-from: type=gha,scope=${{ matrix.image.name }}-${{ inputs.build_arch }}
226229
cache-to: type=gha,scope=${{ matrix.image.name }}-${{ inputs.build_arch }},mode=max
227230

228-
- name: Save and upload ${{ matrix.image.name }} image
231+
- name: Upload ${{ matrix.image.name }} image
229232
uses: $/.github/actions/upload-image
230233
with:
231234
name: ${{ matrix.image.name }}
@@ -250,8 +253,10 @@ jobs:
250253
image:
251254
- name: cache-server
252255
file: CacheServer.Dockerfile
256+
cache-mode: min
253257
- name: cache-test-client
254258
file: CacheClient.Dockerfile
259+
cache-mode: max
255260
steps:
256261
- name: Harden Runner
257262
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
@@ -282,30 +287,33 @@ jobs:
282287
BUILD_CONFIGURATION: ${{ inputs.build_type }}
283288
BUILD_ARCH: ${{ inputs.build_arch }}
284289

290+
- name: Resolve image version
291+
id: image
292+
run: echo "version=$(./.pipelines/scripts/get-version.sh container)" >> "$GITHUB_OUTPUT"
293+
285294
- name: Build ${{ matrix.image.name }} image
286295
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
287296
with:
288297
context: Docker
289298
file: Docker/${{ matrix.image.file }}
290-
tags: ${{ matrix.image.name }}:local
291-
load: true
299+
tags: |
300+
${{ matrix.image.name }}:local
301+
${{ matrix.image.name }}:${{ steps.image.outputs.version }}
292302
push: false
303+
outputs: type=docker,dest=${{ runner.temp }}/dacs-image-${{ matrix.image.name }}.tar
293304
cache-from: type=gha,scope=${{ matrix.image.name }}-${{ inputs.build_arch }}
294-
cache-to: type=gha,scope=${{ matrix.image.name }}-${{ inputs.build_arch }},mode=max
305+
cache-to: type=gha,scope=${{ matrix.image.name }}-${{ inputs.build_arch }},mode=${{ matrix.image.cache-mode }}
295306

296-
- name: Save and upload ${{ matrix.image.name }} image
307+
- name: Upload ${{ matrix.image.name }} image
297308
uses: $/.github/actions/upload-image
298309
with:
299310
name: ${{ matrix.image.name }}
300311
arch: ${{ inputs.build_arch }}
301312

302-
# Build the dacs-client wheel from the compiled native library. Runs in the
303-
# native-build container (glibc-consistent with the compiled .so) via the
304-
# make target, so the composite restores the toolchain image. Runs in parallel
305-
# with the image jobs. Catches packaging regressions (missing data files, broken
306-
# auditwheel repair) before publish.
307-
python-package:
308-
name: Python package (wheel)
313+
# Build the Python wheel and Rust crate in one job so the large native-build
314+
# image and build tree are restored only once.
315+
packages:
316+
name: Python and Rust packages
309317
needs: compile
310318
runs-on: ${{ inputs.runner }}
311319
timeout-minutes: 30
@@ -344,38 +352,6 @@ jobs:
344352
retention-days: 1
345353
if-no-files-found: error
346354

347-
# Package the Rust client with the matching architecture's native library.
348-
# This job only uploads a workflow artifact and has no package publishing
349-
# permissions or registry credentials.
350-
rust-package:
351-
name: Rust package (crate)
352-
needs: compile
353-
runs-on: ${{ inputs.runner }}
354-
timeout-minutes: 20
355-
permissions:
356-
contents: read
357-
env:
358-
BUILD_TYPE: ${{ inputs.build_type }}
359-
BUILD_ARCH: ${{ inputs.build_arch }}
360-
steps:
361-
- name: Harden Runner
362-
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
363-
with:
364-
egress-policy: audit
365-
366-
- name: Check out the code
367-
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
368-
with:
369-
persist-credentials: false
370-
fetch-depth: 0
371-
fetch-tags: true
372-
373-
- name: Restore build tree
374-
uses: $/.github/actions/restore-build
375-
with:
376-
artifact: dacs-build-tree-${{ inputs.build_arch }}
377-
native-build-image-artifact: dacs-native-build-image-${{ inputs.build_arch }}
378-
379355
- name: Cache Cargo dependencies and build outputs
380356
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
381357
with:

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ name: CI
2020
# iterated in the PR itself, and fork PRs get a read-only token with no secrets.
2121
#
2222
# It calls the reusable build workflow once, then fans out in parallel the
23-
# credential-free jobs that consume its artifacts (each `needs: build`, reusing
24-
# the build's artifacts via same-run download - no run-id or token needed). This
25-
# mirrors the ADO pipeline, where Build runs first and the downstream stages run
26-
# in parallel afterwards.
23+
# credential-free jobs that consume its artifacts. Every run executes the full
24+
# AMD64 and ARM64 build, scan, unit-test, and integration-test matrix.
2725
#
2826
# Only credential-free legs run here - each `needs: build` job below is
2927
# documented by its own comment, and those job definitions are the source of
@@ -76,11 +74,10 @@ jobs:
7674
# Vulnerability-scan the container images the build produced. Fans out into
7775
# one parallel job per component image (matrix) so the Trivy step is written
7876
# once instead of copy-pasted per image. Each job downloads only its own
79-
# per-image dacs-image-<name>-<arch> artifact (docker load) instead of
80-
# rebuilding it. fail-fast is off so one image's CVEs do not hide the others.
81-
# This scan is report-only (exit-code 0): findings are uploaded as code-scanning
82-
# alerts to triage over time and never fail the build. New vulnerable
83-
# dependencies a PR adds are gated separately by the Dependency Review workflow.
77+
# per-image dacs-image-<name>-<arch> artifact directly instead of rebuilding
78+
# or loading it into Docker. fail-fast is off so one image's CVEs do not hide
79+
# the others. The SARIF scan reports high and critical findings, then a second
80+
# archive scan fails the job for fixable critical vulnerabilities.
8481
image-scan:
8582
name: Image vulnerability scan (${{ matrix.image }}, ${{ matrix.arch }})
8683
needs: build
@@ -118,15 +115,10 @@ jobs:
118115
name: dacs-image-${{ matrix.image }}-${{ matrix.arch }}
119116
path: /tmp
120117

121-
- name: Load image into Docker
122-
env:
123-
IMAGE_NAME: ${{ matrix.image }}
124-
run: docker load -i "/tmp/dacs-image-${IMAGE_NAME}.tar"
125-
126118
- name: Scan ${{ matrix.image }}
127119
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # 0.36.0
128120
with:
129-
image-ref: '${{ matrix.image }}:local'
121+
input: '/tmp/dacs-image-${{ matrix.image }}.tar'
130122
format: 'sarif'
131123
output: 'trivy-${{ matrix.image }}-${{ matrix.arch }}.sarif'
132124
exit-code: '0'
@@ -145,12 +137,14 @@ jobs:
145137
- name: Block fixable critical vulnerabilities in ${{ matrix.image }}
146138
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # 0.36.0
147139
with:
148-
image-ref: '${{ matrix.image }}:local'
140+
input: '/tmp/dacs-image-${{ matrix.image }}.tar'
149141
format: 'table'
150142
exit-code: '1'
151143
ignore-unfixed: true
152144
vuln-type: 'os,library'
153145
severity: 'CRITICAL'
146+
skip-setup-trivy: true
147+
cache: 'false'
154148

155149
# CPU unit tests: native C++ (ctest), Go, and Rust. Reuses the compiled build
156150
# tree and restores the native-build toolchain image from the gha cache

‎.pipelines/dacs-official.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,4 @@ extends:
7272
azureServiceConnection: ${{ parameters.azureServiceConnection }}
7373
azureStorageBlobPath: ${{ parameters.azureStorageBlobPath }}
7474
pushNativeBuildImages: true
75+
buildCacheScope: main

‎.pipelines/dacs-pullrequest.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ extends:
6161
acrName: ${{ parameters.acrName }}
6262
azureServiceConnection: ${{ parameters.azureServiceConnection }}
6363
azureStorageBlobPath: ${{ parameters.azureStorageBlobPath }}
64+
buildCacheScope: pr
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
# Copyright (c) Microsoft Corporation.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
resolve_build_cache_scope() {
17+
local scope="${1:-main}"
18+
local pull_request_id
19+
20+
if [[ "$scope" == "pr" ]]; then
21+
pull_request_id="${SYSTEM_PULLREQUEST_PULLREQUESTID:-}"
22+
if [[ -z "$pull_request_id" &&
23+
"${BUILD_SOURCEBRANCH:-}" =~ ^refs/pull/([0-9]+)/ ]]; then
24+
pull_request_id="${BASH_REMATCH[1]}"
25+
fi
26+
scope="pr-${pull_request_id:-${BUILD_BUILDID:-local}}"
27+
fi
28+
29+
if [[ ! "$scope" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]]; then
30+
echo "Invalid BUILD_CACHE_SCOPE: $scope" >&2
31+
return 1
32+
fi
33+
34+
printf '%s\n' "$scope"
35+
}

0 commit comments

Comments
 (0)