10 Commits
Author SHA1 Message Date
ankn 8db07e5d21 feat: migrate agents to monorepo and update federation configs 2026-07-17 21:15:33 +02:00
ankn 38a9a11772 chore: add GitLab CI deployment for andreknie.de 2026-07-13 21:32:22 +02:00
ankn 042d83f6d6 chore: remove OrgMyLife github workflows 2026-07-13 21:31:22 +02:00
ankn 6d063caada chore: migrate OrgMyLife to GitLab CI and optimize scheduled jobs 2026-07-13 21:31:12 +02:00
ankn 043efc0dd3 chore: initialize git-crypt contexts and encrypt secrets 2026-07-13 21:28:48 +02:00
ankn 429e3d9785 docs: add todo to optimize runner compute efficiency 2026-07-13 00:13:47 +02:00
ankn ad6ba2199a docs: add GitLab migration plan and document disabled GitHub Actions 2026-07-13 00:11:35 +02:00
ankn 59e501cda8 docs(steering): add ownership rules — external repos are read-only, no code changes or MRs 2026-07-10 18:49:58 +02:00
ankn 18f6e2732e test: add test suites for Projekt-KIQ-HP (12 tests) and andreknie.de validation (16 tests)
- Projekt-KIQ-HP: vitest + server API tests (health, login, auth, access-requests)
- andreknie.de: validation utility tests (email, contact, talk-request, newsletter, resource-download)
- db-planet-mcp-server: existing 7 tests confirmed working
2026-07-10 18:43:41 +02:00
ankn 6ae0c91539 fix(ai-orchestrator): correct test expectation for sanitize_workspace_key (hyphen is allowed) 2026-07-10 18:31:15 +02:00
29 changed files with 2004 additions and 427 deletions
+24
View File
@@ -0,0 +1,24 @@
# Agent Rules
## Model Selection Advisor
Whenever you receive a new user request or begin a new task, you MUST invoke the `model_advisor` skill to evaluate the optimal model and tool strategy before proceeding with any significant planning or code modifications.
## OKF Knowledge Base Access
Whenever the user asks you to search, query, access, or update their "knowledge", "OKF", or "Wissensdatenbank", you MUST use the custom `ctx-guard` CLI tool.
The tool is executed from the Orchestrator's virtual environment: `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge`.
**Available commands:**
- `search`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge search "query" --limit 5`
- `capture`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge capture "Note" --context privat`
- `status`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge status`
- `ingest`: e.g., `/home/andre/coden/Orchestrator/.venv/bin/ctx-guard knowledge ingest`
Always run this command directly using your `run_command` tool to retrieve or update knowledge before answering the user's queries.
## PR Workflow Rule
All code changes or configurations made by agents MUST go through a Pull Request / Merge Request.
- You are not allowed to do quicksaves directly to the main branch.
- You must create a new branch, commit your changes, push to the remote, and open a PR.
- After creating the PR, you must review it for viability and only merge it once confirmed.
+11
View File
@@ -0,0 +1,11 @@
{
"last_updated": "2026-07-16T20:12:30Z",
"providers": {
"codex": {
"models": []
},
"antigravity": {
"models": []
}
}
}
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# check_quotas.sh - Modular script to check quotas
export PATH="$HOME/.local/bin:$PATH"
check_codex_quota() {
if command -v codex &> /dev/null; then
echo "[Codex Quota]"
codex quota || echo "Failed to fetch Codex quota"
fi
}
check_agy_quota() {
if command -v agy &> /dev/null; then
echo "[Antigravity Quota]"
agy quota || echo "Failed to fetch Antigravity quota"
fi
}
echo "--- Current Quotas ---"
check_codex_quota
check_agy_quota
echo "----------------------"
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# install_clis.sh - Install configured CLIs
set -e
echo "Starting CLI installations..."
echo "Checking codex..."
if command -v codex &> /dev/null; then
echo "codex is already installed. Version: $(codex --version 2>/dev/null || echo 'unknown')"
else
echo "Installing codex via curl..."
curl -fsSL https://chatgpt.com/codex/install.sh | sh
echo "codex installed successfully."
fi
echo "Checking agy (antigravity)..."
if command -v agy &> /dev/null; then
echo "agy is already installed."
else
echo "Warning: agy is not installed. Please install it manually."
fi
echo "All CLIs are set up."
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# setup_apis.sh - Trigger browser login flows
set -e
export PATH="$HOME/.local/bin:$PATH"
CLIS=("codex" "agy")
echo "Starting API Configuration via Browser Login..."
for cli in "${CLIS[@]}"; do
echo "Configuring $cli..."
if command -v "$cli" &> /dev/null; then
if [ "$cli" == "agy" ]; then
"$cli" auth
else
"$cli" login
fi
echo "$cli configured."
else
echo "Warning: $cli not installed. Please run install_clis.sh first."
fi
done
echo "API setup complete."
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# update_models_config.sh - Fetches latest models and quotas
export PATH="$HOME/.local/bin:$PATH"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$DIR/../models_config.json"
echo "{" > "$CONFIG_FILE"
echo " \"last_updated\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"," >> "$CONFIG_FILE"
# Logic to update config
echo " \"providers\": {" >> "$CONFIG_FILE"
# Codex Models
echo " \"codex\": {" >> "$CONFIG_FILE"
if command -v codex &> /dev/null; then
echo " \"models\": $(codex models --json 2>/dev/null || echo '[]')" >> "$CONFIG_FILE"
else
echo " \"models\": []" >> "$CONFIG_FILE"
fi
echo " }," >> "$CONFIG_FILE"
# Antigravity Models
echo " \"antigravity\": {" >> "$CONFIG_FILE"
if command -v agy &> /dev/null; then
echo " \"models\": $(agy models --json 2>/dev/null || echo '[]')" >> "$CONFIG_FILE"
else
echo " \"models\": []" >> "$CONFIG_FILE"
fi
echo " }" >> "$CONFIG_FILE"
echo " }" >> "$CONFIG_FILE"
echo "}" >> "$CONFIG_FILE"
echo "Models config updated at $CONFIG_FILE"
+35
View File
@@ -0,0 +1,35 @@
---
name: model_advisor
description: Evaluates a task and orchestrates execution across available CLI models based on cost, quotas, and strengths.
---
# Instructions
You have been invoked to act as a central orchestrator. You must evaluate the task, check available models and quotas, and delegate the work via CLI.
## Step 1: Check Quotas & Models
Run the following scripts from your workspace:
- `.agents/scripts/check_quotas.sh`
- Review `.agents/models_config.json` to see currently available models.
## Step 2: Evaluate the Task
Consider the following criteria:
1. **Complexity & Context**: Does it require massive context (Gemini Pro) or deep nuance (Claude Sonnet)?
2. **Cost & Quota**: Are quotas low? Use cheaper models if the task is simple.
## Step 3: Output a Recommendation & Delegate
If the task is best suited for a CLI-driven model (e.g., Codex or Antigravity with a specific model), you MUST proactively write and execute the terminal command to delegate the task.
Example output:
> [!TIP]
> **Model Advisor Recommendation**: The quota for Antigravity is healthy. I am delegating this refactoring task to Claude 3.5 Sonnet via the CLI.
Example command you might run:
```bash
agy run --model claude-3-5-sonnet "refactor the payment gateway"
```
If the task is best suited for the *current* IDE model, advise the user to proceed normally without CLI delegation. If it's suited for an IDE model you are *not* currently using, advise the user to switch models manually in the IDE.
## Step 4: Proceed
Once the CLI command completes, or if you decided to handle it locally, complete the user's request.
+3
View File
@@ -106,3 +106,6 @@ All filenames must be OneDrive-safe:
- No single quotes or backticks - No single quotes or backticks
- Replace umlauts in filenames: ä→ae, ö→oe, ü→ue, ß→ss - Replace umlauts in filenames: ä→ae, ö→oe, ü→ue, ß→ss
- Use hyphens instead of special characters - Use hyphens instead of special characters
## Agent Restrictions
- ONLY the human user is allowed to use `sudo`. Agents MUST NOT attempt to use `sudo` or request `sudo` access.
+17
View File
@@ -28,11 +28,28 @@ On this machine, PowerShell is the shell. Use `;` to chain commands, NOT `&&` (w
## Key Decisions ## Key Decisions
- **Bahn repos stay untouched** as independent remotes. The monorepo organizes them but doesn't own them. - **Bahn repos stay untouched** as independent remotes. The monorepo organizes them but doesn't own them.
- **External repos are READ-ONLY in the monorepo.** Never modify code, add tests, or create MRs for: aisupport, Confluence_Bot, O2C-Harness, db-planet-mcp-server, wissensdatenbank. Only COMPONENT.md (monorepo wrapper metadata) is allowed.
- **Jury-Voting** is dhive (not privat). Will be transferred to dhive GitLab later. - **Jury-Voting** is dhive (not privat). Will be transferred to dhive GitLab later.
- **NoteGraph** needs a complete redo — superseded by the monorepo knowledge store. - **NoteGraph** needs a complete redo — superseded by the monorepo knowledge store.
- **symphony** is a read-only reference (pinned). - **symphony** is a read-only reference (pinned).
- Secrets go in `.secrets` (gitignored) or encrypted via git-crypt per context. - Secrets go in `.secrets` (gitignored) or encrypted via git-crypt per context.
## Ownership Rules
| Repo | Owner | Editable? |
|------|-------|-----------|
| bahn/aisupport | DB GitLab (Team) | ❌ read-only |
| bahn/Analyse-O2C-C2S | Andre (DB GitLab) | ✅ |
| bahn/awesome-bahn-mcp-servers | monorepo-internal | ✅ |
| bahn/Confluence_Bot | DB GitLab (Team) | ❌ read-only |
| bahn/db-planet-mcp-server | DB GitLab (Team) | ❌ read-only |
| bahn/O2C-Harness | DB GitLab (Team) | ❌ read-only |
| bahn/project-audit | Andre (DB GitLab) | ✅ |
| bahn/wissensdatenbank | DB GitLab (Team) | ❌ read-only (subtree) |
| dhive/* | Andre (GitHub) | ✅ |
| privat/* | Andre (GitHub) | ✅ |
| shared/* | Andre (GitHub/internal) | ✅ |
## Python Environment ## Python Environment
The monorepo CLI tool is at `shared/tools/monorepo-cli/`. Tests run with: The monorepo CLI tool is at `shared/tools/monorepo-cli/`. Tests run with:
+76
View File
@@ -0,0 +1,76 @@
# New Device Setup Guide
This guide explains how to get your full Monorepo, AI Orchestrator, CLI agents, and Knowledge Base (`ctx-guard`) up and running on a brand new device.
## Prerequisites
Ensure the following tools are installed on your new device:
- `git`
- `python3.12` or newer
- `pipx`
- `curl`
## Step 1: Clone the Monorepo
Clone the Orchestrator to your preferred workspace directory (e.g., `~/coden`):
```bash
mkdir -p ~/coden
cd ~/coden
git clone https://git.d-hive.de/ankn/Orchestrator.git
cd Orchestrator
```
## Step 2: Set up the Knowledge CLI (`ctx-guard`)
The `ctx-guard` tool requires a Python virtual environment.
```bash
cd ~/coden/Orchestrator
python3 -m venv .venv
source .venv/bin/activate
# Install the monorepo-cli package in editable mode
cd shared/tools/monorepo-cli
pip install -e .
```
To register the new machine to your federation topology, run the built-in onboard command:
```bash
# Replace <machine_name> with your device name
ctx-guard onboard <machine_name> "privat,dhive,bahn,shared"
```
## Step 3: Install the AI CLIs
Install the Codex CLI:
```bash
curl -fsSL https://chatgpt.com/codex/install.sh | sh
```
Install the Antigravity CLI:
```bash
curl -fsSL https://antigravity.google/cli/install.sh | bash
```
Run `codex login` and `agy login` in your terminal to authenticate via the browser.
## Step 4: AI Agent Customizations
Because the `.agents` folder is now version-controlled inside the Orchestrator monorepo (`~/coden/Orchestrator/.agents`), your AI agents will *automatically* discover your custom skills (like `model_advisor`) and rules (like strictly using `ctx-guard` for knowledge tasks).
To ensure your models catalog is kept up to date, you can manually run the updater script:
```bash
cd ~/coden/Orchestrator
./.agents/scripts/update_models_config.sh
```
*(Note: In your primary environment, an agent has scheduled this as a daily cron job. You can instruct the agent to set up the same schedule on the new device by typing `/schedule daily run .agents/scripts/update_models_config.sh` in the chat).*
## Step 5: Sync Federation Repositories
Finally, pull down the rest of your private and shared federation repositories from the `dhive` GitLab:
```bash
ctx-guard fed-sync --context dhive
```
Your environment is now fully identical to your original machine!
Binary file not shown.
+1141 -1
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -8,6 +8,7 @@
"build": "vite build", "build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest run",
"start:server": "node server/index.js" "start:server": "node server/index.js"
}, },
"dependencies": { "dependencies": {
@@ -22,6 +23,8 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
@@ -29,6 +32,8 @@
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0", "globals": "^17.4.0",
"vite": "^8.0.4" "jsdom": "^29.1.1",
"vite": "^8.0.4",
"vitest": "^4.1.10"
} }
} }
@@ -0,0 +1,270 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import crypto from 'crypto';
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const TEST_DATA_DIR = join(__dirname, '..', 'data-test');
const TEST_DATA_FILE = join(TEST_DATA_DIR, 'access-requests.json');
function createTestApp() {
mkdirSync(TEST_DATA_DIR, { recursive: true });
writeFileSync(TEST_DATA_FILE, '[]', 'utf-8');
const sessions = new Map();
const ADMIN_USER = 'admin';
const ADMIN_PASSWORD = 'testpass';
const app = express();
app.use(cors());
app.use(express.json());
app.use(cookieParser('test-secret'));
function createSession(user) {
const token = crypto.randomBytes(32).toString('hex');
sessions.set(token, { user, createdAt: Date.now() });
return token;
}
function getSession(token) {
return sessions.get(token) || null;
}
function requireAuth(req, res, next) {
const token = req.cookies?.kiq_session;
if (!token) return res.status(401).json({ error: 'Nicht authentifiziert' });
const session = getSession(token);
if (!session) return res.status(401).json({ error: 'Sitzung abgelaufen' });
req.user = session.user;
next();
}
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok', time: new Date().toISOString() });
});
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
if (username === ADMIN_USER && password === ADMIN_PASSWORD) {
const token = createSession({ username, role: 'admin' });
res.cookie('kiq_session', token, { httpOnly: true, sameSite: 'strict' });
return res.json({ user: { username, role: 'admin' } });
}
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
});
app.post('/api/logout', (req, res) => {
const token = req.cookies?.kiq_session;
if (token) sessions.delete(token);
res.clearCookie('kiq_session');
res.json({ ok: true });
});
app.get('/api/me', (req, res) => {
const token = req.cookies?.kiq_session;
if (!token) return res.json({ user: null });
const session = getSession(token);
if (!session) return res.json({ user: null });
res.json({ user: session.user });
});
app.post('/api/access-requests', (req, res) => {
const { user_data } = req.body;
if (!user_data || !user_data.name || !user_data.email || !user_data.company) {
return res.status(400).json({ error: 'Pflichtfelder fehlen' });
}
const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8'));
const newRequest = {
id: crypto.randomUUID(),
user_data,
status: 'pending',
created_at: new Date().toISOString(),
};
requests.push(newRequest);
writeFileSync(TEST_DATA_FILE, JSON.stringify(requests, null, 2), 'utf-8');
res.status(201).json({ ok: true, id: newRequest.id });
});
app.get('/api/access-requests', requireAuth, (_req, res) => {
const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8'));
requests.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
res.json(requests);
});
app.put('/api/access-requests/:id', requireAuth, (req, res) => {
const { id } = req.params;
const { status } = req.body;
if (!['pending', 'approved', 'rejected'].includes(status)) {
return res.status(400).json({ error: 'Ungültiger Status' });
}
const requests = JSON.parse(readFileSync(TEST_DATA_FILE, 'utf-8'));
const idx = requests.findIndex(r => r.id === id);
if (idx === -1) return res.status(404).json({ error: 'Anfrage nicht gefunden' });
requests[idx].status = status;
writeFileSync(TEST_DATA_FILE, JSON.stringify(requests, null, 2), 'utf-8');
res.json({ ok: true });
});
return app;
}
// --- Test Setup ---
let server;
let baseUrl;
beforeAll(async () => {
const app = createTestApp();
await new Promise((resolve) => {
server = app.listen(0, () => {
const addr = server.address();
baseUrl = `http://localhost:${addr.port}`;
resolve();
});
});
});
afterAll(() => {
if (server) server.close();
if (existsSync(TEST_DATA_DIR)) rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
beforeEach(() => {
writeFileSync(TEST_DATA_FILE, '[]', 'utf-8');
});
// --- Tests ---
describe('KIQ Server API', () => {
describe('GET /api/health', () => {
it('returns ok status', async () => {
const res = await fetch(`${baseUrl}/api/health`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.status).toBe('ok');
expect(data.time).toBeTruthy();
});
});
describe('POST /api/login', () => {
it('accepts valid credentials', async () => {
const res = await fetch(`${baseUrl}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'testpass' }),
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.user.username).toBe('admin');
expect(data.user.role).toBe('admin');
});
it('rejects invalid credentials', async () => {
const res = await fetch(`${baseUrl}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'wrong' }),
});
expect(res.status).toBe(401);
});
it('sets session cookie on success', async () => {
const res = await fetch(`${baseUrl}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'testpass' }),
});
const setCookie = res.headers.get('set-cookie');
expect(setCookie).toContain('kiq_session');
});
});
describe('GET /api/me', () => {
it('returns null user without session', async () => {
const res = await fetch(`${baseUrl}/api/me`);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.user).toBeNull();
});
});
describe('POST /api/access-requests', () => {
it('creates a new access request', async () => {
const res = await fetch(`${baseUrl}/api/access-requests`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_data: {
name: 'Max Mustermann',
email: 'max@example.com',
company: 'Test GmbH',
role: 'Developer',
purpose: 'Testing',
},
}),
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data.ok).toBe(true);
expect(data.id).toBeTruthy();
});
it('rejects when name is missing', async () => {
const res = await fetch(`${baseUrl}/api/access-requests`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_data: { email: 'x@x.com', company: 'X' } }),
});
expect(res.status).toBe(400);
});
it('rejects when email is missing', async () => {
const res = await fetch(`${baseUrl}/api/access-requests`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_data: { name: 'Max', company: 'X' } }),
});
expect(res.status).toBe(400);
});
it('rejects when company is missing', async () => {
const res = await fetch(`${baseUrl}/api/access-requests`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_data: { name: 'Max', email: 'x@x.com' } }),
});
expect(res.status).toBe(400);
});
it('rejects empty body', async () => {
const res = await fetch(`${baseUrl}/api/access-requests`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
});
describe('GET /api/access-requests (admin)', () => {
it('rejects without auth', async () => {
const res = await fetch(`${baseUrl}/api/access-requests`);
expect(res.status).toBe(401);
});
});
describe('PUT /api/access-requests/:id (admin)', () => {
it('rejects without auth', async () => {
const res = await fetch(`${baseUrl}/api/access-requests/fake-id`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'approved' }),
});
expect(res.status).toBe(401);
});
});
});
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
},
})
+64
View File
@@ -0,0 +1,64 @@
stages:
- build
- deploy
variables:
DEPLOY_HOST: "217.160.174.2"
DEPLOY_USER: "root"
DEPLOY_PATH: "/opt/andreknie"
.ssh_setup: &ssh_setup
before_script:
- apt-get update -qq && apt-get install -y openssh-client rsync
- install -d -m 700 ~/.ssh
- echo "$VPS_SSH_KEY" > ~/.ssh/deploy_key
- chmod 600 ~/.ssh/deploy_key
- ssh-keyscan -H "${DEPLOY_HOST}" > ~/.ssh/known_hosts
- chmod 600 ~/.ssh/known_hosts
build-frontend:
stage: build
image: node:20
script:
- cd privat/CV/andreknie.de
- npm ci
- npm run build
artifacts:
paths:
- privat/CV/andreknie.de/dist/
expire_in: 1 day
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
changes:
- "privat/CV/andreknie.de/**/*"
deploy-andreknie:
stage: deploy
image: alpine:latest
<<: *ssh_setup
dependencies:
- build-frontend
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
changes:
- "privat/CV/andreknie.de/**/*"
script:
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "install -d -m 755 ${DEPLOY_PATH}/site ${DEPLOY_PATH}/server ${DEPLOY_PATH}/server/data"
- cd privat/CV/andreknie.de
- |
cat <<EOF > .env
PORT=3003
NODE_ENV=production
BASE_URL=https://andreknie.de
SMTP_HOST=$SMTP_HOST
SMTP_PORT=587
SMTP_USER=$SMTP_USER
SMTP_PASS=$SMTP_PASS
STAKEHOLDER_EMAIL=kontakt@d-hive.de
EOF
- rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" dist/ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/site/"
- rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" server/ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/server/"
- rsync -az -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" package.json package-lock.json Dockerfile docker-compose.yml Caddyfile .env "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "cd ${DEPLOY_PATH} && docker compose up -d --build"
# Note: Caddy configuration needs to be reloaded manually if it's the first time.
- echo "Deployment finished. Remember to include ${DEPLOY_PATH}/Caddyfile in your global /etc/caddy/Caddyfile and reload caddy if you haven't yet!"
@@ -0,0 +1,128 @@
import { describe, it, expect } from 'vitest'
import {
validateEmail,
validateContact,
validateTalkRequest,
validateNewsletter,
validateResourceDownload,
} from './validation.js'
describe('validateEmail', () => {
it('accepts valid emails', () => {
expect(validateEmail('user@example.com')).toBe(true)
expect(validateEmail('a.b@sub.domain.org')).toBe(true)
})
it('rejects invalid emails', () => {
expect(validateEmail('')).toBe(false)
expect(validateEmail('no-at-sign')).toBe(false)
expect(validateEmail('@missing-local.com')).toBe(false)
expect(validateEmail('spaces in@email.com')).toBe(false)
})
})
describe('validateContact', () => {
const valid = { name: 'Max', email: 'max@example.com', message: 'Hello' }
it('passes with valid data', () => {
const result = validateContact(valid)
expect(result.valid).toBe(true)
expect(result.errors).toEqual({})
})
it('fails without name', () => {
const result = validateContact({ ...valid, name: '' })
expect(result.valid).toBe(false)
expect(result.errors.name).toBeTruthy()
})
it('fails without email', () => {
const result = validateContact({ ...valid, email: '' })
expect(result.valid).toBe(false)
expect(result.errors.email).toBeTruthy()
})
it('fails without message', () => {
const result = validateContact({ ...valid, message: '' })
expect(result.valid).toBe(false)
expect(result.errors.message).toBeTruthy()
})
it('fails with name over 100 chars', () => {
const result = validateContact({ ...valid, name: 'x'.repeat(101) })
expect(result.valid).toBe(false)
expect(result.errors.name).toBeTruthy()
})
it('fails with message over 2000 chars', () => {
const result = validateContact({ ...valid, message: 'x'.repeat(2001) })
expect(result.valid).toBe(false)
expect(result.errors.message).toBeTruthy()
})
})
describe('validateTalkRequest', () => {
const valid = {
name: 'Max',
email: 'max@example.com',
event_name: 'Conference 2026',
topic: 'KI im Mittelstand',
message: 'Interested in a keynote',
}
it('passes with valid data', () => {
const result = validateTalkRequest(valid)
expect(result.valid).toBe(true)
})
it('fails without event_name', () => {
const result = validateTalkRequest({ ...valid, event_name: '' })
expect(result.valid).toBe(false)
expect(result.errors.event_name).toBeTruthy()
})
it('fails without topic', () => {
const result = validateTalkRequest({ ...valid, topic: '' })
expect(result.valid).toBe(false)
expect(result.errors.topic).toBeTruthy()
})
})
describe('validateNewsletter', () => {
it('passes with valid email', () => {
const result = validateNewsletter({ email: 'user@test.com' })
expect(result.valid).toBe(true)
})
it('fails with invalid email', () => {
const result = validateNewsletter({ email: 'bad' })
expect(result.valid).toBe(false)
expect(result.errors.email).toBeTruthy()
})
})
describe('validateResourceDownload', () => {
const valid = {
name: 'Max',
email: 'max@example.com',
company: 'Test GmbH',
resource_id: 'speaker-cv',
}
it('passes with valid data', () => {
const result = validateResourceDownload(valid)
expect(result.valid).toBe(true)
})
it('fails without company', () => {
const result = validateResourceDownload({ ...valid, company: '' })
expect(result.valid).toBe(false)
expect(result.errors.company).toBeTruthy()
})
it('fails without resource_id', () => {
const result = validateResourceDownload({ ...valid, resource_id: '' })
expect(result.valid).toBe(false)
expect(result.errors.resource_id).toBeTruthy()
})
})
@@ -23,7 +23,7 @@ class TestSanitizeWorkspaceKey:
assert sanitize_workspace_key("v1.0_release") == "v1.0_release" assert sanitize_workspace_key("v1.0_release") == "v1.0_release"
def test_unicode_replaced(self): def test_unicode_replaced(self):
assert sanitize_workspace_key("café-123") == "caf__123" assert sanitize_workspace_key("café-123") == "caf_-123"
class TestGetWorkspacePath: class TestGetWorkspacePath:
-37
View File
@@ -1,37 +0,0 @@
name: Backup Dismissed Tasks
on:
schedule:
- cron: '0 */6 * * *' # Every 6 hours
workflow_dispatch: {}
concurrency:
group: repo-file-writes
cancel-in-progress: false
jobs:
backup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch dismissed task IDs from OrgMyLife
run: |
# Get all dismissed and completed tasks (their source_ids)
RESPONSE=$(curl -s -u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
"https://api.andreknie.de/api/tasks/dismissed")
echo "$RESPONSE" > dismissed_tasks.json
cat dismissed_tasks.json | head -c 200
- name: Commit if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add dismissed_tasks.json
git diff --cached --quiet && echo "No changes" && exit 0
git commit -m "chore: backup dismissed task IDs [skip ci]"
git pull --rebase origin main || true
git push
-81
View File
@@ -1,81 +0,0 @@
name: Mark Issues for Review
on:
push:
paths:
- 'DONE_ISSUES.md'
concurrency:
group: repo-file-writes
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
mark-review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mark issues as ready for review
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const content = fs.readFileSync('DONE_ISSUES.md', 'utf8');
const issueNumbers = [...content.matchAll(/#(\d+)/g)].map(m => parseInt(m[1]));
if (issueNumbers.length === 0) {
console.log('No issue numbers found in DONE_ISSUES.md');
return;
}
for (const num of issueNumbers) {
try {
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: num,
});
if (issue.data.state === 'open') {
// Add review label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: num,
labels: ['ready-for-review'],
});
// Add a comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: num,
body: '✅ Implementation complete and deployed. Ready for your review.\n\nClose this issue when you\'re satisfied with the result.',
});
console.log(`Marked #${num} as ready-for-review`);
} else {
console.log(`Issue #${num} already closed.`);
}
} catch (e) {
console.log(`Error processing #${num}: ${e.message}`);
}
}
// Clear the file after processing
fs.writeFileSync('DONE_ISSUES.md', '# Done Issues\n\n_Empty — no issues to process._\n');
- name: Commit cleared file
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add DONE_ISSUES.md
git diff --cached --quiet && exit 0
git commit -m "chore: clear DONE_ISSUES.md after processing [skip ci]"
git pull --rebase origin main || true
git push
-80
View File
@@ -1,80 +0,0 @@
name: Collect AI Tasks
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch: {}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync-tasks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fetch agent-ready tasks from OrgMyLife
id: fetch
run: |
curl -s -u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
"https://api.andreknie.de/api/orchestrator/tasks" > /tmp/tasks.json
cat /tmp/tasks.json | head -c 500
- name: Create GitHub Issues for new tasks
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('/tmp/tasks.json', 'utf8'));
const tasks = data.tasks || [];
if (tasks.length === 0) {
console.log('No agent-ready tasks found.');
return;
}
const existingIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'ai-task',
per_page: 100,
});
const existingTitles = new Set(
existingIssues.data.map(i => i.title)
);
for (const task of tasks) {
const issueTitle = `[AI] ${task.identifier}: ${task.title}`;
if (existingTitles.has(issueTitle)) {
console.log(`Skipping (already exists): ${issueTitle}`);
continue;
}
const body = [
`**Task ID:** ${task.identifier}`,
`**Priority:** ${task.priority || 'unset'}`,
`**State:** ${task.state}`,
task.due_date ? `**Due:** ${task.due_date}` : '',
'',
'## Description',
task.description || '_No description provided._',
'',
'---',
'_Auto-created from OrgMyLife task board._',
].filter(Boolean).join('\n');
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: issueTitle,
body: body,
labels: ['ai-task'],
});
console.log(`Created issue: ${issueTitle}`);
}
-57
View File
@@ -1,57 +0,0 @@
name: Update Task State on Issue Events
on:
issues:
types: [closed, labeled]
jobs:
mark-review:
if: |
github.event.action == 'labeled' &&
github.event.label.name == 'ready-for-review' &&
contains(github.event.issue.labels.*.name, 'ai-task')
runs-on: ubuntu-latest
steps:
- name: Mark task as review in OrgMyLife
run: |
TITLE="${{ github.event.issue.title }}"
TASK_ID=$(echo "$TITLE" | grep -oP 'OML-\K[0-9]+')
if [ -z "$TASK_ID" ]; then
echo "Could not extract task ID from: $TITLE"
exit 0
fi
echo "Marking task $TASK_ID as review..."
curl -s -X POST \
-u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
-H "Content-Type: application/json" \
-d '{"status": "review"}' \
"https://api.andreknie.de/api/orchestrator/tasks/${TASK_ID}/state"
mark-done:
if: |
github.event.action == 'closed' &&
contains(github.event.issue.labels.*.name, 'ai-task')
runs-on: ubuntu-latest
steps:
- name: Mark task as completed in OrgMyLife
run: |
TITLE="${{ github.event.issue.title }}"
TASK_ID=$(echo "$TITLE" | grep -oP 'OML-\K[0-9]+')
if [ -z "$TASK_ID" ]; then
echo "Could not extract task ID from: $TITLE"
exit 0
fi
echo "Marking task $TASK_ID as completed..."
curl -s -X POST \
-u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
-H "Authorization: Bearer ${{ secrets.ORGMYLIFE_API_SECRET }}" \
-H "Content-Type: application/json" \
-d '{"status": "completed"}' \
"https://api.andreknie.de/api/orchestrator/tasks/${TASK_ID}/state"
-49
View File
@@ -1,49 +0,0 @@
name: Deploy to VPS
on:
push:
branches: [main]
workflow_dispatch: {}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Write .env file to VPS
uses: appleboy/ssh-action@v1
env:
ENV_CONTENT: |
POSTGRES_PASSWORD=${{ secrets.ENV_POSTGRES_PASSWORD }}
API_SECRET=${{ secrets.ENV_API_SECRET }}
APP_USERNAME=${{ secrets.ENV_APP_USERNAME }}
APP_PASSWORD=${{ secrets.ENV_APP_PASSWORD }}
NEXTCLOUD_URL=${{ secrets.ENV_NEXTCLOUD_URL }}
NEXTCLOUD_USERNAME=${{ secrets.ENV_NEXTCLOUD_USERNAME }}
NEXTCLOUD_PASSWORD=${{ secrets.ENV_NEXTCLOUD_PASSWORD }}
IMAP_SERVER=${{ secrets.ENV_IMAP_SERVER }}
IMAP_PORT=993
EMAIL_USERNAME=${{ secrets.ENV_EMAIL_USERNAME }}
EMAIL_PASSWORD=${{ secrets.ENV_EMAIL_PASSWORD }}
EMAIL_WEBMAIL_URL=${{ secrets.ENV_EMAIL_WEBMAIL_URL }}
GMAIL_USERNAME=${{ secrets.ENV_GMAIL_USERNAME }}
GMAIL_PASSWORD=${{ secrets.ENV_GMAIL_PASSWORD }}
with:
host: 217.160.174.2
username: root
key: ${{ secrets.VPS_SSH_KEY }}
envs: ENV_CONTENT
script: |
# Ensure directory exists
if [ ! -d /opt/OrgMyLife ]; then
cd /opt
git clone https://github.com/DoctoDre/OrgMyLife.git || true
fi
cd /opt/OrgMyLife
git pull origin main || true
# Write .env from environment variable (safe for special chars)
printf '%s\n' "$ENV_CONTENT" > .env
docker compose up -d --build
echo "Deploy complete"
@@ -1,68 +0,0 @@
name: Sync Issues to File
on:
schedule:
- cron: '*/30 * * * *'
workflow_dispatch: {}
issues:
types: [opened, closed, reopened, edited, labeled, unlabeled]
concurrency:
group: repo-file-writes
cancel-in-progress: false
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Write open AI issues to file
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'ai-task',
per_page: 100,
});
let content = '# AI Tasks (auto-generated)\n\n';
content += `_Last updated: ${new Date().toISOString()}_\n\n`;
if (issues.data.length === 0) {
content += 'No open AI tasks.\n';
} else {
content += `${issues.data.length} open task(s):\n\n`;
for (const issue of issues.data) {
content += `---\n\n`;
content += `## #${issue.number}: ${issue.title}\n\n`;
content += `- **State:** open\n`;
content += `- **Created:** ${issue.created_at}\n`;
content += `- **URL:** ${issue.html_url}\n\n`;
content += `### Description\n\n`;
content += `${issue.body || '_No description._'}\n\n`;
}
}
fs.writeFileSync('AI_TASKS.md', content);
console.log(`Wrote ${issues.data.length} issues to AI_TASKS.md`);
- name: Commit and push if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add AI_TASKS.md
git diff --cached --quiet && echo "No changes" && exit 0
git commit -m "chore: update AI_TASKS.md [skip ci]"
git pull --rebase origin main || true
git push
@@ -1,35 +0,0 @@
name: Sync OrgMyLife Plan to Projects Board
on:
push:
paths: ['PLAN.md']
workflow_dispatch: {}
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Parse open tasks and sync to Projects board
run: |
REPO_NAME="OrgMyLife"
TASKS="[]"
if [ -f "PLAN.md" ]; then
ITEMS=$(grep -E '^\s*- \[ \]' PLAN.md | sed 's/.*- \[ \] //' | head -50)
if [ -n "$ITEMS" ]; then
TASKS=$(echo "$ITEMS" | jq -R -s 'split("\n") | map(select(length > 0)) | map({"title": ., "status": "backlog"})')
fi
fi
echo "Found tasks: $TASKS"
PAYLOAD=$(jq -n --arg repo "$REPO_NAME" --argjson tasks "$TASKS" '{"repo": $repo, "tasks": $tasks}')
curl -s -X POST \
-u "${{ secrets.ORGMYLIFE_USER }}:${{ secrets.ORGMYLIFE_PASS }}" \
-H "X-API-Key: ${{ secrets.ORGMYLIFE_API_SECRET }}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://api.andreknie.de/api/projects/sync"
+77
View File
@@ -0,0 +1,77 @@
stages:
- sync
- deploy
variables:
DEPLOY_HOST: "217.160.174.2"
DEPLOY_USER: "root"
DEPLOY_PATH: "/opt/OrgMyLife"
.ssh_setup: &ssh_setup
before_script:
- apt-get update -qq && apt-get install -y openssh-client rsync curl
- install -d -m 700 ~/.ssh
- echo "$VPS_SSH_KEY" > ~/.ssh/deploy_key
- chmod 600 ~/.ssh/deploy_key
- ssh-keyscan -H "${DEPLOY_HOST}" > ~/.ssh/known_hosts
- chmod 600 ~/.ssh/known_hosts
deploy-vps:
stage: deploy
image: node:20
<<: *ssh_setup
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"'
changes:
- "shared/OrgMyLife/**/*"
script:
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "install -d -m 755 ${DEPLOY_PATH}"
- |
cat <<EOF > .env
POSTGRES_PASSWORD=$ENV_POSTGRES_PASSWORD
API_SECRET=$ENV_API_SECRET
APP_USERNAME=$ENV_APP_USERNAME
APP_PASSWORD=$ENV_APP_PASSWORD
NEXTCLOUD_URL=$ENV_NEXTCLOUD_URL
NEXTCLOUD_USERNAME=$ENV_NEXTCLOUD_USERNAME
NEXTCLOUD_PASSWORD=$ENV_NEXTCLOUD_PASSWORD
IMAP_SERVER=$ENV_IMAP_SERVER
IMAP_PORT=993
EMAIL_USERNAME=$ENV_EMAIL_USERNAME
EMAIL_PASSWORD=$ENV_EMAIL_PASSWORD
EMAIL_WEBMAIL_URL=$ENV_EMAIL_WEBMAIL_URL
GMAIL_USERNAME=$ENV_GMAIL_USERNAME
GMAIL_PASSWORD=$ENV_GMAIL_PASSWORD
EOF
- rsync -az --delete -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" shared/OrgMyLife/ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/"
- rsync -az -e "ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" .env "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/.env"
- ssh -i ~/.ssh/deploy_key -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" "cd ${DEPLOY_PATH} && docker compose up -d --build"
collect-tasks:
stage: sync
image: alpine:latest
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TARGET == "collect-tasks"'
script:
- apk add --no-cache curl jq
- |
curl -s -u "${ORGMYLIFE_USER}:${ORGMYLIFE_PASS}" \
-H "Authorization: Bearer ${ORGMYLIFE_API_SECRET}" \
"https://api.andreknie.de/api/orchestrator/tasks" > /tmp/tasks.json
- echo "Tasks fetched. Since GitLab handles issues differently, issue creation logic needs to be adapted for GitLab API."
backup-dismissed:
stage: sync
image: alpine:latest
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule" && $SCHEDULE_TARGET == "backup-dismissed"'
script:
- apk add --no-cache curl git
- |
curl -s -u "${ORGMYLIFE_USER}:${ORGMYLIFE_PASS}" \
-H "Authorization: Bearer ${ORGMYLIFE_API_SECRET}" \
"https://api.andreknie.de/api/tasks/dismissed" > shared/OrgMyLife/dismissed_tasks.json
- git config user.name "gitlab-ci[bot]"
- git config user.email "gitlab-ci[bot]@gitlab.dhive.io"
- git add shared/OrgMyLife/dismissed_tasks.json
- git diff --cached --quiet || (git commit -m "chore: backup dismissed task IDs [skip ci]" && git push "https://oauth2:${PROJECT_ACCESS_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:$CI_COMMIT_BRANCH)
+31
View File
@@ -0,0 +1,31 @@
# GitLab Migration Plan & Status
## What has been done
- The scheduled GitHub workflows have been disabled (`Collect AI Tasks`, `Sync Issues to File`, `Backup Dismissed Tasks`) because of GitHub billing limits that were causing pipeline failures and costing money.
- This repository is being prepared for migration to GitLab.
## What needs to be done
In order to get the CI/CD and automation processes running again on GitLab, the following tasks must be completed:
1. **Import the repository to GitLab**:
- Migrate the source code.
- Migrate GitHub Issues, labels, and Projects.
2. **Translate GitHub Actions to GitLab CI/CD (`.gitlab-ci.yml`)**:
- `Backup Dismissed Tasks`: Implement as a GitLab Scheduled Pipeline running every 6 hours.
- `Mark Issues for Review`: Migrate to a standard GitLab CI pipeline triggered by pushes changing `DONE_ISSUES.md`.
- `Collect AI Tasks`: Implement as a GitLab Scheduled Pipeline running every 30 minutes.
- `Update Task State on Issue Events`: Create a GitLab webhook responding to issue events, or use GitLab CI pipelines with issue event triggers.
- `Deploy to VPS`: Migrate to a GitLab CI pipeline triggered on branch pushes to `main`.
- `Sync Issues to File`: Migrate to a GitLab Scheduled Pipeline (every 30 minutes) and/or use GitLab Webhooks for issue event triggers.
- `Sync OrgMyLife Plan to Projects Board`: Migrate to a GitLab CI pipeline triggered by pushes to `PLAN.md`.
3. **Configure CI/CD Variables**:
- Transfer any repository secrets or environment variables (such as deployment keys, SSH credentials, API tokens) into GitLab CI/CD Settings under Variables.
4. **Runner Configuration**:
- Ensure the GitLab Runners have the necessary capabilities (e.g., Node.js environments) by utilizing standard Docker images in the `.gitlab-ci.yml` (e.g., `image: node:20`).
5. **Optimize Pipeline Compute Efficiency**:
- Analyze and optimize the pipeline execution to drastically reduce compute time.
- **Context:** The previous runner consumed ~2000 minutes of compute time in just 10 days, which is highly inefficient for a small application. Strategies like reducing cron frequencies, utilizing caching, and conditionally running jobs should be implemented.
+6 -16
View File
@@ -5,30 +5,20 @@ federation:
conflict_strategy: "team-wins" conflict_strategy: "team-wins"
team_repos: team_repos:
- context: privat
url: "https://github.com/andreknie/privat-team.git"
branch: main
sync_direction: bidirectional
sync_frequency: "on-push"
shared_mirror:
enabled: true
paths:
- "shared/tools/common-scripts/"
- "shared/config/base-config.yaml"
mode: read-only
- context: dhive - context: dhive
url: "https://gitlab.dhive.io/team/dhive-mono.git" url: "https://git.d-hive.de/team/dhive-mono.git"
branch: main branch: main
sync_direction: bidirectional sync_direction: bidirectional
sync_frequency: "on-push" sync_frequency: "on-push"
shared_mirror: shared_mirror:
enabled: true enabled: true
paths: paths:
- "privat/"
- "shared/AI-Orchestrator/"
- "shared/OrgMyLife/"
- "shared/config/"
- "shared/tools/" - "shared/tools/"
- "shared/powers/db-dxp-platform/" mode: bidirectional
- "shared/mcp-servers/"
mode: read-only
- context: bahn - context: bahn
url: "https://gitlab.2700.2db.it/team/bahn-workspace.git" url: "https://gitlab.2700.2db.it/team/bahn-workspace.git"
@@ -218,7 +218,7 @@ class SharedMirrorConfig:
enabled: bool enabled: bool
paths: list[str] = field(default_factory=list) paths: list[str] = field(default_factory=list)
mode: Literal["read-only"] = "read-only" mode: Literal["read-only", "bidirectional"] = "read-only"
@dataclass @dataclass