diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000..97a8bd46 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,36 @@ +# Codecov configuration - require 75% overall coverage by default +# Adjust target as needed + +coverage: + status: + project: + default: + target: 75% + threshold: 0% + +# Fail CI if Codecov upload/report indicates a problem +require_ci_to_pass: yes + +# Exclude folders from Codecov +ignore: + - "**/tests/*" + - "**/test/*" + - "**/__tests__/*" + - "**/test_*.go" + - "**/*_test.go" + - "**/*.test.ts" + - "**/*.test.tsx" + - "docs/*" + - ".github/*" + - "scripts/*" + - "tools/*" + - "frontend/node_modules/*" + - "frontend/dist/*" + - "frontend/coverage/*" + - "backend/cmd/seed/*" + - "backend/cmd/api/*" + - "backend/data/*" + - "backend/coverage/*" + - "backend/internal/services/docker_service.go" + - "backend/internal/api/handlers/docker_handler.go" + - "*.md" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..83d95583 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,82 @@ +# Version control +.git +.gitignore +.github/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +env/ +ENV/ +.pytest_cache/ +.coverage +*.cover +.hypothesis/ +htmlcov/ +*.egg-info/ + +# Node/Frontend build artifacts +frontend/node_modules/ +frontend/coverage/ +frontend/coverage.out +frontend/dist/ +frontend/.vite/ +frontend/*.tsbuildinfo +frontend/frontend/ + +# Go/Backend +backend/coverage.txt +backend/*.out +backend/coverage/ +backend/coverage.*.out +backend/package.json +backend/package-lock.json + +# Databases (runtime) +backend/data/*.db +backend/cmd/api/data/*.db +*.sqlite +*.sqlite3 + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +.trivy_logs +*.log +logs/ + +# Environment +.env +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# Documentation +docs/ +*.md +!README.md + +# Docker +docker-compose*.yml +**/Dockerfile.* + +# CI/CD +.github/ +.pre-commit-config.yaml + +# Scripts +scripts/ +tools/ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..706def22 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms +github: Wikid82 +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: Wikid82 +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/alpha-feature.yml b/.github/ISSUE_TEMPLATE/alpha-feature.yml new file mode 100644 index 00000000..51d0cc0d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/alpha-feature.yml @@ -0,0 +1,93 @@ +name: ποΈ Alpha Feature +description: Create an issue for an Alpha milestone feature +title: "[ALPHA] " +labels: ["alpha", "feature"] +body: + - type: markdown + attributes: + value: | + ## Alpha Milestone Feature + Features that are part of the core foundation and initial release. + + - type: dropdown + id: priority + attributes: + label: Priority + description: How critical is this feature? + options: + - Critical (Blocking, must-have) + - High (Important, should have) + - Medium (Nice to have) + - Low (Future enhancement) + validations: + required: true + + - type: input + id: issue_number + attributes: + label: Planning Issue Number + description: Reference number from PROJECT_PLANNING.md (e.g., Issue #5) + placeholder: "Issue #" + validations: + required: false + + - type: textarea + id: description + attributes: + label: Feature Description + description: What should this feature do? + placeholder: Describe the feature in detail + validations: + required: true + + - type: textarea + id: tasks + attributes: + label: Implementation Tasks + description: List of tasks to complete this feature + placeholder: | + - [ ] Task 1 + - [ ] Task 2 + - [ ] Task 3 + value: | + - [ ] + validations: + required: true + + - type: textarea + id: acceptance + attributes: + label: Acceptance Criteria + description: How do we know this feature is complete? + placeholder: | + - [ ] Criteria 1 + - [ ] Criteria 2 + value: | + - [ ] + validations: + required: true + + - type: checkboxes + id: categories + attributes: + label: Categories + description: Select all that apply + options: + - label: Backend + - label: Frontend + - label: Database + - label: Caddy Integration + - label: Security + - label: SSL/TLS + - label: UI/UX + - label: Deployment + - label: Documentation + + - type: textarea + id: technical_notes + attributes: + label: Technical Notes + description: Any technical considerations or dependencies? + placeholder: Libraries, APIs, or other issues that need to be completed first + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/beta-monitoring-feature.yml b/.github/ISSUE_TEMPLATE/beta-monitoring-feature.yml new file mode 100644 index 00000000..b1965956 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/beta-monitoring-feature.yml @@ -0,0 +1,118 @@ +name: π Beta Monitoring Feature +description: Create an issue for a Beta milestone monitoring/logging feature +title: "[BETA] [MONITORING] " +labels: ["beta", "feature", "monitoring"] +body: + - type: markdown + attributes: + value: | + ## Beta Monitoring & Logging Feature + Features related to observability, logging, and system monitoring. + + - type: dropdown + id: priority + attributes: + label: Priority + description: How critical is this monitoring feature? + options: + - Critical (Essential for operations) + - High (Important visibility) + - Medium (Enhanced monitoring) + - Low (Nice-to-have metrics) + validations: + required: true + + - type: dropdown + id: monitoring_type + attributes: + label: Monitoring Type + description: What aspect of monitoring? + options: + - Dashboards & Statistics + - Log Viewing & Search + - Alerting & Notifications + - CrowdSec Dashboard + - Analytics Integration + - Health Checks + - Performance Metrics + validations: + required: true + + - type: input + id: issue_number + attributes: + label: Planning Issue Number + description: Reference number from PROJECT_PLANNING.md (e.g., Issue #23) + placeholder: "Issue #" + validations: + required: false + + - type: textarea + id: description + attributes: + label: Feature Description + description: What monitoring/logging capability should this provide? + placeholder: Describe what users will be able to see or do + validations: + required: true + + - type: textarea + id: metrics + attributes: + label: Metrics & Data Points + description: What data will be collected and displayed? + placeholder: | + - Metric 1: Description + - Metric 2: Description + validations: + required: false + + - type: textarea + id: tasks + attributes: + label: Implementation Tasks + description: List of tasks to complete this feature + placeholder: | + - [ ] Task 1 + - [ ] Task 2 + - [ ] Task 3 + value: | + - [ ] + validations: + required: true + + - type: textarea + id: acceptance + attributes: + label: Acceptance Criteria + description: How do we verify this monitoring feature works? + placeholder: | + - [ ] Data displays correctly + - [ ] Updates in real-time + - [ ] Performance is acceptable + value: | + - [ ] + validations: + required: true + + - type: checkboxes + id: categories + attributes: + label: Implementation Areas + description: Select all that apply + options: + - label: Backend (Data collection) + - label: Frontend (UI/Charts) + - label: Database (Storage) + - label: Real-time Updates (WebSocket) + - label: External Integration (GoAccess, CrowdSec) + - label: Documentation Required + + - type: textarea + id: ui_design + attributes: + label: UI/UX Considerations + description: Describe the user interface requirements + placeholder: Layout, charts, filters, export options, etc. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/beta-security-feature.yml b/.github/ISSUE_TEMPLATE/beta-security-feature.yml new file mode 100644 index 00000000..d28c9d0d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/beta-security-feature.yml @@ -0,0 +1,116 @@ +name: π Beta Security Feature +description: Create an issue for a Beta milestone security feature +title: "[BETA] [SECURITY] " +labels: ["beta", "feature", "security"] +body: + - type: markdown + attributes: + value: | + ## Beta Security Feature + Advanced security features for the beta release. + + - type: dropdown + id: priority + attributes: + label: Priority + description: How critical is this security feature? + options: + - Critical (Essential security control) + - High (Important protection) + - Medium (Additional hardening) + - Low (Nice-to-have security enhancement) + validations: + required: true + + - type: dropdown + id: security_category + attributes: + label: Security Category + description: What type of security feature is this? + options: + - Authentication & Access Control + - Threat Protection + - SSL/TLS Management + - Monitoring & Logging + - Web Application Firewall + - Rate Limiting + - IP Access Control + validations: + required: true + + - type: input + id: issue_number + attributes: + label: Planning Issue Number + description: Reference number from PROJECT_PLANNING.md (e.g., Issue #15) + placeholder: "Issue #" + validations: + required: false + + - type: textarea + id: description + attributes: + label: Feature Description + description: What security capability should this provide? + placeholder: Describe the security feature and its purpose + validations: + required: true + + - type: textarea + id: threat_model + attributes: + label: Threat Model + description: What threats does this feature mitigate? + placeholder: | + - Threat 1: Description and severity + - Threat 2: Description and severity + validations: + required: false + + - type: textarea + id: tasks + attributes: + label: Implementation Tasks + description: List of tasks to complete this feature + placeholder: | + - [ ] Task 1 + - [ ] Task 2 + - [ ] Task 3 + value: | + - [ ] + validations: + required: true + + - type: textarea + id: acceptance + attributes: + label: Acceptance Criteria + description: How do we verify this security control works? + placeholder: | + - [ ] Security test 1 + - [ ] Security test 2 + value: | + - [ ] + validations: + required: true + + - type: checkboxes + id: special_labels + attributes: + label: Special Categories + description: Select all that apply + options: + - label: SSO (Single Sign-On) + - label: WAF (Web Application Firewall) + - label: CrowdSec Integration + - label: Plus Feature (Premium) + - label: Requires Documentation + + - type: textarea + id: security_testing + attributes: + label: Security Testing Plan + description: How will you test this security feature? + placeholder: Describe testing approach, tools, and scenarios + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/general-feature.yml b/.github/ISSUE_TEMPLATE/general-feature.yml new file mode 100644 index 00000000..497d7735 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general-feature.yml @@ -0,0 +1,97 @@ +name: βοΈ General Feature +description: Create a feature request for any milestone +title: "[FEATURE] " +labels: ["feature"] +body: + - type: markdown + attributes: + value: | + ## Feature Request + Request a new feature or enhancement for CaddyProxyManager+ + + - type: dropdown + id: milestone + attributes: + label: Target Milestone + description: Which release should this be part of? + options: + - Alpha (Core foundation) + - Beta (Advanced features) + - Post-Beta (Future enhancements) + - Unsure (Help me decide) + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this feature? + options: + - Critical + - High + - Medium + - Low + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: What problem does this feature solve? + placeholder: Describe the use case or pain point + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: How should this feature work? + placeholder: Describe your ideal implementation + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: What other approaches could solve this? + placeholder: List alternative solutions you've thought about + validations: + required: false + + - type: textarea + id: user_story + attributes: + label: User Story + description: Describe this from a user's perspective + placeholder: "As a [user type], I want to [action] so that [benefit]" + validations: + required: false + + - type: checkboxes + id: categories + attributes: + label: Feature Categories + description: Select all that apply + options: + - label: Authentication/Authorization + - label: Security + - label: SSL/TLS + - label: Monitoring/Logging + - label: UI/UX + - label: Performance + - label: Documentation + - label: API + - label: Plus Feature (Premium) + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other information, screenshots, or examples? + placeholder: Add links, mockups, or references + validations: + required: false diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..9a11e9ff --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,43 @@ +# CaddyProxyManager+ Copilot Instructions + +## π¨ CRITICAL ARCHITECTURE RULES π¨ +- **Single Frontend Source**: All frontend code MUST reside in `frontend/`. NEVER create `backend/frontend/` or any other nested frontend directory. +- **Single Backend Source**: All backend code MUST reside in `backend/`. +- **No Python**: This is a Go (Backend) + React/TypeScript (Frontend) project. Do not introduce Python scripts or requirements. + +## Big Picture +- `backend/cmd/api` loads config, opens SQLite, then hands off to `internal/server` where routes from `internal/api/routes` are registered. +- `internal/config` respects `CPM_ENV`, `CPM_HTTP_PORT`, `CPM_DB_PATH`, `CPM_FRONTEND_DIR` and creates the `data/` directory; lean on these instead of hard-coded paths. +- All HTTP endpoints live under `/api/v1/*`; keep new handlers inside `internal/api/handlers` and register them via `routes.Register` so `db.AutoMigrate` runs for their models. +- `internal/server` also mounts the built React app (via `attachFrontend`) whenever `frontend/dist` exists, falling back to JSON `{"error": ...}` for any `/api/*` misses. +- Persistent types live in `internal/models`; GORM auto-migrates them each boot, so evolve schemas there before touching handlers or the frontend. + +## Backend Workflow +- Run locally with `cd backend && go run ./cmd/api`; run tests with `go test ./...` (see `proxy_host_handler_test.go` for the in-memory SQLite/Gin harness pattern). +- Handlers return structured errors using `gin.H{"error": "message"}` and standard HTTP codesβmirror the `ProxyHostHandler` lifecycle for new CRUD endpoints. +- UUIDs (`github.com/google/uuid`) are generated server-side and exposed as `uuid` fields; clients never send numeric IDs. +- Query lists sorted by `updated_at desc` (see `.Order("updated_at desc")` in `List`); match that ordering for user-visible collections. +- Long-running work must respect the graceful shutdown flow in `server.Run(ctx)`βavoid background goroutines that ignore the context. + +## Frontend Workflow +- **Location**: Always work within `frontend/`. +- **Stack**: React 18 + Vite + TypeScript + TanStack Query (React Query). +- **State Management**: Use `src/hooks/use*.ts` wrapping React Query. Do not use raw `useEffect` for data fetching. +- **API Layer**: Create typed API clients in `src/api/*.ts` that wrap `client.ts`. +- **Development**: Run `cd frontend && npm run dev`. Vite proxies `/api` to `http://localhost:8080`. +- **Components**: Screens live in `src/pages`. Reusable UI in `src/components`. +- **Forms**: Use local `useState` for form fields, submit via `useMutation` from custom hooks, then `invalidateQueries` on success. + +## Cross-Cutting Notes +- Run the backend before the frontend; React Query expects the exact JSON produced by GORM tags (snake_case), so keep API and UI field names aligned. +- When adding models, update both `internal/models` and the `AutoMigrate` call inside `internal/api/routes/routes.go`; register new Gin routes right after migrations for clarity. +- Tests belong beside handlers (`*_test.go`); reuse the `setupTestRouter` helper structure (in-memory SQLite, Gin router, httptest requests) for fast feedback. +- **Testing Requirement**: All new code (features, bug fixes, refactors) MUST include accompanying unit tests. Ensure tests cover happy paths and error conditions. +- **Ignore Files**: When creating new file types, directories, or build artifacts, ALWAYS check and update `.gitignore`, `.dockerignore`, and `.codecov.yml` to ensure they are properly excluded or included as required. +- The root `Dockerfile` builds the Go binary and the React static assets (multi-stage build). +- Branch from `feature/**` and target `development`. + +## CI/CD & Commit Conventions +- **Docker Builds**: The `docker-publish` workflow skips builds for commits starting with `chore:`. +- **Triggering Builds**: To ensure a new Docker image is built (e.g., for testing on VPS), use `feat:`, `fix:`, or `perf:` prefixes. +- **Beta Branch**: The `feature/beta-release` branch is configured to ALWAYS build, overriding the skip logic. diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..8302d397 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":semanticCommits", + ":separateMultipleMajorReleases", + "helpers:pinGitHubActionDigests" + ], + "baseBranches": ["development"], + "timezone": "UTC", + "dependencyDashboard": true, + "prConcurrentLimit": 10, + "prHourlyLimit": 5, + "labels": ["dependencies"], + "rebaseWhen": "conflicted", + "vulnerabilityAlerts": { "enabled": true }, + "schedule": ["every weekday"], + "rangeStrategy": "bump", + "packageRules": [ + { + "description": "Automerge safe patch updates", + "matchUpdateTypes": ["patch"], + "automerge": true + }, + { + "description": "Frontend npm: automerge minor for devDependencies", + "matchManagers": ["npm"], + "matchDepTypes": ["devDependencies"], + "matchUpdateTypes": ["minor", "patch"], + "automerge": true, + "labels": ["dependencies", "npm"] + }, + { + "description": "Backend Go modules", + "matchManagers": ["gomod"], + "labels": ["dependencies", "go"], + "matchUpdateTypes": ["minor", "patch"], + "automerge": false + }, + { + "description": "GitHub Actions updates", + "matchManagers": ["github-actions"], + "labels": ["dependencies", "github-actions"], + "matchUpdateTypes": ["minor", "patch"], + "automerge": true + }, + { + "description": "Docker: keep Caddy within v2 (no automatic jump to v3)", + "matchManagers": ["dockerfile"], + "matchPackageNames": ["caddy"], + "allowedVersions": "<3.0.0", + "labels": ["dependencies", "docker"], + "automerge": true + }, + { + "description": "Group non-breaking npm minor/patch", + "matchManagers": ["npm"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "npm minor/patch", + "prPriority": -1 + }, + { + "description": "Group docker base minor/patch", + "matchManagers": ["dockerfile"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "docker base updates", + "prPriority": -1 + } + ] +} diff --git a/.github/workflows/auto-add-to-project.yml b/.github/workflows/auto-add-to-project.yml new file mode 100644 index 00000000..78804bb8 --- /dev/null +++ b/.github/workflows/auto-add-to-project.yml @@ -0,0 +1,32 @@ +name: Auto-add issues and PRs to Project + +on: + issues: + types: [opened, reopened] + pull_request: + types: [opened, reopened] + +jobs: + add-to-project: + runs-on: ubuntu-latest + steps: + - name: Determine project URL presence + id: project_check + run: | + if [ -n "${{ secrets.PROJECT_URL }}" ]; then + echo "has_project=true" >> $GITHUB_OUTPUT + else + echo "has_project=false" >> $GITHUB_OUTPUT + fi + + - name: Add issue or PR to project + if: steps.project_check.outputs.has_project == 'true' + uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + continue-on-error: true + with: + project-url: ${{ secrets.PROJECT_URL }} + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + + - name: Skip summary + if: steps.project_check.outputs.has_project == 'false' + run: echo "PROJECT_URL secret missing; skipping project assignment." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml new file mode 100644 index 00000000..cdbafdbd --- /dev/null +++ b/.github/workflows/auto-label-issues.yml @@ -0,0 +1,74 @@ +name: Auto-label Issues + +on: + issues: + types: [opened, edited] + +jobs: + auto-label: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Auto-label based on title and body + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const issue = context.payload.issue; + const title = issue.title.toLowerCase(); + const body = issue.body ? issue.body.toLowerCase() : ''; + const labels = []; + + // Priority detection + if (title.includes('[critical]') || body.includes('priority: critical')) { + labels.push('critical'); + } else if (title.includes('[high]') || body.includes('priority: high')) { + labels.push('high'); + } else if (title.includes('[medium]') || body.includes('priority: medium')) { + labels.push('medium'); + } else if (title.includes('[low]') || body.includes('priority: low')) { + labels.push('low'); + } + + // Milestone detection + if (title.includes('[alpha]') || body.includes('milestone: alpha')) { + labels.push('alpha'); + } else if (title.includes('[beta]') || body.includes('milestone: beta')) { + labels.push('beta'); + } else if (title.includes('[post-beta]') || body.includes('milestone: post-beta')) { + labels.push('post-beta'); + } + + // Category detection + if (title.includes('architecture') || body.includes('architecture')) labels.push('architecture'); + if (title.includes('backend') || body.includes('backend')) labels.push('backend'); + if (title.includes('frontend') || body.includes('frontend')) labels.push('frontend'); + if (title.includes('security') || body.includes('security')) labels.push('security'); + if (title.includes('ssl') || title.includes('tls') || body.includes('certificate')) labels.push('ssl'); + if (title.includes('sso') || body.includes('single sign-on')) labels.push('sso'); + if (title.includes('waf') || body.includes('web application firewall')) labels.push('waf'); + if (title.includes('crowdsec') || body.includes('crowdsec')) labels.push('crowdsec'); + if (title.includes('caddy') || body.includes('caddy')) labels.push('caddy'); + if (title.includes('database') || body.includes('database')) labels.push('database'); + if (title.includes('ui') || title.includes('interface')) labels.push('ui'); + if (title.includes('docker') || title.includes('deployment')) labels.push('deployment'); + if (title.includes('monitoring') || title.includes('logging')) labels.push('monitoring'); + if (title.includes('documentation') || title.includes('docs')) labels.push('documentation'); + if (title.includes('test') || body.includes('testing')) labels.push('testing'); + if (title.includes('performance') || body.includes('optimization')) labels.push('performance'); + if (title.includes('plus') || body.includes('premium feature')) labels.push('plus'); + + // Feature detection + if (title.includes('feature') || body.includes('feature request')) labels.push('feature'); + + // Only add labels if we detected any + if (labels.length > 0) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: labels + }); + + console.log(`Added labels: ${labels.join(', ')}`); + } diff --git a/.github/workflows/caddy-major-monitor.yml b/.github/workflows/caddy-major-monitor.yml new file mode 100644 index 00000000..74a1921b --- /dev/null +++ b/.github/workflows/caddy-major-monitor.yml @@ -0,0 +1,62 @@ +name: Monitor Caddy Major Release + +on: + schedule: + - cron: '17 7 * * 1' # Mondays at 07:17 UTC + workflow_dispatch: {} + +permissions: + contents: read + issues: write + +jobs: + check-caddy-major: + runs-on: ubuntu-latest + steps: + - name: Check for Caddy v3 and open issue + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const upstream = { owner: 'caddyserver', repo: 'caddy' }; + const { data: releases } = await github.rest.repos.listReleases({ + ...upstream, + per_page: 50, + }); + const latestV3 = releases.find(r => /^v3\./.test(r.tag_name)); + if (!latestV3) { + core.info('No Caddy v3 release detected.'); + return; + } + + const issueTitle = `Track upgrade to Caddy v3 (${latestV3.tag_name})`; + + const { data: existing } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100, + }); + + if (existing.some(i => i.title === issueTitle)) { + core.info('Issue already exists β nothing to do.'); + return; + } + + const body = [ + 'Caddy v3 has been released upstream and detected by the scheduled monitor.', + '', + `Detected release: ${latestV3.tag_name} (${latestV3.html_url})`, + '', + '- Create a feature branch to evaluate the v3 migration.', + '- Review breaking changes and update Docker base images/workflows.', + '- Validate Trivy scans and update any policies as needed.', + '', + 'Current policy: remain on latest 2.x until v3 is validated.' + ].join('\n'); + + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: issueTitle, + body, + }); diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..d17a521a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +name: CodeQL - Analyze + +on: + push: + branches: [ main, development, 'feature/**' ] + pull_request: + branches: [ main, development ] + schedule: + - cron: '0 3 * * 1' + +permissions: + contents: read + security-events: write + actions: read + pull-requests: read + +jobs: + analyze: + name: CodeQL analysis (${{ matrix.language }}) + runs-on: ubuntu-latest + # Skip forked PRs where CPMP_TOKEN lacks security-events permissions + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false + permissions: + contents: read + security-events: write + actions: read + pull-requests: read + strategy: + fail-fast: false + matrix: + language: [ 'go', 'javascript-typescript' ] + steps: + - name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/create-labels.yml b/.github/workflows/create-labels.yml new file mode 100644 index 00000000..21670aac --- /dev/null +++ b/.github/workflows/create-labels.yml @@ -0,0 +1,78 @@ +name: Create Project Labels + +# This workflow only runs manually to set up labels +on: + workflow_dispatch: + +jobs: + create-labels: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Create all project labels + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const labels = [ + // Priority labels + { name: 'critical', color: 'B60205', description: 'Must have for the release, blocks other work' }, + { name: 'high', color: 'D93F0B', description: 'Important feature, should be included' }, + { name: 'medium', color: 'FBCA04', description: 'Nice to have, can be deferred' }, + { name: 'low', color: '0E8A16', description: 'Future enhancement, not urgent' }, + + // Milestone labels + { name: 'alpha', color: '5319E7', description: 'Part of initial alpha release' }, + { name: 'beta', color: '0052CC', description: 'Part of beta release' }, + { name: 'post-beta', color: '006B75', description: 'Post-beta enhancement' }, + + // Category labels + { name: 'architecture', color: 'C5DEF5', description: 'System design and structure' }, + { name: 'backend', color: '1D76DB', description: 'Server-side code' }, + { name: 'frontend', color: '5EBEFF', description: 'UI/UX code' }, + { name: 'feature', color: 'A2EEEF', description: 'New functionality' }, + { name: 'security', color: 'EE0701', description: 'Security-related' }, + { name: 'ssl', color: 'F9D0C4', description: 'SSL/TLS certificates' }, + { name: 'sso', color: 'D4C5F9', description: 'Single Sign-On' }, + { name: 'waf', color: 'B60205', description: 'Web Application Firewall' }, + { name: 'crowdsec', color: 'FF6B6B', description: 'CrowdSec integration' }, + { name: 'caddy', color: '1F6FEB', description: 'Caddy-specific' }, + { name: 'database', color: '006B75', description: 'Database-related' }, + { name: 'ui', color: '7057FF', description: 'User interface' }, + { name: 'deployment', color: '0E8A16', description: 'Docker, installation' }, + { name: 'monitoring', color: 'FEF2C0', description: 'Logging and statistics' }, + { name: 'documentation', color: '0075CA', description: 'Docs and guides' }, + { name: 'testing', color: 'BFD4F2', description: 'Test suite' }, + { name: 'performance', color: 'EDEDED', description: 'Optimization' }, + { name: 'community', color: 'D876E3', description: 'Community building' }, + { name: 'plus', color: 'FFD700', description: 'Premium/"Plus" feature' }, + { name: 'enterprise', color: '8B4513', description: 'Enterprise-grade feature' } + ]; + + for (const label of labels) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + console.log(`β Created label: ${label.name}`); + } catch (error) { + if (error.status === 422) { + console.log(`β Label already exists: ${label.name}`); + // Update the label if it exists + await github.rest.issues.updateLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label.name, + color: label.color, + description: label.description + }); + console.log(`β Updated label: ${label.name}`); + } else { + console.error(`β Error creating label ${label.name}:`, error.message); + } + } + } diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..a10b7079 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,252 @@ +name: Docker Build, Publish & Test + +on: + push: + branches: + - main + - development + - feature/beta-release + tags: + - 'v*.*.*' + pull_request: + branches: + - main + - development + - feature/beta-release + workflow_dispatch: + workflow_call: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/cpmp + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + security-events: write + + outputs: + skip_build: ${{ steps.skip.outputs.skip_build }} + digest: ${{ steps.build-and-push.outputs.digest }} + + steps: + - name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + + - name: Normalize image name + run: | + echo "IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Determine skip condition + id: skip + env: + ACTOR: ${{ github.actor }} + EVENT: ${{ github.event_name }} + HEAD_MSG: ${{ github.event.head_commit.message }} + REF: ${{ github.ref }} + run: | + should_skip=false + pr_title="" + if [ "$EVENT" = "pull_request" ]; then + pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '') + fi + if [ "$ACTOR" = "renovate[bot]" ]; then should_skip=true; fi + if echo "$HEAD_MSG" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi + if echo "$HEAD_MSG" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi + if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi + if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi + + # Always build on beta-release branch to ensure artifacts for testing + if [[ "$REF" == "refs/heads/feature/beta-release" ]]; then + should_skip=false + echo "Force building on beta-release branch" + fi + + echo "skip_build=$should_skip" >> $GITHUB_OUTPUT + + - name: Set up QEMU + if: steps.skip.outputs.skip_build != 'true' + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + - name: Set up Docker Buildx + if: steps.skip.outputs.skip_build != 'true' + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + - name: Resolve Caddy base digest + if: steps.skip.outputs.skip_build != 'true' + id: caddy + run: | + docker pull caddy:2-alpine + DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine) + echo "image=$DIGEST" >> $GITHUB_OUTPUT + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.CPMP_TOKEN }} + + - name: Extract metadata (tags, labels) + if: steps.skip.outputs.skip_build != 'true' + id: meta + uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} + type=raw,value=beta,enable=${{ github.ref == 'refs/heads/feature/beta-release' }} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }} + type=sha,format=short,enable=${{ github.event_name != 'pull_request' }} + + - name: Build and push Docker image + if: steps.skip.outputs.skip_build != 'true' + id: build-and-push + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 + with: + context: . + platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + VERSION=${{ steps.meta.outputs.version }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} + VCS_REF=${{ github.sha }} + CADDY_IMAGE=${{ steps.caddy.outputs.image }} + + - name: Run Trivy scan (table output) + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} + format: 'table' + severity: 'CRITICAL,HIGH' + exit-code: '0' + continue-on-error: true + + - name: Run Trivy vulnerability scanner (SARIF) + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + id: trivy + uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + continue-on-error: true + + - name: Check Trivy SARIF exists + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + id: trivy-check + run: | + if [ -f trivy-results.sarif ]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Upload Trivy results + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true' + uses: github/codeql-action/upload-sarif@d3ced5c96c16c4332e2a61eb6f3649d6f1b20bb8 # v3.31.5 + with: + sarif_file: 'trivy-results.sarif' + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Create summary + if: steps.skip.outputs.skip_build != 'true' + run: | + echo "## π Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### π¦ Image Details" >> $GITHUB_STEP_SUMMARY + echo "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY + echo "- **Repository**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY + echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + test-image: + name: Test Docker Image + needs: build-and-push + runs-on: ubuntu-latest + if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request' + + steps: + - name: Normalize image name + run: | + raw="${{ github.repository_owner }}/${{ github.event.repository.name }}" + echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV + + - name: Determine image tag + id: tag + run: | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "tag=latest" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then + echo "tag=dev" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then + echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + else + echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT + fi + + - name: Log in to GitHub Container Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.CPMP_TOKEN }} + + - name: Pull Docker image + run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} + + - name: Run container + run: | + docker run -d \ + --name test-container \ + -p 8080:8080 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} + + - name: Test health endpoint (retries) + run: | + set +e + for i in $(seq 1 30); do + code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000") + if [ "$code" = "200" ]; then + echo "β Health check passed on attempt $i" + exit 0 + fi + echo "Attempt $i/30: health not ready (code=$code); waiting..." + sleep 2 + done + echo "β Health check failed after retries" + docker logs test-container || true + exit 1 + + - name: Check container logs + if: always() + run: docker logs test-container + + - name: Stop container + if: always() + run: docker stop test-container && docker rm test-container + + - name: Create test summary + if: always() + run: | + echo "## π§ͺ Docker Image Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY + echo "- **Health Check**: ${{ job.status == 'success' && 'β Passed' || 'β Failed' }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..c37e3a18 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,353 @@ +name: Deploy Documentation to GitHub Pages + +on: + push: + branches: + - main # Deploy docs when pushing to main + paths: + - 'docs/**' # Only run if docs folder changes + - 'README.md' # Or if README changes + - '.github/workflows/docs.yml' # Or if this workflow changes + workflow_dispatch: # Allow manual trigger + +# Sets permissions to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + name: Build Documentation + runs-on: ubuntu-latest + + steps: + # Step 1: Get the code + - name: π₯ Checkout code + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 + + # Step 2: Set up Node.js (for building any JS-based doc tools) + - name: π§ Set up Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 + with: + node-version: '24.11.1' + + # Step 3: Create a beautiful docs site structure + - name: π Build documentation site + run: | + # Create output directory + mkdir -p _site + + # Copy all markdown files + cp README.md _site/ + cp -r docs _site/ + + # Create a simple HTML index that looks nice + cat > _site/index.html << 'EOF' + + +
+ + +Make your websites easy to reach - No coding required!
++ This documentation will help you get started with Caddy Proxy Manager Plus. + Whether you're a complete beginner or an experienced developer, we've got you covered! +
+Your first setup in just 5 minutes! We'll walk you through everything step by step.
+ Read the Guide β +Learn what the app does, how to install it, and see examples of what you can build.
+ Read More β +Already using Caddy? Learn how to bring your existing configuration into the app.
+ Import Your Configs β +Complete REST API documentation with examples in JavaScript and Python.
+ View API Docs β +Understand how data is stored, relationships, and backup strategies.
+ View Schema β +Want to help make this better? Learn how to contribute code, docs, or ideas.
+ Start Contributing β +Browse all available documentation organized by topic and skill level.
+ View Full Index β ++ Stuck? Have questions? We're here to help! +
+ +