12
12
# It is assumed that the user invoking this script has permissions to use
13
13
# docker. On Linux this means the user must be in the `docker` group.
14
14
15
- DEFAULT_DOCKERFILE=.buildbot_dockerfile_default
16
15
17
16
# Arguments to enable PT and rr support in docker.
18
17
CAP_ARGS=" --cap-add CAP_PERFMON --cap-add SYS_PTRACE --security-opt seccomp=unconfined"
19
18
19
+ TRYCI_BUILD_CTXT=" ."
20
+ TRYCI_REMOTE_CLONE_DEPTH=100 # This is the same as on our CI server
21
+ TRYCI_DOCKERFILES=" "
22
+ TRYCI_DEFAULT_SCRIPT=" .buildbot.sh"
23
+ TRYCI_DOCKERFILE_BASE=.buildbot_dockerfile_
24
+ TRYCI_DEFAULT_DOCKERFILE=${TRYCI_DOCKERFILE_BASE} default
25
+ TRYCI_BUILD_PREFIX=" "
20
26
21
27
set -e
22
28
23
- run_image () {
24
- # Extract the dockerfile suffix. E.g. for '.buildbot_dockerfile_myrepo'
25
- # it's 'myimage'.
26
- suffix=` echo " $1 " | sed -e ' s/^.buildbot_dockerfile_//' `
29
+ usage () {
30
+ cat << EOF
31
+ Runs a soft-dev CI job.
32
+ Must be run from the same directory as the job's .buildbot.sh file.
33
+
34
+ usage: tryci [-p] [-r server_name] [-b <ref>] [-h]
35
+
36
+ Options:
37
+ -p, --post-mortem
38
+ Attach a shell to the image to prod around if the build fails.
39
+
40
+ -r, --remote server_name
41
+ Specify the server \` server_name\` to run the CI job on over SSH.
42
+ Useful if you want to test on a remote CI environment.
27
43
28
- # Generate an identifier for the repository.
29
- if [ " ${REPOSITORY} " != " " ]; then
30
- # Buildbot will set $REPOSITORY to a git url.
44
+ -c, --checkout <ref>
45
+ Tryci will run a CI job from a clone of <ref> instead of the
46
+ working tree. Valid formats:
47
+ - Branch name (e.g., main, feature/x)
48
+ - Commit hash (e.g., a1b2c3d)
49
+ - Tag (e.g., v1.0.0)
50
+ - Remote-tracking branch (e.g., origin/main)
51
+ - Remote URL with branch (e.g., https://github.com/user/repo#branch)
52
+ Useful for debugging the exact version of a CI job that failed on CI.
53
+
54
+ -h, --help
55
+ Show this help message and exit.
56
+ EOF
57
+ }
58
+
59
+ error () { printf " \e[31m[ERROR]\e[0m %s\n" " $1 " >&2 ; }
60
+
61
+ cleanup () {
62
+ if [[ -n " $tmpdir " && -d " $tmpdir " ]]; then
63
+ rm -rf -- " $tmpdir "
64
+ fi
65
+ }
66
+
67
+ trap cleanup EXIT
68
+
69
+ resolve_build_ctxt () {
70
+ # When we pass -c/--checkout <ref> we want the build context to be a clone
71
+ # of <ref>. There isn't really a nice way to do this in docker so this
72
+ # function resolves the build context depending on the contents of <ref>.
73
+
74
+ # First, the simple case: no '--checkout <ref>' option was used. We keep
75
+ # the default build context as the current working directory and generate a
76
+ # best-guess image prefix in the format: local-<pwd>:dirty.
77
+ if [ -z " $ref " ]; then
78
+ if [ ! -f ${TRYCI_DEFAULT_SCRIPT} ]; then
79
+ error " ${TRYCI_DEFAULT_SCRIPT} not found in directory: $pwd " .
80
+ exit 1
81
+ else
82
+ TRYCI_BUILD_PREFIX=" local-$( basename $( pwd) ) :dirty"
83
+ return
84
+ fi
85
+ fi
86
+
87
+ # Second, some --checkout <ref> was provided. If we get here we can't
88
+ # simply use the working tree anymore. We need to find out what <ref> is,
89
+ # clone it into a tmpdir, and set that as our build context.
90
+
91
+ tmpdir=$( mktemp -d) # removed with a cleanup trap on EXIT.
92
+ TRYCI_BUILD_CTXT=" $tmpdir "
93
+
94
+ if [[ " $ref " =~ ^(https? | git| ssh):// ]] || [[ " $ref " =~ ^[^/]+@[^:]+: ]]; then
95
+ # <ref> is a remote repo. We'll need to extract any branch/commit/tag
96
+ # that may have been provided. For example, if passed:
97
+ #
98
+ # '--checkout https://github.com/ykjit/yk#trying'
31
99
#
32
- # Transform URLs like `https://github.com/user/repo` into
33
- # `github.com_user_repo`.
34
- repo=` echo ${REPOSITORY} | \
35
- sed -E ' s/https:\/\/|git:\/\/(.*)/\1/g' | tr ' /' ' _' | sed -E ' s/_$//' `
100
+ # We must extract 'trying' and ensure our clone is checked out on that
101
+ # branch.
102
+ local base_url=" ${ref%%#* } "
103
+ local tag=$( [[ " $base_url " != " $ref " ]] && echo " ${ref#*# } " )
104
+
105
+ if [ -n " $tag " ]; then
106
+ git clone --no-checkout --depth=" $TRYCI_REMOTE_CLONE_DEPTH " " $base_url " " $tmpdir "
107
+ # FIXME: this can fail if the requested commit hash is deeper than
108
+ # TRYCI_REMOTE_CLONE_DEPTH.
109
+ git -C " $tmpdir " checkout " $tag "
110
+ else
111
+ git clone --depth=" $TRYCI_REMOTE_CLONE_DEPTH " " $base_url " " $tmpdir "
112
+ fi
113
+
114
+ # For the image tag, transform URLs like `https://github.com/user/repo`
115
+ # into `github.com_user_repo`.
116
+ local prefix=$( echo $base_url | \
117
+ sed -E ' s/https:\/\/|git:\/\/(.*)/\1/g' | \
118
+ tr ' /' ' _' | \
119
+ sed -E ' s/_$//' )
36
120
else
37
- # If repository isn't set, make a pseudo-name that can be used in place
38
- # of the proper repo identifier.
39
- dir=` pwd`
40
- repo=" local-` basename ${dir} ` "
121
+ # Finally, <ref> is branch/tag/commit of the local repo. This is a bit
122
+ # trickier because we want to ensure that any checkout only hits refs
123
+ # in our local git cache and never tries to pull from remote.
124
+ local prefix=" local-$( basename $( pwd) ) "
125
+ local tag=$ref
126
+ if git rev-parse --verify --quiet " $ref " > /dev/null; then
127
+ # Before we waste time doing anything, lets check if a CI build
128
+ # script even exists at the given ref.
129
+ if ! git cat-file -e $ref :$TRYCI_DEFAULT_SCRIPT 2> /dev/null; then
130
+ error " CI script '$TRYCI_DEFAULT_SCRIPT ' does not exist at revision: $ref "
131
+ exit 1
132
+ fi
133
+
134
+ git clone --no-checkout . " $tmpdir "
135
+
136
+ # This is the important part. By copying the refs and modules over
137
+ # from the working tree, we ensure that any subsequent `git
138
+ # submodule --init` call is either a no-op or checks out an older
139
+ # commit that we already have downloaded. It will never have to
140
+ # clone from the remote.
141
+ cp -r .git/refs " $tmpdir /.git/"
142
+ cp -r .git/modules " $tmpdir /.git/"
143
+ cp .git/config " $tmpdir /.git/config"
144
+
145
+ # Finally, we checkout out the desired ref.
146
+ git -C " $tmpdir " checkout " $ref "
147
+ else
148
+ echo " $ref does not exist locally. Please fetch first if you need it."
149
+ exit 1
150
+ fi
151
+ fi
152
+
153
+ # This is important for projects like 'alloy' and 'yk' because we
154
+ # deliberatly did not clone recursively.
155
+ git -C " $tmpdir " submodule update --progress --init --recursive
156
+
157
+ if [[ " $tag " =~ ^[0-9a-fA-F]{6,40}$ ]]; then
158
+ # If <ref> contained a commit hash, we must shorten it to the first 6
159
+ # chars because docker tags have a strict length limit.
160
+ tag=" ${tag: 0: 6} "
41
161
fi
162
+ TRYCI_BUILD_PREFIX=" $prefix ${tag: +" :$tag " } "
163
+ }
42
164
43
- # Image name must be unique to the buildbot worker so that workers don't clash.
44
- image_tag=${LOGNAME} -${repo} -${suffix}
165
+ build_image () {
166
+ local dockerfile=" $TRYCI_BUILD_CTXT /$1 "
167
+ # Extract the dockerfile suffix. E.g. for '.buildbot_dockerfile_myrepo'
168
+ # it's 'myrepo'.
169
+ local suffix=$( echo " $1 " | sed -e " s/^${TRYCI_DOCKERFILE_BASE} //" )
45
170
46
- # The container will be run as the worker's "host user". The image is
47
- # expected to create a user with the same UID.
171
+ # Create a unique image tag so that old docker image builds can be reused.
172
+ image_tag= ${LOGNAME} - ${TRYCI_BUILD_PREFIX} - ${suffix}
48
173
ci_uid=` id -u`
49
174
50
- # Build an image for the CI job.
51
- docker build --build-arg CI_UID=${ci_uid} --build-arg CI_RUNNER=tryci -t ${image_tag} --file $1 .
175
+ docker build \
176
+ --build-arg CI_UID=" ${ci_uid} " \
177
+ --build-arg CI_RUNNER=tryci \
178
+ -t " ${image_tag} " \
179
+ --file " ${dockerfile} " \
180
+ " ${TRYCI_BUILD_CTXT} "
181
+
182
+ container_tag=$( docker create \
183
+ ${CAP_ARGS} \
184
+ -u " ${ci_uid} " \
185
+ -v /opt/ykllvm_cache:/opt/ykllvm_cache:ro \
186
+ " ${image_tag} " )
187
+ }
52
188
53
- # Run the CI job.
54
- #
55
- # We run the container with CAP_PERFMON capabilities to
56
- # allow perf_event_open() to work (for those repos requiring the use
57
- # of e.g. Intel PT).
58
- container_tag=` docker create ${CAP_ARGS} -u ${ci_uid} -v /opt/ykllvm_cache:/opt/ykllvm_cache:ro ${image_tag} `
189
+ run_image () {
59
190
docker start -a ${container_tag}
60
191
status=$?
61
192
@@ -80,17 +211,9 @@ run_image() {
80
211
return ${status}
81
212
}
82
213
83
- usage () {
84
- echo " Runs a soft-dev CI job."
85
- echo " Must be run from the same directory as the job's .buildbot.sh file."
86
- echo " usage: tryci [-p] [-r server_name]"
87
- echo " -p, --post-mortem Attach a shell to the image to prod around if the build fails."
88
- echo " -r, --remote server_name Specify the server server_name to run the CI job on over SSH."
89
- }
90
-
91
- # Parse arguments
92
214
pm=0
93
215
server=" "
216
+ ref=" "
94
217
95
218
while [ $# -gt 0 ]; do
96
219
case $1 in
@@ -100,8 +223,15 @@ while [ $# -gt 0 ]; do
100
223
;;
101
224
-r | --remote)
102
225
server=" $2 "
103
- shift
104
- shift
226
+ shift 2
227
+ ;;
228
+ -c | --checkout)
229
+ ref=" $2 "
230
+ shift 2
231
+ ;;
232
+ -h | --help)
233
+ usage
234
+ exit 0
105
235
;;
106
236
* )
107
237
usage
@@ -110,17 +240,35 @@ while [ $# -gt 0 ]; do
110
240
esac
111
241
done
112
242
243
+ if [ ! docker buildx version & > /dev/null ] && [ -z ${server} ]; then
244
+ error " Docker Buildx is not installed or not available in your PATH" .
245
+ error " For installation instructions, visit: https://docs.docker.com/buildx/working-with-buildx/"
246
+ exit 1
247
+ fi
248
+
113
249
if [ ! -z ${server} ]; then
114
250
export DOCKER_HOST=" ssh://${server} "
115
251
fi
116
252
253
+ # Start by getting the build context for this CI job.
254
+ resolve_build_ctxt
117
255
118
- # Collect dockerfiles to test inside of.
119
- ci_dockerfiles=` ls .buildbot_dockerfile_* 2> /dev/null || true`
256
+ TRYCI_DOCKERFILES=$(
257
+ find " $TRYCI_BUILD_CTXT " \
258
+ -name " $TRYCI_DOCKERFILE_BASE *" \
259
+ -maxdepth 1 \
260
+ -type f \
261
+ -exec basename {} \; \
262
+ 2> /dev/null
263
+ )
264
+
265
+ if [ ! -f " $TRYCI_BUILD_CTXT /$TRYCI_DEFAULT_SCRIPT " ]; then
266
+ error " ${TRYCI_DEFAULT_SCRIPT} not found in repository."
267
+ fi
120
268
121
269
# If the repo doesn't define any images, then use the default image.
122
- if [ " ${ci_dockerfiles } " = " " ]; then
123
- cat << EOF > ${DEFAULT_DOCKERFILE }
270
+ if [ " ${TRYCI_DOCKERFILES } " = " " ]; then
271
+ cat << EOF > ${TRYCI_DEFAULT_DOCKERFILE }
124
272
FROM debian:bullseye
125
273
ARG CI_UID
126
274
RUN useradd -m -u \$ {CI_UID} ci
@@ -129,9 +277,9 @@ if [ "${ci_dockerfiles}" = "" ]; then
129
277
WORKDIR /ci
130
278
RUN chown \$ {CI_UID}:\$ {CI_UID} .
131
279
COPY --chown=\$ {CI_UID}:\$ {CI_UID} . .
132
- CMD sh -x .buildbot.sh
280
+ CMD sh -x ${TRYCI_DEFAULT_SCRIPT}
133
281
EOF
134
- ci_dockerfiles =${DEFAULT_DOCKERFILE }
282
+ TRYCI_DOCKERFILES =${TRYCI_DEFAULT_DOCKERFILE }
135
283
fi
136
284
137
285
# Sequentially run the images.
141
289
# buildbot run separate jobs in parallel.
142
290
num_failed=0
143
291
failed_dockerfiles=" "
144
- for dockerfile in ${ci_dockerfiles } ; do
292
+ for dockerfile in ${TRYCI_DOCKERFILES } ; do
145
293
echo " CI> Running ${dockerfile} ..."
146
294
rc=0
147
- run_image ${dockerfile} || rc=$?
295
+ build_image ${dockerfile}
296
+ run_image $container_tag || rc=$?
148
297
if [ $rc -eq 0 ]; then
149
298
echo " CI> ${dockerfile} : [ OK ]"
150
299
else
0 commit comments