diff --git a/.actlignore b/.actlignore new file mode 100644 index 0000000..a24a0ad --- /dev/null +++ b/.actlignore @@ -0,0 +1,8 @@ +# Large trajectory outputs should live on /mnt/diffuse-shared (or the pod home +# PVC), not in the synced source checkout. Keep source-controlled templates such +# as artifacts/*.mdp synced. +*.cpt +*.dcd +*.edr +*.trr +*.xtc diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml new file mode 100644 index 0000000..6972499 --- /dev/null +++ b/.github/workflows/build-images.yml @@ -0,0 +1,163 @@ +name: Build & push md-workflows images + +# Builds the three-stage image chain on the self-hosted Astera builder and pushes to Harbor. +# +# Security: this workflow has NO pull_request trigger, so fork PRs can never reach the +# self-hosted runner or the Harbor credentials. Heavy builds run only on push to `astera` +# (path-filtered) and manual dispatch. Credentials live in the `harbor` GitHub Environment, +# which must be restricted to the `astera` deployment branch; the `astera-sh-builder` runner +# should sit in a runner group scoped to this repo. See the runbook in the PR description. +on: + push: + branches: [astera] + paths: + - Dockerfile.base + - Dockerfile.gromacs + - Dockerfile.actl + - pyproject.toml + - md_workflows/** + - .github/workflows/build-images.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: build-images-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: harbor.astera.sh + # base + gromacs (bake the non-redistributable ChimeraX) live in the proprietary project, + # distinguished by a stage tag prefix. The final consumable actl image lands in library. + PROPRIETARY_IMAGE: harbor.astera.sh/diffuse-proprietary/md-workflows + LIBRARY_IMAGE: harbor.astera.sh/library/md-workflows + +jobs: + version: + name: Compute version + runs-on: ubuntu-latest + outputs: + semver: ${{ steps.v.outputs.semver }} + build: ${{ steps.v.outputs.build }} + sha: ${{ steps.v.outputs.sha }} + steps: + - uses: actions/checkout@v4 + - id: v + name: Derive version from pyproject.toml + run: | + set -euo pipefail + SEMVER=$(grep -m1 -E '^version[[:space:]]*=' pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') + if [ -z "${SEMVER}" ]; then echo "could not parse version from pyproject.toml" >&2; exit 1; fi + SHORT="${GITHUB_SHA::7}" + echo "semver=${SEMVER}" >> "$GITHUB_OUTPUT" + echo "build=${SEMVER}-b${{ github.run_number }}" >> "$GITHUB_OUTPUT" + echo "sha=sha-${SHORT}" >> "$GITHUB_OUTPUT" + echo "Resolved: ${SEMVER} / ${SEMVER}-b${{ github.run_number }} / sha-${SHORT}" + + base: + name: Build base + needs: version + runs-on: [self-hosted, diffuse-sh-builder] + environment: harbor + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - id: build + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.base + platforms: linux/amd64 + push: true + provenance: false + tags: | + ${{ env.PROPRIETARY_IMAGE }}:base-${{ needs.version.outputs.semver }} + ${{ env.PROPRIETARY_IMAGE }}:base-${{ needs.version.outputs.build }} + ${{ env.PROPRIETARY_IMAGE }}:base-${{ needs.version.outputs.sha }} + ${{ env.PROPRIETARY_IMAGE }}:base + cache-from: type=registry,ref=${{ env.PROPRIETARY_IMAGE }}:base-buildcache + cache-to: type=registry,ref=${{ env.PROPRIETARY_IMAGE }}:base-buildcache,mode=max + + gromacs: + name: Build gromacs + needs: [version, base] + runs-on: [self-hosted, diffuse-sh-builder] + environment: harbor + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - id: build + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.gromacs + platforms: linux/amd64 + push: true + provenance: false + build-args: | + BASE_IMAGE=${{ env.PROPRIETARY_IMAGE }}@${{ needs.base.outputs.digest }} + tags: | + ${{ env.PROPRIETARY_IMAGE }}:gromacs-${{ needs.version.outputs.semver }} + ${{ env.PROPRIETARY_IMAGE }}:gromacs-${{ needs.version.outputs.build }} + ${{ env.PROPRIETARY_IMAGE }}:gromacs-${{ needs.version.outputs.sha }} + ${{ env.PROPRIETARY_IMAGE }}:gromacs + cache-from: type=registry,ref=${{ env.PROPRIETARY_IMAGE }}:gromacs-buildcache + cache-to: type=registry,ref=${{ env.PROPRIETARY_IMAGE }}:gromacs-buildcache,mode=max + + actl: + name: Build actl (consumable) + needs: [version, gromacs] + runs-on: [self-hosted, diffuse-sh-builder] + environment: harbor + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.HARBOR_USERNAME }} + password: ${{ secrets.HARBOR_PASSWORD }} + - id: build + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.actl + platforms: linux/amd64 + push: true + provenance: false + build-args: | + GROMACS_IMAGE=${{ env.PROPRIETARY_IMAGE }}@${{ needs.gromacs.outputs.digest }} + tags: | + ${{ env.LIBRARY_IMAGE }}:${{ needs.version.outputs.semver }} + ${{ env.LIBRARY_IMAGE }}:${{ needs.version.outputs.build }} + ${{ env.LIBRARY_IMAGE }}:${{ needs.version.outputs.sha }} + ${{ env.LIBRARY_IMAGE }}:actl + ${{ env.LIBRARY_IMAGE }}:latest + cache-from: type=registry,ref=${{ env.LIBRARY_IMAGE }}:actl-buildcache + cache-to: type=registry,ref=${{ env.LIBRARY_IMAGE }}:actl-buildcache,mode=max + - name: Report digests + run: | + echo "base digest: ${{ needs.base.outputs.digest }}" || true + echo "gromacs digest: ${{ needs.gromacs.outputs.digest }}" + echo "actl digest: ${{ steps.build.outputs.digest }}" + echo "Published ${{ env.LIBRARY_IMAGE }}:${{ needs.version.outputs.build }}" diff --git a/Dockerfile.actl b/Dockerfile.actl new file mode 100644 index 0000000..38dc8c9 --- /dev/null +++ b/Dockerfile.actl @@ -0,0 +1,92 @@ +# syntax=docker/dockerfile:1 +# ==================== md-workflows Astera ACTL overlay (stage 3 of 3) ==================== +# Layers Astera workspace conventions on top of the GROMACS stage (Dockerfile.gromacs): the +# /home/dev persisted home, editor/sync/debug tools, root-oriented interactive pods, and the +# global shell init that keeps the baked lunus env active. The scientific stack (CUDA 12.6, +# GROMACS sm_90 + AVX_512, md-workflows) is inherited unchanged from the gromacs image. +# +# Build locally (after building md-gromacs:local): +# docker buildx build --platform linux/amd64 \ +# -f Dockerfile.actl --build-arg GROMACS_IMAGE=md-gromacs:local \ +# -t harbor.astera.sh/library/md-workflows:local-actl . +ARG GROMACS_IMAGE=md-gromacs:local +FROM ${GROMACS_IMAGE} AS actl + +USER root + +ARG ACTL_PACKAGES="bash ca-certificates curl wget rsync tini vim nano emacs-nox git zsh htop tmux ncdu iputils-ping dnsutils" + +ENV DEBIAN_FRONTEND=noninteractive \ + HOME=/home/dev \ + XDG_CONFIG_HOME=/home/dev/.config \ + XDG_CACHE_HOME=/home/dev/.cache \ + XDG_DATA_HOME=/home/dev/.local/share \ + SHELL=/bin/bash \ + MAMBA_ROOT_PREFIX=/opt/micromamba \ + MAMBA_EXE=/opt/micromamba/bin/micromamba \ + CONDA_PREFIX=/opt/micromamba/envs/lunus \ + PYTHONPATH=/home/dev/workspace:/opt/md-workflows \ + PATH="/opt/micromamba/bin:/opt/micromamba/envs/lunus/bin:/usr/local/gromacs/bin:${PATH}" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ${ACTL_PACKAGES} \ + bc \ + bzip2 \ + coreutils \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean \ + && mkdir -p /home/dev/.config /home/dev/.cache /home/dev/.local/share /home/dev/workspace /etc/zsh \ + && cat > /usr/local/share/actl-md-workflows-shell-init.sh <<'EOF' +# Keep the baked md-workflows/lunus environment active while letting ACTL's +# synced checkout at /home/dev/workspace override the baked package for edits. +if [ -d /opt/micromamba/envs/lunus/bin ]; then + case ":${PATH}:" in + *:/opt/micromamba/envs/lunus/bin:*) ;; + *) PATH="/opt/micromamba/envs/lunus/bin:${PATH}" ;; + esac + export PATH + CONDA_PREFIX=/opt/micromamba/envs/lunus + export CONDA_PREFIX +fi + +if [ -x /opt/micromamba/bin/micromamba ]; then + case "${ZSH_VERSION:+zsh}${BASH_VERSION:+bash}" in + zsh*) eval "$(/opt/micromamba/bin/micromamba shell hook --shell zsh 2>/dev/null)" || true ;; + *bash*) eval "$(/opt/micromamba/bin/micromamba shell hook --shell bash 2>/dev/null)" || true ;; + esac +fi + +if [ -d /home/dev/workspace ]; then + case ":${PYTHONPATH:-}:" in + *:/home/dev/workspace:*) ;; + *) PYTHONPATH="/home/dev/workspace${PYTHONPATH:+:${PYTHONPATH}}" ;; + esac +fi +if [ -d /opt/md-workflows ]; then + case ":${PYTHONPATH:-}:" in + *:/opt/md-workflows:*) ;; + *) PYTHONPATH="${PYTHONPATH:+${PYTHONPATH}:}/opt/md-workflows" ;; + esac +fi +export PYTHONPATH + +case "$-" in + *i*) + if [ -d /home/dev/workspace ]; then + cd /home/dev/workspace || true + fi + ;; +esac +EOF +RUN chmod 0644 /usr/local/share/actl-md-workflows-shell-init.sh \ + && printf '\n# Astera md-workflows environment\n[ -r /usr/local/share/actl-md-workflows-shell-init.sh ] && . /usr/local/share/actl-md-workflows-shell-init.sh\n' >> /etc/bash.bashrc \ + && printf '\n# Astera md-workflows environment\n[ -r /usr/local/share/actl-md-workflows-shell-init.sh ] && . /usr/local/share/actl-md-workflows-shell-init.sh\n' >> /etc/zsh/zshrc \ + && for cmd in \ + md_workflows.mdmx gmx micromamba curl wget rsync tini vim nano emacs git zsh htop tmux ncdu ping dig; do \ + command -v "${cmd}" >/dev/null; \ + done + +WORKDIR /home/dev +SHELL ["/bin/bash", "-c"] +CMD ["bash"]