Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
154 commits
Select commit Hold shift + click to select a range
59180e8
spec deployment-scoped artefacts
dannash100 Sep 3, 2026
70fc850
spec reporting schemas
dannash100 Aug 30, 2026
b6eb71e
tighten reporting spec
dannash100 Aug 30, 2026
1aa1942
pin artefact and source
dannash100 Aug 30, 2026
8c46775
adopt group terminology
dannash100 Aug 30, 2026
c49f4b4
settle artefact identity
dannash100 Aug 31, 2026
9a7995b
trim alerting
dannash100 Aug 31, 2026
54b3089
spec version artefacts
dannash100 Aug 31, 2026
c8028d3
demote coverage
dannash100 Aug 31, 2026
5d6a461
build rides the migrate pathway
dannash100 Aug 31, 2026
71b92ce
settle build contract
dannash100 Sep 2, 2026
4c6ab3d
propose apply and publish
dannash100 Sep 2, 2026
14edef8
facilities run it too
dannash100 Sep 3, 2026
dfb931e
rpt review pass
dannash100 Sep 3, 2026
1a82c7e
device applies the schema
dannash100 Sep 3, 2026
7b1e3cc
name and trace artefacts
dannash100 Sep 3, 2026
71c3a38
address spec review
dannash100 Sep 3, 2026
fc6359e
narrow artifact registrars
dannash100 Sep 3, 2026
4fef7ab
Merge remote-tracking branch 'origin/feature/deployment-artefacts' in…
dannash100 Sep 3, 2026
f682611
address spec review
dannash100 Sep 3, 2026
e3716db
canopy holds group artifacts
dannash100 Sep 4, 2026
23afb56
Merge remote-tracking branch 'origin/feature/deployment-artefacts' in…
dannash100 Sep 4, 2026
b08c287
publish through canopy
dannash100 Sep 4, 2026
a143208
artifact lifetime and digests
dannash100 Sep 4, 2026
b3ac405
Merge pull request #525 from beyondessential/feature/reporting-schema…
dannash100 Sep 6, 2026
2b92c15
merge deployment-artefacts specs
dannash100 Sep 6, 2026
9e16ab5
settle artifact spec review
dannash100 Sep 6, 2026
68aebaa
scope artifacts to groups
dannash100 Sep 6, 2026
87435a3
test artifact scoping
dannash100 Sep 6, 2026
96f0e79
regenerate api surface
dannash100 Sep 6, 2026
5c43219
show held artifacts to operators
dannash100 Sep 6, 2026
35f000f
register artifacts for a group
dannash100 Sep 6, 2026
e400b92
test group scoped artifacts
dannash100 Sep 6, 2026
9ecea71
tighten auth comment
dannash100 Sep 6, 2026
49c8cc5
refresh openapi snapshot
dannash100 Sep 6, 2026
e53392b
Merge branch 'epic/deployment-artefacts' into feat/group-scoped-artif…
dannash100 Sep 6, 2026
22be6dc
held artifacts take no url
dannash100 Sep 6, 2026
6e383dc
drop unused resolver
dannash100 Sep 6, 2026
d2c6888
name machines and applications in rpt
dannash100 Sep 6, 2026
dc7e06e
Merge branch 'epic/deployment-artefacts' into feat/group-scoped-artif…
dannash100 Sep 6, 2026
387eee1
record reporting schema builds
dannash100 Sep 6, 2026
29ad8a9
dispatch and report schema builds
dannash100 Sep 6, 2026
e448ee6
builders register their group's artifacts
dannash100 Sep 6, 2026
8f9084b
grade unbuilt reporting schemas
dannash100 Sep 6, 2026
a14a5e0
test reporting schema pairs
dannash100 Sep 6, 2026
a2a9f6b
present pairs to operators
dannash100 Sep 6, 2026
30dce51
test schema dispatch and publishing
dannash100 Sep 6, 2026
40d2739
describe the schema api
dannash100 Sep 6, 2026
3b78fb7
rebuild when a version's artifacts move
dannash100 Sep 6, 2026
0636c50
offer flag per scope
dannash100 Sep 7, 2026
42f055f
scope redaction gap
dannash100 Sep 7, 2026
dba3c1f
bad registrations are 400
dannash100 Sep 7, 2026
a5c780f
cover schema alerting
dannash100 Sep 7, 2026
4d77c40
test the pairs section
dannash100 Sep 7, 2026
4b5ec0b
cover builder authorisation
dannash100 Sep 7, 2026
27405eb
pin artifact resolution rules
dannash100 Sep 7, 2026
9e8023f
assert which artifact is hidden
dannash100 Sep 7, 2026
980f6ad
fix pair derivation
dannash100 Sep 7, 2026
3279655
cover artifact retention and scope
dannash100 Sep 7, 2026
e286119
cover the build report
dannash100 Sep 7, 2026
d12c030
cover the schema round trip
dannash100 Sep 7, 2026
f2aa605
cover credential and media type
dannash100 Sep 7, 2026
7971f28
seed reporting schema builds
dannash100 Sep 7, 2026
642b435
cover the artifact form
dannash100 Sep 7, 2026
486ced0
refuse a blank location
dannash100 Sep 7, 2026
20a75d6
size the artifact body limit
dannash100 Sep 7, 2026
51184fe
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 7, 2026
ee30a20
refuse oversized uploads in the form
dannash100 Sep 7, 2026
868707a
refuse a rename onto a taken identity
dannash100 Sep 7, 2026
9155990
refuse a blank registration
dannash100 Sep 7, 2026
033dc91
cover the offered flag over the wire
dannash100 Sep 7, 2026
a9190fb
pin error slugs to their headings
dannash100 Sep 7, 2026
6afbb5d
refuse a schema range
dannash100 Sep 7, 2026
6811260
cover the planned pair
dannash100 Sep 7, 2026
821c8b8
heading level and doc placement
dannash100 Sep 7, 2026
790c580
never redact a schema build
dannash100 Sep 7, 2026
0c60b4c
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 7, 2026
7de1f95
require an operator identity for artifacts
dannash100 Sep 7, 2026
25c4f5b
pin what is not a pair
dannash100 Sep 7, 2026
7add5f3
settling ignores the snapshot
dannash100 Sep 7, 2026
62107d6
propagate auth faults
dannash100 Sep 7, 2026
3231f52
override ranges across scopes
dannash100 Sep 7, 2026
e2d1630
check digests on arrival
dannash100 Sep 7, 2026
58354d9
record a releaser digest
dannash100 Sep 7, 2026
5ed9d42
answer an exact version for itself
dannash100 Sep 7, 2026
c08eea6
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 7, 2026
f74ae93
settle what clears the check
dannash100 Sep 7, 2026
dad51b0
name a pair's servers
dannash100 Sep 7, 2026
51c2e99
cover the servers column
dannash100 Sep 8, 2026
79d08ff
Merge branch 'main' into epic/deployment-artefacts
dannash100 Sep 8, 2026
75e53b3
Merge branch 'epic/deployment-artefacts' into feat/group-scoped-artif…
dannash100 Sep 8, 2026
18846bd
Merge branch 'feat/group-scoped-artifacts' into feat/reporting-schemas
dannash100 Sep 8, 2026
e27645e
settle where an artifact rests
dannash100 Sep 8, 2026
1c64e62
keep a build's output out of its own trigger
dannash100 Sep 8, 2026
77f0b36
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 8, 2026
b8de277
tighten artifact registration
dannash100 Sep 8, 2026
98e949a
count range artifacts
dannash100 Sep 8, 2026
9aa0f52
index artifact lookups
dannash100 Sep 8, 2026
29de010
settle pairs per group
dannash100 Sep 8, 2026
5cb2ae0
unstick a doc comment
dannash100 Sep 8, 2026
f257a06
count every pair
dannash100 Sep 8, 2026
f3b4c68
gate schema publishing
dannash100 Sep 9, 2026
d9c2ce5
dispatch marked builders
dannash100 Sep 9, 2026
054b733
expose publish flag
dannash100 Sep 9, 2026
218c238
add publish switch
dannash100 Sep 9, 2026
a543e55
respec schema publishing
dannash100 Sep 9, 2026
cc8a82d
refuse rather than fault
dannash100 Sep 9, 2026
b6d94f1
never render proxied bytes
dannash100 Sep 9, 2026
a347b52
spell out intent semantics
dannash100 Sep 9, 2026
0a81a86
sri digests and raw uploads
dannash100 Sep 9, 2026
83615a3
Merge branch 'feat/group-scoped-artifacts' into feat/reporting-schemas
dannash100 Sep 9, 2026
33b7d95
split group registration out
dannash100 Sep 9, 2026
9cd0300
settle artifact reads and digests
dannash100 Sep 9, 2026
2f1024c
Merge branch 'feat/group-scoped-artifacts' into feat/reporting-schemas
dannash100 Sep 9, 2026
ac4ae31
one publisher and one sweep query
dannash100 Sep 9, 2026
4e98362
narrow the schema sweep
dannash100 Sep 10, 2026
e298275
keep a later ask
dannash100 Sep 10, 2026
1c0d2f7
guard the publisher mark
dannash100 Sep 10, 2026
cc74c5d
load a group's members once
dannash100 Sep 10, 2026
71f8a29
narrow the version lookup
dannash100 Sep 10, 2026
9a6693d
index restore checks by run
dannash100 Sep 10, 2026
af1b740
refuse an ask with no pair
dannash100 Sep 10, 2026
cd3a717
read range artifacts once
dannash100 Sep 10, 2026
0e75194
hide the build button from viewers
dannash100 Sep 10, 2026
f709f79
regenerate the private api
dannash100 Sep 10, 2026
ae34d7d
dedup before the identity index
dannash100 Sep 10, 2026
c89bf27
refuse before drafting a version
dannash100 Sep 10, 2026
4e233d5
surface a digest failure
dannash100 Sep 10, 2026
115f19a
read one artifact by id
dannash100 Sep 10, 2026
e075277
hash before taking a connection
dannash100 Sep 10, 2026
70ebf5a
name only the groups listed
dannash100 Sep 10, 2026
867c581
render the section without a provider
dannash100 Sep 10, 2026
da2f502
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 10, 2026
795a2bf
refuse a stale credential
dannash100 Sep 14, 2026
5ccb55e
constrain blanks in sql
dannash100 Sep 14, 2026
64c6f8d
trim the artifact docs
dannash100 Sep 14, 2026
f59f65d
drop the slug anchor test
dannash100 Sep 14, 2026
32d5c7b
seed real digests in fixtures
dannash100 Sep 14, 2026
52fd97c
hold artifacts outside postgres
dannash100 Sep 14, 2026
ba49df8
note where the bytes rest
dannash100 Sep 14, 2026
69859dc
clean up a refused registration
dannash100 Sep 14, 2026
f146a26
format the store tests
dannash100 Sep 14, 2026
ec86842
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 14, 2026
6bda879
sweep artifacts nothing reaches
dannash100 Sep 14, 2026
0ec4209
say what a deletion takes
dannash100 Sep 14, 2026
860a5ec
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 14, 2026
04a0c35
note what raising the cap takes
dannash100 Sep 16, 2026
e1e5251
Merge remote-tracking branch 'origin/feat/group-scoped-artifacts' int…
dannash100 Sep 16, 2026
15a4fd7
Merge remote-tracking branch 'origin/main' into epic/deployment-artef…
dannash100 Sep 16, 2026
135f210
Merge branch 'epic/deployment-artefacts' into feat/group-scoped-artif…
dannash100 Sep 16, 2026
56f54ea
Merge branch 'feat/group-scoped-artifacts' into feat/reporting-schemas
dannash100 Sep 16, 2026
845e533
rank the seeded plans
dannash100 Sep 16, 2026
d11634d
Merge pull request #553 from beyondessential/feat/reporting-schemas
dannash100 Sep 17, 2026
6c7d418
Merge pull request #552 from beyondessential/feat/group-scoped-artifacts
dannash100 Sep 17, 2026
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
88 changes: 88 additions & 0 deletions .workhorse/specs/platform/artifacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
id: ART
---

