diff --git a/.codecov.yml b/.codecov.yml index 48213d795..66638a534 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -11,8 +11,22 @@ coverage: # Fail CI if Codecov upload/report indicates a problem require_ci_to_pass: yes -# Exclude folders from Codecov (adjust as needed) +# Exclude folders from Codecov ignore: - - tests/* - - docs/* - - .github/* + - "**/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/data/*" + - "*.md" diff --git a/.dockerignore b/.dockerignore index 617bf82a3..65fcdf524 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,27 +1,75 @@ +# Version control .git .gitignore -node_modules -venv -__pycache__ -*.pyc -*.pyo -*.pyd +.github/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so .Python +.venv/ +venv/ env/ -build/ -dist/ -*.egg-info -.DS_Store -.idea/ -.vscode/ -.env -.env.* -coverage/ -.coverage +ENV/ .pytest_cache/ -*.log +.coverage +*.cover +.hypothesis/ +htmlcov/ +*.egg-info/ + +# Node/Frontend build artifacts +frontend/node_modules/ +frontend/coverage/ +frontend/.vite/ +frontend/*.tsbuildinfo +# Keep frontend/dist - needed in final image + +# Go/Backend +backend/*.out +backend/coverage.*.out +# Keep backend/api binary - needed in final image + +# Databases (runtime) +backend/data/*.db +backend/cmd/api/data/*.db +*.sqlite *.sqlite3 +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +*.log +logs/ + +# Environment +.env.local +.env.*.local + +# OS +.DS_Store +Thumbs.db + +# Documentation +docs/ +*.md +!README.md + # Docker -docker-compose.override.yml +docker-compose*.yml **/Dockerfile.* + +# CI/CD +.github/ +.pre-commit-config.yaml + +# Scripts +scripts/ +tools/ diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000..8302d397e --- /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 index 0758c5e1f..7894b24e8 100644 --- a/.github/workflows/auto-add-to-project.yml +++ b/.github/workflows/auto-add-to-project.yml @@ -10,15 +10,23 @@ jobs: add-to-project: runs-on: ubuntu-latest steps: - - name: Skip if PROJECT_URL not provided + - name: Determine project URL presence + id: project_check run: | - if [ -z "${{ secrets.PROJECT_URL }}" ]; then - echo "PROJECT_URL secret not set; skipping add-to-project job." - exit 0 + 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@1b844f0c5ac6446a402e0cb3693f9be5eca188c5 # v0.6.1 + continue-on-error: true with: project-url: ${{ secrets.PROJECT_URL }} - github-token: ${{ secrets.GITHUB_TOKEN }} + 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/caddy-major-monitor.yml b/.github/workflows/caddy-major-monitor.yml new file mode 100644 index 000000000..95635ee3c --- /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@v7 + 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/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 21724bcb4..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: CI - Lint, Test & Coverage - -on: - push: - branches: [ main, development, 'feature/**' ] - pull_request: - branches: [ main, development ] - -jobs: - lint: - name: Lint (ruff & flake8) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - - name: Set up Python - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 - with: - python-version: '3.12' - - name: Cache pip - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - name: Install dev dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.dev.txt - - name: Run pre-commit - run: | - pre-commit run --all-files - - name: Run ruff - run: | - ruff check . - - name: Run flake8 - run: | - flake8 . || true - - test-and-coverage: - name: Tests & Coverage - runs-on: ubuntu-latest - needs: [lint] - steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - - name: Set up Python - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 - with: - python-version: '3.12' - - name: Cache pip - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt || true - pip install -r requirements.dev.txt - - name: Run tests with coverage - run: | - # run pytest under coverage and fail if tests fail - coverage run -m pytest -q - coverage report -m --fail-under=75 - - name: Upload coverage to Codecov - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4 - with: - fail_ci_if_error: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Optional: set CODECOV_TOKEN in repo secrets if needed for private repos diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..f378e56a8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,44 @@ +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 + +jobs: + analyze: + name: CodeQL analysis (${{ matrix.language }}) + runs-on: ubuntu-latest + # Skip forked PRs where GITHUB_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 + strategy: + fail-fast: false + matrix: + language: [ 'go', 'javascript-typescript' ] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2e8ea1feb..5dccaf881 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -11,21 +11,56 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + IMAGE_NAME: ${{ github.repository_owner }}/${{ github.event.repository.name }} jobs: build-and-push: name: Build and Push Docker Image runs-on: ubuntu-latest + concurrency: + group: docker-build-${{ github.ref }} + cancel-in-progress: true + outputs: + skip_build: ${{ steps.skip.outputs.skip_build }} permissions: contents: read packages: write + security-events: write steps: # Step 1: Download the code - name: ๐Ÿ“ฅ Checkout code uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + # Normalize IMAGE_NAME to lowercase to satisfy container registry format + - 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 skip condition + id: skip + run: | + should_skip=false + actor='${{ github.actor }}' + event='${{ github.event_name }}' + head_msg='${{ github.event.head_commit.message }}' + 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 + echo "skip_build=$should_skip" >> $GITHUB_OUTPUT + if [ "$should_skip" = true ]; then + echo "Skipping heavy docker build for actor=$actor event=$event (message/title matched)" + else + echo "Proceeding with full docker build" + fi + # Step 2: Set up QEMU for multi-platform builds (ARM, AMD64, etc.) - name: ๐Ÿ”ง Set up QEMU uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 @@ -34,13 +69,21 @@ jobs: - name: ๐Ÿ”ง Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 + # Resolve immutable digest for Caddy base + - name: ๐Ÿ“ฆ Resolve Caddy base digest + id: caddy + run: | + docker pull caddy:2-alpine + DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine) + echo "image=$DIGEST" >> $GITHUB_OUTPUT + # Step 4: Log in to GitHub Container Registry - name: ๐Ÿ” Log in to GitHub Container Registry uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 with: registry: ghcr.io username: ${{ github.actor }} - password: ${{ secrets.CPMP_GHCR_TOKEN }} + password: ${{ secrets.PROJECT_TOKEN }} # Step 5: Figure out what tags to use - name: ๐Ÿท๏ธ Extract metadata (tags, labels) @@ -59,11 +102,14 @@ jobs: type=semver,pattern={{major}}.{{minor}} # Tag major from git tags (v1.2.3 โ†’ 1) type=semver,pattern={{major}} - # Tag with git SHA for tracking (first 7 characters) - type=sha,prefix=sha-,format=short + # Ephemeral tag for pull requests (derive number from GITHUB_REF if available) + type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }} + # Short SHA tag as fallback (for non-default non-dev push events) + type=sha,format=short,enable=${{ github.event_name != 'pull_request' }} # Step 6: Build the frontend first - name: ๐ŸŽจ Build frontend + if: steps.skip.outputs.skip_build != 'true' run: | cd frontend npm ci @@ -71,19 +117,63 @@ jobs: # Step 7: Build and push Docker image - name: ๐Ÿณ Build and push Docker image + if: steps.skip.outputs.skip_build != 'true' uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + id: build with: context: . file: ./Dockerfile - platforms: linux/amd64,linux/arm64 - push: true + 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: | + CADDY_IMAGE=${{ steps.caddy.outputs.image }} - # Step 8: Create a summary + # Step 8: Run Trivy scan (table output first for visibility) + - name: ๐Ÿ“‹ Run Trivy scan (table output) + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + id: trivy_table + continue-on-error: true + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} + format: 'table' + severity: 'CRITICAL,HIGH' + exit-code: '0' + + # Step 9: Run Trivy security scan (SARIF) + - name: ๐Ÿ” Run Trivy vulnerability scanner + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + id: trivy + continue-on-error: true + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }} + format: 'sarif' + output: 'trivy-results.sarif' + exit-code: '0' + severity: 'CRITICAL,HIGH' + + # Step 10: Upload Trivy results to GitHub Security tab + - name: ๐Ÿ“ค Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && (steps.trivy.outcome == 'success' || steps.trivy.outcome == 'failure') + with: + sarif_file: 'trivy-results.sarif' + + # Step 11: Fail if vulnerabilities found + - name: โŒ Check for vulnerabilities + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy_table.outcome == 'failure' + run: | + echo "::error::CRITICAL or HIGH vulnerabilities found in image" + exit 1 + + # Step 11: Create a summary - name: ๐Ÿ“‹ Create summary + if: steps.skip.outputs.skip_build != 'true' run: | echo "## ๐ŸŽ‰ Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -101,12 +191,26 @@ jobs: echo "docker run -d -p 8080:8080 -v caddy_data:/app/data ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY + - name: ๐Ÿ“‹ Create skip summary + if: steps.skip.outputs.skip_build == 'true' + run: | + echo "## ๐Ÿšซ Docker Build Skipped" >> $GITHUB_STEP_SUMMARY + echo "Reason: renovate bot or chore(deps)/chore commit/PR title." >> $GITHUB_STEP_SUMMARY + echo "Actor: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "Event: ${{ github.event_name }}" >> $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' steps: + # Ensure normalized lowercase IMAGE_NAME for this job as well + - name: ๐Ÿ”ค Normalize image name + run: | + raw="${{ github.repository_owner }}/${{ github.event.repository.name }}" + echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV # Step 1: Figure out which tag to test - name: ๐Ÿท๏ธ Determine image tag id: tag @@ -133,21 +237,22 @@ jobs: -p 8080:8080 \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} - # Step 4: Wait for it to start - - name: โณ Wait for container to be ready - run: sleep 10 - - # Step 5: Check if the health endpoint works - - name: ๐Ÿฅ Test health endpoint + # Step 4/5: Wait and check health with retries + - name: ๐Ÿฅ Test health endpoint (retries) run: | - response=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health) - if [ $response -eq 200 ]; then - echo "โœ… Health check passed!" - else - echo "โŒ Health check failed with status $response" - docker logs test-container - exit 1 - fi + 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 # Step 6: Check the logs for errors - name: ๐Ÿ“‹ Check container logs diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index dc39b65ff..7a91053f1 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -11,57 +11,93 @@ on: branches: - main - development - workflow_call: # Allow this workflow to be called by other workflows + workflow_call: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + IMAGE_NAME: ${{ github.repository_owner }}/${{ github.event.repository.name }} jobs: build-and-push: runs-on: ubuntu-latest + timeout-minutes: 30 permissions: contents: read packages: write - + security-events: write + steps: - name: Checkout repository - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - 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 skip condition + id: skip + run: | + should_skip=false + actor='${{ github.actor }}' + event='${{ github.event_name }}' + head_msg='${{ github.event.head_commit.message }}' + 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 + echo "skip_build=$should_skip" >> $GITHUB_OUTPUT + + - name: Set up QEMU + if: steps.skip.outputs.skip_build != 'true' + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 + if: steps.skip.outputs.skip_build != 'true' + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + - 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' - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + password: ${{ secrets.PROJECT_TOKEN }} - name: Extract metadata (tags, labels) + if: steps.skip.outputs.skip_build != 'true' id: meta - uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5 + uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c # v5.0.1 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - # Tag 'latest' for main branch type=raw,value=latest,enable={{is_default_branch}} - # Tag 'development' for development branch - type=raw,value=development,enable=${{ github.ref == 'refs/heads/development' }} - # Semver tags for version releases (v1.0.0 -> 1.0.0, 1.0, 1) + type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - # SHA for all builds - type=sha,prefix={{branch}}- - name: Build and push Docker image + if: steps.skip.outputs.skip_build != 'true' id: build-and-push - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 with: context: . - platforms: linux/amd64,linux/arm64 + # PRs: amd64 only, no push. Pushes: amd64+arm64, push. + 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 }} @@ -71,6 +107,32 @@ jobs: 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: Image digest - run: echo ${{ steps.build-and-push.outputs.digest }} + # Trivy steps only on push + - name: Run Trivy scan (table output) + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + uses: aquasecurity/trivy-action@d43c1f16c00cfd3978dde6c07f4bbcf9eb6993ca # v0.16.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@d43c1f16c00cfd3978dde6c07f4bbcf9eb6993ca # v0.16.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: Upload Trivy results + if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 12958f128..4723453b3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: build: name: Build Documentation runs-on: ubuntu-latest - + steps: # Step 1: Get the code - name: ๐Ÿ“ฅ Checkout code @@ -42,11 +42,11 @@ jobs: 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' @@ -154,12 +154,12 @@ jobs:

