Skip to content

Commit 337b406

Browse files
alukachclaude
andcommitted
chore: keep the repository's helpers under docs/ and .github/
demo/, docs/ and preview/ were three top-level folders of helpers. The demo recorder only produces docs/demos/, so it moves beside it as docs/recorder/, with its one kept script, guide.py, at its top. The preview installer is only used by the Preview build workflow, so it moves to .github/preview/. docs/site/ and docs/demos/ stay put: the README links the recordings there, and republishing a release runs docs/site/build.py from that release's tag. The Docs workflow skips docs/recorder/, so changing only the recorder builds no docs preview, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 26e424b commit 337b406

14 files changed

Lines changed: 44 additions & 44 deletions

File tree

‎.github/workflows/demo-stub.yml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
name: Demo stub
22

3-
# Checks demo/stub.py's answers against the API's OpenAPI spec on staging, so
4-
# the stub can't drift from the API unnoticed. Only when demo/ changes: demos
3+
# Checks docs/recorder/stub.py's answers against the API's OpenAPI spec on staging, so
4+
# the stub can't drift from the API unnoticed. Only when docs/recorder/ changes: demos
55
# are recorded in PRs that touch it, and that's when drift would show.
66
on:
77
pull_request:
8-
paths: ["demo/**", ".github/workflows/demo-stub.yml"]
8+
paths: ["docs/recorder/**", ".github/workflows/demo-stub.yml"]
99
workflow_dispatch:
1010

1111
jobs:
@@ -18,4 +18,4 @@ jobs:
1818
with:
1919
python-version: "3.12"
2020
- run: pip install jsonschema
21-
- run: python demo/check_stub.py
21+
- run: python docs/recorder/check_stub.py

‎.github/workflows/docs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
types: [published]
1212
pull_request:
1313
types: [opened, reopened, synchronize, closed]
14-
paths: ["README.md", "docs/**", ".github/workflows/docs*.yml"]
14+
paths: ["README.md", "docs/**", "!docs/recorder/**", ".github/workflows/docs*.yml"]
1515
workflow_dispatch:
1616
inputs:
1717
tag:

‎.github/workflows/preview-build.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ on:
2626
options: [staging, production]
2727
default: staging
2828
pull_request:
29-
paths: [".github/workflows/preview-build.yml", "preview/install.sh"]
29+
paths: [".github/workflows/preview-build.yml", ".github/preview/install.sh"]
3030

3131
permissions:
3232
contents: read
@@ -120,7 +120,7 @@ jobs:
120120
# a branch or tag from before it has none, and everyone downloads it.
121121
- uses: actions/checkout@v4
122122
with:
123-
sparse-checkout: preview/install.sh
123+
sparse-checkout: .github/preview/install.sh
124124
sparse-checkout-cone-mode: false
125125
- uses: actions/download-artifact@v4
126126
with:
@@ -129,7 +129,7 @@ jobs:
129129
- name: Replace the preview's pre-release
130130
run: |
131131
# This preview's installer: the same script, with its name set.
132-
sed "s/^name=staging\$/name=$NAME/" preview/install.sh > dist/install.sh
132+
sed "s/^name=staging\$/name=$NAME/" .github/preview/install.sh > dist/install.sh
133133
grep -q "^name=$NAME\$" dist/install.sh
134134
install="curl -LsSf https://github.com/$GITHUB_REPOSITORY/releases/download/$NAME/install.sh | sh"
135135
cat > notes.md <<EOF

‎.gitignore‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
.env.local
66
!.env.staging
77

8-
# Recorded demos and the scripts for them, written per PR (demo/README.md)
9-
demo/casts/
10-
demo/scripts/
8+
# Recorded demos and the scripts for them, written per PR (docs/recorder/README.md)
9+
docs/recorder/casts/
10+
docs/recorder/scripts/
1111
__pycache__/

‎AGENTS.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ How this CLI behaves, so every command behaves the same way. Each rule names the
1010
- **Behaviour:** prompts, keys, output formats and defaults.
1111
- **Files and environment variables** it reads or writes.
1212
- **The docs site is the README.** `docs/site/build.py` turns it into the site at <https://source-cooperative.github.io/source-coop-cli/>, one page for each `##` section, so a new `##` heading is a new page. A `<!-- group: NAME -->` line (GitHub doesn't show it) starts a group of sections, listed under NAME in the site's sidebar and the README's contents: put a new section in the group a reader would look in, and add it to the contents list. The team's own workflows, preview builds and setup for whoever runs the sign-in server, are in CONTRIBUTING.md, not the README. Each published release publishes it as that version and, for the newest stable release, as `latest/`. Build it locally with `uv run --with markdown-it-py python docs/site/build.py --out target/site`.
13-
- **Demos:** the README embeds recordings from `docs/demos/`, made by `demo/demos/guide.py`. When a change alters what one shows, update its script and re-record it (`demo/README.md` says how).
13+
- **Demos:** the README embeds recordings from `docs/demos/`, made by `docs/recorder/guide.py`. When a change alters what one shows, update its script and re-record it (`docs/recorder/README.md` says how).
1414

1515
Before committing, read the README sections your change touches and fix anything now untrue, including text written before your change. The README isn't a changelog: describe how the CLI works now, not what changed. `every_command_is_in_the_readme` in `src/main.rs` fails if a command is missing from it, but it can't tell whether the description is right. That part is on you.
1616

@@ -127,11 +127,11 @@ A reviewer should see what a change is like to use without building it. A PR tha
127127

128128
### Demos
129129

130-
Record with `demo/`: `demo/README.md` says how to write a demo, record it and get it into the PR.
130+
Record with `docs/recorder/`: `docs/recorder/README.md` says how to write a demo, record it and get it into the PR.
131131