# Version artifacts

An artifact is a file published for a version: an installer, a package, a set of migrations, a manifest.
Canopy holds what each one is and whom it is for, and a server or the infrastructure acting on a version's behalf learns from Canopy which files a version has and fetches each from where it rests.
An artifact may be for one group alone, and Canopy offers it only to a caller whose group it is.

## What an artifact belongs to

An artifact belongs to one exact version, or to a range of versions given as a semver pattern, and never to both.
A range artifact covers every version its pattern matches, so a file that does not change between releases is published once instead of per release.

An artifact carries a type and a platform, which together say what it is and what it is for.
A version's artifacts are the exact-version ones plus every range artifact whose pattern matches it.

An artifact may belong to a group, and one that does is for that group alone.
An artifact belonging to no group is for every group.
A group scope exists because some artifacts are derived from a group's own data and are wrong for anyone else.

## Where an artifact rests

An artifact belonging to no group rests at a location Canopy records and does not hold, which whoever is offered the artifact reads for itself.

A group-scoped artifact is carried to Canopy by the registration that publishes it, and Canopy holds it.
A publisher sends the bytes on the connection it registers over and is issued no credential to any store, so being authorised to register for a group is the whole of what publishing into it takes.
Canopy holds such an artifact in storage of its own, apart from any group's backup repo, so an artifact carries the retention, access, and cost basis of an artifact rather than those a backup repo is kept under (see [BAK](../public-server/backup.md)).
Where Canopy puts them is its own, and no caller addresses them there.
Canopy holds an artifact's bytes for as long as that artifact is registered, and keeps none of what it has stopped serving.
A registration or a deregistration that fails once the bytes have moved can leave bytes no artifact reaches, and Canopy drops those as well rather than letting them accumulate.
It does not expire bytes by age: an artifact's bytes are kept as long as it is registered however long that is, so age alone never says an artifact is finished with.

