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
24 changes: 12 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
PINS
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
check-latest: true
cache-dependency-path: src/go.sum
# The module lives in src/: a bare `go test ./...` at the root finds no
Expand All @@ -64,7 +64,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
- uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
Expand All @@ -77,9 +77,9 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
# Pinned: govulncheck @latest (v1.4.0) segfaults under the go1.25 toolchain.
# Pinned: govulncheck @latest (v1.4.0) segfaulted under the go1.25 toolchain.
- run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- run: |
cd src && "$(go env GOPATH)/bin/govulncheck" ./...
Expand All @@ -90,18 +90,18 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
# Build gosec with the runner's Go — the gosec docker action bundles an
# older Go that cannot load a `go 1.24` module.
# probaci is a CI orchestrator: it runs subprocesses (G204) and reads/writes
# user-named files (G304/G306) with conventional perms (G301/G302) by
# design, and G104 duplicates errcheck. The high-severity taint rules
# (G702/G703) stay on, with #nosec on the few benign sites.
# Pinned, not @latest: gosec v2.30.0 requires go >= 1.26 and fails to
# install on this job's Go 1.25 (GOTOOLCHAIN=local). v2.29.0 is the
# newest release whose go.mod accepts 1.25; bump with the go-version.
- run: go install github.com/securego/gosec/v2/cmd/gosec@v2.29.0
# Pinned, not @latest, so a new gosec cannot fail a pull request that
# changed nothing it checks. v2.30.0 needs go >= 1.26, which this job now
# runs; move the pin with the go-version.
- run: go install github.com/securego/gosec/v2/cmd/gosec@v2.30.0
- run: |
cd src && "$(go env GOPATH)/bin/gosec" -exclude=G104,G204,G301,G302,G304,G306 ./...

Expand All @@ -111,7 +111,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
# Run gitleaks directly (the gitleaks-action requires a paid license for
# org repos). Scans the working tree and honors .gitleaks.toml. Built from
Expand All @@ -130,7 +130,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
# go -C src starts probaci inside src/; it walks up to the git root and
# proves the whole repository, as it did from the root.
Expand Down Expand Up @@ -158,7 +158,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
- run: make cross
- run: cd build && sha256sum -c checksums.txt
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
# The module lives in src/, which autobuild does not look for: manual mode
# builds it explicitly, so the scan never reports clean on zero code.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version: "1.25"
go-version: "1.26"
cache-dependency-path: src/go.sum
- uses: sigstore/cosign-installer@756fe2d284c7292c4e9b62c8a28f1e5629c59532 # v4.1.3
# syft powers GoReleaser's SBOM generation.
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Changed

