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 |