Build and Publish the titus-scanner Docker Image workflow (SpecterOps/Nemesis)
The Build and Publish the titus-scanner Docker Image workflow from SpecterOps/Nemesis, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the Build and Publish the titus-scanner Docker Image workflow from the SpecterOps/Nemesis repository, a real project running GitHub Actions. It is shown here with attribution under its BSD-3-Clause license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
name: Build and Publish the titus-scanner Docker Image
on:
workflow_dispatch: # For manual triggering
push:
branches: [ "main" ]
# Trigger on changes to titus-scanner files
paths:
- 'projects/titus_scanner/**'
- '.github/workflows/docker-build-titus.yml'
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: "specterops/nemesis"
jobs:
build-titus-scanner:
strategy:
matrix:
include:
- name: titus-scanner
context: .
dockerfile: ./projects/titus_scanner/Dockerfile
runner: ubuntu-22.04
platform: linux/amd64
arch: amd64
- name: titus-scanner
context: .
dockerfile: ./projects/titus_scanner/Dockerfile
runner: ubuntu-22.04-arm
platform: linux/arm64
arch: arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
actions: write # Added for cache cleanup
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
with:
driver-opts: |
image=moby/buildkit:latest
network=host
- name: Log in to the Container registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for ${{ matrix.name }} image
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.name }}
tags: |
type=sha,format=short,suffix=-${{ matrix.arch }}
type=ref,event=branch,suffix=-${{ matrix.arch }}
type=ref,event=pr,suffix=-${{ matrix.arch }}
type=raw,value=latest-${{ matrix.arch }},enable={{is_default_branch}}
type=raw,value=cache-${{ matrix.arch }}
- name: Build and push ${{ matrix.name }} image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
timeout-minutes: 30
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
push: true
platforms: ${{ matrix.platform }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: |
type=gha,scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}
${{ matrix.arch == 'amd64' && 'type=gha,scope=titus-scanner-amd64-main' || 'type=gha,scope=titus-scanner-arm64-main' }}
cache-to: |
type=gha,mode=${{ matrix.arch == 'arm64' && 'min' || 'max' }},scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}
- name: Test built image
run: |
docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner:${{ steps.meta.outputs.version }} --help || true
# Simple cache cleanup - keep only the 2 most recent caches
- name: Simple cache cleanup
if: success()
run: |
gh extension install actions/gh-actions-cache || true
gh actions-cache list --key "titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}" --sort created-at --order desc | tail -n +3 | cut -f1 | xargs -I {} gh actions-cache delete {} --confirm || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
# Create multi-arch manifest
create-manifest:
needs: build-titus-scanner
runs-on: ubuntu-22.04
permissions:
contents: read
packages: write
steps:
- name: Log in to the Container registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for manifest
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner
tags: |
type=sha,format=short
type=ref,event=branch
type=ref,event=pr
type=raw,value=latest,enable={{is_default_branch}}
- name: Create and push multi-arch manifest
run: |
# Create multi-arch manifests for each tag
echo '${{ steps.meta.outputs.tags }}' | while IFS= read -r tag; do
if [ -n "$tag" ]; then
echo "Creating manifest for: $tag"
docker buildx imagetools create \
--tag "$tag" \
"${tag}-amd64" \
"${tag}-arm64"
fi
done
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
name: Build and Publish the titus-scanner Docker Image on: workflow_dispatch: # For manual triggering push: branches: [ "main" ] # Trigger on changes to titus-scanner files paths: - 'projects/titus_scanner/**' - '.github/workflows/docker-build-titus.yml' env: REGISTRY: ghcr.io IMAGE_PREFIX: "specterops/nemesis" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-titus-scanner: timeout-minutes: 30 strategy: matrix: include: - name: titus-scanner context: . dockerfile: ./projects/titus_scanner/Dockerfile runner: ubuntu-22.04 platform: linux/amd64 arch: amd64 - name: titus-scanner context: . dockerfile: ./projects/titus_scanner/Dockerfile runner: ubuntu-22.04-arm platform: linux/arm64 arch: arm64 runs-on: ${{ matrix.runner }} permissions: contents: read packages: write actions: write # Added for cache cleanup steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 with: driver-opts: | image=moby/buildkit:latest network=host - name: Log in to the Container registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for ${{ matrix.name }} image id: meta uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.name }} tags: | type=sha,format=short,suffix=-${{ matrix.arch }} type=ref,event=branch,suffix=-${{ matrix.arch }} type=ref,event=pr,suffix=-${{ matrix.arch }} type=raw,value=latest-${{ matrix.arch }},enable={{is_default_branch}} type=raw,value=cache-${{ matrix.arch }} - name: Build and push ${{ matrix.name }} image uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7 timeout-minutes: 30 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} push: true platforms: ${{ matrix.platform }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: | type=gha,scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }} ${{ matrix.arch == 'amd64' && 'type=gha,scope=titus-scanner-amd64-main' || 'type=gha,scope=titus-scanner-arm64-main' }} cache-to: | type=gha,mode=${{ matrix.arch == 'arm64' && 'min' || 'max' }},scope=titus-scanner-${{ matrix.arch }}-${{ github.ref_name }} - name: Test built image run: | docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner:${{ steps.meta.outputs.version }} --help || true # Simple cache cleanup - keep only the 2 most recent caches - name: Simple cache cleanup if: success() run: | gh extension install actions/gh-actions-cache || true gh actions-cache list --key "titus-scanner-${{ matrix.arch }}-${{ github.ref_name }}" --sort created-at --order desc | tail -n +3 | cut -f1 | xargs -I {} gh actions-cache delete {} --confirm || true env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} continue-on-error: true # Create multi-arch manifest create-manifest: timeout-minutes: 30 needs: build-titus-scanner runs-on: latchkey-small permissions: contents: read packages: write steps: - name: Log in to the Container registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata for manifest id: meta uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/titus-scanner tags: | type=sha,format=short type=ref,event=branch type=ref,event=pr type=raw,value=latest,enable={{is_default_branch}} - name: Create and push multi-arch manifest run: | # Create multi-arch manifests for each tag echo '${{ steps.meta.outputs.tags }}' | while IFS= read -r tag; do if [ -n "$tag" ]; then echo "Creating manifest for: $tag" docker buildx imagetools create \ --tag "$tag" \ "${tag}-amd64" \ "${tag}-arm64" fi done
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Container pulls and builds
This workflow runs 2 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.