- Building probaci needs Go 1.26, and CI runs on it. Go 1.25 has ten standard-library
vulnerabilities fixed only in 1.26, which the web UI's `net/http` server reaches; `golang.org/x/net`
moves to v0.60.0 for the same reason, and gosec to v2.30.0.
- The release workflow no longer starts on a tag push. Binary releases are on hold, and a release
runs only when dispatched by hand on a `v[0-9]*` tag with `-f confirm=release-go`; a test fails
if an automatic trigger returns.
Expand All @@ -27,6 +30,13 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- `probaci serve`: a web UI on this machine, as an option beside `run` and `tui`. It lists the
repositories and their languages, starts a run with the stages you pick, streams its output and
stage results as they happen, can cancel it, and keeps every finished run in
`$XDG_STATE_HOME/probaci/runs`. It follows the other Simtabi Go web UIs: a one-time sign-in
token, a `Host` allowlist, same-origin and CSRF checks, a strict Content-Security-Policy, Tabler
embedded with nothing fetched from the network, an SVG favicon on every page, and `--read-only`.
`third-party-notices.md` carries the embedded licenses and ships in the archives.
- Tests that fail when `build/` holds anything but OS folders, inputs and checksums, when a
container image or docker step returns, and when the docs offer `go install`. The release
trigger test now also requires the refusal to be the workflow's first step.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ probaci init # write a probaci.json for this repo
probaci run # run the pipeline on the current directory
probaci run ./api ./web # run on several repositories
probaci tui # interactive dashboard
probaci serve # web UI in the browser: start runs, watch them
```

Positional arguments are **repository paths**; stage selection is via flags:
Expand Down Expand Up @@ -138,6 +139,7 @@ all output and logs, and releases are signed with provenance + SBOM. Details in
| [Security](docs/security.md) | hardening defaults, the socket-mount caveat |
| [Release](docs/release.md) | tag-driven releases, signing, channels |
| [probaci tool](docs/tools/probaci.md) | command reference |
| [`serve`](docs/tools/serve.md) | the web UI: start runs, watch them, read earlier ones |

## License

Expand Down
1 change: 1 addition & 0 deletions docs/tools/probaci.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ probaci [command] [flags] [PATH...]
| `run [PATH...]` | run the pipeline on one or more repositories (default: `.`) |
| `stage <name> [PATH...]` | run a single stage |
| `tui [PATH...]` | launch the interactive dashboard |
| `serve [PATH...]` | open the web UI on this machine ([`serve`](serve.md)) |
| `doctor [PATH]` | runtime, detected platforms/languages/VCS, config provenance |
| `init [PATH]` | write a `probaci.json` detected from the repo |
| `platforms [PATH]` | list supported CI platforms and which are detected |
Expand Down
90 changes: 90 additions & 0 deletions docs/tools/serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# `serve`

`probaci serve` opens a web UI on this machine: the repositories a run covers, a form to start a run
with the stages you pick, its output as it happens, and every earlier run. It is an option beside
`run` and `tui`, not a replacement; a bare `probaci` still prints help.

## Starting it

```sh
probaci serve # the current repository, 127.0.0.1:7844, opens a browser
probaci serve ../api ../web # several repositories, as for run
probaci serve --no-open # print the address only
probaci serve --read-only # look at runs, start none
```

It prints an address like this:

```text
probaci web UI: http://127.0.0.1:7844/?token=…
press Ctrl-C to stop
```

The token in the address signs the browser in, once. The browser swaps it for a session cookie and
drops it from the address bar. Without it every page answers 401.

If port 7844 is taken, `serve` uses the next free port and says which, trying up to twenty. `--port`
pins a port and fails when that port is taken.

A browser opens only for a person at a terminal: never when `CI` is set, when
`PROBACI_NO_BROWSER=1`, when stdout is not a terminal, or on Linux and the BSDs with neither
`DISPLAY` nor `WAYLAND_DISPLAY`.

## Flags

| Flag | What it does |
|---|---|
| `--addr HOST[:PORT]` | The address to listen on (default `127.0.0.1`). A port here pins it, like `--port`. |
| `--port N` | The port to listen on (default 7844). Given, it is pinned. |
| `--allow-remote` | Allow a non-loopback `--addr`. Plain HTTP: prefer `ssh -L 7844:127.0.0.1:7844 <host>`. |
| `--allow-host NAME` | With `--allow-remote`, an extra `Host` header to accept, such as a proxy's name. Repeatable. |
| `--read-only` | Serve no route that starts or cancels a run. |
| `--no-open` | Do not open a browser. |

The global `--repos`, `-C`, `--config`, `--no-docker`, `--backend`, `--pull`, `--platform` and
`--full-image` apply to every run started from the UI, as they do to `run`.

## Pages

- **Overview** lists the repositories with the languages detected in each, says whether containers
can run, and holds the run form: every enabled stage, ticked, and how many repositories run at
once when there is more than one. It also lists the five newest runs.
- **Runs** lists every run, newest first, with the stages it ran and its result.
- **A run** shows each repository's stages with their result, summary, time and output, and the
run's streamed output below. While it runs the page updates as each stage starts and finishes,
and **Cancel run** stops it. **Report (JSON)** downloads the results.
- **Settings** shows the address, the mode, where runs are kept, the configuration files read and
the enabled stages.

One run goes at a time: starting a second while one is in progress points you to the first.
Every run builds probaci afresh, so it reads the configuration again and writes its own log file,
exactly as `probaci run` does.

## Where runs are kept

Each finished run is saved as one JSON file in `$XDG_STATE_HOME/probaci/runs`, else
`~/.local/state/probaci/runs` (`%LocalAppData%\probaci\runs` on Windows), and the Runs page lists
the newest hundred. A run keeps its first 5,000 lines of output; the run's log file has them all.
A run the server stopped before it finished is listed as canceled.

## Security

The server is built for one person on one machine, and holds to the same rules as the other
Simtabi web UIs:

- It listens on loopback unless told otherwise, and answers only to the `Host` it printed, which
stops a DNS-rebinding page reaching it.
- The sign-in token is 32 random bytes, and the session cookie is `HttpOnly` and `SameSite=Strict`.
- Every request that changes something must come from the server's own pages (`Origin`, or
`Sec-Fetch-Site: same-origin`) and carry the session's CSRF token.
- Every response sends a Content-Security-Policy allowing nothing but this server, and nothing is
inline. The page fetches nothing from the network: Tabler's CSS, scripts and icons are embedded,
and their licenses are on the Third-party licenses page and in `third-party-notices.md`.
- Stage output is shown after probaci's redactor has removed the secret values it knows.

`/healthz` answers `probaci` to a request carrying the token in an `X-Probaci-Token` header, and
401 to anything else.

---

[← Docs index](../../README.md#documentation)
1 change: 1 addition & 0 deletions src/.goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ archives:
- LICENSE
- README.md
- CHANGELOG.md
- third-party-notices.md

# Native Linux packages (nfpm operates on the Linux builds only).
nfpms:
Expand Down
4 changes: 3 additions & 1 deletion src/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ var docTopics = map[string]string{
"commands": "docs/tools/probaci.md",
"cli": "docs/tools/probaci.md",
"tools": "docs/tools/probaci.md",
"serve": "docs/tools/serve.md",
"ui": "docs/tools/serve.md",
}

// Doc returns the embedded markdown for a topic (default "overview"), and
Expand All @@ -57,5 +59,5 @@ func Doc(topic string) (string, bool) {

// DocTopics returns the canonical topic names (deduped), for help/listing.
func DocTopics() []string {
return []string{"overview", "installation", "configuration", "architecture", "security", "release", "commands"}
return []string{"overview", "installation", "configuration", "architecture", "security", "release", "commands", "serve"}
}
2 changes: 2 additions & 0 deletions src/embedded/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ probaci init # write a probaci.json for this repo
probaci run # run the pipeline on the current directory
probaci run ./api ./web # run on several repositories
probaci tui # interactive dashboard
probaci serve # web UI in the browser: start runs, watch them
```

Positional arguments are **repository paths**; stage selection is via flags:
Expand Down Expand Up @@ -138,6 +139,7 @@ all output and logs, and releases are signed with provenance + SBOM. Details in
| [Security](docs/security.md) | hardening defaults, the socket-mount caveat |
| [Release](docs/release.md) | tag-driven releases, signing, channels |
| [probaci tool](docs/tools/probaci.md) | command reference |
| [`serve`](docs/tools/serve.md) | the web UI: start runs, watch them, read earlier ones |

## License

Expand Down
1 change: 1 addition & 0 deletions src/embedded/docs/tools/probaci.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ probaci [command] [flags] [PATH...]
| `run [PATH...]` | run the pipeline on one or more repositories (default: `.`) |
| `stage <name> [PATH...]` | run a single stage |
| `tui [PATH...]` | launch the interactive dashboard |
| `serve [PATH...]` | open the web UI on this machine ([`serve`](serve.md)) |
| `doctor [PATH]` | runtime, detected platforms/languages/VCS, config provenance |
| `init [PATH]` | write a `probaci.json` detected from the repo |
| `platforms [PATH]` | list supported CI platforms and which are detected |
Expand Down
90 changes: 90 additions & 0 deletions src/embedded/docs/tools/serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# `serve`

`probaci serve` opens a web UI on this machine: the repositories a run covers, a form to start a run
with the stages you pick, its output as it happens, and every earlier run. It is an option beside
`run` and `tui`, not a replacement; a bare `probaci` still prints help.

