Add Gitea Actions workflows, CI config, and docs
Some checks failed
Docker / docker (push) Successful in 6s
Security / security (push) Successful in 6s
Deploy / deploy-local-runner (push) Has been cancelled
CI / ci (push) Successful in 1m42s
Deploy / deploy-ssh (push) Successful in 7s

This commit is contained in:
2026-02-28 20:40:14 +01:00
parent 3b48b39561
commit 8cadb2d216
35 changed files with 3216 additions and 0 deletions

121
.ci/config.env Normal file
View File

@@ -0,0 +1,121 @@
# =============================================================================
# .ci/config.env — Central Configuration (Single Source of Truth)
# =============================================================================
# Repository: ${REPO_NAME}
# Generated from template: ${TEMPLATE_NAME}
# Created: ${YEAR}-${MONTH}-${DAY}
#
# All Gitea Actions workflows source this file at runtime.
# Adjust the toggles below to enable/disable features for YOUR project.
# See docs/CONFIG.md for detailed explanations of every flag.
# =============================================================================
# -----------------------------------------------------------------------------
# CI — Continuous Integration
# -----------------------------------------------------------------------------
# Master switch for CI. If false, the CI workflow exits immediately.
ENABLE_CI=true
# If true, lint/test failures cause the workflow to fail (exit 1).
# If false, failures are logged as warnings but the workflow succeeds.
CI_STRICT=true
# The primary branch name. Used by Docker and other workflows.
DEFAULT_BRANCH=main
# -----------------------------------------------------------------------------
# Docker — Build & Push
# -----------------------------------------------------------------------------
# Master switch for Docker build steps. If false, docker.yml skips entirely.
ENABLE_DOCKER=true
# Whether to actually push images to the registry.
# SAFE DEFAULT: false — images are built but never pushed until you opt in.
DOCKER_PUSH=false
# Push images when a commit lands on DEFAULT_BRANCH?
DOCKER_PUSH_ON_BRANCH=true
# Push images when a semver tag (v*) is pushed?
DOCKER_PUSH_ON_TAG=true
# Registry hostname. For Gitea's built-in container registry this is usually
# the same as your Gitea instance domain.
REGISTRY_HOST=git.hiddenden.cafe
# Image owner (org or user). "auto" = derived at runtime from the repo context.
IMAGE_OWNER=auto
# Image name. "auto" = derived at runtime from the repository name.
IMAGE_NAME=auto
# Tag strategy. Controls which tags are applied to pushed images.
# Options:
# semver+latest — tag vX.Y.Z → :X.Y.Z and :latest (default)
# semver — tag vX.Y.Z → :X.Y.Z only
# branch — branch pushes only, tagged as :branchname
# Branch pushes always produce :branchname when DOCKER_PUSH_ON_BRANCH=true.
DOCKER_TAG_STRATEGY=semver+latest
# -----------------------------------------------------------------------------
# Security Scanning
# -----------------------------------------------------------------------------
# Master switch for security workflows. SAFE DEFAULT: false.
ENABLE_SECURITY=false
# If true, any finding fails the workflow. If false, findings are warnings only.
STRICT_SECURITY=false
# -----------------------------------------------------------------------------
# Renovate — Automated Dependency Updates
# -----------------------------------------------------------------------------
# Master switch for Renovate. SAFE DEFAULT: false.
ENABLE_RENOVATE=false
# How often Renovate runs. Cron-style or preset: daily, weekly, monthly.
RENOVATE_SCHEDULE=weekly
# Maximum number of open PRs Renovate can create at once.
RENOVATE_PR_LIMIT=5
# -----------------------------------------------------------------------------
# Deploy — Automated Deployment to VPS
# -----------------------------------------------------------------------------
# Master switch for deployment. SAFE DEFAULT: false.
# Deploy NEVER runs unless you explicitly set this to true.
ENABLE_DEPLOY=false
# Deployment mode. Determines HOW the deploy job reaches the VPS.
# local-runner — The job runs directly on a self-hosted act_runner installed
# on the VPS. No SSH needed. The runner is selected by label.
# ssh — The job runs on any runner and SSHs into the VPS to execute
# deploy commands remotely. Requires SSH secrets.
DEPLOY_MODE=local-runner
# Runner label for local-runner mode. The act_runner on your VPS must be
# registered with this label. Gitea selects the runner via runs-on.
# Examples: deploy-ovh, vps-prod, deploy-hetzner
DEPLOY_RUNNER_LABEL=deploy-ovh
# Working directory on the VPS where your project lives.
# For local-runner mode this is a local path; for ssh mode it's the remote path.
DEPLOY_WORKDIR=/opt/${REPO_NAME}
# Deploy strategy. Determines WHAT happens on the VPS.
# compose — cd into DEPLOY_WORKDIR, pull new images, recreate containers
# systemd — restart a systemd service
# script — run a custom deploy script
DEPLOY_STRATEGY=compose
# (compose) Path to the compose file, relative to DEPLOY_WORKDIR.
DEPLOY_COMPOSE_FILE=docker-compose.yml
# (systemd) Name of the systemd service to restart. Required if strategy=systemd.
DEPLOY_SYSTEMD_SERVICE=
# (script) Path to a custom deploy script, relative to the repo root.
# The script receives DEPLOY_WORKDIR as $1.
DEPLOY_SCRIPT=scripts/deploy.sh
# Also deploy when a semver tag (v*) is pushed? Default: false.
DEPLOY_ON_TAG=false

24
.editorconfig Normal file
View File

