No description
Find a file
2026-03-04 14:54:26 +01:00
.forgejo/workflows chore: disable VM smoke test pending KVM on runner 2026-02-18 13:51:19 +01:00
deploy-nix-site refactor: migrate from S3 to Attic binary cache 2026-02-27 21:19:09 +01:00
deploy-site feat: add NOMAD_TOKEN support and static-sites namespace 2026-02-18 00:18:34 +01:00
deploy-static-site refactor: migrate from S3 to Attic binary cache 2026-02-27 21:19:09 +01:00
docker-build feat: add docker-build reusable action 2026-03-04 12:05:31 +01:00
docker-build-nix fix: use registry.toph.so as default registry 2026-03-04 14:54:26 +01:00
push-nix-cache refactor: migrate from S3 to Attic binary cache 2026-02-27 21:19:09 +01:00
site-lib feat: add deploy-static-site action, site-lib, images; remove deploy-oci-site 2026-02-18 11:27:27 +01:00
.gitignore Initial commit: deploy-site action 2026-02-16 11:05:35 +01:00
README.md refactor: migrate from S3 to Attic binary cache 2026-02-27 21:19:09 +01:00

CI Actions

Reusable Forgejo/Gitea actions for toph's infrastructure.

Available Actions

Deploy a static site built with Nix flake to Attic binary cache and Nomad. Provides proper isolation and reproducibility.

Requirements:

  • Repository must have a flake.nix with a default package output
  • Runner label: nix (uses docker://registry.toph.so/nix-runner:latest)

Usage:

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: nix
    steps:
      - uses: actions/checkout@v4

      - uses: https://git.toph.so/toph/ci-actions/deploy-nix-site@main
        with:
          site-name: mysite
          traefik-rule: Host(`mysite.example.com`)
        env:
          NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}

Inputs:

  • site-name (required): Site identifier
  • traefik-rule (required): Traefik routing rule
  • flake-output (optional): Nix flake output, defaults to .#
  • cache-name (optional): Attic cache name, defaults to ci
  • s3-endpoint (optional): S3 endpoint (for deployment artifacts), defaults to https://s3.toph.so

Environment variables:

  • ATTIC_TOKEN: Attic CI token for pushing to cache (set via Forgejo secrets)
  • NOMAD_TOKEN: Nomad ACL token for the static-sites namespace

Notes:

  • Build artifacts are automatically pushed to the Attic binary cache at cache.toph.so/ci
  • Uses the ci cache by default (lower trust, shorter retention)
  • Pass cache-name: toph to use the main cache for trusted repos

Example flake.nix:

{
  description = "My static site";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      packages.${system}.default = pkgs.stdenv.mkDerivation {
        name = "my-site";
        src = ./.;

        buildInputs = [ pkgs.nodejs_20 ];

        buildPhase = ''
          npm ci
          npm run build
        '';

        installPhase = ''
          cp -r dist $out
        '';
      };

      devShells.${system}.default = pkgs.mkShell {
        packages = [ pkgs.nodejs_20 pkgs.vite ];
        shellHook = "echo 'Run: vite'";
      };
    };
}

deploy-site

Deploy a static site to production via S3 and Nomad (non-Nix).

Usage:

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4

      - name: Build site (if needed)
        run: npm run build

      - name: Deploy
        uses: https://git.toph.so/toph/ci-actions/deploy-site@main
        with:
          site-name: mysite
          traefik-rule: Host(`mysite.example.com`)
          source-dir: dist  # optional, defaults to current dir
        env:
          S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
          S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
          NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}

Inputs:

  • site-name (required): Site identifier used as Nomad service name
  • traefik-rule (required): Traefik routing rule (see examples below)
  • source-dir (optional): Directory with built files, defaults to .
  • s3-endpoint (optional): S3 endpoint, defaults to https://s3.toph.so

Environment variables:

  • S3_ACCESS_KEY: S3 access key (set via Forgejo secrets)
  • S3_SECRET_KEY: S3 secret key (set via Forgejo secrets)
  • NOMAD_TOKEN: Nomad ACL token for the static-sites namespace (set via Forgejo secrets, auto-synced by nomad-acl-forgejo-sync)

