ci: add gitea action to build admin app via dagger

This commit is contained in:
Seth Call 2026-03-09 16:42:42 -05:00
parent 983c690451
commit 966122fb18
8 changed files with 660 additions and 0 deletions

View File

@ -0,0 +1,28 @@
name: Build Admin
on:
push:
paths:
- 'admin/**'
- '.gitea/workflows/**'
jobs:
build:
# This label maps to the act-runner we deployed in K8s
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Dagger
run: |
curl -L https://dl.dagger.io/dagger/install.sh | sh
sudo mv bin/dagger /usr/local/bin/
- name: Login to Gitea Registry
# Gitea automatically injects GITEA_TOKEN into the runner for registry access
run: echo "${{ secrets.GITEA_TOKEN }}" | docker login git.staging.jamkazam.com -u ${{ gitea.actor }} --password-stdin
- name: Build and Publish with Dagger
working-directory: ./admin
run: |
dagger call build-local --source=. publish --address=git.staging.jamkazam.com/seth/jam-cloud-admin:latest

77
admin/ci/src/index.ts Normal file
View File

@ -0,0 +1,77 @@
import { dag, Secret, Directory, object, func, Platform, Container } from "@dagger.io/dagger"
@object()
export class Admin {
/**
* Fast Local Validation (ARM64 Native)
*/
@func()
async validate(source: Directory): Promise<Container> {
return await this.buildWithNix(source, "linux/arm64");
}
/**
* Fast Local Build (ARM64 Native)
*/
@func()
async buildLocal(source: Directory): Promise<Container> {
return await this.buildWithNix(source, "linux/arm64");
}
/**
* Multi-Arch Production Ship
*/
@func()
async ship(source: Directory, kubeconfig: Secret, registry: string): Promise<string> {
const version = this.generateCalVer();
const platforms: Platform[] = ["linux/amd64", "linux/arm64"];
const builds = platforms.map(async (p) => {
const img = await this.buildWithNix(source, p);
return img.publish(`${registry}/jamkazam-admin:${version}-${p.replace("/", "-")}`);
});
await Promise.all(builds);
// K8s Poke & Wait
await dag.container()
.from("bitnami/kubectl")
.withMountedSecret("/root/.kube/config", kubeconfig)
.withExec(["kubectl", "set", "image", "deployment/admin", `web=${registry}/jamkazam-admin:${version}-linux-amd64`])
.withExec(["kubectl", "rollout", "status", "deployment/admin", "--timeout=120s"])
.sync();
return `Shipped ${version}`;
}
private async buildWithNix(source: Directory, platform: Platform): Promise<Container> {
// 1. Start with a Nix-enabled container
let builder = dag.container({ platform })
.from("nixpkgs/nix:latest")
.withDirectory("/src", source)
.withWorkdir("/src")
// 2. Build the image tarball via Nix
// Add a dummy env var to bust cache if needed
.withEnvVariable("CACHE_BUST", new Date().getTime().toString())
.withExec(["nix", "--extra-experimental-features", "nix-command flakes", "build", ".#appImageTarball", "--out-link", "result-tarball"]);
try {
// Force sync to see build output in terminal
await builder.sync();
} catch (e) {
// If sync fails, try to get the last bit of stderr to explain why
const stderr = await builder.stderr();
throw new Error(`Nix build failed: ${e}\nStderr: ${stderr}`);
}
// 3. Extract the tarball file from the builder container
const tarball = builder.file("result-tarball");
// 4. Import the tarball as a new Dagger Container
return dag.container({ platform }).import_(tarball);
}
private generateCalVer(): string {
const d = new Date();
return `${d.getFullYear()}.${(d.getMonth()+1)}.${d.getDate()}.${d.getHours()}${d.getMinutes()}`;
}
}

134
admin/flake.nix Normal file
View File

@ -0,0 +1,134 @@
{
description = "SOTA Rails 8 Native Environment & OCI Image for JamKazam Admin - Cache Bust 2";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nix2container.url = "github:nlewo/nix2container";
};
outputs = { self, nixpkgs, nix2container }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in {
# Track 1: The Native Inner Loop (for 8GB RAM Macs)
devShells = forAllSystems (system: {
default = let pkgs = import nixpkgs { inherit system; }; in
pkgs.mkShell {
buildInputs = with pkgs; [
ruby_3_4
postgresql_16.lib
jemalloc
libyaml
vips
bun # for ci/watch.ts
git
];
shellHook = ''
export RUBY_YJIT_ENABLE=1
export RAILS_ENV=development
# macOS uses .dylib, Linux uses .so
if [ "$(uname)" = "Darwin" ]; then
export LD_PRELOAD="${pkgs.jemalloc}/lib/libjemalloc.dylib"
else
export LD_PRELOAD="${pkgs.jemalloc}/lib/libjemalloc.so"
fi
echo "🚀 SOTA Shell Ready (YJIT + jemalloc enabled)"
'';
};
});
# Track 2: The Production OCI Image
packages = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
n2c = nix2container.packages.${system};
# 1. Define the Ruby Environment with common native gems pre-installed
rubyEnv = pkgs.ruby_3_4.withPackages (ps: with ps; [
psych
pg
nokogiri
]);
# 2. Join dev and lib outputs for common dependencies
libyaml-joined = pkgs.symlinkJoin { name = "libyaml-joined"; paths = [ pkgs.libyaml pkgs.libyaml.dev ]; };
openssl-joined = pkgs.symlinkJoin { name = "openssl-joined"; paths = [ pkgs.openssl pkgs.openssl.dev ]; };
postgres-joined = pkgs.symlinkJoin { name = "postgres-joined"; paths = [ pkgs.postgresql_16 pkgs.postgresql_16.lib ]; };
# 3. Aggregate all dependencies into a single environment
allDeps = pkgs.buildEnv {
name = "jam-admin-env";
paths = with pkgs; [
rubyEnv
postgres-joined
libyaml-joined
openssl-joined
zlib.dev
zlib
libiconv
libxml2
libxml2.dev
libxslt
libxslt.dev
jemalloc
vips
bash
coreutils
git
gnumake
gcc
pkg-config
binutils
];
};
# 4. A startup script to ensure we are in the right place
start-jam-admin = pkgs.writeShellScriptBin "start-jam-admin" ''
export PATH=${allDeps}/bin:$PATH
export LD_LIBRARY_PATH=${allDeps}/lib:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=${allDeps}/lib/pkgconfig
cd /jam-cloud/admin
echo "🔧 Configuring bundler for native extensions..."
bundle config build.psych --with-yaml-dir=${libyaml-joined}
bundle config build.pg --with-pg-config=${postgres-joined}/bin/pg_config
echo "💎 Installing gems..."
bundle install
echo "🚀 Starting Rails..."
exec bundle exec rails server -b 0.0.0.0
'';
appImage = pkgs.dockerTools.buildLayeredImage {
name = "jamkazam-admin-v3";
tag = "local";
config = {
Cmd = [ "${start-jam-admin}/bin/start-jam-admin" ];
Env = [
"RUBY_YJIT_ENABLE=1"
"LD_PRELOAD=${pkgs.jemalloc}/lib/libjemalloc.so"
"RAILS_ENV=production"
"RAILS_SERVE_STATIC_FILES=true"
"PKG_CONFIG_PATH=${allDeps}/lib/pkgconfig"
];
ExposedPorts = { "3000/tcp" = {}; };
};
contents = [
allDeps
start-jam-admin
pkgs.libyaml.dev
pkgs.openssl.dev
pkgs.postgresql_16.lib
pkgs.zlib.dev
];
};
in {
inherit appImage;
appImageTarball = appImage;
}
);
};
}

73
admin/justfile Normal file
View File

