Skip to content

Release 0.78.0 - #3864

Closed
odlbot wants to merge 14 commits into
releasefrom
release-candidate
Closed

odlbot wants to merge 14 commits into
releasefrom
release-candidate

Conversation

@odlbot

@odlbot odlbot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Matt Bertrand

renovate[bot]

Ahtesham Quraish

Tobias Macey

Chris Chudzicki

Shankar Ambady

renovate Bot and others added 14 commits August 27, 2026 14:02
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* adding staleness penalty for resource vector search

* update tests
The Stay Updated button was gated on three things: the
NEXT_PUBLIC_STAY_UPDATED_HUBSPOT_FORM_ID env var, the CMS page's
show_stay_updated flag, and an enrollment-mode check requiring every
enrollment mode to be "verified".

The mode check was doing work the CMS flag already covers, and on courses
it iterated course.courseruns unfiltered — including runs with
live: false / is_enrollable: false that appear nowhere else on the page
(the session selector filters to is_enrollable). One retired audit run
therefore suppressed the button for the whole course.

Example: course-v1:UAI_SOURCE+UAI.MLTL.1 has show_stay_updated: true and
one enrollable verified-only run, but a second run (id 2465, live: false,
is_enrollable: false) carries an "audit" mode, so the button never
rendered.

Visibility is now just: can it be shown (env var, checked in
ProductPageTemplate) and should it be shown (page.show_stay_updated).

Tests for each page collapse to those branches; the shared
PROGRAM_HIDE_STAY_UPDATED_CASES only served the removed mode
permutations. The env-var tests now set show_stay_updated explicitly —
the page factory randomizes it, so without the mode clause they would
have been coin-flips.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add overridable mutation error-toast infrastructure (Option 2)

Global MutationCache.onError shows a top-center error toast for every
browser mutation failure, so a failed mutation is never silent. Call
sites override via typed `meta` (declared in api/mutation-meta):
- `showErrorToast: false` to opt out (e.g. renders its own inline error)
- `errorMessage` / `getErrorMessage(error, variables)` for custom copy

`api` stays UI-free (declares typed `meta` only); the toast lives in
`main`. Copy resolution is defensive: a throwing getErrorMessage or
empty copy falls back to the generic message inside onError, so a bad
override can never make the failure silent again. The store holds one
toast; a newer error replaces it (never stacks), and it persists until
dismissed via smoot Alert's own close control.

The convention is documented in .github/instructions/frontend.instructions.md.

