Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

684 lines · 37.5 KBRaw History
1---
2title: Events that trigger workflows
3description: Every event that can start a Diffinery workflow, its activity types and filters, the commit and ref it runs on, and its github.event payload.
4---
5
6The `on:` section of a workflow says which events start it. Diffinery supports the GitHub Actions events that matter for CI and automation: pushes, pull requests, merge queue groups, manual dispatch, schedules, other workflows' runs, published releases, external `repository_dispatch` events and reusable workflow calls. This page lists each one with its activity types, filters, the `GITHUB_SHA` and `GITHUB_REF` its runs get, and its `github.event` payload.
7
8## Supported events
9
10| Event | Fires when | Activity types | Filters |
11| --- | --- | --- | --- |
12| [`push`](#push) | A branch or tag is created or updated | | `branches`, `branches-ignore`, `tags`, `tags-ignore`, `paths`, `paths-ignore` |
13| [`pull_request`](#pull_request) | A pull request is opened, updated, edited, reopened, closed, marked ready for review or converted to a draft | `opened`, `synchronize`, `reopened`, `closed`, `edited`, `ready_for_review`, `converted_to_draft` | `types`, `branches`, `branches-ignore`, `paths`, `paths-ignore` |
14| [`pull_request_target`](#pull_request_target) | Same as `pull_request`, but runs the base branch's workflow | Same as `pull_request` | Same as `pull_request` |
15| [`merge_group`](#merge_group) | The merge queue builds a merge group | `checks_requested` | `types`, `branches`, `branches-ignore` |
16| [`workflow_dispatch`](#workflow_dispatch) | Someone starts the workflow from the UI, CLI, API or MCP | | `inputs` |
17| [`schedule`](#schedule) | A cron schedule is due | | `cron` |
18| [`workflow_run`](#workflow_run) | A run of another named workflow is requested or completes | `requested`, `completed` | `workflows`, `types`, `branches`, `branches-ignore` |
19| [`release`](#release) | A release is published, unpublished, edited or deleted | `published`, `unpublished`, `created`, `edited`, `deleted`, `prereleased`, `released` | `types` |
20| [`repository_dispatch`](#repository_dispatch) | Someone calls `POST /repos/{owner}/{repo}/dispatches` | Any `event_type` you choose | `types` |
21| [`workflow_call`](#workflow_call) | Another workflow calls this one as a reusable workflow | | `inputs`, `outputs`, `secrets` |
22
23### Other GitHub events
24
25The other GitHub event names (`issues`, `issue_comment`, `create`, `delete`, `pull_request_review`, `check_run`, `status`, `deployment`, `deployment_status`, `label`, `watch` and the rest of GitHub's list) are accepted in `on:` without an error or warning, so workflows that list them still parse, but Diffinery never starts a run for them. A name that isn't a GitHub event at all gives the warning `unknown event '<name>'` in `diffinery workflow validate`.
26
27## Writing on
28
29```yaml
30on: push # one event, no configuration
31```
32
33```yaml
34on: [push, pull_request] # several events, no configuration
35```
36
37```yaml
38on: # events with configuration
39 push:
40 branches: [main]
41 tags: ["v*"]
42 pull_request:
43 types: [opened, synchronize, reopened, closed]
44 paths: ["src/**"]
45 workflow_dispatch:
46```
47
48An event's configuration must be a mapping (`on.<event> must be a mapping`), except `schedule`, which is a list. Each workflow file whose `on:` matches an event gets one run for it.
49
50## Filter patterns
51
52Branch, tag and path filters use GitHub's filter pattern syntax:
53
54| Pattern | Matches |
55| --- | --- |
56| `*` | Any characters except `/`. `feature/*` matches `feature/login` but not `feature/a/b`. |
57| `**` | Any characters, including `/`. `feature/**` matches `feature/a/b`. |
58| `**/` at the start | Any directories, or none. `**/README.md` matches `README.md` and `docs/README.md`. |
59| `?` | Zero or one of the preceding character. `v1?` matches `v` and `v1`. |
60| `+` | One or more of the preceding character. `v1+` matches `v1` and `v111`. |
61| `[abc]`, `[0-9]` | One character from the set or range. |
62| `\` | Escapes the next character, for example `\*` for a literal `*`. |
63| `!pattern` | In a list, excludes what earlier patterns matched. |
64
65Patterns match the whole name: a branch name without `refs/heads/`, a tag name without `refs/tags/`, or a file path relative to the repository root. Quote patterns that start with `*`, `!` or `[` in YAML.
66
67A list is read in order, and the last pattern that matches decides: a value is included when it matches a positive pattern and no later `!` pattern excludes it. A `!` pattern can only exclude what an earlier pattern included, so a list of only `!` patterns matches nothing.
68
69```yaml
70on:
71 push:
72 branches:
73 - "releases/**"
74 - "!releases/**-alpha"
75```
76
77This runs for `releases/10` and `releases/beta/mona`, but not `releases/10-alpha`.
78
79### Include and ignore filters
80
81- `branches` runs the workflow only for matching branches; `branches-ignore` runs it for every branch except matching ones. The same goes for `tags` and `tags-ignore`, and `paths` and `paths-ignore`.
82- You can't use a filter and its `-ignore` form for the same event. The file fails with `'branches' and 'branches-ignore' cannot be used together for the same event` (or the same message for `tags` or `paths`). Use `!` patterns in the positive form instead.
83
84### Path filters
85
86- With `paths`, the workflow runs when at least one changed file matches the list.
87- With `paths-ignore`, the workflow runs unless every changed file matches the list.
88- For a push, the changed files are the difference between the commit before the push and the pushed commit. When the push creates a new branch, or the previous commit isn't known, Diffinery can't compute the change and path filters pass. (GitHub compares a new branch with the default branch instead.)
89- For a pull request, the changed files are those the pull request changes, compared with the merge base of the head and the base branch. When a merged pull request is closed, path filters pass.
90- Path filters are never evaluated for tag pushes.
91- When branch or tag filters and path filters are both set, both must pass.
92
93## push
94
95Runs when you push to a branch or tag, including pushes that create a branch or tag, force pushes, pushes by agents, and merges made through the web UI, CLI, API or merge queue. Deleting a branch or tag doesn't start runs.
96
97```yaml
98on:
99 push:
100 branches: [main, "release/**"]
101 tags: ["v*.*.*"]
102 paths-ignore: ["docs/**", "**.md"]
103```
104
105How `branches` and `tags` combine, as on GitHub:
106
107| Filters set | Branch pushes | Tag pushes |
108| --- | --- | --- |
109| None | Run | Run |
110| Only `branches` or `branches-ignore` | Run when the branch matches | Don't run |
111| Only `tags` or `tags-ignore` | Don't run | Run when the tag matches |
112| Both kinds | Run when the branch matches | Run when the tag matches |
113| Only `paths` or `paths-ignore` | Run when the paths match | Run (paths aren't checked for tags) |
114
115The workflow file is read from the pushed commit, so a push that changes a workflow runs the new version.
116
117When a merge queue builds a merge group, it pushes a branch named `gh-readonly-queue/<base>/pr-<n>-<head sha>`, and `push` workflows run on it as on GitHub. Add `branches-ignore: ["gh-readonly-queue/**"]` (or list only the branches you want) to avoid running twice.
118
119| Variable | Value |
120| --- | --- |
121| `GITHUB_SHA` | The pushed commit |
122| `GITHUB_REF` | The pushed ref, `refs/heads/<branch>` or `refs/tags/<tag>` |
123
124Payload (`github.event`):
125
126| Field | Value |
127| --- | --- |
128| `ref` | The pushed ref |
129| `before` | The commit the ref pointed to before the push (`0000000000000000000000000000000000000000` for a new ref) |
130| `after` | The pushed commit |
131| `created`, `deleted`, `forced` | Whether the push created the ref, deleted it (always `false` for runs) or was a force push |
132| `head_commit` | `id`, `message`, `timestamp` (committer time) and `author` (`name`, `email`) of the pushed commit |
133| `commits` | A list with the pushed commit only (`id`, `message`). GitHub lists up to 20 commits. |
134| `pusher` | `name`: the login of the account that pushed, and `email`: `null` |
135| `sender` | See [The sender object](#the-sender-object) |
136| `repository` | See [The repository object](#the-repository-object) |
137
138## pull_request
139
140Runs for activity on a pull request, using the workflow and code of the pull request's test merge commit: the head branch merged into the current base branch, as on GitHub.
141
142```yaml
143on:
144 pull_request:
145 types: [opened, synchronize, reopened]
146 branches: [main]
147 paths: ["src/**", "package.json"]
148```
149
150| Type | Fires when |
151| --- | --- |
152| `opened` | A pull request is created (drafts included) |
153| `synchronize` | New commits are pushed to the head branch |
154| `reopened` | A closed pull request is reopened |
155| `closed` | A pull request is closed or merged. Check `github.event.pull_request.merged` to tell them apart. |
156| `edited` | The title or description changed. Not for label changes, and not when the text saved is the same. |
157| `ready_for_review` | A draft is marked ready for review: **Ready for review** in the web UI, `diffinery pr ready`, or `PATCH /repos/{owner}/{repo}/pulls/{number}` with `"draft": false` |
158| `converted_to_draft` | An open pull request is turned back into a draft: `diffinery pr ready --undo`, or `PATCH` with `"draft": true` |
159
160Without `types`, the workflow runs for `opened`, `synchronize` and `reopened`, as on GitHub. GitHub's other types (`labeled`, `assigned`, `review_requested`, `locked` and so on) are accepted in `types` but never fire on Diffinery.
161
162A common use is to skip expensive jobs while a pull request is a draft and run them once it's ready:
163
164```yaml
165on:
166 pull_request:
167 types: [opened, synchronize, reopened, ready_for_review]
168jobs:
169 e2e:
170 if: ${{ !github.event.pull_request.draft }}
171 runs-on: ubuntu-latest
172 steps:
173 - uses: actions/checkout@v4
174 - run: npm ci && npm run e2e
175```
176
177For `edited`, the payload has no `changes` object with the old title or body (GitHub sends one).
178
179`branches` and `branches-ignore` filter on the pull request's base branch.
180
181- The run checks out and runs `refs/pull/<n>/merge`. You can fetch that ref and `refs/pull/<n>/head` with git, but not push to them.
182- While a pull request conflicts with its base, there is no test merge commit and its `pull_request` workflows don't run. `pull_request_target` workflows still do.
183- For `closed` on a merged pull request, the run uses the merge commit instead.
184- The run's checks are reported on the pull request's head commit, so they appear on the pull request.
185- Every pull request comes from a branch of the same repository, so `pull_request` runs get the repository's secrets, like `push` runs.
186
187| Variable | Value |
188| --- | --- |
189| `GITHUB_SHA` | The test merge commit (for a merged pull request's `closed` event, the merge commit) |
190| `GITHUB_REF` | `refs/pull/<n>/merge` |
191| `GITHUB_REF_NAME` | `<n>/merge` |
192| `GITHUB_HEAD_REF` | The head branch |
193| `GITHUB_BASE_REF` | The base branch |
194
195Payload (`github.event`):
196
197| Field | Value |
198| --- | --- |
199| `action` | The activity type |
200| `number` | The pull request number |
201| `pull_request.number`, `pull_request.id` | The pull request number (both) |
202| `pull_request.node_id` | An opaque ID |
203| `pull_request.state` | `open` or `closed` |
204| `pull_request.locked` | `false` |
205| `pull_request.title`, `pull_request.body` | Title and description |
206| `pull_request.draft`, `pull_request.merged` | Booleans |
207| `pull_request.merged_at` | When it merged, or `null` |
208| `pull_request.merged_by` | `{ "login": ... }` or `null` |
209| `pull_request.merge_commit_sha` | The merge commit once merged, otherwise the test merge commit (or `null` while it conflicts) |
210| `pull_request.user` | The author, as a user object (see [The sender object](#the-sender-object)) |
211| `pull_request.labels` | `[{ "name": ... }]` |
212| `pull_request.assignees` | `[{ "login": ... }]` |
213| `pull_request.requested_reviewers` | `[]` |
214| `pull_request.author_association` | `MEMBER` |
215| `pull_request.head` | `label` (`<owner>:<branch>`), `ref` (branch), `sha` (head commit), `user` (the repository owner) and `repo` (a repository object) |
216| `pull_request.base` | The same fields for the base branch |
217| `pull_request.html_url` | `https://diffinery.com/acme/website/pull/<n>` |
218| `pull_request.url` | `https://diffinery.com/api/v3/repos/acme/website/pulls/<n>` |
219| `pull_request.diff_url`, `pull_request.patch_url` | `https://diffinery.com/acme/website/pull/<n>.diff` and `.patch` |
220| `pull_request.issue_url`, `pull_request.commits_url`, `pull_request.review_comments_url`, `pull_request.comments_url` | API URLs, as on GitHub |
221| `repository` | See [The repository object](#the-repository-object) |
222| `organization` | For repositories owned by an organization; see [The repository object](#the-repository-object) |
223| `sender` | Whoever caused the event; see [The sender object](#the-sender-object) |
224
225## pull_request_target
226
227Same activity types, filters and payload as `pull_request`, but the run uses the base branch's own workflow file and code: the base branch's current commit. A pull request therefore can't change what runs with the repository's secrets. Check out the pull request's code (for example with `ref: ${{ github.event.pull_request.head.sha }}`) only if you trust it.
228
229```yaml
230on:
231 pull_request_target:
232 types: [opened, reopened]
233jobs:
234 welcome:
235 runs-on: ubuntu-latest
236 steps:
237 - run: echo "Thanks for pull request #${{ github.event.number }}, ${{ github.event.pull_request.user.login }}"
238```
239
240Unlike `pull_request`, it also runs while the pull request conflicts with its base. Checks are reported on the pull request's head commit.
241
242| Variable | Value |
243| --- | --- |
244| `GITHUB_SHA` | The base branch's current commit |
245| `GITHUB_REF` | `refs/heads/<base branch>` |
246| `GITHUB_HEAD_REF`, `GITHUB_BASE_REF` | The head and base branches |
247
248## merge_group
249
250Runs when the merge queue builds a merge group for a pull request: a commit holding the base branch, every pull request ahead in the queue, and this pull request's changes. The queue merges the pull request once the group's required checks pass. See [Merge queue](/docs/collaboration/merge-queue).
251
252```yaml
253on:
254 pull_request:
255 merge_group:
256 types: [checks_requested]
257 branches: [main]
258```
259
260- The only activity type is `checks_requested`, which is also the default.
261- `branches` and `branches-ignore` filter on the queue's base branch.
262- The queue reads the required checks from the merge group commit, so the workflows that produce them must run on it: list `merge_group` next to `pull_request`. (A `push` workflow without branch filters also runs there.) If a required check never reports, the pull request leaves the queue when the queue's check timeout expires.
263- `push` workflows also run on the merge group branch; see [push](#push).
264- A merge group commit with an invalid workflow file gets an `Invalid workflow file` run, like a push.
265
266| Variable | Value |
267| --- | --- |
268| `GITHUB_SHA` | The merge group commit |
269| `GITHUB_REF` | `refs/heads/gh-readonly-queue/<base>/pr-<n>-<head sha>` |
270
271Payload (`github.event`):
272
273| Field | Value |
274| --- | --- |
275| `action` | `checks_requested` |
276| `merge_group.head_sha` | The merge group commit |
277| `merge_group.head_ref` | The merge group ref |
278| `merge_group.base_sha` | The commit the group was built on |
279| `merge_group.base_ref` | `refs/heads/<base>` |
280| `merge_group.head_commit` | `id`, `tree_id`, `message`, `timestamp`, `author` and `committer` (`name`, `email`) |
281| `repository`, `sender` | As for other events |
282
283The run's title is `Merge group for #<n>: <pull request title>`.
284
285## workflow_dispatch
286
287Lets people and agents with write access start the workflow on demand, on a branch or tag they choose, with typed inputs. How to start a run from the web UI, the CLI, the API and MCP is in [Actions overview](/docs/actions/overview#start-a-workflow-manually).
288
289```yaml
290on:
291 workflow_dispatch:
292 inputs:
293 environment:
294 description: Target environment
295 type: choice
296 options: [staging, production]
297 default: staging
298 version:
299 description: Version to deploy
300 required: true
301 replicas:
302 type: number
303 default: 2
304 dry-run:
305 type: boolean
306 default: false
307 target:
308 type: environment
309```
310
311Each input can have:
312
313| Key | Meaning |
314| --- | --- |
315| `description` | Label in the **Run workflow** form (the input's name is shown under it). |
316| `required` | `true` makes the input mandatory: an empty or missing value without a default is rejected with `input '<name>' is required`. Must be a boolean. |
317| `default` | Used when no value is given. |
318| `type` | `string` (the default), `boolean`, `number`, `choice` or `environment`. Another value fails with `input '<name>': invalid type '<type>' (expected one of string, boolean, number, choice, environment)`. |
319| `options` | For `choice`: the allowed values. A `choice` input without options fails with `input '<name>' of type 'choice' must define 'options'`. A value outside the list is rejected with `input '<name>' must be one of <options>`. |
320| `deprecationMessage` | Accepted and ignored. |
321
322Values are converted by type: `boolean` inputs become `true` when the value is `true` or `"true"` and `false` otherwise; `number` inputs become numbers; `string`, `choice` and `environment` inputs stay as given. Values for inputs the workflow doesn't declare are dropped. An `environment` input is a plain text field in the **Run workflow** form, and its value isn't checked against the repository's [environments](/docs/actions/environments).
323
324Read inputs with the `inputs` context (`${{ inputs.version }}`). `github.event.inputs` holds the same values with the same types; on GitHub, `github.event.inputs` values are always strings.
325
326The workflow must exist and have a `workflow_dispatch` trigger at the chosen ref. A ref given without `refs/` is a branch if a branch with that name exists, and a tag otherwise.
327
328| Variable | Value |
329| --- | --- |
330| `GITHUB_SHA` | The latest commit of the chosen ref |
331| `GITHUB_REF` | `refs/heads/<branch>` or `refs/tags/<tag>` |
332
333Payload (`github.event`): `inputs`, `ref` (the full ref), `workflow` (the workflow file's path), `repository` and `sender`.
334
335Each accepted dispatch also sends a `workflow_dispatch` [webhook](/docs/reference/webhook-events#workflow_dispatch) event with the same `ref`, `workflow` and `inputs`, to webhooks subscribed to it.
336
337## schedule
338
339Runs the workflow on a schedule, in UTC.
340
341```yaml
342on:
343 schedule:
344 - cron: "30 5 * * 1-5" # 05:30 UTC, Monday to Friday
345 - cron: "0 */6 * * *" # every 6 hours
346```
347
348`schedule` is a list of entries with a `cron` string. Each cron has 5 fields:
349
350| Field | Values |
351| --- | --- |
352| Minute | `0`-`59` |
353| Hour | `0`-`23` |
354| Day of month | `1`-`31` |
355| Month | `1`-`12` or `JAN`-`DEC` |
356| Day of week | `0`-`6` (Sunday is `0`; `7` also means Sunday) or `SUN`-`SAT` |
357
358Each field accepts `*`, a value, a range (`1-5`), a list (`1,15,30`) and a step (`*/15`, `0-30/10`, or `5/15` meaning from 5 to the end every 15). Names are case-insensitive. As in standard cron, when both day of month and day of week are restricted, a day matches if either one matches.
359
360How schedules run:
361
362- **Granularity is 5 minutes.** Diffinery checks schedules every 5 minutes and starts every schedule that was due at any minute since the last check. A schedule for `2 * * * *` starts a few minutes after :02, and one for `* * * * *` runs once per check, not every minute. GitHub's minimum interval is also 5 minutes. A check that comes late is caught up by the next one (up to an hour back), and a schedule starts once however many of its minutes it covers.
363- **Only the default branch counts.** Schedules are read from the workflow files on the default branch, and refreshed on every push to it. A `schedule` in a workflow on another branch does nothing.
364- Each run uses the workflow file on the default branch at that moment, and only if it still has that exact cron string.
365- Schedules of archived repositories don't run.
366- The run's actor is `diffinery-scheduler`. Notifications for scheduled runs go to the repository's watchers.
367
368Invalid crons fail the file with `Invalid cron: <detail>`, for example `Invalid cron: Cron '0 3 * *' must have exactly 5 fields (minute hour day-of-month month day-of-week)`, `minute value 60 out of range 0-59 in cron '<cron>'` or `Invalid step '0' in minute field of cron '<cron>'`. `schedule` must be a list (`on.schedule must be a list of { cron: '...' } entries`) and each entry needs a `cron` string (`on.schedule entries must have a 'cron' string`).
369
370| Variable | Value |
371| --- | --- |
372| `GITHUB_SHA` | The latest commit on the default branch |
373| `GITHUB_REF` | `refs/heads/<default branch>` |
374
375Payload (`github.event`): `schedule` (the cron string) and `repository`.
376
377## workflow_run
378
379Runs after a run of another workflow is requested or completes. Use it to post-process another workflow's results with access to secrets, or to chain workflows.
380
381```yaml
382name: Report
383on:
384 workflow_run:
385 workflows: [CI]
386 types: [completed]
387 branches: [main]
388jobs:
389 report:
390 if: ${{ github.event.workflow_run.conclusion == 'failure' }}
391 runs-on: ubuntu-latest
392 steps:
393 - run: echo "CI failed on ${{ github.event.workflow_run.head_branch }}: ${{ github.event.workflow_run.html_url }}"
394```
395
396- `workflows` is required: a list of workflow names (each workflow's `name`, or its path when it has none). A `workflow_run` trigger without `workflows` never fires.
397- `types` can be `requested` (when the other run is created, including re-runs) and `completed`. Without `types`, both fire. GitHub's `in_progress` type never fires on Diffinery.
398- `branches` and `branches-ignore` filter on the other run's head branch: the pushed branch, or the pull request's head branch for pull request runs.
399- Like on GitHub, the triggered workflow is read from the default branch and runs there, whatever branch the other run was on.
400- Chains stop after three levels: if A triggers B, B triggers C and C triggers D, D's runs don't trigger anything.
401- If the run it follows came from an agent (for example CI on a branch an agent pushed), the `workflow_run` run counts as agent code too, although it runs on the default branch: its `GITHUB_TOKEN` acts as an agent and environment secrets follow the agent rules. A follower acts on what the other run produced (artifacts, outputs), so it can't be used to carry agent-written results past the agent guardrails. See [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions#runs-whose-code-came-from-an-agent).
402
403| Variable | Value |
404| --- | --- |
405| `GITHUB_SHA` | The latest commit on the default branch |
406| `GITHUB_REF` | `refs/heads/<default branch>` |
407
408Payload (`github.event`):
409
410| Field | Value |
411| --- | --- |
412| `action` | `requested` or `completed` |
413| `workflow_run.id` | The other run's number, which is also its `github.run_id` and what `download-artifact`'s `run-id` takes |
414| `workflow_run.name`, `workflow_run.path` | The other workflow's name and file path |
415| `workflow_run.display_title` | The other run's title |
416| `workflow_run.run_number`, `workflow_run.run_attempt` | Its number and attempt |
417| `workflow_run.event` | The event that started it |
418| `workflow_run.status`, `workflow_run.conclusion` | `queued` and `null` for `requested`; `completed` and the conclusion for `completed` |
419| `workflow_run.head_branch`, `workflow_run.head_sha` | The branch and the commit it reported checks on |
420| `workflow_run.actor`, `workflow_run.triggering_actor` | `{ "login": ... }` |
421| `workflow_run.pull_requests` | `[{ "number": ... }]` for pull request runs, otherwise empty |
422| `workflow_run.created_at`, `workflow_run.updated_at` | Timestamps |
423| `workflow_run.html_url` | Link to the other run |
424| `workflow` | `name` and `path` of the other workflow |
425| `repository`, `sender` | As for other events |
426
427## release
428
429Runs when a release changes: when it's published, unpublished, edited or deleted. Use it to build and attach binaries, publish packages or deploy when you publish a release. Releases are managed on the repository's **Releases** page (`https://diffinery.com/acme/website/releases`), with `POST /api/v3/repos/{owner}/{repo}/releases` and `PATCH /api/v3/repos/{owner}/{repo}/releases/{release_id}`, or with the `create_release` and `update_release` MCP tools. All of them need write access. See [Releases](/docs/collaboration/releases).
430
431```yaml
432name: Release
433on:
434 release:
435 types: [published]
436jobs:
437 build:
438 runs-on: ubuntu-latest
439 steps:
440 - uses: actions/checkout@v4
441 - run: make dist VERSION="${{ github.event.release.tag_name }}"
442```
443
444Each change sends one or more activity types, and a workflow with `on: release` and no `types` runs once for each of them. Creating a published release sends three (`created`, `published` and `released`), so you get three runs. Set `types` to the ones you need; `types: [published]` is the usual choice.
445
446| Change | Activity types sent to workflows |
447| --- | --- |
448| Create a release that isn't a draft (**Publish release** on a new release) | `created`, `published`, then `released` (or `prereleased` for a pre-release) |
449| Publish a draft (**Publish release** on a draft) | `published`, then `released` or `prereleased` |
450| Turn a published release back into a draft (`"draft": true` in the API) | `unpublished` |
451| Edit a published release (**Update release**) | `edited`, plus `released` or `prereleased` when the pre-release setting changed |
452| Delete a published release | `deleted` |
453| Create, edit or delete a draft | None. Webhooks still get `created`, `edited` and `deleted` for drafts, but workflows don't run for draft releases, as on GitHub. |
454
455How a release run is set up:
456
457- The run uses the commit the release's tag points to (the commit, for an annotated tag), and the workflow file at that commit. If the tag doesn't exist (for example it was deleted after the release was published), no run starts.
458- Publishing a release whose tag doesn't exist yet creates the tag at `target_commitish` (the default branch when it's not set). Creating the tag is a push of that tag, so `push` workflows that match the tag run too, as on GitHub. Use either `on: push: tags` or `on: release` for the same job, not both.
459- The run's title is the release's name, or its tag name when the release has no name.
460- The run's actor is whoever made the change. Its checks are reported on the tagged commit.
461- `types` is the only filter. `branches`, `tags` and `paths` aren't applied to `release`; check `github.event.release.tag_name` or `github.ref_name` in an `if:` instead.
462- Invalid workflow files are skipped without an `Invalid workflow file` run.
463
464| Variable | Value |
465| --- | --- |
466| `GITHUB_SHA` | The commit the release's tag points to |
467| `GITHUB_REF` | `refs/tags/<tag>` |
468| `GITHUB_REF_NAME` | The tag name |
469| `GITHUB_REF_TYPE` | `tag` |
470
471Payload (`github.event`):
472
473| Field | Value |
474| --- | --- |
475| `action` | The activity type |
476| `release.id` | The release's ID, a number |
477| `release.tag_name` | The tag, for example `v1.2.0` |
478| `release.target_commitish` | The branch or commit the tag was created from |
479| `release.name`, `release.body` | Title and release notes, or `null` when empty |
480| `release.draft`, `release.prerelease` | Booleans |
481| `release.latest` | Whether this is the repository's latest release (a Diffinery field) |
482| `release.immutable` | Always `false` |
483| `release.created_at`, `release.updated_at`, `release.published_at` | Timestamps (`published_at` is `null` for drafts) |
484| `release.author` | The user object of whoever created the release (`login`, `id`, `type` and URLs) |
485| `release.assets` | The release's assets, each with `id`, `name`, `label`, `content_type`, `size`, `download_count`, `browser_download_url` and `url` |
486| `release.html_url` | `https://diffinery.com/acme/website/releases/tag/<tag>` |
487| `release.url`, `release.assets_url`, `release.upload_url` | API URLs. Upload assets to `upload_url` (with the `{?name,label}` template removed). |
488| `release.tarball_url`, `release.zipball_url` | Source archives of the tag |
489| `release.node_id`, `release.reactions` | As on GitHub |
490| `repository` | See [The repository object](#the-repository-object) |
491| `sender` | See [The sender object](#the-sender-object) |
492
493Uploading an asset needs `contents: write`. Give the job that permission and use its `GITHUB_TOKEN`, or use a personal or agent token stored as a secret (see [GITHUB_TOKEN permissions](/docs/actions/workflow-permissions)):
494
495```yaml
496on:
497 release:
498 types: [published]
499jobs:
500 upload:
501 runs-on: ubuntu-latest
502 steps:
503 - uses: actions/checkout@v4
504 - run: tar czf dist.tar.gz src
505 - name: Attach dist.tar.gz to the release
506 env:
507 TOKEN: ${{ secrets.RELEASE_TOKEN }}
508 UPLOAD_URL: ${{ github.event.release.upload_url }}
509 run: |
510 curl -fsS -X POST "${UPLOAD_URL%\{*}?name=dist.tar.gz" \
511 -H "Authorization: Bearer $TOKEN" \
512 -H "Content-Type: application/gzip" \
513 --data-binary @dist.tar.gz
514```
515
516## repository_dispatch
517
518Runs when someone calls the `dispatches` endpoint of the repository with an event type. Use it to start workflows from outside Diffinery: another system's CI, a deployment tool, a chat bot or an agent.
519
520```yaml
521on:
522 repository_dispatch:
523 types: [deploy, rollback]
524jobs:
525 deploy:
526 runs-on: ubuntu-latest
527 steps:
528 - uses: actions/checkout@v4
529 - run: ./deploy.sh "${{ github.event.action }}" "${{ github.event.client_payload.version }}"
530```
531
532- `types` lists the `event_type` values that start the workflow. Without `types`, every `event_type` does.
533- Like on GitHub, the workflow is read from the default branch's latest commit and runs there.
534- The run's title is the `event_type`, and its actor is the account whose token sent the request.
535- Invalid workflow files are skipped without an `Invalid workflow file` run.
536
537### Send a repository_dispatch event
538
539You need write access to the repository, with a token that has the write scope (see [Tokens](/docs/accounts/tokens)). People and agents can both send events.
540
541```sh
542curl -X POST https://diffinery.com/api/v3/repos/acme/website/dispatches \
543 -H "Authorization: Bearer dfy_..." \
544 -H "Content-Type: application/json" \
545 -d '{"event_type": "deploy", "client_payload": {"version": "1.4.2", "environment": "staging"}}'
546```
547
548| Field | Required | Meaning |
549| --- | --- | --- |
550| `event_type` | Yes | A name for the event, 1 to 100 characters. Workflows match it against `types`, and it becomes `github.event.action`. |
551| `client_payload` | No | A JSON object with at most 10 top-level properties and at most 64 KB as JSON. Becomes `github.event.client_payload`. Default: `{}`. |
552
553The answer is `204 No Content`, as on GitHub, also when no workflow listens for the event type. The request doesn't return the runs it started; find them with `GET /api/v3/repos/acme/website/actions/runs?event=repository_dispatch` or `diffinery run list -R acme/website -e repository_dispatch`. Webhooks subscribed to `repository_dispatch` also get the event.
554
555| Status | Message | Cause |
556| --- | --- | --- |
557| 403 | `write access required` | Your role or token scope is below write. |
558| 403 | `repository is archived (read-only)` | Archived repositories don't accept events. |
559| 422 | `event_type is required (at most 100 characters)` | `event_type` is missing, empty, not a string or too long. |
560| 422 | `client_payload must be an object` | `client_payload` is an array or a scalar. |
561| 422 | `client_payload can have at most 10 top-level properties` | Nest the values in fewer top-level properties. |
562| 422 | `client_payload is too large` | The payload is over 64 KB as JSON. |
563| 422 | `the repository is empty` | The default branch has no commits yet. |
564
565| Variable | Value |
566| --- | --- |
567| `GITHUB_SHA` | The latest commit on the default branch |
568| `GITHUB_REF` | `refs/heads/<default branch>` |
569
570Payload (`github.event`):
571
572| Field | Value |
573| --- | --- |
574| `action` | The `event_type` |
575| `branch` | The default branch |
576| `client_payload` | The `client_payload` you sent |
577| `repository` | See [The repository object](#the-repository-object) |
578| `sender` | The account whose token sent the event; see [The sender object](#the-sender-object) |
579
580## workflow_call
581
582Makes the workflow reusable: other workflows call it from a job with `uses:`. It never starts a run on its own.
583
584```yaml
585on:
586 workflow_call:
587 inputs:
588 node-version:
589 type: string
590 default: "22"
591 secrets:
592 npm-token:
593 required: true
594 outputs:
595 version:
596 value: ${{ jobs.build.outputs.version }}
597```
598
599`inputs` take `type` (required: `string`, `boolean` or `number`), `description`, `required` and `default`. `secrets` take `description` and `required`. `outputs` each need a `value`. Runs of a called workflow use the caller's event, ref, commit and `github` context. The full reference is in [Reusable workflows](/docs/actions/reusable-workflows).
600
601## GITHUB_SHA and GITHUB_REF by event
602
603| Event | `GITHUB_SHA` | `GITHUB_REF` | Workflow file read from |
604| --- | --- | --- | --- |
605| `push` | Pushed commit | Pushed branch or tag | Pushed commit |
606| `pull_request` | Test merge commit (merge commit once merged) | `refs/pull/<n>/merge` | Test merge commit |
607| `pull_request_target` | Base branch's latest commit | `refs/heads/<base>` | Base branch's latest commit |
608| `merge_group` | Merge group commit | `refs/heads/gh-readonly-queue/<base>/pr-<n>-<head sha>` | Merge group commit |
609| `workflow_dispatch` | Latest commit of the chosen ref | Chosen branch or tag | Chosen ref |
610| `schedule` | Default branch's latest commit | Default branch | Default branch |
611| `workflow_run` | Default branch's latest commit | Default branch | Default branch |
612| `release` | The commit the release's tag points to | `refs/tags/<tag>` | The tagged commit |
613| `repository_dispatch` | Default branch's latest commit | Default branch | Default branch |
614
615A re-run keeps the original run's commit, ref, event and payload.
616
617## The repository object
618
619Every payload has a `repository` field with GitHub's full repository object, the same one the REST API returns for `GET /repos/{owner}/{repo}`. The fields workflows read most:
620
621| Field | Value |
622| --- | --- |
623| `id` | The repository's ID, a number |
624| `node_id` | An opaque ID |
625| `name` | `website` |
626| `full_name` | `acme/website` |
627| `private` | `true` for a private repository, `false` for a public one |
628| `visibility` | `public` or `private` |
629| `owner` | A user object for the owner (`login`, a numeric `id`, `type`: `User` or `Organization`, and URLs) |
630| `default_branch` | `main` |
631| `archived` | Whether the repository is archived |
632| `topics` | The repository's topics |
633| `html_url` | `https://diffinery.com/acme/website` |
634| `clone_url` | `https://diffinery.com/acme/website.git` |
635| `url` and the `*_url` templates | API URLs under `https://diffinery.com/api/v3/repos/acme/website` |
636| `created_at`, `updated_at`, `pushed_at` | Timestamps |
637
638For a repository owned by an organization, payloads also have an `organization` field with `login`, `id` (a number), `node_id`, `url`, `repos_url`, `avatar_url` and `description`.
639
640The payload is also written to the file at `GITHUB_EVENT_PATH` for tools that read it from disk, such as `@actions/github`'s `context.payload` in `actions/github-script`.
641
642## The sender object
643
644`sender` is the account that caused the event, as a GitHub user object:
645
646| Field | Value |
647| --- | --- |
648| `login` | The account's login |
649| `id` | A number derived from the account's ID, like GitHub's numeric user IDs |
650| `node_id` | An opaque ID |
651| `type` | `User` for people, `Bot` for agents and Diffinery itself (for example `diffinery-scheduler`) |
652| `site_admin` | `false` |
653| `avatar_url`, `html_url`, `url` | The avatar, the profile page and the API URL |
654
655## Runs started from inside a workflow
656
657As on GitHub, an event caused by a job's own `GITHUB_TOKEN` doesn't start new workflow runs, so workflows can't trigger themselves in a loop. A push, a pull request or a release made with a job token starts nothing. The two exceptions, as on GitHub, are `workflow_dispatch` (a job with `permissions: actions: write` can start another workflow with `POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches`) and `repository_dispatch` (a job with `contents: write` can send `POST /repos/{owner}/{repo}/dispatches`).
658
659Events caused by a personal or agent token stored as a secret are treated like any other event from that account and do start workflows: a push starts `push` workflows, a published release starts `release` workflows, and a dispatch starts `repository_dispatch` workflows.
660
661```yaml
662- name: Ask the deploy workflow to run
663 env:
664 TOKEN: ${{ secrets.DISPATCH_TOKEN }}
665 run: |
666 curl -fsS -X POST "${{ github.api_url }}/repos/${{ github.repository }}/dispatches" \
667 -H "Authorization: Bearer $TOKEN" \
668 -H "Content-Type: application/json" \
669 -d '{"event_type": "deploy", "client_payload": {"sha": "${{ github.sha }}"}}'
670```
671
672Guard against loops, for example with `paths-ignore`, `types`, or an `if:` that checks `github.actor` or the commit message.
673
674## Differences from GitHub
675
676- Only the events in [Supported events](#supported-events) start runs. Other GitHub event names parse but never fire.
677- `pull_request` and `pull_request_target` fire for `opened`, `synchronize`, `reopened`, `closed`, `edited`, `ready_for_review` and `converted_to_draft` only, and the `edited` payload has no `changes` object.
678- `workflow_run` fires for `requested` and `completed`, not `in_progress`, and requires `workflows`.
679- `release` workflows get no `branches`, `tags` or `paths` filtering, and a published release whose tag doesn't exist any more starts no runs.
680- `deployment` and `deployment_status` events go to webhooks (a job with `environment:` sends both), but don't start workflow runs.
681- `push` payloads list only the pushed commit in `commits`.
682- Path filters pass when a push creates a branch.
683- `github.event.inputs` keeps input types.
684- `schedule` is checked every 5 minutes; a schedule runs within about 5 minutes after it is due.