diff --git a/.commitlintrc.js b/.commitlintrc.js new file mode 100644 index 0000000..682fce7 --- /dev/null +++ b/.commitlintrc.js @@ -0,0 +1,43 @@ +const Configuration = { + /* + * Resolve and load @commitlint/config-conventional from node_modules. + * Referenced packages must be installed + */ + extends: ["@commitlint/config-conventional"], + // config-conventional implements conventional commits rules as defined at https://www.conventionalcommits.org + + /* + * Any rules defined here will override rules from @commitlint/config-conventional + */ + rules: { + "type-enum": [1, "always", ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test']], + "header-trim": [2, "always"], + "reference-empty": [2, "never"], + }, + + /* + * Array of functions that return true if commitlint should ignore the given message. + * Given array is merged with predefined functions, which consist of matchers like: + * + * - 'Merge pull request', 'Merge X into Y' or 'Merge branch X' + * - 'Revert X' + * - 'v1.2.3' (ie semver matcher) + * - 'Automatic merge X' or 'Auto-merged X into Y' + * + * To see full list, check https://github.com/conventional-changelog/commitlint/blob/master/%40commitlint/is-ignored/src/defaults.ts. + * To disable those ignores and run rules always, set `defaultIgnores: false` as shown below. + */ + ignores: [(commit) => commit === ""], + /* + * Whether commitlint uses the default ignore rules, see the description above. + */ + defaultIgnores: true, + + /* + * Custom URL to show upon failure + */ + helpUrl: + "", +}; + +export default Configuration; diff --git a/.github/pr406.yml b/.github/pr406.yml new file mode 100644 index 0000000..f86d26d --- /dev/null +++ b/.github/pr406.yml @@ -0,0 +1,4 @@ +threshold: 7 +dry_run: true +label: likely ai-slop +close_on_trigger: false diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml new file mode 100644 index 0000000..92f85f3 --- /dev/null +++ b/.github/workflows/check_dependencies.yml @@ -0,0 +1,17 @@ +--- +name: 'Dependency Check' + +on: [pull_request] + +jobs: + dependency-review: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v6 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/check_integrity.yml b/.github/workflows/check_integrity.yml new file mode 100644 index 0000000..72e9766 --- /dev/null +++ b/.github/workflows/check_integrity.yml @@ -0,0 +1,59 @@ +--- +# Build the project and use django's check command to check for common errors. +name: Check Integrity + +on: [push, pull_request, workflow_dispatch, workflow_call] + +jobs: + python-versions: + uses: ./.github/workflows/extract_python_versions.yml + + check-integrity: + runs-on: ubuntu-latest + needs: python-versions + + permissions: + contents: read + pull-requests: write + issues: write + statuses: write + + strategy: + matrix: + python-version: ${{ fromJson(needs['python-versions'].outputs.versions) }} + + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install . + - name: Check integrity + run: | + python src/manage.py check --deploy --fail-level=WARNING + + update-readme-integrity-badges: + runs-on: ubuntu-latest + needs: [python-versions, check-integrity] + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.ref }} + - name: Add integrity badges to README between comments + env: + PYTHON_VERSIONS: ${{ needs['python-versions'].outputs.versions }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: python .github/workflows/update_integrity_badges.py + - uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: 'ci: update integrity badges in README' + file_pattern: README.md diff --git a/.github/workflows/detect_ai.yml b/.github/workflows/detect_ai.yml new file mode 100644 index 0000000..19bf56f --- /dev/null +++ b/.github/workflows/detect_ai.yml @@ -0,0 +1,24 @@ +--- +name: Detect AI + +on: + pull_request_target: + types: [opened, reopened, synchronize, edited] + +jobs: + pr406: + name: Check for AI-generated PRs + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.base.ref }} + - uses: lu-zhengda/pr406@v0.1.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + config_path: .github/pr406.yml diff --git a/.github/workflows/extract_python_versions.yml b/.github/workflows/extract_python_versions.yml new file mode 100644 index 0000000..4473c86 --- /dev/null +++ b/.github/workflows/extract_python_versions.yml @@ -0,0 +1,56 @@ +--- +name: Extract Python Versions + +description: Extract Python versions from pyproject.toml classifiers and output as JSON array for use in other workflows. + +on: + workflow_call: + outputs: + versions: + description: JSON array of Python versions from pyproject.toml classifiers + value: ${{ jobs.extract.outputs.versions }} + +jobs: + extract: + runs-on: ubuntu-latest + outputs: + versions: ${{ steps.get.outputs.versions }} + + steps: + - uses: actions/checkout@v6 + + - name: Read versions from pyproject.toml + id: get + run: | + python - <<'PY' + import json + import re + import tomllib + from pathlib import Path + + pyproject = Path('pyproject.toml') + if not pyproject.exists(): + raise SystemExit('pyproject.toml not found') + + data = tomllib.loads(pyproject.read_text(encoding='utf-8')) + classifiers = data.get('project', {}).get('classifiers', []) + + pattern = re.compile(r'^Programming Language :: Python :: (\d+\.\d+)$') + versions = sorted( + { + match.group(1) + for classifier in classifiers + if (match := pattern.match(classifier)) + }, + key=lambda version: tuple(map(int, version.split('.'))), + ) + + if not versions: + raise SystemExit( + 'No Python version classifiers found in pyproject.toml under project.classifiers' + ) + + output = Path(__import__('os').environ['GITHUB_OUTPUT']) + output.write_text(f"versions={json.dumps(versions)}\n", encoding='utf-8') + print(f'Extracted versions: {", ".join(versions)}') + PY diff --git a/.github/workflows/owasp-noir.yml b/.github/workflows/owasp-noir.yml new file mode 100644 index 0000000..58d3b1d --- /dev/null +++ b/.github/workflows/owasp-noir.yml @@ -0,0 +1,51 @@ +--- +name: OWASP Noir Security Analysis + +on: [push, pull_request, workflow_dispatch, workflow_call] + +jobs: + noir-analysis: + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + issues: write + + steps: + - uses: actions/checkout@v6 + + - name: Run OWASP Noir + id: noir + uses: owasp-noir/noir@main + with: + base_path: '.' + techs: 'django' + passive_scan: 'true' + passive_scan_severity: 'medium' + use_all_taggers: 'true' + include_path: 'true' + format: 'markdown-table' + output_file: 'noir_results.md' + + # Optional: Higher concurrency can speed up the analysis but may increase resource usage + # and could lead to OOM errors on larger codebases. + # Adjust as needed. 20 is reasonable for default GitHub runners. + concurrency: '20' + + - name: Upload Noir Reports + uses: actions/upload-artifact@v7 + if: always() # Always upload results even if vulnerabilities are found + with: + name: noir-results + path: noir_results.md + + - name: Comment Noir Report on PR + if: github.event_name == 'pull_request' && steps.noir.outputs.report != '' + uses: peter-evans/create-or-update-comment@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ## OWASP Noir Security Analysis Report + ${{ steps.noir.outputs.report }} diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml new file mode 100644 index 0000000..f8e8495 --- /dev/null +++ b/.github/workflows/super-linter.yml @@ -0,0 +1,42 @@ +--- +# This runs classic linters on the codebase, but also uses some more advanced tools. +# Also uses: Trivy, GitLeaks, codespell, pre-commit, spectral, jscpd and more. +# See https://github.com/marketplace/actions/super-linter for more details. +name: Super-Linter + +on: [push, pull_request, workflow_dispatch] + +jobs: + build: + name: Super-Linter + runs-on: ubuntu-latest + + permissions: + # contents permission to clone the repository + contents: read + packages: read + # issues and pull-requests permissions to write results as pull + # request comments. Omit them if you don't need summary comments + issues: write + pull-requests: write + # To report GitHub Actions status checks. Omit if you don't need + # to update commit status + statuses: write + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + # super-linter needs the full git history to get the + # list of files that changed across commits + fetch-depth: 0 + persist-credentials: false + + - name: Super-linter + uses: super-linter/super-linter/slim@v8.5.0 # x-release-please-version + env: + # To report GitHub Actions status checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # To only check files that changed across commits + VALIDATE_ALL_CODEBASE: false + diff --git a/.github/workflows/update_integrity_badges.py b/.github/workflows/update_integrity_badges.py new file mode 100644 index 0000000..5cbcaed --- /dev/null +++ b/.github/workflows/update_integrity_badges.py @@ -0,0 +1,48 @@ +import json +import os +import sys +from pathlib import Path + +readme = Path('README.md') +start_marker = '' +end_marker = '' + +content = readme.read_text(encoding='utf-8') +if start_marker not in content or end_marker not in content: + print('README markers were not found.', file=sys.stderr) + sys.exit(1) + +raw_versions = os.environ.get('PYTHON_VERSIONS', '') + +try: + parsed_versions = json.loads(raw_versions) +except json.JSONDecodeError as exc: + print(f'PYTHON_VERSIONS must be a JSON array of strings: {exc}', file=sys.stderr) + sys.exit(1) + +if not isinstance(parsed_versions, list) or not all( + isinstance(version, str) for version in parsed_versions +): + print('PYTHON_VERSIONS must be a JSON array of strings.', file=sys.stderr) + sys.exit(1) + +versions = [version.strip() for version in parsed_versions if version.strip()] +if not versions: + print('PYTHON_VERSIONS JSON array is empty.', file=sys.stderr) + sys.exit(1) + +repository = os.environ['GITHUB_REPOSITORY'] +workflow = 'check_integrity.yml' + +badges = [ + f'[![Python {version}]' + f'(https://img.shields.io/badge/Python-{version}-3776AB?logo=python&logoColor=white)]' + f'(https://github.com/{repository}/actions/workflows/{workflow})' + for version in versions +] + +start_index = content.index(start_marker) + len(start_marker) +end_index = content.index(end_marker) +replacement = '\n' + '\n'.join(badges) + '\n' +updated = content[:start_index] + replacement + content[end_index:] +readme.write_text(updated, encoding='utf-8') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b9b7525..0952287 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,3 +36,8 @@ repos: rev: v8.30.0 hooks: - id: gitleaks + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: + hooks: + - id: commitlint + stages: [commit-msg] diff --git a/README.md b/README.md index 030bd3b..36008b8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Shiftings +[![Super-Linter](https://github.com///actions/workflows//badge.svg)](https://github.com/marketplace/actions/super-linter) + + + This is a simple shift management system. Built and maintained by students from the self-governed student dormitory [HaDiKo](https://www.hadiko.de/). ## Description