Code, changes and reviews

Version 4 · Updated

Branchscope actions

Workflows, checks and required checks, runs and live logs, your own runners and hosted runners.

Branchscope actions is CI built into Branchscope. Workflows live in the repository, in .branchscope/workflows. Pushes, changes, tags, releases, a schedule or a person start them, and their jobs run on runners: your organization's own machines, or Branchscope's hosted runners in the EU. Each job is a check on its commit. A repository's rulesets can require checks before a change merges into a protected branch, and agents read the checks and their logs over MCP like anything else.

A first workflow#

  1. Add a runner. On your organization's page, under Runners, press Add a runner. It shows a command with a registration token; run it on the machine, then start the runner:

    bscope-runner register --url runners.example.com:443 --token bsc_rrt_… --labels linux
    bscope-runner run
    
  2. Add a workflow as .branchscope/workflows/ci.yml:

    version: 1
    name: CI
    on:
      push: { branches: [main] }
      change: {}
    jobs:
      test:
        runs-on: linux
        image: node:24
        steps:
          - name: install
            run: npm ci
          - name: test
            run: npm test
    
  3. Push it. The push to main starts a run. So does every change you open, and every push to a change's branch. The run appears under Runs in the repository, and its job as the check CI / test on the commit.

Workflows#

A workflow is a YAML file in .branchscope/workflows (.yml or .yaml). The format is deliberately small: no expressions and no marketplace. Steps are shell commands, and they read what they need from the environment. Its JSON Schema is at /v1/workflow-schema.json, for editors and agents.

FieldWhat it is
versionAlways 1.
nameThe workflow's name; checks are named <name> / <job>.
onWhen it runs; at least one trigger (below).
envEnvironment variables for every job.
jobs.<id>.runs-onhosted for Branchscope's hosted runners, or a runner label: the job goes to one of the organization's runners with that label.
jobs.<id>.sizeOn hosted runners: 2cpu (the default), 4cpu or 8cpu (below). Ignored on your own runners.
jobs.<id>.imageA container image for the steps. Without one, a runner with Docker or Podman uses its default image; one without runs the steps on the machine itself.
jobs.<id>.stepsUp to 100 steps, each with a run (shell commands, run with sh -e) and an optional name.
jobs.<id>.needsJobs that must pass first. If one of them doesn't pass, this job is cancelled.
jobs.<id>.matrixA job per combination of values, at most 32 (below).
jobs.<id>.timeoutSuch as 90s, 20m or 2h: 30 minutes if left out, at most 6 hours.
jobs.<id>.envEnvironment variables for this job.
jobs.<id>.environmentThe environment the job deploys to, such as production: it records a deployment whose status follows the job.
jobs.<id>.secretsThe secrets the job gets, by name, such as [NPM_TOKEN], as environment variables.
jobs.<id>.permissionsid-token: write lets the job ask for identity tokens for AWS, Google Cloud or Azure.
jobs.<id>.cacheA cache, restored before the steps and saved after they pass.
jobs.<id>.artifactsArtifacts: files the job keeps, downloadable from the run.

Triggers#

TriggerRuns
push: { branches: [...] }On a push to a matching branch (all branches without branches), at the pushed commit.
change: { branches: [...] }When a change into a matching target branch is opened, and on every push to its branch.
schedule: "0 3 * * *"On a schedule, in cron syntax in UTC, at the default branch's latest commit.
manual: {}By hand: Run a workflow by hand on the Runs page, bscope run start ci.yml, or the API.
tag: { tags: ["v*"] }When a matching tag is created (any tag without tags), by a push or by publishing a release, at the tag's commit. Not when a tag is deleted. A moved tag runs again; its tag rulesets decide whether it may move.
release: { types: [published, prereleased] }When a release is published (published) or a pre-release is (prereleased), at its tag's commit; both without types. Drafts never run.

In branch and tag patterns, * matches any characters but / and ** matches / too, as everywhere in Branchscope (see patterns). Workflows are read from the commit being run, so a change can try out a new workflow before it merges. Schedules come from the default branch.

Tag patterns work as branch patterns do. A release that makes a new tag when it's published starts the tag workflows and the release workflows once each.

Matrices#

jobs:
  test:
    runs-on: linux
    matrix:
      node: [22, 24]
      os: [debian, alpine]
    steps:
      - run: npm test

