diff --git a/.forgejo/workflows/test.yaml b/.forgejo/workflows/test.yaml new file mode 100644 index 0000000..6f9faf1 --- /dev/null +++ b/.forgejo/workflows/test.yaml @@ -0,0 +1,29 @@ +name: Test CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout (git clone) + run: | + # For PRs use head_ref (branch name), for pushes use ref_name + BRANCH="${{ gitea.head_ref || gitea.ref_name }}" + git clone --depth 1 --branch "$BRANCH" \ + "${{ gitea.server_url }}/${{ gitea.repository }}.git" . + env: + GIT_SSL_NO_VERIFY: "true" + + - name: Hello World + run: | + echo "Hello from Forgejo Actions!" + echo "Runner: $(hostname)" + echo "Repository: ${{ gitea.repository }}" + echo "Event: ${{ gitea.event_name }}" + echo "Ref: ${{ gitea.ref }}" + ls -la diff --git a/CLAUDE.md b/CLAUDE.md index 67687db..82ed044 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,6 +102,11 @@ kubectl --context=minikube-indri logs -n # View logs Note: The user has fish abbreviations `ki` for `kubectl --context=minikube-indri` and `k9i` for `k9s --context=minikube-indri`, but these only work in interactive shells. +**ArgoCD login (when token expires):** +```fish +argocd login argocd.tail8d86e.ts.net --username admin --password "$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get srogeebssulhtb6tnqd7ls6qey --fields password --reveal)" +``` + ### Indri Services (via Ansible) Some services remain on indri outside of Kubernetes: diff --git a/README.md b/README.md index 7da378f..61ea208 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ Hooks include: - **TOML**: taplo - **JSON**: prettier +## CI/CD + +This repo uses [Forgejo Actions](https://forgejo.org/docs/latest/user/actions/) for CI/CD. Workflows live in `.forgejo/workflows/` (not `.github/workflows/`). The runner executes jobs in host mode within the Kubernetes cluster. + ## Documentation Detailed documentation lives in my personal zettelkasten, which is not included in this repository. You can view the docs with: diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index c3d5112..6e962f1 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -22,6 +22,45 @@ no_log: true tags: [borgmatic] + # Forgejo secrets + - name: Fetch forgejo LFS JWT secret + ansible.builtin.command: + cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields lfs-jwt-secret --reveal + delegate_to: localhost + register: _forgejo_lfs_jwt + changed_when: false + no_log: true + check_mode: false + tags: [forgejo] + + - name: Fetch forgejo internal token + ansible.builtin.command: + cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields internal-token --reveal + delegate_to: localhost + register: _forgejo_internal_token + changed_when: false + no_log: true + check_mode: false + tags: [forgejo] + + - name: Fetch forgejo OAuth2 JWT secret + ansible.builtin.command: + cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w3663ffnvkewbftncqxtcpeavy --fields oauth2-jwt-secret --reveal + delegate_to: localhost + register: _forgejo_oauth2_jwt + changed_when: false + no_log: true + check_mode: false + tags: [forgejo] + + - name: Set forgejo secrets facts + ansible.builtin.set_fact: + forgejo_lfs_jwt_secret: "{{ _forgejo_lfs_jwt.stdout }}" + forgejo_internal_token: "{{ _forgejo_internal_token.stdout }}" + forgejo_oauth2_jwt_secret: "{{ _forgejo_oauth2_jwt.stdout }}" + no_log: true + tags: [forgejo] + roles: - role: alloy tags: alloy diff --git a/ansible/roles/forgejo/defaults/main.yml b/ansible/roles/forgejo/defaults/main.yml new file mode 100644 index 0000000..23396e8 --- /dev/null +++ b/ansible/roles/forgejo/defaults/main.yml @@ -0,0 +1,51 @@ +--- +# Forgejo configuration +# Secrets are fetched from 1Password in the playbook pre_tasks + +forgejo_app_name: Forgejo +forgejo_app_slogan: "Beyond coding. We Forge." +forgejo_run_user: forgejo +forgejo_run_mode: prod + +# Paths (brew-managed for now, will change to mcquack in Phase 3) +forgejo_work_path: /opt/homebrew/var/forgejo +forgejo_config_path: "{{ forgejo_work_path }}/custom/conf/app.ini" +forgejo_data_path: "{{ forgejo_work_path }}/data" +forgejo_repo_root: "{{ forgejo_data_path }}/forgejo-repositories" +forgejo_lfs_path: "{{ forgejo_data_path }}/lfs" +forgejo_log_path: "{{ forgejo_work_path }}/log" + +# Server settings +forgejo_http_addr: 0.0.0.0 +forgejo_http_port: 3001 +forgejo_domain: forge.tail8d86e.ts.net +forgejo_ssh_domain: "{{ forgejo_domain }}" +forgejo_root_url: "https://{{ forgejo_domain }}/" +forgejo_offline_mode: true + +# SSH settings (built-in SSH server) +forgejo_disable_ssh: false +forgejo_start_ssh_server: true +forgejo_builtin_ssh_user: forgejo +forgejo_ssh_port: 22 +forgejo_ssh_listen_port: 2200 +forgejo_lfs_start_server: true + +# Database (SQLite) +forgejo_db_type: sqlite3 +forgejo_db_path: "{{ forgejo_data_path }}/forgejo.db" + +# Service settings +forgejo_disable_registration: true +forgejo_require_signin_view: false + +# Session +forgejo_session_provider: file + +# Logging +forgejo_log_mode: console +forgejo_log_level: info + +# Actions (Forgejo CI) +forgejo_actions_enabled: true +forgejo_actions_default_url: https://code.forgejo.org diff --git a/ansible/roles/forgejo/tasks/main.yml b/ansible/roles/forgejo/tasks/main.yml index 1e021c4..a6d27b9 100644 --- a/ansible/roles/forgejo/tasks/main.yml +++ b/ansible/roles/forgejo/tasks/main.yml @@ -1,26 +1,29 @@ --- -# Note: forgejo config at /opt/homebrew/var/forgejo/custom/conf/app.ini -# is not managed here (contains secrets). It is backed up by borgmatic. +# Forgejo role +# +# Currently uses brew-managed forgejo. Phase 3 of ci-cd-bootstrap will +# transition to mcquack LaunchAgent with CI-built binary. +# +# Secrets (lfs_jwt_secret, internal_token, oauth2_jwt_secret) are fetched +# from 1Password in the playbook pre_tasks. - name: Install forgejo via homebrew community.general.homebrew: name: forgejo state: present -- name: Check forgejo config exists - ansible.builtin.stat: - path: /opt/homebrew/var/forgejo/custom/conf/app.ini - register: forgejo_config +- name: Ensure forgejo config directory exists + ansible.builtin.file: + path: "{{ forgejo_work_path }}/custom/conf" + state: directory + mode: '0755' -- name: Fail if forgejo config is missing - ansible.builtin.fail: - msg: | - Forgejo config not found at /opt/homebrew/var/forgejo/custom/conf/app.ini - This file contains secrets and is not managed by ansible. - To restore from backup, run: - borgmatic --config ~/.config/borgmatic/config.yaml extract --archive latest \ - --path /opt/homebrew/var/forgejo/custom/conf/app.ini - when: not forgejo_config.stat.exists +- name: Deploy forgejo config + ansible.builtin.template: + src: app.ini.j2 + dest: "{{ forgejo_config_path }}" + mode: '0600' + notify: Restart forgejo - name: Ensure forgejo service is started ansible.builtin.command: brew services start forgejo diff --git a/ansible/roles/forgejo/templates/app.ini.j2 b/ansible/roles/forgejo/templates/app.ini.j2 new file mode 100644 index 0000000..ec0c396 --- /dev/null +++ b/ansible/roles/forgejo/templates/app.ini.j2 @@ -0,0 +1,82 @@ +# {{ ansible_managed }} +APP_NAME = {{ forgejo_app_name }} +APP_SLOGAN = {{ forgejo_app_slogan }} +RUN_USER = {{ forgejo_run_user }} +WORK_PATH = {{ forgejo_work_path }} +RUN_MODE = {{ forgejo_run_mode }} + +[server] +HTTP_ADDR = {{ forgejo_http_addr }} +HTTP_PORT = {{ forgejo_http_port }} +SSH_DOMAIN = {{ forgejo_ssh_domain }} +DOMAIN = {{ forgejo_domain }} +ROOT_URL = {{ forgejo_root_url }} +APP_DATA_PATH = {{ forgejo_data_path }} +DISABLE_SSH = {{ forgejo_disable_ssh | lower }} +START_SSH_SERVER = {{ forgejo_start_ssh_server | lower }} +BUILTIN_SSH_SERVER_USER = {{ forgejo_builtin_ssh_user }} +SSH_PORT = {{ forgejo_ssh_port }} +SSH_LISTEN_PORT = {{ forgejo_ssh_listen_port }} +LFS_START_SERVER = {{ forgejo_lfs_start_server | lower }} +LFS_JWT_SECRET = {{ forgejo_lfs_jwt_secret }} +OFFLINE_MODE = {{ forgejo_offline_mode | lower }} + +[database] +DB_TYPE = {{ forgejo_db_type }} +PATH = {{ forgejo_db_path }} +LOG_SQL = false + +[repository] +ROOT = {{ forgejo_repo_root }} +DEFAULT_REPO_UNITS = repo.code,repo.issues,repo.pulls,repo.releases,repo.wiki,repo.projects,repo.packages,repo.actions + +[lfs] +PATH = {{ forgejo_lfs_path }} + +[mailer] +ENABLED = false + +[service] +REGISTER_EMAIL_CONFIRM = false +ENABLE_NOTIFY_MAIL = false +DISABLE_REGISTRATION = {{ forgejo_disable_registration | lower }} +ALLOW_ONLY_EXTERNAL_REGISTRATION = false +ENABLE_CAPTCHA = false +REQUIRE_SIGNIN_VIEW = {{ forgejo_require_signin_view | lower }} +DEFAULT_KEEP_EMAIL_PRIVATE = false +DEFAULT_ALLOW_CREATE_ORGANIZATION = true +DEFAULT_ENABLE_TIMETRACKING = true +NO_REPLY_ADDRESS = noreply.indri + +[openid] +ENABLE_OPENID_SIGNIN = false +ENABLE_OPENID_SIGNUP = false + +[cron.update_checker] +ENABLED = false + +[session] +PROVIDER = {{ forgejo_session_provider }} + +[log] +MODE = {{ forgejo_log_mode }} +LEVEL = {{ forgejo_log_level }} +ROOT_PATH = {{ forgejo_log_path }} + +[repository.pull-request] +DEFAULT_MERGE_STYLE = merge + +[repository.signing] +DEFAULT_TRUST_MODEL = committer + +[security] +INSTALL_LOCK = true +INTERNAL_TOKEN = {{ forgejo_internal_token }} +PASSWORD_HASH_ALGO = pbkdf2_hi + +[oauth2] +JWT_SECRET = {{ forgejo_oauth2_jwt_secret }} + +[actions] +ENABLED = {{ forgejo_actions_enabled | lower }} +DEFAULT_ACTIONS_URL = {{ forgejo_actions_default_url }} diff --git a/argocd/apps/forgejo-runner.yaml b/argocd/apps/forgejo-runner.yaml new file mode 100644 index 0000000..a584d33 --- /dev/null +++ b/argocd/apps/forgejo-runner.yaml @@ -0,0 +1,23 @@ +# 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/configmap.yaml b/argocd/manifests/forgejo-runner/configmap.yaml new file mode 100644 index 0000000..584efe0 --- /dev/null +++ b/argocd/manifests/forgejo-runner/configmap.yaml @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..d0939de --- /dev/null +++ b/argocd/manifests/forgejo-runner/deployment.yaml @@ -0,0 +1,63 @@ +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: code.forgejo.org/forgejo/runner:3.5.1 + 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 new file mode 100644 index 0000000..332c49c --- /dev/null +++ b/argocd/manifests/forgejo-runner/kustomization.yaml @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..19441b1 --- /dev/null +++ b/argocd/manifests/forgejo-runner/namespace.yaml @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..427d8df --- /dev/null +++ b/argocd/manifests/forgejo-runner/secret-token.yaml.tpl @@ -0,0 +1,10 @@ +# 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 new file mode 100644 index 0000000..ef8cb25 --- /dev/null +++ b/argocd/manifests/forgejo-runner/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: forgejo-runner + namespace: forgejo-runner diff --git a/plans/ci-cd-bootstrap/P4_container_builds.md b/plans/ci-cd-bootstrap/P4_container_builds.md index 6e4297c..6a7ced8 100644 --- a/plans/ci-cd-bootstrap/P4_container_builds.md +++ b/plans/ci-cd-bootstrap/P4_container_builds.md @@ -17,6 +17,78 @@ With Forgejo Actions operational, we can now build container images for: --- +## Use Case 0: Custom Runner Image + +### Problem + +The stock `forgejo/runner` image lacks tools needed for standard GitHub Actions: +- **Node.js** - Required by most actions (checkout, setup-*, etc.) +- **Docker CLI** - For building container images +- **Git** - For repository operations +- **Common build tools** - make, gcc, etc. + +In host mode, jobs run directly in the runner container, so these tools must be pre-installed. + +### Solution + +Build a custom runner image with all necessary tools: + +```dockerfile +# argocd/manifests/forgejo-runner/Dockerfile +FROM code.forgejo.org/forgejo/runner:3.5.1 + +# Install Node.js (required for most GitHub Actions) +RUN apt-get update && apt-get install -y \ + nodejs \ + npm \ + git \ + curl \ + docker.io \ + make \ + gcc \ + && rm -rf /var/lib/apt/lists/* +``` + +### Workflow + +Create `.forgejo/workflows/build-runner.yml`: + +```yaml +name: Build Runner Image + +on: + push: + paths: + - 'argocd/manifests/forgejo-runner/Dockerfile' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + git clone --depth 1 "${{ gitea.server_url }}/${{ gitea.repository }}.git" . + + - name: Build and push + run: | + cd argocd/manifests/forgejo-runner + docker build -t registry.tail8d86e.ts.net/blumeops/forgejo-runner:latest . + docker push registry.tail8d86e.ts.net/blumeops/forgejo-runner:latest +``` + +### Update Deployment + +Once the custom image is built, update `argocd/manifests/forgejo-runner/deployment.yaml`: + +```yaml +image: registry.tail8d86e.ts.net/blumeops/forgejo-runner:latest +``` + +This enables standard GitHub Actions like `actions/checkout@v4` to work in host mode. + +--- + ## Use Case 1: devpi Custom Image ### Current State