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
2 changes: 2 additions & 0 deletions .commitlintrc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
extends:
- "@commitlint/config-conventional"
13 changes: 13 additions & 0 deletions .containerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.git
.github
build
.env
*.md
LICENSE
.dive-ci
.hadolint.yaml
.releaserc.yaml
.commitlintrc.yaml
.containerignore
.pre-commit-config.yaml
scripts/
40 changes: 40 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: CI

on:
pull_request:
branches: [master]

permissions:
contents: read
pull-requests: read

jobs:
commitlint:
name: Lint commit messages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: wagoid/commitlint-github-action@v6

hadolint:
name: Lint Containerfile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Containerfile

build:
name: Test build
runs-on: ubuntu-latest
needs: [hadolint]
steps:
- uses: actions/checkout@v4

- name: Build image
run: docker build -f Containerfile -t test-build .
Comment on lines +39 to +40
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The CI workflow's test build uses docker build which doesn't pass any of the build args defined in manifest.yaml (RUNNER_VERSION, ARGO_VERSION, etc.). This means the test build won't accurately reflect the actual build process used in the release workflow, potentially missing build failures related to these arguments. Consider using the same build approach as release.yaml or at least passing the build args from manifest.yaml.

Suggested change
- name: Build image
run: docker build -f Containerfile -t test-build .
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image using manifest
run: docker buildx bake -f manifest.yaml --set *.tags=test-build

Copilot uses AI. Check for mistakes.
145 changes: 145 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Release

on:
push:
branches: [master]

permissions:
contents: write
packages: write

jobs:
release:
name: Semantic release
runs-on: ubuntu-latest
outputs:
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
new_release_version: ${{ steps.semantic.outputs.new_release_version }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false

- uses: cycjimmy/semantic-release-action@v4
id: semantic
with:
extra_plugins: |
@semantic-release/changelog
@semantic-release/git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-and-push:
name: Build, scan & push
needs: release
if: needs.release.outputs.new_release_published == 'true'
runs-on: ubuntu-latest
env:
IMAGE_VERSION: ${{ needs.release.outputs.new_release_version }}
steps:
- uses: actions/checkout@v4

- name: Install build tools
run: ./scripts/install_tools.sh

- name: Read manifest
id: manifest
run: |
echo "image_name=$(yq e '.name' manifest.yaml)" >> "$GITHUB_OUTPUT"
echo "registry=$(yq e '.registry' manifest.yaml)" >> "$GITHUB_OUTPUT"
echo "format=$(yq e '.build.format' manifest.yaml)" >> "$GITHUB_OUTPUT"

- name: Validate Containerfile
run: |
docker pull -q ghcr.io/hadolint/hadolint:latest
docker run --rm -i hadolint/hadolint:latest < Containerfile
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The hadolint validation step doesn't use the .hadolint.yaml configuration file that exists in the repository. The CI workflow (ci.yaml) uses hadolint/hadolint-action@v3.1.0 which automatically picks up the config, but this step uses a plain Docker run command without specifying the config file. Add --config .hadolint.yaml to the docker run command or mount the config file to ensure consistent linting behavior between CI and release workflows.

Suggested change
docker run --rm -i hadolint/hadolint:latest < Containerfile
docker run --rm -i -v "${PWD}/.hadolint.yaml:/.hadolint.yaml" hadolint/hadolint:latest --config /.hadolint.yaml < Containerfile

Copilot uses AI. Check for mistakes.

- name: Build image
env:
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
IMAGE_FORMAT: ${{ steps.manifest.outputs.format }}
run: |
# Build args from manifest
BUILD_ARGS=""
for arg in $(yq e '.build.args[]' manifest.yaml); do
BUILD_ARGS="${BUILD_ARGS} --build-arg ${arg}"
done

# Labels from manifest
LABELS=""
while IFS= read -r label; do
if [[ -n "${label}" ]]; then
label_key="${label%%=*}"
label_value="${label#*=}"
label_value="${label_value%\"}"
label_value="${label_value#\"}"
LABELS="${LABELS} --label ${label_key}=${label_value}"
fi
done < <(yq e '.build.labels[]' manifest.yaml)

# Add version label
LABELS="${LABELS} --label org.opencontainers.image.version=${IMAGE_VERSION}"

# shellcheck disable=SC2086
buildah build \
--squash \
--pull-always \
--format "${IMAGE_FORMAT}" \
${BUILD_ARGS} \
${LABELS} \
--tag "${IMAGE_NAME}:${IMAGE_VERSION}" \
.

# Save to OCI archive for scanning and pushing
mkdir -p build
buildah push "${IMAGE_NAME}:${IMAGE_VERSION}" "oci-archive:build/${IMAGE_NAME}.tar"

# Load into Docker daemon for dive scan
docker load -i "build/${IMAGE_NAME}.tar" 2>/dev/null || true
docker tag "$(docker images -q | head -1)" "${IMAGE_NAME}:${IMAGE_VERSION}" 2>/dev/null || true
Comment on lines +98 to +99
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The dive scan expects the image to be loaded into the Docker daemon, but the load command on line 98 uses 2>/dev/null || true which silently ignores all errors. If the OCI archive is corrupted or incompatible, the load will fail silently, and the subsequent docker tag on line 99 will also fail silently. This means the dive scan on line 104 will fail with a confusing error about the image not being found. Remove the || true or add explicit error checking to ensure the image is successfully loaded before attempting to scan it.

Suggested change
docker load -i "build/${IMAGE_NAME}.tar" 2>/dev/null || true
docker tag "$(docker images -q | head -1)" "${IMAGE_NAME}:${IMAGE_VERSION}" 2>/dev/null || true
docker load -i "build/${IMAGE_NAME}.tar"
docker tag "$(docker images -q | head -1)" "${IMAGE_NAME}:${IMAGE_VERSION}"

Copilot uses AI. Check for mistakes.

- name: Dive filesystem scan
env:
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
run: dive --ci --source=docker "${IMAGE_NAME}:${IMAGE_VERSION}"

- name: Trivy vulnerability scan
env:
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
run: |
trivy image \
--input "build/${IMAGE_NAME}.tar" \
--severity HIGH,CRITICAL \
--exit-code 1 \
"${IMAGE_NAME}:${IMAGE_VERSION}"

- name: Login to GHCR
env:
REGISTRY: ${{ steps.manifest.outputs.registry }}
run: skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"

- name: Push to registry
env:
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
REGISTRY: ${{ steps.manifest.outputs.registry }}
run: |
MAJOR_MINOR="${IMAGE_VERSION%.*}"
MAJOR="${IMAGE_VERSION%%.*}"

Comment on lines +126 to +128
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The version parsing logic assumes semantic versioning with at least two dots (MAJOR.MINOR.PATCH format). If the release version is "1.0" instead of "1.0.0", the MAJOR_MINOR variable on line 126 will be "1" (same as MAJOR), and MAJOR on line 127 will be empty, causing the subsequent skopeo copy commands to fail or push to incorrect tags. Add validation to ensure the version has the expected format, or make the parsing more robust to handle version strings with fewer components.

Suggested change
MAJOR_MINOR="${IMAGE_VERSION%.*}"
MAJOR="${IMAGE_VERSION%%.*}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$IMAGE_VERSION"
if [ -z "$MAJOR" ]; then
echo "ERROR: Failed to parse IMAGE_VERSION '$IMAGE_VERSION' into a major version component." >&2
exit 1
fi
if [ -n "$MINOR" ]; then
MAJOR_MINOR="${MAJOR}.${MINOR}"
else
MAJOR_MINOR="${MAJOR}"
fi

Copilot uses AI. Check for mistakes.
# Push semantic version tag (1.2.3)
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:${IMAGE_VERSION}"

# Push major.minor tag (1.2)
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:${MAJOR_MINOR}"

# Push major tag (1)
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:${MAJOR}"

# Push latest tag
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:latest"

- name: Verify pushed image
env:
REGISTRY: ${{ steps.manifest.outputs.registry }}
run: |
skopeo inspect "docker://${REGISTRY}:${IMAGE_VERSION}" --format '{{.Labels}}'
132 changes: 132 additions & 0 deletions .github/workflows/update-tools.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Update tool versions

on:
schedule:
- cron: "0 8 * * 1" # Every Monday at 08:00 UTC
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
update-tools:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Fetch latest tool versions
id: versions
env:
GH_TOKEN: ${{ github.token }}
run: |
get_latest_version() {
local repo="$1"
gh api "repos/${repo}/releases/latest" --jq '.tag_name' | sed 's/^v//'
}

ARGO_LATEST=$(get_latest_version "argoproj/argo-workflows")
KARGO_LATEST=$(get_latest_version "akuity/kargo")
PACK_LATEST=$(get_latest_version "buildpacks/pack")
DIVE_LATEST=$(get_latest_version "wagoodman/dive")
HADOLINT_LATEST=$(get_latest_version "hadolint/hadolint")
YQ_LATEST=$(get_latest_version "mikefarah/yq")

echo "argo=${ARGO_LATEST}" >> "$GITHUB_OUTPUT"
echo "kargo=${KARGO_LATEST}" >> "$GITHUB_OUTPUT"
echo "pack=${PACK_LATEST}" >> "$GITHUB_OUTPUT"
echo "dive=${DIVE_LATEST}" >> "$GITHUB_OUTPUT"
echo "hadolint=${HADOLINT_LATEST}" >> "$GITHUB_OUTPUT"
echo "yq=${YQ_LATEST}" >> "$GITHUB_OUTPUT"

ARGO_CURRENT=$(grep -oP 'ARGO_VERSION=\K[0-9.]+' Containerfile)
KARGO_CURRENT=$(grep -oP 'KARGO_VERSION=\K[0-9.]+' Containerfile)
PACK_CURRENT=$(grep -oP 'PACK_VERSION=\K[0-9.]+' Containerfile)
DIVE_CURRENT=$(grep -oP 'DIVE_VERSION=\K[0-9.]+' Containerfile)
HADOLINT_CURRENT=$(grep -oP 'HADOLINT_VERSION=\K[0-9.]+' Containerfile)
YQ_CURRENT=$(grep -oP 'YQ_VERSION=\K[0-9.]+' Containerfile)

echo "argo_current=${ARGO_CURRENT}" >> "$GITHUB_OUTPUT"
echo "kargo_current=${KARGO_CURRENT}" >> "$GITHUB_OUTPUT"
echo "pack_current=${PACK_CURRENT}" >> "$GITHUB_OUTPUT"
echo "dive_current=${DIVE_CURRENT}" >> "$GITHUB_OUTPUT"
echo "hadolint_current=${HADOLINT_CURRENT}" >> "$GITHUB_OUTPUT"
echo "yq_current=${YQ_CURRENT}" >> "$GITHUB_OUTPUT"

UPDATES=""
if [ "${ARGO_CURRENT}" != "${ARGO_LATEST}" ]; then
UPDATES="${UPDATES}- Argo Workflows CLI: ${ARGO_CURRENT} -> ${ARGO_LATEST}\n"
fi
if [ "${KARGO_CURRENT}" != "${KARGO_LATEST}" ]; then
UPDATES="${UPDATES}- Kargo CLI: ${KARGO_CURRENT} -> ${KARGO_LATEST}\n"
fi
if [ "${PACK_CURRENT}" != "${PACK_LATEST}" ]; then
UPDATES="${UPDATES}- pack (Buildpacks): ${PACK_CURRENT} -> ${PACK_LATEST}\n"
fi
if [ "${DIVE_CURRENT}" != "${DIVE_LATEST}" ]; then
UPDATES="${UPDATES}- dive: ${DIVE_CURRENT} -> ${DIVE_LATEST}\n"
fi
if [ "${HADOLINT_CURRENT}" != "${HADOLINT_LATEST}" ]; then
UPDATES="${UPDATES}- hadolint: ${HADOLINT_CURRENT} -> ${HADOLINT_LATEST}\n"
fi
if [ "${YQ_CURRENT}" != "${YQ_LATEST}" ]; then
UPDATES="${UPDATES}- yq: ${YQ_CURRENT} -> ${YQ_LATEST}\n"
fi

if [ -z "${UPDATES}" ]; then
echo "has_updates=false" >> "$GITHUB_OUTPUT"
echo "No updates found."
else
echo "has_updates=true" >> "$GITHUB_OUTPUT"
# Use a delimiter for multiline output
{
echo "summary<<EOF"
echo -e "${UPDATES}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo -e "Updates found:\n${UPDATES}"
fi

- name: Update versions in Containerfile and manifest
if: steps.versions.outputs.has_updates == 'true'
env:
ARGO_LATEST: ${{ steps.versions.outputs.argo }}
KARGO_LATEST: ${{ steps.versions.outputs.kargo }}
PACK_LATEST: ${{ steps.versions.outputs.pack }}
DIVE_LATEST: ${{ steps.versions.outputs.dive }}
HADOLINT_LATEST: ${{ steps.versions.outputs.hadolint }}
YQ_LATEST: ${{ steps.versions.outputs.yq }}
ARGO_CURRENT: ${{ steps.versions.outputs.argo_current }}
KARGO_CURRENT: ${{ steps.versions.outputs.kargo_current }}
PACK_CURRENT: ${{ steps.versions.outputs.pack_current }}
DIVE_CURRENT: ${{ steps.versions.outputs.dive_current }}
HADOLINT_CURRENT: ${{ steps.versions.outputs.hadolint_current }}
YQ_CURRENT: ${{ steps.versions.outputs.yq_current }}
run: |
update_version() {
local name="$1" current="$2" latest="$3"
if [ "${current}" != "${latest}" ]; then
sed -i "s/${name}=${current}/${name}=${latest}/g" Containerfile manifest.yaml
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The sed command uses /g (global replace) which could potentially replace version numbers in comments or other unexpected places throughout both Containerfile and manifest.yaml. Since version numbers should appear only once per file in specific locations (ARG declarations and manifest args), this global replacement is risky. Consider using more specific patterns or line-based replacement to target only the intended ARG and manifest declarations.

Suggested change
sed -i "s/${name}=${current}/${name}=${latest}/g" Containerfile manifest.yaml
# Update ARG declaration in Containerfile, e.g.:
# ARG ARGO_VERSION=1.2.3
sed -i -E "s/^(ARG ${name}=)${current}$/\1${latest}/" Containerfile
# Update YAML key in manifest.yaml, e.g.:
# ARGO_VERSION: 1.2.3
sed -i -E "s/^([[:space:]]*${name}:[[:space:]]*)${current}/\1${latest}/" manifest.yaml

Copilot uses AI. Check for mistakes.
fi
}

update_version "ARGO_VERSION" "${ARGO_CURRENT}" "${ARGO_LATEST}"
update_version "KARGO_VERSION" "${KARGO_CURRENT}" "${KARGO_LATEST}"
update_version "PACK_VERSION" "${PACK_CURRENT}" "${PACK_LATEST}"
update_version "DIVE_VERSION" "${DIVE_CURRENT}" "${DIVE_LATEST}"
update_version "HADOLINT_VERSION" "${HADOLINT_CURRENT}" "${HADOLINT_LATEST}"
update_version "YQ_VERSION" "${YQ_CURRENT}" "${YQ_LATEST}"

- name: Create pull request
if: steps.versions.outputs.has_updates == 'true'
uses: peter-evans/create-pull-request@v7
with:
commit-message: "chore: update tool versions"
branch: chore/update-tool-versions
title: "chore: update tool versions"
body: |
Automated tool version updates:

${{ steps.versions.outputs.summary }}
labels: dependencies
3 changes: 3 additions & 0 deletions .hadolint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
trustedRegistries:
- docker.io
- ghcr.io
30 changes: 30 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-merge-conflict
- id: detect-private-key

- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint-docker
entry: hadolint/hadolint hadolint
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The hadolint-docker hook uses a custom entry that runs the hadolint container, but this approach may not work correctly with pre-commit's hook execution model. The entry hadolint/hadolint hadolint appears to be malformed - it should either use the hadolint Docker image properly or use the hadolint binary directly. Consider using the standard hadolint hook configuration or verify this custom entry works as intended in local testing.

Suggested change
entry: hadolint/hadolint hadolint

Copilot uses AI. Check for mistakes.
args: ["--config", ".hadolint.yaml"]

- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0.1
hooks:
- id: shellcheck
args: ["-e", "SC1091"]

- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.21.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]
14 changes: 14 additions & 0 deletions .releaserc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
branches:
- master

plugins:
- "@semantic-release/commit-analyzer"
- "@semantic-release/release-notes-generator"
- - "@semantic-release/changelog"
- changelogFile: CHANGELOG.md
- - "@semantic-release/github"
- assets: []
- - "@semantic-release/git"
- assets:
- CHANGELOG.md
message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
Loading