Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# Changelog

Entries are collected from the fragments in `changelog.d/` by
[scriv](https://scriv.readthedocs.io/) when a release is cut. Entries before
the switch to scriv were reconstructed from the git history.

<!-- scriv-insert-here -->

<a id='changelog-2026.9.30.1'></a>
## 2026.9.30.1 (2026-09-30)

### Added

- `b2b_learner_records`: serve `/courses` from `mv_b2b_contract_courserun` ([#60](https://github.com/mitodl/ol-analytics-api/pull/60))
- `b2b_learner_records`: serve activity from the learner-records materialized views ([#62](https://github.com/mitodl/ol-analytics-api/pull/62))
- `b2b_learner_records`: bounded date-range filters for partitioned backfills ([#72](https://github.com/mitodl/ol-analytics-api/pull/72))
- `b2b_dashboard`: per-status counts on the learner-progress envelope ([#68](https://github.com/mitodl/ol-analytics-api/pull/68))
- `b2b_dashboard`: learner-progress serves activity and carries a needs-attention count ([#78](https://github.com/mitodl/ol-analytics-api/pull/78))
- `b2b_dashboard`: filter learner-progress by course run ([#79](https://github.com/mitodl/ol-analytics-api/pull/79))
- A per-tenant OpenAPI spec committed under `openapi/specs/`, regenerated by `bin/generate-openapi-spec` and diffed in CI ([#70](https://github.com/mitodl/ol-analytics-api/pull/70))

### Changed

- Dependency and lockfile updates ([#75](https://github.com/mitodl/ol-analytics-api/pull/75), [#77](https://github.com/mitodl/ol-analytics-api/pull/77))

### Fixed

- `b2b`: blank learner names are returned as `null` ([#71](https://github.com/mitodl/ol-analytics-api/pull/71))

<a id='changelog-2026.9.17.1'></a>
## 2026.9.17.1 (2026-09-17)

### Added

- `b2b_learner_records` tenant, a machine-to-machine per-learner progress API ([#56](https://github.com/mitodl/ol-analytics-api/pull/56))
- `b2b_dashboard`: contract-scoped learner-progress endpoint ([#59](https://github.com/mitodl/ol-analytics-api/pull/59))
- Descriptions on analytics response fields ([#57](https://github.com/mitodl/ol-analytics-api/pull/57))
- B2B learner records spec and provider-access decision record ([#55](https://github.com/mitodl/ol-analytics-api/pull/55))

### Changed

- Python base image updated to 3.14 ([#48](https://github.com/mitodl/ol-analytics-api/pull/48))
- Dependencies pinned and updated ([#46](https://github.com/mitodl/ol-analytics-api/pull/46), [#47](https://github.com/mitodl/ol-analytics-api/pull/47), [#49](https://github.com/mitodl/ol-analytics-api/pull/49), [#50](https://github.com/mitodl/ol-analytics-api/pull/50), [#51](https://github.com/mitodl/ol-analytics-api/pull/51), [#61](https://github.com/mitodl/ol-analytics-api/pull/61))

### Security

- Sentry events cap request bodies at 1KB and scrub Postgres `DETAIL` rows ([#54](https://github.com/mitodl/ol-analytics-api/pull/54))

<a id='changelog-2026.9.3.1'></a>
## 2026.9.3.1 (2026-09-03)

### Changed

- Renovate configuration ([#44](https://github.com/mitodl/ol-analytics-api/pull/44))

### Fixed

- OpenTelemetry instruments the FastAPI instance rather than the module attribute ([#52](https://github.com/mitodl/ol-analytics-api/pull/52))

<a id='changelog-2026.8.28.2'></a>
## 2026.8.28.2 (2026-08-28)

### Fixed

- `b2b_dashboard`: `contract_id` is typed as an integer ([#39](https://github.com/mitodl/ol-analytics-api/pull/39))

<a id='changelog-2026.8.28.1'></a>
## 2026.8.28.1 (2026-08-28)

### Added

- `b2b_dashboard`: contract-scoped endpoints mirroring mitxonline's manager dashboard ([#34](https://github.com/mitodl/ol-analytics-api/pull/34))

### Changed

- The reference Kubernetes manifest mirrors the deployed pod hardening ([#32](https://github.com/mitodl/ol-analytics-api/pull/32))

### Fixed

- Stop overriding the OpenTelemetry SDK's OTLP endpoint resolution ([#35](https://github.com/mitodl/ol-analytics-api/pull/35))

### Security

- `b2b_dashboard`: every activity aggregate is floored through the cohort that produced it ([#33](https://github.com/mitodl/ol-analytics-api/pull/33))

<a id='changelog-2026.8.12.1'></a>
## 2026.8.12.1 (2026-08-12)

### Fixed

- `b2b_dashboard`: contract, program, and course run primary key fields are typed as strings ([#29](https://github.com/mitodl/ol-analytics-api/pull/29))

<a id='changelog-2026.8.3.1'></a>
## 2026.8.3.1 (2026-08-03)

### Changed

- Granian is the application server ([#21](https://github.com/mitodl/ol-analytics-api/pull/21))

### Fixed

- `b2b`: the org-manager check authenticates to MITx Online with OAuth2 client credentials ([#22](https://github.com/mitodl/ol-analytics-api/pull/22))
- `b2b`: two `cohort_policy` docstrings that overstated their safety guarantees ([#23](https://github.com/mitodl/ol-analytics-api/pull/23))

### Security

- zizmor analysis of GitHub Actions workflows, SHA-pinned actions, and a 7-day `exclude-newer` window on dependency resolution ([#24](https://github.com/mitodl/ol-analytics-api/pull/24))

<a id='changelog-2026.7.30.1'></a>
## 2026.7.30.1 (2026-07-30)

### Added

- `b2b`: total row count in the analytics envelopes ([#15](https://github.com/mitodl/ol-analytics-api/pull/15))

### Changed

- CI: a dropped `pull_request` run can be recovered without an empty commit ([#17](https://github.com/mitodl/ol-analytics-api/pull/17))

### Fixed

- `b2b`: org endpoints are keyed on the Keycloak org UUID (`sso_organization_id`) ([#13](https://github.com/mitodl/ol-analytics-api/pull/13))

<a id='changelog-2026.7.22.1'></a>
## 2026.7.22.1 (2026-07-22)

First release.

### Added

- Multi-tenant FastAPI service serving aggregated B2B analytics from StarRocks materialized views ([#1](https://github.com/mitodl/ol-analytics-api/pull/1))
- `as_of` + `data` envelope on analytics responses ([#7](https://github.com/mitodl/ol-analytics-api/pull/7))
- Test coverage at 100% with coverage reporting in CI ([#8](https://github.com/mitodl/ol-analytics-api/pull/8))
- `bump-my-version` configuration for the ol-concourse release resource

### Changed

- Endpoint table, tenant contract, and per-view `as_of` from the architecture review ([#6](https://github.com/mitodl/ol-analytics-api/pull/6))

### Fixed

- Tenant readiness is scoped so one tenant's upstream can't fail the whole pod ([#3](https://github.com/mitodl/ol-analytics-api/pull/3))

### Security

- The k-anonymity floor applies across every cohort column ([#5](https://github.com/mitodl/ol-analytics-api/pull/5))
- StarRocks queries are bounded by `LIMIT`/pagination and query and pool timeouts ([#4](https://github.com/mitodl/ol-analytics-api/pull/4))
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,25 @@ uv run ruff check .
uv run mypy src
```

## Changelog

A change that someone deploying or consuming the API would notice should carry
a changelog fragment:

```bash
uv run scriv create
```

This writes a file under `changelog.d/` with a commented-out section per
category (Added, Changed, Deprecated, Removed, Fixed, Security). Uncomment the
one that fits, write the entry, and commit it with the change.

Nothing else needs doing at release time. The Concourse release job runs
bump-my-version, whose `pre_commit_hooks` run `bin/collect-changelog`, which
folds every fragment into `CHANGELOG.md` under the new version and deletes the
fragments in the same release commit. A release with no fragments still goes
out, with no changelog entry.

## The published API contract

Each tenant's OpenAPI document is committed under `openapi/specs/<tenant>.yaml`
Expand Down
24 changes: 24 additions & 0 deletions bin/collect-changelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Collect changelog.d/ fragments into CHANGELOG.md, if there are any.

Run by bump-my-version's pre_commit_hooks when the release job bumps the
version (see [tool.bumpversion] in pyproject.toml).

`scriv collect` exits 2 when there is nothing to collect, and a failing hook
fails the release. A release made only of changes that carry no fragment (e.g.
a batch of Renovate updates) is still a release, so an empty changelog.d/ is a
no-op here rather than an error.
"""

from __future__ import annotations

import sys

from scriv.collect import collect
from scriv.scriv import Scriv

if __name__ == "__main__":
if not Scriv().fragments_to_combine():
sys.stdout.write("No changelog fragments to collect\n")
raise SystemExit(0)
collect.main(args=[])
Empty file added changelog.d/.gitkeep
Empty file.
3 changes: 3 additions & 0 deletions changelog.d/20260930_101301_blarghmatey_scriv_changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Changelog fragments with scriv. A change worth noting adds a fragment under `changelog.d/`, and the release job collects whatever fragments are present into `CHANGELOG.md` during the version bump.
24 changes: 24 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ mitol-django-scim = "0d"
mitol-drf-lint = "0d"

[dependency-groups]
# scriv sits in its own group so the release task can install it without the
# rest of the dev toolchain (see pre_commit_hooks under [tool.bumpversion]).
release = [
"scriv>=1.8.0",
]
dev = [
{ include-group = "release" },
"ruff>=0.8",
"mypy>=1.13",
"pytest>=8.3",
Expand Down Expand Up @@ -141,12 +147,30 @@ plugins = ["pydantic.mypy"]
module = ["aiomysql", "aiomysql.*"]
ignore_missing_imports = true

[tool.scriv]
format = "md"
fragment_directory = "changelog.d"
changelog = "CHANGELOG.md"
version = "literal: pyproject.toml: project.version"
categories = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
md_header_level = "2"
entry_title_template = "{{ version }} ({{ date.strftime('%Y-%m-%d') }})"

[tool.bumpversion]
current_version = "2026.9.30.1"
commit = false
tag = false
parse = "(?P<release>(?:[1-9][0-9]{3})\\.(?:1[0-2]|[1-9])\\.(?:3[0-1]|[12][0-9]|[1-9]))\\.(?P<build>\\d+)"
serialize = ["{release}.{build}"]
# The ol-concourse release job runs `bump-my-version bump --no-commit` in the
# mitodl/ol-concourse-dsl image, which ships uv but not scriv, then the release
# resource commits the tree with `git add -u`. Pre-commit hooks still run under
# --no-commit, after pyproject.toml carries the new version, so scriv reads it
# from there. `git add -u` stages only tracked files, so CHANGELOG.md has to
# exist on main before a release rather than be created by one.
pre_commit_hooks = [
"uv run --frozen --only-group release bin/collect-changelog",
]

[tool.bumpversion.parts.release]
calver_format = "{YYYY}.{MM}.{DD}"
Expand Down
64 changes: 64 additions & 0 deletions tests/test_collect_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""bin/collect-changelog, run against a copy of the repo's real scriv config.

The release job runs this from bump-my-version's pre_commit_hooks, so a failure
here fails the release. Both branches matter: a release with fragments has to
fold them into CHANGELOG.md, and a release with none (e.g. only Renovate
updates) has to succeed without touching it.
"""

import shutil
import subprocess
import sys
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parent.parent
SCRIPT = REPO_ROOT / "bin" / "collect-changelog"
CHANGELOG_HEADER = "# Changelog\n\n<!-- scriv-insert-here -->\n"


@pytest.fixture
def project(tmp_path: Path) -> Path:
shutil.copy(REPO_ROOT / "pyproject.toml", tmp_path / "pyproject.toml")
(tmp_path / "CHANGELOG.md").write_text(CHANGELOG_HEADER)
(tmp_path / "changelog.d").mkdir()
(tmp_path / "changelog.d" / ".gitkeep").touch()
return tmp_path


def run_collect(cwd: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run( # noqa: S603
[sys.executable, str(SCRIPT)],
cwd=cwd,
capture_output=True,
text=True,
check=False,
)


def test_collects_fragments_under_the_project_version(project):
fragment = project / "changelog.d" / "20260101_000000_someone_change.md"
fragment.write_text("### Fixed\n\n- A fixed thing.\n")

result = run_collect(project)

assert result.returncode == 0, result.stderr
changelog = (project / "CHANGELOG.md").read_text()
version = next(
line.split('"')[1]
for line in (project / "pyproject.toml").read_text().splitlines()
if line.startswith("version = ")
)
assert f"## {version} (" in changelog
assert "### Fixed\n\n- A fixed thing." in changelog
assert not fragment.exists()
assert (project / "changelog.d" / ".gitkeep").exists()


def test_no_fragments_is_a_no_op(project):
result = run_collect(project)

assert result.returncode == 0, result.stderr
assert "No changelog fragments to collect" in result.stdout
assert (project / "CHANGELOG.md").read_text() == CHANGELOG_HEADER
Loading
Loading