hive-forge: resolve ci-rerun --run's PR pseudo-ref before dispatching
A run triggered by a pull_request-event workflow carries a #<n> pseudo-ref in prettyref rather than a real branch name. Dispatching a workflow with that as the ref 500s. resolve_run now recognizes the #<n> form and falls back to the same branch_for_pr lookup --pr already uses, so ci-rerun --run works on PR-triggered runs the same way ci-rerun --pr does. diagnosis + discriminating control table (real branch vs #<n> pseudo-ref) from atlas. fixes #4201
This commit is contained in:
parent
ec51ec924a
commit
7b9d8e4c4c
1 changed files with 63 additions and 19 deletions
|
|
@ -1,19 +1,14 @@
|
||||||
//! `ci-rerun --pr <n>` / `ci-rerun --run <n>` / `ci-rerun --branch <name>`
|
//! `ci-rerun --pr <n>` / `ci-rerun --run <n>` / `ci-rerun --branch <name>`
|
||||||
//! — re-run CI without pushing an empty commit.
|
//! — re-run CI without pushing an empty commit.
|
||||||
//!
|
//!
|
||||||
//! When a run fails for a transient reason (a remote-builder flap, a
|
|
||||||
//! cold-daemon window, an `act_runner` hiccup) the only retrigger path used
|
|
||||||
//! to be an empty commit, which litters PR history.
|
|
||||||
//!
|
|
||||||
//! Forgejo exposes no token-usable REST endpoint to *re-run an existing run*:
|
//! Forgejo exposes no token-usable REST endpoint to *re-run an existing run*:
|
||||||
//! the run-page rerun buttons hit CSRF-gated web routes that a bare token
|
//! the run-page rerun buttons hit CSRF-gated web routes that a bare token
|
||||||
//! POST answers with `404`. Instead this verb dispatches a **fresh** run of
|
//! POST answers with `404`. Instead this verb dispatches a **fresh** run of
|
||||||
//! the workflow via the GitHub-compatible workflow-dispatch API,
|
//! the workflow via the GitHub-compatible workflow-dispatch API,
|
||||||
//! `POST /repos/<owner>/<repo>/actions/workflows/<workflow>/dispatches` with
|
//! `POST /repos/<owner>/<repo>/actions/workflows/<workflow>/dispatches` with
|
||||||
//! `{"ref": "<branch>"}`. That creates a brand-new run on the branch — the
|
//! `{"ref": "<branch>"}` — same effect as the empty-commit trick, minus the
|
||||||
//! same effect as the empty-commit trick, minus the commit — and accepts a
|
//! commit (verified end-to-end on Forgejo 15.0.3, dispatches even a
|
||||||
//! plain agent token (verified end-to-end on Forgejo 15.0.3, which dispatches
|
//! `pull_request`-only workflow).
|
||||||
//! even a `pull_request`-only workflow).
|
|
||||||
//!
|
//!
|
||||||
//! The branch (and, for `--run`, the workflow file) is resolved from the
|
//! The branch (and, for `--run`, the workflow file) is resolved from the
|
||||||
//! given handle:
|
//! given handle:
|
||||||
|
|
@ -21,9 +16,10 @@
|
||||||
//! `ci.yml`) on it.
|
//! `ci.yml`) on it.
|
||||||
//! - `--branch <name>` → dispatches `--workflow` on that branch directly.
|
//! - `--branch <name>` → dispatches `--workflow` on that branch directly.
|
||||||
//! - `--run <n>` → looks the run up by its display number (same convention
|
//! - `--run <n>` → looks the run up by its display number (same convention
|
||||||
//! as `ci-log` / `artifact-get` / `ci-runs`) and dispatches the SAME
|
//! as `ci-log` / `artifact-get` / `ci-runs`) and dispatches the same
|
||||||
//! workflow on the SAME ref the run used (the run record's `prettyref` +
|
//! workflow + ref the run used. A PR-triggered run's ref is a `#<n>`
|
||||||
//! `workflow_id`).
|
//! pseudo-ref, not a real branch — that case resolves one hop further via
|
||||||
|
//! the same `branch_for_pr` lookup `--pr` uses.
|
||||||
//!
|
//!
|
||||||
//! Dispatch re-runs the whole workflow (no single-job variant). ⚠️ `--pr`
|
//! Dispatch re-runs the whole workflow (no single-job variant). ⚠️ `--pr`
|
||||||
//! verifies the code but doesn't reliably move the PR's own status
|
//! verifies the code but doesn't reliably move the PR's own status
|
||||||
|
|
@ -110,7 +106,10 @@ fn branch_for_pr(client: &Client, pr: u64) -> Result<String> {
|
||||||
|
|
||||||
/// Resolve the run whose display number is `run_number` to the
|
/// Resolve the run whose display number is `run_number` to the
|
||||||
/// `(workflow, branch)` to dispatch a fresh run of it. `fallback_workflow`
|
/// `(workflow, branch)` to dispatch a fresh run of it. `fallback_workflow`
|
||||||
/// is used when the run carries no workflow file name.
|
/// is used when the run carries no workflow file name. When the run's own
|
||||||
|
/// ref is a PR pseudo-ref (`#<n>`, not a real branch — workflow-dispatch
|
||||||
|
/// 500s on it), resolves the PR's actual head branch instead, same as
|
||||||
|
/// `--pr` would.
|
||||||
fn resolve_run(
|
fn resolve_run(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
repo: &str,
|
repo: &str,
|
||||||
|
|
@ -119,16 +118,22 @@ fn resolve_run(
|
||||||
) -> Result<(String, String)> {
|
) -> Result<(String, String)> {
|
||||||
let run = find_run_by_number(client, run_number)?
|
let run = find_run_by_number(client, run_number)?
|
||||||
.with_context(|| format!("ci-rerun: run #{run_number} not found in {repo}"))?;
|
.with_context(|| format!("ci-rerun: run #{run_number} not found in {repo}"))?;
|
||||||
run_dispatch_target(&run, fallback_workflow)
|
let (workflow, branch) = run_dispatch_target(&run, fallback_workflow)
|
||||||
.with_context(|| format!("ci-rerun: run #{run_number} has no ref"))
|
.with_context(|| format!("ci-rerun: run #{run_number} has no ref"))?;
|
||||||
|
let branch = match pr_number_from_run_ref(&branch) {
|
||||||
|
Some(pr) => branch_for_pr(client, pr)
|
||||||
|
.with_context(|| format!("ci-rerun: run #{run_number} was triggered by PR #{pr}"))?,
|
||||||
|
None => branch,
|
||||||
|
};
|
||||||
|
Ok((workflow, branch))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pull the `(workflow-file, ref)` dispatch target out of a run record:
|
/// Pull the `(workflow-file, ref)` dispatch target out of a run record:
|
||||||
/// `prettyref` is the ref the run ran on (the branch name for push /
|
/// `prettyref` is the ref the run ran on (the branch name for push /
|
||||||
/// dispatch runs — PR-event runs carry a `#<n>` pseudo-ref the dispatch
|
/// dispatch runs — PR-event runs carry a `#<n>` pseudo-ref instead, resolved
|
||||||
/// endpoint will reject with a clear 404), and `workflow_id` is the
|
/// one call site up in [`resolve_run`]), and `workflow_id` is the workflow
|
||||||
/// workflow file name (e.g. `ci.yml`), falling back to
|
/// file name (e.g. `ci.yml`), falling back to `fallback_workflow` when
|
||||||
/// `fallback_workflow` when absent. `None` only when the run has no ref.
|
/// absent. `None` only when the run has no ref.
|
||||||
fn run_dispatch_target(run: &ActionRun, fallback_workflow: &str) -> Option<(String, String)> {
|
fn run_dispatch_target(run: &ActionRun, fallback_workflow: &str) -> Option<(String, String)> {
|
||||||
let branch = run.prettyref.as_deref().filter(|s| !s.is_empty())?;
|
let branch = run.prettyref.as_deref().filter(|s| !s.is_empty())?;
|
||||||
let workflow = run
|
let workflow = run
|
||||||
|
|
@ -139,9 +144,19 @@ fn run_dispatch_target(run: &ActionRun, fallback_workflow: &str) -> Option<(Stri
|
||||||
Some((workflow.to_string(), branch.to_string()))
|
Some((workflow.to_string(), branch.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A run's `prettyref` for a PR-triggered run is `#<n>` — the PR number,
|
||||||
|
/// not a branch (same pseudo-ref form `ci-runs --branch` accepts as a
|
||||||
|
/// listing filter, via its own separate `qualify_ref`; deliberately not
|
||||||
|
/// shared, per that function's own doc comment: same field name, different
|
||||||
|
/// domain). Returns the parsed PR number for exactly that form, `None` for
|
||||||
|
/// anything else (a real branch name, or a malformed `#`-prefixed string).
|
||||||
|
fn pr_number_from_run_ref(value: &str) -> Option<u64> {
|
||||||
|
value.strip_prefix('#')?.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{ActionRun, run_dispatch_target};
|
use super::{ActionRun, pr_number_from_run_ref, run_dispatch_target};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
/// Build a typed run record from an API-shaped JSON fixture. The
|
/// Build a typed run record from an API-shaped JSON fixture. The
|
||||||
|
|
@ -182,4 +197,33 @@ mod tests {
|
||||||
fn no_ref_means_no_target() {
|
fn no_ref_means_no_target() {
|
||||||
assert_eq!(run_dispatch_target(&run_from(json!({})), "ci.yml"), None);
|
assert_eq!(run_dispatch_target(&run_from(json!({})), "ci.yml"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pr_triggered_run_keeps_its_pseudo_ref_at_this_layer() {
|
||||||
|
// resolve_run (untested here — does I/O) is what turns this into a
|
||||||
|
// real branch; the pure extractor must NOT do that resolution
|
||||||
|
// itself, or a run with a genuine branch named e.g. "#weird" (not
|
||||||
|
// possible in git, but worth pinning the boundary) would be handled
|
||||||
|
// in two different places.
|
||||||
|
let run = run_from(json!({ "prettyref": "#4199", "workflow_id": "ci.yml" })); // lint:allow: PR-shaped test fixture, not a tracker reference
|
||||||
|
assert_eq!(
|
||||||
|
run_dispatch_target(&run, "fallback.yml"),
|
||||||
|
Some(("ci.yml".to_string(), "#4199".to_string())) // lint:allow: PR-shaped test fixture, not a tracker reference
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pr_ref_parses_the_number() {
|
||||||
|
assert_eq!(pr_number_from_run_ref("#4199"), Some(4199)); // lint:allow: PR-shaped test fixture, not a tracker reference
|
||||||
|
assert_eq!(pr_number_from_run_ref("#0"), Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_pr_refs_do_not_parse() {
|
||||||
|
assert_eq!(pr_number_from_run_ref("main"), None);
|
||||||
|
assert_eq!(pr_number_from_run_ref("atlas/4199-foo"), None);
|
||||||
|
assert_eq!(pr_number_from_run_ref("#"), None);
|
||||||
|
assert_eq!(pr_number_from_run_ref("#12a"), None);
|
||||||
|
assert_eq!(pr_number_from_run_ref(""), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue