From 452b6c67770f66c0a608b104322d4ed2448c7a38 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Wed, 22 Oct 2025 11:21:03 -0400 Subject: [PATCH] Add devcontainer and container GC pipeline Motivated by having a good Codespaces default UX. This repo will also contain our centralized container image GC. Assisted-by: Cursor (Claude Sonnet 4.5) Signed-off-by: Colin Walters --- .devcontainer | 1 + .github/workflows/build-devcontainer.yml | 131 +++++++++++++++++++++++ .github/workflows/container-gc.yml | 35 ++++++ Justfile | 7 ++ README.md | 14 +++ common/.devcontainer/devcontainer.json | 28 +++++ devenv/.dockerignore | 8 ++ devenv/Containerfile | 108 +++++++++++++++++++ devenv/README.md | 21 ++++ devenv/build-deps.txt | 1 + devenv/devenv-init.sh | 14 +++ devenv/packages.txt | 29 +++++ renovate-shared-config.json | 13 ++- 13 files changed, 409 insertions(+), 1 deletion(-) create mode 120000 .devcontainer create mode 100644 .github/workflows/build-devcontainer.yml create mode 100644 .github/workflows/container-gc.yml create mode 100644 Justfile create mode 100644 common/.devcontainer/devcontainer.json create mode 100644 devenv/.dockerignore create mode 100644 devenv/Containerfile create mode 100644 devenv/README.md create mode 100644 devenv/build-deps.txt create mode 100755 devenv/devenv-init.sh create mode 100644 devenv/packages.txt diff --git a/.devcontainer b/.devcontainer new file mode 120000 index 0000000..4a580c9 --- /dev/null +++ b/.devcontainer @@ -0,0 +1 @@ +common/.devcontainer \ No newline at end of file diff --git a/.github/workflows/build-devcontainer.yml b/.github/workflows/build-devcontainer.yml new file mode 100644 index 0000000..701df3c --- /dev/null +++ b/.github/workflows/build-devcontainer.yml @@ -0,0 +1,131 @@ +name: Build DevContainer + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + paths: + - 'devenv/**' + - '.github/workflows/build-devcontainer.yml' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/devenv-debian + +jobs: + validate-devcontainer: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install just + uses: extractions/setup-just@v2 + + - name: Validate devcontainer.json syntax + run: just devcontainer-validate + + build: + needs: validate-devcontainer + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + platform: linux/amd64 + arch: amd64 + - runner: ubuntu-24.04-arm + platform: linux/arm64 + arch: arm64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: devenv + file: devenv/Containerfile + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.arch }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: build + if: github.event_name != 'pull_request' + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix={{branch}}-,format=short + type=sha,prefix={{branch}}-,format=long + type=ref,event=pr + type=ref,event=tag + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + diff --git a/.github/workflows/container-gc.yml b/.github/workflows/container-gc.yml new file mode 100644 index 0000000..c7d1220 --- /dev/null +++ b/.github/workflows/container-gc.yml @@ -0,0 +1,35 @@ +name: Container Image Garbage Collection + +on: + workflow_dispatch: + inputs: + retention-days: + description: "Delete container images older than this many days" + required: false + default: "14" + type: string + dry-run: + description: "Dry run mode - don't actually delete anything" + required: false + default: false + type: boolean + schedule: + # Run weekly on Sundays at 2 AM UTC + - cron: '0 2 * * 0' + +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Delete old container images + uses: snok/container-retention-policy@v3.0.0 + with: + account: ${{ github.repository_owner }} + token: ${{ secrets.GITHUB_TOKEN }} + cut-off: ${{ github.event.inputs.retention-days || '14' }}d + dry-run: ${{ github.event.inputs.dry-run || false }} + diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..8638191 --- /dev/null +++ b/Justfile @@ -0,0 +1,7 @@ +# Validate devcontainer.json syntax +devcontainer-validate: + npx --yes @devcontainers/cli read-configuration --workspace-folder . + +# Build devenv image with local tag +devenv-build: + cd devenv && podman build --jobs=4 -t localhost/bootc-devenv-debian . diff --git a/README.md b/README.md index 5965552..9353359 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ This repository provides centralised configuration and automation for the [bootc ## Table of Contents - [Purpose](#purpose) +- [Development Environment](#development-environment) +- [Container Image Management](#container-image-management) - [Renovate](#renovate) - [Getting Started](#getting-started) - [Support & Contributions](#support--contributions) @@ -24,6 +26,18 @@ The main goal of this repository is to: --- +## Development Environment + +Containerized development environment with necessary tools and dependencies. For more, +see [devenv/README.md](devenv/README.md). + +--- + +## Container Garbage Collection + +Automated cleanup of old container images from GitHub Container Registry. + +--- ## Renovate diff --git a/common/.devcontainer/devcontainer.json b/common/.devcontainer/devcontainer.json new file mode 100644 index 0000000..26e62a2 --- /dev/null +++ b/common/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "bootc-devenv-debian", + // TODO override this back to prod image + "image": "ghcr.io/bootc-dev/devenv-debian", + "customizations": { + "vscode": { + // Abitrary, but most of our code is in one of these two + "extensions": [ + "rust-lang.rust-analyzer", + "golang.Go" + ] + } + }, + "features": {}, + "runArgs": [ + // Because we want to be able to run podman and also use e.g. /dev/kvm + // among other things + "--privileged" + ], + "postCreateCommand": { + // Our init script + "devenv-init": "sudo /usr/local/bin/devenv-init.sh" + }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/usr/local/cargo/bin" + } +} + diff --git a/devenv/.dockerignore b/devenv/.dockerignore new file mode 100644 index 0000000..96cddfb --- /dev/null +++ b/devenv/.dockerignore @@ -0,0 +1,8 @@ +# Exclude everything by default, then include just what we need +# Especially note this means that .git is not included, and not tests/ +# to avoid spurious rebuilds. +* +# And explicit includes +!packages.txt +!build-deps.txt +!devenv-init.sh diff --git a/devenv/Containerfile b/devenv/Containerfile new file mode 100644 index 0000000..c03b708 --- /dev/null +++ b/devenv/Containerfile @@ -0,0 +1,108 @@ +# These aren't packages, just low-dependency binaries dropped in /usr/local/bin +# so we can fetch them independently in a separate build. +ARG base=docker.io/library/debian:sid +FROM $base as base +# Life is too short to care about dash +RUN ln -sfr /bin/bash /bin/sh +RUN < /etc/apt/sources.list.d/github-cli.list + +# And re-update after we've fetched repos +apt -y update +EORUN + +FROM base as tools +# renovate: datasource=github-releases depName=block/goose +ARG gooseversion=v1.11.1 +# renovate: datasource=github-releases depName=bootc-dev/bcvk +ARG bcvkversion=v0.5.3 +RUN < /etc/sudoers.d/devenv && chmod 0440 /etc/sudoers.d/devenv +EORUN +# To avoid overlay-on-overlay with nested containers +VOLUME [ "/var/lib/containers", "/home/devenv/.local/share/containers/" ] +USER devenv diff --git a/devenv/README.md b/devenv/README.md new file mode 100644 index 0000000..e7023fa --- /dev/null +++ b/devenv/README.md @@ -0,0 +1,21 @@ +# A devcontainer for work on bootc-org projects + +This container image is suitable for use on +developing projects in the bootc-dev organization, +especially bootc. + +It includes all tools used in the Justfile +for relevant projects. + +## Base image + +At the current time the default is using Debian sid, mainly because +other parts of the upstream use CentOS Stream as a *target system* +base, but this helps prove out the general case of "src != target" +that is a philosophy of bootc (and containers in general) +as well as just helping prepare/motivate for bootc-on-Debian. + +## Building locally + +See the `Justfile`, but it's just a thin wrapper around a default +of `podman build` of this directory. diff --git a/devenv/build-deps.txt b/devenv/build-deps.txt new file mode 100644 index 0000000..c920542 --- /dev/null +++ b/devenv/build-deps.txt @@ -0,0 +1 @@ +ostree diff --git a/devenv/devenv-init.sh b/devenv/devenv-init.sh new file mode 100755 index 0000000..ae8661f --- /dev/null +++ b/devenv/devenv-init.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail +# Set things up so that podman can run nested inside the privileged +# docker container of a codespace. + +# Fix the propagation +sudo mount -o remount --make-shared / + +# This is actually safe to expose to all users really, like Fedora derivatives do +chmod a+rw /dev/kvm + +# Handle nested cgroups +sed -i -e 's,^#cgroups =.*,cgroups = "no-conmon",' /usr/share/containers/containers.conf +sed -i -e 's,^#cgroup_manager =.*,cgroup_manager = "cgroupfs",' /usr/share/containers/containers.conf diff --git a/devenv/packages.txt b/devenv/packages.txt new file mode 100644 index 0000000..9bf9e16 --- /dev/null +++ b/devenv/packages.txt @@ -0,0 +1,29 @@ +# Key dependencies +just +podman +curl +git +# Just because lots of things expect it +sudo + +# Generic utilities +acl + +# General build env, note we install rust through rustup later +gcc +clang +clang-format +libkrb5-dev pkg-config libvirt-dev libostree-dev +go-md2man + +# Runtime virt +genisoimage qemu-utils qemu-kvm libvirt-daemon-system virtiofsd + +# TUI editors on general principle +vim nano + +# dependency of other things like gemini CLI +npm + +# from 3rd party repo +gh diff --git a/renovate-shared-config.json b/renovate-shared-config.json index 55a7b9f..cac1206 100644 --- a/renovate-shared-config.json +++ b/renovate-shared-config.json @@ -15,6 +15,16 @@ // where you don't want to burn CI credits, while still keeping your branch // in a mergeable state at all times. "rebaseWhen": "conflicted", + "customManagers": [ + { + "customType": "regex", + "description": "Update ARG version variables in Containerfiles with renovate comments", + "fileMatch": ["(^|/)Containerfile$", "(^|/)Dockerfile$"], + "matchStrings": [ + "# renovate: datasource=(?[a-z-]+) depName=(?[^\\s]+)\\s+ARG \\w+version=(?.+)" + ] + } + ], "packageRules": [ // Group GitHub Actions dependencies { @@ -44,7 +54,8 @@ "Docker dependencies" ], "matchManagers": [ - "dockerfile" + "dockerfile", + "regex" ], "groupName": "Docker", "enabled": true