Jobs now target the static-sites namespace (required by the CI ACL policy) and docs include NOMAD_TOKEN in all workflow examples. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
377 lines
9 KiB
Markdown
377 lines
9 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 S3 and Nomad. Provides proper isolation and reproducibility.
|
|
|
|
**Requirements:**
|
|
- Repository must have a `flake.nix` with a default package output
|
|
- Runner label: `nix` (uses `docker://nixos/nix: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:
|
|
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
|
|
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
|
|
NIX_SIGNING_KEY: ${{ secrets.NIX_SIGNING_KEY }}
|
|
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 `.#`
|
|
- `s3-endpoint` (optional): S3 endpoint, defaults to `https://s3.toph.so`
|
|
|
|
**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. Generate binary cache signing keys
|
|
|
|
```bash
|
|
nix-store --generate-binary-cache-key cache.toph.so cache-priv-key.pem cache-pub-key.pem
|
|
|
|
# Public key (add to nix.conf trusted-public-keys on hosts that will fetch):
|
|
cat cache-pub-key.pem
|
|
# Example: cache.toph.so:9zFo64TPnxaQeyFM6NS9ou2Fd8OQv4Ia+MuLMjLBYjY=
|
|
|
|
# Private key (store in Forgejo secrets):
|
|
cat cache-priv-key.pem
|
|
# Keep this secret!
|
|
```
|
|
|
|
### 3. Create S3 buckets
|
|
|
|
```bash
|
|
# Configure AWS CLI
|
|
export AWS_ACCESS_KEY_ID=<your-access-key>
|
|
export AWS_SECRET_ACCESS_KEY=<your-secret-key>
|
|
export AWS_ENDPOINT_URL=https://s3.toph.so
|
|
export AWS_EC2_METADATA_DISABLED=true
|
|
|
|
# Create binary cache bucket
|
|
aws s3 mb s3://nix-cache
|
|
|
|
# (Optional) Create artifacts bucket for non-Nix deployments
|
|
aws s3 mb s3://artifacts
|
|
aws s3api put-bucket-acl --bucket artifacts --acl public-read
|
|
```
|
|
|
|
### 4. Configure alvin to trust the binary cache
|
|
|
|
Add to `/srv/infra/hosts/alvin/default.nix`:
|
|
|
|
```nix
|
|
nix.settings = {
|
|
substituters = [
|
|
"https://cache.nixos.org"
|
|
"s3://nix-cache?endpoint=s3.toph.so&scheme=https"
|
|
];
|
|
trusted-public-keys = [
|
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
|
"cache.toph.so:9zFo64TPnxaQeyFM6NS9ou2Fd8OQv4Ia+MuLMjLBYjY=" # Your public key
|
|
];
|
|
};
|
|
```
|
|
|
|
Then deploy: `sudo nixos-rebuild switch --flake .#alvin`
|
|
|
|
### 5. Add Forgejo secrets
|
|
|
|
In your repository settings (or organization settings for global secrets):
|
|
- `S3_ACCESS_KEY`: S3 access key
|
|
- `S3_SECRET_KEY`: S3 secret key
|
|
- `NIX_SIGNING_KEY`: Contents of `cache-priv-key.pem`
|
|
- `NOMAD_TOKEN`: Auto-synced by `nomad-acl-forgejo-sync` on alvin (or set manually from `cat /var/lib/nomad-acl/ci.token`)
|
|
|
|
### 6. Configure SSH access from runner to alvin
|
|
|
|
The runner needs to pull store paths to alvin's `/nix/store`. Add the runner's SSH key to alvin or use an agent socket mount.
|
|
|
|
## 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
|