@ -0,0 +1,73 @@
set shell := ["bash", "-c"]
NIX := "/nix/var/nix/profiles/default/bin/nix"
# Start local backing services (one-time, no boot persistence)
infra:
@echo "🐰 Starting RabbitMQ..."
brew services run rabbitmq || true
@echo "💾 Starting Redis..."
brew services run redis || true
@echo "✅ Local infra is running. Use 'brew services list' to verify."
# Setup environment
setup:
@echo "🔧 Installing SOTA stack dependencies..."
cd ci && ~/.bun/bin/bun install
@if [ ! -f dagger.json ]; then \
echo "Initializing Dagger module..."; \
dagger init --sdk=typescript --source=ci; \
fi
# Start coding natively (Track 1)
watch:
~/.bun/bin/bun run ci/watch.ts
# Enter the native Nix development environment
shell:
{{NIX}} develop
# Fast native validation in Dagger (Track 2)
validate:
dagger call --progress=plain validate --source=.
# Build local ARM64 image via Dagger and export to OrbStack
build:
@echo "🔨 Building image via Dagger..."
@rm -f ./admin.tar
dagger call --progress=plain build-local --source=. export --path=./admin.tar
@echo "🧹 Cleaning up old Docker artifacts to free space..."
docker system prune -f
@echo "📦 Loading image into Docker..."
#!/bin/bash
LOAD_OUTPUT=$(docker load < ./admin.tar)
echo "$LOAD_OUTPUT"
IMAGE_ID=$(echo "$LOAD_OUTPUT" | awk '/Loaded image ID: sha256:/ {print $4}' | cut -d: -f2)
if [ -z "$IMAGE_ID" ]; then
IMAGE_ID=$(echo "$LOAD_OUTPUT" | grep -oE '[0-9a-f]{12,}' | tail -n 1)
fi
echo "Tagging $IMAGE_ID as jamkazam-admin:local-v2..."
docker tag "$IMAGE_ID" jamkazam-admin:local-v2
rm ./admin.tar
echo "✅ Image loaded as 'jamkazam-admin:local-v2'"
# Run Rails server natively using the Nix shell (SOTA Inner Loop)
dev: infra
{{NIX}} develop --command bash -c "bundle install && bundle exec rails server"
# Run the local image in OrbStack, connected to host infra
run:
docker run -it --rm \
-p 3000:3000 \
-v {{invocation_directory()}}/..:/jam-cloud \
-w /jam-cloud/admin \
-e DATABASE_URL="postgres://postgres:postgres@host.orb.internal:5432/jam" \
-e REDIS_URL="redis://host.orb.internal:6379/1" \
-e RABBITMQ_URL="amqp://guest:guest@host.orb.internal:5672" \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
jamkazam-admin:local-v2
# The Big Red Button
ship:
@echo "🚀 Shipping to Production..."
dagger call ship --source=. --kubeconfig=file:$KUBECONFIG --registry="your-registry.io"

135
web/flake.nix Normal file
View File

@ -0,0 +1,135 @@
{
description = "SOTA Rails 8 Native Environment & OCI Image for JamKazam Web";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in {
# Track 1: The Native Inner Loop (for 8GB RAM Macs)
devShells = forAllSystems (system: {
default = let pkgs = import nixpkgs { inherit system; }; in
pkgs.mkShell {
buildInputs = with pkgs; [
ruby_3_4
postgresql_16.lib
jemalloc
libyaml
vips
bun
git
libxml2
libxslt
pkg-config
];
shellHook = ''
export RUBY_YJIT_ENABLE=1
export RAILS_ENV=development
# macOS uses .dylib, Linux uses .so
if [ "$(uname)" = "Darwin" ]; then
export LD_PRELOAD="${pkgs.jemalloc}/lib/libjemalloc.dylib"
else
export LD_PRELOAD="${pkgs.jemalloc}/lib/libjemalloc.so"
fi
echo "🚀 SOTA Web Shell Ready (YJIT + jemalloc enabled)"
'';
};
});
# Track 2: The Production OCI Image
packages = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
# 1. Define the Ruby Environment with common native gems pre-installed
rubyEnv = pkgs.ruby_3_4.withPackages (ps: with ps; [
psych
pg
nokogiri
]);
# 2. Join dev and lib outputs for common dependencies
libyaml-joined = pkgs.symlinkJoin { name = "libyaml-joined"; paths = [ pkgs.libyaml pkgs.libyaml.dev ]; };
openssl-joined = pkgs.symlinkJoin { name = "openssl-joined"; paths = [ pkgs.openssl pkgs.openssl.dev ]; };
postgres-joined = pkgs.symlinkJoin { name = "postgres-joined"; paths = [ pkgs.postgresql_16 pkgs.postgresql_16.lib ]; };
# 3. Aggregate all dependencies into a single environment
allDeps = pkgs.buildEnv {
name = "jam-web-env";
paths = with pkgs; [
rubyEnv
postgres-joined
libyaml-joined
openssl-joined
zlib.dev
zlib
libiconv
libxml2
libxml2.dev
libxslt
libxslt.dev
jemalloc
vips
bash
coreutils
git
gnumake
gcc
pkg-config
binutils
];
};
# 4. A startup script
start-jam-web = pkgs.writeShellScriptBin "start-jam-web" ''
export PATH=${allDeps}/bin:$PATH
export LD_LIBRARY_PATH=${allDeps}/lib:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=${allDeps}/lib/pkgconfig
cd /jam-cloud/web
echo "🔧 Configuring bundler for native extensions..."
bundle config build.psych --with-yaml-dir=${libyaml-joined}
bundle config build.pg --with-pg-config=${postgres-joined}/bin/pg_config
echo "💎 Installing gems..."
bundle install
echo "🚀 Starting Rails..."
exec bundle exec rails server -b 0.0.0.0
'';
appImage = pkgs.dockerTools.buildLayeredImage {
name = "jamkazam-web";
tag = "local";
config = {
Cmd = [ "${start-jam-web}/bin/start-jam-web" ];
Env = [
"RUBY_YJIT_ENABLE=1"
"LD_PRELOAD=${pkgs.jemalloc}/lib/libjemalloc.so"
"RAILS_ENV=production"
"RAILS_SERVE_STATIC_FILES=true"
"PKG_CONFIG_PATH=${allDeps}/lib/pkgconfig"
];
ExposedPorts = { "3000/tcp" = {}; };
};
contents = [
allDeps
start-jam-web
pkgs.libyaml.dev
pkgs.openssl.dev
pkgs.postgresql_16.lib
pkgs.zlib.dev
];
};
in {
inherit appImage;
appImageTarball = appImage;
}
);
};
}

