fix: simplify mulga-docs build step by collapsing coverage paths + hardware

This commit is contained in:
Julian Sommer
2026-09-14 12:41:02 +10:00
parent 613231fd18
commit ba98509c5b
40 changed files with 75 additions and 46 deletions
+1 -1
View File
@@ -181,7 +181,7 @@ test-harness:
# JetStream, with only the daemon-side NATS subjects stubbed.
AWS_MODEL_CONFORMANCE_REPORT ?= $(CURDIR)/.cache/aws-model-conformance-report.txt
AWS_MODEL_CONFORMANCE_MODE ?= fail
AWS_MODEL_OPERATION_COVERAGE_DIR ?= $(CURDIR)/docs/compatibility
AWS_MODEL_OPERATION_COVERAGE_DIR ?= $(CURDIR)/docs/coverage
# -count=1 is load-bearing: the conformance report is written by the test binary,
# so a cached pass skips the run, leaves no report, and the cat below fails the
# target. It also keeps the conformance gate honest — a cached result would mean
+1 -1
View File
@@ -2,7 +2,7 @@
This directory contains the Spinifex documentation. This is available in individual markdown files, and a generated website to aid navigation and browsing documentation at: [https://docs.mulgadc.com](https://docs.mulgadc.com/)
The [AWS API coverage](compatibility/aws-api-coverage/README.md) pages are generated from Spinifex's dispatch tables and the pinned AWS API models, one page per service. Run `make aws-model-coverage` to regenerate them and print a summary. Only `pages.json` and the `intro.md` files under `compatibility/` are hand-written; every `README.md` there is generated and will be overwritten.
The [AWS API coverage](coverage/README.md) pages are generated from Spinifex's dispatch tables and the pinned AWS API models, one page per service. Run `make aws-model-coverage` to regenerate them and print a summary. Only `pages.json` and the `intro.md` files under `coverage/` are hand-written; every `README.md` there is generated and will be overwritten.
## Frontmatter
@@ -21,12 +21,12 @@ Spinifex serves the AWS APIs below. Every page counts the operations in the pinn
| Service | Implemented | Modelled | Coverage |
|---|---:|---:|---:|
| [ACM](/docs/acm-api-coverage) | 9 | 15 | 60.0% |
| [EC2](/docs/ec2-api-coverage) | 121 | 625 | 19.4% |
| [ECR](/docs/ecr-api-coverage) | 19 | 47 | 40.4% |
| [ECS](/docs/ecs-api-coverage) | 31 | 56 | 55.4% |
| [EKS](/docs/eks-api-coverage) | 34 | 56 | 60.7% |
| [ELBv2](/docs/elbv2-api-coverage) | 33 | 46 | 71.7% |
| [IAM](/docs/iam-api-coverage) | 75 | 159 | 47.2% |
| [RDS](/docs/rds-api-coverage) | 26 | 162 | 16.0% |
| [STS](/docs/sts-api-coverage) | 4 | 8 | 50.0% |
| [ACM](/coverage/acm) | 9 | 15 | 60.0% |
| [EC2](/coverage/ec2) | 121 | 625 | 19.4% |
| [ECR](/coverage/ecr) | 19 | 47 | 40.4% |
| [ECS](/coverage/ecs) | 31 | 56 | 55.4% |
| [EKS](/coverage/eks) | 34 | 56 | 60.7% |
| [ELBv2](/coverage/elbv2) | 33 | 46 | 71.7% |
| [IAM](/coverage/iam) | 75 | 159 | 47.2% |
| [RDS](/coverage/rds) | 26 | 162 | 16.0% |
| [STS](/coverage/sts) | 4 | 8 | 50.0% |
@@ -1,7 +1,7 @@
{
"category": "Coverage",
"basePath": "/coverage",
"index": {
"slug": "aws-api-coverage",
"name": "All services",
"title": "AWS API Coverage",
"seoTitle": "Which AWS API Operations Spinifex Implements — Spinifex Docs",
@@ -16,7 +16,7 @@
},
"services": {
"acm": {
"slug": "acm-api-coverage",
"slug": "acm",
"name": "ACM",
"title": "ACM API Coverage",
"seoTitle": "AWS Certificate Manager API Coverage — Spinifex Docs",
@@ -31,7 +31,7 @@
]
},
"ec2": {
"slug": "ec2-api-coverage",
"slug": "ec2",
"name": "EC2",
"title": "EC2 API Coverage",
"seoTitle": "Amazon EC2 API Coverage on Spinifex — Spinifex Docs",
@@ -52,7 +52,7 @@
}
},
"ecr": {
"slug": "ecr-api-coverage",
"slug": "ecr",
"name": "ECR",
"title": "ECR API Coverage",
"seoTitle": "Elastic Container Registry API Coverage — Spinifex Docs",
@@ -78,7 +78,7 @@
}
},
"ecs": {
"slug": "ecs-api-coverage",
"slug": "ecs",
"name": "ECS",
"title": "ECS API Coverage",
"seoTitle": "Elastic Container Service API Coverage — Spinifex Docs",
@@ -98,7 +98,7 @@
]
},
"eks": {
"slug": "eks-api-coverage",
"slug": "eks",
"name": "EKS",
"title": "EKS API Coverage",
"seoTitle": "Elastic Kubernetes Service API Coverage — Spinifex Docs",
@@ -119,7 +119,7 @@
]
},
"elasticloadbalancingv2": {
"slug": "elbv2-api-coverage",
"slug": "elbv2",
"name": "ELBv2",
"title": "ELBv2 API Coverage",
"seoTitle": "Elastic Load Balancing v2 API Coverage — Spinifex Docs",
@@ -138,7 +138,7 @@
]
},
"iam": {
"slug": "iam-api-coverage",
"slug": "iam",
"name": "IAM",
"title": "IAM API Coverage",
"seoTitle": "AWS IAM API Operation Coverage on Spinifex — Spinifex Docs",
@@ -180,7 +180,7 @@
}
},
"rds": {
"slug": "rds-api-coverage",
"slug": "rds",
"name": "RDS",
"title": "RDS API Coverage",
"seoTitle": "Relational Database Service API Coverage — Spinifex Docs",
@@ -223,7 +223,7 @@
}
},
"sts": {
"slug": "sts-api-coverage",
"slug": "sts",
"name": "STS",
"title": "STS API Coverage",
"seoTitle": "AWS Security Token Service API Coverage — Spinifex Docs",

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

@@ -17,7 +17,7 @@ sections:
- troubleshooting
resources:
- title: "RDS API Coverage"
url: "/docs/rds-api-coverage"
url: "/coverage/rds"
- title: "Terraform AWS Provider — aws_db_instance"
url: "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance"
- title: "Spinifex Repository"
@@ -30,7 +30,7 @@ resources:
> The smallest end-to-end RDS example on Spinifex: a VPC with a public client subnet and a private DB subnet, a DB subnet group, a parameter group, a PostgreSQL DB instance, and a client VM with `psql` already pointed at the endpoint.
This workbook is **PostgreSQL throughout** — the engine, the `postgres18` parameter group, port `5432`, the client package and the `~/.pgpass` credential are all PostgreSQL's. Spinifex also serves MariaDB 11.8 under `engine = "mariadb"`; running this workbook against it means changing `engine`, `engine_version`, the parameter-group `family` to `mariadb11.8`, the security-group port to `3306` and the client to `mariadb-client`. See the [RDS API coverage page](/docs/rds-api-coverage) for both engines' pins, families and limits. Note that `mysql` is not an accepted engine and is not an alias for `mariadb`.
This workbook is **PostgreSQL throughout** — the engine, the `postgres18` parameter group, port `5432`, the client package and the `~/.pgpass` credential are all PostgreSQL's. Spinifex also serves MariaDB 11.8 under `engine = "mariadb"`; running this workbook against it means changing `engine`, `engine_version`, the parameter-group `family` to `mariadb11.8`, the security-group port to `3306` and the client to `mariadb-client`. See the [RDS API coverage page](/coverage/rds) for both engines' pins, families and limits. Note that `mysql` is not an accepted engine and is not an alias for `mariadb`.
## Overview
+25 -4
View File
@@ -137,9 +137,12 @@ type PageMetadata struct {
Internal []string `json:"internal,omitempty"`
}
// PageSet is the metadata for the index page and every service page.
// PageSet is the metadata for the index page and every service page. BasePath
// is the site section the pages are published under; the index answers it, so
// the index alone carries no slug.
type PageSet struct {
Category string `json:"category"`
BasePath string `json:"basePath"`
Index PageMetadata `json:"index"`
Services map[Service]PageMetadata `json:"services"`
}
@@ -156,9 +159,12 @@ func ParsePageSet(contents []byte) (PageSet, error) {
if pages.Category == "" {
return PageSet{}, fmt.Errorf("awsmodel: coverage pages: category is empty")
}
if !strings.HasPrefix(pages.BasePath, "/") {
return PageSet{}, fmt.Errorf("awsmodel: coverage pages: basePath %q is not a site path", pages.BasePath)
}
slugs := map[string]bool{}
if err := validatePage("index", pages.Index, slugs); err != nil {
if err := validateIndexPage(pages.Index); err != nil {
return PageSet{}, err
}
// A known service may be left out. S3 is: Predastore serves that surface,
@@ -180,6 +186,14 @@ func ParsePageSet(contents []byte) (PageSet, error) {
return pages, nil
}
// The index answers the section's base path, so it has no slug of its own.
func validateIndexPage(page PageMetadata) error {
if page.Slug != "" {
return fmt.Errorf("awsmodel: coverage page index: slug %q is set, but the index answers the base path", page.Slug)
}
return validateFrontmatter("index", page)
}
func validatePage(owner string, page PageMetadata, slugs map[string]bool) error {
fail := func(format string, args ...any) error {
return fmt.Errorf("awsmodel: coverage page %s: %s", owner, fmt.Sprintf(format, args...))
@@ -191,6 +205,13 @@ func validatePage(owner string, page PageMetadata, slugs map[string]bool) error
return fail("slug %q is used by another page", page.Slug)
}
slugs[page.Slug] = true
return validateFrontmatter(owner, page)
}
func validateFrontmatter(owner string, page PageMetadata) error {
fail := func(format string, args ...any) error {
return fmt.Errorf("awsmodel: coverage page %s: %s", owner, fmt.Sprintf(format, args...))
}
if page.Name == "" || page.Title == "" {
return fail("name and title are required")
}
@@ -316,8 +337,8 @@ func RenderIndexPage(coverages []OperationCoverage, pages PageSet, intro string)
if !ok {
continue
}
fmt.Fprintf(&body, "| [%s](/docs/%s) | %d | %d | %.1f%% |\n",
page.Name, page.Slug, len(coverage.Implemented), len(coverage.Modelled), coverage.ImplementedPercent())
fmt.Fprintf(&body, "| [%s](%s/%s) | %d | %d | %.1f%% |\n",
page.Name, pages.BasePath, page.Slug, len(coverage.Implemented), len(coverage.Modelled), coverage.ImplementedPercent())
}
if intro != "" {
+20 -12
View File
@@ -12,7 +12,7 @@ import (
func testPageSet(t *testing.T) PageSet {
t.Helper()
contents, err := os.ReadFile(filepath.Join("..", "..", "docs", "compatibility", "pages.json"))
contents, err := os.ReadFile(filepath.Join("..", "..", "docs", "coverage", "pages.json"))
if err != nil {
t.Fatal(err)
}
@@ -37,18 +37,26 @@ func TestParsePageSetAcceptsTheCheckedInFile(t *testing.T) {
}
func TestParsePageSetRejectsUnpublishableMetadata(t *testing.T) {
// A well-formed index, so each case below fails only for its own reason.
index := `"index":{"name":"All","title":"All","seoTitle":"` + strings.Repeat("x", 39) +
` — Spinifex Docs","description":"` + strings.Repeat("x", 155) + `","tags":["aws"]}`
head := `{"category":"Coverage","basePath":"/coverage",`
for name, contents := range map[string]string{
"unknown service": `{"category":"Coverage","index":{},"services":{"notaservice":{}}}`,
"missing category": `{"index":{},"services":{}}`,
"unknown field": `{"category":"Coverage","colour":"blue","index":{},"services":{}}`,
"short seoTitle": `{"category":"Coverage","index":{"slug":"aws-api-coverage","name":"All","title":"All",` +
"unknown service": head + index + `,"services":{"notaservice":{}}}`,
"missing category": `{"basePath":"/coverage",` + index + `,"services":{}}`,
"missing basePath": `{"category":"Coverage",` + index + `,"services":{}}`,
"unknown field": head + `"colour":"blue",` + index + `,"services":{}}`,
"index with a slug": head + `"index":{"slug":"aws","name":"All","title":"All","seoTitle":"` +
strings.Repeat("x", 39) + ` — Spinifex Docs","description":"` + strings.Repeat("x", 155) + `","tags":["aws"]}}`,
"short seoTitle": head + `"index":{"name":"All","title":"All",` +
`"seoTitle":"Too short — Spinifex Docs","description":"` + strings.Repeat("x", 155) + `","tags":["aws"]}}`,
"missing suffix": `{"category":"Coverage","index":{"slug":"aws-api-coverage","name":"All","title":"All",` +
"missing suffix": head + `"index":{"name":"All","title":"All",` +
`"seoTitle":"` + strings.Repeat("x", 55) + `","description":"` + strings.Repeat("x", 155) + `","tags":["aws"]}}`,
"short description": `{"category":"Coverage","index":{"slug":"aws-api-coverage","name":"All","title":"All",` +
"short description": head + `"index":{"name":"All","title":"All",` +
`"seoTitle":"` + strings.Repeat("x", 39) + ` — Spinifex Docs","description":"too short","tags":["aws"]}}`,
"bad slug": `{"category":"Coverage","index":{"slug":"AWS Coverage","name":"All","title":"All",` +
`"seoTitle":"` + strings.Repeat("x", 39) + ` — Spinifex Docs","description":"` + strings.Repeat("x", 155) + `","tags":["aws"]}}`,
"bad service slug": head + index + `,"services":{"sts":{"slug":"STS Coverage","name":"STS","title":"STS",` +
`"seoTitle":"` + strings.Repeat("x", 39) + ` — Spinifex Docs","description":"` + strings.Repeat("x", 155) + `","tags":["aws"]}}}`,
} {
t.Run(name, func(t *testing.T) {
if _, err := ParsePageSet([]byte(contents)); err == nil {
@@ -325,13 +333,13 @@ func TestRenderIndexPageLinksEveryPublishedService(t *testing.T) {
for _, service := range Services() {
page, published := pages.Services[service]
if !published {
if strings.Contains(index, "/docs/"+string(service)+"-api-coverage") {
if strings.Contains(index, pages.BasePath+"/"+string(service)) {
t.Errorf("index links unpublished service %q", service)
}
continue
}
if !strings.Contains(index, "["+page.Name+"](/docs/"+page.Slug+")") {
t.Errorf("index does not link %q at /docs/%s", page.Name, page.Slug)
if !strings.Contains(index, "["+page.Name+"]("+pages.BasePath+"/"+page.Slug+")") {
t.Errorf("index does not link %q at %s/%s", page.Name, pages.BasePath, page.Slug)
}
}
}
+6 -6
View File
@@ -10,11 +10,11 @@ import (
)
// stagePages copies the checked-in metadata into a temp directory so a write
// test cannot touch docs/compatibility.
// test cannot touch docs/coverage.
func stagePages(t *testing.T) string {
t.Helper()
dir := t.TempDir()
contents, err := os.ReadFile(filepath.Join("..", "..", "docs", "compatibility", "pages.json"))
contents, err := os.ReadFile(filepath.Join("..", "..", "docs", "coverage", "pages.json"))
if err != nil {
t.Fatal(err)
}
@@ -40,25 +40,25 @@ func TestWritePagesWritesAServicePageAndTheIndex(t *testing.T) {
t.Fatal(err)
}
service, err := os.ReadFile(filepath.Join(dir, "sts-api-coverage", "README.md"))
service, err := os.ReadFile(filepath.Join(dir, "sts", "README.md"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(service), "| `AssumeRole` | "+StatusImplemented+" |") {
t.Errorf("service page is missing its operation table:\n%s", service)
}
index, err := os.ReadFile(filepath.Join(dir, "aws-api-coverage", "README.md"))
index, err := os.ReadFile(filepath.Join(dir, "README.md"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(index), "[STS](/docs/sts-api-coverage)") {
if !strings.Contains(string(index), "[STS](/coverage/sts)") {
t.Errorf("index does not link the service page:\n%s", index)
}
}
func TestWritePagesIncludesAnIntroAndOverwritesTheGeneratedPage(t *testing.T) {
dir := stagePages(t)
pageDir := filepath.Join(dir, "sts-api-coverage")
pageDir := filepath.Join(dir, "sts")
if err := os.MkdirAll(pageDir, 0o755); err != nil {
t.Fatal(err)
}
@@ -29,6 +29,6 @@ Six additional production-referenced values are deliberately not accepted by the
## Operation coverage
Phase 5 generates the public model-versus-dispatch inventory as one page per service under `docs/compatibility/`, plus an index carrying the cross-service summary. Run `make aws-model-coverage` to regenerate the pages and print a terminal summary. A normal `make build` and the integration target both regenerate the pages so handler-map changes cannot silently leave them stale.
Phase 5 generates the public model-versus-dispatch inventory as one page per service under `docs/coverage/`, plus an index carrying the cross-service summary. Run `make aws-model-coverage` to regenerate the pages and print a terminal summary. A normal `make build` and the integration target both regenerate the pages so handler-map changes cannot silently leave them stale.
The inventory counts modelled operations bound to real handlers separately from registered stubs and deliberately unsupported handlers. S3 is marked opaque because Spinifex delegates its REST surface to Predastore rather than using an operation-name dispatch table; no mechanical S3 coverage percentage is claimed.