@@ -0,0 +1,24 @@
# EditorConfig — https://editorconfig.org
# Ensures consistent coding styles across editors and IDEs.
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.{yml,yaml}]
indent_size = 2
[*.{json,toml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

38
.gitattributes vendored Normal file
View File

@@ -0,0 +1,38 @@
# =============================================================================
# .gitattributes — Normalize line endings and mark binary files
# =============================================================================
# Default: auto-detect text files, normalize to LF on commit.
* text=auto eol=lf
# Explicitly declare text files
*.py text diff=python
*.js text
*.ts text
*.json text
*.yml text
*.yaml text
*.md text
*.txt text
*.cfg text
*.ini text
*.env text
*.sh text eol=lf
*.bash text eol=lf
Makefile text eol=lf
Dockerfile text eol=lf
# Explicitly declare binary files
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.gz binary
*.zip binary
*.tar binary
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary

View File

@@ -0,0 +1,59 @@
name: Authentication & Security
about: Report a security concern or suggest a security enhancement
title: "[Security] "
labels:
- security
body:
- type: markdown
attributes:
value: |
**For actual vulnerabilities, do NOT use this template.**
Email security@hiddenden.cafe instead. See SECURITY.md.
- type: textarea
id: summary
attributes:
label: Summary
description: Summary of the authentication / security concern or enhancement.
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: Detailed description (vulnerability, misconfiguration, missing control).
validations:
required: true
- type: textarea
id: affected
attributes:
label: Affected Components
description: List services, endpoints, or libraries affected.
- type: textarea
id: reproduction
attributes:
label: Reproduction / PoC
description: Steps to reproduce. Do NOT include exploit payloads in public issues.
- type: textarea
id: remediation
attributes:
label: Suggested Remediation
description: Concrete steps to fix, including links to standards or CVE info.
- type: textarea
id: references
attributes:
label: References
description: Links to auth standards (OAuth2, OIDC), encryption recommendations, or policy.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: Confirmed requirement with security lead
- label: Unit/integration tests planned for fix

View File

@@ -0,0 +1,62 @@
name: Bug Report
about: Report a bug or unexpected behavior
title: "[Bug] "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for reporting a bug! Please fill out the sections below.
- type: textarea
id: description
attributes:
label: Description
description: A clear and concise description of the bug.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: How can we reproduce this issue?
placeholder: |
1. Go to '...'
2. Click on '...'
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened?
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: OS, browser, version, etc.
placeholder: |
- OS: Ubuntu 22.04
- Python: 3.11
- Version: v1.2.3
- type: textarea
id: logs
attributes:
label: Logs / Screenshots
description: Paste any relevant logs or screenshots.

View File

@@ -0,0 +1,47 @@
name: Collaboration Tools
about: Request or suggest collaboration tools and integrations
title: "[Collab] "
labels:
- tooling
body:
- type: textarea
id: summary
attributes:
label: Summary
description: What tool or integration are you suggesting?
validations:
required: true
- type: textarea
id: current
attributes:
label: Current Tools
description: What tools are currently used and what are the pain points?
- type: textarea
id: suggested
attributes:
label: Suggested Tools / Integrations
description: List candidate tools and why they would help.
validations:
required: true
- type: textarea
id: criteria
attributes:
label: Evaluation Criteria
description: "How to evaluate: cost, security, ease of use, integration with repo/CI."
- type: textarea
id: pilot
attributes:
label: Pilot Plan
description: Suggested pilot scope (team, duration, KPIs).
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: Stakeholder signoff obtained
- label: Pilot resources allocated

View File

@@ -0,0 +1,12 @@
# Issue template chooser configuration
# See: https://docs.gitea.com/usage/issue-pull-request-templates
blank_issues_enabled: true
contact_links:
- name: Security Vulnerability
url: https://git.hiddenden.cafe
about: >
DO NOT open a public issue for security vulnerabilities.
Please email security@hiddenden.cafe or see SECURITY.md for
responsible disclosure instructions.

View File

@@ -0,0 +1,47 @@
name: Development Workflow Improvement
about: Suggest improvements to CI, branching, release process, or dev workflow
title: "[Workflow] "
labels:
- workflow
body:
- type: textarea
id: summary
attributes:
label: Summary
description: Short summary of the workflow improvement.
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem
description: Current pain points and inefficiencies in the dev workflow.
validations:
required: true
- type: textarea
id: proposed
attributes:
label: Proposed Change
description: Describe exact changes (branch naming, CI steps, commit message format, etc.).
validations:
required: true
- type: textarea
id: migration
attributes:
label: Migration Plan
description: How to migrate existing repos/teams, training, or automation required.
- type: textarea
id: risks
attributes:
label: Risks
description: Potential issues and mitigations.
- type: textarea
id: acceptance
attributes:
label: Acceptance Criteria
description: How we'll know the workflow is improved (metrics or qualitative feedback).

View File

@@ -0,0 +1,38 @@
name: Feature Request
about: Suggest a new feature or improvement
title: "[Feature] "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Have an idea? We'd love to hear it!
- type: textarea
id: problem
attributes:
label: Problem / Motivation
description: What problem does this feature solve?
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the feature or change you'd like.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any alternative solutions or workarounds you've considered?
- type: textarea
id: context
attributes:
label: Additional Context
description: Screenshots, mockups, links, or anything else.

View File

@@ -0,0 +1,61 @@
name: Performance Improvement
about: Report a performance problem or request an optimization
title: "[Performance] "
labels:
- performance
body:
- type: textarea
id: summary
attributes:
label: Summary
description: Short summary of the performance problem or optimization.
validations:
required: true
- type: textarea
id: observed
attributes:
label: Observed Behavior
description: What is currently happening? Include metrics, traces, screenshots.
validations:
required: true
- type: textarea
id: target
attributes:
label: Expected Behavior / Target
description: "What improvement do we expect? e.g. latency < 200ms, memory < X MB."
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction / Benchmark
description: How to reproduce or reference a benchmark (commands, dataset, environment).
- type: textarea
id: optimization
attributes:
label: Proposed Optimization
description: High-level plan or hypothesis for fix/optimization.
- type: textarea
id: measurement
attributes:
label: Measurement Plan
description: "How to measure before/after: specific metrics, dashboards."
- type: textarea
id: risks
attributes:
label: Risks & Rollback
description: Potential risks and fallback plan.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: Baseline metrics captured
- label: Benchmark scripts included or linked

View File

@@ -0,0 +1,68 @@
name: Project Requirement
about: Document a project requirement with stakeholders and acceptance criteria
title: "[Requirement] "
labels:
- requirement
body:
- type: textarea
id: overview
attributes:
label: Overview
description: High-level description of the requirement.
validations:
required: true
- type: textarea
id: stakeholders
attributes:
label: Stakeholders
description: "List stakeholders: product owner, engineers, designers."
placeholder: |
- Product owner: @username
- Engineers: @user1, @user2
- Designers: @user3
- type: textarea
id: goals
attributes:
label: Business Goals
description: What this requirement enables for the business or users.
validations:
required: true
- type: textarea
id: functional
attributes:
label: Functional Requirements
description: List the functional requirements.
placeholder: |
- Req 1: Description
- Req 2: Description
validations:
required: true
- type: textarea
id: nonfunctional
attributes:
label: Non-Functional Requirements
description: Performance targets, security/compliance constraints, accessibility.
- type: textarea
id: success
attributes:
label: Success Criteria
description: How to measure success.
- type: textarea
id: references
attributes:
label: Notes & References
description: Links to documents, mockups, legal requirements, etc.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: Stakeholders reviewed
- label: Acceptance criteria documented

View File

@@ -0,0 +1,41 @@
<!-- Title: Proposal / Design RFC -->
# Proposal: [Short title]
## Summary
Concise description of the proposal and what outcome you want.
## Problem statement
What problem are we solving and why is it important? Include links to related issues.
## Goals
- Primary goals (what success looks like)
- Non-goals (explicitly out of scope)
## Proposed design
Describe the design in detail. Include:
- Architecture diagrams or ASCII art
- API changes (requests/responses)
- Data model changes or migrations
- UX flows or wireframes
## Alternatives considered
Short list of alternatives and tradeoffs.
## Backwards compatibility & migration plan
Describe how to migrate existing data and any compatibility impacts.
## Security considerations
List potential security/privacy implications.
## Testing & rollout plan
How will this be tested? Phased rollout plan if needed.
## Implementation plan & timeline
High-level tasks and owners.
## Open questions
List any unresolved questions.
**Checklist**
- [ ] Linked related issues
- [ ] Prototype or PoC (if available)

View File

@@ -0,0 +1,30 @@
name: Question / Support
about: Ask a question or get help
title: "[Question] "
labels:
- question
body:
- type: markdown
attributes:
value: |
Need help? Ask away! For security issues, please see SECURITY.md instead.
- type: textarea
id: question
attributes:
label: Your Question
description: What do you need help with?
validations:
required: true
- type: textarea
id: context
attributes:
label: Context
description: What have you tried? What documentation have you read?
- type: textarea
id: environment
attributes:
label: Environment (if relevant)
description: OS, version, setup details.

View File

@@ -0,0 +1,45 @@
name: Repository Management
about: Request repo maintenance (archiving, renaming, structure changes)
title: "[Repo] "
labels:
- repo-management
body:
- type: textarea
id: summary
attributes:
label: Summary
description: What repo maintenance or management change is needed?
validations:
required: true
- type: textarea
id: current
attributes:
label: Current State
description: Describe current repo structure, branches, tags, etc.
- type: textarea
id: change
attributes:
label: Requested Change
description: What you want changed and why.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps
description: Step-by-step plan to complete the change (who will run each step).
- type: textarea
id: impact
attributes:
label: Dependencies & Impact
description: Other repos, CI pipelines, or services that will be affected.
- type: textarea
id: rollback
attributes:
label: Rollback Plan
description: How to undo if something goes wrong.

View File

@@ -0,0 +1,51 @@
name: Task / To-Do
about: Track a task with subtasks, owner, and effort estimate
title: "[Task] "
labels:
- task
body:
- type: textarea
id: description
attributes:
label: Description
description: Detailed description of the task and expected outcome.
validations:
required: true
- type: input
id: assignee
attributes:
label: Owner
description: Who is responsible for this task?
placeholder: "@username"
- type: textarea
id: subtasks
attributes:
label: Subtasks
description: Break down the work into subtasks.
placeholder: |
- [ ] Subtask 1 — description
- [ ] Subtask 2 — description
- [ ] Subtask 3 — description
validations:
required: true
- type: input
id: effort
attributes:
label: Estimated Effort
description: "Time estimate, e.g. 2d, 4h."
placeholder: "e.g. 4h, 2d"
- type: textarea
id: dependencies
attributes:
label: Dependencies
description: List any blocking issues or PRs.
- type: textarea
id: acceptance
attributes:
label: Acceptance Criteria
description: What needs to be true for this task to be considered done.

View File

@@ -0,0 +1,61 @@
name: User Experience (UX)
about: Report a UX problem or suggest a UX improvement
title: "[UX] "
labels:
- ux
body:
- type: textarea
id: summary
attributes:
label: Summary
description: One-line summary of the UX issue or suggestion.
validations:
required: true
- type: textarea
id: problem
attributes:
label: Describe the Problem
description: Where in the product does this occur? Who is affected?
validations:
required: true
- type: textarea
id: impact
attributes:
label: User Impact
description: Explain the user pain, frequency, and severity.
- type: textarea
id: current
attributes:
label: Current Behavior
description: What a user sees now (include screenshots or steps).
- type: textarea
id: proposed
attributes:
label: Proposed Change
description: Detailed description of the UX change or recommendation.
validations:
required: true
- type: textarea
id: mockups
attributes:
label: Mockups / Prototypes
description: Attach or link to designs.
- type: textarea
id: accessibility
attributes:
label: Accessibility Considerations
description: Color contrast, keyboard navigation, screen reader notes.
- type: checkboxes
id: checklist
attributes:
label: Acceptance Criteria
options:
- label: Usability tested with users
- label: Accessibility checks passed

24
.gitea/template Normal file
View File

@@ -0,0 +1,24 @@
# Gitea Template Variable Expansion
# ----------------------------------
# When a new repo is created from this template, Gitea will replace
# template variables (e.g. ${REPO_NAME}) in files matching the globs below.
#
# Supported variables (Gitea ≥ 1.20):
# REPO_NAME name of the new repository
# REPO_DESCRIPTION description entered during creation
# TEMPLATE_NAME name of this template repository
# YEAR current four-digit year
# MONTH current zero-padded month (01-12)
# DAY current zero-padded day (01-31)
# MONTH_ENGLISH current month in English (January, February, …)
#
# Glob patterns — one per line. Files matching these will have variables expanded.
README.md
docs/*.md
.ci/config.env
SECURITY.md
CONTRIBUTING.md
CODE_OF_CONDUCT.md
LICENSE
pull_request_template.md

267
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,267 @@
# =============================================================================
# CI Workflow — Continuous Integration
# =============================================================================
# Triggers on push and pull_request.
#
# Detection logic:
# 1. Python: if requirements.txt exists → install deps, lint, test.
# 2. Node/JS: if package.json exists → npm ci, lint, test, build.
# 3. Neither detected → print a message and exit 0 (never fail).
#
# Controlled by .ci/config.env:
# ENABLE_CI — master switch (default: true)
# CI_STRICT — if true, lint/test failures fail the workflow
# if false, failures are warnings only
#
# See docs/CI.md for full details.
# =============================================================================
name: CI
on:
push:
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
# -----------------------------------------------------------------------
# Step 1: Checkout the code
# -----------------------------------------------------------------------
- name: Checkout
uses: actions/checkout@v4
# -----------------------------------------------------------------------
# Step 2: Load configuration from .ci/config.env
# -----------------------------------------------------------------------
- name: Load config
id: config
run: |
# Source config.env to get ENABLE_CI, CI_STRICT, etc.
if [ -f .ci/config.env ]; then
# Export all non-comment, non-empty lines
set -a
source .ci/config.env
set +a
echo "Config loaded from .ci/config.env"
else
echo "WARNING: .ci/config.env not found, using defaults"
ENABLE_CI=true
CI_STRICT=true
fi
# Pass values to subsequent steps via environment file
echo "ENABLE_CI=${ENABLE_CI:-true}" >> "$GITHUB_ENV"
echo "CI_STRICT=${CI_STRICT:-true}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Check master switch
# -----------------------------------------------------------------------
- name: Check if CI is enabled
run: |
if [ "$ENABLE_CI" != "true" ]; then
echo "CI is disabled (ENABLE_CI=$ENABLE_CI). Exiting."
exit 0
fi
# -----------------------------------------------------------------------
# Step 4: Detect project type
# -----------------------------------------------------------------------
- name: Detect project type
id: detect
run: |
HAS_PYTHON=false
HAS_NODE=false
if [ -f requirements.txt ] || [ -f setup.py ] || [ -f pyproject.toml ]; then
HAS_PYTHON=true
echo "Detected: Python project"
fi
if [ -f package.json ]; then
HAS_NODE=true
echo "Detected: Node.js project"
fi
if [ "$HAS_PYTHON" = "false" ] && [ "$HAS_NODE" = "false" ]; then
echo "No Python or Node.js project detected. CI will skip gracefully."
fi
echo "HAS_PYTHON=${HAS_PYTHON}" >> "$GITHUB_ENV"
echo "HAS_NODE=${HAS_NODE}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 5: Python — Setup & Install
# -----------------------------------------------------------------------
- name: Set up Python
if: env.HAS_PYTHON == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install Python dependencies
if: env.HAS_PYTHON == 'true'
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
# Install optional dev tools (won't fail if not needed)
pip install ruff black flake8 pytest 2>/dev/null || true
# -----------------------------------------------------------------------
# Step 6: Python — Lint
# Runs ruff, then black --check, then flake8. Each is optional:
# only runs if the tool is installed AND relevant config exists.
# -----------------------------------------------------------------------
- name: Python lint
if: env.HAS_PYTHON == 'true'
run: |
EXIT_CODE=0
# --- ruff ---
if command -v ruff >/dev/null 2>&1; then
echo ">>> ruff check ."
ruff check . || EXIT_CODE=$?
else
echo "SKIP: ruff not installed"
fi
# --- black ---
if command -v black >/dev/null 2>&1; then
echo ">>> black --check ."
black --check . || EXIT_CODE=$?
else
echo "SKIP: black not installed"
fi
# --- flake8 ---
if command -v flake8 >/dev/null 2>&1; then
echo ">>> flake8 ."
flake8 . || EXIT_CODE=$?
else
echo "SKIP: flake8 not installed"
fi
if [ "$EXIT_CODE" -ne 0 ]; then
if [ "$CI_STRICT" = "true" ]; then
echo "ERROR: Lint failed (CI_STRICT=true)"
exit 1
else
echo "WARNING: Lint issues found (CI_STRICT=false, continuing)"
fi
fi
# -----------------------------------------------------------------------
# Step 7: Python — Test
# Runs pytest if a tests/ directory or pytest config is detected.
# -----------------------------------------------------------------------
- name: Python test
if: env.HAS_PYTHON == 'true'
run: |
# Check if tests exist
HAS_TESTS=false
if [ -d tests ] || [ -d test ]; then
HAS_TESTS=true
fi
# Check for pytest config in pyproject.toml or pytest.ini
if [ -f pytest.ini ] || [ -f setup.cfg ]; then
HAS_TESTS=true
fi
if [ -f pyproject.toml ] && grep -q '\[tool\.pytest' pyproject.toml 2>/dev/null; then
HAS_TESTS=true
fi
if [ "$HAS_TESTS" = "true" ]; then
echo ">>> pytest"
if ! pytest; then
if [ "$CI_STRICT" = "true" ]; then
echo "ERROR: Tests failed (CI_STRICT=true)"
exit 1
else
echo "WARNING: Tests failed (CI_STRICT=false, continuing)"
fi
fi
else
echo "SKIP: No tests detected (no tests/ dir or pytest config)"
fi
# -----------------------------------------------------------------------
# Step 8: Node.js — Setup & Install
# -----------------------------------------------------------------------
- name: Set up Node.js
if: env.HAS_NODE == 'true'
uses: actions/setup-node@v4
with:
node-version: "lts/*"
- name: Install Node dependencies
if: env.HAS_NODE == 'true'
run: npm ci
# -----------------------------------------------------------------------
# Step 9: Node.js — Lint (only if "lint" script exists in package.json)
# -----------------------------------------------------------------------
- name: Node lint
if: env.HAS_NODE == 'true'
run: |
if grep -q '"lint"' package.json 2>/dev/null; then
echo ">>> npm run lint"
if ! npm run lint; then
if [ "$CI_STRICT" = "true" ]; then
echo "ERROR: Lint failed (CI_STRICT=true)"
exit 1
else
echo "WARNING: Lint issues found (CI_STRICT=false, continuing)"
fi
fi
else
echo "SKIP: no 'lint' script in package.json"
fi
# -----------------------------------------------------------------------
# Step 10: Node.js — Test (only if "test" script exists)
# -----------------------------------------------------------------------
- name: Node test
if: env.HAS_NODE == 'true'
run: |
if grep -q '"test"' package.json 2>/dev/null; then
echo ">>> npm test"
if ! npm test; then
if [ "$CI_STRICT" = "true" ]; then
echo "ERROR: Tests failed (CI_STRICT=true)"
exit 1
else
echo "WARNING: Tests failed (CI_STRICT=false, continuing)"
fi
fi
else
echo "SKIP: no 'test' script in package.json"
fi
# -----------------------------------------------------------------------
# Step 11: Node.js — Build (only if "build" script exists)
# -----------------------------------------------------------------------
- name: Node build
if: env.HAS_NODE == 'true'
run: |
if grep -q '"build"' package.json 2>/dev/null; then
echo ">>> npm run build"
npm run build
else
echo "SKIP: no 'build' script in package.json"
fi
# -----------------------------------------------------------------------
# Step 12: Summary
# -----------------------------------------------------------------------
- name: CI Summary
if: always()
run: |
echo "=============================="
echo " CI Complete"
echo " Python detected: $HAS_PYTHON"
echo " Node detected: $HAS_NODE"
echo " Strict mode: $CI_STRICT"
echo "=============================="

451
.gitea/workflows/deploy.yml Normal file
View File

@@ -0,0 +1,451 @@
# =============================================================================
# Deploy Workflow — Automated Deployment to VPS
# =============================================================================
#
# PURPOSE:
# Deploy your application to a VPS after a successful push to the default
# branch (main). Supports two deployment modes:
#
# (A) local-runner — The deploy job runs directly on a self-hosted act_runner
# installed ON the VPS. No SSH needed. The runner is selected by a label
# (DEPLOY_RUNNER_LABEL). This is the recommended mode.
#
# (B) ssh — The deploy job runs on any runner and SSHs into the VPS to
# execute commands remotely. Requires SSH secrets. Use as fallback when
# you can't install a runner on the target VPS.
#
# SAFE BY DEFAULT:
# ENABLE_DEPLOY=false in .ci/config.env. Deploy never runs unless you
# explicitly enable it. It also never runs on pull_request events.
#
# DEPLOY STRATEGIES:
# compose — docker compose pull && docker compose up -d
# systemd — systemctl restart <service>
# script — run a custom deploy script
#
# TRIGGERS:
# - push to DEFAULT_BRANCH (main) → deploy if enabled
# - tag v* (only if DEPLOY_ON_TAG=true) → deploy if enabled
# - pull_request → NEVER (not in trigger list)
#
# REQUIRED SECRETS (ssh mode only):
# DEPLOY_SSH_KEY — private SSH key (ed25519 or RSA)
# DEPLOY_HOST — VPS hostname or IP
# DEPLOY_USER — SSH username on VPS
# DEPLOY_KNOWN_HOSTS — (optional) known_hosts entry for the VPS
#
# For local-runner mode: NO secrets needed. The runner already has local
# access. Just ensure the runner is registered with the correct label.
#
# See docs/DEPLOY.md for full setup instructions.
# =============================================================================
name: Deploy
# ---------------------------------------------------------------------------
# TRIGGERS
# ---------------------------------------------------------------------------
# Only push events — never pull_request.
# Branch filter is further enforced in the "branch guard" step below,
# because config.env may specify a different DEFAULT_BRANCH.
on:
push:
branches:
- main
tags:
- "v*"
# =============================================================================
# JOB: deploy-local-runner
# =============================================================================
# Runs directly on the VPS via a labeled self-hosted act_runner.
# This job is skipped if DEPLOY_MODE != local-runner.
# ---------------------------------------------------------------------------
jobs:
deploy-local-runner:
# -------------------------------------------------------------------------
# Runner selection:
# The 'runs-on' value is read from config at workflow parse time, but
# Gitea Actions does not support dynamic runs-on from env vars.
# We use the default label here; if you changed DEPLOY_RUNNER_LABEL in
# config.env, you MUST also update this runs-on value to match.
#
# HOW TO CHANGE: replace 'deploy-ovh' below with your label.
# -------------------------------------------------------------------------
runs-on: deploy-ovh
steps:
# -----------------------------------------------------------------------
# Step 1: Load configuration
# -----------------------------------------------------------------------
- name: Checkout (for config only)
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Load config
id: config
run: |
# Source config.env
if [ -f .ci/config.env ]; then
set -a
source .ci/config.env
set +a
echo "Config loaded."
else
echo "WARNING: .ci/config.env not found, using defaults."
fi
# Export all deploy-related vars with safe defaults
echo "ENABLE_DEPLOY=${ENABLE_DEPLOY:-false}" >> "$GITHUB_ENV"
echo "DEPLOY_MODE=${DEPLOY_MODE:-local-runner}" >> "$GITHUB_ENV"
echo "DEPLOY_WORKDIR=${DEPLOY_WORKDIR:-/opt/app}" >> "$GITHUB_ENV"
echo "DEPLOY_STRATEGY=${DEPLOY_STRATEGY:-compose}" >> "$GITHUB_ENV"
echo "DEPLOY_COMPOSE_FILE=${DEPLOY_COMPOSE_FILE:-docker-compose.yml}" >> "$GITHUB_ENV"
echo "DEPLOY_SYSTEMD_SERVICE=${DEPLOY_SYSTEMD_SERVICE:-}" >> "$GITHUB_ENV"
echo "DEPLOY_SCRIPT=${DEPLOY_SCRIPT:-scripts/deploy.sh}" >> "$GITHUB_ENV"
echo "DEPLOY_ON_TAG=${DEPLOY_ON_TAG:-false}" >> "$GITHUB_ENV"
echo "DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 2: Gate checks — abort early if deploy should not run
# -----------------------------------------------------------------------
- name: Check if deploy is enabled
run: |
if [ "$ENABLE_DEPLOY" != "true" ]; then
echo "========================================="
echo " Deploy is DISABLED (ENABLE_DEPLOY=$ENABLE_DEPLOY)"
echo " To enable: set ENABLE_DEPLOY=true in .ci/config.env"
echo "========================================="
exit 0
fi
# Ensure this job is for the correct mode
if [ "$DEPLOY_MODE" != "local-runner" ]; then
echo "DEPLOY_MODE=$DEPLOY_MODE (not local-runner). This job is a no-op."
echo "The ssh job will handle deployment instead."
exit 0
fi
echo "DEPLOY_ACTIVE=true" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Branch guard
# Only deploy from DEFAULT_BRANCH. For tags, check DEPLOY_ON_TAG.
# This is a SAFETY net — even though 'on.push.branches' is set above,
# DEFAULT_BRANCH might differ from 'main'.
# -----------------------------------------------------------------------
- name: Branch guard
if: env.DEPLOY_ACTIVE == 'true'
run: |
REF="${GITHUB_REF:-}"
# Tag push?
if echo "$REF" | grep -q '^refs/tags/v'; then
if [ "$DEPLOY_ON_TAG" != "true" ]; then
echo "Tag push detected but DEPLOY_ON_TAG=$DEPLOY_ON_TAG. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on tag: $REF"
exit 0
fi
# Branch push — verify it's DEFAULT_BRANCH
BRANCH="$(echo "$REF" | sed 's|refs/heads/||')"
if [ "$BRANCH" != "$DEFAULT_BRANCH" ]; then
echo "Branch '$BRANCH' is not DEFAULT_BRANCH '$DEFAULT_BRANCH'. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on branch: $BRANCH"
# -----------------------------------------------------------------------
# Step 4: Execute deploy strategy (LOCAL — runs on the VPS itself)
# -----------------------------------------------------------------------
- name: "Deploy: compose"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'compose'
run: |
echo ">>> Deploy strategy: compose"
echo ">>> Working directory: $DEPLOY_WORKDIR"
echo ">>> Compose file: $DEPLOY_COMPOSE_FILE"
cd "$DEPLOY_WORKDIR" || { echo "ERROR: Cannot cd to $DEPLOY_WORKDIR"; exit 1; }
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE pull"
docker compose -f "$DEPLOY_COMPOSE_FILE" pull
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE up -d"
docker compose -f "$DEPLOY_COMPOSE_FILE" up -d
echo "Deploy (compose) complete."
- name: "Deploy: systemd"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'systemd'
run: |
echo ">>> Deploy strategy: systemd"
if [ -z "$DEPLOY_SYSTEMD_SERVICE" ]; then
echo "ERROR: DEPLOY_SYSTEMD_SERVICE is not set."
echo "Set it in .ci/config.env for strategy=systemd."
exit 1
fi
echo ">>> systemctl restart $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl restart "$DEPLOY_SYSTEMD_SERVICE"
echo ">>> systemctl status $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl status "$DEPLOY_SYSTEMD_SERVICE" --no-pager
echo "Deploy (systemd) complete."
- name: "Deploy: script"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'script'
run: |
echo ">>> Deploy strategy: script"
echo ">>> Script: $DEPLOY_SCRIPT"
echo ">>> Workdir arg: $DEPLOY_WORKDIR"
if [ ! -f "$DEPLOY_SCRIPT" ]; then
echo "ERROR: Deploy script not found: $DEPLOY_SCRIPT"
exit 1
fi
chmod +x "$DEPLOY_SCRIPT"
"./$DEPLOY_SCRIPT" "$DEPLOY_WORKDIR"
echo "Deploy (script) complete."
# -----------------------------------------------------------------------
# Step 5: Summary
# -----------------------------------------------------------------------
- name: Deploy summary
if: always()
run: |
echo "=============================="
echo " Deploy (local-runner)"
echo " Enabled: ${ENABLE_DEPLOY:-false}"
echo " Mode: ${DEPLOY_MODE:-local-runner}"
echo " Strategy: ${DEPLOY_STRATEGY:-compose}"
echo " Workdir: ${DEPLOY_WORKDIR:-/opt/app}"
echo " Active: ${DEPLOY_ACTIVE:-false}"
echo "=============================="
# ===========================================================================
# JOB: deploy-ssh
# ===========================================================================
# Runs on a normal runner and SSHs into the VPS to deploy.
# This job is skipped if DEPLOY_MODE != ssh.
# ---------------------------------------------------------------------------
deploy-ssh:
runs-on: ubuntu-latest
steps:
# -----------------------------------------------------------------------
# Step 1: Load configuration
# -----------------------------------------------------------------------
- name: Checkout (for config + scripts)
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Load config
run: |
if [ -f .ci/config.env ]; then
set -a
source .ci/config.env
set +a
echo "Config loaded."
else
echo "WARNING: .ci/config.env not found, using defaults."
fi
echo "ENABLE_DEPLOY=${ENABLE_DEPLOY:-false}" >> "$GITHUB_ENV"
echo "DEPLOY_MODE=${DEPLOY_MODE:-local-runner}" >> "$GITHUB_ENV"
echo "DEPLOY_WORKDIR=${DEPLOY_WORKDIR:-/opt/app}" >> "$GITHUB_ENV"
echo "DEPLOY_STRATEGY=${DEPLOY_STRATEGY:-compose}" >> "$GITHUB_ENV"
echo "DEPLOY_COMPOSE_FILE=${DEPLOY_COMPOSE_FILE:-docker-compose.yml}" >> "$GITHUB_ENV"
echo "DEPLOY_SYSTEMD_SERVICE=${DEPLOY_SYSTEMD_SERVICE:-}" >> "$GITHUB_ENV"
echo "DEPLOY_SCRIPT=${DEPLOY_SCRIPT:-scripts/deploy.sh}" >> "$GITHUB_ENV"
echo "DEPLOY_ON_TAG=${DEPLOY_ON_TAG:-false}" >> "$GITHUB_ENV"
echo "DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 2: Gate checks
# -----------------------------------------------------------------------
- name: Check if deploy is enabled
run: |
if [ "$ENABLE_DEPLOY" != "true" ]; then
echo "========================================="
echo " Deploy is DISABLED (ENABLE_DEPLOY=$ENABLE_DEPLOY)"
echo " To enable: set ENABLE_DEPLOY=true in .ci/config.env"
echo "========================================="
exit 0
fi
if [ "$DEPLOY_MODE" != "ssh" ]; then
echo "DEPLOY_MODE=$DEPLOY_MODE (not ssh). This job is a no-op."
echo "The local-runner job will handle deployment instead."
exit 0
fi
echo "DEPLOY_ACTIVE=true" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Branch guard (same logic as local-runner)
# -----------------------------------------------------------------------
- name: Branch guard
if: env.DEPLOY_ACTIVE == 'true'
run: |
REF="${GITHUB_REF:-}"
if echo "$REF" | grep -q '^refs/tags/v'; then
if [ "$DEPLOY_ON_TAG" != "true" ]; then
echo "Tag push detected but DEPLOY_ON_TAG=$DEPLOY_ON_TAG. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on tag: $REF"
exit 0
fi
BRANCH="$(echo "$REF" | sed 's|refs/heads/||')"
if [ "$BRANCH" != "$DEFAULT_BRANCH" ]; then
echo "Branch '$BRANCH' is not DEFAULT_BRANCH '$DEFAULT_BRANCH'. Skipping."
echo "DEPLOY_ACTIVE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "Deploying on branch: $BRANCH"
# -----------------------------------------------------------------------
# Step 4: Set up SSH
#
# Secrets required:
# DEPLOY_SSH_KEY — private key (ed25519 recommended)
# DEPLOY_HOST — VPS IP or hostname
# DEPLOY_USER — SSH username
# DEPLOY_KNOWN_HOSTS — (optional) output of ssh-keyscan for the host
#
# If DEPLOY_KNOWN_HOSTS is not set, StrictHostKeyChecking is disabled.
# This is less secure but avoids first-connect failures in CI.
# For production, always set DEPLOY_KNOWN_HOSTS.
# -----------------------------------------------------------------------
- name: Set up SSH
if: env.DEPLOY_ACTIVE == 'true'
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Write private key (never echo it)
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
# Known hosts — if provided, use it; otherwise disable strict checking
KNOWN_HOSTS="${{ secrets.DEPLOY_KNOWN_HOSTS }}"
if [ -n "$KNOWN_HOSTS" ]; then
echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
echo "known_hosts configured from secret."
else
echo "WARNING: DEPLOY_KNOWN_HOSTS not set. Disabling StrictHostKeyChecking."
echo "For production, set DEPLOY_KNOWN_HOSTS (run: ssh-keyscan your-host)"
{
echo "Host *"
echo " StrictHostKeyChecking no"
echo " UserKnownHostsFile /dev/null"
} > ~/.ssh/config
chmod 600 ~/.ssh/config
fi
# Build SSH command for reuse
SSH_CMD="ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}"
echo "SSH_CMD=${SSH_CMD}" >> "$GITHUB_ENV"
# Verify connectivity
echo "Testing SSH connection..."
$SSH_CMD "echo 'SSH connection successful.'" || {
echo "ERROR: SSH connection failed."
exit 1
}
# -----------------------------------------------------------------------
# Step 5: Execute deploy strategy (REMOTE — via SSH)
# -----------------------------------------------------------------------
- name: "Deploy via SSH: compose"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'compose'
run: |
echo ">>> Deploy strategy: compose (via SSH)"
$SSH_CMD << DEPLOY_EOF
set -e
echo ">>> cd $DEPLOY_WORKDIR"
cd "$DEPLOY_WORKDIR" || { echo "ERROR: Cannot cd to $DEPLOY_WORKDIR"; exit 1; }
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE pull"
docker compose -f "$DEPLOY_COMPOSE_FILE" pull
echo ">>> docker compose -f $DEPLOY_COMPOSE_FILE up -d"
docker compose -f "$DEPLOY_COMPOSE_FILE" up -d
echo "Deploy (compose) complete."
DEPLOY_EOF
- name: "Deploy via SSH: systemd"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'systemd'
run: |
echo ">>> Deploy strategy: systemd (via SSH)"
if [ -z "$DEPLOY_SYSTEMD_SERVICE" ]; then
echo "ERROR: DEPLOY_SYSTEMD_SERVICE is not set."
exit 1
fi
$SSH_CMD << DEPLOY_EOF
set -e
echo ">>> sudo systemctl restart $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl restart "$DEPLOY_SYSTEMD_SERVICE"
echo ">>> systemctl status $DEPLOY_SYSTEMD_SERVICE"
sudo systemctl status "$DEPLOY_SYSTEMD_SERVICE" --no-pager
echo "Deploy (systemd) complete."
DEPLOY_EOF
- name: "Deploy via SSH: script"
if: env.DEPLOY_ACTIVE == 'true' && env.DEPLOY_STRATEGY == 'script'
run: |
echo ">>> Deploy strategy: script (via SSH)"
# Copy the deploy script to the VPS
scp -i ~/.ssh/deploy_key \
"$DEPLOY_SCRIPT" \
"${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/_deploy_script.sh"
$SSH_CMD << DEPLOY_EOF
set -e
chmod +x /tmp/_deploy_script.sh
/tmp/_deploy_script.sh "$DEPLOY_WORKDIR"
rm -f /tmp/_deploy_script.sh
echo "Deploy (script) complete."
DEPLOY_EOF
# -----------------------------------------------------------------------
# Step 6: Clean up SSH key (always runs)
# -----------------------------------------------------------------------
- name: Clean up SSH
if: always()
run: |
rm -rf ~/.ssh/deploy_key ~/.ssh/config 2>/dev/null || true
# -----------------------------------------------------------------------
# Step 7: Summary
# -----------------------------------------------------------------------
- name: Deploy summary
if: always()
run: |
echo "=============================="
echo " Deploy (ssh)"
echo " Enabled: ${ENABLE_DEPLOY:-false}"
echo " Mode: ${DEPLOY_MODE:-ssh}"
echo " Strategy: ${DEPLOY_STRATEGY:-compose}"
echo " Workdir: ${DEPLOY_WORKDIR:-/opt/app}"
echo " Active: ${DEPLOY_ACTIVE:-false}"
echo "=============================="

284
.gitea/workflows/docker.yml Normal file
View File

@@ -0,0 +1,284 @@
# =============================================================================
# Docker Workflow — Build & Push to Gitea Container Registry
# =============================================================================
#
# PURPOSE:
# Build Docker images generically; optionally push to the Gitea Container
# Registry at git.hiddenden.cafe.
#
# TRIGGERS:
# - pull_request → build only (never push)
# - push to main → build; push only if ENABLE_DOCKER=true AND DOCKER_PUSH=true
# - tag v* → build; push only if DOCKER_PUSH=true AND DOCKER_PUSH_ON_TAG=true
#
# DETECTION:
# - Dockerfile exists → docker build
# - docker-compose.yml exists → docker compose build
# - Neither → exit 0 gracefully
#
# NAMING (Gitea convention):
# Image ref: ${REGISTRY_HOST}/${IMAGE_OWNER}/${IMAGE_NAME}:${TAG}
# Example: git.hiddenden.cafe/myorg/myrepo:1.2.3
#
# AUTHENTICATION:
# Uses PAT-based secrets (recommended for Gitea Actions):
# - REGISTRY_USERNAME — your Gitea username or bot account
# - REGISTRY_TOKEN — a Personal Access Token with package:write scope
# Set these in: Repository Settings → Secrets (or Organization Secrets).
# NEVER echo secrets in logs.
#
# CONFIG:
# All settings loaded from .ci/config.env. See docs/DOCKER.md.
#
# =============================================================================
name: Docker
on:
push:
branches:
- main
tags:
- "v*"
pull_request:
jobs:
docker:
runs-on: ubuntu-latest
steps:
# -----------------------------------------------------------------------
# Step 1: Checkout
# -----------------------------------------------------------------------
- name: Checkout
uses: actions/checkout@v4
# -----------------------------------------------------------------------
# Step 2: Load configuration
# -----------------------------------------------------------------------
- name: Load config
run: |
if [ -f .ci/config.env ]; then
set -a
source .ci/config.env
set +a
echo "Config loaded from .ci/config.env"
else
echo "WARNING: .ci/config.env not found, using defaults"
fi
# Export with defaults
echo "ENABLE_DOCKER=${ENABLE_DOCKER:-true}" >> "$GITHUB_ENV"
echo "DOCKER_PUSH=${DOCKER_PUSH:-false}" >> "$GITHUB_ENV"
echo "DOCKER_PUSH_ON_BRANCH=${DOCKER_PUSH_ON_BRANCH:-true}" >> "$GITHUB_ENV"
echo "DOCKER_PUSH_ON_TAG=${DOCKER_PUSH_ON_TAG:-true}" >> "$GITHUB_ENV"
echo "REGISTRY_HOST=${REGISTRY_HOST:-git.hiddenden.cafe}" >> "$GITHUB_ENV"
echo "IMAGE_OWNER_CFG=${IMAGE_OWNER:-auto}" >> "$GITHUB_ENV"
echo "IMAGE_NAME_CFG=${IMAGE_NAME:-auto}" >> "$GITHUB_ENV"
echo "DOCKER_TAG_STRATEGY=${DOCKER_TAG_STRATEGY:-semver+latest}" >> "$GITHUB_ENV"
echo "DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Check if Docker is enabled
# -----------------------------------------------------------------------
- name: Check if Docker is enabled
run: |
if [ "$ENABLE_DOCKER" != "true" ]; then
echo "Docker is disabled (ENABLE_DOCKER=$ENABLE_DOCKER). Exiting."
# We still exit 0 — graceful skip.
echo "SKIP_DOCKER=true" >> "$GITHUB_ENV"
fi
# -----------------------------------------------------------------------
# Step 4: Detect Dockerfile or docker-compose.yml
# -----------------------------------------------------------------------
- name: Detect Docker files
if: env.SKIP_DOCKER != 'true'
run: |
if [ -f Dockerfile ]; then
echo "DOCKER_MODE=dockerfile" >> "$GITHUB_ENV"
echo "Detected: Dockerfile"
elif [ -f docker-compose.yml ] || [ -f docker-compose.yaml ]; then
echo "DOCKER_MODE=compose" >> "$GITHUB_ENV"
echo "Detected: docker-compose.yml"
else
echo "No Dockerfile or docker-compose.yml found. Skipping."
echo "SKIP_DOCKER=true" >> "$GITHUB_ENV"
fi
# -----------------------------------------------------------------------
# Step 5: Derive image owner and name dynamically
#
# Logic:
# FULL_REPO is derived from (in priority order):
# 1. $GITEA_REPOSITORY (Gitea native env var)
# 2. github.repository (Gitea Actions compatibility fallback)
# Format: "owner/repo"
#
# If IMAGE_OWNER=auto → use the owner part
# If IMAGE_NAME=auto → use the repo part
# Otherwise, use the explicit config values.
# -----------------------------------------------------------------------
- name: Derive image naming
if: env.SKIP_DOCKER != 'true'
run: |
# Determine FULL_REPO (owner/repo)
# Gitea Actions sets GITEA_REPOSITORY natively in some versions.
# It also maps github.repository for compatibility.
FULL_REPO="${GITEA_REPOSITORY:-${{ github.repository }}}"
if [ -z "$FULL_REPO" ]; then
echo "ERROR: Could not determine repository (GITEA_REPOSITORY and github.repository are both empty)"
exit 1
fi
# Split into OWNER and REPO
OWNER="$(echo "$FULL_REPO" | cut -d'/' -f1)"
REPO="$(echo "$FULL_REPO" | cut -d'/' -f2)"
echo "Derived FULL_REPO=$FULL_REPO OWNER=$OWNER REPO=$REPO"
# Apply config overrides
if [ "$IMAGE_OWNER_CFG" = "auto" ]; then
FINAL_OWNER="$OWNER"
else
FINAL_OWNER="$IMAGE_OWNER_CFG"
fi
if [ "$IMAGE_NAME_CFG" = "auto" ]; then
FINAL_NAME="$REPO"
else
FINAL_NAME="$IMAGE_NAME_CFG"
fi
# Construct the full image reference (without tag)
IMAGE_REF="${REGISTRY_HOST}/${FINAL_OWNER}/${FINAL_NAME}"
echo "Image reference: ${IMAGE_REF}:<tag>"
echo "IMAGE_REF=${IMAGE_REF}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 6: Determine tags based on trigger and strategy
#
# Tag rules:
# - PR: build only, tag = "pr-<number>" (local only)
# - Push to branch: tag = branch name (e.g., "main")
# - Tag v1.2.3: tag = "1.2.3"; also "latest" if strategy includes it
# -----------------------------------------------------------------------
- name: Determine tags
if: env.SKIP_DOCKER != 'true'
run: |
TAGS=""
SHOULD_PUSH=false
REF="${GITHUB_REF:-}"
echo "Event: ${{ github.event_name }}"
echo "Ref: ${REF}"
# --- Pull Request: build only ---
if [ "${{ github.event_name }}" = "pull_request" ]; then
TAGS="${IMAGE_REF}:pr-${{ github.event.number }}"
SHOULD_PUSH=false
echo "PR build — will NOT push"
# --- Tag push (v*) ---
elif echo "${REF}" | grep -qE '^refs/tags/v'; then
# Extract version: refs/tags/v1.2.3 → 1.2.3
VERSION="$(echo "${REF}" | sed 's|refs/tags/v||')"
TAGS="${IMAGE_REF}:${VERSION}"
# Optionally add :latest
if echo "$DOCKER_TAG_STRATEGY" | grep -qi 'latest'; then
TAGS="${TAGS},${IMAGE_REF}:latest"
fi
if [ "$DOCKER_PUSH" = "true" ] && [ "$DOCKER_PUSH_ON_TAG" = "true" ]; then
SHOULD_PUSH=true
fi
echo "Tag push — version=${VERSION}, push=${SHOULD_PUSH}"
# --- Branch push ---
elif echo "${REF}" | grep -q '^refs/heads/'; then
BRANCH="$(echo "${REF}" | sed 's|refs/heads/||')"
TAGS="${IMAGE_REF}:${BRANCH}"
if [ "$DOCKER_PUSH" = "true" ] && [ "$DOCKER_PUSH_ON_BRANCH" = "true" ]; then
SHOULD_PUSH=true
fi
echo "Branch push — branch=${BRANCH}, push=${SHOULD_PUSH}"
else
echo "Unknown ref type: ${REF}. Building with tag 'dev'."
TAGS="${IMAGE_REF}:dev"
SHOULD_PUSH=false
fi
echo "DOCKER_TAGS=${TAGS}" >> "$GITHUB_ENV"
echo "SHOULD_PUSH=${SHOULD_PUSH}" >> "$GITHUB_ENV"
echo "Final tags: ${TAGS}"
echo "Will push: ${SHOULD_PUSH}"
# -----------------------------------------------------------------------
# Step 7: Build the Docker image
# -----------------------------------------------------------------------
- name: Build Docker image
if: env.SKIP_DOCKER != 'true'
run: |
if [ "$DOCKER_MODE" = "dockerfile" ]; then
# Build with the first tag; additional tags are added after
PRIMARY_TAG="$(echo "$DOCKER_TAGS" | cut -d',' -f1)"
echo ">>> docker build -t ${PRIMARY_TAG} ."
docker build -t "${PRIMARY_TAG}" .
# Tag additional images if present
IFS=',' read -ra TAG_ARRAY <<< "$DOCKER_TAGS"
for tag in "${TAG_ARRAY[@]:1}"; do
echo ">>> docker tag ${PRIMARY_TAG} ${tag}"
docker tag "${PRIMARY_TAG}" "${tag}"
done
elif [ "$DOCKER_MODE" = "compose" ]; then
echo ">>> docker compose build"
docker compose build
fi
# -----------------------------------------------------------------------
# Step 8: Login to registry (only when pushing)
#
# Uses PAT-based auth. Requires secrets:
# REGISTRY_USERNAME — Gitea username or bot
# REGISTRY_TOKEN — PAT with package:write scope
# -----------------------------------------------------------------------
- name: Login to container registry
if: env.SKIP_DOCKER != 'true' && env.SHOULD_PUSH == 'true'
run: |
echo "Logging in to ${REGISTRY_HOST}..."
# Use --password-stdin to avoid leaking the token in process list
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login "${REGISTRY_HOST}" \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
# -----------------------------------------------------------------------
# Step 9: Push Docker image(s)
# -----------------------------------------------------------------------
- name: Push Docker image
if: env.SKIP_DOCKER != 'true' && env.SHOULD_PUSH == 'true'
run: |
IFS=',' read -ra TAG_ARRAY <<< "$DOCKER_TAGS"
for tag in "${TAG_ARRAY[@]}"; do
echo ">>> docker push ${tag}"
docker push "${tag}"
done
# -----------------------------------------------------------------------
# Step 10: Summary
# -----------------------------------------------------------------------
- name: Docker Summary
if: always()
run: |
echo "=============================="
echo " Docker Workflow Complete"
echo " Mode: ${DOCKER_MODE:-skipped}"
echo " Tags: ${DOCKER_TAGS:-none}"
echo " Pushed: ${SHOULD_PUSH:-false}"
echo "=============================="

View File

@@ -0,0 +1,107 @@
# =============================================================================
# Renovate Workflow — Automated Dependency Updates
# =============================================================================
#
# DISABLED BY DEFAULT (ENABLE_RENOVATE=false in .ci/config.env).
#
# When enabled, this workflow runs Renovate to:
# - Detect outdated dependencies (pip, npm, Docker FROM, etc.)
# - Open PRs with updates, respecting schedule and PR limits
#
# REQUIRED SECRET:
# RENOVATE_TOKEN — A Gitea PAT (Personal Access Token) with repo scope
# for the Renovate bot user. Set in repo/org secrets.
#
# CONFIG:
# - .ci/config.env → RENOVATE_SCHEDULE, RENOVATE_PR_LIMIT
# - renovate.json → Renovate-specific config (grouping, labels, etc.)
#
# See docs/RENOVATE.md for setup instructions.
# =============================================================================
name: Renovate
on:
# Run on a schedule (default: weekly on Mondays at 04:00 UTC)
schedule:
- cron: "0 4 * * 1"
# Allow manual trigger
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
steps:
# -----------------------------------------------------------------------
# Step 1: Checkout
# -----------------------------------------------------------------------
- name: Checkout
uses: actions/checkout@v4
# -----------------------------------------------------------------------
# Step 2: Load config
# -----------------------------------------------------------------------
- name: Load config
run: |
if [ -f .ci/config.env ]; then
set -a
source .ci/config.env
set +a
fi
echo "ENABLE_RENOVATE=${ENABLE_RENOVATE:-false}" >> "$GITHUB_ENV"
echo "RENOVATE_SCHEDULE=${RENOVATE_SCHEDULE:-weekly}" >> "$GITHUB_ENV"
echo "RENOVATE_PR_LIMIT=${RENOVATE_PR_LIMIT:-5}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Check if Renovate is enabled
# -----------------------------------------------------------------------
- name: Check if enabled
run: |
if [ "$ENABLE_RENOVATE" != "true" ]; then
echo "Renovate is disabled (ENABLE_RENOVATE=$ENABLE_RENOVATE)."
echo "To enable, set ENABLE_RENOVATE=true in .ci/config.env"
echo "SKIP_RENOVATE=true" >> "$GITHUB_ENV"
fi
# -----------------------------------------------------------------------
# Step 4: Run Renovate
#
# Uses the official Renovate CLI via npx. Configures it to point at
# the Gitea instance and the current repository.
# -----------------------------------------------------------------------
- name: Run Renovate
if: env.SKIP_RENOVATE != 'true'
env:
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
run: |
if [ -z "$RENOVATE_TOKEN" ]; then
echo "ERROR: RENOVATE_TOKEN secret is not set."
echo "Please create a Gitea PAT and add it as a repository secret."
exit 1
fi
# Determine repository path
FULL_REPO="${GITEA_REPOSITORY:-${{ github.repository }}}"
echo "Running Renovate for ${FULL_REPO} on ${REGISTRY_HOST:-git.hiddenden.cafe}..."
npx renovate \
--platform gitea \
--endpoint "https://${REGISTRY_HOST:-git.hiddenden.cafe}/api/v1" \
--token "$RENOVATE_TOKEN" \
--pr-hourly-limit "$RENOVATE_PR_LIMIT" \
"$FULL_REPO"
# -----------------------------------------------------------------------
# Step 5: Summary
# -----------------------------------------------------------------------
- name: Renovate Summary
if: always()
run: |
echo "=============================="
echo " Renovate Workflow Complete"
echo " Enabled: ${ENABLE_RENOVATE:-false}"
echo " Schedule: ${RENOVATE_SCHEDULE:-weekly}"
echo " PR Limit: ${RENOVATE_PR_LIMIT:-5}"
echo "=============================="

View File

@@ -0,0 +1,211 @@
# =============================================================================
# Security Workflow — Secret Scanning & Vulnerability Detection
# =============================================================================
#
# DISABLED BY DEFAULT (ENABLE_SECURITY=false in .ci/config.env).
#
# When enabled, this workflow runs:
# 1. gitleaks — scans for hardcoded secrets in the repo
# 2. osv-scanner — checks dependencies for known vulnerabilities
# 3. trivy — scans Docker images for CVEs (if a built image exists)
#
# STRICT_SECURITY=true → any finding fails the workflow
# STRICT_SECURITY=false → findings are logged as warnings (default)
#
# This is "best effort" — tools that aren't available are skipped.
# See docs/SECURITY.md for full details.
# =============================================================================
name: Security
on:
push:
branches:
- main
pull_request:
jobs:
security:
runs-on: ubuntu-latest
steps:
# -----------------------------------------------------------------------
# Step 1: Checkout
# -----------------------------------------------------------------------
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# -----------------------------------------------------------------------
# Step 2: Load configuration
# -----------------------------------------------------------------------
- name: Load config
run: |
if [ -f .ci/config.env ]; then
set -a
source .ci/config.env
set +a
fi
echo "ENABLE_SECURITY=${ENABLE_SECURITY:-false}" >> "$GITHUB_ENV"
echo "STRICT_SECURITY=${STRICT_SECURITY:-false}" >> "$GITHUB_ENV"
# -----------------------------------------------------------------------
# Step 3: Check if security scanning is enabled
# -----------------------------------------------------------------------
- name: Check if enabled
run: |
if [ "$ENABLE_SECURITY" != "true" ]; then
echo "Security scanning is disabled (ENABLE_SECURITY=$ENABLE_SECURITY)."
echo "To enable, set ENABLE_SECURITY=true in .ci/config.env"
echo "SKIP_SECURITY=true" >> "$GITHUB_ENV"
fi
# -----------------------------------------------------------------------
# Step 4: Gitleaks — Secret scanning
#
# Scans the git history for accidentally committed secrets
# (API keys, passwords, tokens, etc.)
# -----------------------------------------------------------------------
- name: Run gitleaks
if: env.SKIP_SECURITY != 'true'
run: |
FINDINGS=0
# Install gitleaks
echo "Installing gitleaks..."
GITLEAKS_VERSION="8.18.4"
curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | \
tar xz -C /usr/local/bin gitleaks || {
echo "WARNING: Failed to install gitleaks, skipping secret scan."
exit 0
}
echo ">>> gitleaks detect"
if ! gitleaks detect --source . --verbose; then
FINDINGS=1
echo "gitleaks found potential secrets!"
fi
if [ "$FINDINGS" -ne 0 ]; then
if [ "$STRICT_SECURITY" = "true" ]; then
echo "ERROR: Secret scan found issues (STRICT_SECURITY=true)"
exit 1
else
echo "WARNING: Secret scan found issues (STRICT_SECURITY=false, continuing)"
fi
else
echo "gitleaks: no secrets found."
fi
# -----------------------------------------------------------------------
# Step 5: OSV-Scanner — Dependency vulnerability scanning
#
# Checks lockfiles (requirements.txt, package-lock.json, etc.) against
# the OSV database for known vulnerabilities.
# -----------------------------------------------------------------------
- name: Run osv-scanner
if: env.SKIP_SECURITY != 'true'
run: |
FINDINGS=0
# Check if there's anything to scan
HAS_DEPS=false
for f in requirements.txt package-lock.json yarn.lock pnpm-lock.yaml go.sum Cargo.lock; do
if [ -f "$f" ]; then
HAS_DEPS=true
break
fi
done
if [ "$HAS_DEPS" = "false" ]; then
echo "SKIP: No dependency lockfiles found for osv-scanner."
exit 0
fi
# Install osv-scanner
echo "Installing osv-scanner..."
OSV_VERSION="1.8.3"
curl -sSfL "https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}/osv-scanner_linux_amd64" \
-o /usr/local/bin/osv-scanner && chmod +x /usr/local/bin/osv-scanner || {
echo "WARNING: Failed to install osv-scanner, skipping."
exit 0
}
echo ">>> osv-scanner --recursive ."
if ! osv-scanner --recursive .; then
FINDINGS=1
echo "osv-scanner found vulnerabilities!"
fi
if [ "$FINDINGS" -ne 0 ]; then
if [ "$STRICT_SECURITY" = "true" ]; then
echo "ERROR: Dependency scan found issues (STRICT_SECURITY=true)"
exit 1
else
echo "WARNING: Dependency scan found issues (STRICT_SECURITY=false, continuing)"
fi
else
echo "osv-scanner: no vulnerabilities found."
fi
# -----------------------------------------------------------------------
# Step 6: Trivy — Container image scanning
#
# Scans a Docker image for OS and library CVEs.
# Only runs if a Dockerfile exists (assumes image was built).
# -----------------------------------------------------------------------
- name: Run trivy
if: env.SKIP_SECURITY != 'true'
run: |
if [ ! -f Dockerfile ]; then
echo "SKIP: No Dockerfile found, skipping Trivy image scan."
exit 0
fi
FINDINGS=0
# Install trivy
echo "Installing trivy..."
curl -sSfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \
sh -s -- -b /usr/local/bin || {
echo "WARNING: Failed to install trivy, skipping."
exit 0
}
# Build the image first so Trivy can scan it
IMAGE_TAG="security-scan:local"
echo ">>> docker build -t ${IMAGE_TAG} ."
docker build -t "${IMAGE_TAG}" . || {
echo "WARNING: Docker build failed, skipping Trivy scan."
exit 0
}
echo ">>> trivy image ${IMAGE_TAG}"
if ! trivy image --exit-code 1 --severity HIGH,CRITICAL "${IMAGE_TAG}"; then
FINDINGS=1
echo "Trivy found vulnerabilities in the Docker image!"
fi
if [ "$FINDINGS" -ne 0 ]; then
if [ "$STRICT_SECURITY" = "true" ]; then
echo "ERROR: Image scan found issues (STRICT_SECURITY=true)"
exit 1
else
echo "WARNING: Image scan found issues (STRICT_SECURITY=false, continuing)"
fi
else
echo "trivy: no HIGH/CRITICAL vulnerabilities found."
fi
# -----------------------------------------------------------------------
# Step 7: Summary
# -----------------------------------------------------------------------
- name: Security Summary
if: always()
run: |
echo "=============================="
echo " Security Workflow Complete"
echo " Enabled: ${ENABLE_SECURITY:-false}"
echo " Strict: ${STRICT_SECURITY:-false}"
echo "=============================="

14
CODEOWNERS Normal file
View File

@@ -0,0 +1,14 @@
# =============================================================================
# CODEOWNERS — Optional
# =============================================================================
# Gitea supports CODEOWNERS for automatic review assignment.
# Uncomment and customize the lines below.
#
# Format: <pattern> <@user-or-team> [<@user-or-team> ...]
#
# Examples:
# * @default-reviewer
# /docs/ @docs-team
# *.py @python-team
# .gitea/ @devops-team
# .ci/ @devops-team

34
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,34 @@
# Code of Conduct — ${REPO_NAME}
## Our Pledge
We pledge to make participation in this project a harassment-free experience for
everyone, regardless of age, body size, disability, ethnicity, gender identity
and expression, level of experience, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
**Positive behavior includes:**
- Using welcoming and inclusive language
- Being respectful of differing viewpoints
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
**Unacceptable behavior includes:**
- Trolling, insulting comments, and personal attacks
- Public or private harassment
- Publishing others' private information without permission
- Other conduct which could reasonably be considered inappropriate
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the project maintainers. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and
appropriate to the circumstances.
## Attribution
This Code of Conduct is adapted from the
[Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.

50
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,50 @@
# Contributing to ${REPO_NAME}
Thank you for your interest in contributing! Here's how to get started.
## Getting Started
1. Fork this repository on [git.hiddenden.cafe](https://git.hiddenden.cafe).
2. Clone your fork locally.
3. Create a feature branch: `git checkout -b feature/my-change`
4. Make your changes and commit with clear messages.
5. Push to your fork and open a Pull Request.
## Development
```bash
# Install dependencies
pip install -r requirements.txt # Python
npm ci # Node (if applicable)
# Run checks locally before pushing
make fmt
make lint
make test
```
## Pull Request Guidelines
- Fill out the PR template completely.
- Keep PRs focused — one logical change per PR.
- Ensure CI passes (lint + tests).
- Update documentation if your change affects behavior.
## Code Style
- Python: Follow PEP 8. We use **ruff** for linting and **black** for formatting.
- JavaScript/TypeScript: Follow the project's ESLint config if present.
- Use `.editorconfig` settings (your editor should pick them up automatically).
## Reporting Issues
Use the issue templates provided:
- **Bug Report** — for defects
- **Feature Request** — for new ideas
- **Question / Support** — for help
For security issues, see [SECURITY.md](SECURITY.md).
## Code of Conduct
Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).

132
Makefile Normal file
View File

@@ -0,0 +1,132 @@
# =============================================================================
# Makefile — Common development targets
# =============================================================================
# Reads .ci/config.env for settings. All targets are fail-safe:
# they skip gracefully if tools or files are not present.
# =============================================================================
SHELL := /bin/bash
.DEFAULT_GOAL := help
# Load config if it exists
-include .ci/config.env
export
# Defaults if config is missing
ENABLE_DOCKER ?= false
DOCKER_PUSH ?= false
REGISTRY_HOST ?= git.hiddenden.cafe
IMAGE_OWNER ?= auto
IMAGE_NAME ?= auto
# Derive image owner/name dynamically if set to "auto"
_OWNER := $(if $(filter auto,$(IMAGE_OWNER)),$(shell basename $$(dirname $$(pwd))),$(IMAGE_OWNER))
_NAME := $(if $(filter auto,$(IMAGE_NAME)),$(shell basename $$(pwd)),$(IMAGE_NAME))
_IMAGE := $(REGISTRY_HOST)/$(_OWNER)/$(_NAME)
# ---------------------------------------------------------------------------
.PHONY: help
help: ## Show this help message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
# ---------------------------------------------------------------------------
# Code quality
# ---------------------------------------------------------------------------
.PHONY: fmt
fmt: ## Format code (Python: black/ruff; JS: prettier)
@if command -v ruff >/dev/null 2>&1; then \
echo ">>> ruff format ."; ruff format .; \
elif command -v black >/dev/null 2>&1; then \
echo ">>> black ."; black .; \
else \
echo "SKIP: no Python formatter found (install ruff or black)"; \
fi
@if [ -f package.json ] && command -v npx >/dev/null 2>&1; then \
if npx --no-install prettier --version >/dev/null 2>&1; then \
echo ">>> npx prettier --write ."; npx prettier --write .; \
else \
echo "SKIP: prettier not installed"; \
fi \
fi
.PHONY: lint
lint: ## Run linters (Python: ruff/flake8; JS: eslint)
@if command -v ruff >/dev/null 2>&1; then \
echo ">>> ruff check ."; ruff check .; \
elif command -v flake8 >/dev/null 2>&1; then \
echo ">>> flake8 ."; flake8 .; \
else \
echo "SKIP: no Python linter found (install ruff or flake8)"; \
fi
@if [ -f package.json ] && command -v npx >/dev/null 2>&1; then \
if npx --no-install eslint --version >/dev/null 2>&1; then \
echo ">>> npx eslint ."; npx eslint .; \
else \
echo "SKIP: eslint not installed"; \
fi \
fi
.PHONY: test
test: ## Run tests (Python: pytest; JS: npm test)
@if [ -d tests ] || [ -f pytest.ini ] || [ -f pyproject.toml ]; then \
if command -v pytest >/dev/null 2>&1; then \
echo ">>> pytest"; pytest; \
else \
echo "SKIP: pytest not installed"; \
fi \
else \
echo "SKIP: no Python tests detected"; \
fi
@if [ -f package.json ]; then \
if grep -q '"test"' package.json 2>/dev/null; then \
echo ">>> npm test"; npm test; \
else \
echo "SKIP: no 'test' script in package.json"; \
fi \
fi
# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
.PHONY: build
build: ## Build the project (JS: npm run build; Python: pip install -e .)
@if [ -f package.json ] && grep -q '"build"' package.json 2>/dev/null; then \
echo ">>> npm run build"; npm run build; \
elif [ -f setup.py ] || [ -f pyproject.toml ]; then \
echo ">>> pip install -e ."; pip install -e .; \
else \
echo "SKIP: no build step detected"; \
fi
# ---------------------------------------------------------------------------
# Docker
# ---------------------------------------------------------------------------
.PHONY: docker-build
docker-build: ## Build Docker image
@if [ "$(ENABLE_DOCKER)" != "true" ]; then \
echo "SKIP: ENABLE_DOCKER is not true"; exit 0; \
fi
@if [ -f Dockerfile ]; then \
echo ">>> docker build -t $(_IMAGE):local ."; \
docker build -t "$(_IMAGE):local" .; \
elif [ -f docker-compose.yml ]; then \
echo ">>> docker compose build"; \
docker compose build; \
else \
echo "SKIP: no Dockerfile or docker-compose.yml found"; \
fi
.PHONY: docker-push
docker-push: ## Push Docker image (requires DOCKER_PUSH=true)
@if [ "$(DOCKER_PUSH)" != "true" ]; then \
echo "ABORT: DOCKER_PUSH is not true in .ci/config.env"; \
echo "Set DOCKER_PUSH=true to enable pushing."; \
exit 1; \
fi
@echo ">>> docker push $(_IMAGE):local"
docker push "$(_IMAGE):local"

31
SECURITY.md Normal file
View File

@@ -0,0 +1,31 @@
# Security Policy — ${REPO_NAME}
## Reporting a Vulnerability
**Do NOT open a public issue for security vulnerabilities.**
Instead, please report vulnerabilities privately:
1. Email: **security@hiddenden.cafe** (preferred)
2. Or use the Gitea "Security" issue template which reminds reporters to use private channels.
Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We aim to acknowledge reports within **48 hours** and provide a fix or mitigation plan
within **7 days** for critical issues.
## Supported Versions
| Version | Supported |
| ------- | --------- |
| latest | Yes |
## Security Scanning
This repository optionally runs automated security scanning via Gitea Actions.
To enable it, set `ENABLE_SECURITY=true` in `.ci/config.env`.
See [docs/SECURITY.md](docs/SECURITY.md) for details.

108
docs/AI.md Normal file
View File

@@ -0,0 +1,108 @@
# AI Workflows — ${REPO_NAME}
## Overview
This template includes five AI-powered workflows that use the
[OpenRabbit](https://git.hiddenden.cafe/Hiddenden/openrabbit) tooling
to provide automated code review, issue triage, and interactive chat
via a bot (default: `@codebot` / user `Bartender`).
All AI workflows check out the central `Hiddenden/openrabbit` repo at runtime
and execute its Python-based tools. No AI code lives in your repository.
## Workflows
### 1. Enterprise AI Code Review (`enterprise-ai-review.yml`)
- **Trigger**: Pull request opened or updated
- **What it does**: Automatically reviews PR diffs for code quality, bugs,
security issues, and style. Posts findings as PR comments.
- **Severity gating**: If the review finds HIGH severity issues, CI fails
(optional — see the `Check Review Result` step).
### 2. AI Issue Triage (`ai-issue-triage.yml`)
- **Trigger**: Comment containing `@codebot triage` on any issue
- **What it does**: Analyzes the issue content and applies appropriate labels,
priority, and category suggestions.
### 3. AI Comment Reply (`ai-comment-reply.yml`)
- **Trigger**: Comment containing a specific `@codebot` command
- **Supported commands**:
- `@codebot help` — show available commands
- `@codebot explain` — explain code or issue context
- `@codebot suggest` — suggest improvements
- `@codebot security` — security-focused analysis
- `@codebot summarize` — summarize a thread
- `@codebot changelog` — generate changelog entries
- `@codebot explain-diff` — explain PR diff
- `@codebot review-again` — re-run review
- `@codebot setup-labels` — configure repo labels
- **PR vs Issue**: Automatically detects whether the comment is on a PR or issue
and dispatches accordingly.
### 4. AI Chat (`ai-chat.yml`)
- **Trigger**: Comment mentioning `@codebot` that is NOT a known command
- **What it does**: Free-form AI chat. Ask the bot any question and it will
respond using the codebase context and optional web search (SearXNG).
- **Routing**: This is the fallback — only fires when no specific command matches.
### 5. AI Codebase Quality Review (`ai-codebase-review.yml`)
- **Trigger**: Manual (workflow_dispatch) or scheduled (weekly, commented out)
- **What it does**: Full codebase analysis generating a quality report.
- **Report types**: `full`, `security`, `quick` (selectable on manual trigger).
## Required Secrets
All AI workflows require these secrets in your repository
(Settings → Actions → Secrets):
| Secret | Required | Description |
|--------|----------|-------------|
| `AI_REVIEW_TOKEN` | Yes | Gitea PAT with repo access (to check out OpenRabbit and post comments) |
| `OPENAI_API_KEY` | Conditional | OpenAI API key (if using OpenAI models) |
| `OPENROUTER_API_KEY` | Conditional | OpenRouter API key (if using OpenRouter) |
| `OLLAMA_HOST` | Conditional | Ollama server URL (if using self-hosted models) |
| `SEARXNG_URL` | Optional | SearXNG instance URL for web search in AI chat |
At least one AI provider key (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, or
`OLLAMA_HOST`) must be set.
## Customization
### Changing the Bot Name
The default bot is `@codebot` (Gitea user: `Bartender`). To change it:
1. Update the `if:` conditions in all AI workflows to match your bot's mention prefix.
2. Update the `github.event.comment.user.login != 'Bartender'` check to your bot's username.
3. Update `config.yml` in the OpenRabbit tooling if applicable.
### Loop Prevention
All AI workflows check `github.event.comment.user.login != 'Bartender'` to
prevent the bot from responding to its own comments. This is critical —
without it, the bot can trigger infinite loops.
### Workflow Routing
The three comment-triggered workflows are carefully routed to avoid duplicates:
```
Issue comment with @codebot
├── Contains "triage"? → ai-issue-triage.yml
├── Contains known command? → ai-comment-reply.yml
└── Free-form mention? → ai-chat.yml (fallback)
```
## Enabling / Disabling
To disable AI workflows without deleting them, either:
- Remove the workflow files from `.gitea/workflows/`
- Or comment out the `on:` triggers in each file
To enable the scheduled codebase review, uncomment the `schedule` trigger
in `ai-codebase-review.yml`.

52
docs/CI.md Normal file
View File

@@ -0,0 +1,52 @@
# CI Pipeline — ${REPO_NAME}
## Overview
The CI workflow (`.gitea/workflows/ci.yml`) runs on every push and pull request.
It auto-detects the project type and runs the appropriate tools.
## Detection Logic
The workflow checks for project files in this order:
### Python Detection
- **Trigger**: `requirements.txt`, `setup.py`, or `pyproject.toml` exists
- **Actions**:
1. Set up Python 3.x
2. `pip install -r requirements.txt` (if present)
3. Install dev tools: ruff, black, flake8, pytest
4. **Lint**: Run ruff, black --check, and flake8 (each skipped if not installed)
5. **Test**: Run pytest (only if `tests/` dir or pytest config detected)
### Node.js Detection
- **Trigger**: `package.json` exists
- **Actions**:
1. Set up Node.js (LTS)
2. `npm ci`
3. **Lint**: `npm run lint` (only if "lint" script exists in package.json)
4. **Test**: `npm test` (only if "test" script exists)
5. **Build**: `npm run build` (only if "build" script exists)
### No Project Detected
- If neither Python nor Node.js files are found, the workflow prints a message
and exits successfully. **It never fails due to missing language detection.**
## Strict Mode
Controlled by `CI_STRICT` in `.ci/config.env`:
| CI_STRICT | Behavior |
|-----------|----------|
| `true` (default) | Lint/test failures cause the workflow to fail |
| `false` | Failures are logged as warnings; workflow succeeds |
Use `CI_STRICT=false` during early development when you want visibility
into issues without blocking merges.
## Adding Support for Other Languages
To add support for another language (Go, Rust, etc.):
1. Add a detection step similar to the Python/Node checks.
2. Add setup, lint, and test steps conditional on detection.
3. Follow the same CI_STRICT pattern for error handling.

100
docs/CONFIG.md Normal file
View File

@@ -0,0 +1,100 @@
# Configuration Reference — ${REPO_NAME}
All settings live in **`.ci/config.env`** and are loaded by every workflow at runtime.
## CI Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_CI` | `true` | Master switch. If `false`, the CI workflow exits immediately. |
| `CI_STRICT` | `true` | If `true`, lint/test failures cause the workflow to fail. If `false`, they are logged as warnings only. |
| `DEFAULT_BRANCH` | `main` | The primary branch. Used by Docker and other workflows to determine branch-push behavior. |
## Docker Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_DOCKER` | `true` | Master switch for Docker build steps. |
| `DOCKER_PUSH` | `false` | Whether to push images to the registry. **Safe default: off.** |
| `DOCKER_PUSH_ON_BRANCH` | `true` | Push when a commit lands on `DEFAULT_BRANCH`. Only effective if `DOCKER_PUSH=true`. |
| `DOCKER_PUSH_ON_TAG` | `true` | Push when a semver tag (`v*`) is pushed. Only effective if `DOCKER_PUSH=true`. |
| `REGISTRY_HOST` | `git.hiddenden.cafe` | Hostname of the container registry. |
| `IMAGE_OWNER` | `auto` | Image owner (org/user). `auto` = derived from repository context at runtime. |
| `IMAGE_NAME` | `auto` | Image name. `auto` = derived from repository name at runtime. |
| `DOCKER_TAG_STRATEGY` | `semver+latest` | Controls tagging. Options: `semver+latest`, `semver`, `branch`. |
### Tag Strategy Details
| Trigger | `semver+latest` | `semver` | `branch` |
|---------|-----------------|----------|----------|
| `v1.2.3` tag | `:1.2.3` + `:latest` | `:1.2.3` | — |
| Push to `main` | `:main` | `:main` | `:main` |
| Pull request | `:pr-<N>` (local only) | `:pr-<N>` (local only) | `:pr-<N>` (local only) |
## Security Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_SECURITY` | `false` | Master switch. Enables gitleaks, osv-scanner, and Trivy. |
| `STRICT_SECURITY` | `false` | If `true`, any finding fails the workflow. If `false`, findings are warnings. |
## Renovate Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_RENOVATE` | `false` | Master switch for Renovate dependency updates. |
| `RENOVATE_SCHEDULE` | `weekly` | How often Renovate runs. |
| `RENOVATE_PR_LIMIT` | `5` | Max open PRs Renovate can create. |
## Deploy Settings
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_DEPLOY` | `false` | Master switch. Deploy never runs unless `true`. |
| `DEPLOY_MODE` | `local-runner` | How to reach the VPS: `local-runner` (runs on VPS directly) or `ssh` (SSH from any runner). |
| `DEPLOY_RUNNER_LABEL` | `deploy-ovh` | Runner label for local-runner mode. Must match the act_runner's registered label. |
| `DEPLOY_WORKDIR` | `/opt/${REPO_NAME}` | Working directory on the VPS where your project lives. |
| `DEPLOY_STRATEGY` | `compose` | What to do on deploy: `compose` (docker compose up), `systemd` (restart service), or `script` (run custom script). |
| `DEPLOY_COMPOSE_FILE` | `docker-compose.yml` | Compose file path relative to `DEPLOY_WORKDIR`. Used with `compose` strategy. |
| `DEPLOY_SYSTEMD_SERVICE` | _(empty)_ | Systemd service name. Required if `DEPLOY_STRATEGY=systemd`. |
| `DEPLOY_SCRIPT` | `scripts/deploy.sh` | Custom deploy script path relative to repo root. Used with `script` strategy. |
| `DEPLOY_ON_TAG` | `false` | Also deploy when a `v*` tag is pushed. |
### Deploy Mode Comparison
| | `local-runner` | `ssh` |
|---|---|---|
| Secrets needed | None | `DEPLOY_SSH_KEY`, `DEPLOY_HOST`, `DEPLOY_USER` |
| Runner location | On the VPS | Any runner (e.g., shared) |
| Setup effort | Install act_runner on VPS | Create SSH key + add secrets |
| Network exposure | None | SSH port must be reachable |
See [docs/DEPLOY.md](DEPLOY.md) for full setup instructions.
## Recommended Defaults
For a **new public project**:
```env
ENABLE_CI=true
CI_STRICT=true
ENABLE_DOCKER=true
DOCKER_PUSH=false # Enable when ready to publish
ENABLE_SECURITY=false # Enable after initial development
ENABLE_RENOVATE=false # Enable after first release
ENABLE_DEPLOY=false # Enable when VPS runner is set up
```
For a **production project**:
```env
ENABLE_CI=true
CI_STRICT=true
ENABLE_DOCKER=true
DOCKER_PUSH=true
DOCKER_PUSH_ON_TAG=true
ENABLE_SECURITY=true
STRICT_SECURITY=true
ENABLE_RENOVATE=true
ENABLE_DEPLOY=true
DEPLOY_MODE=local-runner
DEPLOY_STRATEGY=compose
```

295
docs/DEPLOY.md Normal file
View File

@@ -0,0 +1,295 @@
# Deploy Workflow — ${REPO_NAME}
## Overview
The deploy workflow (`.gitea/workflows/deploy.yml`) automates deployment to a
VPS after a push to the default branch. It supports two modes and three
deployment strategies.
**Disabled by default.** Set `ENABLE_DEPLOY=true` in `.ci/config.env` to enable.
## Deployment Modes
### Mode A: Local Runner (Recommended)
The deploy job runs **directly on the VPS** via a self-hosted `act_runner`.
No SSH keys or network credentials needed — the runner already has local access.
```
┌──────────┐ push ┌──────────┐ runs-on: deploy-ovh ┌───────────┐
│ Developer │ ────────────► │ Gitea │ ────────────────────────► │ VPS │
│ │ │ Server │ │ act_runner│
└──────────┘ └──────────┘ │ (local) │
└───────────┘
```
**Pros:**
- No SSH secrets to manage
- No network exposure (runner is already on the VPS)
- Simpler setup, fewer moving parts
- Runner can access local Docker socket, systemd, files directly
**Cons:**
- Requires installing `act_runner` on the VPS
- One runner per VPS (or one runner with multiple labels)
### Mode B: SSH (Fallback)
The deploy job runs on **any runner** (e.g., shared ubuntu-latest) and SSHs
into the VPS to execute deploy commands remotely.
```
┌──────────┐ push ┌──────────┐ runs job ┌────────┐ SSH ┌─────┐
│ Developer │ ────────────► │ Gitea │ ──────────────► │ Runner │ ────────► │ VPS │
└──────────┘ └──────────┘ │(shared)│ └─────┘
└────────┘
```
**Pros:**
- Works without installing anything on the VPS
- Can deploy to VPSes you don't fully control
**Cons:**
- Requires SSH secrets (private key, host, user)
- VPS SSH port must be reachable from the runner
- More moving parts, more to go wrong
## Deploy Strategies
Regardless of mode, the workflow supports three strategies for what actually
happens on the VPS:
### `compose` (default)
```bash
cd /opt/myapp
docker compose -f docker-compose.yml pull
docker compose -f docker-compose.yml up -d
```
Best for: Docker Compose-based projects. Pulls latest images and recreates
containers with zero-downtime (depends on your compose config).
### `systemd`
```bash
sudo systemctl restart my-service
```
Best for: Applications managed as systemd services (e.g., a Go binary,
Python app with gunicorn, etc.).
### `script`
```bash
./scripts/deploy.sh /opt/myapp
```
Best for: Custom deploy logic that doesn't fit compose or systemd.
The script receives `DEPLOY_WORKDIR` as `$1`.
## Setup Guide
### Local Runner Mode
#### Step 1: Install act_runner on the VPS
```bash
# Download the latest act_runner binary
wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64
chmod +x act_runner-linux-amd64
sudo mv act_runner-linux-amd64 /usr/local/bin/act_runner
```
#### Step 2: Register the runner with your label
```bash
# Register with Gitea (get the token from: Site Admin → Runners → Create)
act_runner register \
--instance https://git.hiddenden.cafe \
--token YOUR_RUNNER_TOKEN \
--labels deploy-ovh
# Or for repo-level: Repository Settings → Actions → Runners → Create
```
#### Step 3: Configure runner labels
The runner config file (usually `~/.config/act_runner/config.yaml` or next to
the binary) contains the labels:
```yaml
runner:
labels:
- "deploy-ovh:host"
```
The `:host` suffix means the job runs directly on the host (not in a container).
This is important for accessing Docker, systemd, and local files.
**After changing labels, restart the runner:**
```bash
sudo systemctl restart act_runner
```
#### Step 4: Update workflow runs-on
In `.gitea/workflows/deploy.yml`, the `deploy-local-runner` job has:
```yaml
runs-on: deploy-ovh
```
If you changed `DEPLOY_RUNNER_LABEL` in config.env, you **must also** update
this `runs-on` value in the workflow file to match. Gitea Actions does not
support dynamic `runs-on` from environment variables.
#### Step 5: Enable deploy
In `.ci/config.env`:
```env
ENABLE_DEPLOY=true
DEPLOY_MODE=local-runner
DEPLOY_RUNNER_LABEL=deploy-ovh
DEPLOY_WORKDIR=/opt/myapp
DEPLOY_STRATEGY=compose
```
#### Step 6: Verify VPS prerequisites
For `compose` strategy:
```bash
# Docker and compose plugin must be installed
docker --version
docker compose version
# The deploy workdir must exist with a docker-compose.yml
ls /opt/myapp/docker-compose.yml
```
For `systemd` strategy:
```bash
# The service must exist
systemctl cat my-service
```
For `script` strategy:
```bash
# The script must be in the repo and executable
```
### SSH Mode
#### Step 1: Create an SSH key
```bash
ssh-keygen -t ed25519 -C "deploy@gitea" -f deploy_key -N ""
```
#### Step 2: Add the public key to the VPS
```bash
ssh-copy-id -i deploy_key.pub user@your-vps
```
#### Step 3: Add secrets to Gitea
Go to **Repository Settings → Actions → Secrets** and add:
| Secret | Value |
|--------|-------|
| `DEPLOY_SSH_KEY` | Contents of `deploy_key` (private key) |
| `DEPLOY_HOST` | VPS IP or hostname |
| `DEPLOY_USER` | SSH username |
| `DEPLOY_KNOWN_HOSTS` | Output of `ssh-keyscan your-vps` (recommended) |
#### Step 4: Enable deploy
In `.ci/config.env`:
```env
ENABLE_DEPLOY=true
DEPLOY_MODE=ssh
DEPLOY_WORKDIR=/opt/myapp
DEPLOY_STRATEGY=compose
```
## Configuration Reference
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_DEPLOY` | `false` | Master switch. Deploy never runs unless `true`. |
| `DEPLOY_MODE` | `local-runner` | `local-runner` or `ssh` |
| `DEPLOY_RUNNER_LABEL` | `deploy-ovh` | Runner label for local-runner mode |
| `DEPLOY_WORKDIR` | `/opt/${REPO_NAME}` | Working directory on the VPS |
| `DEPLOY_STRATEGY` | `compose` | `compose`, `systemd`, or `script` |
| `DEPLOY_COMPOSE_FILE` | `docker-compose.yml` | Compose file path (relative to workdir) |
| `DEPLOY_SYSTEMD_SERVICE` | _(empty)_ | Systemd service name (required for systemd strategy) |
| `DEPLOY_SCRIPT` | `scripts/deploy.sh` | Deploy script path (relative to repo root) |
| `DEPLOY_ON_TAG` | `false` | Also deploy on `v*` tag pushes |
## Safety
### Branch Protection
The workflow only deploys from the `DEFAULT_BRANCH` (main). This is enforced
in two places:
1. **Trigger filter**: `on.push.branches: [main]`
2. **Branch guard step**: Runtime check comparing the actual ref to `DEFAULT_BRANCH`
### Never on Pull Requests
The `pull_request` event is intentionally absent from the trigger list.
Deploy cannot run on PRs.
### Protected Branches
For maximum safety, enable **branch protection** on `main` in Gitea:
1. Go to **Repository Settings → Branches → Branch Protection**
2. Enable for `main`
3. Require: status checks to pass, review approval, no force push
4. This ensures only reviewed, passing code reaches main → gets deployed
### Secrets
- **Local-runner mode**: No secrets needed. The runner has local access.
- **SSH mode**: Private key is written to a temp file, used once, then deleted.
It is never echoed to logs.
## Runner Label Examples
| Label | Description |
|-------|-------------|
| `deploy-ovh` | OVH VPS runner |
| `vps-prod` | Production VPS |
| `deploy-hetzner` | Hetzner VPS runner |
| `staging-runner` | Staging environment |
## Troubleshooting
### Local Runner Mode
- [ ] **Runner registered?** Check Gitea Admin → Runners or Repo Settings → Runners
- [ ] **Label matches?** `runs-on` in workflow must match the runner's label exactly
- [ ] **Runner online?** Check runner status in Gitea; restart if offline
- [ ] **Label suffix correct?** Use `:host` for direct host execution (not container)
- [ ] **Docker accessible?** Runner user must be in the `docker` group
- [ ] **Workdir exists?** `DEPLOY_WORKDIR` must exist and be readable
- [ ] **Compose file present?** Check `docker-compose.yml` exists in the workdir
- [ ] **Permissions?** For systemd: runner user needs sudo for systemctl
### SSH Mode
- [ ] **Secrets set?** Check all four secrets are in repo settings
- [ ] **Key format?** Private key must include `-----BEGIN` header
- [ ] **SSH port open?** VPS firewall must allow port 22 (or custom port) from runner
- [ ] **User permissions?** SSH user needs access to Docker/systemd/workdir
- [ ] **Known hosts?** Set `DEPLOY_KNOWN_HOSTS` for production (avoids MITM risk)
- [ ] **Key not passphrase-protected?** CI keys must have no passphrase
### General
- [ ] **ENABLE_DEPLOY=true?** Check `.ci/config.env`
- [ ] **Pushing to main?** Deploy only triggers on `DEFAULT_BRANCH`
- [ ] **Workflow file correct?** YAML syntax errors prevent the workflow from running
- [ ] **Check Actions tab** in Gitea for workflow run logs

104
docs/DOCKER.md Normal file
View File

@@ -0,0 +1,104 @@
# Docker Build & Registry — ${REPO_NAME}
## Overview
The Docker workflow (`.gitea/workflows/docker.yml`) builds Docker images and
optionally pushes them to the Gitea Container Registry.
## Gitea Container Registry Naming Convention
Gitea's registry follows this pattern:
```
{REGISTRY_HOST}/{OWNER}/{IMAGE}:{TAG}
```
Example:
```
git.hiddenden.cafe/myorg/myapp:1.2.3
```
This is different from Docker Hub (`docker.io/library/myapp:latest`).
The workflow enforces this format automatically.
## Dynamic Owner/Repo Derivation
The workflow dynamically determines the image owner and name so it works
for both user repos and organization repos without hardcoding.
**Logic:**
1. Determine `FULL_REPO` from (in priority order):
- `$GITEA_REPOSITORY` (Gitea native environment variable)
- `${{ github.repository }}` (Gitea Actions compatibility layer)
2. Split into `OWNER` (before `/`) and `REPO` (after `/`).
3. If `IMAGE_OWNER=auto` in config → use `OWNER`; else use the config value.
4. If `IMAGE_NAME=auto` in config → use `REPO`; else use the config value.
This means you rarely need to change `IMAGE_OWNER` or `IMAGE_NAME`.
## Triggers & Push Behavior
| Event | Build? | Push? | Condition |
|-------|--------|-------|-----------|
| Pull Request | Yes | **No** | Never pushes on PRs |
| Push to `main` | Yes | Conditional | `DOCKER_PUSH=true` AND `DOCKER_PUSH_ON_BRANCH=true` |
| Tag `v1.2.3` | Yes | Conditional | `DOCKER_PUSH=true` AND `DOCKER_PUSH_ON_TAG=true` |
**Safe default**: `DOCKER_PUSH=false` — images are built but never pushed.
## Tag Strategy
Controlled by `DOCKER_TAG_STRATEGY` in `.ci/config.env`:
### `semver+latest` (default)
- Tag `v1.2.3` → pushes `:1.2.3` and `:latest`
- Push to `main` → pushes `:main`
### `semver`
- Tag `v1.2.3` → pushes `:1.2.3` only
- Push to `main` → pushes `:main`
### `branch`
- Branch pushes only, tagged as `:branchname`
## Required Secrets
To push images, set these secrets in your Gitea repository
(Settings → Actions → Secrets):
| Secret | Description |
|--------|-------------|
| `REGISTRY_USERNAME` | Gitea username or bot account name |
| `REGISTRY_TOKEN` | Personal Access Token with `package:write` scope |
### Creating a PAT
1. Go to **Settings → Applications → Generate New Token**
2. Name: e.g., `ci-docker-push`
3. Scopes: select **`package`** (read + write)
4. Copy the token and add it as `REGISTRY_TOKEN` in repo secrets
**Why PAT instead of job token?**
Gitea Actions job tokens may not have sufficient permissions for the
container registry in all configurations. PATs are the recommended approach.
## Detection
The workflow auto-detects how to build:
1. **Dockerfile**`docker build -t <image>:<tag> .`
2. **docker-compose.yml**`docker compose build`
3. **Neither** → exits 0 with a message (graceful skip)
## Enabling Docker Push
1. Set `DOCKER_PUSH=true` in `.ci/config.env`
2. Add `REGISTRY_USERNAME` and `REGISTRY_TOKEN` secrets
3. Push a commit or tag — the workflow will build and push
## Pulling Images
After pushing, pull images with:
```bash
docker pull git.hiddenden.cafe/<owner>/<repo>:latest
```

101
docs/RENOVATE.md Normal file
View File

@@ -0,0 +1,101 @@
# Renovate — Automated Dependency Updates — ${REPO_NAME}
## Overview
[Renovate](https://docs.renovatebot.com/) automatically detects outdated
dependencies and opens PRs to update them. This keeps your project secure
and up-to-date with minimal manual effort.
**Disabled by default.** Set `ENABLE_RENOVATE=true` in `.ci/config.env` to enable.
## How It Works
1. The workflow (`.gitea/workflows/renovate.yml`) runs on a schedule (default: weekly).
2. Renovate scans your lockfiles and config for outdated packages.
3. It opens PRs with updates, grouped by minor/patch to reduce noise.
4. You review and merge the PRs.
## Setup
### Step 1: Create a Bot PAT
1. Create a dedicated Gitea user (e.g., `renovate-bot`) or use your own account.
2. Generate a PAT: **Settings → Applications → Generate New Token**
3. Scopes: `repo` (full repository access)
4. Copy the token.
### Step 2: Add the Secret
1. Go to **Repository Settings → Actions → Secrets**
2. Add secret: `RENOVATE_TOKEN` = the PAT from step 1
### Step 3: Enable in Config
In `.ci/config.env`:
```env
ENABLE_RENOVATE=true
RENOVATE_SCHEDULE=weekly
RENOVATE_PR_LIMIT=5
```
### Step 4: Commit and Push
Renovate will run on the next scheduled time, or you can trigger it manually
via the Actions tab → "Renovate" → "Run workflow".
## Configuration
### Workflow Config (.ci/config.env)
| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_RENOVATE` | `false` | Master switch |
| `RENOVATE_SCHEDULE` | `weekly` | How often to run |
| `RENOVATE_PR_LIMIT` | `5` | Max open PRs at once |
### Renovate Config (renovate.json)
The `renovate.json` file in the repo root controls Renovate's behavior:
- **Grouping**: Minor and patch updates are grouped into a single PR.
- **Docker**: Base image updates (`FROM ...`) are enabled.
- **Labels**: PRs get the `dependencies` label.
- **Schedule**: Runs before 6am on Mondays.
Customize `renovate.json` to:
- Pin specific dependencies
- Exclude packages
- Change grouping strategy
- Add automerge for low-risk updates
### Docker Base Image Updates
Renovate will detect `FROM` lines in your Dockerfile and open PRs when
newer base images are available. This is enabled by default in `renovate.json`.
## Noise Control
To reduce PR spam:
1. **Group updates**: Already configured — minor/patch grouped together.
2. **Limit PRs**: `RENOVATE_PR_LIMIT=5` (adjust as needed).
3. **Schedule**: Runs weekly by default, not on every push.
4. **Automerge**: Add to `renovate.json` for trusted updates:
```json
{
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true
}
]
}
```
## Expected Behavior
After enabling, expect:
- An initial burst of PRs for all outdated dependencies
- Weekly batches of 1-5 PRs (depending on updates available)
- PRs labeled `dependencies` for easy filtering
- Each PR includes a changelog and compatibility notes

19
pull_request_template.md Normal file
View File

@@ -0,0 +1,19 @@
## Description
<!-- What does this PR do? Why is it needed? -->
## Changes
- [ ] ...
## Related Issues
<!-- Link related issues: Closes #123, Fixes #456 -->
## Checklist
- [ ] I have tested my changes locally
- [ ] Linting passes (`make lint`)
- [ ] Tests pass (`make test`)
- [ ] Documentation updated (if applicable)
- [ ] No secrets or credentials are committed

27
renovate.json Normal file
View File

@@ -0,0 +1,27 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"description": "Renovate config — groups minor/patch, limits PRs, updates Docker base images.",
"schedule": ["before 6am on Monday"],
"prHourlyLimit": 5,
"prConcurrentLimit": 5,
"labels": ["dependencies"],
"packageRules": [
{
"description": "Group all minor and patch updates to reduce PR noise",
"matchUpdateTypes": ["minor", "patch"],
"groupName": "minor-and-patch",
"groupSlug": "minor-patch"
},
{
"description": "Update Docker base images (FROM ...)",
"matchDatasources": ["docker"],
"enabled": true
}
],
"docker": {
"enabled": true
}
}