diff --git a/.gitattributes b/.gitattributes index 27c560a..4380649 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ .gitattributes text eol=lf *.sh text eol=lf +.github/workflows/*.yml text eol=lf deploy/docker/frontend/*.sh text eol=lf frontend/src/*.js text eol=lf frontend/src/**/*.js text eol=lf diff --git a/.github/workflows/_docker-build-push.yml b/.github/workflows/_docker-build-push.yml new file mode 100644 index 0000000..29862b0 --- /dev/null +++ b/.github/workflows/_docker-build-push.yml @@ -0,0 +1,116 @@ +name: Docker build & push (reusable) + +# Reusable workflow that builds the three image targets defined in +# deploy/docker/Dockerfile (backend, worker, frontend) and optionally pushes +# them to both ghcr.io and Docker Hub under the 3252a8/ namespace. +# +# Called by: +# - docker-dev.yml (tag_mode: dev, push: true) on pushes to dev +# - docker-release.yml (tag_mode: release, push: true) on pushes to main +# - ci.yml (tag_mode: dev, push: false) on pull requests + +on: + workflow_call: + inputs: + push: + description: "Push the built images to the registries" + type: boolean + default: true + tag_mode: + description: "Tagging strategy: 'dev' or 'release'" + type: string + required: true + +permissions: + contents: read + packages: write + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: backend + image: remnawave-minishop-backend + - target: worker + image: remnawave-minishop-worker + - target: frontend + image: remnawave-minishop-frontend + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Full history + tags: the Dockerfile's version-builder stage runs + # `git describe --tags` against the copied .git tree. + fetch-depth: 0 + + - name: Resolve release version + id: version + if: inputs.tag_mode == 'release' + run: | + # On a tag push github.ref_name is the tag (e.g. v3.4.5); for a + # manual workflow_dispatch on a branch, fall back to the latest tag. + if [ "${{ github.ref_type }}" = "tag" ]; then + raw="${{ github.ref_name }}" + else + raw="$(git describe --tags --abbrev=0 2>/dev/null)" + fi + version="${raw#v}" + if [ -z "$version" ]; then + echo "::error::No git tag found to derive the release version from" + exit 1 + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "Release version: ${version}" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: inputs.push + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + if: inputs.push + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + 3252a8/${{ matrix.image }} + ghcr.io/3252a8/${{ matrix.image }} + tags: | + type=raw,value=dev,enable=${{ inputs.tag_mode == 'dev' }} + type=sha,prefix=dev-,format=short,enable=${{ inputs.tag_mode == 'dev' }} + type=raw,value=latest,enable=${{ inputs.tag_mode == 'release' }} + type=raw,value=${{ steps.version.outputs.version }},enable=${{ inputs.tag_mode == 'release' }} + + - name: Build${{ inputs.push && ' & push' || '' }} ${{ matrix.image }} + uses: docker/build-push-action@v6 + with: + context: . + file: deploy/docker/Dockerfile + target: ${{ matrix.target }} + platforms: linux/amd64 + push: ${{ inputs.push }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # The Dockerfile's version-builder appends a "-" suffix to the + # internal version string for non-main builds. Force "main" on release + # (the ref is the tag, not a branch) so release images stay un-suffixed. + build-args: | + GITHUB_REF_NAME=${{ inputs.tag_mode == 'release' && 'main' || github.ref_name }} + cache-from: type=gha,scope=${{ matrix.target }} + cache-to: type=gha,mode=max,scope=${{ matrix.target }} + provenance: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..027a381 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: PR checks + +# Runs on pull requests into main (typically from dev) and into dev (typically +# from feature/* branches): lint + format checks and a no-push image build to +# prove the Docker images still build. + +on: + pull_request: + branches: [main, dev] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: Lint & format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install ruff + run: pip install "ruff>=0.8.0" + + - name: Ruff lint (Python) + run: ruff check . + + - name: Ruff format check (Python) + run: ruff format --check . + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend deps + run: npm ci + working-directory: frontend + + - name: ESLint (frontend) + run: npm run lint + working-directory: frontend + + - name: Prettier check (frontend) + run: npm run format:check + working-directory: frontend + + build: + name: Docker build + uses: ./.github/workflows/_docker-build-push.yml + with: + push: false + tag_mode: dev diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..97457a0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,48 @@ +name: CodeQL + +# Static analysis of the Python and JS/TS code for security and quality issues. +# Results appear under the repository's Security -> Code scanning tab. + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + schedule: + - cron: "27 3 * * 1" # weekly, Monday 03:27 UTC + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - language: python + - language: javascript-typescript + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: none + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..41c850d --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +name: Dependency review + +# On PRs into main/dev, flag any newly added dependency that has a known +# vulnerability or an incompatible license before it gets merged. + +on: + pull_request: + branches: [main, dev] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Dependency review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: on-failure diff --git a/.github/workflows/docker-dev.yml b/.github/workflows/docker-dev.yml new file mode 100644 index 0000000..93d3f88 --- /dev/null +++ b/.github/workflows/docker-dev.yml @@ -0,0 +1,25 @@ +name: Dev images + +# On every push to the dev branch, build all three images and push them to +# ghcr.io and Docker Hub tagged `dev` and `dev-`. + +on: + push: + branches: [dev] + workflow_dispatch: + +concurrency: + group: docker-dev-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + +jobs: + build-push: + uses: ./.github/workflows/_docker-build-push.yml + with: + push: true + tag_mode: dev + secrets: inherit diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..56bfbab --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,28 @@ +name: Release images + +# Build all three images and push them to ghcr.io and Docker Hub tagged +# `latest` and the release version (the pushed tag with its leading `v` +# stripped, e.g. v3.4.5 -> 3.4.5). Triggered only when a new v* tag is pushed, +# so images are built once per release rather than on every commit to main. + +on: + push: + tags: + - "v*" + workflow_dispatch: + +concurrency: + group: docker-release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + packages: write + +jobs: + build-push: + uses: ./.github/workflows/_docker-build-push.yml + with: + push: true + tag_mode: release + secrets: inherit diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..80f74eb --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,89 @@ +name: Security + +# Audits the full dependency set (pip-audit, npm audit) and runs a Trivy +# filesystem scan (dependencies + Dockerfile/IaC misconfig). Trivy results are +# uploaded to the Security -> Code scanning tab. + +on: + pull_request: + branches: [main, dev] + push: + branches: [main, dev] + schedule: + - cron: "27 4 * * 1" # weekly, Monday 04:27 UTC + workflow_dispatch: + +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + python-audit: + name: pip-audit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install pip-audit + run: pip install pip-audit + + - name: Audit Python dependencies + run: pip-audit -r backend/requirements.txt + + npm-audit: + name: npm audit + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install frontend deps + run: npm ci + working-directory: frontend + + - name: Audit npm dependencies + run: npm audit --audit-level=high + working-directory: frontend + + trivy: + name: Trivy filesystem scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Trivy + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: fs + scan-ref: . + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + ignore-unfixed: true + + - name: Upload Trivy results + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-results.sarif + category: trivy-fs