Stop skipping Niall.chat as empty, and arm auto-merge on the bump PRs #29

Merged
geusebio merged 2 commits from fix/propagate-clone-and-automerge into main 2026-08-10 15:50:54 +00:00
Owner

Two findings from run #11 — the first propagate.yml run that ever succeeded.

1. Niall.chat was being skipped silently

The run reported repo is empty — nothing to update. It isn't: 20 branches, 42MB, and a
composer.lock pinning this bundle to e915ac5 ("Make the sidebar optional") while main is
1e24500. It is a live consumer, months behind, and this job has never offered it a bump.

Root cause, reproduced by hand:

$ git clone --depth 1 https://git.grey.ooo/Grey.ooo/Niall.chat.git
warning: You appear to have cloned an empty repository.
$ git symbolic-ref --short HEAD
master

Its server-side git HEAD points at a master that has no commits, while the API reports
default_branch: main (fbae6531). A plain clone follows that symref and gets nothing. The
existing code inferred emptiness from an unresolvable HEAD, and the comment asserting "there
is no code in the repo yet" had gone stale.

Fixed by asking rather than inferring:

  • clone --branch "$default_branch" read from the API
  • "empty" means the API's empty flag, nothing else
  • a clone that still has no resolvable HEAD is red — conflating "did not find out" with
    "nothing to do" is what hid this

Worth noting separately: Niall.chat's HEAD symref is itself misconfigured and will confuse
anyone cloning it by hand, not just this script. This PR routes around it; repointing HEAD to
main on that repo is a separate fix I haven't made.

2. Auto-merge, armed but never performed

Each PR is now asked to merge itself when its own CI goes green. This script never merges.

That needs enforcing rather than intending, because Forgejo doesn't separate the two: scheduled
merge is the ordinary merge endpoint with merge_when_checks_succeed set, so the identical
call against a PR whose checks have already passed merges it on the spot. arm_automerge()
refuses unless the head commit's checks are genuinely still pending:

Head commit state What happens
checks pending armed — the consumer's own CI decides
already green left alone, reported — merging that is a person's call
no checks at all left alone — "on success" has no meaning
repo permits no merge style left alone, reported
instance rejects the field (404/422/501) reported unsupported; never retried as a plain merge

Capability is checked in three parts rather than assumed, because a silent no-op here would be
indistinguishable from a working auto-merge: the repo's permitted merge styles, the presence and
state of checks, and the instance's response to the field itself.

None of those refusals is red — the PR is open and correct, and only a convenience is missing.

Verification

  • sh -n clean, locally and inside matthewbaggett/php-node:8.4
  • the merge-style picker unit-tested against three real repo shapes: all-allowed → merge,
    default-disallowed → falls back to squash, none-allowed → refuses
  • capability facts confirmed against the live instance: all four consumers allow
    merge/squash/rebase with default_merge_style=merge, and the three PRs from run #11 have 9,
    10 and 8 pending checks — so consumer CI does run on these branches, which is both the
    precondition auto-merge needs and confirmation of the real-PAT rationale
  • composer validate clean

bump.sh cannot run locally without WRITE_PAT, so workflow_dispatch gains dry_run and
automerge inputs. A dry dispatch on this branch is how I'd verify it against the real estate
before merging. A push-triggered run has no inputs and keeps today's behaviour.

Not done here

The three open bump PRs from run #11 (Someones.Computer #463, PrintShop #184, Projects #43) are
not armed — that would start auto-merging into production apps, which I'd want asked for
explicitly rather than inferred.

