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
50 changes: 50 additions & 0 deletions .github/workflows/markdown-verification.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Markdown Verification

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
workflow_dispatch:
pull_request:
branches:
- "**"
paths:
- "Documentation/**"
- ".markdownlint.json"
- ".github/workflows/markdown-verification.yml"

permissions:
contents: read

jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Lint Markdown files
uses: DavidAnson/markdownlint-cli2-action@v18
with:
globs: 'Documentation/**/*.md'

link-verification:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

# Site-absolute links point at the aggregated documentation site, which is built from every product
# repository at once - there is nothing in this repository for them to resolve to, and the Documentation
# repository's own build verifies them. Everything else, including every relative link within this
# repository, is checked. Keep linksToSkip in sync with Documentation/verify-markdown.sh.
- name: Check links in Documentation
uses: JustinBeckwith/linkinator-action@v1
with:
paths: 'Documentation/**/*.md'
markdown: true
recurse: true
redirects: allow
statusCodes: '{"403":"ok"}'
linksToSkip: '(^|/)(prologue|cli/getting-started)/'
18 changes: 18 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json",
"default": true,
"MD013": false,
"MD033": {
"allowed_elements": ["a", "div", "img", "h3", "p", "br", "strong", "details", "summary", "b", "code"]
},
"MD051": false,
"MD041": false,
"MD012": {
"maximum": 3
},
"MD024": {
"siblings_only": true
},
"MD032": false,
"MD060": false
}
6 changes: 3 additions & 3 deletions Documentation/context/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ cratis context list -o plain

Example output:

```
```text
* dev chronicle://localhost:35000
prod chronicle://prod.example.com:35000/
```
Expand Down Expand Up @@ -107,7 +107,7 @@ cratis context show -o json

Example output (plain):

```
```text
Server: chronicle://localhost:35000
Event store: default
Namespace: Default
Expand Down Expand Up @@ -159,7 +159,7 @@ cratis context path

Example output:

```
```text
/Users/alice/.cratis/config.json
```

Expand Down
4 changes: 2 additions & 2 deletions Documentation/reference/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ This page documents how the `cratis` CLI connects to a Chronicle server: the con

Chronicle connection strings use the `chronicle://` scheme:

```
```text
chronicle://<host>:<port>/?<options>
```

**Examples:**

```
```text
chronicle://localhost:35000
chronicle://prod.example.com:35000/
```
Expand Down
58 changes: 54 additions & 4 deletions Documentation/reference/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,25 @@ cratis run [PATH]
```

```text
Starting Stage from /work/invoicing on http://localhost:9090
Chronicle Workbench on https://localhost:35000
Running the Screenplay files in /work/invoicing

Stage API http://localhost:9090
API reference http://localhost:9090/scalar/v1
Chronicle Workbench https://localhost:35000
HTTPS only — sign in with admin / ChangeMeNow!

