Skip to content

build: prepare for Renovate and add a dependency cooling-off period - #67

Open
feanil wants to merge 9 commits into
mainfrom
feanil/renovate-prework
Open

build: prepare for Renovate and add a dependency cooling-off period#67
feanil wants to merge 9 commits into
mainfrom
feanil/renovate-prework

Conversation

@feanil

@feanil feanil commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Groundwork for moving dependency management to Renovate. The Renovate config
itself is a small follow-up PR.

The main change is a 7-day minimum release age on every dependency, so a
freshly published release can't be pulled in immediately. It's set in the
package managers rather than in Renovate — exclude-newer in
pyproject.toml and min-release-age in two new .npmrc files — so it
covers transitive dependencies too, and local make upgrade behaves the same
way as the bot will. Renovate's own setting has to be kept at the same value.

Alongside that:

  • Bare dependency names become >=0, since Renovate skips any dependency with
    no version specifier at all. Same intent as the "*" ranges on the npm side.
  • One aggregated required check, all required checks passed, replaces the
    five per-matrix-leg contexts. The frontend and tutor jobs were both named
    build, so neither could ever be required.
  • brand-sample gets CI. Its compiled dist/light.min.css is committed, and
    the README tells consumers to load it straight from this repo via jsdelivr,
    pinned to @main. Nothing rebuilt or checked it, so a dependency bump that
    changed the build output would leave the CSS people load out of step with the
    tokens here. The new job rebuilds and diffs dist/.
  • edx-lint to 6.2, which make upgrade needs and never had, plus a
    make check-constraints guard so the files it generates can't drift again.
  • Two undeclared runtime dependencies, some stale requirements/ references,
    and a dormant .readthedocs.yaml pointing at a deleted file.

The commit messages have the reasoning for each.

Merging

Required checks on main are already updated to all required checks passed,
so this can merge as-is. Merging cuts a patch release.

feanil and others added 3 commits September 2, 2026 11:23
`apps.py` imports `edx_django_utils` and `pipeline.py` imports `crum`, but
neither was in `[project].dependencies`. They resolved anyway because the
`test-base` dependency group pulls in `edx-django-utils`, which brings
`django-crum` transitively -- so the omission was invisible in development
and in CI, and would only surface as an ImportError for someone installing
the published wheel into an environment without the test groups.

The uv.lock change is metadata only: both packages were already resolved,
so only this project's own declared dependency list gains the entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renovate skips any PEP 621 dependency that has no version specifier at all
(`skipReason: "unspecified-version"`), dropping it before it ever queries
PyPI -- a correct `lockedVersion` in uv.lock does not help, because the
manifest is what Renovate reads as the reference. Since almost every entry
here was a bare name, Renovate would have produced no update PRs for them.

`>=0` is the PEP 440 equivalent of the "*" ranges already used in
frontend-plugin-sample/package.json; PEP 440 has no bare wildcard, as
`Django==*` is a parse error. It keeps these deliberately unconstrained --
the Open edX platform pins them itself, and a plugin that narrows them
causes resolution conflicts when installed into edx-platform -- while
stating that intent explicitly instead of leaving it implied by omission.

Real floors were considered and rejected: derived from uv.lock they would
be guesses, since the lock records the newest resolvable version rather
than the oldest supported one. `Django>=5.2` would block installation on a
platform running Django 4.2, quite possibly wrongly.

The one genuine floor is `edx-lint>=6.0`, which is verifiable rather than
guessed: `edx_lint write_uv_constraints` does not exist before 6.0. The
`test` and `django60` Django pins are untouched -- they define the test
matrix.

Every uv.lock change is a metadata-only rewrite of a `{ name = "x" }` entry
to `{ name = "x", specifier = ">=0" }`. No resolution moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wait seven days before resolving to any newly published release. A hijacked
release is usually yanked within a few days, so for a repo where nothing is
urgent this is cheap insurance against installing a compromised version in
the window before anyone notices.

Set in the package managers rather than in Renovate, because Renovate cannot
enforce a cooldown on a lockfile refresh: it delegates that to uv or npm and
never enumerates the individual releases, so `minimumReleaseAge` does not
apply to it. Configuring the resolvers instead covers transitive
dependencies as well as direct ones, and means `make upgrade` or
`npm install` on a laptop behaves exactly like the bot.

Two .npmrc files rather than one at the repo root: npm reads the project
config only from the directory holding package.json and does not walk up.

Verified: `uv lock` is idempotent afterwards, `uv lock --check` and
`uv sync --locked` still pass, and `npm ci` plus `npm run build` are
unaffected in both npm packages. uv records the window in uv.lock as
`[options] exclude-newer-span` and re-evaluates it per resolve; a widening
window cannot invalidate the lock, since exclude-newer only restricts which
candidates are eligible and never forces an upgrade.

Two caveats recorded in the comments. `min-release-age` needs npm >= 11.10.0,
so it does nothing in brand-sample until its .nvmrc moves off Node 20 --
Node 24 bundles npm 11.19.0. And Renovate's own `minimumReleaseAge` must be
kept at the same value, or it will offer versions the resolvers refuse and
leave empty branches behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.04%. Comparing base (1f6766e) to head (dfa8771).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main      #67   +/-   ##
=======================================
  Coverage   93.04%   93.04%           
=======================================
  Files          16       16           
  Lines         503      503           
  Branches       18       18           
=======================================
  Hits          468      468           
  Misses         24       24           
  Partials       11       11           
Flag Coverage Δ
unittests 93.04% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@feanil
feanil requested a review from kdmccormick September 9, 2026 13:37
@feanil
feanil marked this pull request as ready for review September 9, 2026 13:37
@feanil
feanil force-pushed the feanil/renovate-prework branch from 6b93210 to f85520e Compare September 9, 2026 14:03
@feanil
feanil enabled auto-merge (rebase) September 9, 2026 14:04
@feanil
feanil disabled auto-merge September 9, 2026 14:04
feanil and others added 6 commits September 9, 2026 15:19
brand-sample had no CI at all, which matters more than it looks. Its
compiled dist/light.min.css is committed, and brand-sample/README.md tells
consumers to load it directly out of this repository:

    brandOverride: 'https://cdn.jsdelivr.net/gh/openedx/sample-plugin@main/brand-sample/dist/light.min.css'

That URL is pinned to @main, so whatever lands on the default branch is
what MFEs following the brand example immediately start loading. dist/ is
therefore a published artifact rather than a build leftover.

Nothing rebuilt or checked it. Updating package-lock.json does not
regenerate dist/, so a dependency bump that changed the build output would
leave the CSS people load out of step with the tokens in this repo, with
nothing to catch the drift.

The job installs from the lockfile, rebuilds, and diffs dist/ against the
build output. When it fails, the fix is to run `npm run build` in
brand-sample and commit the result.

Also moves .nvmrc from Node 20 to 24. Node 20 is end-of-life, and it bundles
npm 10.8.2, which silently ignores the `min-release-age` cooling-off setting
added in the previous commit -- so brand-sample got no benefit from it until
now. Node 24 bundles npm 11.19.0. Verified that dist/ still builds
byte-identically under Node 24, so this needs no accompanying rebuild.

The workflow is workflow_call only, because the next commit adds a ci.yml
that aggregates every check into one required status check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Branch protection matches required status checks by name, which made the
required list both fragile and incomplete.

Fragile, because naming the backend jobs individually means spelling out
every matrix leg -- `tests (ubuntu-latest, 3.12, django52)` and four more --
so adding or retiring a Python or Django version quietly breaks protection
until someone remembers to update the repository settings.

Incomplete, because the frontend and tutor jobs were both called `build`.
Two check runs with one name cannot be told apart in a required list, so
neither was ever required. Only the five backend contexts and openedx/cla
were, which means a pull request touching nothing but frontend files could
merge on the strength of checks that could not have been affected by it.
That gap matters much more once dependency updates start merging
themselves, because GitHub's auto-merge waits for required checks and
nothing else.

So ci.yml now runs on pull requests, invokes each per-package workflow, and
ends in a job called "all required checks passed" that succeeds only if all
of them did. That is the only name branch protection has to know, and it
never changes whatever happens to the matrix. The per-package workflows
become workflow_call only, so they run once per pull request rather than
twice.

release.yml calls ci.yml instead of the three workflows separately, so a
release runs exactly the checks that were required to merge, and picks up
brand-ci -- and anything added later -- without a change there.

The aggregating job runs `if: always()`, because a skipped required check
never reports at all and would block the pull request forever rather than
failing it.

