diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index f52ad4e..a97018a 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -1,11 +1,8 @@ name: Python CI on: - pull_request: - branches: - - "**" - # This is so we can call CI locally from other workflows that might want to - # run CI before doing whatever task they're doing. Like the release workflow. + # Invoked by ci.yml, which runs on pull requests and aggregates every check + # into a single `gate` job, and by release.yml before it publishes anything. workflow_call: concurrency: diff --git a/.github/workflows/brand-ci.yml b/.github/workflows/brand-ci.yml new file mode 100644 index 0000000..86912d0 --- /dev/null +++ b/.github/workflows/brand-ci.yml @@ -0,0 +1,52 @@ +name: Brand CI + +on: + # Invoked by ci.yml, which aggregates every check into a single `gate` job. + workflow_call: + +concurrency: + # See backend-ci.yml for why non-pull_request events key on the run id. + group: ${{ github.workflow }}-brand-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + working-directory: "./brand-sample" + +jobs: + build: + name: build + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: './brand-sample/.nvmrc' + cache: 'npm' + cache-dependency-path: './brand-sample/package-lock.json' + + # `npm ci` rather than `npm install`, for the same reason as + # frontend-ci.yml: install the lockfile exactly and fail on drift. + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + # dist/ is committed and served to real MFEs straight from GitHub via + # https://cdn.jsdelivr.net/gh/openedx/sample-plugin@main/brand-sample/dist/ + # so it is a published artifact, not a build leftover. Bumping + # package-lock.json does not regenerate it, which means a dependency + # update can silently leave the CSS people download out of step with the + # sources here. Rebuilding and diffing is what catches that. + # + # If this step fails, the fix is to run `npm run build` in brand-sample + # and commit the result. + - name: Check that the committed dist/ matches the build + run: git diff --exit-code -- dist/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f3b81b5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +# Every check that runs on a pull request, aggregated behind a single `gate` +# job. +# +# The point of the aggregation is branch protection. GitHub's required status +# checks are matched by name, so requiring the individual jobs means the +# required list has to spell out every matrix leg -- `tests (ubuntu-latest, +# 3.12, django52)` and friends -- and adding or retiring a Python or Django +# version silently breaks protection until somebody remembers to update the +# repository settings. Worse, two of the per-package jobs were both called +# `build`, so they could not be told apart in that list at all. +# +# `gate` succeeds only when every job it depends on succeeded, so `gate` is +# the only name that has to be required. It never changes, whatever happens to +# the matrix. +# +# This matters for automerge specifically: GitHub's auto-merge waits for +# *required* checks and nothing else, so a check that runs but is not required +# cannot hold a merge back. + +on: + pull_request: + branches: + - "**" + # So the release workflow can run every check before it publishes anything. + workflow_call: + +concurrency: + # See backend-ci.yml for why non-pull_request events key on the run id. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + backend: + uses: ./.github/workflows/backend-ci.yml + + frontend: + uses: ./.github/workflows/frontend-ci.yml + + tutor: + uses: ./.github/workflows/tutor-ci.yml + + brand: + uses: ./.github/workflows/brand-ci.yml + + gate: + name: gate + # `always()` so the gate still reports when something upstream fails. + # Without it the gate would be skipped, and a skipped required check never + # reports at all, which blocks the pull request forever instead of failing + # it. + if: always() + needs: [backend, frontend, tutor, brand] + runs-on: ubuntu-latest + + steps: + # Deliberately not using a third-party aggregation action. This job is + # the one thing standing between a pull request and `main`, so it is the + # last place worth adding a dependency -- and the logic is five lines. + # + # Every result must be `success`. Anything else fails the gate, + # `skipped` included: no job here is conditional, so a skip means + # something went wrong rather than something was legitimately not + # needed. If a genuinely optional job is added later, this is the place + # that has to learn about it. + - name: Check that every job succeeded + run: | + results='${{ join(needs.*.result, ' ') }}' + echo "Job results: $results" + for result in $results; do + if [ "$result" != "success" ]; then + echo "::error::A required job reported '$result'." + exit 1 + fi + done + echo "All jobs succeeded." diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index 4499281..a8df0b8 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -1,10 +1,7 @@ name: Frontend CI on: - pull_request: - branches: - - "**" - # So the release workflow can run these checks before it publishes anything. + # Invoked by ci.yml, which aggregates every check into a single `gate` job. workflow_call: concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8c13ec..abe7ebf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,21 +5,18 @@ on: branches: [main] jobs: - run_backend_tests: - uses: ./.github/workflows/backend-ci.yml - - run_frontend_tests: - uses: ./.github/workflows/frontend-ci.yml - - run_tutor_tests: - uses: ./.github/workflows/tutor-ci.yml + # The same aggregate a pull request has to pass, so the release runs exactly + # the checks that were required to merge -- and picks up any package added to + # ci.yml later without needing a change here. + run_ci: + uses: ./.github/workflows/ci.yml release: # Every package this workflow publishes has to build before we tag # anything. The GitHub release and the PyPI uploads cannot be taken back, # so a package that only fails to build in its publish job would leave the # release half-finished. - needs: [run_backend_tests, run_frontend_tests, run_tutor_tests] + needs: [run_ci] runs-on: ubuntu-latest if: github.ref_name == 'main' concurrency: diff --git a/.github/workflows/tutor-ci.yml b/.github/workflows/tutor-ci.yml index df2a706..5e49489 100644 --- a/.github/workflows/tutor-ci.yml +++ b/.github/workflows/tutor-ci.yml @@ -1,10 +1,7 @@ name: Tutor Plugin CI on: - pull_request: - branches: - - "**" - # So the release workflow can run these checks before it publishes anything. + # Invoked by ci.yml, which aggregates every check into a single `gate` job. workflow_call: concurrency: diff --git a/CLAUDE.md b/CLAUDE.md index 4107427..ef125fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,8 +38,20 @@ This is a **sample plugin repository** that demonstrates all major Open edX plug - Backend testing: `cd backend-plugin-sample && pytest` or `cd backend-plugin-sample && make test` - Run a single test: `cd backend-plugin-sample && pytest tests/test_models.py::test_placeholder` - Quality checks: `cd backend-plugin-sample && make quality` -- Install requirements: `cd backend-plugin-sample && make requirements` -- Compile requirements: `cd backend-plugin-sample && make compile-requirements` + +Python dependencies are managed with `uv` and locked in +`backend-plugin-sample/uv.lock`. There is no `requirements/` directory. + +- Install the dev environment: `cd backend-plugin-sample && make requirements` (`uv sync --group dev`) +- Relock without upgrading: `cd backend-plugin-sample && make compile-requirements` (`uv lock`) +- Upgrade everything and resync the edx-lint constraints: `cd backend-plugin-sample && make upgrade` + +Routine dependency bumps are handled by Renovate, one pull request per +package, so `make upgrade` is only needed for a deliberate full re-resolve. +All three package managers enforce a 7-day minimum release age: see +`exclude-newer` in `backend-plugin-sample/pyproject.toml`, `min-release-age` +in the two `.npmrc` files, and `minimumReleaseAge` in `renovate.json5`. Those +values have to stay in agreement. ## Code Style Guidelines - Python: Follow PEP 8 with max line length of 120 diff --git a/backend-plugin-sample/.gitignore b/backend-plugin-sample/.gitignore index 80a788a..cc31ca9 100644 --- a/backend-plugin-sample/.gitignore +++ b/backend-plugin-sample/.gitignore @@ -60,10 +60,6 @@ docs/modules.rst docs/sample_plugin.rst docs/sample_plugin.*.rst -# Private requirements -requirements/private.in -requirements/private.txt - # Test cruft default.db pii_report diff --git a/backend-plugin-sample/.readthedocs.yaml b/backend-plugin-sample/.readthedocs.yaml index 1d8cc9a..e7ca5d0 100644 --- a/backend-plugin-sample/.readthedocs.yaml +++ b/backend-plugin-sample/.readthedocs.yaml @@ -10,6 +10,16 @@ sphinx: configuration: docs/conf.py fail_on_warning: true +# Note: this configuration is currently dormant. Read the Docs looks for +# .readthedocs.yaml in the repository root, and this file is a directory down, +# so it is not read unless a project is created and pointed at this path +# explicitly. No such project exists today. The docs are built and +# warning-checked on every pull request by `tox -e docs` instead. +# +# It is kept, and kept correct, so that enabling hosted docs later is a matter +# of creating the project rather than rewriting this file. Every path here is +# relative to backend-plugin-sample/, not the repository root. + # Set the version of python needed to build these docs. build: os: "ubuntu-22.04" @@ -18,10 +28,11 @@ build: python: install: - - requirements: requirements/doc.txt - - # This will pip install this repo into the python environment - # if you are using this in a repo that is not pip installable - # then you should remove the following two lines. - - method: pip - path: . + # `uv sync` against the committed uv.lock, so the docs build against the + # same pinned versions as CI -- and inherits the minimum release age + # recorded in the lock. This used to be `requirements: requirements/doc.txt`, + # which stopped existing when the repo moved to uv.lock. + - method: uv + command: sync + groups: + - doc diff --git a/backend-plugin-sample/Makefile b/backend-plugin-sample/Makefile index a19af18..9ade075 100644 --- a/backend-plugin-sample/Makefile +++ b/backend-plugin-sample/Makefile @@ -1,6 +1,7 @@ .PHONY: clean clean_tox compile_translations coverage diff_cover docs dummy_translations \ extract_translations fake_translations help pii_check pull_translations \ - quality requirements selfcheck test test-all upgrade compile-requirements validate install_transifex_client + quality requirements selfcheck test test-all upgrade compile-requirements validate install_transifex_client \ + check-constraints .DEFAULT_GOAL := help @@ -35,10 +36,30 @@ docs: ## generate Sphinx HTML documentation, including API docs compile-requirements: ## generate the uv.lock file without upgrading packages uv lock +# Renovate handles routine dependency bumps now, one pull request per package. +# This target remains the way to re-resolve everything at once, and the only +# thing that regenerates [tool.uv].constraint-dependencies from edx-lint. upgrade: ## upgrade all packages in uv.lock and sync constraints from edx-lint - uv run --with edx-lint edx_lint write_uv_constraints pyproject.toml + uv run --with 'edx-lint>=6.2' edx_lint write_uv_constraints pyproject.toml uv lock --upgrade +# Regenerates the constraints into a scratch copy and diffs, rather than +# checking `git diff`, so that it reports only this drift and not whatever else +# happens to be uncommitted in the working tree. +check-constraints: ## fail if [tool.uv].constraint-dependencies is out of date + @cp pyproject.toml pyproject.toml.orig + @uv run --with 'edx-lint>=6.2' edx_lint write_uv_constraints pyproject.toml >/dev/null + @if cmp -s pyproject.toml.orig pyproject.toml; then \ + rm -f pyproject.toml.orig; \ + else \ + diff -u pyproject.toml.orig pyproject.toml || true; \ + mv pyproject.toml.orig pyproject.toml; \ + echo ""; \ + echo "[tool.uv].constraint-dependencies is out of date with edx-lint."; \ + echo "Run 'make upgrade' and commit the result."; \ + exit 1; \ + fi + quality: ## check coding style with pycodestyle and pylint tox -e quality diff --git a/backend-plugin-sample/pylintrc b/backend-plugin-sample/pylintrc index bd140db..2d188ff 100644 --- a/backend-plugin-sample/pylintrc +++ b/backend-plugin-sample/pylintrc @@ -64,7 +64,7 @@ # SERIOUSLY. # # ------------------------------ -# Generated by edx-lint version: 5.6.0 +# Generated by edx-lint version: 6.2.0 # ------------------------------ [MASTER] ignore = migrations @@ -257,6 +257,8 @@ enable = deprecated-pragma, unrecognized-inline-option, useless-suppression, + + pii-invalid-no-pii-annotation, disable = bad-indentation, broad-exception-raised, @@ -387,4 +389,9 @@ int-import-graph = [EXCEPTIONS] overgeneral-exceptions = builtins.Exception -# 79853056a08a5009db4a6e9a8ca00c194036ac5a +[PII] +pii-terms = + email, + username + +# 80d06cad4da142244dcae7bb096c0394ef420379 diff --git a/backend-plugin-sample/pyproject.toml b/backend-plugin-sample/pyproject.toml index 2934fc1..ab3ba3b 100644 --- a/backend-plugin-sample/pyproject.toml +++ b/backend-plugin-sample/pyproject.toml @@ -28,15 +28,25 @@ keywords = [ dynamic = ["readme", "version"] +# Unconstrained on purpose. The Open edX platform pins these itself, and a +# plugin that narrows them causes resolution conflicts when it is installed +# into edx-platform. `>=0` rather than a bare name because Renovate skips any +# dependency with no version specifier at all, so bare names would never get +# update PRs. It is the PEP 440 equivalent of the "*" ranges in +# frontend-plugin-sample/package.json -- PEP 440 has no bare wildcard. +# Please don't "tidy" these into real floors without a reason to; a floor we +# cannot justify is a compatibility claim we cannot back up. dependencies = [ - "Django", - "djangorestframework", - "django-filter", - "edx-opaque-keys", - "openedx-core", - "openedx-events", - "openedx-filters", - "openedx-atlas", + "Django>=0", + "djangorestframework>=0", + "django-filter>=0", + "edx-opaque-keys>=0", + "openedx-core>=0", + "openedx-events>=0", + "openedx-filters>=0", + "openedx-atlas>=0", + "edx-django-utils>=0", + "django-crum>=0", ] [project.entry-points."lms.djangoapp"] @@ -50,16 +60,19 @@ Homepage = "https://github.com/openedx/sample-plugin" Repository = "https://github.com/openedx/sample-plugin" [dependency-groups] +# `>=0` here for the same reason as [project].dependencies above: Renovate +# skips dependencies with no version specifier. The two Django entries below +# are the real exception -- they define the test matrix. test-base = [ - "pytest-cov", - "pytest-django", - "code-annotations", - "edx-django-utils", - "django-extensions", + "pytest-cov>=0", + "pytest-django>=0", + "code-annotations>=0", + "edx-django-utils>=0", + "django-extensions>=0", ] # Current default Django version used by quality, docs, and the default test # matrix entry. When adding or retiring a Django version from the matrix, see -# docs/how-tos/adding-a-matrix-dependency.rst for the full process. +# https://docs.openedx.org/en/latest/developers/how-tos/manage-uv-dependency-matrix.html test = [ {include-group = "test-base"}, "Django>=5.0,<6.0", @@ -70,28 +83,30 @@ django60 = [ ] quality = [ {include-group = "test"}, - "edx-lint", - "isort", - "pycodestyle", - "pydocstyle", + # A real floor, not a placeholder: `edx_lint write_uv_constraints`, used by + # `make upgrade` and `make check-constraints`, was added in 6.2.0. + "edx-lint>=6.2", + "isort>=0", + "pycodestyle>=0", + "pydocstyle>=0", ] doc = [ {include-group = "test"}, - "doc8", - "sphinx-book-theme", - "twine", - "build", - "Sphinx", + "doc8>=0", + "sphinx-book-theme>=0", + "twine>=0", + "build>=0", + "Sphinx>=0", ] ci = [ - "tox", - "tox-uv", + "tox>=0", + "tox-uv>=0", ] dev = [ {include-group = "quality"}, {include-group = "ci"}, - "diff-cover", - "edx-i18n-tools", + "diff-cover>=0", + "edx-i18n-tools>=0", ] [tool.setuptools] @@ -130,10 +145,35 @@ local_scheme = 'no-local-version' fallback_version = "0.0.0.dev0" [tool.uv] +# Supply-chain cooling-off period: never resolve to a release younger than +# this. A hijacked release is usually yanked within a few days, so waiting is +# cheap insurance for a repo where nothing is urgent. +# +# uv records the window in uv.lock as `[options] exclude-newer-span = "P7D"` +# and re-evaluates it at resolve time. A widening window cannot invalidate the +# lock -- exclude-newer only restricts which candidates are eligible, it never +# forces an upgrade -- so `uv lock --check` and `uv sync --locked` keep passing +# as time goes by. Raising the number of days here can invalidate it, though, +# if the new cutoff excludes something already locked. +# +# This is the real enforcement point, not Renovate: Renovate cannot apply its +# own `minimumReleaseAge` to a lockfile refresh, because it delegates that to +# uv and never sees the individual releases. Setting it here also means a +# developer running `make upgrade` locally gets the same behaviour as the bot. +# +# *** Keep this in sync with `minimumReleaseAge` in ../renovate.json5 and with +# *** `min-release-age` in the .npmrc files. If uv is stricter than Renovate, +# *** Renovate proposes versions uv then refuses and churns empty branches. +# +# To let one urgent security fix through without dropping the window for +# everything, add a temporary exemption rather than changing this value: +# exclude-newer-package = { the-package = false } +exclude-newer = "7 days" + # Each entry lists groups with mutually exclusive version requirements so uv can # produce a single uv.lock that contains a separate resolution for each. Add a # new pair here whenever you add a legacy-version group to [dependency-groups]. -# See docs/how-tos/adding-a-matrix-dependency.rst for the full process. +# See https://docs.openedx.org/en/latest/developers/how-tos/manage-uv-dependency-matrix.html conflicts = [ [{group = "test"}, {group = "django60"}], ] @@ -145,12 +185,21 @@ conflicts = [ constraint-dependencies = [ "Django<7.0", "elasticsearch<7.14.0", + "social-auth-app-django<6.0.0", + "social-auth-core<5.0.0", + "pip<26.2.1", ] [tool.edx_lint] # Repo-specific uv constraints merged with edx-lint's global constraints. # Local entries override global ones for the same package. # Run `make upgrade` to regenerate [tool.uv].constraint-dependencies. +# +# Any cap added here also needs a matching rule in ../renovate.json5, either +# `enabled: false` or an `allowedVersions` for that package. Otherwise +# Renovate offers a version the constraint forbids, `uv lock` refuses it, and +# the branch lands with no changes -- retried on every run. See the Django +# rule there for the worked example. uv_constraints = [ "Django<7.0", # this repo tests against Django 6.x; overrides the global Django<6.0 constraint ] diff --git a/backend-plugin-sample/tox.ini b/backend-plugin-sample/tox.ini index c7ac4b4..673eeed 100644 --- a/backend-plugin-sample/tox.ini +++ b/backend-plugin-sample/tox.ini @@ -34,7 +34,7 @@ match-dir = (?!migrations) [pytest] DJANGO_SETTINGS_MODULE = test_settings addopts = --cov openedx_plugin_sample --cov tests --cov-report term-missing --cov-report xml -norecursedirs = .* docs requirements site-packages +norecursedirs = .* docs site-packages [testenv] runner = uv-venv-lock-runner @@ -82,6 +82,12 @@ commands = pydocstyle src/openedx_plugin_sample tests manage.py isort --check-only --diff tests test_utils src/openedx_plugin_sample manage.py test_settings.py make selfcheck + # Renovate cannot run `edx_lint write_uv_constraints` itself, so instead of + # regenerating the constraints on a schedule we fail here when they drift. + # The loop closes itself: edx-lint is a managed dependency, so the pull + # request that bumps it is the one that goes red if edx-lint's global + # constraints moved with it. + make check-constraints [testenv:pii_check] runner = uv-venv-lock-runner diff --git a/backend-plugin-sample/uv.lock b/backend-plugin-sample/uv.lock index 574fe9f..3c6a165 100644 --- a/backend-plugin-sample/uv.lock +++ b/backend-plugin-sample/uv.lock @@ -15,10 +15,17 @@ conflicts = [[ { package = "openedx-plugin-sample", group = "django60" }, ]] +[options] +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer-span = "P7D" + [manifest] constraints = [ { name = "django", specifier = "<7.0" }, { name = "elasticsearch", specifier = "<7.14.0" }, + { name = "pip", specifier = "<26.2.1" }, + { name = "social-auth-app-django", specifier = "<6.0.0" }, + { name = "social-auth-core", specifier = "<5.0.0" }, ] [[package]] @@ -819,7 +826,7 @@ wheels = [ [[package]] name = "edx-lint" -version = "6.0.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -829,10 +836,11 @@ dependencies = [ { name = "pylint-celery" }, { name = "pylint-django" }, { name = "six" }, + { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/b5/43614ee28105f436b5664fa0ead2ed6f74c125b732e030b7ceaa7f3e953f/edx_lint-6.0.0.tar.gz", hash = "sha256:078e6f3a5d54f295a43c6db59f7fdc373e139aa1bda8b7ce02fb53e15296675b", size = 48460, upload-time = "2026-03-12T18:00:53.047Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/e2/20b4782eba5bbbaeb949877666f871912db05fb22cee2eafdbc83566cffc/edx_lint-6.2.0.tar.gz", hash = "sha256:2ce94b0b1235024e7714792442ea674e1bbe2877e7ca2ca970609e74aa73dfdf", size = 55456, upload-time = "2026-08-18T12:40:35.178Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/fd/f3fbb21bb2ac75dbacbbd8a9afbffc093b09d51128ec80def128761b19b4/edx_lint-6.0.0-py3-none-any.whl", hash = "sha256:5eab30bf1beb6f678b973151b9c0add0efbd758ef33f3df52e687dc48b2c0024", size = 56807, upload-time = "2026-03-12T18:00:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/72bbb973cb654aa9d8291c438dd02b21ffb3bf2bdc9e653f9ea9048e3b20/edx_lint-6.2.0-py3-none-any.whl", hash = "sha256:0e1491f9a049927e35eb05a0fd33464b4ec912b83e387568e37f4ce5d10acb30", size = 64109, upload-time = "2026-08-18T12:40:34.102Z" }, ] [[package]] @@ -1350,8 +1358,10 @@ source = { editable = "." } dependencies = [ { name = "django", version = "5.2.13", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-dev' or extra != 'group-21-openedx-plugin-sample-django60' or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-doc') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-quality') or (extra == 'group-21-openedx-plugin-sample-django60' and extra == 'group-21-openedx-plugin-sample-test')" }, { name = "django", version = "6.0.4", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-21-openedx-plugin-sample-django60'" }, + { name = "django-crum" }, { name = "django-filter" }, { name = "djangorestframework" }, + { name = "edx-django-utils" }, { name = "edx-opaque-keys" }, { name = "openedx-atlas" }, { name = "openedx-core" }, @@ -1431,84 +1441,86 @@ test-base = [ [package.metadata] requires-dist = [ - { name = "django" }, - { name = "django-filter" }, - { name = "djangorestframework" }, - { name = "edx-opaque-keys" }, - { name = "openedx-atlas" }, - { name = "openedx-core" }, - { name = "openedx-events" }, - { name = "openedx-filters" }, + { name = "django", specifier = ">=0" }, + { name = "django-crum", specifier = ">=0" }, + { name = "django-filter", specifier = ">=0" }, + { name = "djangorestframework", specifier = ">=0" }, + { name = "edx-django-utils", specifier = ">=0" }, + { name = "edx-opaque-keys", specifier = ">=0" }, + { name = "openedx-atlas", specifier = ">=0" }, + { name = "openedx-core", specifier = ">=0" }, + { name = "openedx-events", specifier = ">=0" }, + { name = "openedx-filters", specifier = ">=0" }, ] [package.metadata.requires-dev] ci = [ - { name = "tox" }, - { name = "tox-uv" }, + { name = "tox", specifier = ">=0" }, + { name = "tox-uv", specifier = ">=0" }, ] dev = [ - { name = "code-annotations" }, - { name = "diff-cover" }, + { name = "code-annotations", specifier = ">=0" }, + { name = "diff-cover", specifier = ">=0" }, { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "edx-i18n-tools" }, - { name = "edx-lint" }, - { name = "isort" }, - { name = "pycodestyle" }, - { name = "pydocstyle" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "tox" }, - { name = "tox-uv" }, + { name = "django-extensions", specifier = ">=0" }, + { name = "edx-django-utils", specifier = ">=0" }, + { name = "edx-i18n-tools", specifier = ">=0" }, + { name = "edx-lint", specifier = ">=6.2" }, + { name = "isort", specifier = ">=0" }, + { name = "pycodestyle", specifier = ">=0" }, + { name = "pydocstyle", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, + { name = "tox", specifier = ">=0" }, + { name = "tox-uv", specifier = ">=0" }, ] django60 = [ - { name = "code-annotations" }, + { name = "code-annotations", specifier = ">=0" }, { name = "django", specifier = ">=6.0,<7.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "django-extensions", specifier = ">=0" }, + { name = "edx-django-utils", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] doc = [ - { name = "build" }, - { name = "code-annotations" }, + { name = "build", specifier = ">=0" }, + { name = "code-annotations", specifier = ">=0" }, { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "doc8" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "sphinx" }, - { name = "sphinx-book-theme" }, - { name = "twine" }, + { name = "django-extensions", specifier = ">=0" }, + { name = "doc8", specifier = ">=0" }, + { name = "edx-django-utils", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, + { name = "sphinx", specifier = ">=0" }, + { name = "sphinx-book-theme", specifier = ">=0" }, + { name = "twine", specifier = ">=0" }, ] quality = [ - { name = "code-annotations" }, + { name = "code-annotations", specifier = ">=0" }, { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "edx-lint" }, - { name = "isort" }, - { name = "pycodestyle" }, - { name = "pydocstyle" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "django-extensions", specifier = ">=0" }, + { name = "edx-django-utils", specifier = ">=0" }, + { name = "edx-lint", specifier = ">=6.2" }, + { name = "isort", specifier = ">=0" }, + { name = "pycodestyle", specifier = ">=0" }, + { name = "pydocstyle", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] test = [ - { name = "code-annotations" }, + { name = "code-annotations", specifier = ">=0" }, { name = "django", specifier = ">=5.0,<6.0" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "django-extensions", specifier = ">=0" }, + { name = "edx-django-utils", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] test-base = [ - { name = "code-annotations" }, - { name = "django-extensions" }, - { name = "edx-django-utils" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, + { name = "code-annotations", specifier = ">=0" }, + { name = "django-extensions", specifier = ">=0" }, + { name = "edx-django-utils", specifier = ">=0" }, + { name = "pytest-cov", specifier = ">=0" }, + { name = "pytest-django", specifier = ">=0" }, ] [[package]] diff --git a/brand-sample/.npmrc b/brand-sample/.npmrc new file mode 100644 index 0000000..0a44cc9 --- /dev/null +++ b/brand-sample/.npmrc @@ -0,0 +1,13 @@ +# Supply-chain cooling-off period: only install versions published more than +# this many days ago. See ../frontend-plugin-sample/.npmrc for the reasoning. +# +# npm reads the project config only from the directory holding package.json and +# does not walk up, so each npm package in this repo needs its own copy. +# +# Requires npm >= 11.10.0. Note .nvmrc here still pins Node 20, which bundles +# npm 10.8.2 and ignores this setting -- so it has no effect until that moves +# to Node 24. +# +# Keep in sync with `minimumReleaseAge` in ../renovate.json5 and with +# `exclude-newer` in ../backend-plugin-sample/pyproject.toml. +min-release-age=7 diff --git a/brand-sample/.nvmrc b/brand-sample/.nvmrc index 2edeafb..a45fd52 100644 --- a/brand-sample/.nvmrc +++ b/brand-sample/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +24 diff --git a/frontend-plugin-sample/.npmrc b/frontend-plugin-sample/.npmrc new file mode 100644 index 0000000..22b4df4 --- /dev/null +++ b/frontend-plugin-sample/.npmrc @@ -0,0 +1,21 @@ +# Supply-chain cooling-off period: only install versions published more than +# this many days ago. A hijacked release is usually yanked within a few days, +# so waiting is cheap insurance. +# +# This is the real enforcement point, not Renovate. Renovate cannot apply its +# own `minimumReleaseAge` to a lockfile refresh, and setting it here means a +# developer running `npm install` locally gets the same behaviour as the bot. +# Renovate reads this file and uses whichever of the two windows is stricter. +# +# Requires npm >= 11.10.0. .nvmrc pins Node 24, which bundles npm 11.19.0. +# Older npm ignores this and warns "Unknown project config" -- harmless, and it +# goes away once that developer is on a current Node. `npm ci` is unaffected +# either way, since it installs from the lockfile without resolving. +# +# Keep in sync with `minimumReleaseAge` in ../renovate.json5 and with +# `exclude-newer` in ../backend-plugin-sample/pyproject.toml. +# +# To let one urgent security fix through without dropping the window for +# everything, list it here rather than lowering the value above: +# min-release-age-exclude[]=the-package +min-release-age=7