132-
- **Write the demo script for your change:** one per PR, in `demo/scripts/` (git ignores it). You know what your change does, so script that. The only kept scripts are the README's, in `demo/demos/`: re-record those your change affects, but don't add PR demos to them. Grow `demo/stub.py` and `demo/fixtures.json` with what your demos need, and commit those.
132+
- **Write the demo script for your change:** one per PR, in `docs/recorder/scripts/` (git ignores it). You know what your change does, so script that. The only kept script is the README's, `docs/recorder/guide.py`: re-record the demos your change affects, but don't add PR demos to it. Grow `docs/recorder/stub.py` and `docs/recorder/fixtures.json` with what your demos need, and commit those.
133133
- **One demo per command or flow,** each with a bold title. Show the interactive paths: prompts, ← back, the review, and `-i` screens with their keys. Show the failure paths too: a refusal, a typed confirmation typed wrong, and an exit code (`; echo "exit $?"`). Pipe `--json` through `jq -c` to keep it short.
134-
- **Attach each as an SVG.** Paste what `record.py` prints into the description and create or edit the PR with `--attach` for each SVG: `gh` uploads them and points the markdown at the uploads. `demo/README.md` has the command. Use the SVGs `record.py` writes, not svg-term's own: GitHub's CSP stops those animating when opened directly. Don't commit them, and don't host them elsewhere. The README's demos, in `docs/demos/`, are the only SVGs committed.
134+
- **Attach each as an SVG.** Paste what `record.py` prints into the description and create or edit the PR with `--attach` for each SVG: `gh` uploads them and points the markdown at the uploads. `docs/recorder/README.md` has the command. Use the SVGs `record.py` writes, not svg-term's own: GitHub's CSP stops those animating when opened directly. Don't commit them, and don't host them elsewhere. The README's demos, in `docs/demos/`, are the only SVGs committed.
135135
- **Say in the PR that the demos run against the stub.** `record.py` draws the stub's URLs as production's (`source.coop`, `data.source.coop`) and the throwaway `HOME` as `~`, so the records in them could be mistaken for real ones.
136136
- **Attachments are public:** this repository is public, and so are its PRs. Record only the stub's made-up data, never a real account or token.
137137
- **Recording is a review pass.** An awkward message, or a screen titled wrongly, is a bug: fix it in the PR and re-record. Check what each recording shows, not only that it finished.
@@ -145,10 +145,10 @@ A PR that changes `README.md` or `docs/` gets a preview of the docs site at `htt
145145

146146
### The stub server
147147

148-
`demo/stub.py` stands in for the source.coop API when recording, serving the made-up records in `demo/fixtures.json`. `demo/README.md` explains how it runs. It isn't for tests, which mock the API with `wiremock`. When a demo needs more of it:
148+
`docs/recorder/stub.py` stands in for the source.coop API when recording, serving the made-up records in `docs/recorder/fixtures.json`. `docs/recorder/README.md` explains how it runs. It isn't for tests, which mock the API with `wiremock`. When a demo needs more of it:
149149

150150
- **Never point a demo at a real environment instead.** Every session runs with `SOURCE_TOKEN=demo`, a throwaway `HOME` and `SOURCE_PROXY_URL` set to the stub, which gives it an environment of its own, so it can't read or remove a real login. Keep it that way, and leave sign-in out of demos: a saved session still lands in the OS keyring, under the stub's environment.
151151
- **Keep fixtures obviously fake,** under `demo-user` and its organization `demo-org`. Add the records a new demo needs there.
152152
- **Add endpoints as demos need them.** Anything else gets a 404 saying `The demo stub has no METHOD PATH`, so a missing endpoint is plain from the recording. Add a branch to `handle()` for each new path.
153-
- **Match the API's shapes, not the CLI's guesses.** Take paths, bodies, status codes and response shapes from the OpenAPI spec (`https://staging.source.coop/api/openapi`) or source.coop's `src/app/api/v1` routes. `demo/check_stub.py` checks the stub's answers against that spec, and CI runs it on PRs that change `demo/`: give each new route a request in its `requests()`, so it's checked too. Listings come in pages (`{"items", "next_cursor"}`): products, data connections, memberships, service accounts, trusts and API keys. A stub that answers in a shape the API doesn't use makes the demo show behaviour real users never see, and hides the CLI bugs a demo should catch.
153+
- **Match the API's shapes, not the CLI's guesses.** Take paths, bodies, status codes and response shapes from the OpenAPI spec (`https://staging.source.coop/api/openapi`) or source.coop's `src/app/api/v1` routes. `docs/recorder/check_stub.py` checks the stub's answers against that spec, and CI runs it on PRs that change `docs/recorder/`: give each new route a request in its `requests()`, so it's checked too. Listings come in pages (`{"items", "next_cursor"}`): products, data connections, memberships, service accounts, trusts and API keys. A stub that answers in a shape the API doesn't use makes the demo show behaviour real users never see, and hides the CLI bugs a demo should catch.
154154
- **It checks almost nothing.** It answers errors as the API does, as RFC 9457 problems (`application/problem+json`, made by `error()`, with field errors as JSON Pointers), and only where a demo shows a refusal, such as a 409 for an ID that's taken or a key revoked twice. Add those per demo rather than reimplementing the API's rules.

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Running it again updates the preview.
3232
- **It's for testing, not a release.** A preview's pre-release is never marked latest, so the release installers and Homebrew keep installing the real release, and neither the *Release* nor the *Docs* workflow runs for it.
3333
- **Remove a preview you're done with:** `gh release delete NAME --cleanup-tag`, and `rm ~/.local/bin/source-coop-NAME` to uninstall it.
3434

35-
On a PR that changes the workflow or `preview/install.sh`, *Preview build* only builds the default preview, without publishing.
35+
On a PR that changes the workflow or `.github/preview/install.sh`, *Preview build* only builds the default preview, without publishing.
3636

3737
## OIDC provider setup
3838

demo/README.md renamed to docs/recorder/README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Recording demos for PR descriptions
22

3-
`demo/` records terminal demos of the CLI as [asciicasts](https://docs.asciinema.org/manual/asciicast/v2/), so a PR can show what a change looks like to use. Each recording runs against a stub of the source.coop API filled with made-up records, so nothing touches a real account or a real login.
3+
`docs/recorder/` records terminal demos of the CLI as [asciicasts](https://docs.asciinema.org/manual/asciicast/v2/), so a PR can show what a change looks like to use. Each recording runs against a stub of the source.coop API filled with made-up records, so nothing touches a real account or a real login.
44

55
```bash
66
cargo build
7-
# write demo/scripts/pr-60.py for your change (see "Writing a demo"), then:
8-
python3 -I demo/record.py demo/scripts/pr-60.py # every demo in the script, to demo/casts/
9-
python3 -I demo/record.py demo/scripts/pr-60.py product-create # just one
7+
# write docs/recorder/scripts/pr-60.py for your change (see "Writing a demo"), then:
8+
python3 -I docs/recorder/record.py docs/recorder/scripts/pr-60.py # every demo in the script, to docs/recorder/casts/
9+
python3 -I docs/recorder/record.py docs/recorder/scripts/pr-60.py product-create # just one
1010
```
1111

1212
It needs Python 3.10 or later on macOS or Linux, and Node.js for `npx`, which fetches [svg-term-cli](https://github.com/marionebl/svg-term-cli) 2.1.1 to render the SVGs. Putting them in a PR needs [`gh`](https://cli.github.com) 2.103 or later. Demos that pipe through `jq` need it installed.
@@ -15,17 +15,17 @@ It needs Python 3.10 or later on macOS or Linux, and Node.js for `npx`, which fe
1515

1616
- `stub.py` serves `fixtures.json` under `/api/v1` from memory. What a demo creates, edits or deletes, the commands after it see. Every demo starts from a fresh copy. It answers what the CLI asks, in the API's shapes, and checks nothing: it is for demos, not tests, which use `wiremock`. Outside `/api/v1` it stands in for the data proxy's S3 listing (ListObjectsV2 of the fixtures' `objects`), so `aws s3 ls` works against it.
1717
- `fixtures.json` holds the records: the signed-in `demo-user`, who owns the organization `demo-org`, with its products, data connections, service accounts, trusts, API keys, memberships and the objects in its bucket. Add what a new demo needs here.
18-
- `check_stub.py` makes a request to each of the stub's routes and validates the answers against the API's OpenAPI spec on staging. CI runs it on PRs that change `demo/`. Run it yourself after changing the stub or fixtures: `uv run --with jsonschema python demo/check_stub.py` (or `pip install jsonschema` first). When a demo needs a new route, add a request for it to `requests()`.
18+
- `check_stub.py` makes a request to each of the stub's routes and validates the answers against the API's OpenAPI spec on staging. CI runs it on PRs that change `docs/recorder/`. Run it yourself after changing the stub or fixtures: `uv run --with jsonschema python docs/recorder/check_stub.py` (or `pip install jsonschema` first). When a demo needs a new route, add a request for it to `requests()`.
1919
- `record.py` runs bash in a pseudo-terminal at 80×28 and types each demo's commands like a person. It records with real timing, capping idle time at 2.5s. The recording ends at the last prompt. Before it's saved, the stub's URLs are drawn as production's (`https://source.coop` for the site and API, `https://data.source.coop` for the proxy) and the throwaway `HOME` as `~`, so ports and temp paths don't distract; the CLI still only talks to the stub. Each is rendered as an animated SVG beside its cast, with its steps in a JSON file of the same name. The docs site's player marks the steps on its track and lists them: each command run, and each prompt that a key answers. The file also lists each key pressed (`key`, `enter`), which the player shows in the terminal's corner as the demo plays.
20-
- `demos/guide.py` holds the script for the demos the README shows, which are kept in `docs/demos/` (below).
21-
- No other demo scripts are kept. Each PR writes its own, for what it changes, in `demo/scripts/`, which git ignores. A script that isn't run between PRs only goes stale, and whoever made a change knows what to show of it.
20+
- `guide.py` holds the script for the demos the README shows, which are kept in `docs/demos/` (below).
21+
- No other demo scripts are kept. Each PR writes its own, for what it changes, in `docs/recorder/scripts/`, which git ignores. A script that isn't run between PRs only goes stale, and whoever made a change knows what to show of it.
2222

2323
## Writing a demo
2424

2525
A script is a Python file defining `DEMOS = {name: (title, function)}`, where each function drives a `Session`. `record.py` provides `DOWN`, `UP`, `RIGHT`, `LEFT`, `ESC` and `BACKSPACE`.
2626

2727
```python
28-
# demo/scripts/pr-60.py
28+
# docs/recorder/scripts/pr-60.py
2929
def create(s):
3030
s.run("source-coop product create") # type a command at the prompt and run it
3131
s.see("Account") # wait for text (a regex) to appear
@@ -48,15 +48,15 @@ Wait with `see` before every keypress that answers a prompt. That prompt and the
4848

4949
Each session gets `SOURCE_TOKEN=demo`, `SOURCE_API_URL` and `SOURCE_PROXY_URL` pointing at two copies of the stub (the site and the data proxy, as in production), `SOURCE_PAGER=cat` and a throwaway `HOME`. The proxy URL gives the run an environment of its own: the CLI keys its keyring entries and cache files by environment, so it can't read or remove the real login session, and drafts land in the throwaway `HOME`. A demo that saves a session or credentials still writes an entry for that environment (`127.0.0.1:PORT`) to the OS keyring, so leave sign-in out of demos.
5050

51-
To show a signed-in CLI (`auth status`, `auth token`, `creds`, `aws`), call `signed_in(s)` from `demos/guide.py` first. It writes a made-up session, role credentials and an `~/.aws/config` profile into the run's `HOME`, where `s.home`, `s.stub_url` and `s.proxy_url` say. The keyring has nothing for the stub's environment, so the CLI reads those files, and as everything in them is fresh, it only reads: nothing reaches the keyring. Don't let such a demo refresh or sign in. The one thing that does reach the network is the CLI's own lookups elsewhere, such as a GitHub repository's subject claim. Use a public repository for those, like `source-cooperative/source.coop`.
51+
To show a signed-in CLI (`auth status`, `auth token`, `creds`, `aws`), call `signed_in(s)` from `guide.py` first. It writes a made-up session, role credentials and an `~/.aws/config` profile into the run's `HOME`, where `s.home`, `s.stub_url` and `s.proxy_url` say. The keyring has nothing for the stub's environment, so the CLI reads those files, and as everything in them is fresh, it only reads: nothing reaches the keyring. Don't let such a demo refresh or sign in. The one thing that does reach the network is the CLI's own lookups elsewhere, such as a GitHub repository's subject claim. Use a public repository for those, like `source-cooperative/source.coop`.
5252

5353
## In the README
5454

55-
The README embeds animated SVGs from `docs/demos/`, the only recordings git keeps. `demos/guide.py` makes them all. When a change alters what one shows, update its function there and re-record just that demo, then check the SVG shows what the README says it does:
55+
The README embeds animated SVGs from `docs/demos/`, the only recordings git keeps. `guide.py` makes them all. When a change alters what one shows, update its function there and re-record just that demo, then check the SVG shows what the README says it does:
5656

5757
```bash
5858
cargo build
59-
python3 -I demo/record.py demo/demos/guide.py --out docs/demos product-create service-account-keys
59+
python3 -I docs/recorder/record.py docs/recorder/guide.py --out docs/demos product-create service-account-keys
6060
rm docs/demos/*.cast # keep the .svg and .json
6161
```
6262

@@ -67,9 +67,9 @@ A new demo needs a line in `guide.py`'s `DEMOS` and an image in the README. Each
6767
`record.py` prints markdown showing each SVG by its local path. `gh` uploads the files you `--attach` and rewrites those paths to point at the uploads, so the SVGs live on GitHub with the PR and nothing else hosts them:
6868

6969
```bash
70-
python3 -I demo/record.py demo/scripts/pr-60.py > demos.md
70+
python3 -I docs/recorder/record.py docs/recorder/scripts/pr-60.py > demos.md
7171
# write the description in body.md, pasting demos.md where the demos go, then:
72-
attach=(); for f in demo/casts/*.svg; do attach+=(--attach "$f"); done
72+
attach=(); for f in docs/recorder/casts/*.svg; do attach+=(--attach "$f"); done
7373
gh pr create -F body.md "${attach[@]}" # or: gh pr edit 60 -F body.md "${attach[@]}"
7474
```
7575

@@ -78,7 +78,7 @@ Each entry looks like this before `gh` rewrites it:
7878
```markdown
7979
**source-coop product create (prompted)**
8080

81-
![source-coop product create (prompted)](demo/casts/product-create.svg)
81+
![source-coop product create (prompted)](docs/recorder/casts/product-create.svg)
8282
```
8383

8484
Run `gh` from the repository root, or keep `body.md` there: `gh` reads the paths relative to the body file, then to the working directory. The demos show production's URLs, but run against the stub: say so in the PR.

0 commit comments

Comments
 (0)