Repository selection
Agent hint
Rules on this page that most often cause invalid routines:
repositoriesandrepositories.includeare required.includeis the wordallor a list of 1 to 50 selectors. A routine with onlyexcludeis invalid, and so isinclude: [].- A selector has exactly one of
id(a repository UUID) orname. Anamemust have the formowner/name(it contains a/);*and?stay inside one segment and only a whole segment**crosses/. Names are case-insensitive and at most 256 bytes. excludehas 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
whencondition is the only filter allowed here and seesrepository(andpayloadon a webhook); it is evaluated per repository, sowhen: '!repository.private'skips the private ones. A repository Forkline has not synced yet reads asrepository.private == falsefor 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_idsof the runs it started. A call that starts nothing returns areason:no_matching_reposwhen the selection matches no repository, otherwise the most telling outcome (for examplecredit_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 with422repository_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-Keyheader 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.