What it does:

  1. Packages the site directory as a tarball
  2. Uploads to S3 at s3://artifacts/<commit-sha>.tar.gz
  3. Sets public-read ACL on the artifact
  4. Dispatches Nomad job to deploy the site
  5. Site becomes available via the specified Traefik rule with Let's Encrypt SSL

Infrastructure handles:

  • Docker containers (static-web-server)
  • Resource limits (100 CPU, 64MB RAM)
  • Traefik routing & Let's Encrypt SSL
  • Automatic restarts

Traefik Rule Examples

Single domain:

traefik-rule: Host(`example.com`)

Multiple domains (with www):

traefik-rule: Host(`example.com`) || Host(`www.example.com`)

Subdomain:

traefik-rule: Host(`blog.example.com`)

toph.so domain:

traefik-rule: Host(`mysite.toph.so`)

Path-based routing:

traefik-rule: Host(`example.com`) && PathPrefix(`/docs`)

Setup

1. Deploy S3 service (SeaweedFS)

cd /srv/infra
nix run .#bosun -- run s3

2. Bootstrap Attic and create CI cache

Deploy Attic and run the bootstrap script:

cd /srv/infra
nix run .#bosun -- run attic
nix run .#attic-bootstrap

This creates:

  • toph cache: High-trust cache for manual/trusted builds (priority 40, 180d retention)
  • ci cache: Lower-trust cache for CI builds (priority 30, 90d retention)

3. Get the CI token

nomad var get nomad/jobs/attic/tokens | jq -r '.ci'

4. Add Forgejo secrets

Security Model:

  • The ATTIC_TOKEN must be explicitly provided as a Forgejo secret
  • This prevents untrusted repositories from pushing arbitrary binaries to your cache
  • Only repositories with the secret configured can push to the cache

Per-repository secrets (Settings → Secrets):

  • ATTIC_TOKEN: CI token from step 3 (for ci cache access)
  • NOMAD_TOKEN: Auto-synced by nomad-acl-forgejo-sync on alvin

Organization-wide secrets (for trusted internal repos): Set these at the organization level to avoid configuring each repo individually.

Cache selection:

  • Use cache-name: ci (default) for untrusted/external repos → 90-day retention
  • Use cache-name: toph for trusted repos → 180-day retention, requires root token

Examples

Simple HTML site

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4

      - uses: https://git.toph.so/toph/ci-actions/deploy-site@main
        with:
          site-name: mysite
          traefik-rule: Host(`mysite.toph.so`)
          source-dir: .  # HTML files in repo root
        env:
          S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
          S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
          NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}

Node.js/Vite site with custom domain

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install & build
        run: |
          npm ci
          npm run build

      - uses: https://git.toph.so/toph/ci-actions/deploy-site@main
        with:
          site-name: myapp
          traefik-rule: Host(`app.example.com`) || Host(`www.app.example.com`)
          source-dir: dist
        env:
          S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
          S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
          NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}

Hugo site

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true  # For Hugo themes

      - name: Setup Hugo
        run: |
          wget https://github.com/gohugoio/hugo/releases/download/v0.121.0/hugo_extended_0.121.0_linux-amd64.tar.gz
          tar xzf hugo_extended_0.121.0_linux-amd64.tar.gz
          sudo mv hugo /usr/local/bin/

      - name: Build
        run: hugo --minify

      - uses: https://git.toph.so/toph/ci-actions/deploy-site@main
        with:
          site-name: myblog
          traefik-rule: Host(`blog.example.com`)
          source-dir: public
        env:
          S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
          S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
          NOMAD_TOKEN: ${{ secrets.NOMAD_TOKEN }}

S3 Access

Troubleshooting

AWS CLI not found

The action automatically installs AWS CLI if not present on the runner.

Access denied during upload

Check that:

  1. S3_ACCESS_KEY and S3_SECRET_KEY are set in Forgejo secrets
  2. The credentials match those in the S3 Nomad job config
  3. S3 service is running: nomad job status s3

Site not accessible

Check:

  1. Nomad allocation is running: nomad job status static-site
  2. Traefik has picked up the service: check Traefik dashboard
  3. DNS resolves: dig <site-name>.toph.so
  4. Certificate is valid: curl -v https://<site-name>.toph.so

Development

To add a new action:

mkdir -p new-action
cd new-action
vim action.yaml

Then commit and push to main.

License

MIT