Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,16 @@ that is not one task (`go tool taskgate`).
- **The look** is spin-stack/brand's: its palette, checked there for contrast and perceptual
distance on real panels and through colour-vision deficiencies, and the organization's mark.
`task reference` copies them from brand at its pin (`BRAND=` to point elsewhere) into
`src/styles/brand-*.css`, `src/assets/` and `public/`, with the app's screenshots, and
`task reference:check` fails on a copy that is not brand's, on screenshots of another release
than installations run, and on one whose test spin no longer has; `src/styles/custom.css` says only how the site's parts use them.
`src/styles/brand-*.css`, `src/assets/` and `public/`, and `task reference:check` fails on a
copy that is not brand's; `src/styles/custom.css` says only how the site's parts use them.
- **The dashboard's screenshots** are the documented spin release's own: spin's `site-shots.yml`
takes them on the KVM fleet (`dashboard/e2e/site.spec.ts`) and puts them on the release as
`site-shots.tar.gz`, which `task sources` unpacks into `.sources/spin-site-shots` with the
sources token. `task reference` copies the one of each test that shoots into
`src/assets/shots` and records the release, the commit and the test in `src/data/shots.json`;
`task reference:check` fails on a picture that is not the release's, and on a record of
another release than the site documents. A release without the asset fails `task sources`,
saying to give it one: `gh workflow run site-shots.yml -R spin-stack/spin -f tag=<tag>`.
IBM Plex is served from the site, and a hero draws a workspace's disk as its layers, worked
step by step (`src/components/Hero.astro`, `Disk.astro`). `task links` fails a page that loads a
script, a stylesheet, a font or an image from another site.
Expand Down
10 changes: 6 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ version: '3'
# reference pages from the sources they describe, linkcheck fails on a link the build did not
# produce. SPIN, TERRAFORM and BRAND are checkouts of spin-stack/spin, spin-stack/spin-terraform-aws
# and spin-stack/brand: by default the ones task sources fetches into .sources/, at the commits
# versions.yaml pins.
# versions.yaml pins. SHOTS is where it unpacks the dashboard's screenshots, which spin's
# release carries as site-shots.tar.gz.

vars:
SPIN: '{{.SPIN | default ".sources/spin"}}'
TERRAFORM: '{{.TERRAFORM | default ".sources/spin-terraform-aws"}}'
BRAND: '{{.BRAND | default ".sources/brand"}}'
SHOTS: '{{.SHOTS | default ".sources/spin-site-shots"}}'
BASE_PATH: '{{.BASE_PATH | default "/website"}}'

tasks:
Expand All @@ -24,7 +26,7 @@ tasks:
- pnpm dev

sources:
desc: Fetch spin, spin-terraform-aws and brand into .sources/, each at the commit versions.yaml pins. spin is private - SOURCES_TOKEN, or git's own credentials, reads it.
desc: Fetch spin, spin-terraform-aws and brand into .sources/, each at the commit versions.yaml pins, and spin's release's screenshots (site-shots.tar.gz) beside them. spin is private - SOURCES_TOKEN, or git's own credentials, reads it.
cmds:
- go run ./tools/sources spin spin-terraform-aws brand

Expand All @@ -47,13 +49,13 @@ tasks:
desc: Write the reference pages (CLI, Terraform, config file, API) and what the site copies from the spin, spin-terraform-aws and brand checkouts.
cmds:
- task: sources
- go run ./tools/refgen -spin {{.SPIN}} -terraform {{.TERRAFORM}} -brand {{.BRAND}}
- go run ./tools/refgen -spin {{.SPIN}} -terraform {{.TERRAFORM}} -brand {{.BRAND}} -shots {{.SHOTS}}

reference:check:
desc: Fail if a reference page is not what the sources say.
cmds:
- task: sources
- go run ./tools/refgen -spin {{.SPIN}} -terraform {{.TERRAFORM}} -brand {{.BRAND}} -check
- go run ./tools/refgen -spin {{.SPIN}} -terraform {{.TERRAFORM}} -brand {{.BRAND}} -shots {{.SHOTS}} -check

reference:public:
desc: Fail if what the site has of spin-terraform-aws and brand is not what they say at the commits versions.yaml pins. Needs no spin checkout, nor any credential.
Expand Down
Binary file modified src/assets/shots/workspace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/shots/workspaces.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/content/docs/use/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ editUrl: false

<!-- Generated by tools/refgen from spin's dashboard/e2e/site.spec.ts. Edit the source, not this file. -->

The dashboard is at `https://app.<domain>`. These screenshots are of spin v20261019.02, taken by spin's dashboard suite on its end-to-end stack - real workspaces on a real runner - each by a test that first checks the screen shows what its title says. Everything here is also in the [CLI](/reference/cli/) and the [API](/reference/api/).
The dashboard is at `https://app.<domain>`. These screenshots are of spin v20261024.01, taken by spin's dashboard suite on its end-to-end stack - real workspaces on a real runner - each by a test that first checks the screen shows what its title says. Everything here is also in the [CLI](/reference/cli/) and the [API](/reference/api/).

## A person's workspaces

Expand Down
4 changes: 2 additions & 2 deletions src/data/shots.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"release": "v20261019.02",
"commit": "617175ffdf2bb568cfcacf97874e2287d0ad8a5b",
"release": "v20261024.01",
"commit": "b3598142dbcd6ef24135066332441a400dd35980",
"shots": [
{
"name": "workspaces",
Expand Down
26 changes: 26 additions & 0 deletions tools/internal/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
Expand Down Expand Up @@ -52,6 +54,30 @@ func (c *Client) File(ctx context.Context, repo, path, ref string) ([]byte, erro
return c.send(ctx, http.MethodGet, "/repos/"+repo+"/contents/"+path+"?ref="+ref, nil, "application/vnd.github.raw")
}

// ErrNoAsset is a release that has no asset of the name asked for.
var ErrNoAsset = errors.New("no such asset")

// ReleaseAsset is the asset called name of repo's release tag, as its bytes, read through the
// API as the token can read a private repository's. The API redirects to storage on another
// host, and Go's client sends the Authorization header no further than GitHub.
func (c *Client) ReleaseAsset(ctx context.Context, repo, tag, name string) ([]byte, error) {
var rel struct {
Assets []struct {
ID int64 `json:"id"`
Name string `json:"name"`
} `json:"assets"`
}
if err := c.Get(ctx, "/repos/"+repo+"/releases/tags/"+url.PathEscape(tag), &rel); err != nil {
return nil, err
}
for _, a := range rel.Assets {
if a.Name == name {
return c.send(ctx, http.MethodGet, fmt.Sprintf("/repos/%s/releases/assets/%d", repo, a.ID), nil, "application/octet-stream")
}
}
return nil, fmt.Errorf("%s %s has no %s: %w", repo, tag, name, ErrNoAsset)
}

func (c *Client) send(ctx context.Context, method, path string, body any, accept string) ([]byte, error) {
var in io.Reader
if body != nil {
Expand Down
34 changes: 0 additions & 34 deletions tools/refgen/brand.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ var brandFiles = []struct{ from, to string }{
{"logos/mark-512.png", "public/mark.png"},
{"logos/favicon-64.png", "public/favicon.png"},
{"logos/apple-touch-icon.png", "public/apple-touch-icon.png"},
{"shots/shots.json", "src/data/shots.json"},
}

func brandCopies(brand string, check bool) ([]string, error) {
Expand All @@ -35,38 +34,5 @@ func brandCopies(brand string, check bool) ([]string, error) {
stale = append(stale, f.to)
}
}
// The app's screenshots, every one brand keeps, and none it does not.
pngs, err := filepath.Glob(filepath.Join(brand, "shots", "*.png"))
if err != nil {
return nil, err
}
keep := map[string]bool{}
for _, p := range pngs {
raw, err := os.ReadFile(p)
if err != nil {
return nil, err
}
to := filepath.Join(shotsDir, filepath.Base(p))
keep[to] = true
differs, err := syncFile(to, raw, check)
if err != nil {
return nil, err
}
if differs {
stale = append(stale, to)
}
}
have, _ := filepath.Glob(filepath.Join(shotsDir, "*.png"))
for _, h := range have {
if keep[h] {
continue
}
stale = append(stale, h)
if !check {
if err := os.Remove(h); err != nil {
return nil, err
}
}
}
return stale, nil
}
8 changes: 7 additions & 1 deletion tools/refgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func run() error {
spin := flag.String("spin", "../spin", "a checkout of spin-stack/spin")
tf := flag.String("terraform", "../spin-terraform-aws", "a checkout of spin-stack/spin-terraform-aws")
brand := flag.String("brand", "../brand", "a checkout of spin-stack/brand")
shotsFrom := flag.String("shots", ".sources/spin-site-shots", "where task sources unpacked spin's release's site-shots.tar.gz")
only := flag.String("only", "spin,terraform,brand", "the sources to generate from: all three, or terraform,brand, which needs no spin checkout and keeps what the site has of spin")
out := flag.String("out", "src/content/docs/reference", "where the pages go")
docs := flag.String("docs", "src/content/docs", "the hand-written pages, whose commands are checked")
Expand Down Expand Up @@ -65,6 +66,11 @@ func run() error {
}
r, stale, bad = s.ref, s.stale, s.bad
from["spin"] = checkout{*spin, r.Spin.Commit}
shot, err := releaseShots(*spin, *shotsFrom, r.Spin, *check)
if err != nil {
return err
}
stale = append(stale, shot...)
dirs = append(dirs, "status", "cli", "configuration", "api", "security")
if err := spinConstants(&c, *spin); err != nil {
return err
Expand Down Expand Up @@ -146,7 +152,7 @@ func run() error {
return err
}
stale = append(stale, branded...)
shotBad, shotStale, err := checkShots(*spin, r.Spin.Installs, *check)
shotBad, shotStale, err := checkShots(r.Spin, *check)
if err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions tools/refgen/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ var pinned = regexp.MustCompile("\\?ref=([A-Za-z0-9._/-]*)(<[^>\\s`\"]*>?)?|\\bv
func pinReleases(docs, release, module string, check bool) ([]string, error) {
var bad []string
err := handPages(docs, func(path string, raw []byte) error {
// The dashboard's page names the release its screenshots are of: the one the reference
// documents, which installations may not run yet.
if rel, _ := filepath.Rel(docs, path); rel == shotsPageDoc {
return nil
}
lines := bytes.Split(raw, []byte("\n"))
for i, line := range lines {
lines[i] = pinned.ReplaceAllFunc(line, func(m []byte) []byte {
Expand Down
112 changes: 87 additions & 25 deletions tools/refgen/shots.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import (

// The dashboard's screenshots are taken by spin's own dashboard suite, of its e2e stack - the
// user's real workspaces on a real runner - by dashboard/e2e/site.spec.ts, each test checking
// the screen shows what its title says before it shoots, and kept in spin-stack/brand with the
// release of the tree that took them, beside the palette that draws them. refgen copies them
// from brand (brand.go), writes the dashboard's page from them, and fails when they are of
// another release than the site documents, or a test that took one is gone from spin.
// the screen shows what its title says before it shoots. spin's site-shots.yml takes them of
// every release on the KVM fleet and puts them on it as site-shots.tar.gz, which task sources
// unpacks beside the spin checkout. refgen copies them from there with the release, the commit
// and each test's title (releaseShots), writes the dashboard's page from them, and fails when
// they are of another release than the site documents (checkShots). So a release's pictures
// arrive with the pin that moves to it, and nobody runs a stack to take them.
const (
shotsSpec = "dashboard/e2e/site.spec.ts"
shotsDir = "src/assets/shots"
shotsData = "src/data/shots.json"
shotsPage = "src/content/docs/use/dashboard.md"
// shotsPageDoc is the dashboard's page under the docs, written by refgen: it names the
// release its screenshots are of, which pinReleases leaves as it is.
shotsPageDoc = "use/dashboard.md"
shotsPage = "src/content/docs/" + shotsPageDoc
)

type shots struct {
Expand Down Expand Up @@ -51,10 +56,81 @@ func specShots(spin string) ([]shot, error) {
return out, nil
}

// checkShots holds the screenshots to the release an installation runs and to the spec, and
// writes the dashboard's page from them. With no spin checkout (spin is ""), the spec is not
// read: the release and the pictures are brand's, and checked all the same.
func checkShots(spin, installs string, check bool) (bad, stale []string, err error) {
// shotsStamp is the file task sources writes in the screenshots' directory once they are all
// there: "<tag> <pin>\n" of the release they were unpacked from.
const shotsStamp = ".release"

// releaseShots copies, from the directory task sources unpacked spin's release's
// site-shots.tar.gz into, the screenshot of every test of the spec that shoots one, and no
// other, and records them in shots.json with the release and commit spin is at. It fails when
// the directory is of another release, or spin is at none: a screenshot is the documented
// release's or the site has none.
func releaseShots(spin, from string, at spinRef, check bool) (stale []string, err error) {
if at.Tag == "" {
return nil, fmt.Errorf("spin %s is no release, and the dashboard's screenshots are a release's: pin spin at one", at.Commit)
}
raw, err := os.ReadFile(filepath.Join(from, shotsStamp))
if err != nil {
return nil, fmt.Errorf("%s has no screenshots of a release (%w): run task sources", from, err)
}
if got, want := string(raw), at.Tag+" "+at.Commit+"\n"; got != want {
return nil, fmt.Errorf("%s holds the screenshots of %q, and spin is at %q: run task sources", from, strings.TrimSpace(got), strings.TrimSpace(want))
}
inSpec, err := specShots(spin)
if err != nil {
return nil, err
}
keep := map[string]bool{}
for _, t := range inSpec {
png, err := os.ReadFile(filepath.Join(from, t.Name+".png"))
if err != nil {
return nil, fmt.Errorf("spin %s's site-shots.tar.gz has no %s.png, which %q shoots: retake them with `gh workflow run site-shots.yml -R spin-stack/spin -f tag=%s`", at.Tag, t.Name, t.Test, at.Tag)
}
to := filepath.Join(shotsDir, t.Name+".png")
keep[to] = true
differs, err := syncFile(to, png, check)
if err != nil {
return nil, err
}
if differs {
stale = append(stale, to)
}
}
have, err := filepath.Glob(filepath.Join(shotsDir, "*.png"))
if err != nil {
return nil, err
}
for _, h := range have {
if keep[h] {
continue
}
stale = append(stale, h)
if !check {
if err := os.Remove(h); err != nil {
return nil, err
}
}
}
b, err := json.MarshalIndent(shots{Release: at.Tag, Commit: at.Commit, Shots: inSpec}, "", "\t")
if err != nil {
return nil, err
}
differs, err := syncFile(shotsData, append(b, '\n'), check)
if err != nil {
return nil, err
}
if differs {
stale = append(stale, shotsData)
}
return stale, nil
}

// checkShots holds the screenshots shots.json records to the spin release and commit the
// reference documents, and writes the dashboard's page from them. With a spin checkout,
// releaseShots has just written them from the release; without one, what holds the pictures
// to the release is that run's, in Gate with the token, and this holds the record to the
// reference all the same.
func checkShots(at spinRef, check bool) (bad, stale []string, err error) {
raw, err := os.ReadFile(shotsData)
if err != nil {
return nil, nil, err
Expand All @@ -63,24 +139,10 @@ func checkShots(spin, installs string, check bool) (bad, stale []string, err err
if err := json.Unmarshal(raw, &s); err != nil {
return nil, nil, fmt.Errorf("%s: %w", shotsData, err)
}
if s.Release != installs {
bad = append(bad, fmt.Sprintf("%s: the dashboard's screenshots are of %s and installations run %s: run `task screenshots`", shotsData, s.Release, installs))
}
var tests map[string]string
if spin != "" {
inSpec, err := specShots(spin)
if err != nil {
return nil, nil, err
}
tests = map[string]string{}
for _, t := range inSpec {
tests[t.Name] = t.Test
}
if at.Tag == "" || s.Release != at.Tag || s.Commit != at.Commit {
bad = append(bad, fmt.Sprintf("%s: the dashboard's screenshots are of spin %s at %s, and the site documents spin %q at %s: run `task reference`", shotsData, s.Release, s.Commit, at.Tag, at.Commit))
}
for _, t := range s.Shots {
if tests != nil && tests[t.Name] != t.Test {
bad = append(bad, fmt.Sprintf("%s: %q is no test of spin's %s that shoots %s: run `task screenshots`", shotsData, t.Test, shotsSpec, t.Name))
}
if _, err := os.Stat(filepath.Join(shotsDir, t.Name+".png")); err != nil {
bad = append(bad, fmt.Sprintf("%s: no %s.png", shotsDir, t.Name))
}
Expand Down
Loading
Loading