✓ Ready — event model 'Invoicing' as event store 'gentle-zephyr'
Press Ctrl+C to stop
```

You point it at a folder of `.play` files (or run it from inside one), and it hands that folder to the Stage container, which compiles every `.play` file it finds and exposes a live API you can drive — plus the Chronicle Workbench, for looking at what the model does to the event store.

The container's own output is kept out of the way. While it boots you get a
progress line — pulling the image, starting Chronicle, compiling the Screenplay
files, registering read models — and `Ready` appears once the Stage API answers
and the model's read models are registered, so anything you send it from that
point on is actually served.

## Prerequisites

- **Docker** must be installed and the `docker` command on your `PATH`. `cratis run` shells out to `docker run`.
Expand All @@ -31,6 +44,7 @@ You point it at a folder of `.play` files (or run it from inside one), and it ha
| `--tag <TAG>` | The `cratis/stage` image tag to run. Default: `latest`. |
| `--port <PORT>` | Host port to publish the Stage API on. Default: `9090`. |
| `--workbench-port <PORT>` | Host port to publish the Chronicle Workbench on. Default: `35000`. |
| `--verbose` | Stream the container's output instead of showing startup progress. |

Global options such as `-o/--output` are also accepted — see [Global Options](global-options.md).

Expand All @@ -39,12 +53,13 @@ Global options such as `-o/--output` are also accepted — see [Global Options](
The command mounts the folder into the Stage container and publishes both of the container's ports to your host:

```bash
docker run --rm -p 9090:9090 -p 35000:35000 -v "$PWD":/eventmodel cratis/stage:latest
docker run --rm --name cratis-stage-a1b2c3d4 -p 9090:9090 -p 35000:35000 -v "$PWD":/eventmodel cratis/stage:latest
```

- The folder is mounted at `/eventmodel` inside the container; Stage globs `**/*.play` beneath it and compiles them.
- The Stage API is published on `http://localhost:9090` (change the host side with `--port`). Its API reference is at `http://localhost:9090/scalar/v1`.
- The **Chronicle Workbench** is published on `https://localhost:35000` (change the host side with `--workbench-port`), so you can inspect the session's events, observers and read models while it runs.
- The container is named `cratis-stage-<random>`, so a running sandbox is recognizable in `docker ps` and several can run side by side on different ports.
- `--rm` removes the container when it exits, so every run starts from a clean, in-memory store.

The Workbench is **HTTPS only** — open `https://localhost:35000`, not `http://`. The Chronicle port multiplexes
Expand All @@ -53,7 +68,42 @@ HTTP/1.1 and HTTP/2 through ALPN, which requires TLS, so a plain `http://` reque
development one, so your browser warns the first time. Sign in with the Stage image's development credentials —
user `admin`, password `ChangeMeNow!`.

The command streams the container's output and exits with the container's exit code. Stop the session with `Ctrl+C`.
## Output

The container's output is hidden while it starts, and replaced by a progress
line. `Ready` is printed once the Stage API answers *and* the model's read
models have been registered with Chronicle — the API starts listening a few
seconds before that, so waiting for the registration is what makes `Ready` mean
the session is usable.

If the container stops before it gets there, the error it reported is shown
along with the last lines it wrote, and the command exits with a server error.
Run with `--verbose` to stream the container's output as it happens instead —
useful when the captured tail is not enough to tell what went wrong.

With `-o json` or `-o json-compact`, nothing is printed until the session is
ready, and then a single object is emitted with the resolved endpoints:

```json
{
"status": "ready",
"path": "/work/invoicing",
"eventModel": "Invoicing",
"eventStore": "gentle-zephyr",
"stageApi": "http://localhost:9090",
"apiReference": "http://localhost:9090/scalar/v1",
"workbench": "https://localhost:35000"
}
```

## Stopping

The command keeps running until you stop it with `Ctrl+C`. It then stops the
container, waits for Docker to remove it, and exits with `0` — so the prompt
comes back only once the sandbox is actually gone. A second `Ctrl+C` terminates
the command immediately, leaving whatever Docker is doing to finish on its own.
When the session ends by itself, the command exits with a server error if the
container failed.

## Errors

Expand Down
2 changes: 1 addition & 1 deletion Documentation/scenarios/replay-a-projection.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@ cratis chronicle observers show <OBSERVER_ID>
## Done when

The observer's sequence number matches the event log tail and its read model reflects the new shape.
Spot-check the rebuilt data with [`read-models`](/cli/chronicle/read-models/) or by
Spot-check the rebuilt data with [`read-models`](../chronicle/read-models.md) or by
[reading the events](./verify-events-were-appended.md) it was built from.
12 changes: 10 additions & 2 deletions Documentation/verify-markdown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
# Markdown Verification Script
# This script runs the same markdown linting and link verification that runs in CI

set -e
# No `set -e` here: both steps have to run so the summary reports everything that failed, not just the first.
set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# Site-absolute links (`/prologue/`, `/cli/getting-started/`) point at the aggregated documentation site, which
# is built from every product repository at once - there is nothing in this repository for them to resolve to,
# and the Documentation repository's own build verifies them. Everything else, including every relative link
# within this repository, is checked.
# Keep in sync with .github/workflows/markdown-verification.yml.
LINKS_TO_SKIP='(^|/)(prologue|cli/getting-started)/'

echo "=========================================="
echo "Markdown Verification"
echo "=========================================="
Expand Down Expand Up @@ -51,7 +59,7 @@ echo ""
echo "This may take a few minutes to check all links..."
echo ""

npx linkinator "Documentation/**/*.md" --markdown --recurse --verbosity error --status-code "403:ok" --skip "^(https?:\\/\\/)?(localhost|127\\.0\\.0\\.1)(:\\d+)?(\\/|$)"
npx linkinator "Documentation/**/*.md" --markdown --recurse --verbosity error --status-code "403:ok" --skip "$LINKS_TO_SKIP"
LINK_EXIT_CODE=$?

echo ""
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,12 @@ Screenplay. Heuristics build the structure; a language model configured through
```bash
cratis run # every .play in this folder
cratis run ./screenplays --port 9191
cratis run --verbose # stream the container's output
```

Runs the `.play` files in a folder in a local Stage container. Needs Docker on the `PATH`.
Reports progress while the container boots and prints `Ready` once the Stage answers — the
container's own output is hidden unless it fails, or you pass `--verbose`.

