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' + + + + + + Caddy Proxy Manager Plus - Documentation + + + + +
+

πŸš€ Caddy Proxy Manager Plus

+

Make your websites easy to reach - No coding required!

+
+ +
+
+

πŸ‘‹ Welcome!

+

+ 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! +

+
+ +

πŸ“š Getting Started

+
+
+

🏠 Getting Started Guide Start Here

+

Your first setup in just 5 minutes! We'll walk you through everything step by step.

+ Read the Guide β†’ +
+ +
+

πŸ“– README Essential

+

Learn what the app does, how to install it, and see examples of what you can build.

+ Read More β†’ +
+ +
+

πŸ“₯ Import Guide

+

Already using Caddy? Learn how to bring your existing configuration into the app.

+ Import Your Configs β†’ +
+
+ +

πŸ”§ Developer Documentation

+
+
+

πŸ”Œ API Reference Advanced

+

Complete REST API documentation with examples in JavaScript and Python.

+ View API Docs β†’ +
+ +
+

πŸ’Ύ Database Schema Advanced

+

Understand how data is stored, relationships, and backup strategies.

+ View Schema β†’ +
+ +
+

✨ Contributing Guide

+

Want to help make this better? Learn how to contribute code, docs, or ideas.

+ Start Contributing β†’ +
+
+ +

πŸ“‹ All Documentation

+
+

πŸ“š Documentation Index

+

Browse all available documentation organized by topic and skill level.

+ View Full Index β†’ +
+ +

πŸ†˜ Need Help?

+
+

Get Support

+

+ Stuck? Have questions? We're here to help! +