Canopy serves the bytes only to a caller the artifact is offered to.
The boundary is therefore enforced on the read rather than resting on a location being hard to guess.
An artifact whose bytes Canopy cannot produce is answered as one that does not exist, identically to an artifact the caller is not offered, so what Canopy has failed to keep is not discoverable through the read.

An artifact Canopy holds and an artifact Canopy records a location for are one thing to whoever is offered it.
It is offered one artifact per type and platform, and where the bytes rest is not part of what it is offered.

## What a version offers

A read names a version range, and an exact version is a valid one.
An exact version is answered for itself; a range is answered for the latest published version it covers that no known issue covers.

Canopy offers a caller one artifact per type and platform, chosen from the artifacts that caller may see: those belonging to no group, and those scoped to the caller's group where that group is known.
Where several match, the most specific is offered.
An artifact scoped to the caller's group is more specific than one belonging to no group.
Among artifacts of the same scope, an exact-version artifact is more specific than any range artifact, and between two ranges the narrower is more specific.
A group-scoped artifact and an unscoped one of the same type and platform are therefore both recorded, each group is offered the one for it, and no caller is offered both.
A pattern Canopy cannot parse matches nothing rather than everything, so a malformed range withholds a file instead of offering it to the whole fleet.

The full set, including the artifacts specificity passed over, is available to operators.
What resolution hides is a fact about how a version was published and an operator has to be able to see it.

## Who is offered a group-scoped artifact

A caller whose credential is bound to a server has that server's group, where the server has one.
It never names a group and is answered for its own alone, so a server cannot ask what another group is offered.
A caller carrying no group of its own names the group it asks about and is answered for a group it is authorised for: an operator for any group, and a component that produces or applies a group's artifacts for that group, as defined with those artifacts.
A read carrying no identity, or naming no group, is answered with the unscoped artifacts alone, so giving an artifact a group narrows who is offered it rather than widening what an open path serves.
A group-scoped artifact's existence is disclosed only to a caller it is offered to: a caller that names or guesses one it is not offered is answered as though it did not exist, so which groups hold one is not enumerable through the artifact surface.
Canopy passes a group-scoped artifact's bytes only to a caller it is offered to.

## Registration

Registering an unscoped artifact and registering a group's are separate: they name different things, carry different bodies, and are authorised differently, so each is its own path rather than one path that changes shape on a parameter.
An unscoped registration names the version or range, the type and the platform, and carries the artifact's location.
A group-scoped one names the group as well, carries the artifact's bytes, and names an exact version Canopy already holds rather than drafting one.
The group is named on the registration rather than inferred from the caller.

A releaser device registers unscoped artifacts, and carries no authorisation for any group.
An operator registers either.
A component that produces a group-scoped artifact registers it for that group under an authorisation defined with that artifact, and is authorised for no other.
A registration naming a group the caller is not authorised for is refused, which is the only gate publishing into a group has to pass.
A registration replaces whatever is already registered for the same version or range, type, platform, and group, so a rebuilt artifact is published exactly as a first one is and a caller is never offered two of a kind.