Two findings from run #11 — the first `propagate.yml` run that ever succeeded. ## 1. Niall.chat was being skipped silently The run reported `repo is empty — nothing to update`. It isn't: 20 branches, 42MB, and a `composer.lock` pinning this bundle to `e915ac5` ("Make the sidebar optional") while `main` is `1e24500`. It is a live consumer, months behind, and this job has never offered it a bump. Root cause, reproduced by hand: ``` $ git clone --depth 1 https://git.grey.ooo/Grey.ooo/Niall.chat.git warning: You appear to have cloned an empty repository. $ git symbolic-ref --short HEAD master ``` **Its server-side git `HEAD` points at a `master` that has no commits**, while the API reports `default_branch: main` (`fbae6531`). A plain clone follows that symref and gets nothing. The existing code inferred emptiness from an unresolvable `HEAD`, and the comment asserting "there is no code in the repo yet" had gone stale. Fixed by asking rather than inferring: - clone `--branch "$default_branch"` read from the API - "empty" means the API's `empty` flag, nothing else - a clone that still has no resolvable `HEAD` is **red** — conflating "did not find out" with "nothing to do" is what hid this Worth noting separately: **Niall.chat's HEAD symref is itself misconfigured** and will confuse anyone cloning it by hand, not just this script. This PR routes around it; repointing HEAD to `main` on that repo is a separate fix I haven't made. ## 2. Auto-merge, armed but never performed Each PR is now asked to merge *itself* when its own CI goes green. This script never merges. That needs enforcing rather than intending, because Forgejo doesn't separate the two: scheduled merge **is** the ordinary merge endpoint with `merge_when_checks_succeed` set, so the identical call against a PR whose checks have already passed merges it on the spot. `arm_automerge()` refuses unless the head commit's checks are genuinely still `pending`: | Head commit state | What happens | |---|---| | checks pending | armed — the consumer's own CI decides | | already green | **left alone**, reported — merging that is a person's call | | no checks at all | left alone — "on success" has no meaning | | repo permits no merge style | left alone, reported | | instance rejects the field (404/422/501) | reported unsupported; never retried as a plain merge | Capability is checked in three parts rather than assumed, because a silent no-op here would be indistinguishable from a working auto-merge: the repo's permitted merge styles, the presence and state of checks, and the instance's response to the field itself. None of those refusals is red — the PR is open and correct, and only a convenience is missing. ## Verification - `sh -n` clean, locally and inside `matthewbaggett/php-node:8.4` - the merge-style picker unit-tested against three real repo shapes: all-allowed → `merge`, default-disallowed → falls back to `squash`, none-allowed → refuses - capability facts confirmed against the live instance: all four consumers allow merge/squash/rebase with `default_merge_style=merge`, and the three PRs from run #11 have 9, 10 and 8 **pending** checks — so consumer CI does run on these branches, which is both the precondition auto-merge needs and confirmation of the real-PAT rationale - `composer validate` clean `bump.sh` cannot run locally without `WRITE_PAT`, so `workflow_dispatch` gains `dry_run` and `automerge` inputs. A dry dispatch on this branch is how I'd verify it against the real estate before merging. A push-triggered run has no inputs and keeps today's behaviour. ## Not done here The three open bump PRs from run #11 (Someones.Computer #463, PrintShop #184, Projects #43) are **not** armed — that would start auto-merging into production apps, which I'd want asked for explicitly rather than inferred.
Two changes to propagate, found by the first run of it that ever succeeded.

Niall.chat was being skipped silently. It is not empty: 20 branches, 42MB, and a
composer.lock pinning this bundle to a months-old commit. But its server-side git
HEAD points at a `master` with no commits while its actual default branch is
`main`, so `git clone --depth 1` prints "you appear to have cloned an empty
repository" and leaves HEAD unresolvable — which this script read as "repo is
empty — nothing to update". The comment asserting there was no code in the repo
had simply gone stale.

So the branch to clone is now asked for (`default_branch` from the API) rather
than inherited from the server's HEAD, "empty" means the API's `empty` flag and
nothing else, and a clone with no resolvable HEAD is red. Conflating "did not find
out" with "nothing to do" is what hid a live consumer for as long as this existed.

Auto-merge is armed on each PR, and never performed here. The distinction needs
enforcing rather than intending, because Forgejo does not separate them: scheduled
merge is the ordinary merge endpoint with `merge_when_checks_succeed` set, so the
identical call against an already-green PR merges it on the spot. arm_automerge()
refuses unless the head commit's checks are still pending, and leaves the PR for a
human when there are no checks, when they have already passed, when the repo
permits no merge style it could ask for, or when the instance rejects the field.
None of those is red: the PR is open and correct and only a convenience is
missing.

`workflow_dispatch` gains `dry_run` and `automerge` inputs, so a change to this
script can be rehearsed against the real estate before it is trusted. A push-
triggered run has no inputs and keeps the previous behaviour.
"Checks are pending" is not the same as "this merge is blocked". Forgejo`s
mergeable only means there are no git conflicts, so on a repo with no required
status checks the PR may merge right now whatever its CI is doing — and the
scheduled call there merges immediately, which is the one thing this must never
do.

So arm_automerge() now confirms the base branch carries a protection rule with
enable_status_check before anything else, by exact branch name. A glob rule like
main* is deliberately not matched: assuming a pattern covers this branch is the
assumption that would merge something. Anything unconfirmable refuses, since the
failure mode here is an unwanted merge rather than a missed one.

Grey.ooo/Niall.chat was that repo — 588 CI runs producing real contexts, no
protection requiring any of them — and now has a rule matching the estate shape
Someones.Computer, PrintShop and Projects already use. Its Build & Publish job is
deliberately not required, because it reports skipped on every commit and a
required context that never succeeds blocks every merge forever.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Grey.ooo/symfony-brand-kit!29
No description provided.