OXYGENHUB

Land now, over the checks 2a8b621676b4

When required checks are the only thing in the way (pending, or failed,
say with the runners down and the fix in the proposal itself), a
maintainer may land now with a reason: on the proposal (behind a toggle,
with a reason field and a confirmation), through the API's land with
{"over_checks": "reason"}, or oh proposal land N --over-checks REASON.
Only the checks are passed over; approvals, code owners, changes
requested, drafts, dependencies and the trust policy still apply. A
writer is refused, as is an empty reason. How it landed says so ('web,
over checks: the runners are down') on the proposal, its timeline and
the pushes page, and it is audited (proposal.land_over_checks).

Refs #261
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 221625556d3e
When
· 2026-10-18 18:39 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
2a8b621676b42cbc143f985eb3226e83d515504dda995db118836ab6beb672f6
Depends on
0b20f1ec4007 1f1eb06a9009 2291f8cc9c96 42fa95540468 4866be1c9040 5c6e92fd8d71 62ceb2eec18c 71e65fcf95ab 93be9ce4653e a1b9869565aa acc85ac0f70b b64e2537d702 cc1cd197df1d d23464c0ac60 de505df5cf8d e97500b70f9a f4d6ef418a96 f677d6270298 f7bcee49bf24
Depended on by
217814c239f0 2abea30809dd 86a69b53aff0
In bookmarks
api-unknown-fields cancel-landed-runs 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
34 atoms, 0 tree operations, 9 files

Browse the tree at this change

modified crates/hub/src/api/mod.rs +15 −2
…
1124 unchanged lines
1125 1125
1126 1126
/// `POST /repos/{o}/{n}/proposals/{number}/land` (repo:write): land it as
1127 1127
/// the page's Land button does; `409` with the blockers when it cannot.
1128 +
/// `{"over_checks": "reason"}` lands over required checks that have not
1129 +
/// passed, for maintainers (#261).
1128 1130
async fn land_proposal(
1129 1131
    State(app): State<Shared>,
1130 1132
    headers: HeaderMap,
1131 1133
    Path((owner, name, number)): Path<(String, String, i64)>,
1134 +
    body: axum::body::Bytes,
1132 1135
) -> Result<Json<Proposal>, ApiError> {
1133 1136
    let c = caller(&app, &headers).await?;
1134 1137
    let me = c.require("repo:write")?.clone();
1135 -
    let (repo, row, _) = reach(&app, &c, &owner, &name, Access::Write).await?;
1138 +
    let over_checks: Option<String> = if body.iter().all(u8::is_ascii_whitespace) {
1139 +
        None
1140 +
    } else {
1141 +
        serde_json::from_slice::<serde_json::Value>(&body)
1142 +
            .map_err(|_| ApiError(StatusCode::BAD_REQUEST, "the body is JSON: {} or {\"over_checks\": \"reason\"}".into()))?
1143 +
            .get("over_checks")
1144 +
            .and_then(|v| v.as_str())
1145 +
            .map(String::from)
1146 +
    };
1147 +
    let need = if over_checks.is_some() { Access::Maintain } else { Access::Write };
1148 +
    let (repo, row, _) = reach(&app, &c, &owner, &name, need).await?;
1136 1149
    let via = format!("api (token {})", c.token);
1137 -
    match crate::web::proposals::land_as(&app, &repo, &row, &me, number, &via, false).await {
1150 +
    match crate::web::proposals::land_as(&app, &repo, &row, &me, number, &via, false, over_checks.as_deref()).await {
1138 1151
        Ok(_) => proposal(State(app), headers, Path((owner, name, number))).await,
1139 1152
        Err(crate::web::proposals::Landing::NotFound) => Err(not_found("no such proposal")),
1140 1153
        Err(crate::web::proposals::Landing::Refused(m)) => Err(ApiError(StatusCode::CONFLICT, m)),
…
1560 unchanged lines
modified crates/hub/src/diffusion.rs +1 −1
…
1232 unchanged lines
1233 1233
    }
1234 1234
    let me = account(&app.db).await.ok_or_else(|| Held::Refused("the diffusion account is missing".into()))?;
1235 1235
    let via = "Diffusion, both halves green";
1236 -
    match crate::web::proposals::land_as(app, &cname, consumer, &me, bump.number, via, true).await {
1236 +
    match crate::web::proposals::land_as(app, &cname, consumer, &me, bump.number, via, true, None).await {
1237 1237
        Ok(_) => {
1238 1238
            tracing::info!("diffusion: {}/{} landed #{} itself (both halves green)", consumer.owner, consumer.name, bump.number);
1239 1239
            Ok(bump.number)
…
322 unchanged lines
modified crates/hub/src/web/proposals.rs +52 −10
…
570 unchanged lines
571 571
    pub reviews: Vec<ReviewView>,
572 572
    pub approvals: Vec<Approval>,
573 573
    pub blockers: Vec<String>,
574 +
    /// The required-checks blocker, also in `blockers`: the one a
575 +
    /// maintainer may land over with a reason (#261).
576 +
    pub checks_blocker: Option<String>,
574 577
    pub depends_on: Vec<i64>,
575 578
    pub requirements: pdb::Requirements,
576 579
}
…
28 unchanged lines
605 608
    let mut blockers = Vec::new();
606 609
    // Only an open proposal can be held back.
607 610
    if !loaded.row.is_open() {
608 -
        return Ok(Verdict { reviews, approvals, blockers, depends_on: vec![], requirements });
611 +
        return Ok(Verdict { reviews, approvals, blockers, checks_blocker: None, depends_on: vec![], requirements });
609 612
    }
610 613
    if loaded.row.state == "draft" {
611 614
        blockers.push("the proposal is a draft".into());
…
14 unchanged lines
626 629
    if loaded.conflicted && !requirements.allow_conflicts {
627 630
        blockers.push("landing would carry conflicts, which this repository refuses".into());
628 631
    }
632 +
    let mut checks_blocker = None;
629 633
    let required = db::hooks::required_checks(&app.db, repo_id).await?;
630 634
    if !required.is_empty() {
631 635
        let checks = db::hooks::checks_for_state(&app.db, repo_id, &loaded.state).await?;
632 636
        let missing: Vec<String> =
633 637
            required.iter().filter(|n| !checks.iter().any(|c| &c.name == *n && c.status == "passed")).cloned().collect();
634 638
        if !missing.is_empty() {
635 -
            blockers.push(format!("required checks not passed on the projected state: {}", missing.join(", ")));
639 +
            let b = format!("required checks not passed on the projected state: {}", missing.join(", "));
640 +
            checks_blocker = Some(b.clone());
641 +
            blockers.push(b);
636 642
        }
637 643
    }
638 644
    let requesting: Vec<&String> = latest.iter().filter(|(_, r)| r.verdict == "changes").map(|(who, _)| who).collect();
…
14 unchanged lines
653 659
    if !depends_on.is_empty() {
654 660
        blockers.push(format!("depends on {}", depends_on.iter().map(|n| format!("#{n}")).collect::<Vec<_>>().join(", ")));
655 661
    }
656 -
    Ok(Verdict { reviews, approvals, blockers, depends_on, requirements })
662 +
    Ok(Verdict { reviews, approvals, blockers, checks_blocker, depends_on, requirements })
657 663
}
658 664
659 665
#[derive(Template)]
…
43 unchanged lines
703 709
    /// still there, and the viewer may delete it (oxygenhub#86).
704 710
    deletable_source: Option<String>,
705 711
    can_land: bool,
712 +
    can_land_over_checks: bool,
706 713
    hub_can_land: bool,
707 714
    notice: Option<String>,
708 715
    error: Option<String>,
…
102 unchanged lines
811 818
            None
812 819
        };
813 820
    let can_land = role >= Role::Write && p.is_open() && v.blockers.is_empty();
821 +
    // Checks alone in the way: a maintainer may land over them (#261).
822 +
    let can_land_over_checks = role >= Role::Maintain && p.is_open() && v.checks_blocker.is_some() && v.blockers.len() == 1;
814 823
    let can_request = p.is_open() && me.is_some_and(|a| Some(a.id) == p.author_id || role >= Role::Write);
815 824
    let requested = pdb::review_requests(&app.db, p.id).await.unwrap_or_default();
816 825
    let mut reviewable: Vec<String> = if can_request {
…
79 unchanged lines
896 905
        can_edit,
897 906
        deletable_source,
898 907
        can_land,
908 +
        can_land_over_checks,
899 909
        hub_can_land,
900 910
        notice,
901 911
        error,
…
252 unchanged lines
1154 1164
1155 1165
/// Land from the hub: only when the repository has no trust policy (the
1156 1166
/// hub never signs a reference update the policy would have to admit).
1167 +
#[derive(serde::Deserialize)]
1168 +
pub struct LandForm {
1169 +
    #[serde(rename = "_csrf")]
1170 +
    csrf: String,
1171 +
    /// Why it lands over its required checks (#261), for maintainers.
1172 +
    #[serde(default)]
1173 +
    over_checks: Option<String>,
1174 +
}
1175 +
1157 1176
pub async fn land(
1158 1177
    State(app): State<Shared>,
1159 1178
    viewer: Viewer,
1160 1179
    Path((owner, name, number)): Path<(String, String, i64)>,
1161 -
    Form(f): Form<super::session::CsrfForm>,
1180 +
    Form(f): Form<LandForm>,
1162 1181
) -> Result<Response, Error> {
1163 1182
    let shell = Shell::new(&app, &format!("/{owner}/{name}/proposal/{number}"), &viewer);
1164 1183
    let me = require(&viewer, &shell)?;
1165 1184
    csrf(&viewer, &f.csrf, &shell)?;
1166 -
    let (repo, row, _) = lookup_role(&app, &shell, &viewer, &owner, &name, Access::Write).await?;
1167 -
    match land_as(&app, &repo, &row, &me.account, number, "web", false).await {
1185 +
    let need = if f.over_checks.is_some() { Access::Maintain } else { Access::Write };
1186 +
    let (repo, row, _) = lookup_role(&app, &shell, &viewer, &owner, &name, need).await?;
1187 +
    match land_as(&app, &repo, &row, &me.account, number, "web", false, f.over_checks.as_deref()).await {
1168 1188
        Ok(_) => Ok(back(&owner, &name, number, "conversation", Some("Landed.".into()), None)),
1169 1189
        Err(Landing::NotFound) => Err(Error::not_found(shell)),
1170 1190
        Err(Landing::Refused(m)) => Ok(back(&owner, &name, number, "conversation", None, Some(m))),
…
11 unchanged lines
1182 1202
/// (attested by the hub key on `me`'s behalf when they delegated to it),
1183 1203
/// then records, announces and audits the landing, saying how it was done
1184 1204
/// (`via`: `web`, or `api (token NAME)`). `as_hub` signs with the hub's own
1185 -
/// key rather than `me`'s delegation (Diffusion's auto-land). Returns the
1186 -
/// new state.
1205 +
/// key rather than `me`'s delegation (Diffusion's auto-land).
1206 +
/// `over_checks`, a reason, lands over required checks that have not
1207 +
/// passed (#261): the caller has checked `me` maintains the repository;
1208 +
/// every other blocker still holds. Returns the new state.
1209 +
#[allow(clippy::too_many_arguments)]
1187 1210
pub async fn land_as(
1188 1211
    app: &Shared,
1189 1212
    repo: &RepoName,
…
2 unchanged lines
1192 1215
    number: i64,
1193 1216
    via: &str,
1194 1217
    as_hub: bool,
1218 +
    over_checks: Option<&str>,
1195 1219
) -> Result<String, Landing> {
1196 1220
    let (owner, name) = (row.owner.clone(), row.name.clone());
1197 1221
    let refused = |e: String| Landing::Refused(e);
…
2 unchanged lines
1200 1224
    let v = verdict(app, &loaded, row.id).await.map_err(|e| refused(e.to_string()))?;
1201 1225
    if !p.is_open() {
1202 1226
        return Err(refused(format!("Cannot land: the proposal is {}", p.state)));
1227 +
    }
1228 +
    let reason = over_checks.map(str::trim);
1229 +
    if reason == Some("") {
1230 +
        return Err(refused("Landing over checks needs a reason: say why it can't wait for them".into()));
1203 1231
    }
1204 -
    if !v.blockers.is_empty() {
1205 -
        return Err(refused(format!("Cannot land: {}", v.blockers.join("; "))));
1232 +
    let blocking: Vec<&String> = v.blockers.iter().filter(|b| !(reason.is_some() && v.checks_blocker.as_ref() == Some(*b))).collect();
1233 +
    if !blocking.is_empty() {
1234 +
        let list: Vec<&str> = blocking.iter().map(|b| b.as_str()).collect();
1235 +
        return Err(refused(format!("Cannot land: {}", list.join("; "))));
1206 1236
    }
1237 +
    // Only when the checks were in the way does the landing say so.
1238 +
    let over = reason.filter(|_| v.checks_blocker.is_some());
1239 +
    let via_owned = match over {
1240 +
        Some(r) => format!("{via}, over checks: {r}"),
1241 +
        None => via.to_string(),
1242 +
    };
1243 +
    let via = via_owned.as_str();
1207 1244
    let target = p.target.clone();
1208 1245
    let heads = loaded.heads.clone();
1209 1246
    // With a delegation the landing is attested by the hub key on the
…
48 unchanged lines
1258 1295
        super::issues::close_from_messages(app, row, &messages).await;
1259 1296
    }
1260 1297
    accounts::audit(&app.db, Some(me), "proposal.landed", &format!("{owner}/{name}#{number}"), &how).await.ok();
1298 +
    if let (Some(r), Some(checks)) = (over, &v.checks_blocker) {
1299 +
        accounts::audit(&app.db, Some(me), "proposal.land_over_checks", &format!("{owner}/{name}#{number}"), &format!("{r} ({checks})"))
1300 +
            .await
1301 +
            .ok();
1302 +
    }
1261 1303
    db::repos::record_push(
1262 1304
        &app.db,
1263 1305
        row.id,
…
663 unchanged lines
modified crates/hub/tests/flow.rs +1 −1
…
4249 unchanged lines
4250 4250
    .await
4251 4251
    .unwrap();
4252 4252
    let repo = RepoName::parse("andy/demo").unwrap();
4253 -
    let state = oxygenhub::web::proposals::land_as(&h.app, &repo, &row, &andy, 1, "web", false).await.ok().expect("landed");
4253 +
    let state = oxygenhub::web::proposals::land_as(&h.app, &repo, &row, &andy, 1, "web", false, None).await.ok().expect("landed");
4254 4254
    let mut runs = vec![];
4255 4255
    for _ in 0..100 {
4256 4256
        runs = db::flow::runs(&h.app.db, row.id, 10, None).await.unwrap();
…
1522 unchanged lines
modified crates/hub/tests/proposals.rs +71 −0
…
681 unchanged lines
682 682
    assert_eq!(state().await, "open");
683 683
}
684 684
685 +
/// Landing over checks (#261): with required checks not passed, a
686 +
/// maintainer may land now with a reason, recorded and audited; a writer
687 +
/// may not, nor anyone without a reason.
688 +
#[tokio::test(flavor = "multi_thread")]
689 +
async fn a_maintainer_lands_over_checks_with_a_reason() {
690 +
    let h = hub().await;
691 +
    let app = &h.app;
692 +
    let andy = register(app, "andy").await;
693 +
    let bob = register(app, "bob").await;
694 +
    let repo = RepoName::parse("andy/demo").unwrap();
695 +
    app.repos.create(&repo, None).unwrap();
696 +
    db::repos::create(&app.db, "andy", "demo", "", hub_api::Visibility::Public).await.unwrap();
697 +
    let row = db::repos::get(&app.db, "andy", "demo").await.unwrap().unwrap();
698 +
    let bob_id = db::accounts::account_by_name(&app.db, "bob").await.unwrap().unwrap().id;
699 +
    db::accounts::set_collaborator(&app.db, row.id, Some(bob_id), None, "write").await.unwrap();
700 +
    let work = tempfile::tempdir().unwrap();
701 +
    let mut client = Repo::init(work.path()).unwrap();
702 +
    client.create_identity("Andy McCall", "[email protected]", oxy_core::repo::IdentityScope::User).unwrap();
703 +
    client.config.set("remote.origin.url", &format!("{}/andy/demo", h.base)).unwrap();
704 +
    client.config.set("remote.origin.token", "site").unwrap();
705 +
    client.save_config().unwrap();
706 +
    write(work.path(), "a.txt", b"one\n");
707 +
    client.snapshot("t").unwrap();
708 +
    let base = client.commit("base", &[]).unwrap();
709 +
    assert!(client.push("origin", false).unwrap().iter().all(|r| r.ok));
710 +
    client.bookmark_create("feature", Some(&[base])).unwrap();
711 +
    client.bookmark_switch("feature", false).unwrap();
712 +
    write(work.path(), "b.txt", b"b\n");
713 +
    client.snapshot("t").unwrap();
714 +
    client.commit("the fix", &[]).unwrap();
715 +
    assert!(client.push_bookmark("origin", "feature", false).unwrap().iter().all(|r| r.ok));
716 +
    let preview = get(app, "/andy/demo/proposals/new?source=feature&target=main", Some(&andy)).await;
717 +
    let r = post(
718 +
        app,
719 +
        "/andy/demo/proposals/new",
720 +
        Some(&andy),
721 +
        &format!("_csrf={}&title=Fix&body=&target=main&source=feature", csrf_of(&preview.body)),
722 +
    )
723 +
    .await;
724 +
    assert_eq!(r.status, StatusCode::SEE_OTHER, "{}", r.body);
725 +
    db::hooks::set_required_checks(&app.db, row.id, &["flow/test".to_string()]).await.unwrap();
726 +
727 +
    // The checks block it; the maintainer is offered landing over them.
728 +
    let page = get(app, "/andy/demo/proposal/1", Some(&andy)).await;
729 +
    assert!(page.body.contains("required checks not passed") && page.body.contains("Land now, over the checks"), "{}", page.body);
730 +
    let csrf = csrf_of(&page.body);
731 +
    let r = act(app, "/andy/demo/proposal/1/land", Some(&andy), &format!("_csrf={csrf}")).await;
732 +
    assert!(r.body.contains("Cannot land: required checks not passed"), "plain land is refused: {}", r.body);
733 +
    assert_eq!(db::proposals::get(&app.db, row.id, 1).await.unwrap().unwrap().state, "open");
734 +
    // A writer is not offered it and cannot do it.
735 +
    let bob_page = get(app, "/andy/demo/proposal/1", Some(&bob)).await;
736 +
    assert!(!bob_page.body.contains("Land now, over the checks"));
737 +
    let r = post(app, "/andy/demo/proposal/1/land", Some(&bob), &format!("_csrf={}&over_checks=please", csrf_of(&bob_page.body))).await;
738 +
    assert_eq!(r.status, StatusCode::NOT_FOUND, "a writer cannot land over checks");
739 +
    assert_eq!(db::proposals::get(&app.db, row.id, 1).await.unwrap().unwrap().state, "open");
740 +
    // No reason, no landing.
741 +
    let r = act(app, "/andy/demo/proposal/1/land", Some(&andy), &format!("_csrf={csrf}&over_checks=+")).await;
742 +
    assert!(r.body.contains("needs a reason"), "{}", r.body);
743 +
    assert_eq!(db::proposals::get(&app.db, row.id, 1).await.unwrap().unwrap().state, "open");
744 +
    // With one: landed, and it says so.
745 +
    let r = act(app, "/andy/demo/proposal/1/land", Some(&andy), &format!("_csrf={csrf}&over_checks=the+runners+are+down")).await;
746 +
    assert!(r.body.contains("over checks: the runners are down"), "the landing says so: {}", r.body);
747 +
    let p = db::proposals::get(&app.db, row.id, 1).await.unwrap().unwrap();
748 +
    assert_eq!(p.state, "landed");
749 +
    assert!(p.landed_via.contains("over checks: the runners are down"), "{}", p.landed_via);
750 +
    let audit = db::accounts::audit_rows(&app.db, 20).await.unwrap();
751 +
    assert!(audit
752 +
        .iter()
753 +
        .any(|a| a.action == "proposal.land_over_checks" && a.detail.contains("the runners are down") && a.detail.contains("flow/test")));
754 +
}
755 +
685 756
/// Metal Gear 2's finding: a landed proposal's "Closes #N" closed nothing,
686 757
/// because only changes a push imports were read. Changes that arrive on
687 758
/// the default bookmark close issues however they get there.
…
173 unchanged lines
modified crates/oh/src/main.rs +13 −3
…
295 unchanged lines
296 296
    /// The checks on a proposal's projected state
297 297
    Checks { number: i64 },
298 298
    /// Land a proposal on its target, as the page's Land button does
299 -
    Land { number: i64 },
299 +
    Land {
300 +
        number: i64,
301 +
        /// Land before its required checks pass, saying why (maintainers;
302 +
        /// recorded and audited, #261)
303 +
        #[arg(long, value_name = "REASON")]
304 +
        over_checks: Option<String>,
305 +
    },
300 306
    /// Close a proposal without landing it, with an optional comment
301 307
    Close {
302 308
        number: i64,
…
840 unchanged lines
1143 1149
            c.send_text("POST", &format!("{base}/proposals/{number}/comments"), &NewComment { body, path: None, line: None })?;
1144 1150
            println!("{} commented on #{number}", ui::ok("✔"));
1145 1151
        }
1146 -
        ProposalCmd::Land { number } => {
1147 -
            let p: Proposal = c.send("POST", &format!("{base}/proposals/{number}/land"), &serde_json::json!({}))?;
1152 +
        ProposalCmd::Land { number, over_checks } => {
1153 +
            let body = match &over_checks {
1154 +
                Some(r) => serde_json::json!({ "over_checks": r }),
1155 +
                None => serde_json::json!({}),
1156 +
            };
1157 +
            let p: Proposal = c.send("POST", &format!("{base}/proposals/{number}/land"), &body)?;
1148 1158
            if json {
1149 1159
                println!("{}", serde_json::to_string(&p)?);
1150 1160
            } else {
…
597 unchanged lines
modified docs/spec.md +2 −0
…
396 unchanged lines
397 397
398 398
The landed state hash is recorded. Nothing is rewritten: the changes keep their hashes, so links to them stay valid and check results carry over. "Squash on land" is offered as a client-side option only, since it rewrites signed changes.
399 399
400 +
**Landing over checks** (oxygenhub#261). When required checks are the only thing in the way (pending or failed on the projected state, for instance with the runners down and the fix in this very proposal), a repository maintainer or admin may land now with a reason: *Land now, over the checks* on the proposal (a reason field, and a confirmation), `POST /repos/{owner}/{name}/proposals/{n}/land` with `{"over_checks": "REASON"}`, or `oh proposal land N --over-checks REASON`. Only the checks are passed over: approvals, code owners, changes requested, drafts, dependencies and the trust policy still apply, a writer is refused, and an empty reason is refused. How it landed says so ("web, over checks: the runners are down"), on the proposal, its timeline and the pushes page, and it is audited (`proposal.land_over_checks`, with the reason and the checks that had not passed).
401 +
400 402
### 7.7 Closing and reopening
401 403
402 404
`closed` keeps everything; the source bookmark may be deleted on close if the user asks. A proposal whose changes all appear in the target through another route is marked `landed` automatically with a note.
…
433 unchanged lines
modified templates/proposal.html +6 −0
…
194 unchanged lines
195 195
      {% if hub_can_land %}<form method="post" action="{{ repo.url }}/proposal/{{ p.number }}/land"{% if approvals.is_empty() %} onsubmit="return confirm('Land #{{ p.number }} without any approval?')"{% endif %}><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit" data-busy="LANDING">LAND</button></form>
196 196
      {% else %}<p class="small">This repository has a trust policy, so land from a client with your key:</p><p class="secret">oxy proposal land {{ shell.base_url }}{{ repo.url }}/proposal/{{ p.number }}</p>{% endif %}
197 197
    {% endif %}
198 +
    {% if can_land_over_checks && hub_can_land %}<details class="land-over"><summary class="small">Land now, over the checks</summary>
199 +
      <form method="post" action="{{ repo.url }}/proposal/{{ p.number }}/land" data-confirm="Land #{{ p.number }} before its required checks pass?"><input type="hidden" name="_csrf" value="{{ shell.csrf }}">
200 +
        <label for="over-checks" class="small">Why it can't wait for them (shown on the proposal, and audited)</label>
201 +
        <input type="text" id="over-checks" name="over_checks" required maxlength="300" placeholder="the runners are down; this is the fix">
202 +
        <button type="submit" class="danger-btn" data-busy="LANDING">LAND OVER CHECKS</button>
203 +
      </form></details>{% endif %}
198 204
  {% endif %}
199 205
  <!--/frag:landing--></section>
200 206
  {% if !closes.is_empty() %}<section class="side-sec"><div class="side-head"><h3>Closes</h3></div><ul class="plain small side-list">{% for c in closes %}<li><a href="{{ c.url }}" class="kind-issue">issue #{{ c.number }}</a> {{ c.title }} <span class="mute">{{ c.state }}</span></li>{% endfor %}</ul></section>{% endif %}
…
56 unchanged lines
modified templates/style.css +4 −0
…
896 unchanged lines
897 897
body.wall .kpi-v { font-size: 40px; }
898 898
body.wall .kpi-l { font-size: 15px; }
899 899
body.wall .chart-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 560px), 1fr)); }
900 +
/* Landing over required checks (#261): behind a toggle, with its reason. */
901 +
details.land-over { margin-top: 10px; }
902 +
details.land-over > summary { cursor: pointer; color: var(--mute); }
903 +
details.land-over form { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
900 904
/* A Terraform state's kept versions (#255). */
901 905
details.tf-versions { margin-top: 14px; }
902 906
details.tf-versions > summary { cursor: pointer; }
…
459 unchanged lines