ci-actions/README.md
Christopher Mühl 19468d38d8
refactor: migrate from S3 to Attic binary cache
Replace low-level S3 operations with native Attic client for better
performance, simplicity, and proper Nix binary cache protocol support.

Changes:
- Replace 'nix copy' + S3 with 'attic push'
- Remove S3_ACCESS_KEY, S3_SECRET_KEY, NIX_SIGNING_KEY requirements
- Add ATTIC_TOKEN requirement (explicit per-repo security)
- Default to 'ci' cache instead of 'toph'
- Update Nomad fetch task to pull from Attic instead of S3
- Simplify push-nix-cache to single attic push command
- Update documentation with new security model

Security:
- ATTIC_TOKEN must be explicitly provided as Forgejo secret
- Prevents untrusted repos from pushing to cache
- Separate ci/toph caches for different trust levels

Benefits:
- Simpler: Single command instead of sign + copy + sync
- Faster: Native Attic protocol vs S3 object storage
- Safer: Explicit opt-in prevents unauthorized cache writes
- Standards-compliant: Proper Nix binary cache protocol

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 21:19:09 +01:00

360 lines
8.7 KiB
Markdown

# CI Actions
Reusable Forgejo/Gitea actions for toph's infrastructure.
## Available Actions
### `deploy-nix-site` (Recommended)
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:**
```yaml
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:**
```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:**
```yaml
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:**
```yaml
traefik-rule: Host(`example.com`)
```
**Multiple domains (with www):**
```yaml
traefik-rule: Host(`example.com`) || Host(`www.example.com`)
```
**Subdomain:**
```yaml
traefik-rule: Host(`blog.example.com`)
```
**toph.so domain:**
```yaml
traefik-rule: Host(`mysite.toph.so`)
```
**Path-based routing:**
```yaml
traefik-rule: Host(`example.com`) && PathPrefix(`/docs`)
```
## Setup
### 1. Deploy S3 service (SeaweedFS)
```bash
cd /srv/infra
nix run .#bosun -- run s3
```
### 2. Bootstrap Attic and create CI cache
Deploy Attic and run the bootstrap script:
```bash
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
```bash
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
```yaml
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
```yaml
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
```yaml
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
- **API endpoint**: https://s3.toph.so
- **Console**: https://s3-console.toph.so (restricted to Tailscale)
- **Credentials**: Stored in Nomad variables at `nomad/jobs/s3`
- **Backend**: SeaweedFS (S3-compatible)
## 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:
```bash
mkdir -p new-action
cd new-action
vim action.yaml
```
Then commit and push to `main`.
## License
MIT