Skip to content

One plan builds the compose project and its image; the deploy record is a dataclass - #49

Closed
Bobowski wants to merge 1 commit into
tests-layers-fixturesfrom
source-and-record
Closed

Bobowski wants to merge 1 commit into
tests-layers-fixturesfrom
source-and-record

Conversation

@Bobowski

@Bobowski Bobowski commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Intent

Write each rule of a deploy once: the image of a plan, the Dockerfile path, the compose files, and the status of an exit code. Source: the user, in the Yard audit. This pull request stacks on #48.

Found

  • apps._ready_repo, apps._ready_source, and compose.saved_project each built the image and the Dockerfile path with their own copy of the rules: split('@')[0]@<digest> two times, compose:<rev[7:19]> two times, and the yard: Dockerfile rule two times. compose.project took eight arguments, and the callers worked out the checkout themselves.
  • apps._apply and status.deploy_record each turned an exit code into a status and an error text: 0 ok, 3 reverted, any other code failed.
  • status.deploy_record returned a plain dict. The API and apps._join read it by string keys.
  • test_check_repo_debounces failed at random on main. Since The deploy container is the only deploy record and the deploy lock #47, each check runs podman ps. That call can take longer than the 50 ms window of the test.

Changed

  • compose.project(config, app, plan) takes the plan (Deployed). It picks the checkout (the clone for a repo app, the app folder otherwise), the image (compose.plan_image), the Dockerfile, and the files. Project carries image. saved_project only checks that the files are still there, then calls project.
  • status.deploy_status(code) and status.deploy_error(code, previous) hold the exit-code rule. _apply and deploy_record use them.
  • status.Record is the deploy record. Row.deploy and Page.deploy hold a Record. The JSON shapes do not change: asdict writes the same keys, and the deploy route still adds port on ok.
  • The debounce test uses a 0.5 s window.

I did not add a separate Source type. When compose.project owns the rules, the string checks that remain are each in one place.

Not done

Nothing

Proof

$ just check
...
500 passed in 60.45s

tests/server/test_compose.py checks the image and the Dockerfile of each kind through the new project. test_check_repo_debounces passed 5 runs in a row.

Questions

None

🤖 Generated with Claude Code

…is a dataclass

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Bobowski
Bobowski deleted the branch tests-layers-fixtures November 2, 2026 22:53
@Bobowski Bobowski closed this Nov 2, 2026
Bobowski added a commit that referenced this pull request Nov 2, 2026
…is a dataclass (#54)

## Intent

Write each rule of a deploy once: the image of a plan, the Dockerfile
path, the compose files, and the status of an exit code. Source: the
user, in the Yard audit. It replaces #49, which GitHub closed when the
base branch of #49 was deleted. #48 is merged.

## Found

- `apps._ready_repo`, `apps._ready_source`, and `compose.saved_project`
each built the image and the Dockerfile path with their own copy of the
rules: `split('@')[0]@<digest>` two times, `compose:<rev[7:19]>` two
times, and the `yard:` Dockerfile rule two times. `compose.project` took
eight arguments, and the callers worked out the checkout themselves.
- `apps._apply` and `status.deploy_record` each turned an exit code into
a status and an error text: 0 ok, 3 reverted, any other code failed.
- `status.deploy_record` returned a plain dict. The API and `apps._join`
read it by string keys.
- `test_check_repo_debounces` failed at random on `main`. Since #47,
each check runs `podman ps`. That call can take longer than the 50 ms
window of the test.

## Changed

- `compose.project(config, app, plan)` takes the plan (`Deployed`). It
picks the checkout (the clone for a repo app, the app folder otherwise),
the image (`compose.plan_image`), the Dockerfile, and the files.
`Project` carries `image`. `saved_project` only checks that the files
are still there, then calls `project`.
- `status.deploy_status(code)` and `status.deploy_error(code, previous)`
hold the exit-code rule. `_apply` and `deploy_record` use them.
- `status.Record` is the deploy record. `Row.deploy` and `Page.deploy`
hold a `Record`. The JSON shapes do not change: `asdict` writes the same
keys, and the deploy route still adds `port` on `ok`.
- The debounce test uses a 0.5 s window.

I did not add a separate `Source` type. When `compose.project` owns the
rules, the string checks that remain are each in one place.

## Not done

Nothing

## Proof

```
$ just check
...
500 passed in 60.45s
```

`tests/server/test_compose.py` checks the image and the Dockerfile of
each kind through the new `project`. `test_check_repo_debounces` passed
5 runs in a row.

## Questions

None

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bobowski added a commit that referenced this pull request Nov 2, 2026
…through Podman.stream (#50)

## Intent

Remove the duplicate code of the status report and of the compose
runner. Source: the user, in the Yard audit. This pull request stacks on
#49.

## Found

- `status.report` and `status.app_report` each ran `ps -a`, `stats`, and
the crash inspect, and each built the rows of an app.
- Each status call sent one Caddy `/adapt` request for each app, through
`sites.list_hosts`. `yard status` asked Caddy about every site file
every time.
- `_row` walked the data folder of an app once for each container of
that app.
- `compose.py` built its own Podman argv, its own `--remote` flag, and
its own copy of `os.environ` (`_podman`, `_process_env`, `_run`). Podman
holds the same logic. `_process_env` set `CONTAINER_HOST` only when it
was missing. The yard container and the apply image always set it
(`ops/compose.yml`, `ops/Containerfile`), so the function changed
nothing in production.

## Changed

- `status._look` asks Podman once: the containers, the samples, and the
crashes. `report` and `app_report` both use it and both use `_app_rows`.
`_app_rows` reads the tip, the deploy record, and the data size of the
app once.
- `_row` takes the data size and no `Config`.
- `sites.list_hosts` keeps the hosts of each site file in
`Yard.site_hosts`, keyed by the `(mtime_ns, size)` of the file. Caddy
adapts a file again only when the file changes.
- `Podman.stream(*args, on_line)` runs a command with no timeout and
streams each line. `compose` runs `rm`, `run`, `logs -f`, and `build`
through it. `_process_env` and `_run` are gone.

## Not done

- I did not pass `yard.podman` into `compose`. `compose` makes a
`Podman` from the config, so the signatures of `compose.apply` and of
the test fakes stay the same.

## Proof

```
$ just check
...
500 passed in 61.46s
```

`test_list_hosts_adapts_each_file_once` shows one `/adapt` for two
reads, then a new adapt after the file changes.
`test_stream_calls_on_line_in_order` checks the line stream and the exit
code.

## Questions

- A missing podman binary in a compose call now raises the code `podman`
in place of `compose`. Nothing reads that code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Bobowski added a commit that referenced this pull request Nov 2, 2026
…ollow for logs and events (#51)

## Intent

Let each forge own its own differences, and let the journal adapter own
the follow process. Source: the user, in the Yard audit. This pull
request stacks on #50.

## Found

- `origin_hook` and `github_hook` in `api.py` each read the body, parsed
the JSON, found the repo, logged `hook.received`, and scheduled a check.
Only the signature and the body shape differ.
- `forge_put` had one branch for each forge, with `isinstance` checks.
Each branch validated its fields, called `connect`, sent the same
journal event, and built the app answer.
- The error "is not connected. Run yard forge setup" was written three
times, in `apps.py` and two times in `repos.py`.
- `_stream_journal` (with and without a filter) and `_stream_events`
each ran the journalctl follow process, read lines, and killed the
process in a `finally`.
- `logs_get` had three read paths: JSON, text without a filter, and text
with a filter.
- `_deploy_row` in the events had its own copy of the exit-code rule
from #49.

## Changed

- `Forge` gets `signed`, `delivery`, `connect_with`, `app_info`,
`require_connected`, and the attributes `event_header`,
`signature_header`, and `connect_fields`. Origin and GitHub fill them
in. `HooksPage._hook` is the one handler for both routes. `forge_put`
validates `connect_fields` and has no `isinstance` checks.
- `journal.follow_entries(proc, tick)` yields the entries of a follow
process, and `None` at each tick with no new line. It kills the process
when it closes. Both streams use it in `contextlib.aclosing`.
`journal.follow` still starts the process before the headers go out, so
a missing journal is still a 502.
- `logs_get` reads the entries once, applies the filter once, then
writes JSON or text. `_logs_page` is gone.
- `_deploy_row` uses `status.deploy_status` and `status.deploy_error`.

The webhook body shapes moved from `api.py` into `forges.py`.
`forges.py` already reads the other forge-specific answers
(installations, tokens, field names), so a delivery body fits there.

## Not done

- The legacy installation state in `Forge.installations` stays. A later
pull request removes it with the 0.10 migration.

## Proof

```
$ just check
...
501 passed in 61.14s
```

The hook tests in `tests/server/test_api.py` (status codes, a bad
signature, a push, an installation event) and the log and event tests in
`tests/server/test_journal.py` pass without a change.

## Questions

- An Origin delivery with broken JSON now sets `last_delivery` before
the 400. Before, only a delivery that parsed set it. GitHub already set
it before the parse.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant