diff --git a/CHANGELOG.md b/CHANGELOG.md index 47529ac..27a9de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,71 @@ ADR-007 and ADR-002. ## [Unreleased] -### Added +## [0.8.0] - 2026-08-05 + +First release to carry the Windows installer. Everything below shipped after +v0.7.0 was tagged, and a complete install was exercised end to end on Windows +Server 2019 before this release was cut. + +### Fixed - installer, from a real Server 2019 install + +- `packaging` was imported by the plugin loader but declared nowhere. It reached + development and CI only as a dependency of pytest, so the whole suite passed + while a virtual environment built from `requirements.txt` alone - which is + exactly what the installer builds - could not import the application at all. + `tests/test_runtime_dependencies.py` now fails on any runtime import that is + not a declared dependency. +- IIS returned 500.52 before the application was ever launched. `web.config` + declared `` for the X-Forwarded-For rule, and that + section ships `overrideModeDefault="Deny"`, so the whole file was rejected. + The installer now permits the single variable at server level instead of + unlocking the section for every site on the machine, and repairs a + `web.config` an earlier build had made unusable. +- The config unlock ran before the application it unlocks existed, so the scoped + form could never succeed on a first install and every install silently fell + back to granting handler delegation server-wide. +- The stage 5 smoke test discarded the status code and error page it had already + received, reporting "site did not return 200" for a fault IIS had named. It + now records both, plus the tail of the application log. +- Database dumps were readable by every authenticated user: a directory created + under ProgramData inherits `Users:RX`, and the owner-only ACL was applied only + when the installer itself created it. +- The uninstaller ran the 32-bit PowerShell, which cannot see IIS, so the site, + pool and application survived a "successful" uninstall pointing at a deleted + directory. +- Uninstall matched applications by alias alone and would remove an unrelated + application of the same name under another site. +- Wizard input reached a command line unchecked: unvalidated ports, a drive-root + path that escaped its own quote, and a password written as ANSI but read back + as UTF-8, which reported a correct non-ASCII password as wrong. +- Plugin deregistration could never succeed - it omitted `--yes` against a + command that prompts - while the plugin directory was deleted regardless. +- Preflight rows were drawn past the bottom of the panel and silently vanished; + the failures loop had no cap at all. + +### Changed - installer behaviour + +- "Is this a re-run of my install?" is answered from a durable install record + rather than inferred from the state of the machine. Nothing on a server says + who created its database tables, so a retry after a failed first install was + taken for an upgrade of a working system: it demanded a mandatory backup of a + database its own failed attempt had written, then refused to prune tables it + had created minutes earlier. During unfinished first provisioning the backup + is advisory and prune may force; on an established install both are unchanged. +- The installer offers every bundled plugin, so one build serves any site + instead of one build per plugin profile. + +### Added - operator documentation and diagnostics + +- `docs/UPDATES-WINDOWS.md` - what operators should expect from future updates, + bug fixes and security releases, including downtime, what is preserved, and + the effect on other sites sharing the same IIS server. +- `docs/RELEASING-WINDOWS.md` - how to build and release, and the two known gaps. +- `deploy/windows/shopdb-diagnose.py` - collects what IIS answers, the config + lock state, the application logs and the ACLs in one pass, scrubbing secrets + before writing anything. + +### Added - the installer itself - Air-gapped Windows installer (`deploy/windows/installer/`). One `.exe` per site, built from that site's plugin profile, containing Python, the wheels, @@ -539,7 +603,8 @@ letting other GE Aerospace sites stand up their own self-hosted instance integration that passed the key as a query parameter. See `docs/COLLECTOR-INTEGRATION.md`. -[Unreleased]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.7.0...HEAD +[Unreleased]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.8.0...HEAD +[0.8.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.7.0...v0.8.0 [0.7.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.6.0...v0.7.0 [0.6.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/compare/v0.5.0...v0.6.0 [0.5.0]: https://gitea.proudtech.net/ge-aerospace/shopdb-flask/releases/tag/v0.5.0 diff --git a/deploy/site-profile-universal.json b/deploy/site-profile-universal.json new file mode 100644 index 0000000..a80a16e --- /dev/null +++ b/deploy/site-profile-universal.json @@ -0,0 +1,20 @@ +{ + "site": "universal", + "plugins": [ + "computers", + "employees", + "geenforce", + "knowledgebase", + "machines", + "measuringtools", + "network", + "notifications", + "printedparts", + "printers", + "slides", + "usb", + "warranty" + ], + "locked": [], + "_comment": "The profile the released Windows installer is built from. Every bundled plugin that carries a manifest, so ONE exe serves any site: the wizard offers all of them and the operator ticks what that site uses. Plugins left unticked are never installed, and 'flask plugin prune-schema' drops their tables at provisioning (ADR-014). 'applications' is deliberately absent - it is manifest-less core and always ships. Build with: deploy/windows/installer/build-installer.sh deploy/site-profile-universal.json . Use site-profile.example.json instead only when a site genuinely needs a lean build; see ADR-013." +} diff --git a/docs/RELEASING-WINDOWS.md b/docs/RELEASING-WINDOWS.md index 658d805..7798442 100644 --- a/docs/RELEASING-WINDOWS.md +++ b/docs/RELEASING-WINDOWS.md @@ -24,9 +24,16 @@ Almost every release is the first kind. ```bash # bump __version__ in shopdb/__init__.py first -./deploy/windows/installer/build-installer.sh +./deploy/windows/installer/build-installer.sh deploy/site-profile-universal.json ``` +`deploy/site-profile-universal.json` is the profile released builds come from: +every bundled plugin, so one `.exe` serves any site and the operator ticks what +that site uses. Build from a narrower profile only when a site genuinely needs a +lean build (ADR-013). Do not build a release from a profile that is not in the +repository - the build stops being reproducible the moment that file is +somewhere else. + Then on Windows, in `deploy/windows/installer`: ``` diff --git a/docs/openapi.json b/docs/openapi.json index d14d7bc..474a11e 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "ShopDB Flask API", - "version": "0.7.0", + "version": "0.8.0", "description": "Asset-management API (core + plugins). Responses use a `success_response` envelope: `{status, data, meta}`. Auth: Bearer JWT (login or a managed PAT) for `jwt`/`admin`/`permission:*`; `X-API-Key` for collector/managed-token endpoints; public endpoints need neither." }, "servers": [ diff --git a/frontend/package.json b/frontend/package.json index 37f0dfb..3ca7bbb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "shopdb-frontend", - "version": "0.7.0", + "version": "0.8.0", "private": true, "type": "module", "scripts": { diff --git a/scripts/gen_openapi.py b/scripts/gen_openapi.py index 907b6ea..638cebd 100644 --- a/scripts/gen_openapi.py +++ b/scripts/gen_openapi.py @@ -20,6 +20,21 @@ OUT = os.path.join(REPO, 'docs', 'openapi.json') VERBS = ('get', 'post', 'put', 'patch', 'delete', 'head', 'options') +def _product_version(): + """Read __version__ out of shopdb/__init__.py without importing the app. + + Importing shopdb here would pull in Flask, the plugin loader and a database + configuration, none of which this generator needs. + """ + here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + init = os.path.join(here, 'shopdb', '__init__.py') + with open(init, 'r', encoding='utf-8') as handle: + match = re.search(r"^__version__ = '([^']+)'", handle.read(), re.M) + if not match: + raise SystemExit('could not read __version__ from ' + init) + return match.group(1) + + def _norm_path(p): # Flask / -> OpenAPI {id} return re.sub(r'<(?:[^:>]+:)?([^>]+)>', r'{\1}', p or '') @@ -68,7 +83,10 @@ def build(surfaces): 'openapi': '3.1.0', 'info': { 'title': 'ShopDB Flask API', - 'version': '0.7.0', + # Read from the code, not restated. A hardcoded copy here had + # already drifted a release behind, and the same mistake in the + # installer script shipped an exe stamped with the wrong version. + 'version': _product_version(), 'description': ( 'Asset-management API (core + plugins). Responses use a ' '`success_response` envelope: `{status, data, meta}`. Auth: Bearer ' diff --git a/shopdb/__init__.py b/shopdb/__init__.py index dec1204..66e24d0 100644 --- a/shopdb/__init__.py +++ b/shopdb/__init__.py @@ -42,7 +42,7 @@ __contract_version__ = '0.15.0' # plugin-contract version above are distinct series with independent # bump rules. Not part of the shopdb.api contract surface, so it is # not re-exported there. -__version__ = '0.7.0' +__version__ = '0.8.0' def create_app(config_name: str = None) -> Flask: