diff --git a/.editorconfig b/.editorconfig index 35e3b0b..ad2f195 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,18 +1,25 @@ root = true [*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true +charset=utf-8 +end_of_line=lf +indent_size=2 +indent_style=space +insert_final_newline=true +tab_width=2 +trim_trailing_whitespace=true -[*.{json, yaml, yml}] -indent_size = 2 +[*.{bat,cmd,ps1}] +end_of_line=crlf -[*.md] -trim_trailing_whitespace = false +[*.{md,mdx}] +trim_trailing_whitespace=false -[Makefile] -indent_style = tab +[*.{py,rs}] +indent_size=4 +tab_width=4 + +[{*.{go,lua,tsv},go.{mod,sum},Makefile}] +indent_size=4 +indent_style=tab +tab_width=4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 767b388..8a0e190 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,12 @@ on: workflow_call: workflow_dispatch: +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} + jobs: python: strategy: @@ -18,19 +24,15 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] python: [3.12] - name: python runs-on: ${{ matrix.os }} env: ENVIRONMENT: ci steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Set up UV - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5 + - uses: astral-sh/setup-uv@4db96194c378173c656ce18a155ffc14a9fc4355 # v5 - - name: Set up Python - id: setup-python + - id: setup-python uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5 with: python-version: ${{ matrix.python }} @@ -39,39 +41,35 @@ jobs: if: ${{ runner.os != 'Windows' }} run: | mkdir -p .venv - echo "$(realpath .venv)/bin" >> ${GITHUB_PATH} + echo "$(realpath .venv)/bin" >> "${GITHUB_PATH}" - name: Set up environment (Windows) if: ${{ runner.os == 'Windows' }} run: | New-Item -Type Directory -Force .venv - "$(Resolve-Path .venv)/Scripts" | Out-File -FilePath ${env:GITHUB_PATH} -Append + "$(Resolve-Path .venv)/Scripts" | Out-File -FilePath "${env:GITHUB_PATH}" -Append - - name: Install dependencies - run: uv sync + - run: uv sync - - name: Lint and test - run: make lint test + - run: make lint test docker: - name: docker permissions: contents: read packages: write + runs-on: ubuntu-latest env: GHCR_IMAGE_NAME: ghcr.io/${{ github.repository }} steps: - - name: Checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: sparse-checkout: | Dockerfile uv.lock - - name: Cache buildkit mounts - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 + - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 with: path: | var-cache-apt @@ -82,8 +80,7 @@ jobs: buildkit-mounts-${{ runner.os }} buildkit-mounts-${{ runner.os }} - - name: Inject cache into docker - uses: reproducible-containers/buildkit-cache-dance@5b6db76d1da5c8b307d5d2e0706d266521b710de # v3 + - uses: reproducible-containers/buildkit-cache-dance@5b6db76d1da5c8b307d5d2e0706d266521b710de # v3 with: cache-map: | { @@ -93,11 +90,9 @@ jobs: "root-cache-uv": "/root/.cache/uv" } - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3 + - uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3 - - name: Build CI image - id: build-ci + - id: build-ci env: ENVIRONMENT: ci uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 @@ -108,25 +103,21 @@ jobs: ${{ env.GHCR_IMAGE_NAME }}:cache load: true - - name: Run CI image - run: docker run --rm ${{ steps.build-ci.outputs.imageid }} + - run: docker run --rm ${{ steps.build-ci.outputs.imageid }} - - name: Docker metadata - id: docker_metadata + - id: docker_metadata uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5 with: images: ${{ env.GHCR_IMAGE_NAME }} - - name: Login to GHCR - if: ${{ github.event_name != 'pull_request' }} + - if: ${{ github.event_name != 'pull_request' }} uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push dev image - if: ${{ github.event_name != 'pull_request' }} + - if: ${{ github.event_name != 'pull_request' }} env: ENVIRONMENT: dev uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 @@ -139,10 +130,7 @@ jobs: tags: ${{ env.GHCR_IMAGE_NAME }}:dev push: ${{ github.event_name != 'pull_request' }} - - name: Build and push prod image - if: ${{ github.event_name != 'pull_request' }} - env: - ENVIRONMENT: prod + - if: ${{ github.event_name != 'pull_request' }} uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 with: cache-from: | @@ -153,3 +141,5 @@ jobs: labels: ${{ steps.docker_metadata.outputs.labels }} annotations: ${{ steps.docker_metadata.outputs.annotations }} push: ${{ github.event_name != 'pull_request' }} + env: + ENVIRONMENT: prod diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 0000000..fc1696b --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,35 @@ +name: dependabot +on: + pull_request: + branches: [main] + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} + +jobs: + dependabot: + permissions: + contents: write + pull-requests: write + + if: ${{ github.actor == 'dependabot[bot]' }} + runs-on: ubuntu-latest + steps: + - id: metadata + uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2 + + - name: log metadata + run: echo "${DEPENDABOT_METADATA}" + env: + DEPENDABOT_METADATA: ${{ toJson(steps.metadata.outputs) }} + + - name: automerge + if: ${{ !contains(steps.metadata.outputs.update-type, 'major' ) }} + run: gh pr merge --auto --squash "${PR_NUMBER}" + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + GH_REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/ossf.yml b/.github/workflows/ossf.yml new file mode 100644 index 0000000..6af6928 --- /dev/null +++ b/.github/workflows/ossf.yml @@ -0,0 +1,46 @@ +name: ossf +on: + push: + branches: [main] + pull_request: + branches: [main] + merge_group: + branches: [main] + workflow_call: + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} + +jobs: + ossf: + permissions: + contents: read + # Needed for GitHub OIDC token if publish_results is true + id-token: write + # Needed for Code scanning upload + security-events: write + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2 + with: + results_file: results.sarif + results_format: sarif + # Scorecard team runs a weekly scan of public GitHub repos, + # see https://github.com/ossf/scorecard#public-data. + # Setting `publish_results: true` helps us scale by leveraging your workflow to + # extract the results instead of relying on our own infrastructure to run scans. + # And it's free for you! + publish_results: true + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a57fc0d..45e7a80 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,66 +1,46 @@ name: pr - on: pull_request: - types: - - opened - - edited - - reopened - - synchronize + types: [opened, synchronize, reopened, edited] + branches: [main] + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} jobs: lint-title: permissions: pull-requests: read + runs-on: ubuntu-latest steps: - - name: semantic-pull-request - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} label: permissions: contents: read pull-requests: write + runs-on: ubuntu-latest steps: - - name: labeler - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 + - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5 label-size: permissions: contents: read pull-requests: write + runs-on: ubuntu-latest steps: - - name: size-label - uses: pascalgn/size-label-action@f8edde36b3be04b4f65dcfead05dc8691b374348 # v0.5.5 + - uses: pascalgn/size-label-action@f8edde36b3be04b4f65dcfead05dc8691b374348 # v0.5.5 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} IGNORED: | package-lock.json *.lock docs/** - - dependabot: - if: github.actor == 'dependabot[bot]' - permissions: - contents: write - pull-requests: write - runs-on: ubuntu-latest - steps: - - id: metadata - uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2 - - - name: log metadata - env: - DEPENDABOT_METADATA: ${{ toJson(steps.metadata.outputs) }} - run: echo ${DEPENDABOT_METADATA} - - - name: automerge - if: ${{ !contains(steps.metadata.outputs.update-type, 'major' ) }} - run: gh pr merge --auto --squash ${PR_NUMBER} - env: - PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }} - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/scans.yml b/.github/workflows/scans.yml new file mode 100644 index 0000000..0706941 --- /dev/null +++ b/.github/workflows/scans.yml @@ -0,0 +1,162 @@ +name: scans +on: + push: + branches: [main] + pull_request: + branches: [main] + merge_group: + branches: [main] + workflow_call: + workflow_dispatch: + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_name != github.event.repository.default_branch }} + +jobs: + devskim: + permissions: + contents: read + security-events: write + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: microsoft/DevSkim-Action@a6b6966a33b497cd3ae2ebc406edf8f4cc2feec6 # v1 + + - uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + with: + sarif_file: devskim-results.sarif + + megalinter: + permissions: + contents: write + pull-requests: write + security-events: write + statuses: write + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - id: megalinter + # You can override MegaLinter flavor used to have faster performances + # More info at https://megalinter.io/latest/flavors/ + uses: oxsecurity/megalinter@ec124f7998718d79379a3c5b39f5359952baf21d # v8 + env: + # All available variables are described in documentation + # https://megalinter.io/latest/configuration/ + APPLY_FIXES: all + DISABLE_LINTERS: JSON_JSONLINT,SPELL_CSPELL + DISABLE_ERRORS_LINTERS: REPOSITORY_DEVSKIM,REPOSITORY_KICS + FAIL_IF_UPDATED_SOURCES: true + GITHUB_STATUS_REPORTER: true + GITHUB_TOKEN: ${{ github.token }} + PYTHON_DEFAULT_STYLE: ruff + SARIF_REPORTER: true + VALIDATE_ALL_CODEBASE: false + + - if: ${{ success() || failure() }} + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 + with: + name: megalinter-reports + path: | + megalinter-reports + mega-linter.log + + - if: ${{ success() || failure() }} + uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + with: + sarif_file: megalinter-reports/megalinter-report.sarif + ref: ${{ github.head_ref && format('refs/heads/{0}', github.head_ref) || github.ref }} + sha: ${{ github.event.pull_request.head.sha || github.sha }} + + - if: ${{ failure() && steps.megalinter.outputs.has_updated_sources == 1 && github.event_name == 'pull_request' }} + name: commit changes + run: | + git config user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" + git config user.name "${GITHUB_ACTOR}" + git commit --all --message "${COMMIT_MESSAGE}" + git push origin "HEAD:refs/heads/${GITHUB_HEAD_REF}" + env: + COMMIT_MESSAGE: "fix: apply megalinter fixes" + # https://api.github.com/users/megalinter-bot + GITHUB_ACTOR: megalinter-bot + GITHUB_ACTOR_ID: 129584137 + + msdo: + permissions: + contents: read + id-token: write + security-events: write + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: microsoft/security-devops-action@08976cb623803b1b36d7112d4ff9f59eae704de0 # v1 + id: msdo + + - uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + with: + sarif_file: ${{ steps.msdo.outputs.sarifFile }} + + osv-scan-pr: + permissions: + actions: read + contents: read + security-events: write + + if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@764c91816374ff2d8fc2095dab36eecd42d61638 # v1 + + osv-scan-push: + permissions: + actions: read + contents: read + security-events: write + + if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@764c91816374ff2d8fc2095dab36eecd42d61638 # v1 + + trivy: + permissions: + contents: read + security-events: write + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # 0.29.0 + with: + scan-type: fs + exit-code: 1 + ignore-unfixed: true + severity: HIGH,CRITICAL + format: sarif + output: trivy-results.sarif + + - uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + with: + sarif_file: "trivy-results.sarif" + + trufflehog: + permissions: + contents: read + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + fetch-depth: 3 + + - uses: trufflesecurity/trufflehog@709cd089144a6b6452f0915e7ea0d5e4a39d3243 # v3 + with: + extra_args: --results=verified,unknown diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7d5436..7318b5c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -95,7 +95,7 @@ repos: - id: taplo-format - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.5.30 + rev: 0.6.1 hooks: - id: uv-lock diff --git a/Dockerfile b/Dockerfile index 37462db..dbed3a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ LABEL maintainer="wyextay@gmail.com" # set up user ARG USER=user ARG UID=1000 -RUN useradd --no-create-home --shell /bin/false --uid ${UID} ${USER} +RUN useradd --create-home --shell /bin/false --uid ${UID} ${USER} # set up environment ARG APP_HOME=/work/app @@ -31,14 +31,16 @@ APT::Install-Suggests "false"; APT::AutoRemove::RecommendsImportant "false"; APT::AutoRemove::SuggestsImportant "false"; EOF + RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ rm -f /etc/apt/apt.conf.d/docker-clean && \ echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \ apt-get update && \ - apt-get install --yes \ - build-essential \ - curl + apt-get install --yes --no-install-recommends \ + build-essential=12.9 \ + curl=7.88.1-10+deb12u8 \ + && rm -rf /var/lib/apt/lists/* ARG PIP_DISABLE_PIP_VERSION_CHECK=1 \ PIP_NO_CACHE_DIR=0 \ @@ -49,16 +51,16 @@ ARG PIP_DISABLE_PIP_VERSION_CHECK=1 \ # set up python COPY --from=ghcr.io/astral-sh/uv:latest@sha256:90daa0b4d74ea55c7b8e06d25d3826b1eac66e7994387248e6173dd2b66668e2 /uv /uvx /bin/ -COPY --chown=${USER}:${USER} pyproject.toml uv.lock ./ +COPY pyproject.toml uv.lock ./ RUN --mount=type=cache,target=/root/.cache/uv \ - uv venv --seed ${VIRTUAL_ENV} && \ + uv venv --seed "${VIRTUAL_ENV}" && \ uv sync --frozen --no-default-groups --no-install-project && \ - chown -R ${USER}:${USER} ${VIRTUAL_ENV} && \ - chown -R ${USER}:${USER} ${APP_HOME} && \ + chown -R "${USER}:${USER}" "${VIRTUAL_ENV}" && \ + chown -R "${USER}:${USER}" "${APP_HOME}" && \ uv pip list # set up project -COPY --chown=${USER}:${USER} src src +COPY src src RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-default-groups @@ -76,12 +78,13 @@ FROM dev AS ci USER root RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen && \ - chown -R ${USER}:${USER} ${VIRTUAL_ENV} && \ uv pip list -COPY --chown=${USER}:${USER} tests tests -COPY --chown=${USER}:${USER} Makefile Makefile +COPY tests tests +COPY Makefile Makefile +USER ${USER} +RUN mkdir -p "${HOME}/.cache" CMD ["make", "lint", "test"] ## @@ -91,10 +94,12 @@ FROM base AS prod # set up project USER ${USER} -COPY --from=dev --chown=${USER}:${USER} ${VIRTUAL_ENV} ${VIRTUAL_ENV} -COPY --from=dev --chown=${USER}:${USER} ${APP_HOME} ${APP_HOME} +COPY --from=dev ${VIRTUAL_ENV} ${VIRTUAL_ENV} +COPY --from=dev ${APP_HOME} ${APP_HOME} EXPOSE 8000 ARG ENVIRONMENT=prod ENV ENVIRONMENT=${ENVIRONMENT} CMD ["gunicorn", "-c", "python:example_app.gunicorn_conf"] + +HEALTHCHECK CMD ["curl", "-f", "http://localhost/"] diff --git a/compose.yaml b/compose.yaml index 2e98f89..20e7c21 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,9 +10,13 @@ services: - type=inline target: dev ports: - - "8000:8000" + - 8000:8000 volumes: - .:/opt/app + cap_drop: + - all + security_opt: + - no-new-privileges:true profiles: - dev app_ci: @@ -27,7 +31,11 @@ services: - type=inline target: ci ports: - - "8000:8000" + - 8000:8000 + cap_drop: + - all + security_opt: + - no-new-privileges:true profiles: - ci app: @@ -41,6 +49,10 @@ services: - ghcr.io/yxtay/python-example-app:main target: prod ports: - - "8000:8000" + - 8000:8000 + cap_drop: + - all + security_opt: + - no-new-privileges:true profiles: - prod