Each combination is its own job and check, named like CI / test (node: 22, os: alpine). Its values are in the environment as MATRIX_NODE and MATRIX_OS.

What a step sees#

Variable
CI, BRANCHSCOPEtrue
BRANCHSCOPE_REPO, BRANCHSCOPE_SHA, BRANCHSCOPE_REFThe repository, the commit and the branch (refs/tags/<tag> in tag and release runs)
BRANCHSCOPE_TAGIn tag and release runs: the tag's name, such as v1.4.0
BRANCHSCOPE_RELEASE_TAG, BRANCHSCOPE_RELEASE_NAME, BRANCHSCOPE_RELEASE_PRERELEASEIn release runs: its tag, its title, and true for a pre-release (otherwise false)
BRANCHSCOPE_ENVIRONMENTIn a job with environment:: the environment's name
BRANCHSCOPE_RUN_ID, BRANCHSCOPE_JOB_IDThe run and the job
BRANCHSCOPE_TOKENThe job token: it can clone this repository, and nothing else, while the job runs
BRANCHSCOPE_ID_TOKEN_REQUEST_URL, BRANCHSCOPE_ID_TOKEN_REQUEST_TOKENIn a job with permissions: { id-token: write }: where and with what to ask for an identity token
MATRIX_*The job's matrix values

The checkout is the working directory, at the commit, detached. Branchscope hides the job token in logs (***).

Secrets#

Tokens for package registries, clouds and deploy targets are kept as secrets: a repository's (Settings → Secrets) or an organization's (its settings' Secrets tab, for all its repositories or only some). They're encrypted, and once saved they can't be read back, only replaced or deleted. Repository administrators and organization owners set them; agents and apps can't see, set or list them.

jobs:
  publish:
    runs-on: hosted
    secrets: [NPM_TOKEN]
    steps:
      - run: npm publish
  • Only what it names: a job gets the secrets it lists, as environment variables of the same name, and no others. An environment's secret (only for jobs deploying there) wins over its repository's of the same name, and a repository's over its organization's; a name with no secret is left unset.
  • Masked: a secret's value, its base64 and its URL-encoded forms, and each line of a multi-line one, show as *** in the job's log. Values shorter than 4 characters aren't masked.
  • Agents' runs wait too: so an agent can't read a secret by editing a workflow on its branch, a run an agent or app set off gets no secrets and no identity tokens, whoever it acts for: its push, merge or tag, a run it started or ran again, and a change it opened (whoever pushes to it later) or whose latest commit it pushed. The run waits for Approve and run, as below, which only a person can choose, never an agent. A person's rerun of an agent's run waits too, unless they're an administrator: then it's approved. The exception: a run at the latest commit of a branch or tag that an active ruleset with Block agent updates covers, and no agent bypasses, gets them, since that commit got there through a person. The run's page says why its secrets are withheld.
  • Untrusted runs wait: a run set off by someone without write access to the repository gets no secrets and no identity tokens. Its jobs that name secrets or ask for identity tokens wait until an administrator reads the workflow and chooses Approve and run on the run's page (or runs bscope run approve <id>); jobs without secrets run as usual.
  • Not in the run's records: secrets reach a job only when a runner takes it.

A server needs SECRETS_KEY set (32 bytes, base64) to keep secrets; without it, none can be set and runs get none. From the CLI: bscope secret set NPM_TOKEN < token.txt, bscope secret list, bscope secret delete NPM_TOKEN, with --org o for an organization's and --env e for an environment's.

Identity tokens (OIDC)#

Instead of keeping a cloud key as a secret, a job can ask Branchscope for a short-lived identity token (an OpenID Connect JWT) and trade it with AWS, Google Cloud or Azure for credentials of their own. Branchscope is the issuer: its address, such as https://branchscope.example (a server's OIDC_ISSUER), with its discovery document at /.well-known/openid-configuration and its keys at /.well-known/jwks.

jobs:
  deploy:
    runs-on: hosted
    environment: production
    permissions:
      id-token: write
    steps:
      - run: |
          TOKEN=$(curl -sf -H "Authorization: Bearer $BRANCHSCOPE_ID_TOKEN_REQUEST_TOKEN" \
            "$BRANCHSCOPE_ID_TOKEN_REQUEST_URL?audience=sts.amazonaws.com" | jq -r .value)
  • Asking for one: a job with permissions: { id-token: write } gets BRANCHSCOPE_ID_TOKEN_REQUEST_URL and BRANCHSCOPE_ID_TOKEN_REQUEST_TOKEN. A GET to the URL with that token and ?audience= returns {"value": "<token>"}. The request token works only while the job runs. Without an audience, it's the repository owner's address.
  • The token: signed with RS256, valid for 10 minutes. Its subject (sub) is repo:<owner>/<name>:environment:<environment> for a job with environment:, otherwise repo:<owner>/<name>:ref:refs/heads/<branch> (or refs/tags/<tag>). It also has repository, repository_owner, ref, ref_type, sha, workflow, workflow_ref, run_id, job, event, runner_environment (hosted or self-hosted), and actor, environment and tag when there are.
  • The rules secrets follow: none for a run whose secrets are withheld until an administrator approves it, and none before the job's environment's protection passed. A token in the log shows as ***. The run's page lists the tokens each job was issued, by audience.

To trust it, give the cloud the issuer URL and the subject you expect. With an environment's subject, such as repo:acme/web:environment:production, only jobs its protection let through get in.

  • AWS: add an IAM OpenID Connect identity provider with the issuer URL and the audience sts.amazonaws.com. Give a role a trust policy allowing sts:AssumeRoleWithWebIdentity for that provider when <issuer host>:aud is sts.amazonaws.com and <issuer host>:sub is the subject. In the job: aws sts assume-role-with-web-identity --role-arn <role> --role-session-name deploy --web-identity-token "$TOKEN".
  • Google Cloud: make a workload identity pool with an OIDC provider for the issuer URL, mapping google.subject=assertion.sub, with a condition such as assertion.repository == 'acme/web', and let that subject's principal impersonate a service account. Ask for the token with the provider's name as audience (https://iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/<pool>/providers/<provider>), write it to a file, and sign in with gcloud auth login --cred-file=<credential configuration> that reads it.
  • Azure: on an app registration or a user-assigned managed identity, add a federated credential with the issuer URL, the subject, and the audience api://AzureADTokenExchange. In the job: az login --service-principal -u <client id> -t <tenant id> --federated-token "$TOKEN".

A server needs OIDC_SIGNING_KEY set for identity tokens (see self-hosting); without it, asking for one says they're off.

When a workflow is wrong#

A workflow that isn't valid doesn't run. It shows as a failed check named after the file, with the line and what's wrong, for example workflow .branchscope/workflows/ci.yml with "line 15: jobs.deploy.needs: there's no job build".

Checks#

A check is a result on a commit: queued, running, passed, failed, cancelled or timed out, with a summary line and a link. A change shows the checks of its latest commit.

Branchscope's own jobs are checks. Other systems can report theirs too: an OAuth app with the checks:write scope, or a person with write access, posts to POST /v1/repos/{owner}/{name}/commits/{sha}/checks. Posting the same name again updates the check. When a check finishes, the check.completed webhook event is sent.

Required checks#

A branch ruleset's Require status checks to pass names the checks that must pass before a change merges into the branches it targets (Settings → Branches; see Rulesets). They must pass on the change's latest commit. A ruleset that requires checks also requires changes: direct pushes to those branches are refused, since they can't wait for checks. A tag ruleset can require checks on a tag's commit, which a release checks before it makes the tag.

While a required check is failing, missing or still running, Merge is refused, for people and agents alike. Those on the ruleset's bypass list aren't held to its checks. An administrator of the repository can Merge anyway with a reason. The change keeps a record of which checks were skipped, who merged and why, and its change.merged event carries it. Agents can't merge anyway.

Runs#

A run is one workflow at one commit, with its jobs. The Runs page lists a repository's runs. A run's page shows its jobs, with the chosen job's log, followed live while it runs.

  • A newer push supersedes: when a change's branch moves on, the change's older runs of the same workflow are cancelled.
  • Cancel stops a run: queued jobs at once, running ones within 15 seconds (at their runner's next heartbeat).
  • Run again starts the workflow afresh at the same commit.
  • A job's log is kept when it finishes, up to 16 MB.

From the CLI: bscope run list, bscope run view <id>, bscope run log <id> --follow, bscope run start <workflow>, bscope run cancel <id>, bscope run rerun <id>, bscope run artifacts <id>.

Caches and artifacts#

A job can restore a cache before its steps, so it doesn't download its dependencies afresh every time, and keep artifacts after them: a build, a coverage report, test results. Both are .tar.gz files of paths in the workspace (the checkout), kept by Branchscope, and they work the same on hosted runners and your own. Runners send and fetch them through Branchscope with the job's token, while the job runs; they never get the storage's credentials.

Caches#

jobs:
  test:
    runs-on: hosted
    image: node:24
    env:
      npm_config_cache: .npm
    cache:
      key: npm
      hash-files: [package-lock.json]
      paths: [.npm]
      restore-keys: [npm-]
    steps:
      - run: npm ci
      - run: npm test
FieldWhat it is
keyThe cache's name: letters, digits, ., _ and -.
hash-filesOptional: files the cache depends on, such as a lockfile. The key then ends with - and a hash of their contents at the run's commit, so changing them starts a new entry.
pathsUp to 20 files or directories, relative to the workspace: no ~, .., leading / or wildcards.
restore-keysOptional: prefixes to try, in order, when the key itself isn't found; the newest entry that starts with one is restored.
  • Before the first step, the job restores the first of: its key on the run's branch (or tag); its restore keys there; then the same on the repository's default branch. A miss isn't an error. The run's page shows each job's cache: restored, from where, or a miss.
  • After the last step passes, the job saves its paths under its key on the run's branch, unless that key was restored exactly. An entry is never overwritten: change the key (or a hash-files file) to save a new one. A job that fails, is cancelled or times out saves nothing.
  • Kept apart: an entry belongs to its repository, branch, workflow, paths and key. Another workflow's, or one for other paths, is never restored.
  • Agents never write the default branch's cache. A run an agent set off restores from its own branch and the default branch, and saves to its own branch, but not to the default branch, even when it runs there. So an agent can't put something in what every branch restores.
  • Paths in the workspace: tools that cache in your home directory need pointing there: npm_config_cache: .npm, PNPM_STORE_DIR: .pnpm-store, GOMODCACHE: ${PWD}/.gomod (in a step), PIP_CACHE_DIR: .pip.

Repository administrators see the caches under Settings → Caches, with how much of the limit they use, and clear them there (or with DELETE /v1/repos/{owner}/{name}/actions/caches, on the audit log).

Artifacts#

jobs:
  test:
    runs-on: hosted
    steps:
      - run: npm test -- --coverage
    artifacts:
      - name: coverage
        paths: [coverage/]
        when: always
        retention-days: 14
FieldWhat it is
nameLetters, digits, ., _ and -; each once in its job.
pathsUp to 20 files or directories, relative to the workspace, as for caches.
whenon_success (the default): after the last step passed. always: after a failed step too, for test reports. Never for a cancelled or timed-out job.
retention-daysHow long it's kept: 30 days if left out, at most 90.

artifacts: takes one artifact or a list of up to 10. A path that isn't there is left out, and said so in the log; with none at all, nothing is kept. The run's page lists its artifacts with their job, size and expiry, and downloads each as <name>.tar.gz. Anyone who can read the repository can download them, agents too: from the API (GET /v1/repos/{owner}/{name}/runs/{id}/artifacts/{artifact}) or the CLI (bscope run artifacts <id> --download coverage). Expired artifacts are deleted once a day.

Limits#

A cache entry2 GB
A repository's caches10 GB; past it, the entries restored least recently are deleted
An unused cache entrydeleted after 7 days without a restore
An artifact2 GB
Artifacts per job10
Kept30 days by default, at most 90

A server can set other limits (see Running Branchscope). A cache or artifact over its limit isn't kept, and the log says why; the job's result doesn't change.

Secrets in caches and artifacts#

Caches and artifacts aren't logs: nothing in them is masked. A secret that a step writes to a file ends up in any cache or artifact holding that file, and anyone who can read the repository can download it. Keep secrets out of the paths you cache or keep (an .npmrc with a token, a cloud credentials file), or delete them in a last step. Keys and artifact names are literal text from the workflow, never expanded, so they don't hold secrets unless you type one in. A run an agent set off has no secrets to write unless an administrator approved it.

Deployments#

A job with environment: production records a deployment from its run: that its commit runs in that environment, with the tag or branch it came from. Its status follows the job, and its steps see BRANCHSCOPE_ENVIRONMENT. An environment can require a protected tag, and refuses a job from anything else before its first step. The Deployments tab shows what's running where, and Deploy again runs a deployment's workflow again, for a rollback. See Deployments for environments, protection, recording deployments from outside Branchscope, and requiring them before a merge or a tag. An environment can also require reviewers and a wait timer: a job deploying there then waits, before its first step, its secrets and its identity tokens, until a reviewer approves it and the timer is over (see Required reviewers and wait timers).

Hosted runners#

Jobs with runs-on: hosted run on Branchscope's own machines in the EU, so you don't need a runner of your own. Hosted runners are in early access: a site administrator allows an account or organization first, and sets how many of its hosted jobs may run at once (2, unless they say otherwise). A hosted job of an account that isn't allowed fails at once, and says why.

jobs:
  test:
    runs-on: hosted
    size: 4cpu
    image: node:24
    steps:
      - run: npm ci && npm test
sizeCPUsMemoryDisk
2cpu (the default)24 GB20 GB
4cpu48 GB20 GB
8cpu816 GB20 GB
  • Each job gets a microVM of its own (Firecracker), on linux/amd64, thrown away when the job ends. Steps run as root inside it. Without an image, it's debian:stable-slim.
  • The network is the internet, except private and link-local addresses, the cloud metadata address, the machine itself, Branchscope's own servers other than over HTTPS (the API and git), and mail ports. New connections are rate-limited, and their destinations (not their contents) are logged, for abuse reports.
  • Jobs wait in the queue while your account is at its limit, or the machines are full; jobs never share memory.
  • Runs show the runner as Hosted (EU), with the job's size. Your organization's page shows how many of its hosted jobs are running, against its limit.

Runners#

A runner is a machine of your organization's that runs jobs: a server, a VM, or the Mac on the desk for iOS builds. It runs bscope-runner, which connects out to Branchscope over TLS, so it needs no open ports.

Adding and removing#

Organization owners add runners from the organization's page. The registration token works for an hour, for any number of runners. bscope-runner register trades it for the runner's own credential, saved to ~/.config/bscope-runner/runner.json (the right place for each system), readable only by you.

bscope-runner register
--urlWhere runners connect (the command shown has it)
--tokenThe registration token, or BRANCHSCOPE_RUNNER_TOKEN
--nameThe runner's name in the organization; the host name by default. Registering a name again replaces that runner.
--labelsComma-separated; by default its system and architecture, such as linux,x64 or macos,arm64
bscope-runner run
--runtimedocker, podman, none (steps on this machine), or auto (the default: Docker, else Podman, else none)
--default-imageFor jobs without an image when there's a runtime; debian:stable-slim by default
--work-dirWhere job workspaces go; each is removed when its job ends
--onceExit after one job

Members see the organization's runners: online or not, idle or busy. Owners remove them; a removed runner's credential stops working at once, and a job it was running goes to another runner.

How a job runs#

  1. The runner takes a queued job whose runs-on is one of its labels, oldest first.
  2. It clones the commit with the job token, and restores the job's cache into the checkout.
  3. With a container runtime, it starts one container for the job, with the checkout at /workspace, and runs each step in it. Without one, it runs each step in the checkout, in a process group of its own.
  4. The first step that fails fails the job, with its exit code. The log streams to Branchscope as the job runs.
  5. It saves the cache and uploads the artifacts from the checkout. With a container runtime, files the steps made as root must be readable by the runner's user.
  6. Every 15 seconds the runner tells Branchscope it's still there, and hears whether to stop. A runner that goes quiet for about a minute loses its job, which goes to another runner once, then fails.

Keep runners to themselves#

A runner runs whatever the workflows in your repositories say, including those on a change's branch. Anyone who can push a branch and open a change can run code on it. So:

  • Give runners machines of their own, not ones with other credentials or data on them.
  • Prefer a container runtime. Without one, steps run as the runner's user, on the machine itself.
  • Use separate runners, with their own labels, for jobs that need more trust, such as deploying.

Agents and checks#

Agents work with checks over MCP:

  • list_checks shows a change's checks and whether the required ones passed.
  • wait_checks waits for them, up to a minute, and the session is told when they finish.
  • get_check_log reads a failed job's log, or one step's.
  • rerun_checks runs the failed workflows again.

So an agent can push, wait, read why a test failed, fix it and push again, without anyone polling. Agents can't merge past a failing required check.