Not yet shippable: sites that already render inline alerts will
double-fire until opted out (follow-up migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Opt out surfaced inline-error sites from the global error toast

The global MutationCache.onError toast (previous commit) fires on every
browser mutation failure. Sites that already render their own co-located
inline error would therefore double-fire (inline alert + toast).

Give each shared api/ mutation hook an optional `meta` param
(MutationHookOptions, exported from api/mutation-meta) forwarded to
useMutation, and have every consumer that renders an inline error pass
`meta: SILENCE_ERROR_TOAST`. Opt-out lives at the consumer, never baked
into the shared hook, because several hooks (enrollment, baskets) are
used in both a surfaced and a silent context. UpgradeBanner opts out
only when its onUpgradeFailure callback is wired — without it the
banner has no error surface of its own, so the toast must stay.

Also mount <Toaster/> in renderWithProviders and reset the toast store
per test, so a missing opt-out at an inline-error site surfaces as a
double-role="alert" test failure rather than only showing up in manual
QA, and add end-to-end coverage (mutationErrorToast.test.tsx) that the
default toast fires and SILENCE_ERROR_TOAST suppresses it — so removing
the MutationCache wiring can no longer pass green.

Adopting the genuinely-silent sites (Start button, destroy dialogs,
listItemMove rollback) with tailored copy is a separate follow-up; those
already toast via the global default.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Strengthen consumer error-path tests around the toast opt-outs

Convert text-based error assertions to singular role queries
(findByRole("alert")) in the CourseEnrollmentDialog and JustInTimeDialog
tests, so a stray global toast — a missing opt-out, or a shared hook
that stops forwarding `meta` — fails these tests as a second alert.

Add failing-mutation coverage for all three DashboardDialogs (email
settings, unenroll, unenroll program), whose opt-outs and inline error
rendering were previously untested. Writing those tests surfaced that
each dialog awaited mutateAsync inside formik's onSubmit: the rejection
escaped onSubmit (formik logs an unhandled-error warning), and the
post-await isError guards were unreachable on the error path. Switch to
mutate(..., { onSuccess }) — same success/error behavior (inline alert
via isError, busy state via isPending), no escaping rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fail any test that leaves the global error toast unacknowledged

The double-alert test net only tripped where an error-path test happened
to assert via a singular role="alert" query. Make it structural: an
afterEach in setupJest fails any test that ends with the global error
toast still showing, and its message teaches the fix — opt the component
out with `meta: SILENCE_ERROR_TOAST` if it renders its own inline error,
or acknowledge the toast with the new `expectErrorToast(message)` helper
if the toast is the intended error surface. Every mutation failure a
test drives now requires a deliberate decision about its error surface.

The full main suite passes with zero acknowledgment churn — no existing
test drives a toasting failure it doesn't already account for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add tailored enroll error copy as errorMessage PoC

The dashboard enroll flow (useEnrollmentHandler) becomes the first adopter
of meta.errorMessage: a failed enroll toasts course- or program-specific
copy instead of the generic message, demonstrating the per-site override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fall back to errorMessage when getErrorMessage throws or returns blank

resolveErrorMessage collapsed the dynamic and static tiers into one
nullish-coalescing expression, so a getErrorMessage that threw or
returned blank copy skipped a defined errorMessage and jumped straight
to the generic fallback. Evaluate the tiers separately so each falls
through to the next when it yields no message.

Flagged in review: #3837 (comment)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#3825)

* ci: add a ci-gate job so one required check covers the whole suite

GitHub's required status checks take an exact list of context names. There are
no wildcards and no "all checks must pass" option, so requiring these six jobs
from the ruleset in mitodl/ol-infrastructure means restating all six names
there, and that list then goes stale in two ways.

A renamed job keeps being required under its old name, which GitHub will never
report again, so every PR waits forever on it. mitxonline hit exactly this when
its python-tests job became a 4-way matrix and the checks turned into
`python-tests (1)`..`(4)`.

A newly added job is not required until somebody remembers to go and add it, so
new CI silently cannot block a merge.

`ci-gate` fails if any job it needs ends in anything but success or skipped.
Requiring it instead keeps the list of what must pass in the same file as the
jobs it names, so adding or renaming a job is one edit in one repo.

openapi-diff is deliberately not covered: it lives in its own workflow, and it
runs `oasdiff breaking --fail-on ERR`, so it goes red on intentional breaking
API changes. It was red at merge on 8 of the last 40 merged PRs here, release
PRs included, so gating merges on it would mean bypassing the ruleset routinely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QygRcd6WF4BmUi1C5cez4d

* ci: wire up err-ignore allowlist for openapi-diff breaking check

needs can't reach openapi-diff (separate workflow, pull_request trigger),
so it can't join ci-gate directly. Give it the allowlist mechanism oasdiff
already supports so intentional breaking changes stop needing a routine
ruleset bypass, then require it alongside ci-gate in ol-infrastructure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wnKLHVJ2vP4G6mPN9AgFr

* ci: move oasdiff-err-ignore.txt out of openapi/specs/

openapi_spec_check.sh diffs openapi/specs/ against freshly generated
specs and fails on any extra file -- confirmed in the python-tests CI
run for dcd62cf. Move the ignore list to openapi/ instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wnKLHVJ2vP4G6mPN9AgFr

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ync (#3808)

* feat(cohort-1): MicroMasters/MITx Online certificate warehouse-pull sync

Second PR in the replacement stack for closed mit-learn#3566, stacked
on the StarRocks warehouse-pull machinery (#3807). Replaces Hightouch
as the writer of external.programcertificate — currently broken since
~2025-06-01 — with a warehouse-pull Celery task reading
integrations__learn__program_certificates (mitodl/ol-data-platform#2591).

Resolves mitodl/hq#12954.

- profiles/etl.py: transform_program_certificate + upsert_program_certificate.
  Deliberately never prunes, even on full_refresh — a certificate is a
  durable achievement record, not a catalog resource that should
  self-heal by deleting rows a pull didn't see (a transient upstream
  join gap must never read as "certificate revoked").
- profiles/tasks.py: SyncProgramCertificatesTask(BaseWarehouseETLTask),
  registered via app.register_task per the base class's documented
  requirement (the @app.task(base=...) decorator doesn't work for it).
- main/settings_celery.py: one beat entry, gated on STARROCKS_HOST
  (daily). Doesn't participate in the WAREHOUSE_ETL_CUTOVER_SOURCES
  mechanism — Hightouch ran externally, not as an MIT Learn Celery
  task, so there's no legacy beat entry to retire.
- main/settings.py: removed "programcertificate" from EXTERNAL_MODELS.
  That setting made main.routers.ExternalSchemaRouter reject every
  Django-side write to this model, correct back when only Hightouch
  wrote to it — this task is now the intended writer, so the guard no
  longer applies. Flagging explicitly: this removes a safety check
  (main/routers_test.py's test_external_tables_are_readonly, which
  asserted the exact behavior just removed, is deleted alongside it).

Field set was checked directly against profiles.ProgramCertificate
(not assumed from memory) — the model stores 11 more columns than
first planned (user_edxorg_username, user_mitxonline_username, name/
demographic/address fields), all now covered by both the dbt model and
this transform.

* fix: require STARROCKS_USER too before scheduling the certificate sync

The beat entry gate checked only STARROCKS_HOST, but
_connect_starrocks() (learning_resources.lib.warehouse) also requires
STARROCKS_USER — set HOST without USER and the task gets scheduled but
fails every run with ImproperlyConfigured.

Addresses sentry[bot] feedback on #3808.

* Address review feedback: manage the model, isolate bad rows

shanbady: with the warehouse-pull cutover Django owns writes to
external.programcertificate, so it should own the schema too. Flipping
managed generates a state-only migration — migration 0015 already created
the table, so there is no DDL and no data risk.

shanbady: one malformed certificate row no longer aborts the batch. Skipped
rows self-heal because the daily beat schedule runs full_refresh over the
whole view. An all-rows-failed batch still raises: that is a broken database
rather than a bad row, and returning normally would advance an incremental
run's watermark past a window nothing was written for.

Copilot: the mapping test claimed to cover every field but left the three
address fields null and unasserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JFhTucXavYwkjv6o4qXZ8r

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Submit and link each resource under one URL, from learn_url

Four places derived a resource's URL on Learn independently — the resources,
video, podcast and products sitemaps — and the card href and drawer canonical
derived it a fifth and sixth time. They did not agree, and the resources sitemap
submitted a drawer URL for every resource on top of whatever dedicated page the
per-type sitemaps submitted. A video was therefore advertised twice, and because
the drawer canonicalized to itself, both URLs claimed to be the original and
crawlers had no way to consolidate ranking signal onto the dedicated page.

Read `learn_url` instead, which the backend computes as the resource's own page
where it has one and its search drawer where it does not:

- The card href points at it. `pushUrl` is untouched, so a click still opens the
  drawer in place and nothing changes for users; only what a crawler follows
  changes.
- The drawer canonicalizes to it. For a video or podcast episode that is now the
  dedicated page rather than the drawer itself, which is what actually
  consolidates the signal — excluding a URL from the sitemap only stops us
  advertising it, and says nothing about one that is linked or shared.
- The resources sitemap emits it for every published resource, so the video,
  podcast and products sitemaps are gone: they existed to emit dedicated-page
  URLs the frontend had to build itself.

Collapsing the sitemaps also settles which parent scopes an episode's URL. The
podcast sitemap mapped over every parent podcast, so an episode with two parents
would have been submitted twice; one URL per resource cannot.

The count query now asks for `limit: 1`. It reads only `count`, so it was
fetching a thousand rows and discarding them.

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

* Fall back to the drawer URL when learn_url is missing

learn_url is non-nullable and never blank, but a frontend deployed ahead of the
backend sees the field absent, and both call sites degrade badly on a falsy
value rather than merely pointing somewhere less ideal:

- the drawer emitted no canonical tag at all, which is worse than the
  self-canonical it replaces
- the card dropped its href, and BaseLearningResourceCard's `linkTarget` drops
  `pushUrl` along with a falsy href — so the title would be neither a link nor
  clickable, breaking the drawer for that card

Fall back to the drawer URL in both, and assert it: reverting either fix fails
the new cases.

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

* Describe what the sitemap and canonical do, not what changed

Both comments narrated the edit, which only parses while holding the diff.
State the durable facts instead: the sitemap needs no per-type branching and a
resource with several parents appears once under its canonical parent; the
drawer hands its ranking signal to the resource's dedicated page.

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

* Trim the drawer canonical comment to the invariant

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

* Narrow the falsified-learn_url cast to the one field

`as never` on the whole object drops type checking for every other prop, so a
later change to the resource shape would not surface here.

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

* Share the resource's page rather than a drawer over search

A shared link now lands on the page that owns the content where one exists, so
sharing stops seeding drawer URLs for crawlers to find. Slightly user-visible:
the recipient sees the dedicated page instead of the search page with a drawer
over it.

The drawer reads the detail endpoint, which always carries learn_url, so the
fallback here is only for a frontend deployed ahead of the backend.

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

* Read learn_url directly, without a fallback

learn_url is non-nullable, so the drawer canonical, the card href and the share
URL can read it as given. Drops the `||` branches, the comments explaining them,
and the absent/blank cases that covered them.

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

---------

Co-authored-by: Ahtesham Quraish <ahtesham.quraish@arbisoft.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* Update dependency drf-spectacular to >=0.30,<0.31

* update spec

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Anastasia Beglova <abeglova@mit.edu>
@odlbot
odlbot requested a review from a team as a code owner August 31, 2026 12:38
@github-actions

Copy link
Copy Markdown

OpenAPI Changes

418 changes: 366 error, 0 warning, 52 info

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants