Skip to content

Commit 2370a80

Browse files
committed
Add GitHub Actions CI
1 parent 6506ca4 commit 2370a80

18 files changed

Lines changed: 1280 additions & 22 deletions

File tree

.github/workflows/_build.yml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
name: Compiler build (reusable)
2+
3+
# Reusable: compile the MiniZinc compiler for ONE platform against the pinned
4+
# minizinc-vendor release (vendor.lock) and upload it as the `minizinc-<platform>`
5+
# artifact. Called by ci.yml (matrixed over all platforms) and benchmark.yml
6+
# (linux only). Not triggered directly.
7+
#
8+
# Same build model as minizinc-vendor: glibc-linux builds run in a manylinux
9+
# container (low glibc floor), musl in alpine, osx/win64 natively. A per-platform
10+
# `setup` installs whatever the bare base image lacks, then scripts/build.sh runs.
11+
12+
on:
13+
workflow_call:
14+
inputs:
15+
platform: { type: string, required: true }
16+
triple: { type: string, required: true }
17+
runner: { type: string, required: true }
18+
container: { type: string, default: "" }
19+
setup: { type: string, default: "" } # deps to install in the container
20+
generator: { type: string, default: "Ninja" } # manylinux -> "Unix Makefiles"
21+
22+
permissions:
23+
contents: read
24+
25+
env:
26+
# Lets `gh release download` read minizinc-vendor's (public) releases.
27+
GH_TOKEN: ${{ github.token }}
28+
29+
jobs:
30+
build:
31+
runs-on: ${{ inputs.runner }}
32+
name: build
33+
steps:
34+
- uses: actions/checkout@v7
35+
36+
- name: Fetch pinned vendor deps
37+
shell: bash
38+
run: |
39+
set -eux
40+
deps=gecode
41+
# COIN-OR publishes no ARM64 Windows CBC, so that build has no MIP
42+
# backend from CBC; find_package simply does not find it.
43+
[ "${{ inputs.triple }}" = aarch64-windows ] || deps="$deps cbc"
44+
bash scripts/fetch_vendor.sh ${{ inputs.triple }} $deps
45+
46+
- name: Set up MSVC (win64)
47+
if: startsWith(inputs.platform, 'win64')
48+
uses: ilammy/msvc-dev-cmd@v1
49+
with:
50+
arch: ${{ inputs.platform == 'win64-arm' && 'arm64' || 'x64' }}
51+
52+
- name: Configure & build
53+
shell: bash
54+
env:
55+
# MSVC reports __cplusplus as 199711L unless asked not to, so Gecode's
56+
# bundled Boost cannot pick a rounding-control implementation on ARM64,
57+
# where its x86/x64 paths do not apply. minizinc-vendor builds Gecode
58+
# itself with the same flag.
59+
CXXFLAGS: ${{ inputs.platform == 'win64-arm' && '/Zc:__cplusplus' || '' }}
60+
run: |
61+
set -eux
62+
SETUP='${{ inputs.setup }}'
63+
if [ -n "${{ inputs.container }}" ]; then
64+
docker run --rm -v "$PWD:/work" -w /work \
65+
-e ROOT=/work \
66+
-e CMAKE_GENERATOR="${{ inputs.generator }}" \
67+
-e BUILD_REF="${{ github.run_id }}" \
68+
-e EXPECT_CBC="${{ inputs.triple == 'aarch64-windows' && '0' || '1' }}" \
69+
"${{ inputs.container }}" \
70+
sh -c "${SETUP}${SETUP:+ && }bash scripts/build.sh"
71+
else
72+
ROOT="$PWD" \
73+
CMAKE_GENERATOR="${{ inputs.generator }}" \
74+
BUILD_REF="${{ github.run_id }}" \
75+
EXPECT_CBC="${{ inputs.triple == 'aarch64-windows' && '0' || '1' }}" \
76+
bash scripts/build.sh
77+
fi
78+
79+
# Catches the deployment target silently following the runner image.
80+
- name: Check macOS deployment target
81+
if: inputs.platform == 'osx'
82+
shell: bash
83+
run: vtool -show-build minizinc/bin/minizinc | grep -w "minos 12.0"
84+
85+
# Archived here, not uploaded as a tree: artifacts do not preserve the
86+
# executable bit, so a bare tree arrives unrunnable and every consumer has
87+
# to chmod it back. This tarball is also exactly what the release ships, so
88+
# it is built once and never repacked.
89+
- name: Archive
90+
shell: bash
91+
run: tar -czf "minizinc-compiler-only-${{ inputs.triple }}.tar.gz" -C minizinc .
92+
93+
- uses: actions/upload-artifact@v7
94+
with:
95+
name: minizinc-${{ inputs.platform }}
96+
path: minizinc-compiler-only-${{ inputs.triple }}.tar.gz

