Skip to content

Commit 6db1c23

Browse files
committed
feat: add semantic-release pipeline with build tools and best practices
- Add build tools to image (dive, trivy, buildah, yq, hadolint) so the image can build itself as a self-hosted runner - Configure buildah vfs storage driver for container/rootless usage - Create semantic-release config for automated versioning from conventional commits with changelog generation - Add release workflow: semantic-release -> buildah build -> dive filesystem scan -> trivy vulnerability scan -> skopeo push with semver tags (major, major.minor, full, latest) - Add CI workflow: commitlint, hadolint lint, and build test on PRs - Update scheduled update-tools workflow with new tools (dive, hadolint, yq) - Add best practice configs: .hadolint.yaml (trusted registries), .commitlintrc.yaml (conventional commits), .containerignore (minimal build context) https://claude.ai/code/session_01RofXXAMZxK4irobNYjYn3W
1 parent 02d7b2e commit 6db1c23

9 files changed

Lines changed: 305 additions & 13 deletions

File tree

.commitlintrc.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
extends:
2+
- "@commitlint/config-conventional"

.containerignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.git
2+
.github
3+
build
4+
.env
5+
*.md
6+
LICENSE
7+
.dive-ci
8+
.hadolint.yaml
9+
.releaserc.yaml
10+
.commitlintrc.yaml
11+
.containerignore
12+
scripts/

.github/workflows/ci.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [master]
6+
7+
permissions:
8+
contents: read
9+
pull-requests: read
10+
11+
jobs:
12+
commitlint:
13+
name: Lint commit messages
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- uses: wagoid/commitlint-github-action@v6
21+
22+
hadolint:
23+
name: Lint Containerfile
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- uses: hadolint/hadolint-action@v3.1.0
29+
with:
30+
dockerfile: Containerfile
31+
32+
build:
33+
name: Test build
34+
runs-on: ubuntu-latest
35+
needs: [hadolint]
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- name: Build image
40+
run: docker build -f Containerfile -t test-build .

.github/workflows/release.yaml

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches: [master]
6+
7+
permissions:
8+
contents: write
9+
packages: write
10+
11+
jobs:
12+
release:
13+
name: Semantic release
14+
runs-on: ubuntu-latest
15+
outputs:
16+
new_release_published: ${{ steps.semantic.outputs.new_release_published }}
17+
new_release_version: ${{ steps.semantic.outputs.new_release_version }}
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
persist-credentials: false
22+
23+
- uses: cycjimmy/semantic-release-action@v4
24+
id: semantic
25+
with:
26+
extra_plugins: |
27+
@semantic-release/changelog
28+
@semantic-release/git
29+
env:
30+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
32+
build-and-push:
33+
name: Build, scan & push
34+
needs: release
35+
if: needs.release.outputs.new_release_published == 'true'
36+
runs-on: ubuntu-latest
37+
env:
38+
IMAGE_VERSION: ${{ needs.release.outputs.new_release_version }}
39+
steps:
40+
- uses: actions/checkout@v4
41+
42+
- name: Install build tools
43+
run: ./scripts/install_tools.sh
44+
45+
- name: Read manifest
46+
id: manifest
47+
run: |
48+
echo "image_name=$(yq e '.name' manifest.yaml)" >> "$GITHUB_OUTPUT"
49+
echo "registry=$(yq e '.registry' manifest.yaml)" >> "$GITHUB_OUTPUT"
50+
echo "format=$(yq e '.build.format' manifest.yaml)" >> "$GITHUB_OUTPUT"
51+
52+
- name: Validate Containerfile
53+
run: |
54+
docker pull -q ghcr.io/hadolint/hadolint:latest
55+
docker run --rm -i hadolint/hadolint:latest < Containerfile
56+
57+
- name: Build image
58+
env:
59+
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
60+
IMAGE_FORMAT: ${{ steps.manifest.outputs.format }}
61+
run: |
62+
# Build args from manifest
63+
BUILD_ARGS=""
64+
for arg in $(yq e '.build.args[]' manifest.yaml); do
65+
BUILD_ARGS="${BUILD_ARGS} --build-arg ${arg}"
66+
done
67+
68+
# Labels from manifest
69+
LABELS=""
70+
while IFS= read -r label; do
71+
if [[ -n "${label}" ]]; then
72+
label_key="${label%%=*}"
73+
label_value="${label#*=}"
74+
label_value="${label_value%\"}"
75+
label_value="${label_value#\"}"
76+
LABELS="${LABELS} --label ${label_key}=${label_value}"
77+
fi
78+
done < <(yq e '.build.labels[]' manifest.yaml)
79+
80+
# Add version label
81+
LABELS="${LABELS} --label org.opencontainers.image.version=${IMAGE_VERSION}"
82+
83+
# shellcheck disable=SC2086
84+
buildah build \
85+
--squash \
86+
--pull-always \
87+
--format "${IMAGE_FORMAT}" \
88+
${BUILD_ARGS} \
89+
${LABELS} \
90+
--tag "${IMAGE_NAME}:${IMAGE_VERSION}" \
91+
.
92+
93+
# Save to OCI archive for scanning and pushing
94+
mkdir -p build
95+
buildah push "${IMAGE_NAME}:${IMAGE_VERSION}" "oci-archive:build/${IMAGE_NAME}.tar"
96+
97+
# Load into Docker daemon for dive scan
98+
docker load -i "build/${IMAGE_NAME}.tar" 2>/dev/null || true
99+
docker tag "$(docker images -q | head -1)" "${IMAGE_NAME}:${IMAGE_VERSION}" 2>/dev/null || true
100+
101+
- name: Dive filesystem scan
102+
env:
103+
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
104+
run: dive --ci --source=docker "${IMAGE_NAME}:${IMAGE_VERSION}"
105+
106+
- name: Trivy vulnerability scan
107+
env:
108+
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
109+
run: |
110+
trivy image \
111+
--input "build/${IMAGE_NAME}.tar" \
112+
--severity HIGH,CRITICAL \
113+
--exit-code 1 \
114+
"${IMAGE_NAME}:${IMAGE_VERSION}"
115+
116+
- name: Login to GHCR
117+
env:
118+
REGISTRY: ${{ steps.manifest.outputs.registry }}
119+
run: skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
120+
121+
- name: Push to registry
122+
env:
123+
IMAGE_NAME: ${{ steps.manifest.outputs.image_name }}
124+
REGISTRY: ${{ steps.manifest.outputs.registry }}
125+
run: |
126+
MAJOR_MINOR="${IMAGE_VERSION%.*}"
127+
MAJOR="${IMAGE_VERSION%%.*}"
128+
129+
# Push semantic version tag (1.2.3)
130+
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:${IMAGE_VERSION}"
131+
132+
# Push major.minor tag (1.2)
133+
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:${MAJOR_MINOR}"
134+
135+
# Push major tag (1)
136+
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:${MAJOR}"
137+
138+
# Push latest tag
139+
skopeo copy --all "oci-archive:build/${IMAGE_NAME}.tar" "docker://${REGISTRY}:latest"
140+
141+
- name: Verify pushed image
142+
env:
143+
REGISTRY: ${{ steps.manifest.outputs.registry }}
144+
run: |
145+
skopeo inspect "docker://${REGISTRY}:${IMAGE_VERSION}" --format '{{.Labels}}'

.github/workflows/update-tools.yaml

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,30 @@ jobs:
2929
ARGO_LATEST=$(get_latest_version "argoproj/argo-workflows")
3030
KARGO_LATEST=$(get_latest_version "akuity/kargo")
3131
PACK_LATEST=$(get_latest_version "buildpacks/pack")
32+
DIVE_LATEST=$(get_latest_version "wagoodman/dive")
33+
HADOLINT_LATEST=$(get_latest_version "hadolint/hadolint")
34+
YQ_LATEST=$(get_latest_version "mikefarah/yq")
3235
3336
echo "argo=${ARGO_LATEST}" >> "$GITHUB_OUTPUT"
3437
echo "kargo=${KARGO_LATEST}" >> "$GITHUB_OUTPUT"
3538
echo "pack=${PACK_LATEST}" >> "$GITHUB_OUTPUT"
39+
echo "dive=${DIVE_LATEST}" >> "$GITHUB_OUTPUT"
40+
echo "hadolint=${HADOLINT_LATEST}" >> "$GITHUB_OUTPUT"
41+
echo "yq=${YQ_LATEST}" >> "$GITHUB_OUTPUT"
3642
3743
ARGO_CURRENT=$(grep -oP 'ARGO_VERSION=\K[0-9.]+' Containerfile)
3844
KARGO_CURRENT=$(grep -oP 'KARGO_VERSION=\K[0-9.]+' Containerfile)
3945
PACK_CURRENT=$(grep -oP 'PACK_VERSION=\K[0-9.]+' Containerfile)
46+
DIVE_CURRENT=$(grep -oP 'DIVE_VERSION=\K[0-9.]+' Containerfile)
47+
HADOLINT_CURRENT=$(grep -oP 'HADOLINT_VERSION=\K[0-9.]+' Containerfile)
48+
YQ_CURRENT=$(grep -oP 'YQ_VERSION=\K[0-9.]+' Containerfile)
4049
4150
echo "argo_current=${ARGO_CURRENT}" >> "$GITHUB_OUTPUT"
4251
echo "kargo_current=${KARGO_CURRENT}" >> "$GITHUB_OUTPUT"
4352
echo "pack_current=${PACK_CURRENT}" >> "$GITHUB_OUTPUT"
53+
echo "dive_current=${DIVE_CURRENT}" >> "$GITHUB_OUTPUT"
54+
echo "hadolint_current=${HADOLINT_CURRENT}" >> "$GITHUB_OUTPUT"
55+
echo "yq_current=${YQ_CURRENT}" >> "$GITHUB_OUTPUT"
4456
4557
UPDATES=""
4658
if [ "${ARGO_CURRENT}" != "${ARGO_LATEST}" ]; then
@@ -52,6 +64,15 @@ jobs:
5264
if [ "${PACK_CURRENT}" != "${PACK_LATEST}" ]; then
5365
UPDATES="${UPDATES}- pack (Buildpacks): ${PACK_CURRENT} -> ${PACK_LATEST}\n"
5466
fi
67+
if [ "${DIVE_CURRENT}" != "${DIVE_LATEST}" ]; then
68+
UPDATES="${UPDATES}- dive: ${DIVE_CURRENT} -> ${DIVE_LATEST}\n"
69+
fi
70+
if [ "${HADOLINT_CURRENT}" != "${HADOLINT_LATEST}" ]; then
71+
UPDATES="${UPDATES}- hadolint: ${HADOLINT_CURRENT} -> ${HADOLINT_LATEST}\n"
72+
fi
73+
if [ "${YQ_CURRENT}" != "${YQ_LATEST}" ]; then
74+
UPDATES="${UPDATES}- yq: ${YQ_CURRENT} -> ${YQ_LATEST}\n"
75+
fi
5576
5677
if [ -z "${UPDATES}" ]; then
5778
echo "has_updates=false" >> "$GITHUB_OUTPUT"
@@ -73,19 +94,29 @@ jobs:
7394
ARGO_LATEST: ${{ steps.versions.outputs.argo }}
7495
KARGO_LATEST: ${{ steps.versions.outputs.kargo }}
7596
PACK_LATEST: ${{ steps.versions.outputs.pack }}
97+
DIVE_LATEST: ${{ steps.versions.outputs.dive }}
98+
HADOLINT_LATEST: ${{ steps.versions.outputs.hadolint }}
99+
YQ_LATEST: ${{ steps.versions.outputs.yq }}
76100
ARGO_CURRENT: ${{ steps.versions.outputs.argo_current }}
77101
KARGO_CURRENT: ${{ steps.versions.outputs.kargo_current }}
78102
PACK_CURRENT: ${{ steps.versions.outputs.pack_current }}
103+
DIVE_CURRENT: ${{ steps.versions.outputs.dive_current }}
104+
HADOLINT_CURRENT: ${{ steps.versions.outputs.hadolint_current }}
105+
YQ_CURRENT: ${{ steps.versions.outputs.yq_current }}
79106
run: |
80-
if [ "${ARGO_CURRENT}" != "${ARGO_LATEST}" ]; then
81-
sed -i "s/ARGO_VERSION=${ARGO_CURRENT}/ARGO_VERSION=${ARGO_LATEST}/g" Containerfile manifest.yaml
82-
fi
83-
if [ "${KARGO_CURRENT}" != "${KARGO_LATEST}" ]; then
84-
sed -i "s/KARGO_VERSION=${KARGO_CURRENT}/KARGO_VERSION=${KARGO_LATEST}/g" Containerfile manifest.yaml
85-
fi
86-
if [ "${PACK_CURRENT}" != "${PACK_LATEST}" ]; then
87-
sed -i "s/PACK_VERSION=${PACK_CURRENT}/PACK_VERSION=${PACK_LATEST}/g" Containerfile manifest.yaml
88-
fi
107+
update_version() {
108+
local name="$1" current="$2" latest="$3"
109+
if [ "${current}" != "${latest}" ]; then
110+
sed -i "s/${name}=${current}/${name}=${latest}/g" Containerfile manifest.yaml
111+
fi
112+
}
113+
114+
update_version "ARGO_VERSION" "${ARGO_CURRENT}" "${ARGO_LATEST}"
115+
update_version "KARGO_VERSION" "${KARGO_CURRENT}" "${KARGO_LATEST}"
116+
update_version "PACK_VERSION" "${PACK_CURRENT}" "${PACK_LATEST}"
117+
update_version "DIVE_VERSION" "${DIVE_CURRENT}" "${DIVE_LATEST}"
118+
update_version "HADOLINT_VERSION" "${HADOLINT_CURRENT}" "${HADOLINT_LATEST}"
119+
update_version "YQ_VERSION" "${YQ_CURRENT}" "${YQ_LATEST}"
89120
90121
- name: Create pull request
91122
if: steps.versions.outputs.has_updates == 'true'

.hadolint.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
trustedRegistries:
2+
- docker.io
3+
- ghcr.io

.releaserc.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
branches:
2+
- master
3+
4+
plugins:
5+
- "@semantic-release/commit-analyzer"
6+
- "@semantic-release/release-notes-generator"
7+
- - "@semantic-release/changelog"
8+
- changelogFile: CHANGELOG.md
9+
- - "@semantic-release/github"
10+
- assets: []
11+
- - "@semantic-release/git"
12+
- assets:
13+
- CHANGELOG.md
14+
message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"

Containerfile

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,51 @@ RUN apt-get update \
3232
&& apt-get clean \
3333
&& rm -rf /var/lib/apt/lists/*
3434

35+
# Install buildah
36+
# hadolint ignore=DL3008
37+
RUN apt-get update \
38+
&& apt-get install --no-install-recommends -y buildah \
39+
&& apt-get clean \
40+
&& rm -rf /var/lib/apt/lists/*
41+
42+
# Configure buildah storage for container/rootless usage
43+
RUN mkdir -p /etc/containers \
44+
&& printf '[storage]\ndriver = "vfs"\n' > /etc/containers/storage.conf
45+
46+
# Install trivy (vulnerability scanner)
47+
# hadolint ignore=DL3008,DL4006
48+
RUN curl -fsSL https://aquasecurity.github.io/trivy-repo/deb/public.key \
49+
| gpg --dearmor -o /usr/share/keyrings/trivy.gpg \
50+
&& echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" \
51+
| tee /etc/apt/sources.list.d/trivy.list \
52+
&& apt-get update \
53+
&& apt-get install --no-install-recommends -y trivy \
54+
&& apt-get clean \
55+
&& rm -rf /var/lib/apt/lists/*
56+
57+
# Install dive (container filesystem analysis)
58+
ARG DIVE_VERSION=0.12.0
59+
# hadolint ignore=DL3008
60+
RUN curl -sSL -o /tmp/dive.deb \
61+
"https://github.com/wagoodman/dive/releases/download/v${DIVE_VERSION}/dive_${DIVE_VERSION}_linux_amd64.deb" \
62+
&& apt-get update \
63+
&& apt-get install --no-install-recommends -y /tmp/dive.deb \
64+
&& rm /tmp/dive.deb \
65+
&& apt-get clean \
66+
&& rm -rf /var/lib/apt/lists/*
67+
68+
# Install hadolint (Dockerfile/Containerfile linter)
69+
ARG HADOLINT_VERSION=2.12.0
70+
RUN curl -sSL -o /usr/local/bin/hadolint \
71+
"https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-Linux-x86_64" \
72+
&& chmod +x /usr/local/bin/hadolint
73+
74+
# Install yq (YAML processor)
75+
ARG YQ_VERSION=4.45.4
76+
RUN curl -sSL -o /usr/local/bin/yq \
77+
"https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_amd64" \
78+
&& chmod +x /usr/local/bin/yq
79+
3580
# Install Argo Workflows CLI
3681
ARG ARGO_VERSION=3.6.4
3782
RUN curl -sSL -o /tmp/argo-linux-amd64.gz \
@@ -85,6 +130,3 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh
85130

86131
# Add Poetry and UV to PATH
87132
RUN echo "export PATH=\"${APP_HOME}/.local/bin:\$PATH\"" >> ~/.bashrc
88-
89-
# Placeholder command to keep the container running
90-
# CMD ["/bin/bash", "-c", "while true; do sleep 1; done"]

manifest.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: python-github-runner
2-
tags:
2+
tags:
33
- latest
44
registry: ghcr.io/deerhide/python-github-runner
55
build:
@@ -9,6 +9,9 @@ build:
99
- ARGO_VERSION=3.6.4
1010
- KARGO_VERSION=1.9.2
1111
- PACK_VERSION=0.36.4
12+
- DIVE_VERSION=0.12.0
13+
- HADOLINT_VERSION=2.12.0
14+
- YQ_VERSION=4.45.4
1215
labels:
1316
- org.opencontainers.image.source=https://github.com/deerhide/python-github-runner
1417
- org.opencontainers.image.description="Python GitHub Runner"

0 commit comments

Comments
 (0)