Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions .github/workflows/internal-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,10 @@ jobs:
RUN_URL: ${{ steps.dispatch.outputs.run_url }}
run: |
set -euo pipefail
echo "Waiting for workflow result... ${RUN_URL}"
set +e
# Due to our limited-scope token permissions, `gh run watch` spams errors about not being able to get annotations. They look worrying but they're benign, so we filter them out.
gh run watch "$RUN_ID" \
cargo ci other-workflows watch \
--repo "$TARGET_OWNER/$TARGET_REPO" \
--exit-status \
--interval 30 2>&1 \
| grep -Fv "requesting annotations returned 403 Forbidden as the token does not have sufficient permissions"
watch_status="${PIPESTATUS[0]}"
set -e
exit "$watch_status"
--run-id "$RUN_ID" \
--run-url "$RUN_URL"

- name: Cancel invoked run if workflow cancelled
if: ${{ cancelled() && steps.dispatch.outputs.run_id }}
Expand Down
16 changes: 16 additions & 0 deletions tools/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,22 @@ Usage: help [COMMAND]...

- `subcommand <COMMAND>`: Print help for the subcommand(s)

#### `watch`

**Usage:**
```bash
Usage: watch [OPTIONS] --repo <REPO> --run-id <RUN_ID>
```

**Options:**

- `--repo <REPO>`: Repository containing the workflow run, in owner/repo form
- `--run-id <RUN_ID>`: GitHub Actions workflow run ID
- `--run-url <RUN_URL>`: Optional URL printed while waiting for the run
- `--interval-seconds <INTERVAL_SECONDS>`: Seconds to sleep between polls
- `--max-attempts <MAX_ATTEMPTS>`: Maximum number of polls before timing out
- `--help`: Print help

#### `help`

**Usage:**
Expand Down
103 changes: 103 additions & 0 deletions tools/ci/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use anyhow::{bail, Context, Result};
use clap::{CommandFactory, Parser, Subcommand};
use duct::{cmd, Expression};
use serde::Deserialize;
use serde_json::Value;
use std::collections::BTreeSet;
use std::ffi::OsString;
Expand Down Expand Up @@ -457,6 +458,95 @@ enum OtherWorkflowsCmd {
#[command(subcommand)]
cmd: cla_assistant::ClaAssistantCmd,
},
/// Waits for a GitHub Actions workflow run to complete.
Watch {
/// Repository containing the workflow run, in owner/repo form.
#[arg(long)]
repo: String,
/// GitHub Actions workflow run ID.
#[arg(long)]
run_id: u64,
/// Optional URL printed while waiting for the run.
#[arg(long)]
run_url: Option<String>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this. we can construct this from run_id and repo.

/// Seconds to sleep between polls.
#[arg(long, default_value_t = 30)]
interval_seconds: u64,
/// Maximum number of polls before timing out.
#[arg(long, default_value_t = 240)]
max_attempts: u64,
},
}

#[derive(Deserialize)]
struct WorkflowRunView {
status: String,
conclusion: Option<String>,
url: Option<String>,
jobs: Vec<WorkflowJobView>,
}

#[derive(Deserialize)]
struct WorkflowJobView {
name: String,
status: String,
conclusion: Option<String>,
}

fn get_workflow_run(repo: &str, run_id: u64) -> Result<WorkflowRunView> {
let raw = cmd!(
"gh",
"run",
"view",
run_id.to_string(),
"--repo",
repo,
"--json",
"status,conclusion,url,jobs",
)
.read()
.with_context(|| format!("failed to read workflow run {run_id} in {repo}"))?;
serde_json::from_str(&raw).with_context(|| format!("failed to parse workflow run {run_id} in {repo}"))
}

fn print_workflow_job_summary(run: &WorkflowRunView) {
println!("Job summary:");
for job in &run.jobs {
let result = job.conclusion.as_deref().unwrap_or(&job.status);
println!(" {result:>11} {}", job.name);
}
}

fn watch_workflow_run(
repo: &str,
run_id: u64,
run_url: Option<String>,
interval_seconds: u64,
max_attempts: u64,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this optional. poll forever by default.

) -> Result<()> {
let run_url = run_url.or_else(|| get_workflow_run(repo, run_id).ok().and_then(|run| run.url));

if let Some(run_url) = run_url {
println!("Waiting for workflow result... {run_url}");
} else {
println!("Waiting for workflow result: {repo}/actions/runs/{run_id}");
}

for _ in 0..max_attempts {
let run = get_workflow_run(repo, run_id)?;
if run.status == "completed" {
print_workflow_job_summary(&run);
let conclusion = run.conclusion.as_deref().unwrap_or("success");
if conclusion == "success" {
return Ok(());
}
bail!("workflow run {run_id} completed with conclusion: {conclusion}");
}

std::thread::sleep(std::time::Duration::from_secs(interval_seconds));
}

bail!("timed out waiting for workflow run {run_id} to complete")
}

fn run_all_clap_subcommands(skips: &[String]) -> Result<()> {
Expand Down Expand Up @@ -900,6 +990,19 @@ fn main() -> Result<()> {
cla_assistant::run(cmd)?;
}

Some(CiCmd::OtherWorkflows {
cmd:
OtherWorkflowsCmd::Watch {
repo,
run_id,
run_url,
interval_seconds,
max_attempts,
},
}) => {
watch_workflow_run(&repo, run_id, run_url, interval_seconds, max_attempts)?;
}

None => run_all_clap_subcommands(&cli.skip)?,
}

Expand Down
Loading