๐Ÿš€ 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. + 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!

@@ -220,15 +220,15 @@ jobs: Stuck? Have questions? We're here to help!

- ๐Ÿ’ฌ Ask a Question - ๐Ÿ› Report a Bug - โญ View on GitHub @@ -243,10 +243,10 @@ jobs: 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 @@ -254,14 +254,14 @@ jobs: 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 @@ -296,10 +296,10 @@ jobs:
HEADER - + # Append original content cat "$html_file" >> "$temp_file" - + # Add footer cat >> "$temp_file" << 'FOOTER'
@@ -309,11 +309,11 @@ jobs: FOOTER - + mv "$temp_file" "$html_file" fi done - + echo "โœ… Documentation site built successfully!" # Step 4: Upload the built site @@ -329,7 +329,7 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build - + steps: # Deploy to GitHub Pages - name: ๐Ÿš€ Deploy to GitHub Pages diff --git a/.github/workflows/propagate-changes.yml b/.github/workflows/propagate-changes.yml index c59085a1f..1781a6e2c 100644 --- a/.github/workflows/propagate-changes.yml +++ b/.github/workflows/propagate-changes.yml @@ -14,82 +14,93 @@ jobs: propagate: name: Create PR to synchronize branches runs-on: ubuntu-latest - if: "\n github.actor != 'github-actions[bot]' && \ - github.event.pusher != null\n " + if: github.actor != 'github-actions[bot]' && github.event.pusher != null steps: - name: Set up Node (for github-script) uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '18' - - name: Determine source and target branches - id: branches - run: | - echo "source=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT - if [ "${GITHUB_REF#refs/heads/}" = "main" ]; then - echo "target=development" >> $GITHUB_OUTPUT - else - echo "target=main" >> $GITHUB_OUTPUT - fi - - - name: Create or update Pull Request + - name: Propagate Changes uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | - const source = process.env.SOURCE_BRANCH || process.env.GITHUB_REF.replace('refs/heads/',''); - const target = process.env.TARGET_BRANCH || (source === 'main' ? 'development' : 'main'); + const currentBranch = context.ref.replace('refs/heads/', ''); - const src = source; - const base = target; + async function createPR(src, base) { + if (src === base) return; - // Do not create PR if source and base are same - if (src === base) { - core.info(`Source and base are the same (${src}); skipping.`); - return; - } + core.info(`Checking propagation from ${src} to ${base}...`); - // Check for existing open PRs from src->base - 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, - }); + // 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(`Found existing PR(s) from ${src} to ${base}. Skipping creation.`); - return; - } + if (pulls.length > 0) { + core.info(`Existing PR found for ${src} -> ${base}. Skipping.`); + return; + } - // Compare commits: only create PR if source is ahead of base - const compare = await github.rest.repos.compareCommits({ - owner: context.repo.owner, - repo: context.repo.repo, - base: base, - head: src, - }); + // 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 (compare.data && compare.data.status === 'identical') { - core.info(`${src} and ${base} are identical. No PR needed.`); - return; + // 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}`); + } } - // Create PR - const title = `Propagate changes from ${src} into ${base}`; - const body = `Automated PR to propagate commits from ${src} into ${base}.\n\nTriggered by push by @${context.actor}.`; + 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/')); - const pr = await github.rest.pulls.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title, - head: src, - base, - body, - }); + core.info(`Found ${featureBranches.length} feature branches: ${featureBranches.join(', ')}`); - core.info(`Created PR #${pr.data.number}: ${pr.data.html_url}`); + for (const featureBranch of featureBranches) { + await createPR('development', featureBranch); + } + } env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SOURCE_BRANCH: ${{ steps.branches.outputs.source }} - TARGET_BRANCH: ${{ steps.branches.outputs.target }} + GITHUB_TOKEN: ${{ secrets.PROJECT_TOKEN }} diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml new file mode 100644 index 000000000..5cddf2a22 --- /dev/null +++ b/.github/workflows/quality-checks.yml @@ -0,0 +1,58 @@ +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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: '1.22' + cache-dependency-path: backend/go.sum + + - name: Run Go tests + working-directory: backend + run: go test -v ./... + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up Node.js + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: '20' + 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 test + + - 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 index b7c269c02..88d4baf59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: run: | # Get previous tag PREV_TAG=$(git describe --tags --abbrev=0 $(git rev-list --tags --skip=1 --max-count=1) 2>/dev/null || echo "") - + if [ -z "$PREV_TAG" ]; then echo "First release - generating full changelog" CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges) @@ -31,7 +31,7 @@ jobs: echo "Generating changelog since $PREV_TAG" CHANGELOG=$(git log $PREV_TAG..HEAD --pretty=format:"- %s (%h)" --no-merges) fi - + # Save to file for GitHub release echo "$CHANGELOG" > CHANGELOG.txt echo "Generated changelog with $(echo "$CHANGELOG" | wc -l) commits" @@ -44,7 +44,7 @@ jobs: draft: false prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.PROJECT_TOKEN }} build-and-publish: needs: create-release diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml new file mode 100644 index 000000000..6463e6583 --- /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@v4 + with: + fetch-depth: 1 + - name: Run Renovate + uses: renovatebot/github-action@v40.1.11 + with: + configurationFile: .github/renovate.json + token: ${{ secrets.PROJECT_TOKEN }} + env: + LOG_LEVEL: info diff --git a/.gitignore b/.gitignore index a0acf02ea..1fba65a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,60 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +env/ +ENV/ +.pytest_cache/ +.coverage +*.cover +.hypothesis/ +htmlcov/ -# Node modules -frontend/node_modules +# Node/Frontend +frontend/node_modules/ +frontend/dist/ +frontend/coverage/ +frontend/.vite/ +frontend/*.tsbuildinfo + +# Go/Backend +backend/api +backend/*.out +backend/coverage.*.out + +# Databases +*.db +*.sqlite +*.sqlite3 +backend/data/*.db +backend/cmd/api/data/*.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Logs +*.log +logs/ + +# Environment +.env +.env.* +!.env.example + +# OS +Thumbs.db + +# Caddy +backend/data/caddy/ + +# Docker +docker-compose.override.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c0812f31a..e2ee236f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,16 @@ repos: 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 diff --git a/Dockerfile b/Dockerfile index b6a72bb94..05fad2a3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,12 +6,21 @@ ARG VERSION=dev ARG BUILD_DATE ARG VCS_REF +# Allow pinning Caddy base image by digest via build-arg +ARG CADDY_IMAGE=caddy:2-alpine + # ---- Frontend Builder ---- -FROM node:20-alpine AS frontend-builder +# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues +FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend-builder WORKDIR /app/frontend # Copy frontend package files COPY frontend/package*.json ./ + +# Set environment to bypass native binary requirement for cross-arch builds +ENV npm_config_rollup_skip_nodejs_native=1 \ + ROLLUP_SKIP_NODEJS_NATIVE=1 + RUN npm ci # Copy frontend source and build @@ -19,7 +28,7 @@ COPY frontend/ ./ RUN npm run build # ---- Backend Builder ---- -FROM golang:latest AS backend-builder +FROM golang:alpine AS backend-builder WORKDIR /app/backend # Install build dependencies @@ -46,11 +55,12 @@ RUN CGO_ENABLED=1 GOOS=linux go build \ -o api ./cmd/api # ---- Final Runtime with Caddy ---- -FROM caddy:latest +FROM ${CADDY_IMAGE} WORKDIR /app -# Install runtime dependencies for CPM+ -RUN apk --no-cache add ca-certificates sqlite-libs bash +# Install runtime dependencies for CPM+ (no bash needed) +RUN apk --no-cache add ca-certificates sqlite-libs \ + && apk --no-cache upgrade # Copy Go binary from backend builder COPY --from=backend-builder /app/backend/api /app/api diff --git a/GHCR_MIGRATION_SUMMARY.md b/GHCR_MIGRATION_SUMMARY.md index 1fd10508c..163b2da18 100644 --- a/GHCR_MIGRATION_SUMMARY.md +++ b/GHCR_MIGRATION_SUMMARY.md @@ -13,11 +13,11 @@ Updated all workflows and documentation to use GitHub Container Registry (GHCR) - **After**: GitHub Container Registry (`ghcr.io/wikid82/caddyproxymanagerplus`) ### Benefits of GHCR: -โœ… **No extra accounts needed** - Uses your GitHub account -โœ… **Automatic authentication** - Uses built-in `GITHUB_TOKEN` -โœ… **Free for public repos** - No Docker Hub rate limits -โœ… **Integrated with repo** - Packages show up on your GitHub profile -โœ… **Better security** - No need to store Docker Hub credentials +โœ… **No extra accounts needed** - Uses your GitHub account +โœ… **Automatic authentication** - Uses built-in `GITHUB_TOKEN` +โœ… **Free for public repos** - No Docker Hub rate limits +โœ… **Integrated with repo** - Packages show up on your GitHub profile +โœ… **Better security** - No need to store Docker Hub credentials ### Files Updated: @@ -35,7 +35,7 @@ env: REGISTRY: docker.io IMAGE_NAME: wikid82/caddy-proxy-manager-plus -# After +# After env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/PHASE_7_SUMMARY.md b/PHASE_7_SUMMARY.md index bc5ac61f7..c1d9cfeb5 100644 --- a/PHASE_7_SUMMARY.md +++ b/PHASE_7_SUMMARY.md @@ -277,6 +277,6 @@ The application is now **production-ready** with: --- -**Phase 7 Status**: โœ… **COMPLETE** -**Implementation Date**: January 18, 2025 +**Phase 7 Status**: โœ… **COMPLETE** +**Implementation Date**: January 18, 2025 **Total Implementation Time**: 7 phases completed diff --git a/backend/go.mod b/backend/go.mod index ac19fa213..e3a24bb5f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,10 +1,11 @@ module github.com/Wikid82/CaddyProxyManagerPlus/backend -go 1.22 +go 1.25.4 require ( github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.11.1 gorm.io/driver/sqlite v1.5.6 gorm.io/gorm v1.25.12 ) @@ -32,14 +33,13 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.11.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/crypto v0.23.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 4b66be5ea..ee19bcd00 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -65,7 +65,6 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -76,16 +75,16 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= diff --git a/backend/internal/api/handlers/handlers_test.go b/backend/internal/api/handlers/handlers_test.go index 3a0a9ce40..0625d74dd 100644 --- a/backend/internal/api/handlers/handlers_test.go +++ b/backend/internal/api/handlers/handlers_test.go @@ -18,7 +18,7 @@ import ( ) func setupTestDB() *gorm.DB { - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{}) if err != nil { panic("failed to connect to test database") } diff --git a/backend/internal/api/handlers/health_handler.go b/backend/internal/api/handlers/health_handler.go index 864e74849..2da479448 100644 --- a/backend/internal/api/handlers/health_handler.go +++ b/backend/internal/api/handlers/health_handler.go @@ -12,8 +12,8 @@ func HealthHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "ok", "service": version.Name, - "version": version.SemVer, + "version": version.Version, "git_commit": version.GitCommit, - "build_date": version.BuildDate, + "build_time": version.BuildTime, }) } diff --git a/backend/internal/api/handlers/proxy_host_handler.go b/backend/internal/api/handlers/proxy_host_handler.go index 9fdeefbbc..584831c23 100644 --- a/backend/internal/api/handlers/proxy_host_handler.go +++ b/backend/internal/api/handlers/proxy_host_handler.go @@ -114,10 +114,3 @@ func (h *ProxyHostHandler) Delete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "proxy host deleted"}) } - -// HealthHandler returns a simple health check response. -func HealthHandler(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "status": "ok", - }) -} diff --git a/backend/internal/models/proxy_host.go b/backend/internal/models/proxy_host.go index 937d7293d..0f9a82bb7 100644 --- a/backend/internal/models/proxy_host.go +++ b/backend/internal/models/proxy_host.go @@ -19,11 +19,3 @@ type ProxyHost struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } - -// CaddyConfig stores the generated Caddy JSON configuration. -type CaddyConfig struct { - ID uint `json:"id" gorm:"primaryKey"` - Config string `json:"config" gorm:"type:text"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index fea68f28a..ce8ee4d29 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -4,6 +4,7 @@ version: '3.9' services: app: + image: ghcr.io/wikid82/caddyproxymanagerplus:dev # Development: expose Caddy admin API externally for debugging ports: - "80:80" diff --git a/docker-compose.yml b/docker-compose.yml index f600a82d7..559c1ec78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,7 @@ version: '3.9' services: app: - build: - context: . - dockerfile: Dockerfile + image: ghcr.io/wikid82/caddyproxymanagerplus:latest container_name: caddyproxymanagerplus restart: unless-stopped ports: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 254faad70..62628d2cb 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh set -e # Entrypoint script to run both Caddy and CPM+ in a single container @@ -8,17 +8,20 @@ echo "Starting CaddyProxyManager+ with integrated Caddy..." # Start Caddy in the background with initial empty config echo '{"apps":{}}' > /config/caddy.json -caddy run --config /config/caddy.json --adapter json & +# Use JSON config directly; no adapter needed +caddy run --config /config/caddy.json & CADDY_PID=$! echo "Caddy started (PID: $CADDY_PID)" # Wait for Caddy to be ready echo "Waiting for Caddy admin API..." -for i in {1..30}; do +i=1 +while [ "$i" -le 30 ]; do if wget -q -O- http://localhost:2019/config/ > /dev/null 2>&1; then echo "Caddy is ready!" break fi + i=$((i+1)) sleep 1 done @@ -31,25 +34,25 @@ echo "CPM+ started (PID: $APP_PID)" # Function to handle shutdown gracefully shutdown() { echo "Shutting down..." - kill -TERM $APP_PID 2>/dev/null || true - kill -TERM $CADDY_PID 2>/dev/null || true - wait $APP_PID 2>/dev/null || true - wait $CADDY_PID 2>/dev/null || true + kill -TERM "$APP_PID" 2>/dev/null || true + kill -TERM "$CADDY_PID" 2>/dev/null || true + wait "$APP_PID" 2>/dev/null || true + wait "$CADDY_PID" 2>/dev/null || true exit 0 } # Trap signals for graceful shutdown -trap shutdown SIGTERM SIGINT +trap 'shutdown' TERM INT echo "CaddyProxyManager+ is running!" echo " - Management UI: http://localhost:8080" echo " - Caddy Proxy: http://localhost:80, https://localhost:443" echo " - Caddy Admin API: http://localhost:2019" -# Wait for either process to exit -wait -n $APP_PID $CADDY_PID +# Wait loop: exit when either process dies, then shutdown the other +while kill -0 "$APP_PID" 2>/dev/null && kill -0 "$CADDY_PID" 2>/dev/null; do + sleep 1 +done -# If one process exits, shut down the other -EXIT_CODE=$? -echo "A process exited with code $EXIT_CODE, shutting down..." +echo "A process exited, initiating shutdown..." shutdown diff --git a/docs/github-setup.md b/docs/github-setup.md index 19ed5737c..5f965edcf 100644 --- a/docs/github-setup.md +++ b/docs/github-setup.md @@ -4,35 +4,18 @@ This guide will help you set up GitHub Actions for automatic Docker builds and d --- -## ๐Ÿ“ฆ Step 1: Set Up GitHub Container Registry Token - -The Docker build workflow uses GitHub Container Registry (GHCR) to store your images. - -### Add Your GHCR Token: - -1. **Create a Personal Access Token (PAT):** - - Go to https://github.com/settings/tokens - - Click **"Generate new token"** โ†’ **"Generate new token (classic)"** - - Give it a name: `CPMP GHCR Token` - - Select scopes: - - โœ… `write:packages` (upload container images) - - โœ… `read:packages` (download container images) - - โœ… `delete:packages` (optional - delete old images) - - Click **"Generate token"** - - **Copy the token** - you won't see it again! - -2. **Add Secret to Repository:** - - Go to your repository โ†’ https://github.com/Wikid82/CaddyProxyManagerPlus - - Click **"Settings"** โ†’ **"Secrets and variables"** โ†’ **"Actions"** - - Click **"New repository secret"** - - Name: `CPMP_GHCR_TOKEN` - - Value: Paste the token you copied - - Click **"Add secret"** - -**Where images go:** -- โœ… `ghcr.io/wikid82/caddyproxymanagerplus` -- โœ… Images are linked to your repository -- โœ… Free for public repositories +## ๐Ÿ“ฆ Step 1: Docker Image Publishing (Automatic!) + +The Docker build workflow uses GitHub Container Registry (GHCR) to store your images. **No setup required!** GitHub automatically provides authentication tokens for GHCR. + +### How It Works: + +GitHub Actions automatically uses the built-in `GITHUB_TOKEN` which has permission to: +- โœ… Push images to `ghcr.io/wikid82/caddyproxymanagerplus` +- โœ… Link images to your repository +- โœ… Publish images for free (public repositories) + +**Nothing to configure!** Just push code and images will be built automatically. ### Make Your Images Public (Optional): @@ -77,7 +60,7 @@ https://wikid82.github.io/CaddyProxyManagerPlus/ **Triggers when:** - โœ… You push to `main` branch โ†’ Creates `latest` tag -- โœ… You push to `development` branch โ†’ Creates `dev` tag +- โœ… You push to `development` branch โ†’ Creates `dev` tag - โœ… You create a version tag like `v1.0.0` โ†’ Creates version tags - โœ… You manually trigger it from GitHub UI @@ -174,17 +157,12 @@ When you're ready to release a new version: ### Docker Build Fails **Problem**: "Error: denied: requested access to the resource is denied" -- **Fix**: Make sure you created the `CPMP_GHCR_TOKEN` secret with the right permissions -- **Check**: Token needs `write:packages` scope -- **Verify**: Settings โ†’ Secrets and variables โ†’ Actions โ†’ Check `CPMP_GHCR_TOKEN` exists - -**Problem**: "Error: CPMP_GHCR_TOKEN not found" -- **Fix**: You need to add the secret (see Step 1 above) -- **Double-check**: The secret name is exactly `CPMP_GHCR_TOKEN` (case-sensitive!) +- **Fix**: This shouldn't happen with `GITHUB_TOKEN` - check workflow permissions +- **Verify**: Settings โ†’ Actions โ†’ General โ†’ Workflow permissions โ†’ "Read and write permissions" enabled **Problem**: Can't pull the image - **Fix**: Make the package public (see Step 1 above) -- **Or**: Authenticate with GitHub: `echo $CPMP_GHCR_TOKEN | docker login ghcr.io -u USERNAME --password-stdin` +- **Or**: Authenticate with GitHub: `echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin` ### Docs Don't Deploy @@ -257,11 +235,9 @@ git push origin :refs/tags/v1.2.3 Before pushing to production, make sure: -- [ ] Created Personal Access Token with `write:packages` scope -- [ ] Added `CPMP_GHCR_TOKEN` secret to repository - [ ] GitHub Pages is enabled with "GitHub Actions" source -- [ ] You've tested the Docker build workflow -- [ ] You've tested the docs deployment workflow +- [ ] You've tested the Docker build workflow (automatic on push) +- [ ] You've tested the docs deployment workflow - [ ] Container package is set to "Public" visibility (optional, for easier pulls) - [ ] Documentation looks good on the published site - [ ] Docker image runs correctly diff --git a/frontend/.vite/deps_temp_c8b409d7/package.json b/frontend/.vite/deps_temp_c8b409d7/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/frontend/.vite/deps_temp_c8b409d7/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 04a2ab257..ded5a96a1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,9 @@ "name": "caddy-proxy-manager-plus-frontend", "version": "0.1.0", "dependencies": { + "@tanstack/react-query": "^5.62.8", + "axios": "^1.7.9", + "clsx": "^2.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.2" @@ -1335,6 +1338,30 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "dev": true }, + "node_modules/@tanstack/query-core": { + "version": "5.90.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz", + "integrity": "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.10.tgz", + "integrity": "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw==", + "dependencies": { + "@tanstack/query-core": "5.90.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -2034,6 +2061,11 @@ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/autoprefixer": { "version": "10.4.22", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", @@ -2071,6 +2103,16 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2161,6 +2203,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2260,6 +2314,14 @@ "node": ">= 6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2278,6 +2340,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2406,6 +2479,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2434,6 +2515,19 @@ "dev": true, "peer": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2464,12 +2558,53 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -3241,6 +3376,25 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3257,6 +3411,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -3288,7 +3457,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3302,6 +3470,41 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -3346,6 +3549,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3361,11 +3575,35 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -3848,6 +4086,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", @@ -3876,6 +4122,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -4369,6 +4634,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 07665a4f7..beccc055d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,9 @@ "test:coverage": "vitest --coverage" }, "dependencies": { + "@tanstack/react-query": "^5.62.8", + "axios": "^1.7.9", + "clsx": "^2.1.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.30.2" diff --git a/frontend/src/hooks/__tests__/useImport.test.ts b/frontend/src/hooks/__tests__/useImport.test.ts index 634e6c1cf..6abbc5269 100644 --- a/frontend/src/hooks/__tests__/useImport.test.ts +++ b/frontend/src/hooks/__tests__/useImport.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { renderHook, waitFor } from '@testing-library/react' +import { renderHook, waitFor, act } from '@testing-library/react' import { useImport } from '../useImport' import * as api from '../../services/api' @@ -55,7 +55,9 @@ describe('useImport', () => { const { result } = renderHook(() => useImport()) - await result.current.upload('example.com { reverse_proxy localhost:8080 }') + await act(async () => { + await result.current.upload('example.com { reverse_proxy localhost:8080 }') + }) await waitFor(() => { expect(result.current.session).toEqual(mockSession) @@ -71,9 +73,19 @@ describe('useImport', () => { const { result } = renderHook(() => useImport()) - await expect(result.current.upload('invalid')).rejects.toThrow('Upload failed') + let threw = false + await act(async () => { + try { + await result.current.upload('invalid') + } catch { + threw = true + } + }) + expect(threw).toBe(true) - expect(result.current.error).toBe('Upload failed') + await waitFor(() => { + expect(result.current.error).toBe('Upload failed') + }) }) it('commits import with resolutions', async () => { @@ -86,24 +98,27 @@ describe('useImport', () => { } vi.mocked(api.importAPI.upload).mockResolvedValue({ session: mockSession }) - vi.mocked(api.importAPI.status) - .mockResolvedValueOnce({ has_pending: true, session: mockSession }) - .mockResolvedValueOnce({ has_pending: false }) + // Keep session pending during initial checks so upload retains session state + vi.mocked(api.importAPI.status).mockResolvedValue({ has_pending: true, session: mockSession }) vi.mocked(api.importAPI.preview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] }) vi.mocked(api.importAPI.commit).mockResolvedValue({}) const { result } = renderHook(() => useImport()) - await result.current.upload('test') + await act(async () => { + await result.current.upload('test') + }) await waitFor(() => { expect(result.current.session).toEqual(mockSession) }) - await result.current.commit({ 'test.com': 'skip' }) + await act(async () => { + await result.current.commit({ 'test.com': 'skip' }) + }) expect(api.importAPI.commit).toHaveBeenCalledWith('session-2', { 'test.com': 'skip' }) - + await waitFor(() => { expect(result.current.session).toBeNull() }) @@ -125,16 +140,22 @@ describe('useImport', () => { const { result } = renderHook(() => useImport()) - await result.current.upload('test') + await act(async () => { + await result.current.upload('test') + }) await waitFor(() => { expect(result.current.session).toEqual(mockSession) }) - await result.current.cancel() + await act(async () => { + await result.current.cancel() + }) expect(api.importAPI.cancel).toHaveBeenCalledWith('session-3') - expect(result.current.session).toBeNull() + await waitFor(() => { + expect(result.current.session).toBeNull() + }) }) it('handles commit errors', async () => { @@ -149,20 +170,32 @@ describe('useImport', () => { vi.mocked(api.importAPI.upload).mockResolvedValue({ session: mockSession }) vi.mocked(api.importAPI.status).mockResolvedValue({ has_pending: true, session: mockSession }) vi.mocked(api.importAPI.preview).mockResolvedValue({ hosts: [], conflicts: [], errors: [] }) - + const mockError = new Error('Commit failed') vi.mocked(api.importAPI.commit).mockRejectedValue(mockError) const { result } = renderHook(() => useImport()) - await result.current.upload('test') + await act(async () => { + await result.current.upload('test') + }) await waitFor(() => { expect(result.current.session).toEqual(mockSession) }) - await expect(result.current.commit({})).rejects.toThrow('Commit failed') + let threw = false + await act(async () => { + try { + await result.current.commit({}) + } catch { + threw = true + } + }) + expect(threw).toBe(true) - expect(result.current.error).toBe('Commit failed') + await waitFor(() => { + expect(result.current.error).toBe('Commit failed') + }) }) }) diff --git a/frontend/src/hooks/__tests__/useProxyHosts.test.ts b/frontend/src/hooks/__tests__/useProxyHosts.test.ts index 081fc16c2..88fad37d6 100644 --- a/frontend/src/hooks/__tests__/useProxyHosts.test.ts +++ b/frontend/src/hooks/__tests__/useProxyHosts.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { renderHook, waitFor } from '@testing-library/react' +import { renderHook, waitFor, act } from '@testing-library/react' import { useProxyHosts } from '../useProxyHosts' import * as api from '../../services/api' @@ -63,7 +63,7 @@ describe('useProxyHosts', () => { vi.mocked(api.proxyHostsAPI.list).mockResolvedValue([]) const newHost = { domain_names: 'new.com', forward_host: 'localhost', forward_port: 9000 } const createdHost = { uuid: '3', ...newHost, enabled: true } - + vi.mocked(api.proxyHostsAPI.create).mockResolvedValue(createdHost) const { result } = renderHook(() => useProxyHosts()) @@ -72,16 +72,20 @@ describe('useProxyHosts', () => { expect(result.current.loading).toBe(false) }) - await result.current.createHost(newHost) + await act(async () => { + await result.current.createHost(newHost) + }) expect(api.proxyHostsAPI.create).toHaveBeenCalledWith(newHost) - expect(api.proxyHostsAPI.list).toHaveBeenCalledTimes(2) // Initial load + reload after create + await waitFor(() => { + expect(result.current.hosts).toContainEqual(createdHost) + }) }) it('updates an existing proxy host', async () => { const existingHost = { uuid: '1', domain_names: 'test.com', enabled: true, forward_host: 'localhost', forward_port: 8080 } vi.mocked(api.proxyHostsAPI.list).mockResolvedValue([existingHost]) - + const updatedHost = { ...existingHost, domain_names: 'updated.com' } vi.mocked(api.proxyHostsAPI.update).mockResolvedValue(updatedHost) @@ -91,10 +95,14 @@ describe('useProxyHosts', () => { expect(result.current.loading).toBe(false) }) - await result.current.updateHost('1', { domain_names: 'updated.com' }) + await act(async () => { + await result.current.updateHost('1', { domain_names: 'updated.com' }) + }) expect(api.proxyHostsAPI.update).toHaveBeenCalledWith('1', { domain_names: 'updated.com' }) - expect(api.proxyHostsAPI.list).toHaveBeenCalledTimes(2) + await waitFor(() => { + expect(result.current.hosts[0].domain_names).toBe('updated.com') + }) }) it('deletes a proxy host', async () => { @@ -111,10 +119,15 @@ describe('useProxyHosts', () => { expect(result.current.loading).toBe(false) }) - await result.current.deleteHost('1') + await act(async () => { + await result.current.deleteHost('1') + }) expect(api.proxyHostsAPI.delete).toHaveBeenCalledWith('1') - expect(api.proxyHostsAPI.list).toHaveBeenCalledTimes(2) + await waitFor(() => { + expect(result.current.hosts).toHaveLength(1) + expect(result.current.hosts[0].uuid).toBe('2') + }) }) it('handles create errors', async () => { diff --git a/frontend/src/hooks/__tests__/useRemoteServers.test.ts b/frontend/src/hooks/__tests__/useRemoteServers.test.ts index 69d155eff..8d0736731 100644 --- a/frontend/src/hooks/__tests__/useRemoteServers.test.ts +++ b/frontend/src/hooks/__tests__/useRemoteServers.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { renderHook, waitFor } from '@testing-library/react' +import { renderHook, waitFor, act } from '@testing-library/react' import { useRemoteServers } from '../useRemoteServers' import * as api from '../../services/api' @@ -87,7 +87,7 @@ describe('useRemoteServers', () => { vi.mocked(api.remoteServersAPI.list).mockResolvedValue([]) const newServer = { name: 'New Server', host: 'new.local', port: 5000, enabled: true, provider: 'generic' } const createdServer = { uuid: '4', ...newServer } - + vi.mocked(api.remoteServersAPI.create).mockResolvedValue(createdServer) const { result } = renderHook(() => useRemoteServers()) @@ -96,16 +96,20 @@ describe('useRemoteServers', () => { expect(result.current.loading).toBe(false) }) - await result.current.createServer(newServer) + await act(async () => { + await result.current.createServer(newServer) + }) expect(api.remoteServersAPI.create).toHaveBeenCalledWith(newServer) - expect(api.remoteServersAPI.list).toHaveBeenCalledTimes(2) + await waitFor(() => { + expect(result.current.servers).toContainEqual(createdServer) + }) }) it('updates an existing remote server', async () => { const existingServer = { uuid: '1', name: 'Server 1', host: 'localhost', port: 8080, enabled: true } vi.mocked(api.remoteServersAPI.list).mockResolvedValue([existingServer]) - + const updatedServer = { ...existingServer, name: 'Updated Server' } vi.mocked(api.remoteServersAPI.update).mockResolvedValue(updatedServer) @@ -115,10 +119,14 @@ describe('useRemoteServers', () => { expect(result.current.loading).toBe(false) }) - await result.current.updateServer('1', { name: 'Updated Server' }) + await act(async () => { + await result.current.updateServer('1', { name: 'Updated Server' }) + }) expect(api.remoteServersAPI.update).toHaveBeenCalledWith('1', { name: 'Updated Server' }) - expect(api.remoteServersAPI.list).toHaveBeenCalledTimes(2) + await waitFor(() => { + expect(result.current.servers[0].name).toBe('Updated Server') + }) }) it('deletes a remote server', async () => { @@ -135,10 +143,15 @@ describe('useRemoteServers', () => { expect(result.current.loading).toBe(false) }) - await result.current.deleteServer('1') + await act(async () => { + await result.current.deleteServer('1') + }) expect(api.remoteServersAPI.delete).toHaveBeenCalledWith('1') - expect(api.remoteServersAPI.list).toHaveBeenCalledTimes(2) + await waitFor(() => { + expect(result.current.servers).toHaveLength(1) + expect(result.current.servers[0].uuid).toBe('2') + }) }) it('tests server connection', async () => { diff --git a/frontend/src/hooks/useImport.ts b/frontend/src/hooks/useImport.ts index 099bfd941..f01bb0602 100644 --- a/frontend/src/hooks/useImport.ts +++ b/frontend/src/hooks/useImport.ts @@ -69,7 +69,7 @@ export function useImport() { const commit = async (resolutions: Record) => { if (!session) throw new Error('No active session') - + try { setLoading(true) setError(null) @@ -87,7 +87,7 @@ export function useImport() { const cancel = async () => { if (!session) return - + try { setLoading(true) setError(null) diff --git a/frontend/src/hooks/useRemoteServers.ts b/frontend/src/hooks/useRemoteServers.ts index 0382837a7..02c6de8f3 100644 --- a/frontend/src/hooks/useRemoteServers.ts +++ b/frontend/src/hooks/useRemoteServers.ts @@ -66,13 +66,25 @@ export function useRemoteServers() { } } + const testConnection = async (uuid: string) => { + try { + return await remoteServersAPI.test(uuid) + } catch (err) { + throw new Error(err instanceof Error ? err.message : 'Failed to test connection') + } + } + + const enabledServers = servers.filter(s => s.enabled) + return { servers, + enabledServers, loading, error, refresh: fetchServers, createServer, updateServer, deleteServer, + testConnection, } } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 1d20dea50..83dd3f9ff 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -27,26 +27,26 @@ export default function Dashboard() { return (