The aggregation is five lines of shell rather than a third-party action.
This job is the one thing standing between a pull request and main, so it
is the last place worth adding a dependency -- particularly in a change
whose purpose is to reduce how much unreviewed third-party code we pull in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`make upgrade` calls `edx_lint write_uv_constraints`, and that subcommand
does not exist before edx-lint 6.2.0 -- so with edx-lint pinned at 6.0.0 the
target has been failing, printing the usage text instead of regenerating
anything. That is also why the previous commit's `edx-lint>=6.0` floor was
wrong; the real floor is 6.2.

Raising it surfaced two files edx-lint owns that had drifted:

[tool.uv].constraint-dependencies was missing three of edx-lint's global
constraints -- social-auth-app-django<6.0.0, social-auth-core<5.0.0 and
pip<26.2.1. Nothing had regenerated it since those were added upstream,
which is precisely the failure the next commit puts a check around.

pylintrc was still stamped "Generated by edx-lint version: 5.6.0", predating
the pii_annotation_check plugin's requirement that `pii_terms` be
configured. Under 6.2.0 pylint aborted with "The 'pii_terms' setting must be
configured", taking models.py down with an astroid-error. Regenerated with
`edx_lint write pylintrc`, which adds the [PII] section and enables
pii-invalid-no-pii-annotation. The file says DO NOT EDIT, so this is
entirely tool output plus the local pylintrc_tweaks.

`tox -e quality`, `-e docs` and `-e pii_check` all pass afterwards. The two
test envs segfault locally, but they do so identically on an unmodified
checkout of main, so that is a local environment problem rather than
anything from this change -- CI on main is green, and `pytest` run directly
passes all 23 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[tool.uv].constraint-dependencies is generated by edx-lint from its global
common_constraints.txt plus this repo's [tool.edx_lint].uv_constraints, and
nothing regenerated it automatically -- the previous commit found it three
constraints behind upstream.

The scheduled workflow that used to run `make upgrade` is being removed, and
its replacement cannot take over this job: the Mend-hosted Renovate app has
no postUpgradeTasks, so it cannot run `edx_lint write_uv_constraints` after
an update. Reinstating a cron that opens a pull request is not much better,
because a workflow using the default GITHUB_TOKEN produces pull requests
that do not trigger CI.

So instead of regenerating on a schedule, fail when it drifts. 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
along with it. The drift is reported by the change that caused it, and
automerge is blocked until someone regenerates.

The target diffs against a scratch copy rather than checking `git diff`, so
it reports only this drift and not whatever else is uncommitted in the
working tree, and it restores the file when it fails rather than leaving the
regenerated version behind.

Also fixes `make upgrade` to ask for 'edx-lint>=6.2' rather than a bare
`edx-lint`, so it cannot quietly resolve to a version without the
subcommand it depends on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The requirements/ directory went away when this repo moved to uv.lock, but
several references to it survived:

- tox.ini told pytest not to recurse into `requirements`
- .gitignore still ignored requirements/private.in and .private.txt
- CLAUDE.md described the dependency commands without mentioning uv at all
- pyproject.toml pointed twice at docs/how-tos/adding-a-matrix-dependency.rst,
  which does not exist -- docs/how-tos/index.rst links to the real thing on
  docs.openedx.org, so these now point there directly

Also records in CLAUDE.md that Renovate owns routine bumps now, and that the
7-day minimum release age is configured in three places that have to stay in
agreement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.readthedocs.yaml still asked for `requirements/doc.txt`, which stopped
existing when the repo moved to uv.lock, so the config could not have
worked. Switched to Read the Docs' native uv support -- `method: uv` with
`command: sync` and the `doc` dependency group -- which resolves from the
committed uv.lock, so hosted docs would build against the same pinned
versions as CI and inherit the minimum release age recorded there.

Worth knowing: this file is dormant, not broken in production. Read the Docs
reads .readthedocs.yaml from the repository root and this one is a directory
down, so nothing reads it unless a project is created and pointed at this
path. No project exists -- both plausible subdomains 404 and nothing in the
repo references one -- and the docs are built with fail_on_warning on every
pull request by `tox -e docs` regardless. A note in the file now says so,
along with the fact that its paths are relative to backend-plugin-sample/
rather than the repository root.

Kept rather than deleted so that turning on hosted docs later is a matter of
creating the project instead of rewriting the config. Untested by
definition: with no project to build it, Read the Docs would be the first
thing to actually run this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@feanil
feanil force-pushed the feanil/renovate-prework branch from f85520e to dfa8771 Compare September 9, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant