View as Markdown

Repository selection

Agent hint

Rules on this page that most often cause invalid routines:

  • repositories and repositories.include are required. include is the word all or a list of 1 to 50 selectors. A routine with only exclude is invalid, and so is include: [].
  • A selector has exactly one of id (a repository UUID) or name. A name must have the form owner/name (it contains a /); * and ? stay inside one segment and only a whole segment ** crosses /. Names are case-insensitive and at most 256 bytes.
  • exclude has the same selectors (up to 50) and always wins: a repository is covered when it is included and not excluded.
  • A schedule or webhook routine runs once per selected repository and must select 1 to 200 repositories at save time. An event routine is not fan-out limited: each event matches only the repository it came from.
  • Only repositories you have already connected to Forkline can be covered. New ones that match a glob are covered automatically.

repositories says which of your repositories a routine covers. The result is everything in include that is not in exclude:

repositories:
  include:
    - name: "acme/api-*"
    - id: 7f1c2c3e-0000-4000-8000-000000000001
  exclude:
    - name: "acme/api-legacy"
Field Required Values
repositories.include yes all, or a list of 1 to 50 selectors
repositories.exclude no up to 50 selectors (an empty list excludes nothing). Omit it for none

Selectors

A selector is an object with exactly one of two keys:

Selector Matches Notes
id: <uuid> The one repository with this Forkline repository ID The ID is Forkline’s own UUID (forkline repo view <name> prints the full id; forkline repo list shows a shortened ID, the full one with -o wide or --json), not the number the forge uses. Stable when the repository is renamed
name: "<owner/name>" Every repository whose full name matches the glob The full name is owner/name. On GitLab it is the nested path, group/subgroup/project

Setting both keys, or neither, is the error set exactly one of `id` and `name` at the selector (/repositories/include/0). A name without a / is the error use the form `owner/name`, with `*` and `**` as wildcards at /repositories/include/0/name. An id that is not a UUID is an error at .../id.

Name globs

Pattern Matches Does not match
acme/api acme/api (and ACME/Api: case does not matter) acme/api-core
acme/* every repository of acme acme/team/tool (a * stays inside one segment)
acme/api-* acme/api-core, acme/api-web acme/api (the - is literal)
acme/service-? acme/service-a acme/service-ab
*/docs the repository docs of any owner acme/team/docs
acme/** acme/api and nested acme/team/api, acme/team/sub/api (GitLab) other/api
**/infra infra under any path acme/infra-old

Rules: * matches any run of characters inside one /-separated segment (also none); ? matches one character; a segment that is exactly ** matches any number of segments, including none. ** inside a segment (acme/api**) behaves like *. There are no character classes ([a-z]) and no braces ({a,b}): list two selectors instead. Matching is case-insensitive and applies to the whole name. A pattern is 1 to 256 bytes.

include: all

all covers every repository you have connected, including ones you connect later. Use it for routines that a filter narrows anyway (a comment.created routine with a command filter), and avoid it for schedules and webhooks, where every covered repository starts its own run. all is not capped at 200: on a schedule or webhook, all is valid only while you have 1 to 200 connected repositories. Saving it with more is refused with 422 at /repositories, and a routine saved earlier whose account later grows above 200 is refused at every firing (outcome fan_out_refused, stage fan_out) and starts no run, not even for the first 200. Use include with globs and exclude to stay under 200.

Excluding

exclude takes the same selectors and wins over include. Excluding a name that is not included does nothing.

name: review-api-prs
description: Review pull requests of every acme API repository except the legacy one
on:
  events: [pr.opened, pr.updated]
repositories:
  include:
    - name: "acme/api-*"
    - id: 7f1c2c3e-0000-4000-8000-000000000001
  exclude:
    - name: "acme/api-legacy"
    - name: "acme/api-sandbox-*"
agent:
  prompt: |
    Review pull request #{{ pr.number }} in {{ repository.fullName }}.
name: triage-all-issues
description: Triage new issues in every connected repository
on:
  events: [issue.opened]
repositories:
  include: all
  exclude:
    - name: "acme/playground"
agent:
  prompt: |
    Triage issue #{{ issue.number }} "{{ issue.title }}" in {{ repository.fullName }}.

A GitLab group with nested projects:

name: review-platform-mrs
description: Review merge requests in every project below the platform group
on:
  events: [pr.opened]
repositories:
  include:
    - name: "platform/**"
agent:
  prompt: |
    Review merge request !{{ pr.number }} in {{ repository.fullName }}.

Which repositories are considered

Only repositories that you added to Forkline and that belong to a connected, not deleted connector are considered (for GitHub, a repository whose GitHub App installation is connected). A selector that matches none of them, for example an id of a repository you removed or one that is not yours, simply matches nothing: it is not an error today. The selection is evaluated against your repositories each time the routine fires, not when you save it. A repository you connect later is covered at once if it matches the selection, and one you remove stops being covered.

Event routines

An event routine (on.events) matches only the repository the event came from. When an event arrives, Forkline finds the repository (by connector and repository URL), then checks it against include and exclude. The routine starts at most one run for that event; filters then decide whether it starts. There is no fan-out and no limit of 200. A selection that matches no repository is accepted for an event routine and never runs until one matches.

Fan-out

Schedules and custom webhooks fan out.

A schedule (on.schedule) or a custom webhook (on.webhook) is not tied to one repository, so it starts one run per selected repository:

  • Each repository gets its own prompt: repository.* in the template is that repository, and the runner checks out that repository’s default branch (see Which ref a run checks out).
  • Each repository is checked, run and recorded on its own. The when condition is the only filter allowed here and sees repository (and payload on a webhook); it is evaluated per repository, so when: '!repository.private' skips the private ones. A repository Forkline has not synced yet reads as repository.private == false for schedules and webhooks. One repository failing, being filtered, being skipped because of concurrency or lacking credits does not stop the others.
  • A schedule that fires once and selects 6 repositories starts 6 runs, and costs 6 runs of credits. Check the count before you publish.
  • A webhook call, or a Run now, returns the task_ids of the runs it started. A call that starts nothing returns a reason: no_matching_repos when the selection matches no repository, otherwise the most telling outcome (for example credit_exhausted).
  • Run now starts one run per selected repository too. Through the API or CLI you can name a subset of the selection (repositories, --repository); a repository outside the selection is refused with 422 repository_not_selected. See Run now.
  • Retries of the same occurrence do not start the run twice: a schedule occurrence is keyed by its scheduled time and repository, and a webhook call by its Idempotency-Key header and repository (without the header every call runs).

Limits and what happens at them

Situation When Result
Selection matches 0 repositories (schedule, webhook) Save and publish Refused with status 422 at /repositories: the selection matches none of your repositories, so the routine would never run. forkline routine validate and POST /v1/routines/validate return the same 422. Only an event routine whose selection matches nothing can be saved; validate reports it as the warning no_repositories (“No repository matches this selection right now. The routine won’t run until one does.”)
Selection matches more than 200 repositories (schedule, webhook) Save and publish Refused with status 422 at /repositories: the selection matches {count} repositories, over the limit of 200; narrow it with `include` and `exclude`
The selection grows above 200 after the routine was saved (you connected more repositories) Each firing The whole firing is refused: outcome fan_out_refused at stage fan_out, and no repository gets a run. The reason reads: This schedule now matches {n} repositories, above the limit of 200. Narrow the repository selection. (webhook for a webhook). The next firing evaluates the selection again, so narrowing it fixes the routine
More than 50 selectors in include or exclude Save 422 at /repositories/include or /repositories/exclude: at most 50 selectors, this has {n}. Use globs
Event routine Never limited by the fan-out cap

The firing is refused as a whole, never partly: a routine that matches 214 repositories does not run on the first 200. Narrow the selection (more exclude entries, or a tighter glob) and save the routine.

New repositories that match a glob are picked up automatically. This is the way a routine can cross the limit unnoticed, so schedules and webhooks should use selective patterns (acme/api-*) rather than all, or an exclude list.

Schedules and webhooks in practice

name: weekly-dependency-update
description: Update dependencies in the Go services every Monday
on:
  schedule:
    cron: "0 6 * * 1"
    timezone: Europe/Berlin
repositories:
  include:
    - name: "acme/svc-*"
  exclude:
    - name: "acme/svc-archive"
filters:
  when: '!repository.private'
agent:
  prompt: |
    Update the dependencies of {{ repository.fullName }} (default branch {{ repository.defaultBranch }}).
    Open a pull request with the changes.
name: deploy-failed
description: Investigate a failed deployment reported by the pipeline for the one repository it names
on:
  webhook: {}
repositories:
  include:
    - name: "acme/api"
agent:
  prompt: |
    A deployment of {{ repository.fullName }} failed: {{ payload.message | default("no message", true) }}
    Find out why and open an issue.

Invalid selections

A name that is not owner/name:

name: bad-name
description: Selects by a name without an owner
on:
  events: [pr.opened]
repositories:
  include:
    - name: "api-*"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.

Fix: name: "acme/api-*", or name: "*/api-*" for any owner.

A selector with both keys:

name: both-keys
description: Sets id and name on one selector
on:
  events: [pr.opened]
repositories:
  include:
    - id: 7f1c2c3e-0000-4000-8000-000000000001
      name: "acme/api"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.

Fix: use two selectors, or only one key.

An empty include list:

name: nothing-included
description: Includes no repository
on:
  events: [pr.opened]
repositories:
  include: []
agent:
  prompt: |
    Review pull request #{{ pr.number }}.

Fix: include: all, or list at least one selector. A routine that should not run yet is a draft, not an empty selection.

An id that is not a UUID (it is the Forkline repository ID, not the forge’s number):

name: bad-id
description: Uses a forge repository number as the id
on:
  events: [pr.opened]
repositories:
  include:
    - id: "12345"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.

A bad name in exclude is reported at /repositories/exclude/{i}/name:

name: bad-exclude
description: Excludes a name without an owner
on:
  events: [pr.opened]
repositories:
  include: all
  exclude:
    - name: "legacy"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.

Only exclude is not enough, include is required (the pointer is the missing key, /repositories/include):

name: only-exclude
description: Excludes without including
on:
  events: [pr.opened]
repositories:
  exclude:
    - name: "acme/legacy"
agent:
  prompt: |
    Review pull request #{{ pr.number }}.

Selector errors that depend on your account (more than 200 repositories, no repository at all) cannot be reproduced from the file alone; they are listed in Errors that need more than the file.