Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: Check documentation contracts
run: node --test scripts/documentation/contracts.test.mjs

- name: Check Docker image contracts
run: node --test scripts/deployment/docker-images.test.mjs

- name: Run ESLint
run: npm run lint

Expand Down Expand Up @@ -140,25 +143,41 @@ jobs:
strategy:
fail-fast: false
matrix:
variant: [bookworm, alpine, ubuntu]
include:
- variant: bookworm-core
file: Dockerfile
target: core
profile: core
- variant: bookworm-batteries
file: Dockerfile
target: batteries
profile: batteries
- variant: alpine
file: Dockerfile.alpine
target: ""
profile: bolt
- variant: ubuntu
file: Dockerfile.ubuntu
target: ""
profile: bolt
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build and verify shipped image
run: |
file=Dockerfile
if [ "${{ matrix.variant }}" != bookworm ]; then file="Dockerfile.${{ matrix.variant }}"; fi
docker build --no-cache -f "$file" -t pabawi:test .
bash scripts/supply-chain/image-smoke.sh pabawi:test
target=()
if [ -n "${{ matrix.target }}" ]; then target=(--target "${{ matrix.target }}"); fi
docker build --no-cache "${target[@]}" -f "${{ matrix.file }}" -t pabawi:test .
bash scripts/supply-chain/image-smoke.sh pabawi:test "${{ matrix.profile }}"
mkdir -p artifacts
docker run --rm -i --entrypoint node pabawi:test < scripts/supply-chain/dependency-graph.cjs > artifacts/dependencies.json
docker build --no-cache --target backend-deps -f "$file" -t pabawi:deps-repeat .
docker build --no-cache --target backend-deps -f "${{ matrix.file }}" -t pabawi:deps-repeat .
docker run --rm -i --entrypoint node pabawi:deps-repeat < scripts/supply-chain/dependency-graph.cjs > artifacts/dependencies-repeat.json
cmp artifacts/dependencies.json artifacts/dependencies-repeat.json
if [ "${{ matrix.variant }}" = ubuntu ]; then
docker run --rm --entrypoint ruby pabawi:test /opt/bolt/test-winrm-rubyzip.rb
docker run --rm -i --entrypoint ruby pabawi:test < docker/bolt/dependency-graph.rb > artifacts/ruby-dependencies.json
docker build --no-cache --target bolt-builder -f "$file" -t pabawi:ruby-repeat .
docker build --no-cache --target bolt-builder -f "${{ matrix.file }}" -t pabawi:ruby-repeat .
docker run --rm -i --entrypoint ruby pabawi:ruby-repeat < docker/bolt/dependency-graph.rb > artifacts/ruby-dependencies-repeat.json
cmp artifacts/ruby-dependencies.json artifacts/ruby-dependencies-repeat.json
fi
Expand Down
62 changes: 44 additions & 18 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
DOCKERHUB_IMAGE: example42/pabawi

