chore(theme): re-vendor Xore/theme to 4a02619 #4072
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" |