+
+ + πŸ’¬ Ask a Question + + + πŸ› Report a Bug + + + ⭐ View on GitHub + +
+
+
+ + + + + EOF + + # Convert markdown files to HTML using a simple converter + npm install -g marked + + # Convert each markdown file + for file in _site/docs/*.md; do + if [ -f "$file" ]; then + filename=$(basename "$file" .md) + marked "$file" -o "_site/docs/${filename}.html" --gfm + fi + done + + # Convert README and CONTRIBUTING + marked _site/README.md -o _site/README.html --gfm + if [ -f "CONTRIBUTING.md" ]; then + cp CONTRIBUTING.md _site/ + marked _site/CONTRIBUTING.md -o _site/CONTRIBUTING.html --gfm + fi + + # Add simple styling to all HTML files + for html_file in _site/*.html _site/docs/*.html; do + if [ -f "$html_file" ] && [ "$html_file" != "_site/index.html" ]; then + # Add a header with navigation to each page + temp_file="${html_file}.tmp" + cat > "$temp_file" << 'HEADER' + + + + + + Caddy Proxy Manager Plus - Documentation + + + + + +
+ HEADER + + # Append original content + cat "$html_file" >> "$temp_file" + + # Add footer + cat >> "$temp_file" << 'FOOTER' +
+ + + + FOOTER + + mv "$temp_file" "$html_file" + fi + done + + echo "βœ… Documentation site built successfully!" + + # Step 4: Upload the built site + - name: πŸ“€ Upload artifact + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 + with: + path: '_site' + + deploy: + name: Deploy to GitHub Pages + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + + steps: + # Deploy to GitHub Pages + - name: πŸš€ Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 + + # Create a summary + - name: πŸ“‹ Create deployment summary + run: | + echo "## πŸŽ‰ Documentation Deployed!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Your documentation is now live at:" >> $GITHUB_STEP_SUMMARY + echo "πŸ”— ${{ steps.deployment.outputs.page_url }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### πŸ“š What's Included" >> $GITHUB_STEP_SUMMARY + echo "- Getting Started Guide" >> $GITHUB_STEP_SUMMARY + echo "- Complete README" >> $GITHUB_STEP_SUMMARY + echo "- API Documentation" >> $GITHUB_STEP_SUMMARY + echo "- Database Schema" >> $GITHUB_STEP_SUMMARY + echo "- Import Guide" >> $GITHUB_STEP_SUMMARY + echo "- Contributing Guidelines" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/propagate-changes.yml b/.github/workflows/propagate-changes.yml new file mode 100644 index 00000000..d7238ba8 --- /dev/null +++ b/.github/workflows/propagate-changes.yml @@ -0,0 +1,106 @@ +name: Propagate Changes Between Branches + +on: + push: + branches: + - main + - development + +permissions: + contents: write + pull-requests: write + +jobs: + propagate: + name: Create PR to synchronize branches + runs-on: ubuntu-latest + if: github.actor != 'github-actions[bot]' && github.event.pusher != null + steps: + - name: Set up Node (for github-script) + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 + with: + node-version: '24.11.1' + + - name: Propagate Changes + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const currentBranch = context.ref.replace('refs/heads/', ''); + + async function createPR(src, base) { + if (src === base) return; + + core.info(`Checking propagation from ${src} to ${base}...`); + + // Check for existing open PRs + const { data: pulls } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${src}`, + base: base, + }); + + if (pulls.length > 0) { + core.info(`Existing PR found for ${src} -> ${base}. Skipping.`); + return; + } + + // Compare commits to see if src is ahead of base + try { + const compare = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: base, + head: src, + }); + + // If src is not ahead, nothing to merge + if (compare.data.ahead_by === 0) { + core.info(`${src} is not ahead of ${base}. No propagation needed.`); + return; + } + } catch (error) { + // If base branch doesn't exist, etc. + core.warning(`Error comparing ${src} to ${base}: ${error.message}`); + return; + } + + // Create PR + try { + const pr = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Propagate changes from ${src} into ${base}`, + head: src, + base: base, + body: `Automated PR to propagate changes from ${src} into ${base}.\n\nTriggered by push to ${currentBranch}.`, + }); + core.info(`Created PR #${pr.data.number} to merge ${src} into ${base}`); + } catch (error) { + core.warning(`Failed to create PR from ${src} to ${base}: ${error.message}`); + } + } + + if (currentBranch === 'main') { + // Main -> Development + await createPR('main', 'development'); + } else if (currentBranch === 'development') { + // Development -> Feature branches + const branches = await github.paginate(github.rest.repos.listBranches, { + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const featureBranches = branches + .map(b => b.name) + .filter(name => name.startsWith('feature/')); + + core.info(`Found ${featureBranches.length} feature branches: ${featureBranches.join(', ')}`); + + for (const featureBranch of featureBranches) { + await createPR('development', featureBranch); + } + } + env: + CPMP_TOKEN: ${{ secrets.CPMP_TOKEN }} diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml new file mode 100644 index 00000000..ec51014a --- /dev/null +++ b/.github/workflows/quality-checks.yml @@ -0,0 +1,74 @@ +name: Quality Checks + +on: + push: + branches: [ main, development, 'feature/**' ] + pull_request: + branches: [ main, development ] + +jobs: + backend-quality: + name: Backend (Go) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + + - name: Set up Go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + with: + go-version: '1.25.4' + cache-dependency-path: backend/go.sum + + - name: Run Go tests + working-directory: backend + run: go test -v -coverprofile=coverage.out ./... + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./backend/coverage.out + flags: backend + fail_ci_if_error: true + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 + with: + version: latest + working-directory: backend + args: --timeout=5m + continue-on-error: true + + frontend-quality: + name: Frontend (React) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + + - name: Set up Node.js + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: '24.11.1' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Run frontend tests + working-directory: frontend + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: ./frontend/coverage + flags: frontend + fail_ci_if_error: true + + - name: Run frontend lint + working-directory: frontend + run: npm run lint + continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..aaf00d4a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,133 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +permissions: + contents: write + packages: write + +jobs: + build-frontend: + name: Build Frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version: '20.19.5' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install Dependencies + working-directory: frontend + run: npm ci + + - name: Build + working-directory: frontend + run: npm run build + + - name: Archive Frontend + working-directory: frontend + run: tar -czf ../frontend-dist.tar.gz dist/ + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: frontend-dist + path: frontend-dist.tar.gz + + build-backend: + name: Build Backend + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: '1.25.4' + + - name: Build + working-directory: backend + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 1 + run: | + # Install dependencies for CGO (sqlite) + if [ "${{ matrix.goarch }}" = "arm64" ]; then + sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu + export CC=aarch64-linux-gnu-gcc + fi + + go build -ldflags "-s -w -X github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/version.Version=${{ github.ref_name }}" -o ../cpmp-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/api + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: backend-${{ matrix.goos }}-${{ matrix.goarch }} + path: cpmp-${{ matrix.goos }}-${{ matrix.goarch }} + + build-caddy: + name: Build Caddy + runs-on: ubuntu-latest + strategy: + matrix: + goos: [linux] + goarch: [amd64, arm64] + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 + with: + go-version: '1.25.4' + + - name: Install xcaddy + run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + + - name: Build Caddy + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + xcaddy build v2.9.1 \ + --replace github.com/quic-go/quic-go=github.com/quic-go/quic-go@v0.49.1 \ + --replace golang.org/x/crypto=golang.org/x/crypto@v0.35.0 \ + --output caddy-${{ matrix.goos }}-${{ matrix.goarch }} + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: caddy-${{ matrix.goos }}-${{ matrix.goarch }} + path: caddy-${{ matrix.goos }}-${{ matrix.goarch }} + + create-release: + name: Create Release + needs: [build-frontend, build-backend, build-caddy] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: artifacts + + - name: Display structure of downloaded files + run: ls -R artifacts + + - name: Create GitHub Release + uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 + with: + files: | + artifacts/frontend-dist/frontend-dist.tar.gz + artifacts/backend-linux-amd64/cpmp-linux-amd64 + artifacts/backend-linux-arm64/cpmp-linux-arm64 + artifacts/caddy-linux-amd64/caddy-linux-amd64 + artifacts/caddy-linux-arm64/caddy-linux-arm64 + generate_release_notes: true + prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} + token: ${{ secrets.CPMP_TOKEN }} + + build-and-publish: + needs: create-release + uses: ./.github/workflows/docker-publish.yml # Reusable workflow present; path validated + secrets: inherit diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 00000000..d5e2b7da --- /dev/null +++ b/.github/workflows/renovate.yml @@ -0,0 +1,27 @@ +name: Renovate + +on: + schedule: + - cron: '0 5 * * *' # daily 05:00 EST + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + renovate: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 + with: + fetch-depth: 1 + - name: Run Renovate + uses: renovatebot/github-action@03026bd55840025343414baec5d9337c5f9c7ea7 # v44.0.4 + with: + configurationFile: .github/renovate.json + token: ${{ secrets.CPMP_TOKEN }} + env: + LOG_LEVEL: info diff --git a/.github/workflows/renovate_prune.yml b/.github/workflows/renovate_prune.yml new file mode 100644 index 00000000..f7005a05 --- /dev/null +++ b/.github/workflows/renovate_prune.yml @@ -0,0 +1,94 @@ +name: "Prune Renovate Branches" + +on: + workflow_dispatch: + schedule: + - cron: '0 3 * * *' # daily at 03:00 UTC + pull_request: + types: [closed] # also run when any PR is closed (makes pruning near-real-time) + +permissions: + contents: write # required to delete branch refs + pull-requests: read + +jobs: + prune: + runs-on: ubuntu-latest + concurrency: + group: prune-renovate-branches + cancel-in-progress: true + + env: + BRANCH_PREFIX: "renovate/" # adjust if you use a different prefix + + steps: + - name: Prune renovate branches + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + github-token: ${{ secrets.CPMP_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const branchPrefix = (process.env.BRANCH_PREFIX || 'renovate/').replace(/^refs\/heads\//, ''); + const refPrefix = `heads/${branchPrefix}`; // e.g. "heads/renovate/" + + core.info(`Searching for refs with prefix: ${refPrefix}`); + + // List matching refs (branches) under the prefix + let refs; + try { + refs = await github.rest.git.listMatchingRefs({ + owner, + repo, + ref: refPrefix + }); + } catch (err) { + core.info(`No matching refs or API error: ${err.message}`); + refs = { data: [] }; + } + + for (const r of refs.data) { + const fullRef = r.ref; // "refs/heads/renovate/..." + const branchName = fullRef.replace('refs/heads/', ''); + core.info(`Evaluating branch: ${branchName}`); + + // Find PRs for this branch (head = "owner:branch") + const prs = await github.rest.pulls.list({ + owner, + repo, + head: `${owner}:${branchName}`, + state: 'all', + per_page: 100 + }); + + let shouldDelete = false; + if (!prs.data || prs.data.length === 0) { + core.info(`No PRs found for ${branchName} β€” marking for deletion.`); + shouldDelete = true; + } else { + // If none of the PRs are open, safe to delete + const hasOpen = prs.data.some(p => p.state === 'open'); + if (!hasOpen) { + core.info(`All PRs for ${branchName} are closed β€” marking for deletion.`); + shouldDelete = true; + } else { + core.info(`Open PR(s) exist for ${branchName} β€” skipping deletion.`); + } + } + + if (shouldDelete) { + try { + await github.rest.git.deleteRef({ + owner, + repo, + ref: `heads/${branchName}` + }); + core.info(`Deleted branch: ${branchName}`); + } catch (delErr) { + core.warning(`Failed to delete ${branchName}: ${delErr.message}`); + } + } + } + + - name: Done + run: echo "Prune run completed." diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0a0f3832 --- /dev/null +++ b/.gitignore @@ -0,0 +1,83 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +env/ +ENV/ +.pytest_cache/ +.coverage +*.cover +.hypothesis/ +htmlcov/ + +# Node/Frontend +node_modules/ +frontend/node_modules/ +backend/node_modules/ +frontend/dist/ +frontend/coverage/ +frontend/.vite/ +frontend/*.tsbuildinfo + +# Go/Backend +backend/api +backend/*.out +backend/coverage/ +backend/coverage.*.out + +# Databases +*.db +*.sqlite +*.sqlite3 +backend/data/*.db +backend/cmd/api/data/*.db + +# IDE +.idea/ +*.swp +*.swo +*~ +.DS_Store + + +# Logs +.trivy_logs +*.log +logs/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment +.env +.env.* +!.env.example + +# OS +Thumbs.db + +# Caddy +backend/data/caddy/ + +# Docker +docker-compose.override.yml + +# Testing +coverage/ +*.xml +.trivy_logs/trivy-report.txt +backend/coverage.txt + +# CodeQL +codeql-db/ +codeql-results.sarif +**.sarif +codeql-results-js.sarif +codeql-results-go.sarif +remote_logs/Unconfirmed 312410.crdownload +.vscode/launch.json +docker-compose.local.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..1e92ba74 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +repos: + - repo: local + hooks: + - id: python-compile + name: python compile check + entry: tools/python_compile_check.sh + language: script + files: ".*\\.py$" + pass_filenames: false + always_run: true + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: end-of-file-fixer + exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)' + - id: trailing-whitespace + exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)' + - id: check-yaml + - id: check-added-large-files + - repo: local + hooks: + - id: dockerfile-check + name: dockerfile validation + entry: tools/dockerfile_check.sh + language: script + files: "Dockerfile.*" + pass_filenames: true + - id: go-test-coverage + name: Go Test Coverage + entry: scripts/go-test-coverage.sh + language: script + files: '\.go$' + pass_filenames: false + verbose: true + - id: go-vet + name: Go Vet + entry: bash -c 'cd backend && go vet ./...' + language: system + files: '\.go$' + pass_filenames: false + - id: frontend-type-check + name: Frontend TypeScript Check + entry: bash -c 'cd frontend && npm run type-check' + language: system + files: '^frontend/.*\.(ts|tsx)$' + pass_filenames: false + - id: frontend-lint + name: Frontend Lint (Fix) + entry: bash -c 'cd frontend && npm run lint -- --fix' + language: system + files: '^frontend/.*\.(ts|tsx|js|jsx)$' + pass_filenames: false + - id: frontend-test-coverage + name: Frontend Test Coverage + entry: scripts/frontend-test-coverage.sh + language: script + files: '^frontend/.*\.(ts|tsx|js|jsx)$' + pass_filenames: false + verbose: true diff --git a/.sourcery.yml b/.sourcery.yml new file mode 100644 index 00000000..628ec063 --- /dev/null +++ b/.sourcery.yml @@ -0,0 +1,4 @@ +version: 1 +exclude: + - frontend/dist/** + - frontend/node_modules/** diff --git a/.version b/.version new file mode 100644 index 00000000..6e172a05 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +0.2.0-beta.1 diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..5eef0001 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,59 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Git Remove Cached", + "type": "shell", + "command": "git rm -r --cached .", + "group": "test" + }, + { + "label": "Run Pre-commit (All Files)", + "type": "shell", + "command": "${workspaceFolder}/.venv/bin/pre-commit run --all-files", + "group": "test" + }, + { + "label": "Build & Run Local Docker", + "type": "shell", + "command": "docker build --build-arg VCS_REF=$(git rev-parse HEAD) -t cpmp:local . && docker compose -f docker-compose.local.yml up -d", + "group": "test" + }, + { + "label": "Run Local Docker (debug)", + "type": "shell", + "command": "docker run --rm -it --name cpmp-debug --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -p 8080:8080 -p 2345:2345 -e CPM_ENV=development -e CPMP_DEBUG=1 cpmp:local", + "group": "test" + }, + { + "label": "Run Trivy Scan (Local)", + "type": "shell", + "command": "docker", + "args": [ + "run", + "--rm", + "-v", + "/var/run/docker.sock:/var/run/docker.sock", + "-v", + "${userHome}/.cache/trivy:/root/.cache/trivy", + "-v", + "${workspaceFolder}/.trivy_logs:/logs", + "aquasec/trivy:latest", + "image", + "--severity", + "CRITICAL,HIGH", + "--output", + "/logs/trivy-report.txt", + "cpmp:local" + ], + "isBackground": false, + "group": "test" + }, + { + "label": "Run CodeQL Scan (Local)", + "type": "shell", + "command": "${workspaceFolder}/tools/codeql_scan.sh", + "group": "test" + } + ] +} diff --git a/ARCHITECTURE_PLAN.md b/ARCHITECTURE_PLAN.md new file mode 100644 index 00000000..4801271e --- /dev/null +++ b/ARCHITECTURE_PLAN.md @@ -0,0 +1,49 @@ +# CaddyProxyManager+ Architecture Plan + +## Stack Overview +- **Backend**: Go 1.24, Gin HTTP framework, GORM ORM, SQLite for local/stateful storage. +- **Frontend**: React 18 + TypeScript with Vite, React Query for data fetching, React Router for navigation. +- **API Contract**: REST/JSON over `/api/v1`, versioned to keep room for breaking changes. +- **Deployment**: Container-first via multi-stage Docker build (Node β†’ Go), future compose bundle for Caddy runtime. + +## Backend +- `backend/cmd/api`: Entry point wires configuration, database, and HTTP server lifecycle. +- `internal/config`: Reads environment variables (`CPM_ENV`, `CPM_HTTP_PORT`, `CPM_DB_PATH`). Defaults to `development`, `8080`, `./data/cpm.db` respectively. +- `internal/database`: Wraps GORM + SQLite connection handling and enforces data-directory creation. +- `internal/server`: Creates Gin engine, registers middleware, wires graceful shutdown, and exposes `Run(ctx)` for signal-aware lifecycle. +- `internal/api`: Versioned routing layer. Initial resources: + - `GET /api/v1/health`: Simple status response for readiness checks. + - CRUD `/api/v1/proxy-hosts`: Minimal data model used to validate persistence, shape matches Issue #1 requirements (name, domain, upstream target, toggles). +- `internal/models`: Source of truth for persistent entities. Future migrations will extend `ProxyHost` with SSL, ACL, audit metadata. +- Testing: In-memory SQLite harness verifies handler lifecycle via unit tests (`go test ./...`). + +## Frontend +- Vite dev server with proxy to `http://localhost:8080` for `/api` paths keeps CORS trivial. +- React Router organizes initial pages (Dashboard, Proxy Hosts, System Status) to mirror Issue roadmap. +- React Query centralizes API caching, invalidation, and loading states. +- Basic layout shell provides left-nav reminiscent of NPM while keeping styling simple (CSS utility file, no design system yet). Future work will slot shadcn/ui components without rewriting data layer. +- Build outputs static assets in `frontend/dist` consumed by Docker multi-stage for production. + +## Data & Persistence +- SQLite chosen for Alpha milestone simplicity; GORM migrates schema automatically on boot (`AutoMigrate`). +- Database path configurable via env to allow persistent volumes in Docker or alternative DB (PostgreSQL/MySQL) when scaling. + +## API Principles +1. **Version Everything** (`/api/v1`). +2. **Stateless**: Each request carries all context; session/story features will rely on cookies/JWT later. +3. **Dependable validation**: Gin binding ensures HTTP 400 responses include validation errors. +4. **Observability**: Gin logging + structured error responses keep early debugging simple; plan to add Zap/zerolog instrumentation during Beta. + +## Local Development Workflow +1. Start backend: `cd backend && go run ./cmd/api`. +2. Start frontend: `cd frontend && npm run dev` (Vite proxy sends API calls to backend automatically). +3. Optional: run both via Docker (see updated Dockerfile) once containers land. +4. Tests: + - Backend: `cd backend && go test ./...` + - Frontend build check: `cd frontend && npm run build` + +## Next Steps +- Layer authentication (Issue #7) once scaffolding lands. +- Expand data model (certificates, access lists) and add migrations. +- Replace basic CSS with component system (e.g., shadcn/ui) + design tokens. +- Compose file bundling backend, frontend assets, Caddy runtime, and SQLite volume. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..9e55d7b5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,387 @@ +# Contributing to CaddyProxyManager+ + +Thank you for your interest in contributing to CaddyProxyManager+! This document provides guidelines and instructions for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Coding Standards](#coding-standards) +- [Testing Guidelines](#testing-guidelines) +- [Pull Request Process](#pull-request-process) +- [Issue Guidelines](#issue-guidelines) +- [Documentation](#documentation) + +## Code of Conduct + +This project follows a Code of Conduct that all contributors are expected to adhere to: + +- Be respectful and inclusive +- Welcome newcomers and help them get started +- Focus on what's best for the community +- Show empathy towards other community members + +## Getting Started + +-### Prerequisites + +- **Go 1.24+** for backend development +- **Node.js 20+** and npm for frontend development +- Git for version control +- A GitHub account + +### Fork and Clone + +1. Fork the repository on GitHub +2. Clone your fork locally: +```bash +git clone https://github.com/YOUR_USERNAME/CaddyProxyManagerPlus.git +cd CaddyProxyManagerPlus +``` + +3. Add the upstream remote: +```bash +git remote add upstream https://github.com/Wikid82/CaddyProxyManagerPlus.git +``` + +### Set Up Development Environment + +**Backend:** +```bash +cd backend +go mod download +go run ./cmd/seed/main.go # Seed test data +go run ./cmd/api/main.go # Start backend +``` + +**Frontend:** +```bash +cd frontend +npm install +npm run dev # Start frontend dev server +``` + +## Development Workflow + +### Branching Strategy + +- **main** - Production-ready code +- **development** - Main development branch (default) +- **feature/** - Feature branches (e.g., `feature/add-ssl-support`) +- **bugfix/** - Bug fix branches (e.g., `bugfix/fix-import-crash`) +- **hotfix/** - Urgent production fixes + +### Creating a Feature Branch + +Always branch from `development`: + +```bash +git checkout development +git pull upstream development +git checkout -b feature/your-feature-name +``` + +### Commit Message Guidelines + +Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification: + +``` +(): + + + +