Skip to content

Commit 6223999

Browse files
Merge pull request #5 from runtimeverification/raoul/release-workflow
Add release workflow publishing to Nix caches and Docker Hub
2 parents 84de11e + 005fbd5 commit 6223999

6 files changed

Lines changed: 329 additions & 0 deletions

File tree

.dockerignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Build context hygiene: keep local dev cruft out of the Docker image.
2+
# Anything the package actually needs (src/, pyproject.toml, uv.lock) is kept.
3+
.git
4+
.github
5+
.devcontainer
6+
.direnv
7+
.venv
8+
.mypy_cache
9+
.pytest_cache
10+
**/__pycache__
11+
*.pyc
12+
dist
13+
cov-*
14+
.coverage
15+
result
16+
result-*
17+
state.kore
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Verifies that `kup publish` actually pushed AND pinned komet-node in the
5+
# private k-framework-binary cache. Cachix has intermittently dropped the
6+
# `cachix pin` requests kup makes under the hood, so we confirm both the pin
7+
# (visible via the pin API) and the narinfo (the pushed store path) before
8+
# treating the publish as successful.
9+
CACHE="k-framework-binary"
10+
OWNER_REPO="${OWNER_REPO:-$(git remote get-url origin | sed -E 's#(git@github.com:|https://github.com/)##; s#\.git$##')}"
11+
REV="${REV:-${GITHUB_SHA:-$(git rev-parse HEAD)}}"
12+
UNAME_S="$(uname -s)"
13+
UNAME_M="$(uname -m)"
14+
case "${UNAME_S}-${UNAME_M}" in
15+
Linux-x86_64) SYSTEM="x86_64-linux" ;;
16+
Linux-aarch64 | Linux-arm64) SYSTEM="aarch64-linux" ;;
17+
Darwin-x86_64) SYSTEM="x86_64-darwin" ;;
18+
Darwin-arm64) SYSTEM="aarch64-darwin" ;;
19+
*)
20+
echo "Unsupported platform: ${UNAME_S}-${UNAME_M}" >&2
21+
exit 1
22+
;;
23+
esac
24+
PIN_API_URL="https://app.cachix.org/api/v1/cache/${CACHE}/pin"
25+
CHECK_PACKAGES=(komet-node)
26+
27+
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/stdout}"
28+
29+
# Append to the GitHub step summary when set; always print to stdout for live job logs.
30+
summary_and_log() {
31+
if [[ "${SUMMARY}" == "/dev/stdout" ]]; then
32+
cat
33+
else
34+
tee -a "${SUMMARY}"
35+
fi
36+
}
37+
38+
{
39+
echo "## Cachix Publish Summary"
40+
echo "CACHE: $CACHE"
41+
echo "OWNER_REPO: $OWNER_REPO"
42+
echo "REV: $REV"
43+
echo "SYSTEM: $SYSTEM"
44+
echo "PACKAGES: ${CHECK_PACKAGES[*]}"
45+
} >> "$SUMMARY"
46+
47+
# Verify push + pin together for each package. Both can become visible with delay.
48+
PIN_VISIBILITY_TIMEOUT_SECONDS=120 # 2 minutes
49+
PIN_VISIBILITY_INTERVAL_SECONDS=5 # 5 seconds
50+
PIN_VISIBILITY_ATTEMPTS=$((PIN_VISIBILITY_TIMEOUT_SECONDS / PIN_VISIBILITY_INTERVAL_SECONDS))
51+
for i in $(seq 1 "$PIN_VISIBILITY_ATTEMPTS"); do
52+
# `|| true` keeps `set -e` from aborting the whole script on a transient
53+
# network/API hiccup: an empty/failed response is treated as not-ready and
54+
# retried, rather than failing the release outright.
55+
PIN_JSON="$(curl -fsSL "${PIN_API_URL}?q=${REV}" || true)"
56+
if [ -z "$PIN_JSON" ]; then
57+
printf 'cachix-check-attempt-%s: pin-api-unreachable, retrying in %ss\n' "$i" "$PIN_VISIBILITY_INTERVAL_SECONDS" | summary_and_log
58+
sleep "$PIN_VISIBILITY_INTERVAL_SECONDS"
59+
continue
60+
fi
61+
ALL_OK=1
62+
63+
for PKG in "${CHECK_PACKAGES[@]}"; do
64+
KEY="github:${OWNER_REPO}/${REV}#packages.${SYSTEM}.${PKG}"
65+
STORE_PATH="$(
66+
echo "$PIN_JSON" \
67+
| jq -r --arg k "$KEY" 'map(select(.name == $k)) | first | (.lastRevision.storePath // .storePath // .store_path // .path // "")'
68+
)"
69+
if [ -z "$STORE_PATH" ]; then
70+
PIN_STATUS="pin-missing"
71+
PUSH_STATUS="000"
72+
ALL_OK=0
73+
{
74+
echo "key-${PKG}: ${KEY}"
75+
echo "pin-status-${PKG}: ${PIN_STATUS}"
76+
echo "push-http-${PKG}: ${PUSH_STATUS}"
77+
} | summary_and_log
78+
continue
79+
fi
80+
81+
PIN_STATUS="pin-ok"
82+
HASH="$(basename "$STORE_PATH" | cut -d- -f1)"
83+
PUSH_NARINFO_URL="https://${CACHE}.cachix.org/${HASH}.narinfo"
84+
PUSH_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' "$PUSH_NARINFO_URL")" || PUSH_STATUS="000"
85+
if [ "$PUSH_STATUS" != "200" ]; then
86+
ALL_OK=0
87+
fi
88+
89+
{
90+
echo "key-${PKG}: ${KEY}"
91+
echo "store-path-${PKG}: ${STORE_PATH}"
92+
echo "pin-status-${PKG}: ${PIN_STATUS}"
93+
echo "push-http-${PKG}: ${PUSH_STATUS}"
94+
} | summary_and_log
95+
done
96+
97+
if [ "$ALL_OK" = "1" ]; then
98+
echo "cachix-status: push-and-pin-ok-for-all-packages" >> "$SUMMARY"
99+
exit 0
100+
fi
101+
102+
RETRY_MSG="cachix-check-attempt-${i}: not-ready, retrying in ${PIN_VISIBILITY_INTERVAL_SECONDS}s"
103+
printf '%s\n' "$RETRY_MSG" | summary_and_log
104+
sleep "$PIN_VISIBILITY_INTERVAL_SECONDS"
105+
done
106+
107+
echo "cachix-status: push-or-pin-missing-after-${PIN_VISIBILITY_TIMEOUT_SECONDS}s-for-at-least-one-package" >> "$SUMMARY"
108+
# Pin API bulk JSON goes to job logs only (step summary stays readable); helps if the response shape changes.
109+
echo "check-cachix-pin: raw Cachix pin API response (last fetch):" >&2
110+
echo "$PIN_JSON" >&2
111+
exit 1

.github/workflows/release.yml

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
---
2+
name: 'Release'
3+
on:
4+
push:
5+
branches:
6+
- 'main'
7+
jobs:
8+
9+
draft-release:
10+
name: 'Draft Release'
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: 'Check out code'
14+
uses: actions/checkout@v4
15+
- name: 'Make release'
16+
id: 'make-release'
17+
env:
18+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19+
run: |
20+
set -x
21+
VERSION=v$(cat package/version)
22+
gh release create "${VERSION}" \
23+
--repo "${GITHUB_REPOSITORY}" \
24+
--draft \
25+
--title "${VERSION}" \
26+
--target ${{ github.sha }}
27+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
28+
29+
nix-cache:
30+
name: 'Populate Nix Caches'
31+
strategy:
32+
matrix:
33+
runner: [normal, ARM64]
34+
runs-on: ${{ matrix.runner }}
35+
needs: draft-release
36+
steps:
37+
- name: 'Check out code'
38+
uses: actions/checkout@v4
39+
with:
40+
ref: ${{ github.sha }}
41+
fetch-depth: 0
42+
43+
# Build the komet-node derivation once, then push it to both the public
44+
# k-framework cache (full build closure, via `cachix push`) and the
45+
# private k-framework-binary cache (the kup-installable binary, via
46+
# `kup publish`). Both pushes reuse the build output already present in
47+
# this runner's Nix store, so the derivation is never built twice.
48+
- name: 'Build and publish komet-node to both Nix caches'
49+
uses: workflow/nix-shell-action@v3
50+
env:
51+
GC_DONT_GC: '1'
52+
# Enable flakes for every nix invocation in this step (the initial
53+
# build, the kup build, and the builds kup runs internally) so the
54+
# job does not depend on flakes being globally enabled on the runner.
55+
NIX_CONFIG: 'extra-experimental-features = nix-command flakes'
56+
CACHIX_PUBLIC_TOKEN: '${{ secrets.CACHIX_PUBLIC_TOKEN }}'
57+
CACHIX_PRIVATE_KFB_TOKEN: '${{ secrets.CACHIX_PRIVATE_KFB_TOKEN }}'
58+
OWNER_REPO: '${{ github.repository }}'
59+
REV: '${{ github.sha }}'
60+
with:
61+
packages: jq
62+
script: |
63+
KOMET_NODE=$(nix build .#komet-node --json | jq -r '.[].outputs | to_entries[].value')
64+
DRV=$(nix-store --query --deriver ${KOMET_NODE})
65+
66+
# Push the full build closure to the public k-framework cache.
67+
export CACHIX_AUTH_TOKEN="${CACHIX_PUBLIC_TOKEN}"
68+
nix-store --query --requisites --include-outputs ${DRV} | cachix push k-framework
69+
70+
# Publish the binary to the private k-framework-binary cache. kup
71+
# reuses the store paths built above.
72+
export CACHIX_AUTH_TOKEN="${CACHIX_PRIVATE_KFB_TOKEN}"
73+
export PATH="$(nix build github:runtimeverification/kup --no-link --json | jq -r '.[].outputs | to_entries[].value')/bin:$PATH"
74+
kup publish k-framework-binary .#komet-node --keep-days 180
75+
# Cachix has not been responding to 'cachix pin' requests made under the hood by kup. Verify the push and pin manually.
76+
.github/scripts/check-cachix-pin.sh
77+
78+
- name: 'On failure, delete drafted release'
79+
if: failure()
80+
env:
81+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82+
run: |
83+
set -x
84+
VERSION=v$(cat package/version)
85+
gh release delete "${VERSION}" \
86+
--repo "${GITHUB_REPOSITORY}" \
87+
--yes \
88+
--cleanup-tag
89+
90+
dockerhub:
91+
name: 'Build and Publish Docker Image'
92+
runs-on: [self-hosted, linux, normal]
93+
needs: draft-release
94+
steps:
95+
- name: 'Check out code'
96+
uses: actions/checkout@v4
97+
with:
98+
ref: ${{ github.sha }}
99+
fetch-depth: 0
100+
101+
- name: 'Set environment'
102+
run: |
103+
KOMET_NODE_VERSION=$(cat package/version)
104+
TAG=runtimeverificationinc/komet-node:ubuntu-jammy-${KOMET_NODE_VERSION}
105+
echo "TAG=${TAG}" >> ${GITHUB_ENV}
106+
107+
- name: 'Build Docker image'
108+
run: |
109+
K_VERSION=$(cat deps/k_release)
110+
docker build . --no-cache --tag ${TAG} --build-arg K_VERSION=${K_VERSION}
111+
112+
- name: 'Run Docker image'
113+
run: docker run --rm ${TAG} komet-node --help
114+
115+
- name: 'Push Docker image to Docker Hub'
116+
run: |
117+
echo "${{ secrets.DOCKERHUB_PASSWORD }}" | docker login --username rvdockerhub --password-stdin
118+
docker image push ${TAG}
119+
120+
- name: 'On failure, delete drafted release'
121+
if: failure()
122+
env:
123+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
124+
run: |
125+
set -x
126+
VERSION=v$(cat package/version)
127+
gh release delete "${VERSION}" \
128+
--repo "${GITHUB_REPOSITORY}" \
129+
--yes \
130+
--cleanup-tag
131+
132+
cut-release:
133+
name: 'Cut Release'
134+
runs-on: ubuntu-latest
135+
needs: [dockerhub, nix-cache]
136+
steps:
137+
- name: 'Check out code'
138+
uses: actions/checkout@v4
139+
with:
140+
ref: ${{ github.sha }}
141+
fetch-depth: 0
142+
143+
- name: 'Finalize Release'
144+
env:
145+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
146+
run: |
147+
set -x
148+
VERSION=v$(cat package/version)
149+
gh release edit "${VERSION}" \
150+
--repo "${GITHUB_REPOSITORY}" \
151+
--draft=false

Dockerfile

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# komet-node runtime image.
2+
#
3+
# Built on the K Framework base image (which already bundles K and a matching
4+
# Z3), then layered with the komet-node Python package and its kompiled K
5+
# semantics. komet-node only needs `wat2wasm` (wabt) at runtime to turn the
6+
# `.wat` test contracts into wasm -- it does not invoke stellar-cli/soroban or
7+
# cargo, so the Rust/Soroban toolchain is intentionally left out to keep the
8+
# image small.
9+
#
10+
# K_VERSION is supplied by the release workflow from deps/k_release.
11+
ARG K_VERSION
12+
FROM runtimeverificationinc/kframework-k:ubuntu-jammy-${K_VERSION}
13+
14+
ARG PYTHON_VERSION=3.10
15+
16+
RUN apt-get -y update \
17+
&& apt-get -y install --no-install-recommends \
18+
curl \
19+
git \
20+
graphviz \
21+
python${PYTHON_VERSION} \
22+
python${PYTHON_VERSION}-dev \
23+
python3-pip \
24+
wabt \
25+
&& apt-get -y clean \
26+
&& rm -rf /var/lib/apt/lists/*
27+
28+
ARG USER_ID=1010
29+
ARG GROUP_ID=1010
30+
RUN groupadd -g ${GROUP_ID} user \
31+
&& useradd -m -u ${USER_ID} -s /bin/bash -g user user
32+
33+
USER user
34+
WORKDIR /home/user
35+
36+
COPY --chown=user:user . komet-node
37+
38+
ENV PATH=/home/user/.local/bin:${PATH}
39+
# Installs komet-node together with its dependencies (the `komet` package, which
40+
# carries the Soroban K semantics source, and the matching `kframework` pyk).
41+
# `--user` installs into ~/.local (already on PATH) and `--no-cache-dir` keeps
42+
# pip's download cache out of the image layer.
43+
RUN pip install --no-cache-dir --user ./komet-node \
44+
&& rm -rf komet-node
45+
46+
# Pre-kompile the K semantics so the container is ready to serve immediately.
47+
# The sources come from the installed packages, so the checkout is no longer needed.
48+
RUN kdist --verbose build -j2 'komet-node.*' 'soroban-semantics.*'

deps/k_release

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7.1.319

package/version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.0

0 commit comments

Comments
 (0)