Actions: on: schedule fires, from the default branch

ad37081 · Gabriel Morell · 2026-10-17 18:00

25 files +506 -1
Message
{commit_body(@commit)}

Files changed

modified README.md
+2 −0
@@ -91,6 +91,8 @@ reinvent them.
91 91 - **Triggers**: `push` (with `branches:` / `branches-ignore:` / `tags:` / `tags-ignore:`, GitHub's rule that naming one kind excludes the other kind of ref, and a list beating its `-ignore` if both are set), `pull_request` and `workflow_dispatch`
92 92 - **`on: pull_request`** for pull requests within one repository: `types:` `opened`, `synchronize` (the head moved) and `reopened` (the default; other activity types never fire). `branches:` / `branches-ignore:` filter the **target** branch, and `paths:` the PR's diff from its merge-base. Workflows are read from the PR's head commit, so a PR can change its own CI, and the job tests the head itself at `refs/pull/<N>/head` — not a merge with the target, as GitHub's `refs/pull/<N>/merge` does. The job gets `github.event_name`, `head_ref` and `base_ref` (the source and target branches), `ref_name` `<N>/head` and an empty `ref_type`. **Fork and federated pull requests don't run yet**: their head is code the target's maintainers haven't reviewed, and a job is handed the target repository's secrets and a token that can publish its packages; GitHub runs them without secrets and with a read-only token, and until that exists here they don't run at all. `pull_request_target` isn't supported
93 93 - **`on: workflow_dispatch`** from the Actions page ("Run workflow", for anyone who can write to the repository: pick a branch and fill in the inputs) or the Gitea/GitHub API (`POST /api/v1/repos/:owner/:name/actions/workflows/:file/dispatches`, a `ci:write` token or a job token). Declared `inputs:` are checked as GitHub checks them — an undeclared input, a missing `required` one, a `choice` outside its `options`, a `boolean` that isn't true/false or a `number` that isn't numeric answers 422 — and every declared input, defaults applied, reaches the job as `inputs.*` and `github.event.inputs.*` (strings)
94 + - **`on: schedule`** (`- cron:`, five-field cron in UTC): read from the **default branch's** latest commit and run there, whichever branch the workflow file was pushed to; `github.event.schedule` is the cron that fired. Crons that can fire more often than **every five minutes** (GitHub's minimum), `@reboot` and ones that don't parse are ignored with a warning in the log. Archived repositories don't run their schedules
95 + - **`[skip ci]`**, `[ci skip]`, `[no ci]`, `[skip actions]` or `[actions skip]` in the head commit's message (any case), or a `skip-checks: true` trailer, starts no `push` or `pull_request` runs — the head commit only, as on GitHub; other events aren't skipped
94 96 - **Filter patterns** in `branches:`, `tags:`, `paths:` and their `-ignore` lists follow GitHub's cheat sheet: `*` is any run of characters but `/`, `**` any run at all (`**/` also no directory), `?` zero or one and `+` one or more of the **preceding** character, `[a-z0-9]` one listed character (alphanumerics and `a-z`/`A-Z`/`0-9` ranges only), `\x` a literal `x`. In `branches:`, `tags:` and `paths:` the last matching pattern decides, so `!pattern` excludes and a later positive pattern includes again; a list with only `!` patterns matches nothing. `*` no longer crosses `/` in `branches:` / `tags:` (`branches: ['*']` doesn't run for `feature/x`), and `!` works there now. Branch protection and required-check patterns are separate: their `*` still crosses `/`
95 97 - **`paths:` / `paths-ignore:`**: a push runs if any changed file is selected by `paths:`, or if not every changed file is matched by `paths-ignore:`; both set → `paths:` wins, with a warning. Changed files are the ref's old → new diff (renames count as both paths); a new branch is diffed from where it left the default branch. Tag pushes and manual runs ignore path filters, and when the changed files can't be worked out (no default branch to compare with, an unknown base, a git error, more than 3000 files) the filter passes — a run is never skipped because the forge couldn't look
96 98 - **The job's context** follows Forgejo's: `github.event_name`, `ref`, `ref_name`, `ref_type`, `sha`, `repository` (`owner/name`), `repository_id`, `repository_owner(_id)`, `actor(_id)`, `run_id`, `run_number`, `run_attempt` (1, bumped by re-running the job; re-running the whole run makes a new run and a new `run_id`), `workflow`, `workflow_ref` (`owner/name/<path>@<ref>`), `job`, `server_url`, `api_url`, `token`. **`github.event`** is GitHub's webhook payload, trimmed: every event has `repository` and `sender`; `push` has `ref`, `before` (40 zeros for a new ref), `after`, `created`, `deleted`, `forced` (always `false`: not tracked), `compare`, `commits` (at most 20) and `head_commit`, `pusher`; `pull_request` has `action`, `number` and `pull_request` (`title`, `body`, `html_url`, `user`, `head`/`base` with `ref`, `sha`, `label`, `repo`); `workflow_dispatch` has `inputs`, `ref`, `workflow`. **`secrets.GITHUB_TOKEN`** (and `GITEA_TOKEN`, `FORGEJO_TOKEN`) is the job token. The workflow's top-level `env:`, `defaults:`, `permissions:`, `run-name:` and `on:` reach the runner with the job
modified config/config.exs
+3 −1
@@ -68,7 +68,9 @@ config :git_gud, Oban,
68 68 # Requeue workflow_jobs stuck in `running` after a claim-then-
69 69 # can't-send (network blip, encoder bug, runner crash). Threshold
70 70 # is configurable via :stale_after_minutes; default 5 minutes.
71 − {"* * * * *", GitGud.Workflows.Workers.StaleJobReaper}
71 + {"* * * * *", GitGud.Workflows.Workers.StaleJobReaper},
72 + # Fire the workflows' own `on: schedule` crons that are due.
73 + {"* * * * *", GitGud.Workflows.Workers.ScheduleTick}
72 74 ]}
73 75 ]
74 76
modified lib/git_gud/pull_requests.ex
+5 −0
@@ -705,6 +705,11 @@ defmodule GitGud.PullRequests do
705 705 commit_sha
706 706 )
707 707
708 + # A merge moves the target without the post-receive hook, so
709 + # nothing else re-reads the default branch's `on: schedule`.
710 + if pr.target_ref == repo.default_branch,
711 + do: {:ok, _count} = GitGud.Workflows.Schedules.sync(repo)
712 +
708 713 _ = GitGud.Federation.Publishing.publish_pr_merged(repo, updated)
709 714 {:ok, updated}
710 715 end
modified lib/git_gud/repositories.ex
+8 −0
@@ -1722,6 +1722,14 @@ defmodule GitGud.Repositories do
1722 1722 repo
1723 1723 |> Repository.update_changeset(attrs)
1724 1724 |> Repo.update()
1725 + |> tap(fn
1726 + # `on: schedule` follows the default branch.
1727 + {:ok, %Repository{default_branch: branch} = updated} when branch != repo.default_branch ->
1728 + {:ok, _count} = GitGud.Workflows.Schedules.sync(updated)
1729 +
1730 + _ ->
1731 + :ok
1732 + end)
1725 1733
1726 1734 err ->
1727 1735 err
modified lib/git_gud/repositories/workers/commit_backfill.ex
+10 −0
@@ -43,10 +43,12 @@ defmodule GitGud.Repositories.Workers.CommitBackfill do
43 43
44 44 if zero?(new_sha) do
45 45 Webhooks.notify(repo, "push", push_payload(repo, ref, old_sha, new_sha, :delete))
46 + sync_schedules(repo, ref)
46 47 :ok
47 48 else
48 49 :ok = walk_and_cache(repo, old_sha, new_sha)
49 50 Webhooks.notify(repo, "push", push_payload(repo, ref, old_sha, new_sha, :update))
51 + sync_schedules(repo, ref)
50 52 if dispatch?, do: dispatch_workflows(repo, ref, old_sha, new_sha, pusher_id)
51 53 publish_push(repo, ref, old_sha, new_sha)
52 54 refresh_cross_repo_prs(repo, ref, new_sha, pusher_id, dispatch?)
@@ -56,6 +58,14 @@ defmodule GitGud.Repositories.Workers.CommitBackfill do
56 58 end
57 59 end
58 60
61 + # `on: schedule` is read from the default branch, so any change to it —
62 + # a job-token push and a `[skip ci]` one included, since neither changes
63 + # what the file says — re-reads the crons.
64 + defp sync_schedules(%Repository{default_branch: branch} = repo, "refs/heads/" <> branch),
65 + do: {:ok, _count} = Workflows.Schedules.sync(repo)
66 +
67 + defp sync_schedules(_repo, _ref), do: :ok
68 +
59 69 defp enqueue_language_stats(repo, "refs/heads/" <> _branch, new_sha) do
60 70 _ =
61 71 GitGud.Repositories.Workers.LanguageStats.new_job(%{
modified lib/git_gud/workflows.ex
+43 −0

Click to load diff…

modified lib/git_gud/workflows/event_payload.ex
+9 −0

Click to load diff…

added lib/git_gud/workflows/schedules.ex
+200 −0

Click to load diff…

added lib/git_gud/workflows/workers/schedule_tick.ex
+30 −0

Click to load diff…

added lib/git_gud/workflows/workflow_schedule.ex
+21 −0

Click to load diff…

added priv/repo/migrations/20261017175632_create_workflow_schedules.exs
+16 −0

Click to load diff…

added test/git_gud/workflows/schedule_test.exs
+159 −0

Click to load diff…

Parents: 9b97d32