</details>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class with_a_custom_host_port : Specification
{
IReadOnlyList<string> _arguments;

void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "1.2.0", 9191, 35001);
void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "1.2.0", 9191, 35001, "cratis-stage-abc123");

[Fact] void should_publish_the_custom_host_port_to_the_api_port() => _arguments.ShouldContain("9191:9090");
[Fact] void should_publish_the_custom_workbench_host_port_to_the_workbench_port() => _arguments.ShouldContain("35001:35000");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ public class with_matching_ports : Specification
{
IReadOnlyList<string> _arguments;

void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "latest", 9090, 35000);
void Because() => _arguments = StageContainer.BuildRunArguments("/work/space", "latest", 9090, 35000, "cratis-stage-abc123");

[Fact] void should_run_a_container() => _arguments.ShouldContain("run");
[Fact] void should_remove_the_container_on_exit() => _arguments.ShouldContain("--rm");
[Fact] void should_name_the_container() => _arguments.ShouldContain("cratis-stage-abc123");
[Fact] void should_publish_the_host_port_to_the_api_port() => _arguments.ShouldContain("9090:9090");
[Fact] void should_publish_the_host_port_to_the_workbench_port() => _arguments.ShouldContain("35000:35000");
[Fact] void should_mount_the_folder_at_the_model_path() => _arguments.ShouldContain("/work/space:/eventmodel");
Expand Down
20 changes: 20 additions & 0 deletions Source/Cli.Specs/for_StageContainer/when_generating_a_name.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_StageContainer;

public class when_generating_a_name : Specification
{
string _first;
string _second;

void Because()
{
_first = StageContainer.GenerateName();
_second = StageContainer.GenerateName();
}

[Fact] void should_recognize_the_name_as_a_stage_sandbox() => _first.StartsWith(StageContainer.NamePrefix, StringComparison.Ordinal).ShouldBeTrue();
[Fact] void should_be_unique_so_sandboxes_can_run_side_by_side() => _first.ShouldNotEqual(_second);
[Fact] void should_stay_short_enough_to_read_in_docker_ps() => _first.Length.ShouldEqual(StageContainer.NamePrefix.Length + 8);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_StageEndpoints;

public class when_resolving_for_host_ports : Specification
{
StageEndpoints _endpoints;

void Because() => _endpoints = StageEndpoints.For(9191, 35001);

[Fact] void should_serve_the_api_over_http_on_the_host_port() => _endpoints.Api.ShouldEqual("http://localhost:9191");
[Fact] void should_point_the_api_reference_at_the_same_host_port() => _endpoints.ApiReference.ShouldEqual("http://localhost:9191/scalar/v1");
[Fact] void should_serve_the_workbench_over_https_on_its_own_host_port() => _endpoints.Workbench.ShouldEqual("https://localhost:35001");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_StageOutput.when_taking_the_tail;

public class and_fewer_lines_are_asked_for_than_were_written : Specification
{
StageOutput _output;
IReadOnlyList<string> _result;

void Establish()
{
_output = new();
for (var line = 1; line <= 5; line++)
{
_output.Append($"line {line}");
}
}

void Because() => _result = _output.Tail(2);

[Fact] void should_return_the_most_recent_lines_oldest_first() => string.Join(',', _result).ShouldEqual("line 4,line 5");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_StageOutput.when_taking_the_tail;

public class and_more_lines_were_written_than_the_capacity : Specification
{
StageOutput _output;
IReadOnlyList<string> _result;

void Establish()
{
_output = new(3);
for (var line = 1; line <= 5; line++)
{
_output.Append($"line {line}");
}
}

void Because() => _result = _output.Tail(10);

[Fact] void should_only_keep_the_last_lines() => string.Join(',', _result).ShouldEqual("line 3,line 4,line 5");
}
11 changes: 11 additions & 0 deletions Source/Cli.Specs/for_StageStartup/given/a_stage_startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_StageStartup.given;

public class a_stage_startup : Specification
{
protected StageStartup _startup;

void Establish() => _startup = new("cratis/stage:latest");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Cratis.Cli.for_StageStartup.when_observing_output;

public class and_an_earlier_phase_is_reported_again : given.a_stage_startup
{
bool _changed;

void Establish()
{
_startup.Observe("Starting Chronicle (in-memory storage)...");
_startup.Observe("Chronicle is ready.");
_startup.Observe("Starting Stage...");
}

void Because() => _changed = _startup.Observe("Waiting for Chronicle to be ready...");

[Fact] void should_stay_in_the_phase_it_reached() => _startup.Phase.ShouldEqual(StagePhase.StartingStage);
[Fact] void should_not_report_a_change() => _changed.ShouldBeFalse();
}
Loading
Loading