Skip to content
Draft
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
31 changes: 31 additions & 0 deletions .github/workflows/build-base-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Build base images

on:
workflow_dispatch:

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
strategy:
matrix:
platform:
- ubuntu-20
- ubuntu-22
- ubuntu-24
- debian-11
- debian-12
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Log in to ghcr.io
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push image
run: ./build-in-container.py --platform ${{ matrix.platform }} --push-image
113 changes: 113 additions & 0 deletions build-in-container-inner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/bin/bash
set -e

# Configuration via environment variables:
# PROJECT, BUILD_TYPE, EXPLICIT_ROLE, BUILD_NUMBER, EXPLICIT_VERSION

BASEDIR=/home/builder/build
export BASEDIR
export AUTOBUILD_PATH="$BASEDIR/buildscripts"

mkdir -p "$BASEDIR"

# Bind-mounted directories may be owned by the host user's UID.
# Fix ownership so builder can write to them.
sudo chown -R "$(id -u):$(id -g)" "$HOME/.cache" /output

# Prevent git "dubious ownership" errors
git config --global --add safe.directory '*'

# === Sync source repos ===
repos="buildscripts core masterfiles"
if [ "$PROJECT" = "nova" ]; then
repos="$repos enterprise nova mission-portal"
fi

for repo in $repos; do
src="/srv/source/$repo"
# Use rsync -aL to follow symlinks during copy.
# The source dir may use symlinks (e.g., core -> cfengine/core/).
# -L resolves them at copy time, so the destination gets real files
# regardless of the host directory layout.
# Exclude acceptance test workdirs — they contain broken symlinks left
# over from previous test runs and are not needed for building.
if [ -d "$src" ] || [ -L "$src" ]; then
echo "Syncing $repo..."
sudo rsync -aL --exclude='config.cache' --exclude='workdir' --chown="$(id -u):$(id -g)" "$src/" "$BASEDIR/$repo/"
else
echo "ERROR: Required repository $repo not found" >&2
exit 1
fi
done

install_mission_portal_deps() (
set -e

if [ -f "$BASEDIR/mission-portal/public/scripts/package.json" ]; then
echo "Installing npm dependencies..."
npm ci --prefix "$BASEDIR/mission-portal/public/scripts/"
echo "Building react components..."
npm run build --prefix "$BASEDIR/mission-portal/public/scripts/"
rm -rf "$BASEDIR/mission-portal/public/scripts/node_modules"
fi

if [ -f "$BASEDIR/mission-portal/composer.json" ]; then
echo "Installing Mission Portal PHP dependencies..."
(cd "$BASEDIR/mission-portal" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs)
fi

if [ -f "$BASEDIR/nova/api/http/composer.json" ]; then
echo "Installing Nova API PHP dependencies..."
(cd "$BASEDIR/nova/api/http" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs)
fi

if [ -f "$BASEDIR/mission-portal/public/themes/default/bootstrap/cfengine_theme.less" ]; then
echo "Compiling Mission Portal styles..."
mkdir -p "$BASEDIR/mission-portal/public/themes/default/bootstrap/compiled/css"
(cd "$BASEDIR/mission-portal/public/themes/default/bootstrap" &&
lessc --compress ./cfengine_theme.less ./compiled/css/cfengine.less.css)
fi

if [ -f "$BASEDIR/mission-portal/ldap/composer.json" ]; then
echo "Installing LDAP API PHP dependencies..."
(cd "$BASEDIR/mission-portal/ldap" && php /usr/bin/composer.phar install --no-dev --ignore-platform-reqs)
fi
)

# === Step runner with failure reporting ===
# Disable set -e so we can capture exit codes and report which step failed.
set +e
run_step() {
local name="$1"
shift
echo "=== Running $name ==="
"$@"
local rc=$?
if [ $rc -ne 0 ]; then
echo ""
echo "=== FAILED: $name (exit code $rc) ==="
exit $rc
fi
}

# === Build steps ===
run_step "01-autogen" "$BASEDIR/buildscripts/build-scripts/autogen"
run_step "02-install-dependencies" "$BASEDIR/buildscripts/build-scripts/install-dependencies"
if [ "$EXPLICIT_ROLE" = "hub" ]; then
run_step "03-mission-portal-deps" install_mission_portal_deps
fi
run_step "04-configure" "$BASEDIR/buildscripts/build-scripts/configure"
run_step "05-compile" "$BASEDIR/buildscripts/build-scripts/compile"
run_step "06-package" "$BASEDIR/buildscripts/build-scripts/package"
Comment on lines +93 to +101
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we should rename build-scripts folder to steps / build-steps 😅.


# === Copy output packages ===
# Packages are created under $BASEDIR/<project>/ by dpkg-buildpackage / rpmbuild.
# Exclude deps-packaging to avoid copying dependency packages.
find "$BASEDIR" -maxdepth 4 \
-path "$BASEDIR/buildscripts/deps-packaging" -prune -o \
\( -name '*.deb' -o -name '*.rpm' -o -name '*.pkg.tar.gz' \) -print \
-exec cp {} /output/ \;

echo ""
echo "=== Build complete ==="
ls -lh /output/
142 changes: 142 additions & 0 deletions build-in-container.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# build-in-container

Build CFEngine packages inside Docker containers using build scripts. Requires
only Docker and Python 3 on the host.

## Quick start

```bash
# Build a community agent .deb for Ubuntu 22
./build-in-container.py --platform ubuntu-22 --project community --role agent --build-type DEBUG

# Build a nova hub release package for Debian 12
./build-in-container.py --platform debian-12 --project nova --role hub --build-type RELEASE
```

In the examples above, we run the script from inside `buildscripts/` (with
`buildscripts` as our current working directory). This is not required — if not
specified, defaults will:

- Look for sources relative to the script (parent directory of
`build-in-container.py`).
- Place cache files in the user's home directory
(`~/.cache/cfengine/buildscripts`).
- Use the current working directory for output packages (`./output/`).

## Usage

```
./build-in-container.py --platform PLATFORM --project PROJECT --role ROLE --build-type TYPE [OPTIONS]
```

### Required arguments

| Option | Description |
| -------------- | ------------------------------------------------------- |
| `--platform` | Target platform (e.g. `ubuntu-22`, `debian-12`) |
| `--project` | `community` or `nova` (not required for `--push-image`) |
| `--role` | `agent` or `hub` (not required for `--push-image`) |
| `--build-type` | `DEBUG` or `RELEASE` (not required for `--push-image`) |

### Optional arguments

| Option | Default | Description |
| ------------------ | -------------------------------- | ----------------------------------------------------------- |
| `--output-dir` | `./output` | Where to write output packages |
| `--cache-dir` | `~/.cache/cfengine/buildscripts` | Dependency cache directory |
| `--build-number` | `1` | Build number for package versioning |
| `--version` | auto | Override version string |
| `--rebuild-image` | | Force rebuild of Docker image (bypasses Docker layer cache) |
| `--push-image` | | Build image and push to registry, then exit |
| `--shell` | | Drop into a bash shell inside the container for debugging |
| `--list-platforms` | | List available platforms and exit |
| `--source-dir` | parent of `buildscripts/` | Root directory containing repos |

## Supported platforms

| Name | Base image |
| ----------- | -------------- |
| `ubuntu-20` | `ubuntu:20.04` |
| `ubuntu-22` | `ubuntu:22.04` |
| `ubuntu-24` | `ubuntu:24.04` |
| `debian-11` | `debian:11` |
| `debian-12` | `debian:12` |

Adding a new Debian/Ubuntu platform requires only a new entry in the `PLATFORMS`
dict in `build-in-container.py`. Adding a non-debian based platform (e.g.,
RHEL/CentOS) requires a new `container/Dockerfile.rhel` plus platform entries.

## How it works

The system has three components:

1. **`build-in-container.py`** (Python) -- the orchestrator that runs on the host.
Parses arguments, builds the Docker image, and launches the container with
the correct mounts and environment variables.

2. **`build-in-container-inner.sh`** (Bash) -- runs inside the container. Copies
source repos from the read-only mount, then calls the existing build scripts
in order.

3. **`container/Dockerfile.debian`** -- parameterized Dockerfile shared by all
Debian/Ubuntu platforms via a `BASE_IMAGE` build arg.

### Container mounts

| Host path | Container path | Mode | Purpose |
| ---------------------------------------- | ----------------------------------------- | ---------- | ------------------------------------- |
| Source repos (parent of `buildscripts/`) | `/srv/source` | read-only | Protects host repos from modification |
| `~/.cache/cfengine/buildscripts/` | `/home/builder/.cache/buildscripts_cache` | read-write | Dependency cache shared across builds |
| `./output/` | `/output` | read-write | Output packages copied here |

### Build steps

The inner script runs these steps in order:

1. **autogen** -- runs `autogen.sh` in each repo
2. **install-dependencies** -- builds and installs bundled dependencies
3. **mission-portal-deps** -- (hub only) installs PHP/npm/LESS assets
4. **configure** -- runs `./configure` with platform-appropriate flags
5. **compile** -- compiles and installs to the dist tree
6. **package** -- creates `.deb` or `.rpm` packages

## Docker image management

By default, the script pulls a pre-built image from the container registry
(`ghcr.io/cfengine`). If the pull fails (e.g. no network, image not yet
published), it falls back to building the image locally.

Use `--rebuild-image` to skip the registry and force a local rebuild — useful
when iterating on the Dockerfile. The local build tracks the Dockerfile content
hash and skips rebuilding when nothing has changed.

### Container registry

Images are hosted at `ghcr.io/cfengine` and versioned via `IMAGE_VERSION` in
`build-in-container.py`. To push a new image:

```bash
# Build and push a single platform
./build-in-container.py --platform ubuntu-22 --push-image
```

`--push-image` always builds with `--no-cache` to pick up the latest upstream
packages, then pushes to the registry.

### Updating the toolchain

1. Edit `container/Dockerfile.debian` as needed
2. Test locally with `--rebuild-image`
3. Bump `IMAGE_VERSION` in `build-in-container.py`
4. Commit the Dockerfile change + version bump
5. Push new images with `--push-image` (or trigger the GitHub Actions workflow)

## Debugging

```bash
# Drop into a shell inside the container
./build-in-container.py --platform ubuntu-22 --project community --role agent --build-type DEBUG --shell
```

The shell session has the same mounts and environment as a build run. The
container is ephemeral (`--rm`), so any changes are lost on exit.
Loading
Loading