Compare commits

...

3 Commits
main ... dev

Author SHA1 Message Date
Jeff Emmett 5112b20dce Fix backlog-aggregator healthcheck: use bun instead of wget, increase start_period
The oven/bun:1 image doesn't include wget, causing healthchecks to always
fail. Switched to bun-based fetch check and increased start_period from 60s
to 300s to allow time for Gitea repo scanning on startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 12:13:35 +00:00
Jeff Emmett 9931fa4105 Fix healthcheck: use bun instead of missing wget
The oven/bun:1 base image's default HEALTHCHECK uses wget, which isn't
installed. Override with a bun-based HTTP check in the compose file.
Also fix pre-commit hook to fall back to npx when bun is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:09:13 +00:00
Jeff Emmett 4bfe1c82ff fix: enable Gitea scanner for aggregator — poll all repos with backlogs
- Switch docker-compose to Dockerfile.aggregator (includes git, cron, openssh)
- Remove command override so entrypoint.sh runs Gitea scanner on startup
- Use HTTPS cloning with token auth for private repos
- CI now builds from Dockerfile.aggregator and syncs docker-compose on deploy
- Cron rescans Gitea every 6 hours; aggregator now tracks 120 projects / 780 tasks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:13:53 -04:00
7 changed files with 56 additions and 15 deletions

View File

@ -31,7 +31,7 @@ jobs:
- name: Build and push image
run: |
docker build -t ${{ env.IMAGE }}:${{ env.IMAGE_TAG }} -t ${{ env.IMAGE }}:latest .
docker build -f Dockerfile.aggregator -t ${{ env.IMAGE }}:${{ env.IMAGE_TAG }} -t ${{ env.IMAGE }}:latest .
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin
docker push ${{ env.IMAGE }}:${{ env.IMAGE_TAG }}
docker push ${{ env.IMAGE }}:latest
@ -41,6 +41,7 @@ jobs:
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" | base64 -d > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
scp -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key docker-compose.yml root@${{ secrets.DEPLOY_HOST }}:/opt/apps/backlog-md/docker-compose.yml
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} "
cd /opt/apps/backlog-md
cat .last-deployed-tag 2>/dev/null > .rollback-tag || true

View File

@ -2,4 +2,4 @@
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
bun lint-staged
bun lint-staged 2>/dev/null || npx lint-staged

View File

@ -24,8 +24,8 @@ RUN bun run build:css || true
# Make entrypoint executable
RUN chmod +x /app/entrypoint.sh || true
# Create cron job for daily Gitea sync (runs at 2 AM and 2 PM)
RUN echo "0 2,14 * * * cd /app && bun run src/aggregator/gitea-scanner.ts --verbose >> /var/log/gitea-scanner.log 2>&1" > /etc/cron.d/gitea-scanner \
# Create cron job for Gitea sync (every 6 hours)
RUN echo "0 */6 * * * GIT_SSH_COMMAND='ssh -i /tmp/gitea_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' cd /app && bun run src/aggregator/gitea-scanner.ts --verbose >> /var/log/gitea-scanner.log 2>&1" > /etc/cron.d/gitea-scanner \
&& chmod 0644 /etc/cron.d/gitea-scanner \
&& crontab /etc/cron.d/gitea-scanner

View File

@ -9,6 +9,12 @@ services:
dockerfile: Dockerfile.aggregator
container_name: backlog-aggregator
restart: unless-stopped
healthcheck:
test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:6420/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
interval: 30s
timeout: 10s
start_period: 300s
retries: 3
volumes:
# Mount all project directories that contain backlog folders
- /opt/websites:/projects/websites:rw

View File

