diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b479192..6dfc9ad 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,6 +19,61 @@ jobs: - name: Run actionlint run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:latest + lint-yaml: + name: Lint YAML + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install lint dependencies + run: | + python -m pip install --upgrade pip + pip install ".[dev]" + + - name: Run yamllint + run: yamllint --strict . + + lint-js: + name: Lint JavaScript + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: app/package.json + + - name: Install Node dependencies + working-directory: app + run: npm install + + - name: Run eslint + working-directory: app + run: npx eslint . + + lint-shell: + name: Lint shell scripts + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Run shellcheck + run: docker run --rm -v "$PWD:/mnt" -w /mnt koalaman/shellcheck:stable scripts/*.sh + lint-python: name: Lint Python runs-on: ubuntu-latest diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..8fce015 --- /dev/null +++ b/.yamllint @@ -0,0 +1,23 @@ +--- +# Correctness only. No `extends: default`, so nothing but the rules below runs: +# the repository predates this linter and its cosmetic findings (indentation, +# line length, trailing spaces) would be noise nobody acts on. +# +# key-duplicates is the rule that earns its keep: PyYAML keeps the last of two +# identical keys without complaining, so a duplicated entry in a translation +# catalogue silently drops a translation. +ignore: | + .git/ + .venv/ + venv/ + node_modules/ + app/node_modules/ + app/static/vendor/ + build/ + .ruff_cache/ + +rules: + key-duplicates: enable + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true diff --git a/Makefile b/Makefile index 9dc9a2b..666efed 100644 --- a/Makefile +++ b/Makefile @@ -78,6 +78,8 @@ run-dev: env config config -p "$$PORT:$$PORT" \ --name portfolio \ -v "$(PWD)/app/:/app" \ + -e PORT="$$PORT" \ + -e TRUSTED_HOSTS="$$TRUSTED_HOSTS" \ -e FLASK_APP=app.py \ -e FLASK_ENV=development \ "$$IMAGE_NAME" @@ -89,6 +91,8 @@ run-prod: env config config docker run -d \ -p "$$PORT:$$PORT" \ --name portfolio \ + -e PORT="$$PORT" \ + -e TRUSTED_HOSTS="$$TRUSTED_HOSTS" \ "$$IMAGE_NAME" .PHONY: logs @@ -132,6 +136,14 @@ install-dev: # Install runtime and developer dependencies from pyproject.toml. $(PYTHON) -m pip install -e ".[dev]" +.PHONY: i18n +i18n: env config + # Fill missing content translations in app/i18n/content/ via LibreTranslate. + @$(call _require_env,LIBRETRANSLATE_URL); \ + $(PYTHON) utils/i18n_sync.py \ + --url "$$LIBRETRANSLATE_URL" \ + --api-key "$$LIBRETRANSLATE_API_KEY" + .PHONY: lint-actions lint-actions: # Lint GitHub Actions workflows. @@ -148,6 +160,21 @@ lint-docker: # Lint the Dockerfile. docker run --rm -i hadolint/hadolint < Dockerfile +.PHONY: lint-yaml +lint-yaml: install-dev + # Lint YAML for duplicate keys and ambiguous scalars (see .yamllint). + $(PYTHON) -m yamllint --strict . + +.PHONY: lint-js +lint-js: node-deps + # Lint the browser and Cypress JavaScript. + cd app && env -u ELECTRON_RUN_AS_NODE npx eslint . + +.PHONY: lint-shell +lint-shell: + # Lint the shell scripts. + docker run --rm -v "$$PWD:/mnt" -w /mnt koalaman/shellcheck:stable scripts/*.sh + .PHONY: test-lint test-lint: # Run lint guardrail tests. @@ -169,7 +196,7 @@ test-security: install $(PYTHON) -m unittest discover -s tests/security -t . .PHONY: lint -lint: lint-actions lint-python lint-docker test-lint +lint: lint-actions lint-python lint-yaml lint-js lint-docker lint-shell test-lint # Run the full lint suite. .PHONY: security @@ -179,9 +206,20 @@ security: install-dev test-security $(PYTHON) utils/export_runtime_requirements.py > /tmp/portfolio-runtime-requirements.txt $(PYTHON) -m pip_audit -r /tmp/portfolio-runtime-requirements.txt +.PHONY: node-deps +node-deps: + # Install the Cypress binary and the browser vendor assets into app/. + cd app && npm install + .PHONY: test-e2e -test-e2e: - # Run Cypress end-to-end tests via act (stop portfolio container to free port first). +test-e2e: env config node-deps + # Run Cypress against a locally started Flask app — no act, no runner image. + @$(call _require_env,PORT); \ + PORT="$$PORT" PYTHON="$(PYTHON)" scripts/run-e2e.sh + +.PHONY: test-e2e-act +test-e2e-act: + # Run the CI end-to-end job through act (stop portfolio container to free port first). -docker stop portfolio 2>/dev/null || true $(ACT) workflow_dispatch -W .github/workflows/tests.yml -j e2e -docker start portfolio 2>/dev/null || true diff --git a/app/eslint.config.js b/app/eslint.config.js new file mode 100644 index 0000000..50ea303 --- /dev/null +++ b/app/eslint.config.js @@ -0,0 +1,78 @@ +'use strict'; +/** + * Correctness only — eslint's recommended set, no stylistic rules. + * + * The browser scripts are plain