A repeatable, end-to-end demo for mainframe modernization: a legacy IBM COBOL + DB2 program is migrated to a modern Python FastAPI + Vue app, driven by a CLI that embeds the GitHub Copilot CLI, follows Test-Driven Development, swaps DB2 → Azure SQL, and deploys to Azure (Web App + Function App).
The sample keeps database configuration external to the application. It can use an existing Azure SQL database through an application setting, or SQLite for local development and automated tests, without changing application code.
legacy/ Legacy mainframe source (the input)
STOREOPS.cbl COBOL program: stock enquiry / issue / receive / add / reorder
copybook/ PARTREC.cpy record layout
db2/schema.sql Original DB2 DDL (PART_MASTER, STOCK_TXN)
migrator/ The migration CLI that embeds GitHub Copilot CLI
cobol_migrator/ analyze | migrate (TDD) | test | deploy | all
app/ The modernized application (migration output)
backend/ FastAPI + SQLAlchemy 2.0 + Pydantic v2, pytest (TDD)
functions/ Azure Functions (timer + HTTP) reorder batch
frontend/ Vue 3 + Vite SPA (built into backend/app/static)
infra/ Bicep (App Service, Function App, optional Azure SQL, App Insights)
azure.yaml Azure Developer CLI (azd) manifest
| COBOL paragraph | Modern module |
|---|---|
2000-STOCK-ENQUIRY |
GET /api/parts/{part_number} |
3000-ISSUE-PART (insufficient-stock guard) |
POST /api/parts/{n}/issue |
4000-RECEIVE-PART |
POST /api/parts/{n}/receive |
5000-ADD-PART (duplicate guard) |
POST /api/parts |
6000-REORDER-REPORT |
GET /api/parts/reorder-report + Function App |
7000-WRITE-TRANSACTION |
repository._write_txn (STOCK_TXN insert) |
COMMIT / ROLLBACK |
SQLAlchemy session commit/rollback |
The hardest question in mainframe modernization is not "can we rewrite it?" — it's "how do we prove the new system behaves exactly like the old one?" This project answers that with strict TDD: the tests are the migration contract. We capture each COBOL business rule (and every edge case) as an executable test first, then build the Python to satisfy it. Green tests = the rule survived.
- RED — write a test that states a rule; run it; it fails (no code yet). A failing test first proves the test actually checks something.
- GREEN — write the minimum code to make it pass. No production code exists unless a failing test demanded it.
- REFACTOR — clean up with the tests still green.
The migration is driven by migrator/cobol_migrator/prompts.py, whose prompt forces Copilot through these phases and ships an explicit TEST MATRIX of edge cases (boundaries, validation, and rollback paths).
| Layer | File | Purpose |
|---|---|---|
| API / behaviour | app/backend/tests/test_store.py | Full HTTP contract: status code, response body, and resulting DB state |
| Business rules (unit) | app/backend/tests/test_repository.py (generated for new migrations) |
Pure rule checks on the repository |
| Business rule | COBOL | Test asserts |
|---|---|---|
| Look up a part | 2000-STOCK-ENQUIRY |
200 + record; unknown → 404 |
| Issue reduces stock | 3000-ISSUE-PART |
stock −qty, one STOCK_TXN (I) written |
| Can't oversell | 3000-ISSUE-PART |
qty > on‑hand → 409, stock unchanged, no txn written |
| Boundary: qty == on‑hand | 3000-ISSUE-PART |
allowed, drives stock to exactly 0 |
| Bad quantity | validation | 0 / negative / missing → 422 |
| Receive adds stock | 4000-RECEIVE-PART |
stock +qty, one STOCK_TXN (R) written |
| No duplicate parts | 5000-ADD-PART |
existing number → 409, no overwrite |
| Field limits | validation | part≤10 / desc≤40 / loc≤8 → 422; status defaults to A |
| Reorder filter | 6000-REORDER-REPORT |
on‑hand ≤ point and active only |
| Boundary: on‑hand == point | 6000-REORDER-REPORT |
included; point+1 excluded |
| Obsolete excluded | 6000-REORDER-REPORT |
O/H parts excluded even when low |
| Transactional integrity | 9000-COMMIT-AND-EXIT |
a failed op rolls back part and txn together |
cd app/backend
pip install -r requirements.txt -r requirements-dev.txt
pytest # the TDD contract, runs in < 5s, no Azure needed
pytest --cov=app --cov-report=term-missing # coverage reportTests are hermetic — they run against an isolated SQLite database per test
(see tests/conftest.py), so there is no dependency on Azure SQL and the suite
is fast and deterministic for CI. The shipped suite covers the app package at
~88%; the migration prompt enforces a ≥95% gate on the suites it generates.
- Faithful by construction — behaviour is pinned by tests, not by hope.
- Edge cases are first-class — boundaries, validation, and rollback are encoded, not left to manual QA.
- Model-agnostic — the same TDD prompt works with any model via
--model. - CI-ready — the migration prompt sets a coverage gate (
fail-under=95) so generated suites block regressions.
pip install -e migrator
# Read-only modernization brief of the COBOL
cobol-migrate analyze --model claude-sonnet-4.5
# TDD migration: Copilot writes failing tests first, then the implementation
cobol-migrate migrate --model claude-sonnet-4.5
# See the exact Copilot command + prompt without calling the model
cobol-migrate migrate --dry-run
# Run the migrated backend's pytest suite
cobol-migrate test
# Provision + deploy to Azure
cobol-migrate deploy
# migrate -> test -> deploy
cobol-migrate all --model claude-sonnet-4.5Under the hood it runs the Copilot CLI non-interactively:
copilot -C <repo> --model <model> --allow-all-tools -p "<TDD migration prompt>".
Swap --model for any model (e.g. gpt-5, auto) — "a model of our choice".
# Backend + tests
cd app/backend
pip install -r requirements.txt -r requirements-dev.txt
pytest # 9 passing (the TDD contract)
python -m app.seed # sample parts
uvicorn app.main:app --reload
# Frontend (dev, proxies /api to :8000)
cd app/frontend
npm install
npm run devapp/backend/app/config.py resolves the database in this order:
DATABASE_URL(full SQLAlchemy URL) — used by CI/tests.AZURE_SQL_CONNECTIONSTRING(ODBC) — the swap; used in Azure. Set this app setting to an Azure SQL connection string and the app targets Azure SQL with no code change.- Local SQLite fallback — so everything runs with zero infrastructure.
Provision a demo Azure SQL server with the infra by setting DEPLOY_AZURE_SQL=true
(azd env set DEPLOY_AZURE_SQL true) in a subscription whose policy allows it.
The App Service startup.sh installs the ODBC Driver 18 only when the
connection string is present.
azd auth login
azd env new storeops-dev
azd env set AZURE_SQL_ADMIN_PASSWORD <StrongPassword>
azd up