Skip to content

Commit a9cb12a

Browse files
ci: add release workflow publishing to Nix caches and Docker Hub
Add a Kontrol-style release pipeline triggered on push to the `release` branch: draft-release -> {nix-cache, dockerhub} -> cut-release - nix-cache: builds `.#komet-node` once, pushes the full closure to the public `k-framework` cachix cache and `kup publish`es it to the private `k-framework-binary` cache (matrix: normal, ARM64). - dockerhub: builds a lean image (FROM kframework-k + wabt + kdist build), smoke-tests `komet-node --help`, and pushes `runtimeverificationinc/komet-node:ubuntu-jammy-<version>`. Supporting files: package/version (version source), deps/k_release (Docker K_VERSION build arg), Dockerfile, .dockerignore, and a check-cachix-pin.sh helper that verifies the kup push+pin landed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 84de11e commit a9cb12a

6 files changed

Lines changed: 316 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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
PIN_JSON="$(curl -fsSL "${PIN_API_URL}?q=${REV}")"
53+
ALL_OK=1
54+
55+
for PKG in "${CHECK_PACKAGES[@]}"; do
56+
KEY="github:${OWNER_REPO}/${REV}#packages.${SYSTEM}.${PKG}"
57+
STORE_PATH="$(
58+
echo "$PIN_JSON" \
59+
| jq -r --arg k "$KEY" 'map(select(.name == $k)) | first | (.lastRevision.storePath // .storePath // .store_path // .path // "")'
60+
)"
61+
if [ -z "$STORE_PATH" ]; then
62+
PIN_STATUS="pin-missing"
63+
PUSH_STATUS="000"
64+
ALL_OK=0
65+
{
66+
echo "key-${PKG}: ${KEY}"
67+
echo "pin-status-${PKG}: ${PIN_STATUS}"
68+
echo "push-http-${PKG}: ${PUSH_STATUS}"
69+
} | summary_and_log
70+
continue
71+
fi
72+
73+
PIN_STATUS="pin-ok"
74+
HASH="$(basename "$STORE_PATH" | cut -d- -f1)"
75+
PUSH_NARINFO_URL="https://${CACHE}.cachix.org/${HASH}.narinfo"
76+
PUSH_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' "$PUSH_NARINFO_URL")" || PUSH_STATUS="000"
77+
if [ "$PUSH_STATUS" != "200" ]; then
78+
ALL_OK=0
79+
fi
80+
81+
{
82+
echo "key-${PKG}: ${KEY}"
83+
echo "store-path-${PKG}: ${STORE_PATH}"
84+
echo "pin-status-${PKG}: ${PIN_STATUS}"
85+
echo "push-http-${PKG}: ${PUSH_STATUS}"
86+
} | summary_and_log
87+
done
88+
89+
if [ "$ALL_OK" = "1" ]; then
90+
echo "cachix-status: push-and-pin-ok-for-all-packages" >> "$SUMMARY"
91+
exit 0
92+
fi
93+
94+
RETRY_MSG="cachix-check-attempt-${i}: not-ready, retrying in ${PIN_VISIBILITY_INTERVAL_SECONDS}s"
95+
printf '%s\n' "$RETRY_MSG" | summary_and_log
96+
sleep "$PIN_VISIBILITY_INTERVAL_SECONDS"
97+
done
98+
99+
echo "cachix-status: push-or-pin-missing-after-${PIN_VISIBILITY_TIMEOUT_SECONDS}s-for-at-least-one-package" >> "$SUMMARY"
100+
# Pin API bulk JSON goes to job logs only (step summary stays readable); helps if the response shape changes.
101+
echo "check-cachix-pin: raw Cachix pin API response (last fetch):" >&2
102+
echo "$PIN_JSON" >&2
103+
exit 1

.github/workflows/release.yml

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

Dockerfile

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 \
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+
27+
ARG USER_ID=1010
28+
ARG GROUP_ID=1010
29+
RUN groupadd -g ${GROUP_ID} user \
30+
&& useradd -m -u ${USER_ID} -s /bin/bash -g user user
31+
32+
USER user
33+
WORKDIR /home/user
34+
35+
ADD --chown=user:user . komet-node
36+
37+
ENV PATH=/home/user/.local/bin:${PATH}
38+
# Installs komet-node together with its dependencies (the `komet` package, which
39+
# carries the Soroban K semantics source, and the matching `kframework` pyk).
40+
RUN pip install ./komet-node \
41+
&& rm -rf komet-node
42+
43+
# Pre-kompile the K semantics so the container is ready to serve immediately.
44+
# The sources come from the installed packages, so the checkout is no longer needed.
45+
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)