dev #12
+121
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<!-- Title: Proposal / Design RFC -->
|
||||||
|
# Proposal: [Short title]
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
Concise description of the proposal and what outcome you want.
|
||||||
|
|
||||||
|
## Problem statement
|
||||||
|
What problem are we solving and why is it important? Include links to related issues.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
- Primary goals (what success looks like)
|
||||||
|
- Non-goals (explicitly out of scope)
|
||||||
|
|
||||||
|
## Proposed design
|
||||||
|
Describe the design in detail. Include:
|
||||||
|
- Architecture diagrams or ASCII art
|
||||||
|
- API changes (requests/responses)
|
||||||
|
- Data model changes or migrations
|
||||||
|
- UX flows or wireframes
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
Short list of alternatives and tradeoffs.
|
||||||
|
|
||||||
|
## Backwards compatibility & migration plan
|
||||||
|
Describe how to migrate existing data and any compatibility impacts.
|
||||||
|
|
||||||
|
## Security considerations
|
||||||
|
List potential security/privacy implications.
|
||||||
|
|
||||||
|
## Testing & rollout plan
|
||||||
|
How will this be tested? Phased rollout plan if needed.
|
||||||
|
|
||||||
|
## Implementation plan & timeline
|
||||||
|
High-level tasks and owners.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
List any unresolved questions.
|
||||||
|
|
||||||
|
**Checklist**
|
||||||
|
- [ ] Linked related issues
|
||||||
|
- [ ] Prototype or PoC (if available)
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
name: AI Codebase Quality Review
|
name: AI Codebase Quality Review
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# # Weekly scheduled run
|
# Weekly scheduled run
|
||||||
# schedule:
|
# schedule:
|
||||||
# - cron: "0 0 * * 0" # Every Sunday at midnight
|
# - cron: "0 0 * * 0" # Every Sunday at midnight
|
||||||
|
|
||||||
|
|||||||
@@ -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 "=============================="
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Deploy Workflow — Automated Deployment to VPS
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# PURPOSE:
|
||||||
|
# Deploy your application to a VPS after a successful push to the default
|
||||||
|
# branch (main). Supports two deployment modes:
|
||||||
|
#
|
||||||
|
# (A) local-runner — The deploy job runs directly on a self-hosted act_runner
|
||||||
|
# installed ON the VPS. No SSH needed. The runner is selected by a label
|
||||||
|
# (DEPLOY_RUNNER_LABEL). This is the recommended mode.
|
||||||
|
#
|
||||||
|
# (B) ssh — The deploy job runs on any runner and SSHs into the VPS to
|
||||||
|
# execute commands remotely. Requires SSH secrets. Use as fallback when
|
||||||
|
# you can't install a runner on the target VPS.
|
||||||
|
#
|
||||||
|
# SAFE BY DEFAULT:
|
||||||
|
# ENABLE_DEPLOY=false in .ci/config.env. Deploy never runs unless you
|
||||||
|
# explicitly enable it. It also never runs on pull_request events.
|
||||||
|
#
|
||||||
|
# DEPLOY STRATEGIES:
|
||||||
|
# compose — docker compose pull && docker compose up -d
|
||||||
|
# systemd — systemctl restart <service>
|
||||||
|
# script — run a custom deploy script
|
||||||
|
#
|
||||||
|
# TRIGGERS:
|
||||||
|
# - push to DEFAULT_BRANCH (main) → deploy if enabled
|
||||||
|
# - tag v* (only if DEPLOY_ON_TAG=true) → deploy if enabled
|
||||||
|
# - pull_request → NEVER (not in trigger list)
|
||||||
|
#
|
||||||
|
# REQUIRED SECRETS (ssh mode only):
|
||||||
|
# DEPLOY_SSH_KEY — private SSH key (ed25519 or RSA)
|
||||||
|
# DEPLOY_HOST — VPS hostname or IP
|
||||||
|
# DEPLOY_USER — SSH username on VPS
|
||||||
|
# DEPLOY_KNOWN_HOSTS — (optional) known_hosts entry for the VPS
|
||||||
|
#
|
||||||
|
# For local-runner mode: NO secrets needed. The runner already has local
|
||||||
|
# access. Just ensure the runner is registered with the correct label.
|
||||||
|
#
|
||||||
|
# See docs/DEPLOY.md for full setup instructions.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TRIGGERS
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Only push events — never pull_request.
|
||||||
|
# Branch filter is further enforced in the "branch guard" step below,
|
||||||
|
# because config.env may specify a different DEFAULT_BRANCH.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# JOB: deploy-local-runner
|
||||||
|
# =============================================================================
|
||||||
|
# Runs directly on the VPS via a labeled self-hosted act_runner.
|
||||||
|
# This job is skipped if DEPLOY_MODE != local-runner.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
jobs:
|
||||||
|
deploy-local-runner:
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# DISABLED: Runner 'deploy-ovh' is not yet configured.
|
||||||
|
# To re-enable:
|
||||||
|
# 1. Remove the 'if: false' line below
|
||||||
|
# 2. Change runs-on back to your runner label (e.g. deploy-ovh)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
if: false
|
||||||
|
runs-on: ubuntu-latest # placeholder — real label: 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 "=============================="
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Docker requires all image references to be lowercase
|
||||||
|
FINAL_OWNER="$(echo "$FINAL_OWNER" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
FINAL_NAME="$(echo "$FINAL_NAME" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
|
||||||
|
# Construct the full image reference (without tag)
|
||||||
|
IMAGE_REF="${REGISTRY_HOST}/${FINAL_OWNER}/${FINAL_NAME}"
|
||||||
|
echo "Image reference: ${IMAGE_REF}:<tag>"
|
||||||
|
|
||||||
|
echo "IMAGE_REF=${IMAGE_REF}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Step 6: Determine tags based on trigger and strategy
|
||||||
|
#
|
||||||
|
# Tag rules:
|
||||||
|
# - PR: build only, tag = "pr-<number>" (local only)
|
||||||
|
# - Push to branch: tag = branch name (e.g., "main")
|
||||||
|
# - Tag v1.2.3: tag = "1.2.3"; also "latest" if strategy includes it
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
- name: Determine tags
|
||||||
|
if: env.SKIP_DOCKER != 'true'
|
||||||
|
run: |
|
||||||
|
TAGS=""
|
||||||
|
SHOULD_PUSH=false
|
||||||
|
REF="${GITHUB_REF:-}"
|
||||||
|
|
||||||
|
echo "Event: ${{ github.event_name }}"
|
||||||
|
echo "Ref: ${REF}"
|
||||||
|
|
||||||
|
# --- Pull Request: build only ---
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
TAGS="${IMAGE_REF}:pr-${{ github.event.number }}"
|
||||||
|
SHOULD_PUSH=false
|
||||||
|
echo "PR build — will NOT push"
|
||||||
|
|
||||||
|
# --- Tag push (v*) ---
|
||||||
|
elif echo "${REF}" | grep -qE '^refs/tags/v'; then
|
||||||
|
# Extract version: refs/tags/v1.2.3 → 1.2.3
|
||||||
|
VERSION="$(echo "${REF}" | sed 's|refs/tags/v||')"
|
||||||
|
TAGS="${IMAGE_REF}:${VERSION}"
|
||||||
|
|
||||||
|
# Optionally add :latest
|
||||||
|
if echo "$DOCKER_TAG_STRATEGY" | grep -qi 'latest'; then
|
||||||
|
TAGS="${TAGS},${IMAGE_REF}:latest"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DOCKER_PUSH" = "true" ] && [ "$DOCKER_PUSH_ON_TAG" = "true" ]; then
|
||||||
|
SHOULD_PUSH=true
|
||||||
|
fi
|
||||||
|
echo "Tag push — version=${VERSION}, push=${SHOULD_PUSH}"
|
||||||
|
|
||||||
|
# --- Branch push ---
|
||||||
|
elif echo "${REF}" | grep -q '^refs/heads/'; then
|
||||||
|
BRANCH="$(echo "${REF}" | sed 's|refs/heads/||')"
|
||||||
|
TAGS="${IMAGE_REF}:${BRANCH}"
|
||||||
|
|
||||||
|
if [ "$DOCKER_PUSH" = "true" ] && [ "$DOCKER_PUSH_ON_BRANCH" = "true" ]; then
|
||||||
|
SHOULD_PUSH=true
|
||||||
|
fi
|
||||||
|
echo "Branch push — branch=${BRANCH}, push=${SHOULD_PUSH}"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Unknown ref type: ${REF}. Building with tag 'dev'."
|
||||||
|
TAGS="${IMAGE_REF}:dev"
|
||||||
|
SHOULD_PUSH=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "DOCKER_TAGS=${TAGS}" >> "$GITHUB_ENV"
|
||||||
|
echo "SHOULD_PUSH=${SHOULD_PUSH}" >> "$GITHUB_ENV"
|
||||||
|
echo "Final tags: ${TAGS}"
|
||||||
|
echo "Will push: ${SHOULD_PUSH}"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Step 7: Build the Docker image
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
- name: Build Docker image
|
||||||
|
if: env.SKIP_DOCKER != 'true'
|
||||||
|
run: |
|
||||||
|
if [ "$DOCKER_MODE" = "dockerfile" ]; then
|
||||||
|
# Build with the first tag; additional tags are added after
|
||||||
|
PRIMARY_TAG="$(echo "$DOCKER_TAGS" | cut -d',' -f1)"
|
||||||
|
echo ">>> docker build -t ${PRIMARY_TAG} ."
|
||||||
|
docker build -t "${PRIMARY_TAG}" .
|
||||||
|
|
||||||
|
# Tag additional images if present
|
||||||
|
IFS=',' read -ra TAG_ARRAY <<< "$DOCKER_TAGS"
|
||||||
|
for tag in "${TAG_ARRAY[@]:1}"; do
|
||||||
|
echo ">>> docker tag ${PRIMARY_TAG} ${tag}"
|
||||||
|
docker tag "${PRIMARY_TAG}" "${tag}"
|
||||||
|
done
|
||||||
|
|
||||||
|
elif [ "$DOCKER_MODE" = "compose" ]; then
|
||||||
|
echo ">>> docker compose build"
|
||||||
|
docker compose build
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Step 8: Login to registry (only when pushing)
|
||||||
|
#
|
||||||
|
# Uses PAT-based auth. Requires secrets:
|
||||||
|
# REGISTRY_USERNAME — Gitea username or bot
|
||||||
|
# REGISTRY_TOKEN — PAT with package:write scope
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
- name: Login to container registry
|
||||||
|
if: env.SKIP_DOCKER != 'true' && env.SHOULD_PUSH == 'true'
|
||||||
|
run: |
|
||||||
|
echo "Logging in to ${REGISTRY_HOST}..."
|
||||||
|
# Use --password-stdin to avoid leaking the token in process list
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | \
|
||||||
|
docker login "${REGISTRY_HOST}" \
|
||||||
|
-u "${{ secrets.REGISTRY_USERNAME }}" \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Step 9: Push Docker image(s)
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
- name: Push Docker image
|
||||||
|
if: env.SKIP_DOCKER != 'true' && env.SHOULD_PUSH == 'true'
|
||||||
|
run: |
|
||||||
|
IFS=',' read -ra TAG_ARRAY <<< "$DOCKER_TAGS"
|
||||||
|
for tag in "${TAG_ARRAY[@]}"; do
|
||||||
|
echo ">>> docker push ${tag}"
|
||||||
|
docker push "${tag}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Step 10: Summary
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
- name: Docker Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "=============================="
|
||||||
|
echo " Docker Workflow Complete"
|
||||||
|
echo " Mode: ${DOCKER_MODE:-skipped}"
|
||||||
|
echo " Tags: ${DOCKER_TAGS:-none}"
|
||||||
|
echo " Pushed: ${SHOULD_PUSH:-false}"
|
||||||
|
echo "=============================="
|
||||||
@@ -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 "=============================="
|
||||||
@@ -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 "=============================="
|
||||||
+48
-25
@@ -1,41 +1,64 @@
|
|||||||
# Build output
|
# =============================================================================
|
||||||
|
# .gitignore — Python + Node + General
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---- Python ----
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
dist/
|
dist/
|
||||||
.astro/
|
build/
|
||||||
|
.eggs/
|
||||||
|
*.whl
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
|
||||||
# Dependencies
|
# ---- Node / JavaScript ----
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Logs
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
dist/
|
||||||
|
.next/
|
||||||
|
.nuxt/
|
||||||
|
.output/
|
||||||
|
|
||||||
# Environment variables
|
# ---- Docker ----
|
||||||
.env
|
# Don't ignore Dockerfile or docker-compose.yml themselves
|
||||||
.env.local
|
*.tar
|
||||||
.env.production
|
docker-compose.override.yml
|
||||||
.env.*.local
|
|
||||||
|
|
||||||
# macOS
|
# ---- IDEs / Editors ----
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
|
.DS_Store
|
||||||
# OS
|
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Package lock files (keep package-lock.json but ignore others)
|
# ---- OS ----
|
||||||
yarn.lock
|
*.log
|
||||||
pnpm-lock.yaml
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
# Local development
|
# ---- Environment / Secrets ----
|
||||||
*.local
|
.env
|
||||||
|
.env.*
|
||||||
# Build artifacts
|
!.env.example
|
||||||
*.tsbuildinfo
|
*.pem
|
||||||
|
*.key
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# CODEOWNERS — Optional
|
||||||
|
# =============================================================================
|
||||||
|
# Gitea supports CODEOWNERS for automatic review assignment.
|
||||||
|
# Uncomment and customize the lines below.
|
||||||
|
#
|
||||||
|
# Format: <pattern> <@user-or-team> [<@user-or-team> ...]
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# * @default-reviewer
|
||||||
|
# /docs/ @docs-team
|
||||||
|
# *.py @python-team
|
||||||
|
# .gitea/ @devops-team
|
||||||
|
# .ci/ @devops-team
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
+31
@@ -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.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- What does this PR do? Why is it needed? -->
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
## Related Issues
|
||||||
|
|
||||||
|
<!-- Link related issues: Closes #123, Fixes #456 -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have tested my changes locally
|
||||||
|
- [ ] Linting passes (`make lint`)
|
||||||
|
- [ ] Tests pass (`make test`)
|
||||||
|
- [ ] Documentation updated (if applicable)
|
||||||
|
- [ ] No secrets or credentials are committed
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user