Skip to content

Commit 7bc9058

Browse files
authored
Merge pull request #8 from jacob-hughes/ci_checkouts
Add a --checkout option to tweak which jobs are run
2 parents 6311f71 + a1dad5d commit 7bc9058

File tree

2 files changed

+228
-47
lines changed

2 files changed

+228
-47
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,38 @@ can prod around inside the container.
2727

2828
The docker image and container for the build are removed before the script exits.
2929

30+
## Checking out a specific CI job
31+
32+
By default, `TryCI` uses the working tree as the build context for the CI job.
33+
This works well for iterating on features, but can be annoying when you want to
34+
debug a remote CI job which failed.
35+
36+
You can specify which version of the repo you want to `TryCI` with the
37+
`-c/--checkout <ref>` option. `<ref>` can be either a branch, commit, or tag of
38+
the local repository; or a remote URL of the git repository. Here are some
39+
examples:
40+
41+
```sh
42+
# run tryci on the local HEAD
43+
tryci -c HEAD
44+
45+
# run tryci on the local feature-branch
46+
tryci -c feature-branch
47+
48+
# run tryci on the master branch of jacob-hughes/yk
49+
tryci -c https://github.com/jacob-hughes/yk
50+
51+
# run tryci on the 'trying' branch of ykjit/yk
52+
tryci -c https://github.com/ykjit/yk#trying
53+
54+
# run tryci on a specified commit ykjit/yk
55+
tryci -c https://github.com/ykjit/yk#0a6902a
56+
```
57+
58+
This works with the `--post-mortem` flag, so -- provided docker is installed --
59+
you can even use `TryCI` to prod around in a CI job on machines which you
60+
haven't cloned the original repo or setup to develop on.
61+
3062
## Troubleshooting
3163

3264
* Docker must be installed on both the local machine and remote machine used to

tryci

Lines changed: 196 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,181 @@
1212
# It is assumed that the user invoking this script has permissions to use
1313
# docker. On Linux this means the user must be in the `docker` group.
1414

15-
DEFAULT_DOCKERFILE=.buildbot_dockerfile_default
1615

1716
# Arguments to enable PT and rr support in docker.
1817
CAP_ARGS="--cap-add CAP_PERFMON --cap-add SYS_PTRACE --security-opt seccomp=unconfined"
1918

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=""
2026

2127
set -e
2228

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.
2743
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'
3199
#
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/_$//')
36120
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}"
41161
fi
162+
TRYCI_BUILD_PREFIX="$prefix${tag:+":$tag"}"
163+
}
42164

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}//")
45170

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}
48173
ci_uid=`id -u`
49174

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+
}
52188

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() {
59190
docker start -a ${container_tag}
60191
status=$?
61192

@@ -80,17 +211,9 @@ run_image() {
80211
return ${status}
81212
}
82213

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
92214
pm=0
93215
server=""
216+
ref=""
94217

95218
while [ $# -gt 0 ]; do
96219
case $1 in
@@ -100,8 +223,15 @@ while [ $# -gt 0 ]; do
100223
;;
101224
-r | --remote)
102225
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
105235
;;
106236
*)
107237
usage
@@ -110,17 +240,35 @@ while [ $# -gt 0 ]; do
110240
esac
111241
done
112242

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+
113249
if [ ! -z ${server} ]; then
114250
export DOCKER_HOST="ssh://${server}"
115251
fi
116252

253+
# Start by getting the build context for this CI job.
254+
resolve_build_ctxt
117255

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
120268

121269
# 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}
124272
FROM debian:bullseye
125273
ARG CI_UID
126274
RUN useradd -m -u \${CI_UID} ci
@@ -129,9 +277,9 @@ if [ "${ci_dockerfiles}" = "" ]; then
129277
WORKDIR /ci
130278
RUN chown \${CI_UID}:\${CI_UID} .
131279
COPY --chown=\${CI_UID}:\${CI_UID} . .
132-
CMD sh -x .buildbot.sh
280+
CMD sh -x ${TRYCI_DEFAULT_SCRIPT}
133281
EOF
134-
ci_dockerfiles=${DEFAULT_DOCKERFILE}
282+
TRYCI_DOCKERFILES=${TRYCI_DEFAULT_DOCKERFILE}
135283
fi
136284

137285
# Sequentially run the images.
@@ -141,10 +289,11 @@ fi
141289
# buildbot run separate jobs in parallel.
142290
num_failed=0
143291
failed_dockerfiles=""
144-
for dockerfile in ${ci_dockerfiles}; do
292+
for dockerfile in ${TRYCI_DOCKERFILES}; do
145293
echo "CI> Running ${dockerfile}..."
146294
rc=0
147-
run_image ${dockerfile} || rc=$?
295+
build_image ${dockerfile}
296+
run_image $container_tag || rc=$?
148297
if [ $rc -eq 0 ]; then
149298
echo "CI> ${dockerfile}: [ OK ]"
150299
else

0 commit comments

Comments
 (0)