concurrency:
group: publish-${{ github.ref }}
Expand All @@ -20,6 +21,7 @@ jobs:
fail-fast: false
matrix:
arch: [amd64, arm64]
profile: [core, batteries]
permissions:
contents: read
packages: write
Expand All @@ -40,40 +42,55 @@ jobs:
with:
context: .
platforms: linux/${{ matrix.arch }}
target: ${{ matrix.profile }}
load: true
push: false
tags: pabawi:candidate
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=release-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=release-${{ matrix.arch }}
cache-from: type=gha,scope=release-${{ matrix.profile }}-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=release-${{ matrix.profile }}-${{ matrix.arch }}
- name: Verify release candidate
env:
DOCKER_DEFAULT_PLATFORM: linux/${{ matrix.arch }}
run: |
bash scripts/supply-chain/image-smoke.sh pabawi:candidate
bash scripts/supply-chain/image-smoke.sh pabawi:candidate ${{ matrix.profile }}
mkdir -p artifacts
docker run --rm -i --entrypoint node pabawi:candidate < scripts/supply-chain/dependency-graph.cjs > artifacts/dependencies.json
- name: Scan release candidate
continue-on-error: true
run: bash scripts/supply-chain/scan-image.sh pabawi:candidate artifacts
run: |
ignore_file=()
if [[ "${{ matrix.profile }}" = batteries ]]; then
ignore_file=(scripts/supply-chain/trivy-batteries-ignore.yaml)
fi
bash scripts/supply-chain/scan-image.sh pabawi:candidate artifacts "${ignore_file[@]}"
- name: Retain release evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: release-evidence-${{ matrix.arch }}
name: release-evidence-${{ matrix.profile }}-${{ matrix.arch }}
path: artifacts/*.json
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push the verified image without rebuilding
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push the verified image to both registries without rebuilding
id: push
run: |
tag="${REGISTRY}/${IMAGE_NAME}:${GITHUB_REF_NAME}-${{ matrix.arch }}"
docker tag pabawi:candidate "$tag"
docker push "$tag"
digest=$(docker inspect --format '{{index .RepoDigests 0}}' "$tag")
suffix=""
if [[ "${{ matrix.profile }}" = batteries ]]; then suffix="-batteries"; fi
architecture_tag="${GITHUB_REF_NAME}${suffix}-${{ matrix.arch }}"
ghcr_tag="${REGISTRY}/${IMAGE_NAME}:${architecture_tag}"
dockerhub_tag="${DOCKERHUB_IMAGE}:${architecture_tag}"
docker tag pabawi:candidate "$ghcr_tag"
docker tag pabawi:candidate "$dockerhub_tag"
docker push "$ghcr_tag"
docker push "$dockerhub_tag"
digest=$(docker inspect --format '{{index .RepoDigests 0}}' "$ghcr_tag")
echo "digest=${digest#*@}" >> "$GITHUB_OUTPUT"
- name: Attest the verified image
uses: actions/attest-build-provenance@v1
Expand All @@ -94,16 +111,25 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Publish verified architecture images
run: |
version="${GITHUB_REF_NAME#v}"
image="${REGISTRY}/${IMAGE_NAME}"
tags=(--tag "$image:$version" --tag "$image:sha-${GITHUB_SHA:0:7}")
if [[ "$version" != *-* ]]; then
tags+=(--tag "$image:${version%.*}" --tag "$image:${version%%.*}" --tag "$image:latest")
fi
docker buildx imagetools create "${tags[@]}" \
"$image:${GITHUB_REF_NAME}-amd64" "$image:${GITHUB_REF_NAME}-arm64"
for image in "${REGISTRY}/${IMAGE_NAME}" "${DOCKERHUB_IMAGE}"; do
core_tags=(--tag "$image:$version" --tag "$image:sha-${GITHUB_SHA:0:7}")
batteries_tags=(--tag "$image:batteries-$version" --tag "$image:batteries-sha-${GITHUB_SHA:0:7}")
if [[ "$version" != *-* ]]; then
core_tags+=(--tag "$image:${version%.*}" --tag "$image:${version%%.*}" --tag "$image:latest")
batteries_tags+=(--tag "$image:batteries-${version%.*}" --tag "$image:batteries-${version%%.*}" --tag "$image:batteries")
fi
docker buildx imagetools create "${core_tags[@]}" \
"$image:${GITHUB_REF_NAME}-amd64" "$image:${GITHUB_REF_NAME}-arm64"
docker buildx imagetools create "${batteries_tags[@]}" \
"$image:${GITHUB_REF_NAME}-batteries-amd64" "$image:${GITHUB_REF_NAME}-batteries-arm64"
done

create-release:
needs: publish-manifest
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## [1.5.1] - 2026-09-16

### Container images

- `example42/pabawi` and `ghcr.io/example42/pabawi` now publish a minimal core
image as the default tag family. The core image contains Pabawi but excludes
Bolt, Ansible, Puppet/OpenVox, OpenSSH and the other integration CLIs.
- A batteries image adds Bolt 5.6.0, Ansible 2.14.18, Puppet/OpenVox 8.29.0,
Facter 5.6.1, OpenSSH, Git, curl, rsync and sshpass. It is published under
`batteries`, `batteries-1`, `batteries-1.5`, `batteries-1.5.1` and immutable
commit tags.
- Release CI builds and smoke-tests both profiles for amd64 and arm64, then
publishes the verified images to Docker Hub and GHCR without rebuilding.
Compose, setup and convenience scripts select the batteries image where the
integration toolchain is expected.

### Security

- Core and batteries images retain SBOM and vulnerability evidence. Fixable
high or critical findings block publishing, except for a reviewed
batteries-only exception for `CVE-2026-85396` in `rubyzip` 2.4.1. OpenBolt's
current `winrm-fs` dependency prevents a compatible upgrade. The exception
is scoped to the exact gem path and package URL and expires on 2026-12-31.

## [1.5.0] - 2026-09-14

### Security: breaking for operators
Expand Down
66 changes: 49 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ RUN npm ci --workspace=backend --omit=dev --ignore-scripts --no-audit --no-fund
# Keep both hoisted and workspace-local packages in their locked locations.
RUN mkdir -p backend/node_modules

# Stage 3: Install OpenBolt from OpenVox upstream packages
FROM node:24.21.0-bookworm-slim@sha256:2fe369e969550cde8e867afc3fe370b260140cab4a23d467074295b42163d553 AS bolt-builder
# Stage 3: Build the optional infrastructure toolchain. The default core
# target does not depend on this stage, so BuildKit skips it entirely.
FROM node:24.21.0-bookworm-slim@sha256:2fe369e969550cde8e867afc3fe370b260140cab4a23d467074295b42163d553 AS toolchain-builder

# hadolint ignore=DL3008
RUN apt-get update && \
Expand All @@ -39,7 +40,9 @@ RUN apt-get update && \
&& dpkg -i openvox8-release-debian12.deb \
&& rm openvox8-release-debian12.deb \
&& apt-get update \
&& apt-get install -y --no-install-recommends openbolt=5.6.0-1+debian12 \
&& apt-get install -y --no-install-recommends \
openbolt=5.6.0-1+debian12 \
openvox-agent=8.29.0-1+debian12 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

Expand All @@ -58,34 +61,36 @@ RUN apt-get update && \
faraday:2.14.3 \
jwt:2.10.3 \
resolv:0.7.2 \
&& /opt/puppetlabs/puppet/bin/gem install --no-document resolv:0.7.2 \
&& /opt/puppetlabs/bolt/bin/gem uninstall --force --ignore-dependencies \
concurrent-ruby:1.3.6 faraday:2.14.2 jwt:2.10.2 \
&& rm -rf /opt/puppetlabs/bolt/lib/ruby/gems/3.2.0/specifications/default/resolv-0.2.3.gemspec \
/opt/puppetlabs/bolt/lib/ruby/gems/3.2.0/gems/resolv-0.2.3 \
/opt/puppetlabs/puppet/lib/ruby/gems/3.2.0/specifications/default/resolv-0.2.3.gemspec \
/opt/puppetlabs/puppet/lib/ruby/gems/3.2.0/gems/resolv-0.2.3 \
&& apt-get purge -y make gcc libc6-dev && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

# Stage 4: Production image
FROM node:24.21.0-bookworm-slim@sha256:2fe369e969550cde8e867afc3fe370b260140cab4a23d467074295b42163d553
# Stage 4: Core production image. This is the default published image and
# contains only Pabawi and the operating-system packages it needs to start.
FROM node:24.21.0-bookworm-slim@sha256:2fe369e969550cde8e867afc3fe370b260140cab4a23d467074295b42163d553 AS core
ARG TARGETPLATFORM
ARG BUILDPLATFORM

# Add metadata labels
LABEL org.opencontainers.image.title="Pabawi"
LABEL org.opencontainers.image.description="Puppet Ansible Bolt Awesome Web Interface"
LABEL org.opencontainers.image.version="1.5.0"
LABEL org.opencontainers.image.title="Pabawi Core"
LABEL org.opencontainers.image.description="Pabawi infrastructure management web interface"
LABEL org.opencontainers.image.version="1.5.1"
LABEL org.opencontainers.image.vendor="example42"
LABEL org.opencontainers.image.source="https://github.com/example42/pabawi"

# Install only runtime dependencies
# Install only core runtime dependencies. Integration CLIs belong in the
# batteries target below.
# hadolint ignore=DL3008
RUN apt-get update && \
apt-get install -y --no-install-recommends \
bash \
openssh-client \
git \
coreutils \
ansible \
ca-certificates \
&& apt-get install -y --only-upgrade libpcre2-8-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
Expand All @@ -95,10 +100,6 @@ RUN apt-get update && \
# dependencies (e.g. brace-expansion, tar, ip-address) don't ship either.
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack

# Copy Bolt installation from upstream package builder stage
COPY --from=bolt-builder /opt/puppetlabs /opt/puppetlabs
RUN ln -s /opt/puppetlabs/bolt/bin/bolt /usr/local/bin/bolt

# Create non-root user
RUN groupadd -g 1001 pabawi && \
useradd -u 1001 -g pabawi -m -s /bin/bash pabawi
Expand Down Expand Up @@ -168,3 +169,34 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \

# Start the application
CMD ["node", "dist/server.js"]

# Stage 5: Optional batteries-included image. It adds every local CLI invoked
# by an integration plus the Puppet/OpenVox operator toolchain. Cloud and API
# integrations use the Node SDKs already present in the core image.
FROM core AS batteries
USER root

# hadolint ignore=DL3008
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ansible \
curl \
git \
openssh-client \
rsync \
sshpass \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

COPY --from=toolchain-builder /opt/puppetlabs /opt/puppetlabs
RUN ln -s /opt/puppetlabs/bolt/bin/bolt /usr/local/bin/bolt \
&& ln -s /opt/puppetlabs/puppet/bin/facter /usr/local/bin/facter \
&& ln -s /opt/puppetlabs/puppet/bin/puppet /usr/local/bin/puppet

LABEL org.opencontainers.image.title="Pabawi Batteries Included"
LABEL org.opencontainers.image.description="Pabawi with Bolt, Ansible, Puppet/OpenVox, and SSH tooling"

USER pabawi

# Keep the default Dockerfile result backward-compatible with the core image.
FROM core AS final
2 changes: 1 addition & 1 deletion Dockerfile.alpine
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ ARG BUILDPLATFORM
# Add metadata labels
LABEL org.opencontainers.image.title="Pabawi"
LABEL org.opencontainers.image.description="Puppet Ansible Bolt Awesome Web Interface"
LABEL org.opencontainers.image.version="1.5.0"
LABEL org.opencontainers.image.version="1.5.1"
LABEL org.opencontainers.image.vendor="example42"
LABEL org.opencontainers.image.source="https://github.com/example42/pabawi"

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.ubuntu
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ ARG BUILDPLATFORM
# Add metadata labels
LABEL org.opencontainers.image.title="Pabawi"
LABEL org.opencontainers.image.description="Puppet Ansible Bolt Awesome Web Interface"
LABEL org.opencontainers.image.version="1.5.0"
LABEL org.opencontainers.image.version="1.5.1"
LABEL org.opencontainers.image.vendor="example42"
LABEL org.opencontainers.image.source="https://github.com/example42/pabawi"

Expand Down
Loading
Loading