Canopy records which device registered an artifact and, where the registration names one, the run that produced it, so an artifact that arrived by automation is distinguishable from one entered by hand and traceable to what made it.

## Digests

An artifact carries a digest where whoever registers it records one, and an artifact Canopy holds carries one always.
A digest is expressed in Subresource Integrity format, using SHA-256.
A registration naming anything else is refused.
Canopy verifies the bytes it holds against their digest as they arrive and refuses the registration on a mismatch, so a corrupted upload is refused while whoever sent it is still there to send it again.
It verifies them again as it serves them and refuses them on a mismatch, so an artifact corrupted after it was taken in fails the read rather than reaching a server as the artifact it is not.
An unscoped artifact is read from its location by the caller rather than by Canopy, so its digest is what that caller checks what it fetched against, and an artifact registered without one is fetched unchecked.
87 changes: 87 additions & 0 deletions .workhorse/specs/public-server/reporting-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
id: RPT
---

# Reporting schemas

A reporting schema is the set of database views a Tamanu application's reports read from.
It is built for one Tamanu version against a database of one group, since its views follow from the group's configuration as well as from the version's schema, and every Tamanu application in that group applies the same one.
Canopy holds zero or one reporting schema per pair of group and Tamanu version, has one built for every pair it knows of, and offers it to the Tamanu applications of that group.

## Why it exists

Part of a reporting schema follows from the Tamanu version's database schema and is the same for every group on that version.
The rest follows from the group's own configuration, which only a database carrying that configuration can supply.
A schema for a pair is therefore built from a database of that group at that version, and ahead of an upgrade no such database exists, since the group's applications run the version they are leaving.
Canopy restores a group's backups into replicas and migrates them to a version (see [RST](restore-replicas.md)), so it is where such a database is produced, and it knows the version each group runs and the one it is moving to (see [APP](../servers/application-types.md), [UPG](../private-server/upgrade-plans.md)), so it is where the pairs are known.

## Actors

A **schema builder** produces a reporting schema from a database Canopy has restored and migrated for it, and publishes the result.
It is a restore consumer (see [RST](restore-replicas.md)): a build operates on a replica, so the builder is dispatched, credentialled, and reports over the replica pathways and authorisations, and it advertises an intent carrying `reporting-schema`.
How the builder produces a schema is the builder's own.

An **operator** declares which groups have a builder, marks the declaration that publishes a group's schema, reads which schema each application runs, and asks for the builds the derivation does not produce.

The **device of a machine a Tamanu application runs on** fetches the schema Canopy offers that application and applies it (see [DID](machine-identity.md)).

Canopy owns which pairs exist, the replica a build is given, the artifact that results, and offering it to the group's applications.

## Pairs

A reporting schema is unique per pair of group and Tamanu version, and Canopy holds zero or one per pair.
The pairs are, for each group covered by an enabled declaration marked as publishing its schema, each version a Tamanu application of the group reports running and the version its open plan moves it to (see [UPG](../private-server/upgrade-plans.md)).
That declaration is what covers a group: it names the group, is enabled or disabled, carries the operator's mark, and is audited (see [RST](restore-replicas.md)).
Only a published version is in a pair, since a version's migrations reach a builder as its published artifacts (see [ART](../platform/artifacts.md)) and an unpublished one has none.

