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: 2 additions & 11 deletions .github/workflows/internal-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,11 @@ jobs:
env:
GH_TOKEN: ${{ secrets.SPACETIMEDB_PRIVATE_TOKEN }}
RUN_ID: ${{ steps.dispatch.outputs.run_id }}
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"

- name: Cancel invoked run if workflow cancelled
if: ${{ cancelled() && steps.dispatch.outputs.run_id && steps.dispatch.outputs.did_start == 'true' }}
Expand Down
15 changes: 15 additions & 0 deletions tools/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,21 @@ 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
- `--interval-seconds <INTERVAL_SECONDS>`: Seconds to sleep between polls
- `--max-attempts <MAX_ATTEMPTS>`: Maximum number of polls before timing out. Polls forever by default
- `--help`: Print help

#### `help`

**Usage:**
Expand Down
28 changes: 28 additions & 0 deletions tools/ci/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod internal_tests;
mod keynote_bench;
mod smoketest;
mod util;
mod workflow_watch;

use util::ensure_repo_root;

Expand Down Expand Up @@ -460,6 +461,21 @@ 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,
/// Seconds to sleep between polls.
#[arg(long, default_value_t = 30)]
interval_seconds: u64,
/// Maximum number of polls before timing out. Polls forever by default.
#[arg(long)]
max_attempts: Option<u64>,
},
}

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

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

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

Expand Down
70 changes: 70 additions & 0 deletions tools/ci/src/workflow_watch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use anyhow::{bail, Context, Result};
use duct::cmd;
use serde::Deserialize;

#[derive(Deserialize)]
struct WorkflowRunView {
status: String,
conclusion: 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,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);
}
}

pub(crate) fn watch_workflow_run(
repo: &str,
run_id: u64,
interval_seconds: u64,
max_attempts: Option<u64>,
) -> Result<()> {
println!("Waiting for workflow result... https://github.com/{repo}/actions/runs/{run_id}");

let mut attempts = 0;
loop {
attempts += 1;
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}");
}

if max_attempts.is_some_and(|max| attempts >= max) {
bail!("timed out waiting for workflow run {run_id} to complete")
}

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