keepalive: avoid diverging commits in forks#20
Conversation
6f70cff to
b8d1227
Compare
| test 0 -lt $(git rev-list --count --since=3.weeks.ago HEAD) | ||
| test 0 -lt $(git rev-list --count --since=${{ github.event.repository.fork == true && '4' || '3' }}.weeks.ago HEAD) | ||
| then | ||
| echo "::notice::No need to keep alive, there were commits in the last three weeks" |
There was a problem hiding this comment.
Should this message be more specific? If I understand this correctly (stop laughing!) the notice will be issued more frequently for four weeks.
b8d1227 to
52a0571
Compare
|
@webstech I pivoted rather dramatically to a totally different strategy. How do you like it? |
webstech
left a comment
There was a problem hiding this comment.
I like the change in direction. Aside from the other comments, is it known if the state will reset to disabled_inactivity on a predictable frequency? Just wondering if cycling disable/enable will reset the inactivity clock. Hmmm, should the script check for that state as well as active?
| name=${path##*/} | ||
| echo "Refreshing $name" | ||
| gh api -XPUT "/repos/$REPO/actions/workflows/$name/disable" && | ||
| gh api -XPUT "/repos/$REPO/actions/workflows/$name/enable" || { |
There was a problem hiding this comment.
Any reason for not using the gh workflow enable command?
There was a problem hiding this comment.
Huh, good point. I was imitating gh-workflow-immortality.sh and never thought that maybe the GitHub CLI sprouted a command for that... Fixed!
| echo "Refreshing $name" | ||
| gh api -XPUT "/repos/$REPO/actions/workflows/$name/disable" && | ||
| gh api -XPUT "/repos/$REPO/actions/workflows/$name/enable" || { | ||
| echo "::error::could not refresh $name" >&2 |
There was a problem hiding this comment.
Could this be a little more assertive? Maybe suggest making sure the workflow is not disabled?
GitHub disables scheduled workflow triggers after 60 days without
repository activity. The current workaround commits to
`.github/cached/keepalive.txt` every three weeks, which forces every
downstream fork (e.g. `cygwingitgadget/gitgitgadget-workflows`) to
either diverge from upstream or merge the churn back on a schedule.
A much cleaner mechanism exists:
PUT /repos/{owner}/{repo}/actions/workflows/{id}/enable
resets the 60-day timer even when the workflow is already enabled. The
trick is documented in
https://github.com/PhrozenByte/gh-workflow-immortality, and it works
against forks like `cygwingitgadget/gitgitgadget-workflows`, without
conflicting with upstream changes.
That endpoint is not reachable with the default `GITHUB_TOKEN`, so this
reuses the `actions/create-github-app-token` pattern (and the per-fork
`GITGITGADGET_GITHUB_APP_*` secrets) that the other workflows in this
repo already rely on.
Only workflows in state `active` are refreshed; anything a fork has
deliberately disabled (e.g. `sync-ref.yml` in the Cygwin fork, which has
no sister repo to sync from) stays disabled.
Assisted-by: Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Calling `enable` on a workflow that is already `active` is not documented to reset GitHub's internal 60-day inactivity clock; the REST documentation at https://docs.github.com/en/rest/actions/workflows leaves the semantics of a no-op call unspecified. Bouncing through `active` -> `disabled_manually` -> `active` forces a real state transition and removes the ambiguity. The `&&` chaining is deliberate: GitHub Actions runs shell steps with `set -e`, so if either call fails the loop exits and the step is marked failed loudly, rather than silently leaving a workflow in `disabled_manually`. Assisted-by: Opus 4.7 Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Now that the previous commit uses the `PUT
.../actions/workflows/{id}/enable` endpoint to reset the 60-day
scheduled-workflow inactivity timer directly, the older fallback that
invented a commit under `.github/cached/` has no remaining purpose. In
every fork it either did nothing or produced a divergent commit that
then had to be pushed; both outcomes are strictly worse than the
enable-endpoint approach.
The upstream-merge step is kept deliberately: its purpose (keeping forks
close to upstream) is orthogonal to the inactivity timer, and its push
retry loop still applies to the merge result.
Assisted-by: Opus 4.7
Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
This is now no longer needed. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
52a0571 to
7f46067
Compare
True to form, GitHub's documentation does not lose any words about any interesting aspect of this behavior. The description of this behavior is hidden in the documentation how to disable/enable workflows. Here is the full text, in all of its dubitable glory: Warning To prevent unnecessary workflow runs, scheduled workflows may be disabled automatically. When a public repository is forked, scheduled workflows are disabled by default. In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days. It talks about "repository activity". Now, you might think that opening a PR, pushing a new branch or tag, or commenting on an issue would count as activity. Nope. It's not documented anywhere, but experiments demonstrated that those activities don't count as activities. Which is curious because GitHub itself calls pushes to all branches activity in URLs like https://github.com/gitgitgadget/gitgitgadget/activity. BTW I did not conduct those experiments, I lack that kind of patience to suffer through iterations that cost 60 days each, just because somebody could not be bothered to document their product properly.
I only have the lore of
It would be nice, wouldn't it? But that clock isn't surfaced anywhere in the API nor UI. For example, you might think that getting all the information for a given workflow would show that clock in some attribute, right? You'd be wrong: $ (REPO=gitgitgadget/gitgitgadget-workflows; name=keepalive.yml; gh api "/repos/$REPO/actions/workflows/$name")
{
"id": 173723163,
"node_id": "W_kwDOHv049s4KWs4b",
"name": "Keep scheduled workflows alive",
"path": ".github/workflows/keepalive.yml",
"state": "active",
"created_at": "2025-07-10T10:27:12.000+02:00",
"updated_at": "2025-07-10T10:27:12.000+02:00",
"url": "https://api.github.com/repos/gitgitgadget/gitgitgadget-workflows/actions/workflows/173723163",
"html_url": "https://github.com/gitgitgadget/gitgitgadget-workflows/blob/main/.github/workflows/keepalive.yml",
"badge_url": "https://github.com/gitgitgadget/gitgitgadget-workflows/workflows/Keep%20scheduled%20workflows%20alive/badge.svg"
}No sign of me disabling/re-enabling it a couple of minutes ago using |
|
BTW here is a test run in my fork, where I temporarily enabled some workflows to show that they do get refreshed. |
Ideally, we do not diverge in forks of
gitgitgadget-workflows, but committing to keep the scheduled workflows alive is prone to cause exactly that.Instead, disable and immediately re-enable the scheduled workflows (including the
keepaliveone itself). That also re-sets the 60-day timer.Keep the automatic merge from upstream as it is most likely desirable. Should the need arise for a downstream fork to opt-out of that behavior, it is easy enough to add a mechanism for that, once (or: if) that need arises.