52
web/justfile Normal file
View File

@ -0,0 +1,52 @@
set shell := ["bash", "-c"]
NIX := "/nix/var/nix/profiles/default/bin/nix"
# Start local backing services
infra:
@echo "🐰 Starting RabbitMQ..."
brew services run rabbitmq || true
@echo "💾 Starting Redis..."
brew services run redis || true
@echo "✅ Local infra is running."
# Setup environment
setup:
@echo "🔧 Installing dependencies..."
# bun install etc
# Start Rails server natively (Track 1)
dev: infra
{{NIX}} develop --command bash -c "bundle install && bundle exec rails server"
# Enter the native Nix development environment
shell:
{{NIX}} develop
# Build local ARM64 image
build:
@echo "🔨 Building image via Nix..."
{{NIX}} build .#appImageTarball --out-link result-tarball
@echo "📦 Loading image into Docker..."
#!/bin/bash
LOAD_OUTPUT=$(docker load < ./result-tarball)
echo "$LOAD_OUTPUT"
IMAGE_ID=$(echo "$LOAD_OUTPUT" | awk '/Loaded image ID: sha256:/ {print $4}' | cut -d: -f2)
if [ -z "$IMAGE_ID" ]; then
IMAGE_ID=$(echo "$LOAD_OUTPUT" | grep -oE '[0-9a-f]{12,}' | tail -n 1)
fi
echo "Tagging $IMAGE_ID as jamkazam-web:local..."
docker tag "$IMAGE_ID" jamkazam-web:local
echo "✅ Image loaded as 'jamkazam-web:local'"
# Run the local image
run:
docker run -it --rm \
-p 3001:3000 \
-v {{invocation_directory()}}/..:/jam-cloud \
-w /jam-cloud/web \
-e DATABASE_URL="postgres://postgres:postgres@host.orb.internal:5432/jam" \
-e REDIS_URL="redis://host.orb.internal:6379/1" \
-e RABBITMQ_URL="amqp://guest:guest@host.orb.internal:5672" \
-e RAILS_MASTER_KEY=$RAILS_MASTER_KEY \
jamkazam-web:local

126
websocket-gateway/flake.nix Normal file
View File

