AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
bridgewatch: GitLab CI·GitHub Actions용 macOS·Linux 오픈소스 트레이 모니터
bridgewatch is our open-source tray monitor for GitLab CI and GitHub Actions on macOS and Linux: each push, its downstream pipelines, whether it deployed.
개발 도구Rust · TypeScriptGitHub @distronode-corporation온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Tray monitor for GitLab CI and GitHub Actions. One row per push, every downstream pipeline, and whether it actually deployed. macOS and Linux.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
GitHub and GitLab pipelines in one tray app.
bridgewatch is a tray monitor for GitLab CI and GitHub Actions. It shows one row per push
with every downstream pipeline as a bridge, tells a scheduled pipeline apart from a push,
and reports whether the thing you care about actually deployed, instead of the raw
pipeline status.
A Rust core crate, a bridgewatch CLI and a Tauri 2 GUI (Svelte 5). macOS and Linux.
Apache-2.0.README 전문 보기 ↗GitHub and GitLab pipelines in one tray app.
bridgewatch is a tray monitor for GitLab CI and GitHub Actions. It shows one row per push with every downstream pipeline as a bridge, tells a scheduled pipeline apart from a push, and reports whether the thing you care about actually deployed, instead of the raw pipeline status.
A Rust core crate, a bridgewatch CLI and a Tauri 2 GUI (Svelte 5). macOS and Linux.
Apache-2.0.
Status: 1.2.0, released 2026-10-25. Download it from the
Releases page: a
signed and notarised universal .dmg for macOS, and a .deb, an .rpm and an
.AppImage for Linux on x86_64 and arm64. See Install.
Links: project page · GitLab mirror (read-only mirror; issues and pull requests live on GitHub) · CHANGELOG
Most CI tray monitors define "project status" as the status of the newest pipeline on a branch. On a real project three things go wrong with that.
Scheduled pipelines dominate the branch. A project with an hourly schedule on main
has far more scheduled pipelines than pushes, and a schedule can be red by design (a
reconciler that fails on purpose so drift is visible, for example). A monitor that reads
the newest pipeline then shows red most of the day and is soon ignored. It is not wrong
about any single pipeline; it is answering a different question.
Fan-out hides the real result. On GitLab a parent pipeline fans out to child pipelines through trigger jobs (GitLab calls them bridges), and the parent's status is an aggregate. On GitHub a push fans out into several independent workflow runs, and nothing above them has a status at all. Either way nothing says which component broke, or "the website deployed while a sibling failed", which is the most common shape of a partly-bad push, and nothing notices a workflow that should have run and never started.
"Pipeline succeeded" is not "it shipped". A pipeline can be green because the deploy job was skipped, is manual, or never ran. The question worth putting in a tray icon is whether a named job ran and succeeded.
So bridgewatch is:
dive.depth levels deep, default 1). On GitHub it is a commit group
(group = "commit"): every workflow run one push started, folded into one row with
each run as a bridge. A bridge that never produced its downstream (a trigger job that
settled without creating a child, or a workflow listed in expect that never started)
reads dead; one still on its way to creating it reads running.push,
schedule, ...) and by ref, so pushes, schedules and preflight branches are separate
watches on the same project. Only primary watches drive the tray icon and
notifications; secondary watches are rows in the popover and nothing else.deploy:origins), found anywhere in the parent or a child, not from the parent's
status field.| State | Glyph (builtin) | Means |
|---|---|---|
failed | red octagon | The pipeline carrying the deploy marker failed before any marker succeeded, a bridge is dead (it never produced its downstream) and no marker exists, a blocking job in the parent failed, (with no marker) a bridge failed, or the pipeline failed before it had any job. |
deployed_with_failure | amber triangle | A marker succeeded, and a sibling bridge failed or a blocking job failed around it, under the default downgrade policies. |
deployed | green filled check | A marker succeeded, no other marker can still move, and nothing is holding it back. |
running | blue arrows | A marker is in flight (even after another one succeeded), or (with no marker) something is still running. |
canceled | grey slash | The parent was canceled, or every marker was. A skipped pipeline ran nothing and is not listed. |
parked_gate | amber hourglass | No marker, nothing running, nothing broken, and a manual job or bridge is waiting for a person. |
succeeded_no_deploy | green outline check | Everything settled green and no marker ran. Normal for a lane that does not deploy. |
unknown | grey question mark | No data yet, the pipeline's job lists could not be read, a marker is in a status this build does not know, or a verdict script failed. |
The icon reflects the newest pipeline of each primary watch. With more than one primary
watch the worst state wins (in the order failed, deployed_with_failure, running,
canceled, parked_gate, unknown, deployed, succeeded_no_deploy), and
config validate warns about it. On macOS the glyphs are monochrome template images
that follow the menu bar; on Linux they are coloured. The exact rules are under
Verdict rules.
GitHub has no parent/child pipelines. A push fans out into independent workflow runs,
related only by their commit, event and branch. bridgewatch reads a GitHub watch in one of
two forms, chosen with the watch's group key:
group = "run" (the default): one workflow run is one row, and the run's jobs are
its job list. workflow narrows the watch to one workflow file; without it every
workflow's runs are rows of their own.group = "commit": every run of one push (same commit, event and branch, created
within fan_out_secs of the newest run) is one row, and each run is a bridge on it. This
is the GitHub equivalent of a GitLab parent pipeline, and it is what gives the verdict
something to say: a push that deployed while a sibling workflow failed reads
deployed_with_failure, exactly as a GitLab parent with a failed bridge does. The row's
status is the worst of its runs, and its link is the commit's checks page. dive
selects which runs' jobs are fetched, by workflow name.expect adds the one thing the runs alone cannot show: a workflow that should have run
and never started (its on: did not match, or GitHub refused the file). See
expect.
Pick GitHub Actions on the first step of the setup wizard,
then github.com or GitHub Enterprise. If you have run gh auth login, the wizard
offers Use gh's token: it writes the keyring source for gh's item (see
Reusing gh's login) and never copies the token. Then pick a
repository (or type owner/repo or its URL), the events to watch, and optionally one
workflow file. A new GitHub watch polls every 30 s while live and every 120 s when idle,
because of the rate limit.
The command-line equivalent prints the same file without touching the network:
bridgewatch init --provider github --project octo-org/octo-repo --gh --workflow ci.yml
The wizard writes a group = "run" watch: one row per workflow run, of the one workflow
you named or of every workflow. To get one row per push, set One row is to commit on
the watch in Settings, and add expect there, or write it by hand as below.
The shipped examples/github.toml follows this repository's own
workflows in both forms. The minimal versions:
[accounts.github]
provider = "github" # base_url, api_path and header default to github.com's
token = { keyring = { service = "gh:github.com", user = "" } }
# One row per push, each workflow run a bridge.
[[watches]]
id = "main-push"
account = "github"
project = "octo-org/octo-repo" # owner/repo; GitHub has no numeric form
ref = "main"
sources = ["push"]
group = "commit"
expect = ["ci.yml", "release.yml"] # workflow FILES every push to main must start
deploy_markers = ["deploy"]
poll = { live_secs = 30, idle_secs = 120 }
# One workflow, one row per run.
[[watches]]
id = "nightly"
account = "github"
project = "octo-org/octo-repo"
ref = "main"
sources = ["schedule"]
workflow = "nightly.yml" # the file name, or the workflow's numeric id
role = "secondary"
poll = { live_secs = 30, idle_secs = 120 }
Write the poll line yourself in a hand-written file: without it a watch gets the
defaults of 5 s and 60 s, which are sized for GitLab.
A classic personal access token needs the repo scope to read a private repository,
and no scope for a public one. There is no narrower classic scope for reading Actions, and
workflow is not one: it grants write access to workflow files. A fine-grained token
needs Actions: read and Metadata: read on the repositories you watch.
Test connection in the wizard lists a classic token's scopes and warns when repo is
missing; GitHub reports no scopes for a fine-grained token, so the wizard says it cannot
check one. GitHub reads a token only from the Authorization header, which is
the default for a github account, and header = "PRIVATE-TOKEN" on one is an error.
If you have run gh auth login, gh keeps its token in the OS keyring under service
gh:<host> (gh:github.com for github.com). gh writes two items there: one under your
login and one with an empty user, which is the active account that gh auth switch
moves. bridgewatch reads the empty-user item, so switching accounts in gh carries over:
token = { keyring = { service = "gh:github.com", user = "" } }
bridgewatch init --gh and the wizard's Use gh's token write exactly that, with the
host taken from the account (gh:ghe.example.com for an Enterprise Server host). The
portable alternative runs gh itself, and works wherever gh does, including when gh keeps
its token in a plain file rather than the keyring:
token = { command = ["gh", "auth", "token"] }
# GitHub Enterprise Server: ["gh", "auth", "token", "--hostname", "ghe.example.com"]
A command source asks for confirmation before the app writes it, see Command sources need confirmation.
github.com serves its API on its own host with no path prefix, so a github account
defaults to base_url = "https://api.github.com" and an empty api_path. Enterprise
Server serves it under a path on the server's own host:
[accounts.ghe]
provider = "github"
base_url = "https://ghe.example.com"
api_path = "/api/v3"
token = { keyring = { service = "gh:ghe.example.com", user = "" } }
The wizard and bridgewatch init --provider github --base-url https://ghe.example.com
write the /api/v3 for you. Links open on the web host: the server itself for
Enterprise Server, and https://github.com for an account on api.github.com.
A GitHub token gets 5,000 API requests an hour, shared with everything else that uses the same token, gh included. That is about 83 a minute, where gitlab.com allows 2,000 a minute, which is why a GitHub watch should poll more slowly than a GitLab one.
What a tick costs, per GitHub watch: one request for the list of runs, plus one (/jobs)
for each run that is live or changed and whose jobs are read. GitHub has no trigger jobs to
fetch, and a commit group's bridges come from the list itself.
bridgewatch sends conditional requests to GitHub (If-None-Match with the ETag of the
last answer), and GitHub does not count an authenticated 304 Not Modified against the
limit. So a watch whose runs have not changed costs nothing: at the 120 s idle interval a
settled watch sends 30 requests an hour and spends none of its budget. The validators are
kept in memory only, so the first poll after a start is paid for in full. The Debug
section shows the 304s and the remaining budget.
An exhausted limit is a 403 or 429 carrying x-ratelimit-remaining: 0 or retry-after, or
one whose message says it is a rate limit (GitHub's secondary limit can arrive with neither
header). bridgewatch treats it as a rate limit, not a bad token, and waits until GitHub's
reset time, the retry-after, or one minute when neither applies (a secondary limit with
budget left and no retry-after), before asking again, whatever
rate_limit_backoff.max_secs says.
expect: a workflow that never startedOn GitLab a trigger job that created no child is still there to be read as dead. On
GitHub a workflow that did not start leaves nothing at all: no run, so nothing to be red.
expect names the workflows every commit group must hold, and an absent one becomes a
dead bridge.
ci.yml, or the full .github/workflows/ci.yml),
not the display names the bridges show. A file name does not change when someone edits
the workflow's name:.fan_out_secs has
passed since its newest run. Until then a missing workflow is a pending job, and the
row reads running, not green. After that it is a dead bridge: the row reads failed,
or deployed_with_failure when a marker has already succeeded (under the default
sibling_failure = "downgrade"), and a primary watch raises a blocking_failure
notification. A real failure in a run that does exist still turns the row red at once.startup_failure is a failed run, not an absence.expect applies to every group the watch shows, so list only workflows that run on
every event in sources, and none whose paths or branches filters can
legitimately skip a push. A workflow that starts later than fan_out_secs after the
others lands in a row of its own.config validate warns about expect on a GitLab watch or with group = "run", about
duplicates, empty entries and entries that are not .yml or .yaml file names, about an
entry that workflow rules out, and about expect with an empty sources.
allow_failure. continue-on-error does not appear in GitHub's jobs API, so
every failed job counts as a failure. Use a [watches.jobs] override to
tolerate one ("lint" = "warning"). On a commit group the same table applies to the
bridges, so it can tolerate a whole workflow by its name.dive.depth. GitHub runs do not nest: a called (reusable) workflow's jobs are in
its caller's job list, named caller / callee, and bridgewatch groups them by the part
before /. dive.depth above 1 on a GitHub watch is a warning and is ignored./ prefix above is the only grouping a GitHub job list has.pf/* watch is a GitLab habit; on GitHub add a
watch with a glob ref in Settings or by hand if you want one.bridgewatch fixture record refuses a github account (exit
64) until GitHub recordings have their own personal-data allow-list.| Platform | Status |
|---|---|
| macOS 10.15 or newer, Apple Silicon and Intel | Supported. Release builds are signed and notarised, see below. |
| Linux x86_64 and arm64, glibc 2.35 or newer (Ubuntu 22.04, Debian 12, Fedora 36 or newer, and equivalents) | Supported. |
| Windows | Not targeted. |
Linux release builds are made on Ubuntu 22.04, on both x86_64 and arm64 (the oldest
build machines GitHub offers). glibc is forward-compatible but not backward-compatible, so
that is the floor: the .deb, .rpm and .AppImage need glibc 2.35 or newer, and do
not start on older systems.
On Linux the tray is a StatusNotifierItem through libayatana-appindicator. GNOME has no
built-in host for that protocol, so on GNOME you need the AppIndicator/KStatusNotifierItem
extension enabled or the icon never appears. KDE, Cinnamon and XFCE work as they are.
Download from the Releases page.
| File | For |
|---|---|
bridgewatch_<version>_universal.dmg | macOS, Apple Silicon and Intel |
bridgewatch_<version>_amd64.deb | Debian and Ubuntu, x86_64 |
bridgewatch_<version>_arm64.deb | Debian and Ubuntu, arm64 |
bridgewatch-<version>-1.x86_64.rpm | Fedora and openSUSE, x86_64 |
bridgewatch-<version>-1.aarch64.rpm | Fedora and openSUSE, arm64 |
bridgewatch_<version>_amd64.AppImage | Other glibc 2.35+ Linux, x86_64 |
bridgewatch_<version>_aarch64.AppImage | Other glibc 2.35+ Linux, arm64 |
The .dmg is universal: one download runs natively on both Apple Silicon and Intel
Macs. The Linux file names follow each format's own architecture spelling (amd64 and
arm64 for Debian, x86_64 and aarch64 for RPM). Every asset carries a GitHub build
provenance attestation:
gh attestation verify bridgewatch_1.2.0_amd64.deb --repo distronode-corporation/bridgewatch
Every release also carries the attestation as
bridgewatch_<version>.intoto.jsonl, so the same check works offline with
--bundle bridgewatch_<version>.intoto.jsonl added. Every bundle carries LICENSE,
NOTICE and the licence texts of the Rust crates and npm packages built into it
(THIRD-PARTY-LICENSES-rust.txt, THIRD-PARTY-LICENSES-npm.txt): in
/usr/share/doc/bridgewatch/ for the .deb, /usr/share/licenses/bridgewatch/ for
the .rpm, and among the app's resources in the .dmg and the .AppImage.
The release assets contain the GUI only. The bridgewatch CLI is built from source,
see CLI.
The .app inside the .dmg is signed with the Developer ID Distronode Corporation
(R935BA6767) and notarised by Apple, with the ticket stapled, so it opens like any other
downloaded app. To check a copy yourself:
codesign --verify --deep --strict /Applications/bridgewatch.app
spctl --assess --type execute --verbose /Applications/bridgewatch.app
The second command should end with source=Notarized Developer ID.
sudo apt install ./bridgewatch_1.2.0_amd64.deb
On arm64, use bridgewatch_1.2.0_arm64.deb. The package depends on libsecret-tools,
because bridgewatch runs secret-tool to read a keyring item that has an empty user name,
which is how glab stores its token and how gh stores its active account's (see
Tokens).
sudo dnf install ./bridgewatch-1.2.0-1.x86_64.rpm # Fedora
sudo zypper install ./bridgewatch-1.2.0-1.x86_64.rpm # openSUSE
On arm64, use bridgewatch-1.2.0-1.aarch64.rpm. The package requires the file
/usr/bin/secret-tool rather than a package name, for the same reason as the .deb: the
package that ships it is libsecret on Fedora but secret-tool on openSUSE, and
a file dependency resolves to the right one on each. The package is not GPG-signed; the
attestation above is how to check where it came from.
chmod +x bridgewatch_1.2.0_amd64.AppImage
./bridgewatch_1.2.0_amd64.AppImage
On arm64, use bridgewatch_1.2.0_aarch64.AppImage. An AppImage declares no dependencies.
If you reuse glab's or gh's token, install secret-tool yourself (sudo apt install libsecret-tools on Debian and Ubuntu, sudo dnf install libsecret on Fedora). A token stored in
bridgewatch's own keyring entry goes through the Secret Service over D-Bus and does not
need it; either way a Secret Service (GNOME Keyring, KWallet) has to be running.
When there is no config file at the default path, bridgewatch opens a setup wizard. It is
optional: Skip, I'll edit config.toml writes nothing and opens Settings on the Edit
as text tab, where you can write the file yourself (start from
examples/distronode.toml for GitLab or
examples/github.toml for GitHub). Saving from that tab creates
it.
The wizard's steps:
read_api scope; on GitHub it
lists a classic token's scopes and warns when repo is missing, see
Tokens for GitHub.group/project path or a project URL on GitLab, or owner/repo or a
repository URL on GitHub. A GitLab project access token cannot list projects, so it
always gets the typing form.pf/*.verify, test, lint, check,
validate, smoke) is never offered, whatever else its name says.The wizard writes through the same path as Settings. Run on an existing file, it edits
that file rather than replacing it, and it refuses to write text that does not load or
that contains something shaped like a GitLab token (or, for a GitHub account, a GitHub
token). When the file already has a primary watch, the new watch is added as
role = "secondary", so it does not take over the tray icon; tick Make this watch
primary too on the review step to keep it primary. Re-open the wizard any time from the
tray menu (Setup wizard…) or from Settings.
bridgewatch init is the same logic from the command line, see CLI.
A --config or BRIDGEWATCH_CONFIG path that does not exist is treated as a typo, not
as a first run: bridgewatch reports it and creates nothing.
The release builds can send a crash report to Distronode's Sentry organisation in the
European Union (de.sentry.io) when the tray app fails. They are off until you turn them on. The popover asks once, with Don't
send as the default, and Settings → UI → Send crash reports changes the answer
later; both write [ui].send_crash_reports. Turning it off takes effect at once.
false, no crash-report client
is created at all. A test (src-tauri/tests/crash_reports.rs) points the client at a
local server and checks that a panic sends zero requests with consent off, one with
consent on, and none again once consent is taken back./home/<name>, /Users/<name>,
C:\Users\<name>) has the name replaced with <user> before it leaves the machine,
and the user, IP address and host name fields are removed. The config file, tokens,
pipelines and logs are not attached. Sentry still sees the connection's IP address, as
any server does, and is set not to store it.BRIDGEWATCH_SENTRY_DSN), and the code does not contain it, so a build you compile
yourself, or a fork's, never asks and never sends. The CLI has no crash reporting.--config <path> (also -c <path> or --config=<path>), --help,
--version. BRIDGEWATCH_CONFIG names the config file too; an empty value counts as
unset.bridgewatch.<date>.log, of which the last 7 are kept. The folder is
~/Library/Logs/bridgewatch on macOS (Console.app lists it) and
$XDG_STATE_HOME/bridgewatch on Linux (by default ~/.local/state/bridgewatch);
Open log folder in the tray menu opens it. On macOS the file is the only log an
app started from Finder, the Dock or a login item has, because its stderr goes
nowhere. If the folder cannot be created or written, the app keeps running with stderr
alone and says so once, at warn. The level is the same for both: a non-empty
RUST_LOG wins for the life of the process; otherwise [log].level applies (to
bridgewatch's own crates; everything else stays at warn) and is re-applied on
reload; with neither, the level is , so the file stays nearly empty until you
raise it. The CLI logs to stderr only.Each pipeline row in the popover expands into its jobs: the parent's own jobs, then one
row per bridge with that child pipeline's (or workflow run's) jobs, each list grouped by
stage in pipeline order, with a status dot per job, a marker on running jobs and a duration that ticks
locally between polls. Live pipelines start expanded and settled ones collapsed; a bridge
that failed or lost its child starts expanded either way. What you open or close stays
that way across refreshes. Every job links to its page on GitLab or GitHub. A GitHub
workflow in expect that never started shows as a bridge marked never started, linked
to the workflow's page.
A failed job, in the parent or in a child pipeline, on GitLab or GitHub, has a log
button. It opens the end of that job's log under the row: the last 40 lines, in a
scrollable box, with a link to the full log on its page (opened through the same
host rule as every other link). The log is read when you open it and
never by the poller, and at most 256 KiB of it is read, from the end. It is shown as
plain text: terminal colour codes, GitLab's collapsible-section markers, lines a
progress bar overwrote, control characters and the runner's per-line timestamps
(GitHub's, and GitLab's when the runner adds them) are taken out. GitHub serves a log
from a signed storage URL that its API redirects to. bridgewatch follows that one
redirect itself, only to an https URL, and sends no token or credential header there:
the signature in the URL is the authorisation, and the request log shows that leg as
(redirect) host/path without the signature.
[ui].jobs picks what an expanded row lists: all (every job, the default) or
failures (only failed jobs, tolerated failures and manual gates, and only the bridges
that have something to say). watches.show.jobs overrides it per watch. This is
presentation only: the verdict never depends on it.
Neither GitLab nor GitHub offers a desktop app a push channel. Webhooks need a public
inbound URL, and the web UIs' live updates use internal channels. "Real time" in
bridgewatch therefore means polling: every poll.live_secs (default 5 s) while any
pipeline in any watch is running, and every poll.idle_secs (default 60 s) when
everything has settled. The popover shows "updated Ns ago", and opening it polls at once if the last poll is at
least 2 s old.
What a live tick costs, per GitLab watch: one pipeline-list request, plus two (/jobs and
/bridges) for each pipeline that is live or changed, plus one for each dived child
pipeline that is live or moved. A settled, unchanged pipeline is served from cache, so an
idle watch costs its list request only. For the shipped example (a push pipeline with
four child pipelines, plus two secondary watches) that is 9 requests a tick, 108 a
minute at 5 s. GitLab.com currently allows authenticated API traffic of 2,000 requests a
minute per user. GitLab.com has also published proposed per-plan limits, not in effect
as of this release, whose Free-plan figure is 100 a minute; under those a busy estate on
a Free account would be rate limited while a pipeline runs, and bridgewatch would back
off (up to rate_limit_backoff.max_secs). Raise live_secs if that matters to you.
GitHub's budget is far smaller and counts differently, see Rate limits.
bridgewatch reads by default and writes nothing. Set actions = true on an account and
the popover offers retry on a failed or canceled job and play on a GitLab manual
job, for that account's watches only. Each one asks first, naming the job, the project and
the pipeline, and sends one request however many times it is clicked. When the request
goes through, the watch is polled at once. A refusal is shown under the job.
[accounts.gitlab]
token = { keyring = { service = "glab:gitlab.com:token", user = "" } }
actions = true
actions off (the default) the client
refuses a retry or play before anything is sent, whatever asks for it: the popover, the
CLI or a script in the webview. Turning actions on from Settings (or Edit as text)
is one of the changes the app asks you to confirm before it writes the file, see
Command sources need confirmation.info in the log file: the account, the project, the job
id, the action and how it ended. Never the token.bridgewatch retry <job-url> and bridgewatch play <job-url>, see CLI.What the token needs:
| Token | To read logs | To retry and play |
|---|---|---|
| GitLab personal access token, or glab's login | read_api | api, and a role that may run pipelines on the project (Developer or above). read_api cannot run jobs; glab auth login creates a token with api unless it was narrowed. |
GitLab sign-in (token = { oauth = .. }) | read_api | api. The sign-in asks for api only on an account with actions = true, so after turning it on, sign in again: a sign-in made with read_api is refused before a write is sent, with a message that says so. The OAuth application must allow the api scope too (on your own application, tick it; the built-in gitlab.com application has to allow it as well). |
| GitHub classic token, or gh's login | repo (private repositories) | repo, and write access to the repository. gh's default login includes repo. |
| GitHub fine-grained token | Actions: read | Actions: read and write. |
| GitHub sign-in (the bridgewatch-ci GitHub App) | Actions: read | The App needs Actions: read and write in its own permissions, which its owner sets on GitHub; each installation then has to approve the new permission. A GitHub App's user token asks for no scope, so signing in again changes nothing until then. |
A 403 on a write reads "this token cannot run jobs", with what it needs, and not "check the token": the token works, it may only read. A 401 is still a bad token, and a GitHub rate limit is still a rate limit.
While a push is still on its way, a row says how long this watch's deploys usually take
and how far in this one is: usually ~11m · 6m 30s in in the popover, and
usually ~11m, 6m 30s in in bridgewatch check and bridgewatch watch. It costs no
API requests: it is learned from pipelines the watch has already fetched.
live (a marker succeeded
and no other marker can still move), bridgewatch records the time from the pipeline's
creation to the marker job finishing. A pipeline is measured once. A missing or
unreadable time is no sample, and so is a zero or negative one. Any watch with
deploy_markers learns, primary or secondary; one without has nothing to learn from.eta.json beside the notification ledger, in the state
directory: ~/Library/Application Support/bridgewatch on macOS (the data directory,
as macOS has no state directory) and $XDG_STATE_HOME/bridgewatch on Linux (by
default ~/.local/state/bridgewatch). The app and the CLI share it; each re-reads it
and merges before writing, and writes to a temporary file that is renamed over it. A
corrupt file costs its samples and one warning, never a poll. check --fixture and
watch --fixture learn in memory only.id. Renaming a watch starts its history again.eta = false on a watch turns all of it off: nothing is recorded and nothing is shown.
In check --json and watch --json a running row with an estimate carries
"eta": { "typical_secs": 660, "elapsed_secs": 390 }; every other row has no eta key.
bridgewatch never writes a token to its config file, and the config loader refuses a literal token string with an error that lists the alternatives. The file records where the token comes from; it is resolved when the poller starts and again on every reload.
token = { keyring = { service = "glab:gitlab.com:token", user = "" } } # glab's item
token = { keyring = { service = "gh:github.com", user = "" } } # gh's active account
token = { own = true } # bridgewatch's own item
token = { env = "BRIDGEWATCH_TOKEN_GITLAB" } # an environment variable
token = { command = ["pass", "gitlab/pat"] } # a program's output
| Source | Notes |
|---|---|
keyring | Any OS keyring item, addressed by service and user. Use it to reuse glab's or gh's login. |
own | bridgewatch's own keyring item, service bridgewatch:<account>, written when you paste a token into Settings or the wizard. This is the default when an account has no token key. |
env | Read from the process environment, so it works when bridgewatch is started from a shell or a service unit that sets the variable. |
command | Runs the program (no shell) with stdin closed and a 10 s timeout, and uses the first line of its stdout. Writing one from Settings or the wizard needs an explicit confirmation, see below. |
A token source table names exactly one source; a table naming two is refused.
If you have run glab auth login, the token is already in your OS keyring under service
glab:<host>:token (so glab:gitlab.com:token for gitlab.com) with an empty user
name. The empty user is required: glab's keyring library writes the item that way and a
lookup with any other user misses. The wizard and bridgewatch init --glab write this
source for you. The self-managed glab:<host>:token form follows glab's naming but has
not been checked against a live self-managed install. gh's login works the same way, see
Reusing gh's login.
On macOS glab's keyring library (which gh uses too) stores the value wrapped as
go-keyring-base64: plus base64 (older versions: go-keyring-encoded: plus hex);
bridgewatch unwraps either wrapper, and passes an unwrapped value through unchanged, so the same config works on macOS and Linux.
macOS may ask whether bridgewatch may read an item another program created. Always Allow stops it asking again each time the token is read.
⚠️ secret-tool lookup service glab:gitlab.com:token username "" prints the token.
If you check the Linux item by hand, pipe it to wc -c, never into a log or an issue.
A command source turns the config file into something that runs a program with your
permissions every time a token is needed. So when Settings or the wizard would write one
(a new one, or a changed command), the app writes nothing and shows the exact command for
you to confirm first. The same applies to a change that would send a credential to a host
it has not been sent to before: a new base_url for an account that already has a
token, or a keyring or environment source on a host no account used, and to turning
actions on for an account (see Retry and play). Edits you make in your own editor are not
subject to this: the check is on the app's write path, and the file is yours.
config validate warns about every command source.
A GitLab project access token can read only its own project, so it cannot list projects.
The wizard then asks you to type the project id or path instead of offering a list, and a
watch can always name a project by id. To watch several projects, use a personal access
token with read_api, or add one account per project.
config.toml lives in the OS config directory:
| OS | Path |
|---|---|
| Linux | ~/.config/bridgewatch/config.toml (or under $XDG_CONFIG_HOME) |
| macOS | ~/Library/Application Support/bridgewatch/config.toml |
--config <path> wins, then BRIDGEWATCH_CONFIG, then that default.
bridgewatch config path prints the one in effect.
The file is edited through toml_edit, so comments, key order and formatting survive a
save from the GUI, and it is hot-reloaded: save it in an editor and the running app picks
it up. The GUI saves by compare-and-swap, so a Settings window holding an old copy cannot
overwrite a change made on disk since; it tells you to re-read instead.
GUI and file parity. Every key in the file has a control in Settings and every control maps to a key; tests compare the Settings registry with the generated JSON Schema, so a key without a control fails CI. Settings has tabs for Accounts, Watches, Icon, Verdict, UI and Log, plus Edit as text, which edits the same file directly with diagnostics by line and column.
bridgewatch config validate reports every problem with its line and column.
bridgewatch config schema prints the JSON Schema (the same one committed as
src/lib/config.schema.json) for editor completion. bridgewatch config dump prints the
configuration with every default filled in.
Every key is optional unless marked required. Defaults in brackets.
[accounts.<name>], one per GitLab instance or GitHub host (at least one is required).
Accounts of both providers can sit in one file:
| Key | Meaning |
|---|---|
provider | gitlab or github [gitlab, so a file written before GitHub support means what it always did]. base_url, api_path and header default per provider. |
base_url | Instance root, no trailing slash [https://gitlab.com, or https://api.github.com for github]. For GitHub Enterprise Server, the server's root. http:// is accepted, with a warning unless the host is loopback. |
api_path | API prefix [/api/v4, or empty for github]. GitHub Enterprise Server needs /api/v3. |
token | Token source, see Tokens [{ own = true }]. |
header | "PRIVATE-TOKEN" (GitLab personal and project access tokens) or "Authorization: Bearer" (GitLab OAuth and CI job tokens, and every GitHub token) [PRIVATE-TOKEN, or Authorization: Bearer for github, where PRIVATE-TOKEN is an error]. |
timeout_secs | Per-request timeout [15]. |
rate_limit_backoff.max_secs | Ceiling for the doubled interval after rate limiting or errors [300]. A retry-after, or GitHub's rate-limit reset time, is waited out in full even when it is longer. |
actions | Allow retrying jobs and starting manual ones from the popover and the CLI [false]. The token then needs more than read access, see Retry and play. |
[[watches]], in display order:
| Key | Meaning |
|---|---|
id | Required. Unique; used by --watch, notifications and the GUI. |
account | Required. An [accounts.<name>] key. |
project | Required. GitLab: numeric id or group/path. GitHub: owner/repo (a numeric id is an error). |
ref | Exact name, glob (pf/*), or re: plus a regex [main]. A re: pattern is unanchored; write re:^…$ to match the whole ref. |
sources | Pipeline sources (GitLab: push, schedule, web, merge_request_event, ...) or workflow events (GitHub: push, schedule, pull_request, workflow_dispatch, ...) to accept. Empty means all [[]]. |
workflow | GitHub only. One workflow, by file name (ci.yml) or numeric id; each run is a row. Absent means every workflow [unset]. |
group | GitHub only. run (one workflow run is one row) or commit (every run of one push is one row, each run a bridge) [run]. See GitHub Actions. |
fan_out_secs | GitHub only, with group = "commit". Runs of one commit, event and branch created within this many seconds of the group's newest run are one row [90]. |
expect | GitHub only, with group = "commit". Workflow file names every group must hold; one with no run is a dead bridge once the group has settled and its window has passed. See expect [[]]. |
role | primary (drives the icon, may notify) or secondary (rows only) []. |
[icon]:
| Key | Meaning |
|---|---|
mode | template (monochrome, macOS inverts it), color, or auto (template on macOS, colour elsewhere) [auto]. |
theme | builtin, or a directory of <name>.png files (<name>@1x.png also works). A missing file falls back to the builtin one [builtin]. |
states | State name to glyph: question, octagon, triangle, check, check-outline, arrows, slash, hourglass, or another state's name [{}]. |
[verdict]: script, a path to a Rhai script (~ expanded), or
script_source, an inline script that takes precedence. The script receives each
pipeline's view as pipeline and returns an icon-state name, replacing the built-in rules
for every watch. Sandboxed: import cannot load files, eval is disabled, and operations
and call depth are capped. A script that errors or returns an unknown name yields
unknown and an error line, never a confident wrong colour.
[ui]:
| Key | Meaning |
|---|---|
popover.width, popover.max_height | Popover size in logical pixels [440, 720]. |
popover.hide_on_blur | Hide the popover when it loses focus [true]. |
theme_css | Path to a .css file (at most 256 KiB) layered over the built-in theme [""]. |
launch_at_login | Register a login item. The checkbox in Settings and the tray menu set the OS login item and mirror its state here [false]. |
send_crash_reports | Send a crash report when the app fails, see Crash reports (opt in). Unset means not answered yet, and the popover asks once; only true sends [unset]. |
jobs | all or failures, see the job view [all]. |
[log]:
| Key | Meaning |
|---|---|
level | error, warn, info, debug, trace or off [info], applied to bridgewatch's own crates with everything else left at warn. A non-empty RUST_LOG wins instead, whole and unscoped; an empty one counts as unset. Both the app and bridgewatch follow this. |
keep_requests | Recent requests kept for the Debug section; 0 disables it [50]. |
An unknown key, and a value of the right type that names nothing (a misspelt source,
glyph or state, an unknown icon mode), is a warning and is ignored, so a file written for
a newer build still loads on an older one. A key that belongs to the other provider
(workflow on a GitLab watch, dive.depth above 1 on a GitHub one) is a warning too, so
a watch can move between accounts without making the file unloadable. config validate lists every warning with its
line and column. A value of the wrong type, or a literal token string, is an error.
The GitLab example, examples/distronode.toml, is the
configuration the project was built against: one gitlab.com account and three watches on
the same project.
main-push is the only primary watch: push pipelines on main. It dives into every
bridge, treats deploy:origins or deploy:marketing succeeding as "deployed", and
lets a failure elsewhere downgrade a deploy to deployed_with_failure rather than
hiding it or calling the deploy a failure. Its [watches.jobs] table marks a job that
is manual on main as a gate (so waiting on it is not a failure, while a run of it
that fails still is) and ignores a scanner job entirely.hourly is the same branch with sources = ["schedule"], secondary, so a red
scheduled pipeline shows as a row and never touches the icon.
dive.only_when = "failed" skips the child-pipeline requests unless a bridge failed.preflights watches the pf/* branch glob with dive.bridges = "": for a
preflight, the parent's status is the answer, and one request a tick is enough.The GitHub example, examples/github.toml, follows this
repository's own workflows on github.com through gh's keyring item.
main-push is the primary watch: push runs on main, folded by group = "commit"
into one row per push, with CI and Scorecard as its bridges. expect names both files,
so a push where either never started reads failed once the group settles. Nothing on
main deploys, so it has no marker; config validate warns about that, deliberately,
and the file's comment says why.audit follows one workflow, audit.yml, on its weekly schedule: secondary, one
row per run.Both GitHub watches poll at 30 s and 120 s, what the wizard writes for GitHub.
Job classes: blocking is the default for a job that is not allowed to fail, and a red
one fails the verdict. warning shows in the popover and does not fail it (a GitLab job
with allow_failure: true is a warning already, and blocking promotes it; GitHub
reports no such flag, so there an override is the only way to get one). gate marks a
manual or never-run job as a deliberate wait; it does not excuse a gated job that ran
and failed. ignore removes the job from every verdict and list.
For the newest pipeline of each watch that matches its ref and sources (a skipped
pipeline, which ran nothing, is passed over), bridgewatch
reads the parent's jobs and bridges, then the jobs of every child pipeline that dive
selects, classifies each job, and looks for the deploy_markers across all of them. On
GitHub the "parent" is a workflow run (with no bridges), or with group = "commit" the
push, whose bridges are its runs and whose own jobs are empty. The same rules apply to
both. The state is the first rule that matches:
unknown if the pipeline's job lists could not be read and nothing was cached.failed if no marker succeeded and the markers' own pipeline has a blocking
failure (a marker that failed counts even when it was allowed to fail), or no marker
job exists and a bridge is dead (a trigger job that never created its child pipeline,
or an expected GitHub workflow that never started), or a blocking job in the parent
failed (unless it started after a successful marker, which rule 4 handles), or there
is no marker and any bridge failed, or the pipeline itself failed with no jobs and no
bridges at all (a .gitlab-ci.yml that does not parse, a GitHub run that ended
startup_failure). That last one is named pipeline failed in failures.unknown if a marker is in a status this build does not recognise.failed when a sibling failure or post-deploy failure
applies with policy fail; deployed_with_failure when one applies with policy
downgrade; otherwise deployed. A sibling failure is a failed or dead bridge
other than the one carrying the marker; a post-deploy failure is a blocking job that
started after the marker, or one in the marker's own pipeline.running if a marker is in flight, or there is no marker and something is live.
A job waiting on a manual gate is not live. A marker still queued after another one
succeeded counts as in flight, and so does a live trigger job that dive would walk
into whose child has not been read yet (it may not exist yet): a marker in there has
not been seen.canceled if the parent was canceled (or skipped, when a watch has nothing
newer to show), or every marker was.parked_gate if there is no marker, nothing is live, nothing blocking failed,
and something is waiting on a manual gate. A marker that is itself a manual job counts
as "no marker" here.succeeded_no_deploy if there is no marker, nothing is live and nothing blocking
failed.[verdict].script replaces all of this for a watch's pipelines if your project needs
different rules. bridgewatch check --json prints each pipeline's facts (deploy,
failures, warnings, gates, sibling_failures, post_deploy_failures, the bridges
and their jobs), which is the fastest way to see why a pipeline got the state it did.
Every URL the app opens (a job or pipeline in the popover, Open pipelines page in the
tray menu) goes through one function in the Rust shell. It opens only http and https
URLs whose scheme, host and port are those of a configured account's base_url, or for a
github account its web host (https://github.com for https://api.github.com, the
server itself for Enterprise Server), and refuses and logs anything else, look-alike
hosts and other ports included. The URLs come from the provider's API, so this is what
stops a hostile or compromised instance from making the app open an arbitrary page or a
file: URL. The web views have no permission to open URLs themselves.
The CLI runs the same core as the GUI, so anything the icon knows it can print: in a shell prompt, a status bar or a script. It is not in the release assets; build it with Rust 1.90 or newer, from a clone of this repository:
cargo install --locked --path crates/bridgewatch-cli
The CLI does not depend on Tauri, so it needs none of the GUI build packages.
bridgewatch check # resolve every watch once, print a summary
bridgewatch check --watch main-push # only this watch (repeatable)
bridgewatch check --json # the whole snapshot as JSON
bridgewatch check --fixture <dir> # answer from a recorded fixture, no network, no token
bridgewatch watch # poll until Ctrl-C, one line per change
bridgewatch watch --json # one JSON object per line: a snapshot per change,
# {"notify": {...}} per notification
bridgewatch watch --ticks 3 # stop after three ticks
bridgewatch init --project group/project --glab --deploy-marker deploy:production
# print the config the wizard would write
bridgewatch init --provider github --project octo-org/octo-repo --gh --workflow ci.yml
# the same for a GitHub repository
bridgewatch config path # the config file in effect
bridgewatch config validate # every problem, with line and column
bridgewatch config schema # JSON Schema for editor completion
bridgewatch config dump # the config with every default filled in
bridgewatch fixture record <pipeline-id> [--out <dir>] [--account <name>] [--project <id|path>] [--list]
bridgewatch fixture scrub <dir>... [--check]
bridgewatch log <job-url> [--account <name>] [--json]
# the last 40 lines of a job's log
bridgewatch retry <job-url> [--account <name>] [--yes]
bridgewatch play <job-url> [--account <name>] [--yes]
# retry a job, or start a manual one (GitLab)
--config <path> (or -c) is accepted before or after any subcommand. watch does not
write the GUI's notification ledger; it keeps its own.
init is the wizard without the UI or the network. It takes --provider (gitlab,
the default, or github), --project (required; an id, a path or a project URL on
GitLab, owner/repo or a repository URL on GitHub), --base-url, --account, --ref,
--watch-id, --workflow (GitHub only), --source (repeatable; a workflow event on
GitHub), --deploy-marker (repeatable), --schedule, --preflight <glob> (GitLab
only), --primary, --live-secs, and one token source (--glab, --gh, --token-env <var>, --token-keyring <service> or --token-command <program> <arg>...; none means
own = true). --token-command takes everything after it, arguments starting with -
included (--token-command gh auth token --hostname ghe.example.com), so it goes last. It reads no token, fetches nothing and writes nothing: it prints the file
to stdout, and if a config file already exists it prints that file edited, with its
comments and other watches kept. When that file already has a primary watch, the new
watch is added as secondary with a note on stderr; --primary keeps it primary. A new
GitHub watch polls at 30 s live and 120 s idle unless --live-secs says otherwise.
Redirect it yourself once it reads right.
log, retry and play take a job's page URL, as the popover links to it:
https://<host>/<group>/<project>/-/jobs/<id> on GitLab (a base_url with a path prefix
included) or https://github.com/<owner>/<repo>/actions/runs/<run>/job/<id> on GitHub.
The URL is matched to a configured account by its host, the same rule links are opened
by; when two accounts share a host, --account names one. retry and play are writes:
they need actions = true on the account (exit 78 otherwise, before any token is read),
ask Retry job ... ? [y/N] on the terminal, and without a terminal refuse unless --yes
is given (exit 64). Answering no exits 1 and sends nothing. play on a GitHub account is
a usage error.
fixture record walks one parent pipeline and its children into a fixture directory
for the core's tests; fixture scrub re-applies the allow-list that keeps personal data
out of fixtures. It reads each named directory's own files, not subdirectories, and a
directory with no recorded files in it is refused with exit 64. Recording is GitLab-only: a github account is refused with exit 64. See
CONTRIBUTING.md.
Verdict codes and error codes never overlap, so a script can tell a red pipeline from a
typo in its own command line. For check and watch the code is the tray icon's state,
that is the worst state among the primary watches.
--watch <id> makes the watches you name the subject, so when none of them is primary
the code is the worst state among all of them: check --watch hourly on a failed
schedule exits 1, not 4. Name a primary watch alongside and the tray rule applies again,
because a secondary watch must not be able to outvote a primary one.
0 check/watch: deployed or succeeded_no_deploy; any other command: success
1 check/watch: failed; fixture scrub --check: a fixture would change; retry/play: answered no
2 check/watch: deployed_with_failure
3 check/watch: running, parked_gate or canceled
4 check/watch: unknown (nothing matched, or a request failed; errors on stderr)
64 usage error: bad arguments, a --watch, --account or job URL that names nothing configured, a fixture scrub directory with no recordings, or retry/play with no terminal and no --yes
70 any other error (token, fixture, recording, I/O, or a log or job request that failed)
78 the configuration is missing, unreadable or invalid, or retry/play on an account without actions = true
Requirements: Rust 1.90 or newer (via rustup; the repository pins
the stable channel), Node.js 24 (CI's version; package.json also accepts 20.19+ and
22.12+), and the platform toolchain.
macOS: Xcode Command Line Tools (xcode-select --install).
Ubuntu 22.04 or newer:
sudo apt-get update
sudo apt-get install -y \
build-essential curl file \
libwebkit2gtk-4.1-dev \
libjavascriptcoregtk-4.1-dev \
libsoup-3.0-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
xdg-utils \
libsecret-tools
libwebkit2gtk-4.1 (not 4.0) is the Tauri 2 line and libayatana-appindicator3 puts the
icon in the tray; without the latter the app runs with no icon and no window, which looks
like a crash. libsecret-tools is a runtime dependency (secret-tool), not a build one.
No OpenSSL is needed: HTTPS is rustls.
Then:
git clone https://github.com/distronode-corporation/bridgewatch
cd bridgewatch
npm ci
npm run tauri build
Bundles land in target/release/bundle/ at the repository root. On a Mac with both
aarch64-apple-darwin and x86_64-apple-darwin installed through rustup,
npm run tauri build -- --target universal-apple-darwin makes the universal .dmg the
release ships, under target/universal-apple-darwin/release/bundle/. For development, see
CONTRIBUTING.md.
A plain npm run tauri build bundles LICENSE and NOTICE. To bundle the third-party
licence texts as a release does (which you should, if you distribute the build),
generate them first and pass the overlay config; this needs
cargo-about 0.9:
mkdir -p target/licenses
cargo about generate --locked --fail --manifest-path src-tauri/Cargo.toml -c about.toml \
-o target/licenses/THIRD-PARTY-LICENSES-rust.txt licenses/about.hbs
node scripts/third-party-licenses-npm.mjs target/licenses/THIRD-PARTY-LICENSES-npm.txt
npm run tauri build -- --config src-tauri/tauri.licenses.conf.json
ref by exact name and source by one value,
and GitHub's runs API filters branch and event the same way, so those watches read
the newest page for the project (4 × show.max_rows pipelines or runs, between 30 and
100) and filter it. A matching pipeline older than that page is not seen. On GitHub a
page without workflow holds every workflow's runs, so it covers less time.notify.click has no
effect yet. The popover row carries the same link.merge_request_event works as a source like any other,
but a merged-result pipeline is not paired with its branch pipeline.group = "commit" and expect in Settings or by hand.fan_out_secs after the rest of its push (a slow workflow_dispatch, a re-push of the
same commit) is a row of its own. See expect.cargo test -p bridgewatch-core, which runs against recorded
fixtures and scripted responses with no network and no token.On every push to main unless the item says otherwise:
cargo deny against deny.toml: RustSec advisories, a permissive-only
licence allow-list, crates.io as the only source. Tolerated advisories are listed
there with their reason, and mirrored in osv-scanner.toml for
Scorecard's Vulnerabilities check.cargo deny and of npm audit --audit-level=high, because an
advisory lands against code that did not change and so no push is coming to catch it.
The npm side audits the whole tree, dev dependencies included: this is a Vite app, so
they are what gets bundled into the shipped frontend.main ruleset requires have passed (all three rust legs, including
rust (ubuntu-22.04-arm), plus msrv, repo, frontend, cargo-deny, zizmor and
dependency-review). A major waits for a person.release environment that only a v* tag can reach and that
holds the macOS signing identity as its own environment secrets, so no branch and no
pull request can read it. Release tags are protected by a ruleset against moves and
deletion (repository administrators can bypass it).Apache License 2.0. See LICENSE and NOTICE.
The bridgewatch name and icon are trademarks of Distronode Corporation. They are not licensed under the Apache License 2.0: a build you distribute must use its own name and icon. bridgewatch is not affiliated with or endorsed by GitLab Inc. or GitHub, Inc.
warnbridgewatchmacos-private-api feature.primaryshow.max_rows | Most rows this watch shows [5]. |
show.settled | Settled pipelines kept below the unsettled ones [1]. A skipped pipeline ran nothing, is not listed and does not count. |
show.jobs | all or failures; overrides [ui].jobs for this watch [unset]. |
poll.live_secs | Interval while anything is live [5; the wizard writes 30 for GitHub]. |
poll.idle_secs | Interval when everything has settled [60; the wizard writes 120 for GitHub]. |
dive.bridges | Glob over trigger-job names to walk into, or over workflow names on a GitHub commit group; "" walks none [*]. |
dive.exclude | Trigger-job (or workflow) name globs to skip [[]]. |
dive.depth | Levels of child pipeline to walk [1]. GitLab only: GitHub runs do not nest. |
dive.only_when | Walk only into bridges in this status, e.g. "failed" [unset]. |
deploy_markers | Job names or re: patterns whose success means "deployed". With several, a success counts only once none of the others is still in flight; config order then decides which success is reported [[]]. |
sibling_failure | What a failed or dead bridge other than the marker's does to a deploy: downgrade, fail or ignore [downgrade]. |
post_deploy_failure | The same for a blocking failure after the marker, or in the marker's own pipeline [downgrade]. |
eta | Learn how long this watch's deploys take and show it on a running row, see Deploy ETA [true]. |
[watches.jobs] | Job-name pattern (literal or re:) to gate, warning, blocking or ignore. File order, first match wins. Applies to bridges too. On GitHub it is the only way to tolerate a failure. |
notify.deployed, notify.blocking_failure, notify.finished | Notify on these events [true]. |
notify.started, notify.gate_opened | Notify on these events [false]. |
notify.title, notify.body | MiniJinja templates over the pipeline view (watch.id, sha7, state, failures, warnings, gates, ref, source, url, ...). |
notify.click | first_failure_or_pipeline, pipeline, marker_job or none. Computed, but desktop notifications cannot open a URL yet, see Limitations. |