@ -1,24 +1,44 @@
services:
backlog:
build: .
image: localhost:3000/jeffemmett/backlog-md:${IMAGE_TAG:-latest}
build:
context: .
dockerfile: Dockerfile.aggregator
container_name: backlog-aggregator
restart: unless-stopped
volumes:
- /opt/websites:/projects/websites
- /opt/apps:/projects/apps
- /opt/gitea-repos:/projects/gitea
command: ["bun", "src/cli.ts", "aggregator", "--port", "6420", "--paths", "/projects/websites,/projects/apps,/projects/gitea"]
- /opt/infisical/entrypoint-wrapper.sh:/infisical-entrypoint.sh:ro
entrypoint: ["/infisical-entrypoint.sh"]
command: ["/app/entrypoint.sh"]
labels:
- "traefik.enable=true"
- "traefik.http.routers.backlog.rule=Host(`backlog.jeffemmett.com`)"
- "traefik.http.routers.backlog.entrypoints=web"
- "traefik.http.services.backlog.loadbalancer.server.port=6420"
- "traefik.docker.network=traefik-public"
healthcheck:
test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:6420/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
interval: 30s
timeout: 10s
retries: 3
start_period: 300s
networks:
- traefik-public
env_file:
- .env
environment:
- PORT=6420
- INFISICAL_CLIENT_ID=${INFISICAL_CLIENT_ID}
- INFISICAL_CLIENT_SECRET=${INFISICAL_CLIENT_SECRET}
- INFISICAL_PROJECT_SLUG=backlog-md
- INFISICAL_URL=http://infisical:8080
- NODE_ENV=production
- GITEA_URL=https://gitea.jeffemmett.com
- GITEA_OUTPUT_DIR=/projects/gitea
- GITEA_OWNER=jeffemmett
networks:
traefik-public:

View File

@ -4,12 +4,19 @@ set -e
# Start cron daemon
cron
# Configure SSH for git
export GIT_SSH_COMMAND="ssh -i /root/.ssh/gitea_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
# Configure SSH for git — copy mounted key so we can fix permissions
if [ -f /root/.ssh/gitea_ed25519 ]; then
cp /root/.ssh/gitea_ed25519 /tmp/gitea_ed25519
chmod 600 /tmp/gitea_ed25519
export GIT_SSH_COMMAND="ssh -i /tmp/gitea_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
echo "SSH key configured for Gitea access"
else
echo "WARNING: No SSH key found at /root/.ssh/gitea_ed25519 — Gitea scan will use HTTPS"
fi
# Run initial Gitea scan
echo "Running initial Gitea scan..."
cd /app && bun run src/aggregator/gitea-scanner.ts --verbose 2>&1 || echo "Scan completed with errors"
cd /app && bun run src/aggregator/gitea-scanner.ts --verbose 2>&1 || echo "Gitea scan completed with errors (non-fatal)"
# Start aggregator
echo "Starting aggregator..."

View File

@ -118,8 +118,20 @@ class GiteaScanner {
return contents.some((item) => item.name === "backlog" && item.type === "dir");
}
private getAuthCloneUrl(repo: GiteaRepo): string {
// Embed token in HTTPS URL for private repo access
if (this.config.giteaToken && repo.private) {
const url = new URL(repo.clone_url);
url.username = "git";
url.password = this.config.giteaToken;
return url.toString();
}
return repo.clone_url;
}
async cloneOrPullRepo(repo: GiteaRepo): Promise<boolean> {
const repoDir = join(this.config.outputDir, repo.name);
const cloneUrl = this.getAuthCloneUrl(repo);
try {
// Check if already cloned
@ -139,14 +151,9 @@ class GiteaScanner {
await $`cd ${repoDir} && git fetch origin && git reset --hard origin/${repo.default_branch} 2>&1`.quiet();
}
} else {
// Clone the repo
if (this.config.verbose) {
console.log(`Cloning ${repo.full_name}...`);
}
// Use SSH URL if we have an SSH key configured, otherwise HTTPS
const cloneUrl = this.config.sshKeyPath ? repo.ssh_url : repo.clone_url;
const result = await $`git clone --depth 1 ${cloneUrl} ${repoDir} 2>&1`.quiet();
if (result.exitCode !== 0) {
console.warn(`Failed to clone ${repo.full_name}: ${result.stderr}`);
@ -252,7 +259,7 @@ function parseArgs(): ScannerConfig {
const giteaUrl = getArg("gitea-url", "GITEA_URL", "https://gitea.jeffemmett.com");
const giteaToken = getArg("gitea-token", "GITEA_TOKEN");
const outputDir = getArg("output", "GITEA_OUTPUT_DIR", "/opt/gitea-repos");
const sshKeyPath = getArg("ssh-key", "GITEA_SSH_KEY", "/root/.ssh/gitea_ed25519");
const sshKeyPath = getArg("ssh-key", "GITEA_SSH_KEY");
const owner = getArg("owner", "GITEA_OWNER", "jeffemmett");
const concurrency = Number.parseInt(getArg("concurrency", "GITEA_CONCURRENCY", "5") || "5", 10);
const verbose = args.includes("--verbose") || args.includes("-v");