@ -0,0 +1,126 @@
{
description = "SOTA Rails 8 Native Environment & OCI Image for JamKazam WebSocket Gateway";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
in {
devShells = forAllSystems (system: {
default = let pkgs = import nixpkgs { inherit system; }; in
pkgs.mkShell {
buildInputs = with pkgs; [
ruby_3_4
postgresql_16.lib
jemalloc
libyaml
vips
bun
git
libxml2
libxslt
pkg-config
];
shellHook = ''
export RUBY_YJIT_ENABLE=1
export RAILS_ENV=development
if [ "$(uname)" = "Darwin" ]; then
export LD_PRELOAD="${pkgs.jemalloc}/lib/libjemalloc.dylib"
else
export LD_PRELOAD="${pkgs.jemalloc}/lib/libjemalloc.so"
fi
echo "🚀 SOTA WebSocket Shell Ready"
'';
};
});
packages = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
rubyEnv = pkgs.ruby_3_4.withPackages (ps: with ps; [
psych
pg
nokogiri
]);
libyaml-joined = pkgs.symlinkJoin { name = "libyaml-joined"; paths = [ pkgs.libyaml pkgs.libyaml.dev ]; };
openssl-joined = pkgs.symlinkJoin { name = "openssl-joined"; paths = [ pkgs.openssl pkgs.openssl.dev ]; };
postgres-joined = pkgs.symlinkJoin { name = "postgres-joined"; paths = [ pkgs.postgresql_16 pkgs.postgresql_16.lib ]; };
allDeps = pkgs.buildEnv {
name = "jam-ws-env";
paths = with pkgs; [
rubyEnv
postgres-joined
libyaml-joined
openssl-joined
zlib.dev
zlib
libiconv
libxml2
libxml2.dev
libxslt
libxslt.dev
jemalloc
vips
bash
coreutils
git
gnumake
gcc
pkg-config
];
};
start-jam-ws = pkgs.writeShellScriptBin "start-jam-ws" ''
export PATH=${allDeps}/bin:$PATH
export LD_LIBRARY_PATH=${allDeps}/lib:$LD_LIBRARY_PATH
export PKG_CONFIG_PATH=${allDeps}/lib/pkgconfig
cd /jam-cloud/websocket-gateway
echo "🔧 Configuring bundler..."
bundle config build.psych --with-yaml-dir=${libyaml-joined}
bundle config build.pg --with-pg-config=${postgres-joined}/bin/pg_config
echo "💎 Installing gems..."
bundle install
echo "🚀 Starting WebSocket Gateway..."
# TODO: Add actual start command if different from Rails
exec bundle exec ruby bin/server
'';
appImage = pkgs.dockerTools.buildLayeredImage {
name = "jamkazam-ws";
tag = "local";
config = {
Cmd = [ "${start-jam-ws}/bin/start-jam-ws" ];
Env = [
"RUBY_YJIT_ENABLE=1"
"LD_PRELOAD=${pkgs.jemalloc}/lib/libjemalloc.so"
"RAILS_ENV=production"
"PKG_CONFIG_PATH=${allDeps}/lib/pkgconfig"
];
};
contents = [
allDeps
start-jam-ws
pkgs.libyaml.dev
pkgs.openssl.dev
pkgs.postgresql_16.lib
pkgs.zlib.dev
];
};
in {
inherit appImage;
appImageTarball = appImage;
}
);
};
}

View File

@ -0,0 +1,35 @@
set shell := ["bash", "-c"]
NIX := "/nix/var/nix/profiles/default/bin/nix"
# Start local backing services
infra:
@echo "🐰 Starting RabbitMQ..."
brew services run rabbitmq || true
@echo "💾 Starting Redis..."
brew services run redis || true
@echo "✅ Local infra is running."
# Start server natively
dev: infra
{{NIX}} develop --command bash -c "bundle install && bundle exec ruby bin/server"
# Enter shell
shell:
{{NIX}} develop
# Build image
build:
{{NIX}} build .#appImageTarball --out-link result-tarball
docker load < ./result-tarball
docker tag jamkazam-ws:local jamkazam-ws:latest
# Run container
run:
docker run -it --rm
-v {{invocation_directory()}}/..:/jam-cloud
-w /jam-cloud/websocket-gateway
-e DATABASE_URL="postgres://postgres:postgres@host.orb.internal:5432/jam"
-e REDIS_URL="redis://host.orb.internal:6379/1"
-e RABBITMQ_URL="amqp://guest:guest@host.orb.internal:5672"
jamkazam-ws:latest