.github/workflows/benchmark.yml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: Benchmarks
2+
3+
# Flattening benchmarks on the SLURM cluster. GitHub-hosted runners cannot reach
4+
# it, and running a self-hosted runner on a shared academic machine is its own
5+
# maintenance burden, so the runner connects over SSH instead: it ships the
6+
# compiler across, submits the work, and copies the results back. Everything
7+
# heavy happens on the cluster.
8+
#
9+
# Secrets:
10+
# BENCH_SSH_KEY private key, no passphrase, whose public half is in
11+
# the cluster account's authorized_keys
12+
# BENCH_SSH_HOST hostname of the submit node
13+
# BENCH_SSH_USER account to connect as
14+
# BENCH_SSH_KNOWN_HOSTS output of `ssh-keyscan <host>`. Without it the
15+
# connection would be trusted blindly, which is how one
16+
# ends up benchmarking someone else's machine.
17+
#
18+
# Two steps, because these runs take hours: `submit` queues an sbatch job and
19+
# exits, and `collect` fetches the results once it has finished. Holding an SSH
20+
# session open for the duration would be at the mercy of idle timeouts, network
21+
# blips and the six-hour limit on a job, and would bill a runner for the wait.
22+
23+
on:
24+
workflow_dispatch:
25+
inputs:
26+
action:
27+
description: "submit queues the job; collect fetches a finished one."
28+
type: choice
29+
options: [submit, collect]
30+
default: submit
31+
mznlib:
32+
description: "Library to flatten against (submit)."
33+
type: choice
34+
options: [std, linear]
35+
default: std
36+
release:
37+
description: "libminizinc release to benchmark (blank = edge, submit)."
38+
type: string
39+
default: ""
40+
job:
41+
description: "Remote directory reported by submit, e.g. 12345678 (collect)."
42+
type: string
43+
default: ""
44+
45+
permissions:
46+
contents: read
47+
48+
env:
49+
GH_TOKEN: ${{ github.token }}
50+
51+
jobs:
52+
bench:
53+
name: ${{ inputs.action }} (${{ inputs.mznlib }})
54+
runs-on: ubuntu-24.04
55+
steps:
56+
- uses: actions/checkout@v7
57+
58+
- name: Set up SSH
59+
shell: bash
60+
run: |
61+
set -eu
62+
# Fail here rather than half way through a cluster job.
63+
test -n "${{ secrets.BENCH_SSH_KEY }}"
64+
test -n "${{ secrets.BENCH_SSH_KNOWN_HOSTS }}"
65+
mkdir -p ~/.ssh && chmod 700 ~/.ssh
66+
printf '%s\n' "${{ secrets.BENCH_SSH_KEY }}" > ~/.ssh/id_bench
67+
chmod 600 ~/.ssh/id_bench
68+
printf '%s\n' "${{ secrets.BENCH_SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
69+
cat >> ~/.ssh/config <<'EOF'
70+
Host bench
71+
IdentityFile ~/.ssh/id_bench
72+
IdentitiesOnly yes
73+
StrictHostKeyChecking yes
74+
BatchMode yes
75+
EOF
76+
printf ' HostName %s\n User %s\n' \
77+
"${{ secrets.BENCH_SSH_HOST }}" "${{ secrets.BENCH_SSH_USER }}" >> ~/.ssh/config
78+
79+
- name: Send the compiler and the benchmark script
80+
if: inputs.action == 'submit'
81+
shell: bash
82+
run: |
83+
set -eux
84+
ref="${{ inputs.release }}"; [ -n "$ref" ] || ref=edge
85+
gh release download "$ref" --repo "$GITHUB_REPOSITORY" \
86+
--pattern "minizinc-compiler-only-x86_64-linux-gnu.tar.gz" --dir . --clobber
87+
# One directory per run: concurrent runs must not share state, and the
88+
# cluster keeps them around for post-mortems.
89+
remote="minizinc-bench/${GITHUB_RUN_ID}"
90+
echo "REMOTE=$remote" >> "$GITHUB_ENV"
91+
ssh bench "mkdir -p '$remote'"
92+
scp minizinc-compiler-only-x86_64-linux-gnu.tar.gz "bench:$remote/"
93+
scp tests/flattening/flatten.py "bench:$remote/"
94+
95+
- name: Queue the job
96+
if: inputs.action == 'submit'
97+
shell: bash
98+
run: |
99+
set -eux
100+
# sbatch, not a foreground srun: the work outlives this workflow. The
101+
# script is written on the cluster so nothing depends on the connection.
102+
ssh bench bash -seu <<EOF
103+
cd '$REMOTE'
104+
mkdir -p minizinc results
105+
tar -xzf minizinc-compiler-only-x86_64-linux-gnu.tar.gz -C minizinc
106+
cat > job.sh <<'SCRIPT'
107+
#!/bin/bash
108+
set -eux
109+
cd "\$(dirname "\$0")"
110+
curl --retry 10 --location --silent \
111+
https://gitlab.com/-/snippets/2095682/raw/master/instances.csv | tr -d '\r' > instances.csv
112+
[ -d minizinc-benchmarks ] || git clone --depth 1 \
113+
https://github.com/minizinc/minizinc-benchmarks.git
114+
python3 flatten.py --minizinc="\$PWD/minizinc/bin/minizinc" \
115+
--prefix-args="srun -w critical001 --nice --mem=8192 -t 10" \
116+
--args="-G ${{ inputs.mznlib }}" --parallel="-1" instances.csv results/stats.csv
117+
SCRIPT
118+
chmod +x job.sh
119+
sbatch --parsable --job-name=mzn-flatten-${{ inputs.mznlib }} \
120+
--output=slurm-%j.out job.sh > jobid
121+
cat jobid
122+
EOF
123+
id=$(ssh bench "cat '$REMOTE/jobid'")
124+
{
125+
echo "### Benchmark submitted"
126+
echo
127+
echo "| | |"
128+
echo "|-|-|"
129+
echo "| SLURM job | \`$id\` |"
130+
echo "| directory | \`$REMOTE\` |"
131+
echo
132+
echo "Collect it with this workflow, action \`collect\`, job \`$GITHUB_RUN_ID\`."
133+
} >> "$GITHUB_STEP_SUMMARY"
134+
135+
- name: Collect the results
136+
if: inputs.action == 'collect'
137+
shell: bash
138+
run: |
139+
set -eux
140+
test -n "${{ inputs.job }}"
141+
remote="minizinc-bench/${{ inputs.job }}"
142+
id=$(ssh bench "cat '$remote/jobid'")
143+
# Refuse to report partial results as if they were a finished run.
144+
state=$(ssh bench "sacct -j $id -n -o State%20 | head -1 | tr -d ' '")
145+
echo "SLURM job $id is $state"
146+
[ "$state" = COMPLETED ] || { echo "job has not completed" >&2; exit 1; }
147+
scp -r "bench:$remote/results" ./results
148+
scp "bench:$remote/slurm-$id.out" ./results/
149+
150+
- uses: actions/upload-artifact@v7
151+
if: inputs.action == 'collect'
152+
with:
153+
name: bench-${{ inputs.job }}
154+
path: results/

0 commit comments

Comments
 (0)