Dashboard

- +
Proxy Hosts
{hosts.length}
{enabledHosts} enabled
- +
Remote Servers
{servers.length}
{enabledServers} enabled
- +
SSL Certificates
0
Coming soon
- +
System Status
@@ -69,7 +69,7 @@ export default function Dashboard() {
Create a new reverse proxy
- + Register a backend server
- +
- +
{/* File Upload */}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 83a0b752c..3304be021 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -21,7 +21,7 @@ async function request(endpoint: string, options: RequestOptions = {}): Promi } const response = await fetch(url, config) - + if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })) throw new Error(error.error || `HTTP ${response.status}`) diff --git a/requirements.dev.txt b/requirements.dev.txt index 9a9e7c38f..0d5c2d30c 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -2,11 +2,11 @@ # Testing, linters, formatting, and security checks # pytest-xdist is not used - tests run serially to reproduce a user's experience more accurately. -pytest>=7.4 +pytest>=7.4.4 pytest-cov>=4.1 -black>=24.3 -ruff>=0.15 -isort>=5.12 +black>=24.10.0 +ruff>=0.6.0 +isort>=5.13.2 mypy>=1.6 pre-commit>=3.4 bandit>=1.9.1 @@ -17,5 +17,5 @@ pytest-timeout==2.4.0 # Add more dev tools as required # Coverage tooling and additional linters -coverage>=7.2 +coverage>=7.12.0 flake8>=6.1 diff --git a/requirements.txt b/requirements.txt index b737adfc1..2c947cf8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ # Base runtime requirements - adapt to your stack. # Example for a Python FastAPI backend. Remove or replace if using Go/Node/etc. -fastapi>=0.100.0 +fastapi>=0.121.2 uvicorn[standard]>=0.22.0 pydantic>=2.0 -sqlalchemy>=2.0 +sqlalchemy>=2.0.44 alembic>=1.17.2 python-dotenv>=1.0 -passlib[bcrypt]>=1.7 -httpx>=0.24 +passlib[bcrypt]>=1.7.4 +httpx>=0.28.1 requests>=2.31 -python-multipart>=0.0.6 +python-multipart>=0.0.20 # Add additional runtime libs below diff --git a/tools/dockerfile_check.sh b/tools/dockerfile_check.sh new file mode 100755 index 000000000..906483c4f --- /dev/null +++ b/tools/dockerfile_check.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Dockerfile validation script +# Checks for common mismatches between base images and package managers + +set -e + +DOCKERFILE="${1:-Dockerfile}" + +if [ ! -f "$DOCKERFILE" ]; then + echo "Error: $DOCKERFILE not found" + exit 1 +fi + +echo "Checking $DOCKERFILE for base image / package manager mismatches..." + +# Read file content +dockerfile_content=$(cat "$DOCKERFILE") + +# Check for golang:latest or golang:1.x (Debian) with apk commands in the same stage +while IFS= read -r line; do + if echo "$line" | grep -qE "^FROM\s+golang:(latest|[0-9]+(\.[0-9]+)?)\s"; then + # Found a Debian-based golang image, check the next 20 lines for apk + current_stage="$line" + checking_stage=true + elif echo "$line" | grep -qE "^FROM\s+" && [ "$checking_stage" = true ]; then + # New FROM statement, reset + checking_stage=false + fi + + if [ "$checking_stage" = true ] && echo "$line" | grep -qE "RUN.*apk\s+(add|update|del)"; then + echo "โŒ ERROR: Found Debian-based golang image with Alpine package manager (apk)" + echo " Stage: $current_stage" + echo " Command: $line" + echo " Fix: Use 'golang:alpine' or 'golang:1.x-alpine' instead" + exit 1 + fi +done < "$DOCKERFILE" + +# Check for node:latest or node:XX (Debian) with apk commands +if echo "$dockerfile_content" | grep -E "FROM\s+node:(latest|[0-9]+)\s" > /dev/null; then + if echo "$dockerfile_content" | grep -A 10 "FROM\s\+node:(latest|[0-9]\+)\s" | grep -E "RUN.*apk\s+(add|update)" > /dev/null; then + echo "โŒ ERROR: Found Debian-based node image (node:latest or node:XX) with Alpine package manager (apk)" + echo " Fix: Use 'node:alpine' or 'node:XX-alpine' instead" + exit 1 + fi +fi + +# Check for alpine images with apt/apt-get +if echo "$dockerfile_content" | grep -E "FROM\s+.*:.*alpine" > /dev/null; then + if echo "$dockerfile_content" | grep -A 10 "FROM\s\+.*:.*alpine" | grep -E "RUN.*(apt-get|apt)\s+(install|update)" > /dev/null; then + echo "โŒ ERROR: Found Alpine-based image with Debian package manager (apt/apt-get)" + echo " Fix: Use 'apk add' instead of 'apt install'" + exit 1 + fi +fi + +echo "โœ“ Dockerfile validation passed" +exit 0 diff --git a/tools/python_compile_check.sh b/tools/python_compile_check.sh index 3221756cd..2615f958d 100755 --- a/tools/python_compile_check.sh +++ b/tools/python_compile_check.sh @@ -2,4 +2,11 @@ set -euo pipefail # Run python -m compileall quietly to catch syntax errors in the repo. -python -m compileall -q . +if command -v python3 &>/dev/null; then + python3 -m compileall -q . +elif command -v python &>/dev/null; then + python -m compileall -q . +else + echo "Error: neither python3 nor python found." + exit 1 +fi