oficina / docs Public

Reuse parsed credits and fingerprint shared stylesheet

Paul Campbell pushed by paul@keymail.dev 7c6d293bde46548ef44f6a3587dfce84eea8f626 parent f361ca9
10 files changed, +32 −23
  • .amadan/ci.d/10-gate +1 −0
  • go.mod +2 −2
  • go.sum +2 −6
  • internal/docs/app.go +0 −1
  • internal/docs/credits.go +7 −9
  • internal/docs/credits_test.go +13 −1
  • internal/docs/render.go +2 −0
  • internal/docs/views/credits.html +1 −1
  • scripts/build-release.sh +1 −0
  • scripts/credits.py +3 −3

CI passed — run details

diff --git a/.amadan/ci.d/10-gate b/.amadan/ci.d/10-gate
index 11b8464..decb9f3 100755
--- a/.amadan/ci.d/10-gate
+++ b/.amadan/ci.d/10-gate
@@ -3,6 +3,7 @@
# squash-merge, in one script so CI and what you run locally are one
# definition rather than two that drift.
set -euo pipefail
+export GOTOOLCHAIN=go1.26.5
# rustup installs to ~/.cargo/bin, which is not on the PATH of a systemd
# user unit and is not on the PATH of a login shell when rustup was run
diff --git a/go.mod b/go.mod
index 437bfbb..f637ff5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/carlosframework/docs
go 1.26.5
require (
- amadan.net/oficina/estilo.git v0.0.0-20260911162556-f5fa1128b794
+ amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489
amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf
amadan.net/rastrillo/rastrillo v0.26.1-0.20260908132728-d9c28d708556
@@ -11,6 +11,7 @@ require (
github.com/go-chi/chi/v5 v5.3.2
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2
github.com/google/uuid v1.6.0
+ github.com/keymaildev/signin v0.1.1
github.com/tetratelabs/wazero v1.12.0
golang.org/x/text v0.20.0
gorm.io/gorm v1.31.2
@@ -21,7 +22,6 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
- github.com/keymaildev/signin v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
diff --git a/go.sum b/go.sum
index ff43fb6..4e349a1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,9 @@
-amadan.net/oficina/estilo.git v0.0.0-20260908001419-672a447da335 h1:YogZKYINrPNXRfPedXxOIDbGCMY4ff7yAdohYphEWMg=
-amadan.net/oficina/estilo.git v0.0.0-20260908001419-672a447da335/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
-amadan.net/oficina/estilo.git v0.0.0-20260911162556-f5fa1128b794 h1:2oBeIneg/f+m3/n8qLsQGlpEhdnAfW3kOI80yRv60pw=
-amadan.net/oficina/estilo.git v0.0.0-20260911162556-f5fa1128b794/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
+amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d h1:3HsB96tGs+P0tGf2VzqpqA8yqYBhJ+apBh2txAoz9qI=
+amadan.net/oficina/estilo.git v0.0.0-20260911182719-0bcb9287988d/go.mod h1:0R45/vCUAT+kpS7S6K/ZxBdE8f7YxFN4HT3HyDJ3nPY=
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM=
amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489/go.mod h1:YISpzFJ+iMgrM0rUL032OSzsAQbP4/nozvXLCCOcn1k=
amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf h1:SRKjCUgtk42WwR7VSn+V0eLNk20mfiY8dQmzdw13Sbo=
amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf/go.mod h1:gqshuXyDtFZUCYbhuZyV3Epc51pUcsLbpqAqiA5UiXg=
-amadan.net/rastrillo/rastrillo v0.26.1-0.20260908075707-29e4491631bd h1:jlFSt95M1ontOg+VePU+kIJpChtWqDQAbnmlxqCnbgc=
-amadan.net/rastrillo/rastrillo v0.26.1-0.20260908075707-29e4491631bd/go.mod h1:RpyHVPD0udcSfHJroY0KZE/FUjgw6Tvfo8SJm2qeZGE=
amadan.net/rastrillo/rastrillo v0.26.1-0.20260908132728-d9c28d708556 h1:/vHMeq9T+PZmUACixGkg32YEjyM8s31+bfAqqrMha0A=
amadan.net/rastrillo/rastrillo v0.26.1-0.20260908132728-d9c28d708556/go.mod h1:RpyHVPD0udcSfHJroY0KZE/FUjgw6Tvfo8SJm2qeZGE=
github.com/carlosframework/rastrillo v0.23.0 h1:tKAAlwPt4Pg/rkqk7o6jvV12bbZ0r34oEVpuOlKfX7w=
diff --git a/internal/docs/app.go b/internal/docs/app.go
index 7328502..422b895 100644
--- a/internal/docs/app.go
+++ b/internal/docs/app.go
@@ -489,7 +489,6 @@ func New(cfg Config) (*App, error) {
// "/signin" and "/signout".
r.Use(homeCSRF(cfg.Origin, home))
r.Get("/help/open-source", h.credits)
- r.Get("/static/credits.css", serveCreditsCSS)
// Unauthenticated: the sign-in page and its four handlers, and the
// static assets that page (and the shell around it) needs to
// render at all.
diff --git a/internal/docs/credits.go b/internal/docs/credits.go
index 95673a2..c79c602 100644
--- a/internal/docs/credits.go
+++ b/internal/docs/credits.go
@@ -23,21 +23,19 @@ type creditsPage struct {
Credits credits.PageData
}
-func (h *handlers) credits(w http.ResponseWriter, r *http.Request) {
+var embeddedCredits = func() creditInventory {
var inventory creditInventory
if err := json.Unmarshal(creditNotices, &inventory); err != nil {
- http.Error(w, rastrillo.T(r, "docs.error.generic"), h.internalFailureStatus())
- return
+ panic(err)
}
+ return inventory
+}()
+
+func (h *handlers) credits(w http.ResponseWriter, r *http.Request) {
title := rastrillo.T(r, "docs.credits.title")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Referrer-Policy", "no-referrer")
- if err := h.render.page(w, r, "credits", creditsPage{Title: title, Brand: rastrillo.T(r, "docs.list.title"), Credits: credits.PageData{Title: title, Components: inventory.Components}}); err != nil {
+ if err := h.render.page(w, r, "credits", creditsPage{Title: title, Brand: rastrillo.T(r, "docs.list.title"), Credits: credits.PageData{Title: title, MissingNoticeLabel: rastrillo.T(r, "docs.credits.missing"), Components: embeddedCredits.Components}}); err != nil {
http.Error(w, rastrillo.T(r, "docs.error.generic"), h.internalFailureStatus())
}
}
-
-func serveCreditsCSS(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/css; charset=utf-8")
- w.Write(credits.CSS())
-}
diff --git a/internal/docs/credits_test.go b/internal/docs/credits_test.go
index 114992a..d4742ea 100644
--- a/internal/docs/credits_test.go
+++ b/internal/docs/credits_test.go
@@ -29,16 +29,28 @@ func TestCreditsArePublicAndContainDependencyNotices(t *testing.T) {
if response.Header().Get("Cache-Control") != "no-store" || response.Header().Get("Referrer-Policy") != "no-referrer" {
t.Fatal("credits shell may retain account context")
}
- for _, wanted := range []string{"estilo-credits", "Open source", "Lucide and Feather", "gorm.io/gorm", "gormlite/LICENSE", "prosemirror-view", "yrs", "Rust standard library", "Liberation Fonts", "/static/credits.css"} {
+ for _, wanted := range []string{"estilo-credits", "Open source", "Lucide and Feather", "gorm.io/gorm", "gormlite/LICENSE", "prosemirror-view", "yrs", "Rust standard library", "Liberation Fonts", "No notice file is included in this release."} {
if !strings.Contains(response.Body.String(), wanted) {
t.Fatal("missing credit", wanted)
}
}
+ match := regexp.MustCompile(`href="(/static/credits\.[a-z0-9]+\.css)"`).FindStringSubmatch(response.Body.String())
+ if len(match) != 2 {
+ t.Fatal("credits stylesheet is not fingerprinted")
+ }
+ fingerprinted := httptest.NewRecorder()
+ app.ServeHTTP(fingerprinted, httptest.NewRequest(http.MethodGet, "http://example.com"+match[1], nil))
+ if fingerprinted.Code != 200 || !strings.Contains(fingerprinted.Header().Get("Cache-Control"), "immutable") {
+ t.Fatal("fingerprinted credits style not immutable")
+ }
style := httptest.NewRecorder()
app.ServeHTTP(style, httptest.NewRequest(http.MethodGet, "http://example.com"+"/static/credits.css", nil))
if style.Code != http.StatusOK || !strings.HasPrefix(style.Header().Get("Content-Type"), "text/css") || !strings.Contains(style.Body.String(), "[estilo-credits]") {
t.Fatal("shared credit styles not served")
}
+ if !strings.Contains(style.Header().Get("Cache-Control"), "no-cache") {
+ t.Fatal("bare credits CSS can be stale")
+ }
if output := os.Getenv("DOCS_CREDITS_EVIDENCE_DIR"); output != "" {
if err := os.MkdirAll(output, 0700); err != nil {
t.Fatal(err)
diff --git a/internal/docs/render.go b/internal/docs/render.go
index e05e23d..52be5a7 100644
--- a/internal/docs/render.go
+++ b/internal/docs/render.go
@@ -62,6 +62,7 @@ const (
var Catalog = rastrillo.Catalog{
"docs.credits.title": "Open source",
"docs.credits.help": "Help",
+ "docs.credits.missing": "No notice file is included in this release.",
"docs.home.signedout_title": "Signed out of Docs",
"docs.home.signedout_body": "You can sign in again with Home.",
"docs.home.signedout_action": "Sign in with Home",
@@ -606,6 +607,7 @@ func vendoredStaticFS() (fs.FS, error) {
return nil, fmt.Errorf("docs: rastrillo ui: theme %q not shipped", dayTheme)
}
return fstest.MapFS{
+ "static/credits.css": {Data: credits.CSS()},
"static/estilo-save-status.css": {Data: savestatus.CSS()},
"static/estilo-save-status.mjs": {Data: savestatus.JS()},
"static/tokens.css": {Data: ui.TokensCSS()},
diff --git a/internal/docs/views/credits.html b/internal/docs/views/credits.html
index fdfb4a5..efd5192 100644
--- a/internal/docs/views/credits.html
+++ b/internal/docs/views/credits.html
@@ -1,4 +1,4 @@
{{define "title"}}{{.Title}}{{end}}
{{define "brand"}}<a rst-shell-brand href="/">{{.Brand}}</a>{{end}}
-{{define "head"}}{{template "docs/head" .}}<link rel="stylesheet" href="/static/credits.css">{{end}}
+{{define "head"}}{{template "docs/head" .}}<link rel="stylesheet" href="{{asset "static/credits.css"}}">{{end}}
{{define "content"}}{{template "estilo/credits" .Credits}}{{end}}
diff --git a/scripts/build-release.sh b/scripts/build-release.sh
index 1869b7e..c2c9475 100755
--- a/scripts/build-release.sh
+++ b/scripts/build-release.sh
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
+export GOTOOLCHAIN=go1.26.5
export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}amadan.net/oficina/estilo.git"
test "$#" -ge 1
diff --git a/scripts/credits.py b/scripts/credits.py
index 82e4f99..967b183 100755
--- a/scripts/credits.py
+++ b/scripts/credits.py
@@ -11,7 +11,7 @@ from html.parser import HTMLParser
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / 'internal/docs/credits'
-ENV = os.environ | {'GOPRIVATE': 'amadan.net/oficina/estilo.git'}
+ENV = os.environ | {'GOTOOLCHAIN': 'go1.26.5', 'GOPRIVATE': 'amadan.net/oficina/estilo.git'}
EXCLUDE = ','.join(['amadan.net/oficina/estilo.git', 'amadan.net/oficina/home/handoff', 'amadan.net/oficina/home/roster'])
@@ -37,7 +37,7 @@ def notices(directory):
if path.is_file() and any(lower == word or lower.startswith(word + '.') or lower.startswith(word + '-') for word in ['license', 'licence', 'copying', 'notice']):
if path.is_symlink() or path.stat().st_size > 2 * 1024 * 1024:
raise ValueError('notice must be a bounded regular file')
- text = path.read_text()
+ text = path.read_bytes().decode('utf-8')
if not text.strip():
raise ValueError('empty notice')
result.append({'Name': path.name, 'Text': text})
@@ -122,7 +122,7 @@ console.log(JSON.stringify([...inputs]));
provenance = json.loads((OUT / 'upstream/yrs-source.json').read_text())
if digest(OUT / 'upstream/yrs-LICENSE') != provenance['sha256']:
raise ValueError('yrs original notice checksum changed')
- texts = [{'Name': 'LICENSE', 'Text': (OUT / 'upstream/yrs-LICENSE').read_text()}]
+ texts = [{'Name': 'LICENSE', 'Text': (OUT / 'upstream/yrs-LICENSE').read_bytes().decode('utf-8')}]
components.append({'Name': package['name'], 'Version': package['version'], 'SourceURL': 'https://crates.io/crates/' + package['name'] + '/' + package['version'], 'Notices': texts})
sysroot = Path(run(['rustc', '--print', 'sysroot']).decode().strip())
rust_version = run(['rustc', '--version']).decode().split()[1]