Skip to content

chore(theme): re-vendor Xore/theme to 4a02619 #4072

chore(theme): re-vendor Xore/theme to 4a02619

chore(theme): re-vendor Xore/theme to 4a02619 #4072

Workflow file for this run

name: Containers
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:
permissions:
contents: read
packages: write
# The ci-target router dispatches the ci-heartbeat canary with
# GITHUB_TOKEN, and a called reusable workflow can never exceed the
# caller's envelope -- under-granting it startup-fails the whole run
# as "Invalid workflow file" (quality.yml's inline router instead
# elevates at its own job level).
actions: write
jobs:
# Executor routing ("homeserver first, GitHub-hosted fallback") -- the
# same ci-target decision quality.yml documents in full: trusted events
# (push to main, tags, workflow_dispatch; same-repo pull_request only
# when repo variable CI_HOMESERVER_PRS=true) may run on the homeserver,
# and only if a fresh ci-heartbeat canary proves a honeypot-ci runner
# actually executable right now. Fork pull_request runs never reach the
# box and land on GitHub-hosted exactly as before.
#
# Unlike quality.yml there is a single matrix job rather than PAIR twins:
# every row is executor-agnostic (buildx builds and -- on non-PR events
# -- ghcr pushes work identically under either runner), so the one job
# just picks its runs-on off the router output. The matrix name carries
# the "(GitHub-hosted)" suffix on fallback days per quality.yml's pair-
# naming rule, so a degraded day reads honestly in the checks list.
# Matrix rows serialize behind the single registered runner instance;
# the 120-min per-row ceiling only fires on a wedged pickup, mirroring
# the timeout-minutes-on-homeserver-only convention.
ci-target:
name: Pick CI executor
uses: ./.github/workflows/ci-router.yml
with:
ci_homeserver_prs: ${{ vars.CI_HOMESERVER_PRS || '' }}
build:
name: ${{ matrix.image }}${{ needs.ci-target.outputs.homeserver != 'true' && ' (GitHub-hosted)' || '' }}
needs: [ci-target]
runs-on: ${{ needs.ci-target.outputs.homeserver == 'true' && fromJSON('["self-hosted", "linux", "x64", "honeypot-ci"]') || fromJSON('["ubuntu-latest"]') }}
timeout-minutes: ${{ needs.ci-target.outputs.homeserver == 'true' && 120 || 360 }}
strategy:
fail-fast: false
# #1502: contexts below repointed at arcane/home/honeypot-<name>/ for
# every image whose stack migrated there (build context now lives
# self-contained next to its own compose.yml, not at repository
# root) -- llm-worker/portbridge/vps-portbridge are untouched, their
# stacks either stayed at their existing self-contained path or are
# VPS-side and out of #1502's scope entirely.
matrix:
include:
- image: agent-intrusion-worker
context: arcane/home/honeypot-agent-intrusion-worker/analysis/agent-intrusion-corpus
- image: backend-service
context: arcane/home/honeypot-dashboard/backend-service
- image: cisco-asa-honeypot
context: arcane/home/honeypot-cisco-asa-honeypot/cisco-asa-honeypot
- image: citrix-honeypot
context: arcane/home/honeypot-citrix-honeypot/citrix-honeypot
- image: conpot
context: arcane/home/honeypot-conpot/conpot
- image: dashboard-next
context: arcane/home/honeypot-dashboard/frontend-next
- image: dicompot
context: arcane/home/honeypot-dicompot/dicompot
- image: dionaea
context: arcane/home/honeypot-dionaea/dionaea
- image: dnp3-honeypot
context: arcane/home/honeypot-dnp3/dnp3-honeypot
- image: dns-honeypot
context: arcane/home/honeypot-dns-honeypot/dns-honeypot
- image: endlessh-honeypot
context: arcane/home/honeypot-endlessh/endlessh-honeypot
- image: http-honeypot
context: arcane/home/honeypot-http/http-honeypot
- image: llm-worker
context: llm-worker
- image: multipot
context: arcane/home/honeypot-multipot/multipot
- image: portbridge
context: portbridge
- image: rdp-honeypot
context: arcane/home/honeypot-rdp-honeypot/rdp-honeypot
- image: sonicwall-sma-honeypot
context: arcane/home/honeypot-sonicwall-sma/sonicwall-sma-honeypot
- image: tftp-relay
context: arcane/home/honeypot-dionaea/tftp-relay
- image: vps-portbridge
context: vps/portbridge
steps:
- uses: actions/checkout@v7
# #2819: point buildkit at the self-hosted executor's Docker Hub
# pull-through cache when there is one. This has to be buildkit's
# own config -- the docker-container driver runs its own containerd
# and never reads the host daemon's /etc/docker/daemon.json, so a
# mirror configured there would be silently ignored. The address
# comes from repo variable CI_REGISTRY_MIRROR (host:port) and is
# applied ONLY when ci-target actually routed us to the homeserver:
# a GitHub-hosted fallback runner cannot reach that address, and a
# mirror it cannot dial turns every base-image resolve into a
# timeout instead of a pull. Written as a file rather than an
# inline expression because the "no mirror" case still needs a
# valid (empty) config -- an unset buildkitd-config would be one
# more conditional step to keep in sync.
- name: Compose the buildkit registry config
id: buildkit
env:
MIRROR: ${{ needs.ci-target.outputs.homeserver == 'true' && vars.CI_REGISTRY_MIRROR || '' }}
run: |
set -euo pipefail
cfg="$RUNNER_TEMP/buildkitd.toml"
if [ -n "$MIRROR" ]; then
# http = true: the mirror is a plain-HTTP service bound to a
# container-only address on the executor, not a public
# registry, so there is no certificate to verify. Built with
# printf rather than a heredoc because a heredoc terminator
# has to sit at column 0, which is outside this YAML block
# scalar's indentation.
printf '%s\n' \
'[registry."docker.io"]' \
" mirrors = [\"$MIRROR\"]" \
"[registry.\"$MIRROR\"]" \
' http = true' >"$cfg"
echo "using registry mirror $MIRROR"
cat "$cfg"
else
echo '# no registry mirror configured for this executor' >"$cfg"
fi
echo "config=$cfg" >>"$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v4
with:
# Concurrent matrix rows share the single self-hosted runner
# instance; without an explicit stop+rm at job end, a builder
# left mid-flight after a graceful_stop aborts the next row's
# GHA cache export against the same builder (#2639).
cleanup: true
buildkitd-config: ${{ steps.buildkit.outputs.config }}
# #2819: authenticate to Docker Hub, INCLUDING on pull_request --
# that is precisely the event where the gap bites. Docker Hub meters
# anonymous pulls per source IP, all 18 matrix rows leave the
# homeserver through one address, and the tree carries 74 non-scratch
# Hub FROM lines, so a single cold run spends roughly three quarters
# of the ~100/6h anonymous budget and the run after it 429s. The
# layer cache does not help here: #2771's type=gha scopes store our
# own layers, never the base image, so every run re-resolves every
# FROM against the registry. Guarded on the secret being non-empty
# so a fork pull_request -- which by design cannot see secrets --
# keeps building anonymously rather than failing on empty creds.
- uses: docker/login-action@v4
if: env.DOCKERHUB_USERNAME != ''
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/login-action@v4
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v6
id: metadata
with:
images: ghcr.io/${{ github.repository_owner }}/honeypot-${{ matrix.image }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha
type=raw,value=latest,enable={{is_default_branch}}
# #2822: the type=gha layer cache measured 10.59 GB against GitHub's
# 10 GB per-repository ceiling -- over quota means GitHub is
# continuously LRU-evicting while the cache is in use, so it was not
# occasionally missing, it was being actively deleted between runs.
# On the homeserver executor (the common case -- see ci-target above)
# there is a persistent local disk right there, so export to
# type=local under /var/buildx-cache/<image> instead: it never
# touches the GHA quota, never crosses the network, and that box has
# 7.0 TB free on /var (measured 2026-09-02). The GitHub-hosted
# fallback path has no persistent disk between runs, so it keeps
# type=gha -- a cold cache there is the expected cost of falling
# back, not a regression. type=local has no eviction of any kind, so
# the prune step below bounds it; without that step this trades one
# unbounded-growth incident for a slower one.
- name: Pick cache backend
id: cache
run: |
# scripts/install-homeserver.sh's provision-buildx-cache step owns
# creating /var/buildx-cache as github-ci-runner (/var itself
# is root:root 0755, so this step cannot create it). Degrade to
# type=gha rather than `bash -e`-failing all 18 matrix rows if
# that step has not run on this box yet -- losing the local cache
# is a slow build, failing here is a Containers outage.
dir="/var/buildx-cache/${{ matrix.image }}"
# 2026-09-06: the box runs seven runner users, all in the
# github-ci-runner group, and any of them can take this row. With
# the default umask 022 the first one to build an image left
# <image> at 2755 -- group-owned but not group-writable -- so the
# next runner's mkdir inside it got EACCES and the row silently
# degraded to the type=gha cache #2822 moved off. 002 makes the
# dir 2775, writable by every runner.
umask 002
if [ "${{ needs.ci-target.outputs.homeserver }}" = "true" ] \
&& mkdir -p "$dir" 2>/dev/null && [ -w "$dir" ]; then
echo "from=type=local,src=$dir" >> "$GITHUB_OUTPUT"
echo "to=type=local,dest=$dir,mode=max" >> "$GITHUB_OUTPUT"
else
if [ "${{ needs.ci-target.outputs.homeserver }}" = "true" ]; then
echo "::warning::$dir is not writable by $(id -un) -- falling back to type=gha. Run scripts/install-homeserver.sh's provision-buildx-cache step (#2822)."
fi
echo "from=type=gha,scope=${{ matrix.image }}" >> "$GITHUB_OUTPUT"
echo "to=type=gha,mode=max,scope=${{ matrix.image }}" >> "$GITHUB_OUTPUT"
fi
- uses: docker/build-push-action@v7
with:
context: ${{ matrix.context }}
push: ${{ github.event_name != 'pull_request' }}
load: false
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
# Scope the layer cache per image (#2771). Without an explicit
# scope every matrix row defaults to `buildkit`, so all 18
# concurrent builds read and write ONE shared cache index per
# ref -- confirmed from the cache listing, which carried exactly
# one `index-buildkit-1-<hash>` key per git ref rather than one
# per image. Eighteen parallel `mode=max` read-modify-writes
# against a single index is a lost-update race: whichever row
# exports last wins and the rows it overwrote find their layers
# missing next run. dionaea showed it cleanly -- its apt layer
# is instruction #1 after a digest-pinned FROM, so nothing can
# invalidate it, yet it re-ran in 4 of 4 consecutive runs while
# backend-service's equivalent layer hit. Per-image scopes also
# keep the over-quota LRU eviction from taking out a
# neighbour's layers. (#2822 moved the homeserver's export off
# type=gha entirely, but the per-image scope stays -- the
# GitHub-hosted fallback still uses it and the local dest= path
# above is per-image on its own account.)
cache-from: ${{ steps.cache.outputs.from }}
cache-to: ${{ steps.cache.outputs.to }}
- name: Prune local buildx cache (#2822)
# type=local has no eviction of any kind -- run unconditionally
# (always(), not just on success) so a failed build still gets
# the chance to trim a cache dir it may have partially written.
# No-ops instantly on the GitHub-hosted fallback (script check).
if: always() && needs.ci-target.outputs.homeserver == 'true'
run: scripts/prune-buildx-cache.sh "/var/buildx-cache/${{ matrix.image }}"