Create reusable runner image build action
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Meghdad Fadaee
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
106
README.md
Normal file
106
README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Build Runner Image Action
|
||||
|
||||
A reusable composite action for Gitea Actions that builds a Docker runner image,
|
||||
runs project-specific verification commands, and publishes both an immutable Git
|
||||
revision tag and `latest` to a private Gitea Container Registry.
|
||||
|
||||
The action is hosted on Gitea and does not use GitHub. Call it with an absolute
|
||||
URL, which Gitea requires for actions hosted outside the configured default
|
||||
action source:
|
||||
|
||||
```yaml
|
||||
- name: Build, verify, and publish image
|
||||
uses: https://mahgit.ir/MeghdadFadaee/build-runner-image-action@v1
|
||||
with:
|
||||
registry: mahgit.ir
|
||||
image: mahgit.ir/example/project-runner
|
||||
source-url: https://mahgit.ir/example/project
|
||||
registry-username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
registry-token: ${{ secrets.REGISTRY_TOKEN }}
|
||||
verify-commands: |
|
||||
docker run --rm "$IMAGE_REF" node --version
|
||||
docker run --rm "$IMAGE_REF" python3 --version
|
||||
```
|
||||
|
||||
`IMAGE_REF` is available only to `verify-commands` and contains the immutable
|
||||
candidate image reference. The action publishes `latest` only after every
|
||||
verification command succeeds.
|
||||
|
||||
## Default caller layout
|
||||
|
||||
```text
|
||||
project/
|
||||
├── .dockerignore
|
||||
├── requirements.lock
|
||||
└── .gitea/workflows/build-runner-image.yml
|
||||
```
|
||||
|
||||
The caller does not need a Dockerfile. Use this `.dockerignore` so the root
|
||||
build context sends only the dependency lock to Docker:
|
||||
|
||||
```dockerignore
|
||||
**
|
||||
!requirements.lock
|
||||
```
|
||||
|
||||
If `requirements-file` points somewhere else, update the exception to include
|
||||
that path.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Required | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `registry` | Yes | - | Registry hostname |
|
||||
| `image` | Yes | - | Fully qualified image name without a tag |
|
||||
| `base-image` | No | Gitea-hosted `node-20-bookworm` | Base image override |
|
||||
| `context` | No | `.` | Docker build context |
|
||||
| `requirements-file` | No | `requirements.lock` | Lock path relative to the context |
|
||||
| `dockerfile` | No | Shared Dockerfile | Optional caller Dockerfile override |
|
||||
| `source-url` | Yes | - | OCI source-label URL |
|
||||
| `registry-username` | Yes | - | Gitea registry username |
|
||||
| `registry-token` | Yes | - | PAT with package Read and Write permission |
|
||||
| `verify-commands` | No | Empty | Trusted shell commands using `IMAGE_REF` |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
| --- | --- |
|
||||
| `image` | Image name without a tag |
|
||||
| `revision` | Twelve-character Git revision |
|
||||
| `revision_tag` | Immutable published image reference |
|
||||
| `latest_tag` | Published `latest` image reference |
|
||||
|
||||
## Runner requirements
|
||||
|
||||
- The caller must check out its repository before invoking this action.
|
||||
- The default Dockerfile expects a fully pinned `requirements.lock` at the
|
||||
build-context root.
|
||||
- The job container must be Debian-based or already include a Docker client.
|
||||
- The Docker daemon socket must be available to the job container.
|
||||
- The runner should use `container.force_pull: true`.
|
||||
- Only trusted repositories should use a runner with Docker socket access.
|
||||
|
||||
The action installs the Docker client only when it is absent. It sends the
|
||||
registry token through standard input, logs out on exit, builds with `--pull`,
|
||||
sets OCI revision/source labels, verifies the candidate, then publishes the
|
||||
immutable and moving tags.
|
||||
|
||||
The default Dockerfile lives at `runner-image/Dockerfile` in this action. It
|
||||
installs the shared Debian, Git, Python, virtual-environment, and timezone
|
||||
dependencies. A caller can provide `requirements-file` for a differently named
|
||||
lock inside its build context, or `dockerfile` when it needs different system
|
||||
packages.
|
||||
|
||||
The default base is:
|
||||
|
||||
```text
|
||||
mahgit.ir/meghdadfadaee/build-runner-image-base:node-20-bookworm
|
||||
```
|
||||
|
||||
Use `base-image` only when a project needs a different base.
|
||||
|
||||
## Versioning
|
||||
|
||||
Create immutable releases such as `v1.0.0` and move the major `v1` tag only for
|
||||
backward-compatible releases. Caller workflows should use `@v1` rather than
|
||||
`@main`.
|
||||
159
action.yml
Normal file
159
action.yml
Normal file
@@ -0,0 +1,159 @@
|
||||
name: Build and publish runner image
|
||||
description: Build, verify, and publish a runner image to a private Gitea registry.
|
||||
|
||||
inputs:
|
||||
registry:
|
||||
description: Registry hostname, for example mahgit.ir.
|
||||
required: true
|
||||
image:
|
||||
description: Fully qualified image name without a tag.
|
||||
required: true
|
||||
base-image:
|
||||
description: Base image used by the default Dockerfile.
|
||||
required: false
|
||||
default: mahgit.ir/meghdadfadaee/build-runner-image-base:node-20-bookworm
|
||||
context:
|
||||
description: Docker build context relative to the checked-out repository.
|
||||
required: false
|
||||
default: .
|
||||
requirements-file:
|
||||
description: Requirements lock path relative to the Docker build context.
|
||||
required: false
|
||||
default: requirements.lock
|
||||
dockerfile:
|
||||
description: Optional caller Dockerfile path; defaults to the action Dockerfile.
|
||||
required: false
|
||||
default: ""
|
||||
source-url:
|
||||
description: Repository URL stored in the OCI source label.
|
||||
required: true
|
||||
registry-username:
|
||||
description: Registry username.
|
||||
required: true
|
||||
registry-token:
|
||||
description: Registry token with package Read and Write permission.
|
||||
required: true
|
||||
verify-commands:
|
||||
description: Shell commands that verify the candidate image using IMAGE_REF.
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
outputs:
|
||||
image:
|
||||
description: Fully qualified image name without a tag.
|
||||
value: ${{ steps.publish.outputs.image }}
|
||||
revision:
|
||||
description: Twelve-character Git revision used as the immutable tag.
|
||||
value: ${{ steps.publish.outputs.revision }}
|
||||
revision_tag:
|
||||
description: Fully qualified immutable image reference.
|
||||
value: ${{ steps.publish.outputs.revision_tag }}
|
||||
latest_tag:
|
||||
description: Fully qualified latest image reference.
|
||||
value: ${{ steps.publish.outputs.latest_tag }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install Docker client
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker version --format 'Docker client {{.Client.Version}}, server {{.Server.Version}}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v apt-get >/dev/null 2>&1; then
|
||||
echo "Docker is unavailable and this job image does not provide apt-get"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
--no-install-recommends docker.io; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "Unable to install the Docker client after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep "$((attempt * 5))"
|
||||
done
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
docker version --format 'Docker client {{.Client.Version}}, server {{.Server.Version}}'
|
||||
|
||||
- name: Build, verify, and publish image
|
||||
id: publish
|
||||
shell: sh
|
||||
env:
|
||||
INPUT_REGISTRY: ${{ inputs.registry }}
|
||||
INPUT_IMAGE: ${{ inputs.image }}
|
||||
INPUT_BASE_IMAGE: ${{ inputs.base-image }}
|
||||
INPUT_CONTEXT: ${{ inputs.context }}
|
||||
INPUT_REQUIREMENTS_FILE: ${{ inputs.requirements-file }}
|
||||
INPUT_DOCKERFILE: ${{ inputs.dockerfile }}
|
||||
INPUT_SOURCE_URL: ${{ inputs.source-url }}
|
||||
INPUT_REGISTRY_USERNAME: ${{ inputs.registry-username }}
|
||||
INPUT_REGISTRY_TOKEN: ${{ inputs.registry-token }}
|
||||
INPUT_VERIFY_COMMANDS: ${{ inputs.verify-commands }}
|
||||
ACTION_PATH: ${{ gitea.action_path }}
|
||||
run: |
|
||||
set -eu
|
||||
: "${INPUT_REGISTRY:?registry is required}"
|
||||
: "${INPUT_IMAGE:?image is required}"
|
||||
: "${INPUT_BASE_IMAGE:?base-image is required}"
|
||||
: "${INPUT_CONTEXT:?context is required}"
|
||||
: "${INPUT_REQUIREMENTS_FILE:?requirements-file is required}"
|
||||
: "${INPUT_SOURCE_URL:?source-url is required}"
|
||||
: "${INPUT_REGISTRY_USERNAME:?registry-username is required}"
|
||||
: "${INPUT_REGISTRY_TOKEN:?registry-token is required}"
|
||||
|
||||
revision="$(git rev-parse --short=12 HEAD)"
|
||||
candidate="$INPUT_IMAGE:$revision"
|
||||
latest="$INPUT_IMAGE:latest"
|
||||
dockerfile="$INPUT_DOCKERFILE"
|
||||
if [ -z "$dockerfile" ]; then
|
||||
dockerfile="$ACTION_PATH/runner-image/Dockerfile"
|
||||
fi
|
||||
if [ ! -f "$dockerfile" ]; then
|
||||
echo "Dockerfile does not exist: $dockerfile"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
docker logout "$INPUT_REGISTRY" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf '%s' "$INPUT_REGISTRY_TOKEN" | docker login "$INPUT_REGISTRY" \
|
||||
--username "$INPUT_REGISTRY_USERNAME" \
|
||||
--password-stdin
|
||||
|
||||
docker build \
|
||||
--pull \
|
||||
--label "org.opencontainers.image.revision=$revision" \
|
||||
--label "org.opencontainers.image.source=$INPUT_SOURCE_URL" \
|
||||
--build-arg "BASE_IMAGE=$INPUT_BASE_IMAGE" \
|
||||
--build-arg "REQUIREMENTS_FILE=$INPUT_REQUIREMENTS_FILE" \
|
||||
--file "$dockerfile" \
|
||||
--tag "$candidate" \
|
||||
"$INPUT_CONTEXT"
|
||||
|
||||
if [ -n "$INPUT_VERIFY_COMMANDS" ]; then
|
||||
export IMAGE_REF="$candidate"
|
||||
sh -eu -c "$INPUT_VERIFY_COMMANDS"
|
||||
fi
|
||||
|
||||
docker push "$candidate"
|
||||
docker tag "$candidate" "$latest"
|
||||
docker push "$latest"
|
||||
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
{
|
||||
echo "image=$INPUT_IMAGE"
|
||||
echo "revision=$revision"
|
||||
echo "revision_tag=$candidate"
|
||||
echo "latest_tag=$latest"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
26
runner-image/Dockerfile
Normal file
26
runner-image/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
||||
ARG BASE_IMAGE
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
git \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
tzdata && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG REQUIREMENTS_FILE=requirements.lock
|
||||
COPY ${REQUIREMENTS_FILE} /tmp/requirements.lock
|
||||
|
||||
RUN python3 -m venv /opt/runner-venv && \
|
||||
/opt/runner-venv/bin/pip install --no-cache-dir -r /tmp/requirements.lock && \
|
||||
/opt/runner-venv/bin/pip check && \
|
||||
rm /tmp/requirements.lock
|
||||
|
||||
ENV PATH="/opt/runner-venv/bin:$PATH" \
|
||||
PYTHONDONTWRITEBYTECODE="1" \
|
||||
PYTHONUNBUFFERED="1"
|
||||
|
||||
CMD ["bash"]
|
||||
Reference in New Issue
Block a user