Release 0.8.0
The Windows installer has never shipped under a version: v0.7.0 was tagged before any of it existed, so every build handed out so far stamped a server with 0.7.0. Two servers running different builds were indistinguishable, and the installer logged each upgrade as "same version already installed" rather than recording what changed. This cuts the release that fixes that. 0.8.0 rather than a patch: the air-gapped installer is a new capability, and pre-1.0 semantic versioning puts that in the minor slot (ADR-007). CHANGELOG gains a 0.8.0 section covering the twelve defects a real Windows Server 2019 install surfaced, the move from inferring "is this a re-run of my install?" to recording it, and the operator documentation. deploy/site-profile-universal.json is now in the repository. Released builds were being produced from a profile in a temporary directory, so the next release could not have been reproduced once that file was cleaned up. docs/RELEASING-WINDOWS.md points at the committed profile and says why. scripts/gen_openapi.py reads __version__ out of shopdb/__init__.py instead of restating it. Its hardcoded copy had already drifted a release behind, which is the same mistake that once shipped an installer stamped with the wrong version.
This commit is contained in:
69
CHANGELOG.md
69
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 `<allowedServerVariables>` 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
|
||||
|
||||
20
deploy/site-profile-universal.json
Normal file
20
deploy/site-profile-universal.json
Normal file
@@ -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 <repo>. Use site-profile.example.json instead only when a site genuinely needs a lean build; see ADR-013."
|
||||
}
|
||||
@@ -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 <site-profile.json> <repo-path>
|
||||
./deploy/windows/installer/build-installer.sh deploy/site-profile-universal.json <repo-path>
|
||||
```
|
||||
|
||||
`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`:
|
||||
|
||||
```
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shopdb-frontend",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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 <int:id> / <id> -> 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 '
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user