## Starting it

```sh
probaci serve # the current repository, 127.0.0.1:7844, opens a browser
probaci serve ../api ../web # several repositories, as for run
probaci serve --no-open # print the address only
probaci serve --read-only # look at runs, start none
```

It prints an address like this:

```text
probaci web UI: http://127.0.0.1:7844/?token=…
press Ctrl-C to stop
```

The token in the address signs the browser in, once. The browser swaps it for a session cookie and
drops it from the address bar. Without it every page answers 401.

If port 7844 is taken, `serve` uses the next free port and says which, trying up to twenty. `--port`
pins a port and fails when that port is taken.

A browser opens only for a person at a terminal: never when `CI` is set, when
`PROBACI_NO_BROWSER=1`, when stdout is not a terminal, or on Linux and the BSDs with neither
`DISPLAY` nor `WAYLAND_DISPLAY`.

## Flags

| Flag | What it does |
|---|---|
| `--addr HOST[:PORT]` | The address to listen on (default `127.0.0.1`). A port here pins it, like `--port`. |
| `--port N` | The port to listen on (default 7844). Given, it is pinned. |
| `--allow-remote` | Allow a non-loopback `--addr`. Plain HTTP: prefer `ssh -L 7844:127.0.0.1:7844 <host>`. |
| `--allow-host NAME` | With `--allow-remote`, an extra `Host` header to accept, such as a proxy's name. Repeatable. |
| `--read-only` | Serve no route that starts or cancels a run. |
| `--no-open` | Do not open a browser. |

The global `--repos`, `-C`, `--config`, `--no-docker`, `--backend`, `--pull`, `--platform` and
`--full-image` apply to every run started from the UI, as they do to `run`.

## Pages

- **Overview** lists the repositories with the languages detected in each, says whether containers
can run, and holds the run form: every enabled stage, ticked, and how many repositories run at
once when there is more than one. It also lists the five newest runs.
- **Runs** lists every run, newest first, with the stages it ran and its result.
- **A run** shows each repository's stages with their result, summary, time and output, and the
run's streamed output below. While it runs the page updates as each stage starts and finishes,
and **Cancel run** stops it. **Report (JSON)** downloads the results.
- **Settings** shows the address, the mode, where runs are kept, the configuration files read and
the enabled stages.

One run goes at a time: starting a second while one is in progress points you to the first.
Every run builds probaci afresh, so it reads the configuration again and writes its own log file,
exactly as `probaci run` does.

## Where runs are kept

Each finished run is saved as one JSON file in `$XDG_STATE_HOME/probaci/runs`, else
`~/.local/state/probaci/runs` (`%LocalAppData%\probaci\runs` on Windows), and the Runs page lists
the newest hundred. A run keeps its first 5,000 lines of output; the run's log file has them all.
A run the server stopped before it finished is listed as canceled.

## Security

The server is built for one person on one machine, and holds to the same rules as the other
Simtabi web UIs:

- It listens on loopback unless told otherwise, and answers only to the `Host` it printed, which
stops a DNS-rebinding page reaching it.
- The sign-in token is 32 random bytes, and the session cookie is `HttpOnly` and `SameSite=Strict`.
- Every request that changes something must come from the server's own pages (`Origin`, or
`Sec-Fetch-Site: same-origin`) and carry the session's CSRF token.
- Every response sends a Content-Security-Policy allowing nothing but this server, and nothing is
inline. The page fetches nothing from the network: Tabler's CSS, scripts and icons are embedded,
and their licenses are on the Third-party licenses page and in `third-party-notices.md`.
- Stage output is shown after probaci's redactor has removed the secret values it knows.

`/healthz` answers `probaci` to a request carrying the token in an `X-Probaci-Token` header, and
401 to anything else.

---

[← Docs index](../../README.md#documentation)
10 changes: 5 additions & 5 deletions src/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/simtabi/probaci/src

go 1.25.0
go 1.26.0

require (
github.com/charmbracelet/bubbles v1.0.0
Expand Down Expand Up @@ -47,8 +47,8 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.17 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.39.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
)
Loading
Loading