OXYGENHUB

Changelog per change: a check in the gate, and Unreleased caught up b9eb70fb9509

From now on a commit that changes code adds a line under ## Unreleased
(scripts/changelog-check.sh, run in the gate; NO_CHANGELOG=1 for changes
users won't notice), and versions change only at release. Unreleased now
lists everything since the last release.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key e01a00b649d5
When
· 2026-10-19 20:16 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
b9eb70fb95093d2f5542ebc2c94a199d4d7b1c7e1423f1821e20fef9021bd661
Depends on
1870e4fa9808 99ca93d60bca a7e6e8e6094c a9f1c0bf403a b5668108487f d23464c0ac60
Depended on by
c30444e35f2e
In bookmarks
api-unknown-fields diffusion-pin-bookmark draft-ready main proposal-list-fast
In tags
v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
8 atoms, 1 tree operation, 4 files

Browse the tree at this change

added scripts/changelog-check.sh +27 −0
1 +
#!/bin/sh
2 +
# The changelog check (2026-10-19): a commit that changes what users get
3 +
# adds a line under "## Unreleased" in CHANGELOG.md, so a release's notes
4 +
# are written as the work is done and versions say what changed.
5 +
#
6 +
#   scripts/changelog-check.sh [PATH...]
7 +
#
8 +
# PATHs are what the commit will take (as given to `oxy commit`); without
9 +
# any, the working copy's changes. Code is anything under src/, crates/,
10 +
# templates/, assets/, packaging/, images/ or scripts/, except tests.
11 +
# Docs, pipelines, pins (deps/) and lock files need no line. For a code
12 +
# change users won't notice (a refactor, a test helper), set NO_CHANGELOG=1.
13 +
set -eu
14 +
[ "${NO_CHANGELOG:-}" = 1 ] && exit 0
15 +
cd "$(dirname "$0")/.."
16 +
if [ "$#" -gt 0 ]; then
17 +
    paths=$(printf '%s\n' "$@")
18 +
else
19 +
    paths=$(oxy --plain status --json | python3 -c 'import json,sys; print("\n".join(p["path"] for p in json.load(sys.stdin)["paths"]))')
20 +
fi
21 +
code=$(printf '%s\n' "$paths" | grep -E '^(src|crates|templates|assets|packaging|images|scripts)/' | grep -vE '(^|/)tests?/|_test\.rs$' || true)
22 +
[ -z "$code" ] && exit 0
23 +
printf '%s\n' "$paths" | grep -qx 'CHANGELOG.md' && exit 0
24 +
echo "changelog: these changes touch code but CHANGELOG.md has no new line under ## Unreleased:" >&2
25 +
printf '%s\n' "$code" | sed 's/^/  /' >&2
26 +
echo "add one (Added / Changed / Fixed / Security, with the issue number), or set NO_CHANGELOG=1 if users won't notice" >&2
27 +
exit 1
modified CHANGELOG.md +42 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **Land when checks pass, or land now over them (#261):** anyone who may land a proposal can set it to land by itself, on the proposal, through `POST`/`DELETE …/proposals/{n}/auto-land` or with `oh proposal land N --when-ready` (`--off` to clear it). Once every required check has passed and nothing else blocks it, it lands as the person who set it, if they may still land. A required check that fails or is cancelled turns it off with a note on the timeline; a push to the source makes it wait again. When required checks are the only thing in the way (pending, or failed with the runners down), a maintainer may land now with a reason: on the proposal, with `{"over_checks": "reason"}` on the API's land, or `oh proposal land N --over-checks REASON`. Only the checks are passed over; approvals, code owners, requested changes, drafts, dependencies and the trust policy still apply. Both are audited, and the proposal, its timeline and the pushes page say how it landed.
8 +
- **The repository bar has an Artifacts menu and a More menu (#254):** the bar is now Code, Changes, Proposals, Issues, Flow, Releases, Wiki, Artifacts, More and Settings, and no longer wraps. Artifacts holds Releases, Build artifacts (every run's outputs in one list, new), Images (the runner images the repository's jobs used, new), Terraform state and Reports; More holds Bookmarks, Tags, Diffusion, Trust, Pushes and Conflicts. Projects leaves the bar (it is on the site band and every profile). The menus work without JavaScript.
9 +
- **The hub keeps Terraform and OpenTofu state (#252, #255):** an HTTP backend per repository at `/api/v1/repos/OWNER/NAME/terraform/state/STATE`, with locking. A job uses it with an identity token for `{base}/terraform` or its job token, for its own repository only; people use a token of theirs and their role. State is sealed at rest and its last 20 versions are kept; backups carry it. The Terraform state page (under Artifacts) lists each state's versions; writers download the current state or any version as a `.tfstate` file, and maintainers restore one. A restore writes the version again with a serial above the current one, since OpenTofu and Terraform only take a state forward, and keeps the state it replaces as a version, so nothing is lost. Refused while the state is locked, never by a job, and audited.
10 +
- **Insights (#257):** OxygenFlow's charts for the site (`/admin/insights`, filtered by repository, owner or runner), an owner (`/insights/{owner}`) and a repository (`/{owner}/{name}/insights`): pickup time (median and p95), jobs finished and pass rate, duration, queued and running, runners online and slots busy, a runners table and the busiest repositories, over 1 hour, 24 hours or 7 days. The page refreshes itself every 30 seconds, and `?wall=1` is a view without menus for a second monitor. `[server] grafana_url` (`OXYHUB_SERVER_GRAFANA_URL`) adds an Open in Grafana link. `log`, `metrics` and `insights` are now reserved names.
11 +
- **Prometheus metrics (#212):** `/metrics` serves Prometheus text to a bearer of `[server] metrics_token` (`OXYHUB_SERVER_METRICS_TOKEN`); without one it is 404. It reports the build, uptime, memory, the state cache, bucket requests by kind, HTTP requests by route class with latency histograms, protocol bytes by endpoint, the database pool, pushes accepted and refused, Flow jobs and runs, runners' last heartbeats, backups, mail sent and failed, and the transparency log's size.
12 +
- **A transparency log (#238):** the hub logs every signed object it accepts (reference and tag attestations, seals, the policy, its acceptances, short-lived keys' certificates, kept receipts and sealed approvals) in one append-only Merkle tree, and signs a checkpoint every 5 seconds while entries arrive. The log is served as C2SP tlog-tiles under `/log/`, with a `/log` page. Every refs answer carries the latest checkpoint and each bookmark's entry with its inclusion proof, so oxy checks them on fetch. Hubs can witness each other's logs: `[tlog] witnesses` and `witness_for` in `hub.toml` (none by default, so nothing changes until set).
13 +
- **Trusted time (#240):** the hub keeps each cosigned checkpoint, lists its witnesses at `GET /log/witnesses` and in every refs answer, and serves for each logged bookmark the first cosigned checkpoints that hold its entry, from which oxy takes the integrated time: when a witness first saw it, by the witness's clock.
14 +
- **Agents act for their owner with no key (#241, #253):** an owner makes a grant with `oh agent grant NAME [--scope] [--for 30d]`, signed with their own oxy key; the hub keeps it and answers with the agent's token, shown once. The agent exchanges it at `POST /api/v1/keyless/certify` for ten-minute certificates, so its fresh keys' commits count in the owner's name, and the same token works for fetch, push and the API as an agent's token of the owner (writing when the grant allows). What the agent does is marked as its own and its owner is told. `oh agent list` and `oh agent revoke` manage grants, and a revoked grant gets no more certificates. When a push moves a bookmark with a short-lived key, the hub countersigns it with its own clock. Runners' receipts signed with keys this hub certified are kept, and a release's trust report counts them as known signers.
15 +
- **Approval gates (oxygenflow#57, #235):** a job whose pipeline asks for approval waits until someone with the role it names approves it on the run page, through the API, or with `oh run approve N --job NAME` (`oh run reject` to refuse). Everyone who may decide is notified once the job is ready. A rejected or expired job ends cancelled and the jobs after it are skipped. A gate that asks for a sealed approval (the default for a job with an environment) is released only by a signature of the approver's own key (`oxy seal --flow`), kept in the repository and named in the job's identity tokens.
16 +
- **Environments and deployments (oxygenflow#59):** a job's identity tokens name its environment only once the job is approved. `/{owner}/{name}/deployments` and `GET /api/v1/repos/{o}/{n}/deployments` show what each environment runs, who approved it, and the history.
17 +
- **Artifacts between jobs, and allow-listed egress (oxygenflow#58, oxygenflow#55):** a job's inputs come from the artifacts of a job it depends on in the same run, served to the runner holding it. A sandbox with `network "egress"` goes only to runners offering egress.
18 +
- **Flow priorities (#244):** runners take queued jobs by level first (urgent, high, normal, low), then by turn, so repositories take turns instead of one repository's burst holding everyone. The level comes from the run when expedited, else the repository (Settings › OxygenFlow), else the organisation, else the site. A maintainer can expedite a queued or running run to urgent with a reason; at most 2 expedited runs per owner go at once unless Admin › Runners › Settings says otherwise. A pipeline may lower its own jobs with `priority "low"`, never raise them.
19 +
- **Runners show Connectivity and Status apart (#243):** Admin › Runners shows whether a runner is online (when the hub last heard from it) apart from what it is doing (disabled, draining, idle, or running N of M). A busy runner sends a heartbeat, so a quiet job no longer shows it offline. The queue filters by repository, status and runner, and shows when each job started.
20 +
- **Why a job waits (#259):** a job some runner could take now says why it still waits: which of those runners are busy (`earth-1 busy (3 of 3)`) or lack the memory or disk for it now, and how many ready jobs come first.
21 +
- **Pages update themselves (#203–#207):** the proposal page's timeline and Landing section, the notification bell, the inbox, the dashboard, the issue page, the repository overview and Diffusion refresh in place when something changes, over one `/live` stream per page. Checks keep their width and refresh in place instead of reloading the whole page (#232).
22 +
- **Signing in from an invitation with GitHub, GitLab or Google (#214):** the registration page shows the providers' buttons for a usable invitation, and the account is made only with an unused code.
23 +
- **`git clone` on the Code panel (#215)** when the repository's Git mirror is on, below `oxy clone`.
24 +
- **`oh issue comment -F FILE` (#260):** takes the comment from a file, or from standard input with `-F -`, as other `oh` commands with a body do, and refuses an empty comment.
25 +
- **PostgreSQL (#211, #242):** the hub can run on PostgreSQL 16 (or 15) when built with `--features postgres`, set by `[database] url` (`OXYHUB_DATABASE_URL`). `oxygenhub admin copy-db --from <old>/hub.db` moves a stopped SQLite hub's records into an empty Postgres database (`--replace` empties a fresh one first). Releases include `oxygenhub-server-VERSION-linux-x86_64.tar.gz`, built for PostgreSQL. See docs/postgres.md.
26 +
- **SLSA provenance for build artifacts (#239):** an artifact of a job whose signed receipt the hub kept has `…/provenance`, an in-toto SLSA provenance v1 statement in a DSSE envelope signed with the hub's key; release assets link it (`built_by.provenance_url` in the API). `cosign verify-blob-attestation --key oxyhub-signing.pem --type slsaprovenance1 --insecure-ignore-tlog --signature PROVENANCE ASSET` checks a download, with the key from `/.well-known/oxyhub-signing.pem`.
27 +
- **Assistants stream their answers (#248):** the time limit is now for silence, not the whole answer, so a large proposal no longer times out with nothing. Drafts fill the editor as they are written. The assistant is told every changed file before the diff, and which files it could not see; a provider that refuses a prompt as too long is asked once more with less.
28 +
- **Agent tokens decide approval gates only where the repository allows it (#258, #229):** a gate exists so a person decides. A decision through an agent's token (an agent-marked token or an agent's grant) is refused, saying why, unless Settings › OxygenFlow lets agents decide the repository's gates (off by default). Where allowed, the decision names the agent ("approved by carol via claude-code"). A sealed approval is a person's own key and is not affected.
29 +
- **A landed proposal's runs that have not started are cancelled (#263):** they can no longer change anything and held the queue ahead of the target's own run. Each such job says "moot: proposal #N landed"; runs already going finish.
7 30
- **Newest run wins (#267):** a new push run cancels the older push runs of its bookmark still going, as before, and now a proposal's new run cancels that proposal's older runs too, so `proposal/N` runs no longer pile up three or four deep. Each cancelled job says which run superseded it. Runs started by hand, by a schedule, by a tag or as a re-run are never cancelled this way. Settings › OxygenFlow › Newest run wins lets maintainers keep superseded runs that are already running, cancelling only the queued ones.
8 31
- **Flow on your own runners (#225):** a repository's maintainers can turn OxygenFlow on for it themselves once it (or its owner) has a runner of its own: Settings › OxygenFlow › "on, own runners only". Its jobs then go only to those runners, never to the site's; letting the site's runners take them stays a site administrator's choice.
32 +
- **Readers never wait for a push or gc (#249):** pages, the API and fetches stalled for up to 43 s while a repository's gc or a push held its lock. With oxy's store now safe to read alongside a writer, reads take only a shared lock that deleting or moving a repository holds alone; pushes, gc, landings, web edits and the rest still take turns as writers.
33 +
- **Repositories kept in a bucket answer quickly (#228, #234):** the hub shares one client per bucket and credentials, keeps ranges of large packs on disk, looks in packs first, remembers a missing key for 30 s and lists packs once a while. The nightly gc remembers the day it ran across restarts, so a deploy during `gc_hour` no longer runs it again, and a store that is already one pack is left alone. `/health` shows bucket requests by kind.
34 +
- **Restarts drain (#233):** on SIGTERM the hub stops taking connections, ends live streams (browsers reconnect) and gives requests in flight up to 30 s, then stops within a second. It can take its socket from systemd (socket activation), so requests that arrive during a restart wait instead of failing: install `oxygenhub.socket` and the service changes in docs/deploy.md.
35 +
- **Diffusion mends itself (#210):** every 10 minutes the pin index is compared with each followed bookmark and re-indexed where it differs, and a consumer left behind with no open bump is caught up. A failed index write says which step failed and is tried again.
36 +
- **The state cache counts what states really cost (#246):** a loaded state was weighed at a quarter of its real size, so the hub held far more memory than its 3 GiB budget. It now weighs states by what they take, so fewer stay cached; raise `OXYHUB_STATE_CACHE_MB` if large repositories miss it too often.
37 +
- **Sessions are kept on the server (#223):** the cookie holds a random id, and a session ends after 14 days unused or 30 days in all. Settings › Security lists your signed-in browsers with Sign out on each. Old cookies no longer work, so everyone signs in once more.
38 +
- **A new hub gives administrator only to whoever brings its setup code (#245):** on a hub with no accounts the first registration made an administrator with no invitation, whatever the registration mode. The hub now writes a one-time setup code to `<data>/setup-code` (owner-only) and logs where it is, or takes `[registration] setup_code` (`OXYHUB_REGISTRATION_SETUP_CODE`); the first registration must bring it. A sign-in provider never makes the first account. `oxygenhub admin user create --admin` needs no code.
39 +
- **Terraform state needs the write role to read (#256):** every account has the Read role on public and internal repositories, so any account's `repo:read` token could read their Terraform state, which often holds secrets. Reading a state now needs a `repo:write` token and the write role; the repository's jobs keep their access.
40 +
- **HTTPS only (#218):** on a site whose `base_url` is https, a request that a proxy reports came over plain http is redirected (GET, HEAD) or refused (anything else), so a form or password is never taken over http. Every cookie is Secure on an https site.
41 +
- **Requests out only to public addresses (#219):** webhooks and assistants refuse a host that resolves to a loopback, private, link-local (the cloud metadata address among them) or other local address, pin the connection to the checked addresses and do not follow redirects. A site administrator who wants a local service, such as a local Ollama, must allow it with `[outbound] allow` in `hub.toml`.
42 +
- **Webhooks read at most 64 KiB of an answer (#220),** and a hook with 1000 deliveries waiting takes no more until it catches up.
43 +
- **Guessing is slowed (#221):** failed sign-ins, two-factor codes, device codes and reset requests count per account (5 free) and per address (20 free), then each doubles the wait, from 2 seconds to a 15-minute lockout; the page says when to try again.
44 +
- **Security headers (#222, #227, #231):** every response is sent with `nosniff`, a referrer policy and a permissions policy, and pages with `X-Frame-Options: DENY` and a report-only Content-Security-Policy, with no inline scripts left in the pages. `Strict-Transport-Security` is sent on https sites with the max-age from `[server] hsts_max_age` (`OXYHUB_SERVER_HSTS_MAX_AGE`); none at 0, the default.
45 +
- **security.txt and a security contact page (#224, #230):** once an administrator sets a contact under Administration › Security contact, `/.well-known/security.txt` and `/security-contact` show it.
46 +
- **Fetches no longer fail on a bookmark moved by a key that signed nothing (#266, #271):** a delegate or short-lived key that moved a bookmark without signing any of its changes left every client, runners included, failing the whole fetch with "unknown key". Fetches now carry the identities of the keys that signed every bookmark's and tag's latest attestation, so any client can verify the move. The fix first made every fetch of a repository with many bookmarks in a bucket slow (14 s for one with 145); the signers are now gathered once per change to the repository.
47 +
- **A renewed key approves as the account its predecessor is linked to (#236):** a sealed approval signed by a renewed or delegated key with the same key pair was refused because the new identity was not linked. The hub now follows the key's earlier identities to a linked one, as pushes already did.
48 +
- **Closing keywords close every issue they list (#226):** `Closes #1, #2 and #3`, `Fixes: #4 #5` and `resolves #6 & #7` close every number, not only the first.
49 +
- **Long hashes in change titles show their two ends (#216),** the whole in a tip, and change rows wrap rather than grow wider than their card.
50 +
- **A minified file is shown plain at once:** a file with a line over 4,000 bytes was highlighted for seconds and is now shown as plain text, as other forges do.
9 51
10 52
## 0.5.2 (2026-10-15)
11 53
…
202 unchanged lines
modified Makefile +5 −1
…
2 unchanged lines
3 3
build:
4 4
	cargo build --release
5 5
6 -
check: fmt clippy test
6 +
check: changelog fmt clippy test
7 7
8 +
# A code change needs a CHANGELOG.md line (scripts/changelog-check.sh).
9 +
changelog:
10 +
	scripts/changelog-check.sh
11 +
8 12
fmt:
9 13
	cargo fmt --all -- --check
10 14
…
36 unchanged lines
modified README.md +3 −1
…
72 unchanged lines
73 73
74 74
## Developing
75 75
76 -
`make check` runs formatting, clippy with warnings as errors, and the tests.
76 +
`make check` runs the changelog check, formatting, clippy with warnings as errors, and the tests.
77 +
**Changelog:** a commit that changes code adds a line under `## Unreleased` in CHANGELOG.md (Added, Changed, Fixed or Security, with the issue number); versions change only at release. `scripts/changelog-check.sh [PATH...]` checks it for the paths a commit takes (the working copy's changes without any) and runs in the gate; `NO_CHANGELOG=1` for code changes users won't notice.
78 +
77 79
The tests use a real `oxy-core` client against a hub on a loopback port and
78 80
never touch your own Oxygen configuration.
79 81
…
7 unchanged lines