No description
  • Dockerfile 72.6%
  • Just 21.9%
  • Shell 5.5%
Find a file
Charles Jacquin d7bbcd67da
All checks were successful
release / build-agent (push) Successful in 20s
release / build-ci-base-infra (push) Successful in 34s
qa / qa (push) Successful in 49s
release / build-bun (push) Successful in 1m19s
release / build-ci-base (push) Successful in 2m37s
release / build-android-ci (push) Successful in 2m24s
fix(android-ci): JDK 21 for trixie + correct cargo-ndk version check
Debian trixie has no openjdk-17 (ships JDK 21); use openjdk-21-jdk-headless,
which also matches the geneathing Gradle daemon pin. Verify cargo-ndk via
`cargo ndk --version` (the bare binary rejects --version). Image builds clean
locally and the toolchain (JDK 21, rust android targets, cargo-ndk 3.5.4,
NDK 27, SDK 36, just, jq) is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:44:34 +02:00
.forgejo/workflows feat(android-ci): add Android build image for Rust-core apps 2026-06-15 14:39:04 +02:00
biome feat: initial scaffold (v0.1.0) 2026-05-03 13:00:48 +02:00
docker fix(android-ci): JDK 21 for trixie + correct cargo-ndk version check 2026-06-15 14:44:34 +02:00
husky feat: initial scaffold (v0.1.0) 2026-05-03 13:00:48 +02:00
just docs(just): add override examples to rust.just CI contract recipes 2026-06-12 22:30:02 +00:00
pi-skills feat(agent): reusable pi-agent workflow + agent image (#2) 2026-06-05 11:28:09 +00:00
tsconfig feat: initial scaffold (v0.1.0) 2026-05-03 13:00:48 +02:00
.gitignore feat: initial scaffold (v0.1.0) 2026-05-03 13:00:48 +02:00
README.md feat(android-ci): add Android build image for Rust-core apps 2026-06-15 14:39:04 +02:00

forge-base

Reusable Docker base images, callable Forgejo workflows, justfile recipes, and tooling configs shared across personal projects on forge.jacquin.app.

Solo-friendly — every consumer (human or agent) inherits the same baseline without copy-paste drift.

Repo must stay public — Forgejo cross-repo uses: requires the callee to be publicly readable (no token-based cross-repo auth as of Forgejo 15.0).

What's in v0.4.1

Path Purpose
docker/bun.Dockerfile Bun + git + just + ca-certs base. Multi-arch.
docker/ci-base.Dockerfile Rust + just + tombi + rumdl. Multi-arch. General-purpose CI image.
docker/ci-base-infra.Dockerfile Terraform + Ansible + just + gitleaks. amd64-only. For IaC repos.
docker/agent.Dockerfile CI coding-agent runtime — Pi + umans provider + forge CLI. amd64-only.
docker/android-ci.Dockerfile Rust + cargo-ndk + JDK 17 + Android SDK/NDK + just. amd64-only. For Android apps with a Rust core.
.forgejo/workflows/qa-bun.yml Reusable workflow — Bun project QA pipeline (typecheck/lint/fmt-check/test).
.forgejo/workflows/qa-generic.yml Reusable workflow — Language-agnostic QA pipeline (fmt-check/lint/test).
.forgejo/workflows/build-bun-tarball.yml Reusable workflow — Bun release artifact build + package.
.forgejo/workflows/release-generic.yml Reusable workflow — Dual-arch build + package + Forgejo release.
.forgejo/workflows/pi-agent.yml Reusable workflow — AI coding bot. Label an issue → Pi implements + opens PR → reviewer pass.
.forgejo/workflows/deploy.yml Reusable workflow — Trigger proxmox-iac deployment.
.forgejo/workflows/destroy.yml Reusable workflow — Trigger proxmox-iac teardown.
pi-skills/ Generic Pi skills (forgejo-cli, forgejo-branch-checkout) baked into the agent image.
just/{shared,bun,rust}.just Reusable just recipes. Inherited via import — see below.
husky/pre-push Full QA gate (just qa) — blocks git push on test/typecheck failure.
tsconfig/base.json Strict TypeScript baseline.
biome/base.json Biome lint/format defaults.
.forgejo/workflows/release.yml This repo's self-release pipeline — builds + publishes all Docker images on tag v*.
.forgejo/workflows/qa.yml This repo's self-CI — validates justfile + Dockerfile + workflow YAML.

Versioning

Tag every meaningful change. Downstream projects pin forge-base@vX.Y.Z. SemVer applies — bump major when a recipe contract or workflow input changes.

Justfile inheritance

Forge-base provides shared justfile recipes that projects import instead of copy-pasting. This eliminates drift — change qa, lint, fmt-check etc. in one place and every consumer gets the fix.

How it works

Every CI image (ci-base, ci-base-infra, bun) bakes the just recipes at /opt/forge-base/just/. The reusable workflows copy them into .forge-base/just/ before calling just. For local dev, use git subtree.

In both cases the project justfile imports from the same relative path:

import '.forge-base/just/shared.just'    # provides ci-package, _default
import '.forge-base/just/rust.just'       # provides qa, fmt-check, lint, test, ci-setup, ci-build

Recipes defined after the import override the imported ones — just's standard rule. This means a project can keep its import lines and only define what differs.

Before vs after

Before (copy-paste, ~140 lines per project):

# Every Rust repo had its own copy of these identical recipes:
qa:
    @echo "=== Formatting ==="
    @just fmt
    @just lint
    @just test
    @echo "All quality checks passed!"

fmt-check:
    cargo fmt --all -- --check

clippy:
    cargo clippy -- -D warnings

lint: clippy lint-toml lint-md
lint-toml:
    tombi check .
lint-md:
    rumdl check .

test:
    cargo test

ci-setup:
    cargo fetch

ci-build:
    cargo build --release

# ... plus 15 project-specific recipes

After (inheritance, ~60 lines):

import '.forge-base/just/shared.just'
import '.forge-base/just/rust.just'

# Project-specific recipes only
run *ARGS:
    cargo run -- {{ARGS}}

build:
    cargo build

install:
    cargo build --release
    cp target/release/myapp ~/.local/bin/

# Overrides (defined after import → win over the base)
ci-setup:
    apt-get update && apt-get install -y musl-tools
    rustup target add x86_64-unknown-linux-musl

ci-build:
    cargo build --release --target x86_64-unknown-linux-musl

What each file provides

File Recipes
shared.just _default, ci-package
rust.just qa, fmt, fmt-check, clippy, lint, lint-toml, lint-md, test, ci-setup, ci-build
bun.just qa, dev, start, typecheck, lint, lint-fix, fmt, fmt-check, test, ci-setup, ci-build

Local dev — git subtree

git subtree add --prefix=.forge-base \
  https://forge.jacquin.app/charles/forge-base.git v0.4.1 --squash

Bump version: git subtree pull --prefix=.forge-base ... v0.4.1 --squash.

CI — automatic

The reusable workflows handle this. They copy /opt/forge-base/just/* into .forge-base/just/ before calling just. No project configuration needed.

Consumption

CI — reusable workflow refs (Forgejo 15+)

Note: cache-key is computed inside the reusable workflow. Do NOT pass ${{ hashFiles(...) }} via with: — Forgejo's workflow_call does not support hashFiles() in input values.

Bun project — QA

name: qa
on:
  push:
  pull_request:
    branches: [main]
jobs:
  qa:
    uses: charles/forge-base/.forgejo/workflows/qa-bun.yml@v0.4.3
    with:
      image_tag: v0.4.3

Bun project — Release

name: release
on:
  push:
    tags: [v*]
jobs:
  build-x86_64:
    uses: charles/forge-base/.forgejo/workflows/build-bun-tarball.yml@v0.4.3
    with:
      arch: x86_64
      image_tag: v0.4.3
      artifact_name: dist-x86_64
  build-aarch64:
    uses: charles/forge-base/.forgejo/workflows/build-bun-tarball.yml@v0.4.3
    with:
      arch: aarch64
      image_tag: v0.4.3
      artifact_name: dist-aarch64

Rust / Generic project — QA

name: qa
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  qa:
    uses: charles/forge-base/.forgejo/workflows/qa-generic.yml@v0.4.3
    with:
      container: forge.jacquin.app/charles/forge-base/ci-base:v0.4.1
      cache-path: |
        ~/.cargo/registry
        ~/.cargo/git
        target

Rust / Generic project — Release

name: release
on:
  push:
    tags: [v*]
jobs:
  release:
    uses: charles/forge-base/.forgejo/workflows/release-generic.yml@v0.4.3
    with:
      container: forge.jacquin.app/charles/forge-base/ci-base:v0.4.1
      cache-path: |
        ~/.cargo/registry
        ~/.cargo/git
        target

IaC project — QA + Deploy

# qa.yml
name: qa
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  qa:
    uses: charles/forge-base/.forgejo/workflows/qa-generic.yml@v0.4.3
    with:
      container: forge.jacquin.app/charles/forge-base/ci-base-infra:v0.4.1

# deploy.yml
name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    uses: charles/forge-base/.forgejo/workflows/deploy.yml@v0.4.3
    with:
      iac_dir: iac/prod
      service_vars: '{"myapp_ct_id": "246", "myapp_ct_ip": "192.168.1.246/24"}'
    secrets:
      FORGEJO_TOKEN: ${{ secrets.FORGE_TOKEN }}

CI — AI coding bot (pi-agent)

Label an issue and a Pi agent implements it, opens a PR, then reviews its own diff. Add .forgejo/workflows/pi-agent.yml to the consumer repo:

name: pi-agent
on:
  issues:
    types: [labeled]
concurrency:
  group: pi-agent-issue-${{ github.event.issue.number }}
  cancel-in-progress: true
jobs:
  agent:
    if: ${{ github.event.label.name == 'pi-agent' }}
    uses: charles/forge-base/.forgejo/workflows/pi-agent.yml@v0.4.3
    with:
      image_tag: v0.4.3
      bot_name: my-bot
      bot_email: my-bot@example.com

Then add a .pi-agent/ directory to the repo:

.pi-agent/
├── settings.json     # Pi config (provider/model/compaction)
├── ci-coder.md       # coder system prompt — stack conventions + git workflow
├── ci-reviewer.md    # reviewer system prompt
└── skills/           # PROJECT-specific Pi skills (auto-discovered, optional)

Generic skills (forgejo-cli, forgejo-branch-checkout) are baked into the agent image — do not copy them into .pi-agent/skills/. Inputs config_dir (default .pi-agent), label (default pi-agent) and model (default umans-coder) are overridable.

Docker — remote ref

# Bun projects
FROM forge.jacquin.app/charles/forge-base/bun:v0.4.1

Pinning

@vX.Y.Z (tag) or @<sha> both work. Tags are mutable so SHA-pinning is safer for prod-critical pipelines; tags are fine for everything else and make Renovate-style auto-bumps trivial.

Justfile + tsconfig + biome — git subtree

git subtree add --prefix=.forge-base \
  https://forge.jacquin.app/charles/forge-base.git v0.4.1 --squash

Then in justfile:

import '.forge-base/just/shared.just'
import '.forge-base/just/bun.just'    # or rust.just

In tsconfig.json:

{ "extends": "./.forge-base/tsconfig/base.json" }

In biome.json:

{ "extends": ["./.forge-base/biome/base.json"] }

In .husky/pre-push:

#!/usr/bin/env sh
exec sh .forge-base/husky/pre-push "$@"

Bump version: git subtree pull --prefix=.forge-base ... v0.4.1 --squash.

Justfile contract for downstream projects

Reusable workflows assume the caller's justfile exposes:

Recipe Used by Notes
ci-setup qa-bun, qa-generic, build-bun-tarball, release-generic Install workspace deps (e.g. bun install, cargo fetch).
typecheck qa-bun bun x turbo run typecheck (Bun only).
fmt-check qa-bun, qa-generic Check formatting without modifying files.
lint qa-bun, qa-generic Run all linters.
test qa-bun, qa-generic Run test suite.
ci-build build-bun-tarball, release-generic Build release artifacts.
ci-package build-bun-tarball, release-generic Pack into dist/${PROJECT_NAME}-${VERSION}-${ARCH}-linux.tar.gz. Reads ARCH, VERSION, PROJECT_NAME env vars.
agent-setup pi-agent Optional. Install the project's language toolchain inside the agent run. Skipped if undefined.

just/bun.just and just/rust.just define these recipes. Import them and override only what differs for your project.

Docker images

Image Base Platforms Key tools
forge-base/bun debian:bookworm-slim amd64, arm64 Bun, just
forge-base/ci-base debian:trixie-slim amd64, arm64 Rust (stable + clippy + rustfmt), just, tombi, rumdl
forge-base/ci-base-infra alpine:3.21 amd64 Terraform, Ansible, just, gitleaks, yamlfmt, shellcheck, rumdl
forge-base/agent node:22-alpine amd64 Pi agent, forge CLI, just
forge-base/android-ci debian:trixie-slim amd64 Rust (+ android targets), cargo-ndk, JDK 17, Android SDK 36 + NDK, just, jq

Repo conventions

Per global Forgejo Actions naming:

  • Workflow names: qa (push + PR), release (tag v*)
  • Job IDs: qa; for releases build-x86_64, build-aarch64, publish
  • Architecture naming: x86_64 / aarch64 (Rust target-triple convention)

Container registry secret

The release workflow pushes images to Forgejo's container registry. Add a repo secret PACKAGE_TOKEN containing a Forgejo PAT with write:package scope. The workflow logs in as the repo owner.