diff --git a/.forgejo/actions/build-push-image/action.yaml b/.forgejo/actions/build-push-image/action.yaml new file mode 100644 index 0000000..ac6f711 --- /dev/null +++ b/.forgejo/actions/build-push-image/action.yaml @@ -0,0 +1,54 @@ +name: 'Build and Push Image' +description: 'Build a container image with Docker and push to zot registry' + +# TODO: Investigate zot tag immutability to prevent overwriting released versions +# See: https://zotregistry.dev/v2.1.1/articles/immutable-tags/ + +inputs: + context: + description: 'Build context path' + required: true + dockerfile: + description: 'Dockerfile path (relative to context)' + required: false + default: 'Dockerfile' + image_name: + description: 'Image name (without registry, e.g. blumeops/devpi)' + required: true + version: + description: 'Version tag (e.g. v1.0.0)' + required: true + registry: + description: 'Registry URL' + required: false + default: 'registry.tail8d86e.ts.net' + +runs: + using: 'composite' + steps: + - name: Build image with Docker + shell: bash + run: | + echo "Building ${{ inputs.image_name }}:${{ inputs.version }}" + docker build \ + --tag ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }} \ + --tag ${{ inputs.registry }}/${{ inputs.image_name }}:${{ github.sha }} \ + --file ${{ inputs.context }}/${{ inputs.dockerfile }} \ + ${{ inputs.context }} + + - name: Push to registry + shell: bash + run: | + echo "Pushing ${{ inputs.image_name }}:${{ inputs.version }}" + docker push ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }} + docker push ${{ inputs.registry }}/${{ inputs.image_name }}:${{ github.sha }} + + - name: Summary + shell: bash + run: | + echo "Built and pushed:" + echo " ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }}" + echo " ${{ inputs.registry }}/${{ inputs.image_name }}:${{ github.sha }}" + echo "" + echo "Registry tags:" + curl -sf "https://${{ inputs.registry }}/v2/${{ inputs.image_name }}/tags/list" | jq -r '.tags[]' | sort -V | tail -10 || true diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml index 134992b..6aa3236 100644 --- a/.forgejo/workflows/test.yaml +++ b/.forgejo/workflows/test.yaml @@ -1,3 +1,4 @@ +# Workflow to verify CI environment and available tools name: Test CI on: @@ -16,22 +17,22 @@ jobs: - name: Verify tools run: | echo "=== Node.js ===" - node --version - npm --version + node --version || echo "Node.js not available" + npm --version || echo "npm not available" echo "" echo "=== Git ===" git --version echo "" echo "=== Build tools ===" - make --version | head -1 - gcc --version | head -1 + make --version 2>&1 | head -1 || echo "make not available" + gcc --version 2>&1 | head -1 || echo "gcc not available" echo "" - echo "=== Docker ===" - docker --version + echo "=== Container tools (Docker) ===" + docker --version || echo "Docker CLI not available" echo "" echo "=== Other tools ===" - curl --version | head -1 - jq --version + curl --version 2>&1 | head -1 || echo "curl not available" + jq --version || echo "jq not available" - name: Show repo info run: | diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..bea6b15 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,9 @@ +# .github directory + +This directory contains configuration for GitHub-ecosystem tooling only. + +**Workflows and actions belong in `.forgejo/`** - this repository uses Forgejo Actions, not GitHub Actions. + +## Contents + +- `actionlint.yaml` - Configuration for actionlint pre-commit hook (custom runner labels) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..12f3259 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,5 @@ +self-hosted-runner: + labels: + - docker-builder + - ubuntu-latest + - ubuntu-22.04 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 421de65..d673cc3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -86,4 +86,5 @@ repos: rev: v1.7.10 hooks: - id: actionlint-system + args: ['-config-file', '.github/actionlint.yaml'] files: ^\.forgejo/workflows/ diff --git a/CLAUDE.md b/CLAUDE.md index 82ed044..1399f9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,6 +134,13 @@ When migrating a service from indri to k8s, the Tailscale hostname must be freed Use `ssh indri 'tailscale serve status --json'` to check current serve entries (the non-JSON output may be empty even when entries exist). +## Container Image Releases + +```fish +mise run container-list # Show containers and recent tags +mise run container-release runner v1.0.0 # Tag and trigger build workflow +``` + ## Third-Party Projects When a task requires cloning or using a third-party git repository (e.g., for building from source), **ask the user to mirror it on forge first**, then clone from the mirror: diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index 6e962f1..b12e905 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -61,6 +61,23 @@ no_log: true tags: [forgejo] + # Forgejo runner token (for indri-based runner) + - name: Fetch forgejo runner token + ansible.builtin.command: + cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields runner_reg --reveal + delegate_to: localhost + register: _forgejo_runner_token + changed_when: false + no_log: true + check_mode: false + tags: [forgejo_runner] + + - name: Set forgejo runner token fact + ansible.builtin.set_fact: + forgejo_runner_token: "{{ _forgejo_runner_token.stdout }}" + no_log: true + tags: [forgejo_runner] + roles: - role: alloy tags: alloy @@ -82,3 +99,5 @@ tags: plex_metrics - role: tailscale_serve tags: tailscale-serve + - role: forgejo_runner + tags: forgejo_runner diff --git a/ansible/roles/forgejo_runner/defaults/main.yml b/ansible/roles/forgejo_runner/defaults/main.yml new file mode 100644 index 0000000..75cbd0c --- /dev/null +++ b/ansible/roles/forgejo_runner/defaults/main.yml @@ -0,0 +1,23 @@ +--- +# Forgejo Runner - host execution mode +# +# The runner daemon runs directly on indri and executes jobs on the host. +# This avoids container networking complexity since it can reach Forgejo +# at localhost:3001 directly. + +forgejo_runner_binary: /Users/erichblume/code/3rd/forgejo-runner/forgejo-runner +forgejo_runner_data_dir: /Users/erichblume/.forgejo-runner +forgejo_runner_config_dir: /Users/erichblume/.config/forgejo-runner +forgejo_runner_log_dir: /Users/erichblume/Library/Logs + +# Runner registration - use localhost since we're running on indri +forgejo_runner_instance_url: "http://localhost:3001" +forgejo_runner_name: "indri-host-runner" + +# Labels format for host execution: label:host +# Jobs run directly on the host, not in containers +forgejo_runner_labels: "ubuntu-latest:host,ubuntu-22.04:host" + +# Runner config +forgejo_runner_capacity: 2 +forgejo_runner_timeout: 3h diff --git a/ansible/roles/forgejo_runner/handlers/main.yml b/ansible/roles/forgejo_runner/handlers/main.yml new file mode 100644 index 0000000..8ace37c --- /dev/null +++ b/ansible/roles/forgejo_runner/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Restart forgejo-runner + listen: Restart forgejo-runner + ansible.builtin.shell: | + launchctl unload ~/Library/LaunchAgents/mcquack.forgejo-runner.plist 2>/dev/null || true + launchctl load ~/Library/LaunchAgents/mcquack.forgejo-runner.plist + changed_when: true diff --git a/ansible/roles/forgejo_runner/tasks/main.yml b/ansible/roles/forgejo_runner/tasks/main.yml new file mode 100644 index 0000000..0f28d88 --- /dev/null +++ b/ansible/roles/forgejo_runner/tasks/main.yml @@ -0,0 +1,57 @@ +--- +# Forgejo Runner - host execution mode +# +# The runner daemon runs directly on indri using a locally compiled binary. +# Jobs execute on the host, reaching Forgejo at localhost:3001. + +- name: Ensure forgejo-runner directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - "{{ forgejo_runner_data_dir }}" + - "{{ forgejo_runner_config_dir }}" + +- name: Deploy forgejo-runner config + ansible.builtin.template: + src: config.yaml.j2 + dest: "{{ forgejo_runner_config_dir }}/config.yaml" + mode: '0644' + notify: Restart forgejo-runner + +- name: Check if runner is registered + ansible.builtin.stat: + path: "{{ forgejo_runner_data_dir }}/.runner" + register: forgejo_runner_registered + +- name: Register runner with Forgejo + ansible.builtin.command: + cmd: > + {{ forgejo_runner_binary }} register + --instance "{{ forgejo_runner_instance_url }}" + --token "{{ forgejo_runner_token }}" + --name "{{ forgejo_runner_name }}" + --labels "{{ forgejo_runner_labels }}" + --no-interactive + chdir: "{{ forgejo_runner_data_dir }}" + when: not forgejo_runner_registered.stat.exists + changed_when: true + +- name: Deploy forgejo-runner launchd plist + ansible.builtin.template: + src: forgejo-runner.plist.j2 + dest: ~/Library/LaunchAgents/mcquack.forgejo-runner.plist + mode: '0644' + notify: Restart forgejo-runner + +- name: Check if forgejo-runner is loaded + ansible.builtin.command: launchctl list mcquack.forgejo-runner + register: forgejo_runner_launchctl_check + changed_when: false + failed_when: false + +- name: Load forgejo-runner if not loaded + ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.forgejo-runner.plist + when: forgejo_runner_launchctl_check.rc != 0 + changed_when: true diff --git a/ansible/roles/forgejo_runner/templates/config.yaml.j2 b/ansible/roles/forgejo_runner/templates/config.yaml.j2 new file mode 100644 index 0000000..07bdb8d --- /dev/null +++ b/ansible/roles/forgejo_runner/templates/config.yaml.j2 @@ -0,0 +1,13 @@ +# {{ ansible_managed }} +log: + level: info + +runner: + file: {{ forgejo_runner_data_dir }}/.runner + capacity: {{ forgejo_runner_capacity }} + timeout: {{ forgejo_runner_timeout }} + +# Even in host execution mode, some actions run in containers. +# Use host networking so containers can access localhost services. +container: + network: "host" diff --git a/ansible/roles/forgejo_runner/templates/forgejo-runner.plist.j2 b/ansible/roles/forgejo_runner/templates/forgejo-runner.plist.j2 new file mode 100644 index 0000000..e04fa0d --- /dev/null +++ b/ansible/roles/forgejo_runner/templates/forgejo-runner.plist.j2 @@ -0,0 +1,33 @@ + + + + + + Label + mcquack.forgejo-runner + ProgramArguments + + {{ forgejo_runner_binary }} + daemon + --config + {{ forgejo_runner_config_dir }}/config.yaml + + WorkingDirectory + {{ forgejo_runner_data_dir }} + EnvironmentVariables + + PATH + /Users/erichblume/.local/share/mise/shims:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + HOME + /Users/erichblume + + RunAtLoad + + KeepAlive + + StandardOutPath + {{ forgejo_runner_log_dir }}/mcquack.forgejo-runner.out.log + StandardErrorPath + {{ forgejo_runner_log_dir }}/mcquack.forgejo-runner.err.log + + diff --git a/argocd/apps/forgejo-runner.yaml b/argocd/apps/forgejo-runner.yaml deleted file mode 100644 index a584d33..0000000 --- a/argocd/apps/forgejo-runner.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Forgejo Actions Runner -# Runs in k8s, polls Forgejo for workflow jobs -# -# Before syncing, create the runner token secret: -# kubectl create namespace forgejo-runner -# op inject -i argocd/manifests/forgejo-runner/secret-token.yaml.tpl | kubectl apply -f - -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: forgejo-runner - namespace: argocd -spec: - project: default - source: - repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git - targetRevision: main - path: argocd/manifests/forgejo-runner - destination: - server: https://kubernetes.default.svc - namespace: forgejo-runner - syncPolicy: - syncOptions: - - CreateNamespace=true diff --git a/argocd/manifests/forgejo-runner/Dockerfile b/argocd/manifests/forgejo-runner/Dockerfile deleted file mode 100644 index e511440..0000000 --- a/argocd/manifests/forgejo-runner/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM code.forgejo.org/forgejo/runner:3.5.1 - -# Switch to root to install packages -USER root - -# The base image is Alpine Linux -# Install tools needed for GitHub Actions and builds -RUN apk add --no-cache \ - # Required for actions/checkout and other Node-based actions - nodejs \ - npm \ - # Build essentials - git \ - curl \ - wget \ - jq \ - make \ - gcc \ - g++ \ - musl-dev \ - # For container builds - ca-certificates \ - docker-cli - -# Verify tools are available -RUN node --version && npm --version && docker --version - -# Switch back to non-root user -USER 1000 diff --git a/argocd/manifests/forgejo-runner/configmap.yaml b/argocd/manifests/forgejo-runner/configmap.yaml deleted file mode 100644 index 584efe0..0000000 --- a/argocd/manifests/forgejo-runner/configmap.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: forgejo-runner-config - namespace: forgejo-runner -data: - config.yaml: | - log: - level: info - runner: - file: /data/.runner - capacity: 1 - timeout: 3h diff --git a/argocd/manifests/forgejo-runner/deployment.yaml b/argocd/manifests/forgejo-runner/deployment.yaml deleted file mode 100644 index 0848e4a..0000000 --- a/argocd/manifests/forgejo-runner/deployment.yaml +++ /dev/null @@ -1,63 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: forgejo-runner - namespace: forgejo-runner -spec: - replicas: 1 - selector: - matchLabels: - app: forgejo-runner - template: - metadata: - labels: - app: forgejo-runner - spec: - serviceAccountName: forgejo-runner - containers: - - name: runner - image: registry.tail8d86e.ts.net/blumeops/forgejo-runner:latest - env: - # Use internal k8s service via Tailscale operator egress - - name: FORGEJO_INSTANCE_URL - value: "http://forge.tailscale.svc.cluster.local:3001" - - name: RUNNER_NAME - value: "k8s-runner-1" - - name: RUNNER_TOKEN - valueFrom: - secretKeyRef: - name: forgejo-runner-token - key: token - command: - - /bin/sh - - -c - - | - # Register runner if not already registered - if [ ! -f /data/.runner ]; then - forgejo-runner register \ - --instance "$FORGEJO_INSTANCE_URL" \ - --token "$RUNNER_TOKEN" \ - --name "$RUNNER_NAME" \ - --labels "ubuntu-latest:host,ubuntu-22.04:host" \ - --no-interactive - fi - # Start the runner daemon with config - forgejo-runner daemon --config /config/config.yaml - volumeMounts: - - name: runner-data - mountPath: /data - - name: runner-config - mountPath: /config - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "1Gi" - cpu: "1000m" - volumes: - - name: runner-data - emptyDir: {} - - name: runner-config - configMap: - name: forgejo-runner-config diff --git a/argocd/manifests/forgejo-runner/kustomization.yaml b/argocd/manifests/forgejo-runner/kustomization.yaml deleted file mode 100644 index 332c49c..0000000 --- a/argocd/manifests/forgejo-runner/kustomization.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: forgejo-runner -resources: - - namespace.yaml - - serviceaccount.yaml - - configmap.yaml - - deployment.yaml diff --git a/argocd/manifests/forgejo-runner/namespace.yaml b/argocd/manifests/forgejo-runner/namespace.yaml deleted file mode 100644 index 19441b1..0000000 --- a/argocd/manifests/forgejo-runner/namespace.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: forgejo-runner diff --git a/argocd/manifests/forgejo-runner/secret-token.yaml.tpl b/argocd/manifests/forgejo-runner/secret-token.yaml.tpl deleted file mode 100644 index 427d8df..0000000 --- a/argocd/manifests/forgejo-runner/secret-token.yaml.tpl +++ /dev/null @@ -1,10 +0,0 @@ -# Template for op inject -# Usage: op inject -i secret-token.yaml.tpl | kubectl apply -f - -apiVersion: v1 -kind: Secret -metadata: - name: forgejo-runner-token - namespace: forgejo-runner -type: Opaque -stringData: - token: "op://blumeops/w3663ffnvkewbftncqxtcpeavy/runner_reg" diff --git a/argocd/manifests/forgejo-runner/serviceaccount.yaml b/argocd/manifests/forgejo-runner/serviceaccount.yaml deleted file mode 100644 index ef8cb25..0000000 --- a/argocd/manifests/forgejo-runner/serviceaccount.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: forgejo-runner - namespace: forgejo-runner diff --git a/mise-tasks/container-list b/mise-tasks/container-list new file mode 100755 index 0000000..21a2ad9 --- /dev/null +++ b/mise-tasks/container-list @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +#MISE description="List available containers and their recent tags" + +set -euo pipefail + +REGISTRY="registry.tail8d86e.ts.net" +WORKFLOW_DIR=".forgejo/workflows" + +echo "Container Images" +echo "================" +echo "" + +# Find all build-*.yaml workflows +for workflow in "$WORKFLOW_DIR"/build-*.yaml; do + [[ -f "$workflow" ]] || continue + + # Extract container name from filename: build-runner.yaml -> runner + filename=$(basename "$workflow") + container="${filename#build-}" + container="${container%.yaml}" + + # Skip if not a container build workflow (check for image_name) + if ! grep -q "image_name:" "$workflow" 2>/dev/null; then + continue + fi + + # Extract image name from workflow + image=$(grep -E "^\s+image_name:" "$workflow" | head -1 | awk '{print $2}') + + echo "📦 $container" + echo " Image: $REGISTRY/$image" + echo " Workflow: $workflow" + + # Query zot for recent tags + tags=$(curl -sf "https://$REGISTRY/v2/$image/tags/list" 2>/dev/null | jq -r '.tags // [] | .[]' | grep -E '^v[0-9]' | sort -V | tail -4 || true) + + if [[ -n "$tags" ]]; then + echo " Recent tags:" + echo "$tags" | while read -r tag; do + echo " - $tag" + done + else + echo " Recent tags: (none)" + fi + echo "" +done + +echo "---" +echo "To release a new version:" +echo " mise run container-release " +echo "" +echo "Example:" +echo " mise run container-release runner v1.0.0" diff --git a/mise-tasks/container-release b/mise-tasks/container-release new file mode 100755 index 0000000..9e8802b --- /dev/null +++ b/mise-tasks/container-release @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +#MISE description="Release a container image by creating a git tag" + +set -euo pipefail + +CONTAINER="${1:-}" +VERSION="${2:-}" + +if [[ -z "$CONTAINER" || -z "$VERSION" ]]; then + echo "Usage: mise run container-release " + echo "" + echo "Run 'mise run container-list' to see available containers and recent tags." + exit 1 +fi + +# Validate version format +if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format vX.Y.Z (e.g. v1.0.0)" + exit 1 +fi + +TAG="${CONTAINER}-${VERSION}" + +echo "Creating release tag: $TAG" +echo "" + +# Check if tag already exists +if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Error: Tag '$TAG' already exists" + echo "Existing tags for $CONTAINER:" + git tag -l "${CONTAINER}-v*" | sort -V | tail -5 + exit 1 +fi + +# Find the workflow file to determine image name +WORKFLOW_FILE=".forgejo/workflows/build-${CONTAINER}.yaml" +if [[ ! -f "$WORKFLOW_FILE" ]]; then + echo "Error: No workflow found for container '$CONTAINER'" + echo "" + echo "Run 'mise run container-list' to see available containers." + exit 1 +fi + +# Extract image name from workflow +IMAGE=$(grep -E "^\s+image_name:" "$WORKFLOW_FILE" | head -1 | awk '{print $2}') +if [[ -z "$IMAGE" ]]; then + echo "Error: Could not determine image name from $WORKFLOW_FILE" + exit 1 +fi + +echo "Container: $CONTAINER" +echo "Workflow: $WORKFLOW_FILE" +echo "Image: registry.tail8d86e.ts.net/$IMAGE:$VERSION" +echo "" + +# Confirm +read -p "Create tag and push? [y/N] " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 +fi + +# Create and push tag +git tag "$TAG" +git push origin "$TAG" + +echo "" +echo "✅ Tag '$TAG' created and pushed" +echo "" +echo "The workflow will now build and push:" +echo " registry.tail8d86e.ts.net/$IMAGE:$VERSION" +echo "" +echo "Monitor the build at:" +echo " https://forge.tail8d86e.ts.net/eblume/blumeops/actions" diff --git a/pulumi/policy.hujson b/pulumi/policy.hujson index 7f18820..037f085 100644 --- a/pulumi/policy.hujson +++ b/pulumi/policy.hujson @@ -74,6 +74,7 @@ "dst": ["tag:homelab"], "ip": ["tcp:3001", "tcp:2200"], }, + // Homelab can reach k8s services: PostgreSQL, CNPG metrics, Prometheus/Loki { "src": ["tag:homelab"],