OXYGENHUB

A skip marker quoted in code does not skip (refs #284) 3b3a51afde59

A change message that mentions a skip marker between backticks or in a
fenced block, documenting it say, starts its run as usual; only a marker
in the text itself skips it.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 0b52438ba53a
When
· 2026-10-21 00:09 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
3b3a51afde594619738050b8c6e57cf751fa53c87ecb2e6bfb5739d53cc94d30
Depends on
367f34b8e590 42fa95540468 737de0fac722 93be9ce4653e a7e6e8e6094c a9f1c0bf403a b5668108487f
Depended on by
5588bb3631d2 9041db4e4d92
In bookmarks
api-unknown-fields diffusion-pin-bookmark draft-ready main proposal-list-fast
In tags
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, 0 tree operations, 4 files

Browse the tree at this change

modified CHANGELOG.md +1 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **A skip marker quoted in code does not skip:** a change message that mentions `[skip ci]` between backticks or in a fenced block (documenting the marker, say) now starts its run as usual; only a marker in the text itself skips it.
7 8
- **Administration has a sidebar, one page per section (#288):** the admin area was one long page plus separate pages for runners, images, Diffusion, backups and storage. Every page now has the same sidebar, grouped as Overview, People (accounts, registration and invitations, organisations), Sign-in (providers, security contact), Mail, Flow (runners, images, runner settings, insights, variables, secrets), Integrations (AI assistants, Diffusion), Data (storage and maintenance, backups) and Audit, each at its own address (`/admin/mail`, `/admin/audit`, …). `/admin` is an overview of the site's numbers. Forms keep their addresses and answer with their own page, and a kept `/admin#mail` link opens the Mail page.
8 9
- **Help: AI identities (#287):** a section on letting an AI agent work for you: keyless agents (a grant with `oh agent grant`, an agent home with `oxy auth grant`, ten-minute certified keys, nothing secret on disk), how its work shows (`via agent:NAME@YOU`), admitting it to a repository's trust policy (`oxy policy grant-subject`), agents with keys of their own (delegation), agent-marked tokens, approval gates, and revoking. "Letting the hub act for you" now says to export your identity again after delegating and upload that.
9 10
- **Settings › Keys says when each key was last used (#286):** like tokens, each linked key shows when it was linked, when the hub last accepted a push it signed (a change or a seal; "never" until then) and when the identity expires.
…
271 unchanged lines
modified crates/hub/src/flow.rs +33 −2
…
63 unchanged lines
64 64
/// spell them; matched in any case.
65 65
pub const SKIP_MARKERS: [&str; 3] = ["[skip flow]", "[skip ci]", "[ci skip]"];
66 66
67 -
/// The marker in `message` that asks for no run, if any.
67 +
/// The marker in `message` that asks for no run, if any. A marker quoted
68 +
/// in code (between backticks, or in a fenced block) only mentions one
69 +
/// and does not count.
68 70
pub fn skip_marker(message: &str) -> Option<&'static str> {
69 -
    let m = message.to_lowercase();
71 +
    let m = outside_code(message).to_lowercase();
70 72
    SKIP_MARKERS.into_iter().find(|k| m.contains(k))
71 73
}
72 74
75 +
/// `text` without its fenced blocks (lines between ``` fences) and inline
76 +
/// code spans (between backticks).
77 +
fn outside_code(text: &str) -> String {
78 +
    let mut out = String::new();
79 +
    let mut fenced = false;
80 +
    for line in text.lines() {
81 +
        if line.trim_start().starts_with("```") {
82 +
            fenced = !fenced;
83 +
            continue;
84 +
        }
85 +
        if fenced {
86 +
            continue;
87 +
        }
88 +
        // Inline spans: every other backtick-separated piece is code; an
89 +
        // unmatched backtick leaves the rest as text.
90 +
        let pieces: Vec<&str> = line.split('`').collect();
91 +
        let closed = pieces.len() % 2 == 1;
92 +
        for (i, piece) in pieces.iter().enumerate() {
93 +
            let in_code = i % 2 == 1 && (closed || i + 1 < pieces.len());
94 +
            if !in_code {
95 +
                out.push_str(piece);
96 +
                out.push(' ');
97 +
            }
98 +
        }
99 +
        out.push('\n');
100 +
    }
101 +
    out
102 +
}
103 +
73 104
/// Whether the repository lets a push to `bookmark` skip its run: `any`
74 105
/// bookmark (the default), `others` than the default one, or `never`.
75 106
pub fn skip_allowed(row: &db::repos::RepoRow, bookmark: &str) -> bool {
…
2597 unchanged lines
modified crates/hub/tests/flow.rs +5 −0
…
1898 unchanged lines
1899 1899
    assert_eq!(skip_marker("[CI SKIP] docs"), Some("[ci skip]"));
1900 1900
    assert_eq!(skip_marker("Docs\n\n[Skip CI]"), Some("[skip ci]"));
1901 1901
    assert_eq!(skip_marker("skip ci without brackets"), None);
1902 +
    // Mentioned in code, not asked for (F1 of #284's follow-up).
1903 +
    assert_eq!(skip_marker("Document the `[skip flow]` marker"), None);
1904 +
    assert_eq!(skip_marker("Example:\n\n```\ngit commit -m 'x [skip ci]'\n```\n"), None);
1905 +
    assert_eq!(skip_marker("`code` then [skip ci]"), Some("[skip ci]"));
1906 +
    assert_eq!(skip_marker("an odd ` backtick [skip ci]"), Some("[skip ci]"));
1902 1907
}
1903 1908
1904 1909
/// oxygenflow#32: a run still going is cancelled from the API, the web or
…
4301 unchanged lines
modified docs/spec.md +1 −1
…
517 unchanged lines
518 518
  Runs only ever take committed states already in the repository, never uncommitted work. A run writes no history: its receipts are signed statements about a state (Oxygen spec §14.6). Running a state again adds another statement. The environment hash shows whether it ran in the same environment, and a matching result is independent confirmation. The new run's checks replace the old ones for that state, and the run page says what started it (pushed by, started by, re-run of #N by).
519 519
- **Runs and jobs.** A run holds the repository, bookmark, state hash and heads, pipeline hash and pusher; one job per pipeline job with its compiled form, sandbox and dependencies. Job states: `queued`, `running`, `passed`, `failed`, `skipped` (a dependency did not pass), `error` (the runner could not run it). A run is `running` while any job is, then `passed` when every job passed, else `failed`.
520 520
- **Newest run wins** (oxygenhub#267). A new run cancels the older runs of the same thing still live, so runs do not pile up behind states that have moved on: a push run cancels the older push runs of its bookmark (Diffusion's bumps start push runs), and a proposal's run (bookmark `proposal/N`, started when its target moves) the older runs of that proposal. Each cancelled job says `superseded by run #N`, and the next push run's scope reaches back past what was cancelled. A proposal's run leaves its source bookmark's push runs alone: they test the source head, another state, whose checks the bookmark and other proposals may want. Tag, manual, scheduled and re-run runs were each asked for in their own right, so they supersede nothing and nothing supersedes them; they are cancelled by hand. A repository's maintainers choose under Settings › OxygenFlow › Newest run wins whether superseded runs already running are cancelled too (the default) or finish, with only the queued ones cancelled (`repositories.flow_keep_running`, audited as `repo.flow_keep_running`).
521 -
- **Pushing without a run** (oxygenhub#284). A push whose newest change (by its clock) has `[skip flow]`, `[skip ci]` or `[ci skip]` in its message, in any case, starts no jobs: the hub records a run with status `skipped`, finished at once, whose note names the marker, the change and who pushed it, so the run list shows the gap. The marker is in a signed message, so who skipped is verifiable. A skipped run is never a later run's baseline: the next push run's scope reaches back past it, so the changes it brought are still tested. A pushed tag always runs. A repository's maintainers choose under Settings › Runs where skipping is allowed: on any bookmark (the default), on any but the default bookmark, or never (`repositories.flow_skip`: `any`, `others`, `never`; audited as `repo.flow_skip`).
521 +
- **Pushing without a run** (oxygenhub#284). A push whose newest change (by its clock) has `[skip flow]`, `[skip ci]` or `[ci skip]` in its message, in any case and outside code (a marker between backticks or in a fenced block only mentions one), starts no jobs: the hub records a run with status `skipped`, finished at once, whose note names the marker, the change and who pushed it, so the run list shows the gap. The marker is in a signed message, so who skipped is verifiable. A skipped run is never a later run's baseline: the next push run's scope reaches back past it, so the changes it brought are still tested. A pushed tag always runs. A repository's maintainers choose under Settings › Runs where skipping is allowed: on any bookmark (the default), on any but the default bookmark, or never (`repositories.flow_skip`: `any`, `others`, `never`; audited as `repo.flow_skip`).
522 522
- **Runner API** (runner token): `POST /api/v1/flow/claim` hands out the oldest queued job whose dependencies passed and whose tier the runner offers, marking it `running`, with a **job token**; `204` when there is none. `POST /api/v1/flow/jobs/{id}/log` appends log records (text cut at 16 KiB, at most 50 000 per job); `POST /api/v1/flow/jobs/{id}/finish` records the exit code and receipt (the hub recomputes the receipt hash) and settles dependents. Only the runner holding the job may report on it.
523 523
- **Runners** (Admin → Runners, site administrators).
524 524
  - A runner's page has a Settings form (#98): labels an administrator gives it beside those it reports (a job asking for a label matches either), and a job time limit in minutes below the site's, applied when it claims.
…
363 unchanged lines