Skip to content
Merged
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
98 changes: 75 additions & 23 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Keep responses direct and task-focused.
<!-- copilot instructions end -->

<!-- SPECKIT START -->
When writing markdown files, use the following guidelines:
- wrap lines at 80 characters

For additional context about technologies to be used, project structure,
shell commands, and edge-case handling details, read specs/003-support-adduser-utils/plan.md
and treat it as the authoritative implementation context.
Expand All @@ -21,45 +24,94 @@ within the Ruby 2.6.x series (intentionally pinned to 2.6, not a newer major ver
is a hard requirement.

The image must include the `jemalloc` implementation of `malloc` for improved memory performance.
To support development and testing, the resulting container will be multi-architecture, supporting both
`linux/amd64` and `linux/arm64` architectures.
The resulting container must be multi-architecture, supporting both `linux/amd64` and `linux/arm64`
architectures.

The project will use `dependabot` to keep the pinned versions of Ruby, OpenSSL, and jemalloc up to
date.
The image must be derived from a `debian:bookworm-slim` base layer so that downstream images can
install additional packages via `apt-get`. Build artifacts must be minimized, but a functional
package manager must be present in the final image.

## Docker Specification

The resulting Docker image will be based on `debian:bookworm-slim`, providing a practical runtime
footprint that supports downstream user-management workflows. It will use a multi-stage build to
limit image contents to compiled Ruby binaries, required runtime libraries, and files needed for
testing and validating the image.
The build stage must use `debian:bookworm-slim` as the builder image.
It will use a multi-stage build to remove unnecessary image contents.

The `Dockerfile` will be formatted with "here-doc" `RUN` blocks for clarity and maintainability.
The `Dockerfile` must be linted with `hadolint` and follow best practices for layer management and caching.
The `Dockerfile` must include a LABEL with the project name, description, and version.

## Dependency Version Management

The project will use `dependabot` to keep the base builder image up to date. Dependabot is
configured using the `docker` ecosystem in `.github/dependabot.yml`.

Ruby, OpenSSL, and jemalloc version strings are embedded in Dockerfile `ARG` declarations
and are not trackable by Dependabot's built-in ecosystems; these must be updated manually
via pull requests when new patch releases are available.

## CICD

[registry]: ghcr.io/umnlibraries/ruby2.6-jemalloc-docker

* The CI workflow will be implemented with GitHub Actions, using `Buildx` for multi-architecture
builds and cache management. Buildx cache must use the GitHub Actions cache backend (`type=gha`) with `mode=max` to cache all build layers across workflow runs.
* The workflow must push the final multi-arch manifest to [registry]:latest. Tags must include the full Ruby
version (e.g. `2.6.10`) and a `latest` alias for the highest 2.6.x version. The `latest` tag must be updated only
when the image being built carries a Ruby patch version higher than any previously published 2.6.x tag in the
registry; the workflow should derive the Ruby version from build args and compare it against existing registry tags
before deciding to also push `latest`. The registry credentials will be
provided via GitHub Actions secrets named `REGISTRY_USERNAME` and `REGISTRY_PASSWORD`.
* The CI workflow will be implemented with GitHub Actions, using `Buildx` for
multi-architecture builds and cache management. Buildx cache must use the
GitHub Actions cache backend (`type=gha`) with `mode=max` to cache all build
layers across workflow runs.
* The workflow is triggered only when a semantic version tag (e.g., `v1.2.3`) is pushed to the
repository. Pull request and branch builds must not push images to the registry.
* Tags pushed to the registry must include the full Ruby version (e.g., `2.6.10`). The `latest`
tag is managed as follows:
1. The workflow receives the Ruby version as a build arg (e.g., `RUBY_VERSION=2.6.10`).
2. After a successful build, the workflow queries the GHCR registry API to list all published
tags matching the pattern `2.6.*`.
3. The workflow compares `RUBY_VERSION` against the highest published `2.6.x` tag using
semver comparison.
4. If `RUBY_VERSION` is strictly greater than the highest published tag, the workflow also
pushes the `latest` tag.
5. If `RUBY_VERSION` is equal to or lower than the highest published tag, only the
version-specific tag (e.g., `2.6.10`) is pushed.
6. If the registry API query fails or returns no existing `2.6.x` tags (e.g., on first
publish), the workflow must treat the current version as the highest and push both the
version-specific tag and `latest`. The comparison step must not fail the entire build if
tag comparison is unavailable.
* The registry credentials will be provided via GitHub Actions secrets named `REGISTRY_USERNAME`
and `REGISTRY_PASSWORD`.

## Version Control

* The project will be hosted on GitHub, with a clear branching strategy for development and releases.
* Use semantic versioning for release tags, and maintain a changelog to document changes.
* Use `dependabot` to keep dependencies up to date.
* The project will be hosted on GitHub, with a clear branching strategy for
development and releases.
* Use semantic versioning (`MAJOR.MINOR.PATCH`, e.g., `1.0.0`) for the
Dockerfile project's own release tags. These project release tags are
distinct from the version tags of the image dependencies, including Ruby,
Debian, and jemalloc.
* Maintain a `CHANGELOG.md` file updated with each project release.
* Pull requests will be used for all changes, with code review and automated
testing before merging.
* Built images will not be pushed to the registry from pull request builds.
Only builds triggered by a tag push should push images to the registry.

## Release Management

* When a new tag is pushed to the repository, the CI workflow will
automatically build and push the corresponding Docker image to the registry.
In addition it will update the "latest" tag if the new version is a higher
patch version than the current "latest" tag.
* The release process will include validation steps to ensure the image is
built correctly and functions as expected before it is published.
* The project should contain a changelog file that is updated with each
release, detailing the changes made in that release.
* The project will use semantic versioning for release tags, following the
format `MAJOR.MINOR.PATCH` (e.g., `1.2.3`).

## Validation

* The project will use the `pre-commit` toolchain for local development to ensure code quality
and consistency.
* A `make lint` target will be used to lint all markdown files in the repository, ensuring
documentation quality.
* The project will use the `pre-commit` toolchain for local development to
ensure code quality and consistency.
* A `make lint` target will be used to lint files in the repository by running
`pre-commit run --all-files`. The pre-commit configuration invokes `yamllint`
(with `.yamllint.yml`) for `.yml`/`.yaml` files and `check-yaml` for YAML
syntax validation. Markdown and JSON files are checked for trailing whitespace
and line-ending consistency.

<!-- project-specific instructions end -->
82 changes: 67 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ name: Build and publish Docker image

"on":
push:
branches: [main]
tags: [v*]
pull_request:
branches: [main]
workflow_dispatch:
release:
types: [published]

env:
REGISTRY: ghcr.io
Expand All @@ -17,7 +15,7 @@ env:

jobs:
release:
if: github.event_name != 'pull_request'
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -39,22 +37,72 @@ jobs:
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}

- name: Normalize image name
run: echo "IMAGE_NAME_LOWER=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
id: normalize
run: echo "image_name_lower=${IMAGE_NAME,,}" >> "$GITHUB_OUTPUT"

- name: Extract Ruby version from Dockerfile
run: |
ruby_version="$(grep -m1 '^ARG RUBY_VERSION=' Dockerfile | cut -d= -f2)"
echo "RUBY_VERSION=${ruby_version}" >> "$GITHUB_ENV"

- name: Determine latest tag eligibility
run: |
set -eu

image_path="${{ steps.normalize.outputs.image_name_lower }}"
tags_api="https://ghcr.io/v2/${image_path}/tags/list"
auth_user="${{ secrets.REGISTRY_USERNAME }}"
auth_pass="${{ secrets.REGISTRY_PASSWORD }}"

push_latest=true
response="$({
curl -fsSL -u "${auth_user}:${auth_pass}" "${tags_api}" 2>/dev/null
} || true)"

if [ -n "${response}" ]; then
tags="$(
printf '%s' "${response}" \
| python3 -c 'import json,sys; p=sys.stdin.read(); d=json.loads(p);'\
'print("\\n".join(d.get("tags", [])))' \
2>/dev/null || true
)"

current_tags="$({
printf '%s\n' "${tags}" | grep -E '^2\.6\.[0-9]+$'
} || true)"

if [ -n "${current_tags}" ]; then
highest_existing="$(printf '%s\n' "${current_tags}" | sort -V | tail -1)"
winner="$({
printf '%s\n%s\n' "${highest_existing}" "${RUBY_VERSION}" \
| sort -V \
| tail -1
})"

if [ "${winner}" = "${RUBY_VERSION}" ] \
&& [ "${RUBY_VERSION}" != "${highest_existing}" ]; then
push_latest=true
else
push_latest=false
fi
fi
fi

echo "PUSH_LATEST=${push_latest}" >> "$GITHUB_ENV"

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}
images: ${{ env.REGISTRY }}/${{ steps.normalize.outputs.image_name_lower }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ env.RUBY_VERSION }}
type=raw,value=latest,enable=${{ env.PUSH_LATEST }}
type=sha,prefix=sha-
type=ref,event=branch
type=ref,event=pr

- name: Build and push Docker image
id: build
Expand All @@ -73,7 +121,7 @@ jobs:
JEMALLOC_VERSION=5.3.0

verify-release:
if: github.event_name != 'pull_request'
if: startsWith(github.ref, 'refs/tags/')
needs: release
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -106,19 +154,23 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}

- name: Normalize image name
run: echo "IMAGE_NAME_LOWER=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
id: normalize
run: echo "image_name_lower=${IMAGE_NAME,,}" >> "$GITHUB_OUTPUT"

- name: Resolve release image reference
run: |
release_digest="${{ needs.release.outputs.release_digest }}"
echo "RELEASE_IMAGE=${REGISTRY}/${IMAGE_NAME_LOWER}@${release_digest}" >> "$GITHUB_ENV"
image_ref="${REGISTRY}/${{ steps.normalize.outputs.image_name_lower }}"
echo "RELEASE_IMAGE=${image_ref}@${release_digest}" >> "$GITHUB_ENV"

- name: Pull release image
run: docker pull "$RELEASE_IMAGE"

- name: Verify Ruby and jemalloc runtime
run: |
./scripts/verify-ruby-jemalloc.sh "$RELEASE_IMAGE" "${{ matrix.platform }}"
./scripts/verify-ruby-jemalloc.sh \
"$RELEASE_IMAGE" \
"${{ matrix.platform }}"

verify-pr:
if: github.event_name == 'pull_request'
Expand Down
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,18 @@ repos:
hooks:
- id: yamllint
args: [--strict, -c=.yamllint.yml]

# Dockerfile linter (pinned image, no host binary dependency)
- repo: local
hooks:
- id: hadolint
name: hadolint
language: system
files: (^|/)Dockerfile$
entry: >-
bash -c '
for file in "$@"; do
docker run --rm -i hadolint/hadolint:v2.12.0 \
hadolint --ignore DL3003 --ignore DL3008 - < "$file"
done
' --
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/002-multiarch-image-cache"
"feature_directory": "specs/003-support-adduser-utils"
}
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
"true": true,
"git worktree": true,
"pre-commit": true,
"docker build": true
"docker build": true,
".specify/extensions/git/scripts/bash/create-new-feature.sh": true,
"adduser": true,
"cp": true,
"get_terminal_output": true,
"/^bash \\.specify/scripts/bash/setup-tasks\\.sh --json 2>/dev/null$/": {
"approve": true,
"matchCommandLine": true
},
".specify/extensions/git/scripts/bash/auto-commit.sh": true
}
}
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project follows Semantic
Versioning for project releases.

## [Unreleased]

### Changed

- Align release workflow with tag-based publishing and conditional `latest`
promotion.
- Add hadolint to pre-commit checks.

## [1.2.0] - 2026-06-11

### Changed

- Switched final runtime image to `debian:bookworm-slim` to support downstream
package installation and user-management workflows.
- Kept Ruby 2.6 and jemalloc verification behavior while updating runtime image
composition.
- Updated verification docs for downstream usage and runtime consistency.

## [1.1.0] - 2026-06-09

### Changed

- Added multi-architecture Buildx workflow support for `linux/amd64` and
`linux/arm64`.
- Added GitHub Actions cache backend usage (`type=gha`, `mode=max`) for
improved layer reuse.

## [1.0.0] - 2026-06-04

### Changed

- Standardized Dockerfile build steps using heredoc `RUN` blocks for improved
readability and maintenance.
- Established initial Ruby 2.6 + jemalloc container baseline.
Loading
Loading