diff --git a/.ci/config.env b/.ci/config.env new file mode 100644 index 0000000..9743746 --- /dev/null +++ b/.ci/config.env @@ -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 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2b0142a --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e9f214c --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitea/ISSUE_TEMPLATE/auth_security.yml b/.gitea/ISSUE_TEMPLATE/auth_security.yml new file mode 100644 index 0000000..b59ccf4 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/auth_security.yml @@ -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 diff --git a/.gitea/ISSUE_TEMPLATE/bug.yml b/.gitea/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..2215599 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug.yml @@ -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. diff --git a/.gitea/ISSUE_TEMPLATE/collaboration_tools.yml b/.gitea/ISSUE_TEMPLATE/collaboration_tools.yml new file mode 100644 index 0000000..94af28c --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/collaboration_tools.yml @@ -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 diff --git a/.gitea/ISSUE_TEMPLATE/config.yml b/.gitea/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..1b542c2 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/config.yml @@ -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. diff --git a/.gitea/ISSUE_TEMPLATE/dev_workflow.yml b/.gitea/ISSUE_TEMPLATE/dev_workflow.yml new file mode 100644 index 0000000..25fa6ac --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/dev_workflow.yml @@ -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). diff --git a/.gitea/ISSUE_TEMPLATE/feature.yml b/.gitea/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..c997183 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature.yml @@ -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. diff --git a/.gitea/ISSUE_TEMPLATE/performance_improvement.yml b/.gitea/ISSUE_TEMPLATE/performance_improvement.yml new file mode 100644 index 0000000..e60fede --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/performance_improvement.yml @@ -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 diff --git a/.gitea/ISSUE_TEMPLATE/project_requirement.yml b/.gitea/ISSUE_TEMPLATE/project_requirement.yml new file mode 100644 index 0000000..96847cb --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/project_requirement.yml @@ -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 diff --git a/.gitea/ISSUE_TEMPLATE/proposal.md b/.gitea/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 0000000..3e1eca7 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,41 @@ + +# 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) diff --git a/.gitea/ISSUE_TEMPLATE/question.yml b/.gitea/ISSUE_TEMPLATE/question.yml new file mode 100644 index 0000000..5579f6e --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/question.yml @@ -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. diff --git a/.gitea/ISSUE_TEMPLATE/repo_management.yml b/.gitea/ISSUE_TEMPLATE/repo_management.yml new file mode 100644 index 0000000..ecd18ed --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/repo_management.yml @@ -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. diff --git a/.gitea/ISSUE_TEMPLATE/task_todo.yml b/.gitea/ISSUE_TEMPLATE/task_todo.yml new file mode 100644 index 0000000..c8ae0a8 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/task_todo.yml @@ -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. diff --git a/.gitea/ISSUE_TEMPLATE/ux.yml b/.gitea/ISSUE_TEMPLATE/ux.yml new file mode 100644 index 0000000..2fb1eee --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/ux.yml @@ -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 diff --git a/.gitea/template b/.gitea/template new file mode 100644 index 0000000..71577cc --- /dev/null +++ b/.gitea/template @@ -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 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..5fb7088 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 "==============================" diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..7569df4 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -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 +# 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 "==============================" diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml new file mode 100644 index 0000000..f39931f --- /dev/null +++ b/.gitea/workflows/docker.yml @@ -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}:" + + echo "IMAGE_REF=${IMAGE_REF}" >> "$GITHUB_ENV" + + # ----------------------------------------------------------------------- + # Step 6: Determine tags based on trigger and strategy + # + # Tag rules: + # - PR: build only, tag = "pr-" (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 "==============================" diff --git a/.gitea/workflows/renovate.yml b/.gitea/workflows/renovate.yml new file mode 100644 index 0000000..ebbfc57 --- /dev/null +++ b/.gitea/workflows/renovate.yml @@ -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 "==============================" diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml new file mode 100644 index 0000000..e4d339e --- /dev/null +++ b/.gitea/workflows/security.yml @@ -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 "==============================" diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..397f8f8 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,14 @@ +# ============================================================================= +# CODEOWNERS — Optional +# ============================================================================= +# Gitea supports CODEOWNERS for automatic review assignment. +# Uncomment and customize the lines below. +# +# Format: <@user-or-team> [<@user-or-team> ...] +# +# Examples: +# * @default-reviewer +# /docs/ @docs-team +# *.py @python-team +# .gitea/ @devops-team +# .ci/ @devops-team diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..bb2d2a4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..69b4190 --- /dev/null +++ b/CONTRIBUTING.md @@ -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). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f7aae17 --- /dev/null +++ b/Makefile @@ -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" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..da90e72 --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/docs/AI.md b/docs/AI.md new file mode 100644 index 0000000..60967e9 --- /dev/null +++ b/docs/AI.md @@ -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`. diff --git a/docs/CI.md b/docs/CI.md new file mode 100644 index 0000000..61bc033 --- /dev/null +++ b/docs/CI.md @@ -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. diff --git a/docs/CONFIG.md b/docs/CONFIG.md new file mode 100644 index 0000000..2d3594e --- /dev/null +++ b/docs/CONFIG.md @@ -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-` (local only) | `:pr-` (local only) | `:pr-` (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 +``` diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..61d92bb --- /dev/null +++ b/docs/DEPLOY.md @@ -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 diff --git a/docs/DOCKER.md b/docs/DOCKER.md new file mode 100644 index 0000000..9fc45cc --- /dev/null +++ b/docs/DOCKER.md @@ -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 : .` +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//:latest +``` diff --git a/docs/RENOVATE.md b/docs/RENOVATE.md new file mode 100644 index 0000000..7750481 --- /dev/null +++ b/docs/RENOVATE.md @@ -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 diff --git a/pull_request_template.md b/pull_request_template.md new file mode 100644 index 0000000..64b964a --- /dev/null +++ b/pull_request_template.md @@ -0,0 +1,19 @@ +## Description + + + +## Changes + +- [ ] ... + +## Related Issues + + + +## 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 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..6ccd6be --- /dev/null +++ b/renovate.json @@ -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 + } +}