A pair with no schema is built, and a pair with one is settled.
A settled pair is built again when the version's own artifacts change, since a schema built from a superseded release of the version is not the schema that version describes, and when an operator asks for it.
An operator asking for a pair's build is how a schema is refreshed after the group's configuration changes.
A rebuilt pair's schema replaces the one it held.
A failed build settles the pair as well, since a build against a fixed version and configuration fails the same way every time, and the pair is built again on the same two events.

## The build contract

Canopy dispatches a build to the builder as a restore replica, through the worklist every replica is dispatched through (see [RST](restore-replicas.md)).
The entry names the group and the Tamanu version the schema is for, and a machine of the group running a central Tamanu application, whose snapshot the replica is restored from, since the configuration a schema follows from is held centrally.
It carries what any replica's entry carries: the snapshot to restore, the repo coordinates, and the intent's parameter values.
The replica is migrated to the named version before the build reads it, and is not de-identified, since masking alters the configuration a schema follows from.

The builder obtains read credentials for the restore per run as any consumer does, and no storage credential of any kind for what it publishes (see [RST](restore-replicas.md)).

In the run it reports, the builder registers the **reporting schema** as an artifact of the exact version being built for, scoped to the group, of type `reporting-schema` on platform `any`, carrying a digest and the bytes themselves, which Canopy holds and serves (see [ART](../platform/artifacts.md)).
The schema is the only type it may register: what is published for a group is offered to every machine in it and fetched, so the authorisation stays defined with the artifact it was written for.
The builder is authorised to register artifacts for a group whose enabled declaration an operator has marked as publishing its reporting schema, and for no other, and is the one device other than a releaser that registers artifacts (see [ART](../platform/artifacts.md)).
The mark is the operator's alone, and is the whole of the authorisation: a consumer registers the set of semantics it advertises itself, so they shape what Canopy dispatches to it and grant it nothing, and what is published for a group is offered to every machine in it and applied.
Only a group-wide, non-redacting declaration of an intent carrying `reporting-schema` can carry the mark, which is the same declaration a build is dispatched for, so Canopy asks for no build it would refuse the result of.

A schema is published for the exact version and never for a range, since it follows from the migrations that version applies, and one built against a patch is not the schema another patch of the same minor describes.

## What a build reports

A build reports as its replica's restore report (see [RST](restore-replicas.md)), which names the group, the machine, the snapshot restored, and when it was observed.
Beyond those it carries:

- the **version** it was built for;
- the **outcome**, built or failed, and on failure a description of what went wrong;
- a reference to each **artifact** the build registered, of which the schema is one.

The restore's health and the build's outcome stay separate signals from the one report, as a migration test's do: a healthy replica whose build failed reports a healthy restore and a failed build.
Reports are retained indefinitely as an audit trail.

## The offering contract

Canopy offers a Tamanu application's device the schema for the pair of its group and the version that application reports running, resolved as any group-scoped artifact is (see [ART](../platform/artifacts.md)).
The device's credential resolves to the machine it is enrolled as and so to that machine's group, so it is offered its own group's schema and can fetch no other's.
A facility application is offered the same schema as the central applications of its group, since a schema follows the group and the version rather than the application it was built from.

The device compares what its application runs with what it is offered, applies the offered schema where they differ, and reports the result as a check on that application through the status contract (see [STA](statuses.md)), which is graded, presented, and alerted as any source's check is (see [CHK](../monitoring/checks.md)).

## Alerting

A failed build raises a reporting-schema check on the group's central Tamanu application, carrying the failure description (see [CHK](../monitoring/checks.md)).
The check is a warning rather than a failure, and does not escalate: the application is up and its reports return rows, and a schema that cannot be built for the version its group is moving to is for whoever maintains the reports rather than whoever is on call.
A replica that failed to restore or come up is the restore's own health rather than a build failure, and is dispatched again as any unhealthy restore is.
The check recovers when the pair is built, and nothing else clears it: an operator asking for the build puts the pair back on the worklist, and the warning stands until a build lands, since asking changes nothing about whether the group's applications can be offered a schema.

Pairs are presented per group, showing which have a schema, which are being built, and which failed, so whether a group's applications can be offered the schema for the version they run or are moving to is answered in one place.
Loading