From fb6ccf2279301bf2323fc997b5c5edfce6d019e2 Mon Sep 17 00:00:00 2001 From: Fernando Korndorfer Date: Sun, 8 Mar 2026 09:19:57 +0100 Subject: [PATCH 1/4] fix: eliminate code duplication in version extraction Refactored workflow to extract versions once in a dedicated job and reuse outputs across all dependent jobs, eliminating code duplication and ensuring consistency. Changes: - Added extract-versions job that runs first and outputs agent/os versions - All other jobs now depend on extract-versions and use needs.extract-versions.outputs - Removed duplicate version extraction code from build-and-test, build-push, and create-manifest jobs - Ensures version consistency across all pipeline stages Benefits: - Single source of truth for version numbers - Eliminates risk of version mismatches between jobs - Reduces workflow complexity and maintenance burden - Improves reliability by ensuring all jobs use identical versions --- .github/workflows/docker-image.yml | 106 +++++++++++++++-------------- 1 file changed, 55 insertions(+), 51 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index e9bd447..ff785d1 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -32,13 +32,12 @@ env: GHCR_IMAGE: ghcr.io/${{ github.repository }} jobs: - build-and-test: - name: Build and Test (${{ matrix.profile.name }}, ${{ matrix.platform }}) - runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - strategy: - matrix: - profile: ${{ github.event_name == 'pull_request' && fromJSON('[{"name":"full","docker":1,"azure_cli":1,"aws_cli":1,"powershell":1,"azure_pwsh":1,"aws_pwsh":1,"kubectl":1,"kubelogin":1,"kustomize":1,"helm":1,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":1}]') || fromJSON('[{"name":"full","docker":1,"azure_cli":1,"aws_cli":1,"powershell":1,"azure_pwsh":1,"aws_pwsh":1,"kubectl":1,"kubelogin":1,"kustomize":1,"helm":1,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":1},{"name":"minimal","docker":0,"azure_cli":0,"aws_cli":0,"powershell":0,"azure_pwsh":0,"aws_pwsh":0,"kubectl":0,"kubelogin":0,"kustomize":0,"helm":0,"jq":0,"yq":0,"terraform":0,"opentofu":0,"terraspace":0,"sudo":1,"gh_cli":0},{"name":"k8s","docker":1,"azure_cli":0,"aws_cli":0,"powershell":0,"azure_pwsh":0,"aws_pwsh":0,"kubectl":1,"kubelogin":1,"kustomize":1,"helm":1,"jq":1,"yq":1,"terraform":0,"opentofu":0,"terraspace":0,"sudo":1,"gh_cli":0},{"name":"iac","docker":1,"azure_cli":1,"aws_cli":1,"powershell":0,"azure_pwsh":0,"aws_pwsh":0,"kubectl":0,"kubelogin":0,"kustomize":0,"helm":0,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":0},{"name":"iac-pwsh","docker":1,"azure_cli":1,"aws_cli":1,"powershell":1,"azure_pwsh":1,"aws_pwsh":1,"kubectl":0,"kubelogin":0,"kustomize":0,"helm":0,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":0}]') }} - platform: ${{ github.event_name == 'pull_request' && fromJSON('["linux/amd64", "linux/arm64"]') || fromJSON('["linux/amd64"]') }} + extract-versions: + name: Extract Versions + runs-on: ubuntu-latest + outputs: + agent: ${{ needs.extract-versions.outputs.agent }} + os: ${{ needs.extract-versions.outputs.os }} steps: - name: Check out the repo uses: actions/checkout@v6 @@ -58,6 +57,18 @@ jobs: echo "agent=${AGENT_VERSION}" >> $GITHUB_OUTPUT echo "os=${OS_VERSION}" >> $GITHUB_OUTPUT + build-and-test: + name: Build and Test (${{ matrix.profile.name }}, ${{ matrix.platform }}) + runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + needs: extract-versions + strategy: + matrix: + profile: ${{ github.event_name == 'pull_request' && fromJSON('[{"name":"full","docker":1,"azure_cli":1,"aws_cli":1,"powershell":1,"azure_pwsh":1,"aws_pwsh":1,"kubectl":1,"kubelogin":1,"kustomize":1,"helm":1,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":1}]') || fromJSON('[{"name":"full","docker":1,"azure_cli":1,"aws_cli":1,"powershell":1,"azure_pwsh":1,"aws_pwsh":1,"kubectl":1,"kubelogin":1,"kustomize":1,"helm":1,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":1},{"name":"minimal","docker":0,"azure_cli":0,"aws_cli":0,"powershell":0,"azure_pwsh":0,"aws_pwsh":0,"kubectl":0,"kubelogin":0,"kustomize":0,"helm":0,"jq":0,"yq":0,"terraform":0,"opentofu":0,"terraspace":0,"sudo":1,"gh_cli":0},{"name":"k8s","docker":1,"azure_cli":0,"aws_cli":0,"powershell":0,"azure_pwsh":0,"aws_pwsh":0,"kubectl":1,"kubelogin":1,"kustomize":1,"helm":1,"jq":1,"yq":1,"terraform":0,"opentofu":0,"terraspace":0,"sudo":1,"gh_cli":0},{"name":"iac","docker":1,"azure_cli":1,"aws_cli":1,"powershell":0,"azure_pwsh":0,"aws_pwsh":0,"kubectl":0,"kubelogin":0,"kustomize":0,"helm":0,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":0},{"name":"iac-pwsh","docker":1,"azure_cli":1,"aws_cli":1,"powershell":1,"azure_pwsh":1,"aws_pwsh":1,"kubectl":0,"kubelogin":0,"kustomize":0,"helm":0,"jq":1,"yq":1,"terraform":1,"opentofu":1,"terraspace":1,"sudo":1,"gh_cli":0}]') }} + platform: ${{ github.event_name == 'pull_request' && fromJSON('["linux/amd64", "linux/arm64"]') || fromJSON('["linux/amd64"]') }} + steps: + - name: Check out the repo + uses: actions/checkout@v6 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -93,6 +104,8 @@ jobs: platforms: ${{ matrix.platform }} push: false target: ${{ matrix.profile.name }} + build-args: | + AGENT_VERSION=${{ needs.extract-versions.outputs.agent }} tags: ${{ env.REGISTRY_IMAGE }}:test-${{ matrix.profile.name }}-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} labels: ${{ steps.meta.outputs.labels }} cache-from: | @@ -136,13 +149,14 @@ jobs: continue-on-error: true - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.34.1 + uses: aquasecurity/trivy-action@0.35.0 with: image-ref: ${{ env.REGISTRY_IMAGE }}:test-${{ matrix.profile }}-${{ matrix.platform }} format: 'sarif' output: 'trivy-results-${{ matrix.profile }}-${{ matrix.platform }}.sarif' severity: 'CRITICAL' scanners: 'vuln' + ignore-unfixed: true continue-on-error: true - name: Upload Trivy results to GitHub Security @@ -153,13 +167,14 @@ jobs: continue-on-error: true - name: Run Trivy vulnerability scanner (table output) - uses: aquasecurity/trivy-action@0.34.1 + uses: aquasecurity/trivy-action@0.35.0 with: image-ref: ${{ env.REGISTRY_IMAGE }}:test-${{ matrix.profile }}-${{ matrix.platform }} format: 'table' exit-code: '0' severity: 'CRITICAL' scanners: 'vuln' + ignore-unfixed: true continue-on-error: true - name: Clean up disk space @@ -215,7 +230,7 @@ jobs: build-push: name: Build and Push (${{ matrix.profile.name }}, ${{ matrix.platform }}) runs-on: ${{ matrix.platform == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [build-and-test, security-scan, test] + needs: [extract-versions, build-and-test, security-scan, test] if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' strategy: matrix: @@ -316,21 +331,6 @@ jobs: - name: Check out the repo uses: actions/checkout@v6 - - name: Extract versions from Dockerfile - id: versions - run: | - # Use workflow input if provided, else repository variable, else Dockerfile default - if [ -n "${{ inputs.AGENT_VERSION }}" ]; then - AGENT_VERSION="${{ inputs.AGENT_VERSION }}" - elif [ -n "${{ vars.AGENT_VERSION }}" ]; then - AGENT_VERSION="${{ vars.AGENT_VERSION }}" - else - AGENT_VERSION=$(grep '^ARG AGENT_VERSION=' Dockerfile | cut -d'=' -f2) - fi - OS_VERSION=$(grep '^FROM ubuntu:' Dockerfile | cut -d':' -f2 | cut -d' ' -f1) - echo "agent=${AGENT_VERSION}" >> $GITHUB_OUTPUT - echo "os=${OS_VERSION}" >> $GITHUB_OUTPUT - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -362,9 +362,9 @@ jobs: push: true target: ${{ matrix.profile.name }} build-args: | - AGENT_VERSION=${{ steps.versions.outputs.agent }} + AGENT_VERSION=${{ needs.extract-versions.outputs.agent }} tags: | - ${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }}-${{ matrix.platform }} + ${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }}-${{ matrix.platform }} labels: ${{ steps.meta.outputs.labels }} cache-from: | type=gha,scope=base-${{ matrix.platform }} @@ -378,7 +378,7 @@ jobs: - name: Verify image was pushed run: | echo "Verifying image exists in registry..." - IMAGE="${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }}-${{ matrix.platform }}" + IMAGE="${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }}-${{ matrix.platform }}" # Wait a bit for the image to be available in the registry sleep 10 @@ -399,7 +399,7 @@ jobs: create-manifest: name: Create Multi-Arch Manifest (${{ matrix.profile.name }}) runs-on: ubuntu-latest - needs: build-push + needs: [extract-versions, build-push] if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' strategy: matrix: @@ -410,17 +410,6 @@ jobs: - name: iac - name: iac-pwsh steps: - - name: Check out the repo - uses: actions/checkout@v6 - - - name: Extract versions from Dockerfile - id: versions - run: | - AGENT_VERSION=$(grep '^ARG AGENT_VERSION=' Dockerfile | cut -d'=' -f2) - OS_VERSION=$(grep '^FROM ubuntu:' Dockerfile | cut -d':' -f2 | cut -d' ' -f1) - echo "agent=${AGENT_VERSION}" >> $GITHUB_OUTPUT - echo "os=${OS_VERSION}" >> $GITHUB_OUTPUT - - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: @@ -434,15 +423,30 @@ jobs: docker system prune -af # Verify both images exist before creating manifest - AMD64_IMAGE="${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }}-amd64" - ARM64_IMAGE="${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }}-arm64" + AMD64_IMAGE="${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }}-amd64" + ARM64_IMAGE="${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }}-arm64" echo "Verifying source images exist..." - docker buildx imagetools inspect "$AMD64_IMAGE" - docker buildx imagetools inspect "$ARM64_IMAGE" + + # Retry verification with exponential backoff + for i in {1..5}; do + if docker buildx imagetools inspect "$AMD64_IMAGE" && \ + docker buildx imagetools inspect "$ARM64_IMAGE"; then + echo "✓ Both images verified successfully" + break + fi + if [ $i -lt 5 ]; then + WAIT_TIME=$((i * 15)) + echo "Images not ready yet, waiting ${WAIT_TIME}s before retry $((i+1))/5..." + sleep $WAIT_TIME + else + echo "✗ Failed to verify images after 5 attempts" + exit 1 + fi + done # Create multi-arch manifest with retry logic - MANIFEST_TAG="${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }}" + MANIFEST_TAG="${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }}" for i in {1..3}; do if docker buildx imagetools create \ @@ -466,26 +470,26 @@ jobs: # Create shorter version tag (e.g., 2.321.0-full) docker buildx imagetools create \ - -t ${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-${{ matrix.profile.name }} \ - ${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }} + -t ${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-${{ matrix.profile.name }} \ + ${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }} # Create latest tag (e.g., latest-full) docker buildx imagetools create \ -t ${{ env.GHCR_IMAGE }}:latest-${{ matrix.profile.name }} \ - ${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }} + ${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }} # Create 'latest' tag only for full profile if [ "${{ matrix.profile.name }}" = "full" ]; then docker buildx imagetools create \ -t ${{ env.GHCR_IMAGE }}:latest \ - ${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-ubuntu${{ steps.versions.outputs.os }}-${{ matrix.profile.name }} + ${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-ubuntu${{ needs.extract-versions.outputs.os }}-${{ matrix.profile.name }} fi continue-on-error: true - name: Generate SBOM uses: anchore/sbom-action@v0 with: - image: ${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-${{ matrix.profile.name }} + image: ${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-${{ matrix.profile.name }} format: spdx-json output-file: sbom-spdx-${{ matrix.profile.name }}.json continue-on-error: true @@ -503,6 +507,6 @@ jobs: continue-on-error: true run: | docker buildx imagetools create \ - ${{ env.GHCR_IMAGE }}:${{ steps.versions.outputs.agent }}-${{ matrix.profile.name }} \ + ${{ env.GHCR_IMAGE }}:${{ needs.extract-versions.outputs.agent }}-${{ matrix.profile.name }} \ --tag ${{ env.GHCR_IMAGE }}:latest From 9c6646079a82f936c5d760cef54d66c7609710d5 Mon Sep 17 00:00:00 2001 From: Fernando Korndorfer Date: Sun, 8 Mar 2026 10:12:30 +0100 Subject: [PATCH 2/4] fix: update runner image tag in build commands and remove unused tool installations --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9e89f0a..6a8b423 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,6 @@ docker build --target full -t github-runner:full . # Build for specific architecture docker buildx build --platform linux/amd64 --target full -t github-runner:full-amd64 . ``` -- `ADD_YQ`: Installs `yq` tool -- `ADD_TERRAFORM`: Installs `terraform` tool -- `ADD_OPENTOFU`: Installs `opentofu` tool -- `ADD_TERRASPACE`: Installs `terraspace` tool -- `ADD_SUDO`: Installs and enables `sudo` for the runner user group ## Available Profiles @@ -140,10 +135,10 @@ export GITHUB_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxx" export RUNNER_LABELS="self-hosted,linux,x64" # Start the runners in privileged mode, one runner for each vCPU (default), using the parameters above: -sudo ./run.sh fok666/github-runner:latest $GITHUB_URL $GITHUB_TOKEN $RUNNER_LABELS +sudo ./run.sh fok666/github-runner:latest-full $GITHUB_URL $GITHUB_TOKEN $RUNNER_LABELS # Or specify a custom number of runners (e.g., 4 runners): -sudo ./run.sh fok666/github-runner:latest $GITHUB_URL $GITHUB_TOKEN $RUNNER_LABELS 4 +sudo ./run.sh fok666/github-runner:latest-full $GITHUB_URL $GITHUB_TOKEN $RUNNER_LABELS 4 ``` From 7a43761ce9a5fe1f7e9d383ddd1c9134613921f5 Mon Sep 17 00:00:00 2001 From: Fernando Korndorfer Date: Sun, 8 Mar 2026 10:12:53 +0100 Subject: [PATCH 3/4] feat: add GitHub Copilot instructions for self-hosted runner setup --- .github/copilot-instructions.md | 206 ++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..4ec3b7e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,206 @@ +# GitHub Copilot Instructions for GitHub Self-Hosted Runner + +## Project Overview + +This project builds and publishes multi-profile Docker images for running **GitHub Actions self-hosted runners** on Linux (Ubuntu 24.04). Images are multi-architecture (amd64/arm64), built using a multi-stage Dockerfile optimized for maximum layer reusability, and published to both Docker Hub and GitHub Container Registry (GHCR). + +**Key files:** + +| File | Purpose | +|------|---------| +| `Dockerfile` | Multi-stage build definition (all profiles) | +| `run.sh` | Launches N runner containers on a VM | +| `start.sh` | Entrypoint executed inside the container | +| `stop.sh` | Graceful runner deregistration | +| `test-tools.sh` | Smoke-tests installed tools at build time | +| `vmss_monitor.sh` | Handles Azure VMSS termination events | +| `ec2_monitor.sh` | Handles AWS EC2 spot-interruption notices | +| `checkRunnerVersion.sh` | Fetches latest GitHub Actions Runner release version | +| `ARCHITECTURE.md` | Deep-dive into multi-stage build design | + +## Repository Structure + +``` +. +├── Dockerfile # Multi-stage build (base → common → … → profiles) +├── run.sh # VM-side launcher +├── start.sh # Container entrypoint +├── stop.sh # Graceful shutdown / deregistration +├── test-tools.sh # Tool smoke tests +├── vmss_monitor.sh # Azure VMSS termination handler +├── ec2_monitor.sh # AWS EC2 spot termination handler +├── checkRunnerVersion.sh # Latest version helper +├── ARCHITECTURE.md # Build architecture documentation +└── .github/ + ├── copilot-instructions.md + ├── dependabot.yml + └── workflows/ + ├── docker-image.yml # Main CI: build, test, push multi-arch images + ├── docker-hub-release.yml # Publish release tags to Docker Hub + ├── docker-automate.yaml # Port.io-triggered automation workflow + └── version-check.yml # Scheduled upstream version checker +``` + +## Docker Image Profiles + +| Profile | Size | Description | Included Tools | +|---------|------|-------------|----------------| +| **minimal** | ~550 MB | Essential tools only | GitHub Runner, sudo | +| **k8s** | ~850 MB | Kubernetes-focused | + Docker, kubectl, kubelogin, kustomize, Helm, jq, yq | +| **iac** | ~1.75 GB | Infrastructure as Code (bash) | + Docker, Azure CLI, AWS CLI, Terraform, OpenTofu, Terraspace, jq, yq | +| **iac-pwsh** | ~2.25 GB | IaC with PowerShell | + PowerShell (Az + AWS modules) | +| **full** | ~2.45 GB | All tools | k8s + iac-pwsh combined | + +### Multi-Stage Build Layer Hierarchy + +``` +base (Ubuntu 24.04 + GitHub Actions Runner) +└── common (+ sudo) + ├── minimal ← PROFILE + └── docker-tools (+ Docker, jq, yq) + ├── k8s-tools (+ kubectl, kubelogin, kustomize, Helm) + │ └── k8s ← PROFILE + └── cloud-tools (+ Azure CLI, AWS CLI) + └── iac-tools (+ Terraform, OpenTofu, Terraspace) + ├── iac ← PROFILE + └── pwsh-tools (+ PowerShell + Az/AWS modules) + ├── iac-pwsh ← PROFILE + └── full-tools (+ k8s tools copied) + └── full ← PROFILE +``` + +The GitHub Actions runner binary is extracted from the official release tarball and `installdependencies.sh` is called during the `base` stage — do not move this to a later stage. + +## Common Commands + +```bash +# Build a specific profile locally +docker build --target minimal -t github-runner:minimal . +docker build --target full -t github-runner:full . + +# Multi-arch build (requires buildx) +docker buildx build --platform linux/amd64,linux/arm64 --target full \ + -t ghcr.io/fok666/github-selfhosted-runner:latest-full . + +# Run runners on a VM (auto-detects CPU count) +./run.sh [LABELS] [COUNT] +# Example: +./run.sh github-runner:2.321.0 https://github.com/myorg/myrepo ghs-xxxx "self-hosted,linux" 4 + +# Check latest upstream runner version +./checkRunnerVersion.sh + +# Smoke-test tools inside a running container +docker exec /test-tools.sh +``` + +## Architecture Patterns & Coding Standards + +### Dockerfile Guidelines + +- **Stage naming**: use lowercase kebab-case (`base`, `common`, `docker-tools`, etc.) +- **Final profile stages** are named after the profile: `minimal`, `k8s`, `iac`, `iac-pwsh`, `full` +- **COPY --from**: use named stages, never numeric indices +- **ARG scope**: re-declare `ARG TARGETARCH` in any stage that references it; GitHub runner archive uses `x64` for amd64 +- **Version pinning**: all tool versions are controlled via `ARG` at the top of the `base` stage +- **Cleanup**: always end `RUN` blocks that call `apt-get` with `&& apt clean && rm -rf /var/lib/apt/lists/*` +- **WORKDIR**: the runner is installed to `/runner`; do not change this — `start.sh` relies on it + +```dockerfile +# Pattern for mapping TARGETARCH to runner arch convention +RUN RUNNER_ARCH=$([ "${TARGETARCH}" = "amd64" ] && echo "x64" || echo "arm64") && \ + curl -LsS "https://github.com/actions/runner/releases/download/v${AGENT_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${AGENT_VERSION}.tar.gz" | tar -xz \ + && ./bin/installdependencies.sh +``` + +### Shell Script Guidelines + +- Always start with `#!/bin/bash` and `set -e` +- Validate all required parameters before using them; print `USAGE_HELP` and `exit 1` on failure +- Auto-detect CPU count; cap CPUs per runner at 2 (`MAX_CPU=$((CPU_COUNT > 1 ? 2 : 1))`) +- Use `jq` to safely construct JSON payloads — never string-concatenate JSON +- Token/URL validation: use regex guards (`[[ "$URL" =~ ^https?://... ]]`) + +### Workflow Guidelines + +- Use specific action versions (`@v6`, `@v4`, etc.) — never `@latest` +- Set `timeout-minutes` on all jobs +- Use `>> $GITHUB_OUTPUT` (not `set-output`) for step outputs +- Matrix strategy for profiles and platforms: + - **PR builds**: `full` profile only, both `linux/amd64` + `linux/arm64` + - **Push to main**: all profiles, `linux/amd64` only +- Always use `actions/checkout@v6` as the first step + +```yaml +# Correct output pattern +- name: Extract version + id: ver + run: echo "version=2.321.0" >> $GITHUB_OUTPUT + +- run: echo "Version is ${{ steps.ver.outputs.version }}" +``` + +## Test-Tools Smoke Test Pattern + +`test-tools.sh` uses `command -v ` guards so it is safe to run in any profile: + +```bash +if command -v gh &> /dev/null; then + echo "Testing GitHub CLI..." + gh --version + echo "GitHub CLI: OK" +fi +``` + +When adding a new tool, add a corresponding guarded test block to `test-tools.sh`. + +## Versioning & Release + +- Runner version is controlled by `ARG AGENT_VERSION=` in the Dockerfile +- To release a new version: update `AGENT_VERSION` in the Dockerfile **or** pass it as a workflow input/repository variable +- Image tags follow the pattern: `latest-`, `-`, `--` +- `checkRunnerVersion.sh` fetches the latest release from GitHub Releases (redirects to the tag URL) + +## VMSS / EC2 Termination Handling + +- `vmss_monitor.sh`: polls Azure IMDS (`169.254.169.254`) for `Terminate` scheduled events; calls `stop.sh` and acknowledges the event using `jq`-built JSON +- `ec2_monitor.sh`: polls AWS IMDS for spot-interruption notices; calls `stop.sh` +- `stop.sh`: deregisters the runner from GitHub and stops the container gracefully +- These scripts are designed to run from a cron job or systemd timer on the host VM + +## Common Pitfalls + +- **TARGETARCH vs runner arch**: GitHub runner archives use `x64` (not `amd64`) — always map with `$([ "${TARGETARCH}" = "amd64" ] && echo "x64" || echo "arm64")` +- **Runner registration token vs PAT**: `run.sh` expects a short-lived registration token (starts with `ghs-` or `ghp-`), not a PAT +- **Layer order**: put the heaviest stable layers earliest (AWS CLI, Azure CLI before Terraform) +- **Do not add tools to `base`**: the base stage is 100% shared; tool installation belongs in dedicated intermediate stages +- **Do not hardcode credentials**: use `${{ secrets.* }}` in workflows; environment variables in shell scripts +- **GHCR visibility**: ensure the package is set to public in GitHub organization/user settings +- **Port.io workflow** (`docker-automate.yaml`): requires `PORT_CLIENT_ID` and `PORT_CLIENT_SECRET` repository secrets + +## Adding a New Tool + +1. Decide which existing stage to build on (usually `docker-tools` or `cloud-tools`) +2. Create a new intermediate stage (e.g., `my-tool`) +3. Rebuild dependent profile stages referencing the new stage as their base +4. Add a guarded test block in `test-tools.sh` +5. Update profile size estimates in `README.md` and `ARCHITECTURE.md` +6. Update the workflow matrix in `docker-image.yml` to include/test the new tool flag + +## Security Considerations + +- Never open SSH in Docker images — access via `docker exec` or platform tooling +- `NOPASSWD:ALL` sudo is a conscious trade-off for CI/CD automation; document any changes +- Use `--no-install-recommends` in all `apt-get install` calls to minimize attack surface +- Prefer downloading binaries from official GitHub releases over third-party PPAs +- All tokens must come from environment variables or GitHub Secrets — never bake them into images +- The runner registration token is short-lived; `stop.sh` handles deregistration to avoid orphaned runners + +## References + +- [GitHub Actions Runner](https://github.com/actions/runner) +- [GitHub Actions Self-Hosted Runners](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners) +- [Docker Hub: fok666/github-runner](https://hub.docker.com/r/fok666/github-runner) +- [GitHub Container Registry](https://ghcr.io/fok666/github-selfhosted-runner) +- [Azure VMSS Scheduled Events](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/scheduled-events) +- [AWS EC2 Spot Interruption Notices](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-interruptions.html) From b37822262f275484802d06038f5be5fe599aa3da Mon Sep 17 00:00:00 2001 From: Fernando Korndorfer Date: Sun, 8 Mar 2026 10:30:06 +0100 Subject: [PATCH 4/4] fix: correct self-referential outputs in extract-versions job The job-level outputs referenced needs.extract-versions.outputs.* (self- referential), which always resolves to empty strings. This caused all downstream jobs to receive an empty AGENT_VERSION build-arg, resulting in a failed Docker build when constructing the runner download URL. Fix outputs to reference steps.versions.outputs.* as intended. --- .github/workflows/docker-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index ff785d1..4dfd7dd 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -36,8 +36,8 @@ jobs: name: Extract Versions runs-on: ubuntu-latest outputs: - agent: ${{ needs.extract-versions.outputs.agent }} - os: ${{ needs.extract-versions.outputs.os }} + agent: ${{ steps.versions.outputs.agent }} + os: ${{ steps.versions.outputs.os }} steps: - name: Check out the repo uses: actions/checkout@v6