From eec55dbf012075a3afc6e3d2a8782e3160ce5dc4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 4 Aug 2026 17:10:52 +0000 Subject: [PATCH] chore(autoducks): update machinery to v0.5.10 --- .autoducks/.installed.json | 16 ++++---- .autoducks/CHANGELOG.md | 5 +++ .autoducks/VERSION | 2 +- .autoducks/agents/update/run.sh | 41 +++++++++++++++---- .autoducks/design/AGENTS.md | 2 + .../providers/git/github/default-branch.sh | 20 +++++++++ .autoducks/providers/git/interface.sh | 6 +++ 7 files changed, 75 insertions(+), 17 deletions(-) create mode 100755 .autoducks/providers/git/github/default-branch.sh diff --git a/.autoducks/.installed.json b/.autoducks/.installed.json index b9b1589..fef784d 100644 --- a/.autoducks/.installed.json +++ b/.autoducks/.installed.json @@ -2,14 +2,14 @@ "schemaVersion": 1, "source_repo": "deepducks/autoducks", "channel": "stable", - "ref": "0a09e02a45b55e8b0b9ad460a466a123be81fcee", - "sha": "0a09e02a45b55e8b0b9ad460a466a123be81fcee", - "version": "0.5.9", - "installed_at": "2026-08-04T16:12:17Z", - "installed_by": "autoducks-update.yml#30927964151", + "ref": "7e80e724b9269033ab288ff6379b8fea9038fd7f", + "sha": "7e80e724b9269033ab288ff6379b8fea9038fd7f", + "version": "0.5.10", + "installed_at": "2026-08-04T17:10:49Z", + "installed_by": "autoducks-update.yml#30932673290", "previous": { - "ref": "4ac745e52ee271fff7ef8b677cfb875c5572ae44", - "sha": "4ac745e52ee271fff7ef8b677cfb875c5572ae44", - "version": "0.5.8" + "ref": "0a09e02a45b55e8b0b9ad460a466a123be81fcee", + "sha": "0a09e02a45b55e8b0b9ad460a466a123be81fcee", + "version": "0.5.9" } } diff --git a/.autoducks/CHANGELOG.md b/.autoducks/CHANGELOG.md index fd795c4..703c948 100644 --- a/.autoducks/CHANGELOG.md +++ b/.autoducks/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.5.10] - 2026-08-04 + +### Fixed +- fix(update): install onto the default branch, the one that actually executes (#1185) + ## [0.5.9] - 2026-08-04 ### Fixed diff --git a/.autoducks/VERSION b/.autoducks/VERSION index 416bfb0..50c76ef 100644 --- a/.autoducks/VERSION +++ b/.autoducks/VERSION @@ -1 +1 @@ -0.5.9 +0.5.10 diff --git a/.autoducks/agents/update/run.sh b/.autoducks/agents/update/run.sh index caf389d..fc431bb 100755 --- a/.autoducks/agents/update/run.sh +++ b/.autoducks/agents/update/run.sh @@ -38,6 +38,31 @@ PIN="$AUTODUCKS_UPDATE_PIN" MODE="${MODE:-$AUTODUCKS_UPDATE_MODE}" [[ "$MODE" =~ ^(pr|commit|off)$ ]] || MODE="$AUTODUCKS_UPDATE_MODE" +# ── Delivery target ────────────────────────────────────────────────────── +# Where the updated machinery is installed. This is the repository's default +# branch, NOT AUTODUCKS_BASE_BRANCH, and the distinction is load-bearing. +# +# A scheduled or dispatched run executes the workflow files and .autoducks/ +# scripts from the default branch — that is what `actions/checkout@v4` with no +# `ref:` gives every lane. So the default branch is the only place an install +# takes effect. AUTODUCKS_BASE_BRANCH means something else entirely: the branch +# the pipeline cuts feature/fix branches from. +# +# For most repos the two are the same branch and nothing changes. Where they +# differ the old behaviour was silently useless: deepducks/swanapse cuts from +# `master` but is served from `ggondim`, so v0.5.8 and v0.5.9 both landed on +# `master` while every run kept executing v0.5.2 off `ggondim`. Two consecutive +# releases reported success and changed nothing. +# +# The fallback to AUTODUCKS_BASE_BRANCH covers only an unreachable host; it +# preserves the old behaviour rather than aborting a cycle over a transient API +# failure, and says so out loud. +UPDATE_TARGET_BRANCH="$(git::default_branch)" +if [[ -z "$UPDATE_TARGET_BRANCH" ]]; then + UPDATE_TARGET_BRANCH="$AUTODUCKS_BASE_BRANCH" + echo "::warning::update: could not resolve the default branch of $REPO — falling back to base_branch '$AUTODUCKS_BASE_BRANCH'. If the two differ, this update will land where it does not execute." >&2 +fi + UPDATE_FAILURE_MARKER="" # Identifies the "a newer version is available" note already posted on an open # update PR, per target SHA, so a weekly cycle does not repeat itself. @@ -153,7 +178,7 @@ update::preflight() { fi local open_prs existing - open_prs="$(git::list_open_prs "$AUTODUCKS_BASE_BRANCH" 2>/dev/null || echo '[]')" + open_prs="$(git::list_open_prs "$UPDATE_TARGET_BRANCH" 2>/dev/null || echo '[]')" existing="$(printf '%s' "$open_prs" | jq -r '[.[] | select(.headRefName | startswith("autoducks/update-"))] | .[0].number // empty')" if [[ -n "$existing" ]]; then printf 'existing-pr\t%s' "$existing" @@ -683,7 +708,7 @@ ${UPDATE_AVAILABLE_MARKER}${target_sha}" || true exit 0 fi - update::apply_branch "$AUTODUCKS_BASE_BRANCH" "$branch" "$target_sha" "$CHANNEL" + update::apply_branch "$UPDATE_TARGET_BRANCH" "$branch" "$target_sha" "$CHANNEL" local migration_report; migration_report="$(mktemp)" if ! update::run_migrations "$installed_version" "$target_version" ".autoducks/migrations" "$migration_report"; then @@ -779,23 +804,23 @@ No PR was opened and no commit was made; the update branch was discarded." local _why="a major bump" [[ "$bump_kind" != "major" && "$has_breaking" == "1" ]] && _why="a breaking changelog entry" [[ "$bump_kind" != "major" && "$has_breaking" != "1" ]] && _why="local machinery drift (or drift that could not be evaluated)" - echo "::notice::update: mode is 'commit' but this update carries $_why — opening a PR instead of pushing to $AUTODUCKS_BASE_BRANCH." >&2 + echo "::notice::update: mode is 'commit' but this update carries $_why — opening a PR instead of pushing to $UPDATE_TARGET_BRANCH." >&2 else - update::deliver_commit "$branch" "$AUTODUCKS_BASE_BRANCH" "$target_version" - git push origin "HEAD:refs/heads/$AUTODUCKS_BASE_BRANCH" + update::deliver_commit "$branch" "$UPDATE_TARGET_BRANCH" "$target_version" + git push origin "HEAD:refs/heads/$UPDATE_TARGET_BRANCH" # deliver_commit pushes the branch because the PR path needs it; this path # does not. Without this the branch outlives the cycle — apply_branch clears # a stale ref on the next run, so it self-healed, but a commit-mode repo # carried one visible orphan branch between cycles. git::delete_branch "$branch" 2>/dev/null || true - update::report_success "Pushed \`$title\` directly to \`$AUTODUCKS_BASE_BRANCH\` (mode: commit)." + update::report_success "Pushed \`$title\` directly to \`$UPDATE_TARGET_BRANCH\` (mode: commit)." exit 0 fi fi - update::deliver_commit "$branch" "$AUTODUCKS_BASE_BRANCH" "$target_version" + update::deliver_commit "$branch" "$UPDATE_TARGET_BRANCH" "$target_version" local pr_number - pr_number="$(git::create_pr "$branch" "$AUTODUCKS_BASE_BRANCH" "$title" "$body" "false")" + pr_number="$(git::create_pr "$branch" "$UPDATE_TARGET_BRANCH" "$title" "$body" "false")" its::add_label "$pr_number" "Autoducks:update" || true if [[ "$bump_kind" == "major" || "$has_breaking" == "1" ]]; then diff --git a/.autoducks/design/AGENTS.md b/.autoducks/design/AGENTS.md index a69f714..294e9cb 100644 --- a/.autoducks/design/AGENTS.md +++ b/.autoducks/design/AGENTS.md @@ -224,6 +224,8 @@ Two sources, in this order, and **never a literal** (#1181): `main` was previously hardcoded as the fallback in `autoducks-commit-lint.yml` and `autoducks-developer.yml`, so a repo on `master` got a push trigger that never fired and a checkout of a ref that did not exist — both silently, because a trigger that does not fire is indistinguishable from one with nothing to report. +**The Update lane installs onto the default branch, not `base_branch`.** Every lane runs `actions/checkout@v4` with no `ref:` on a scheduled or dispatched trigger, so the workflow files and `.autoducks/` scripts that execute are the default branch's copy. That makes the default branch the only place an install takes effect, which is a fact about the host rather than a preference. `base_branch` answers a different question — where the pipeline cuts `feature/`/`fix/` branches from — and the two are the same branch in most repos. Where they differ, delivering to `base_branch` produced an update that reported success and changed nothing: `deepducks/swanapse` cuts from `master` but is served from `ggondim`, and took two consecutive releases on `master` while every run kept executing the older machinery on `ggondim`. `update/run.sh` resolves `UPDATE_TARGET_BRANCH` from [`git::default_branch`](../providers/git/interface.sh), falling back to `base_branch` with a warning only when the host cannot answer. + **`AUTODUCKS_BASE_BRANCH` carries source 1 only, and may be empty.** `load-config.sh` exports the configured value verbatim and does not resolve step 2; the caller does, because the caller knows whether it can reach the host. [`sync-child-gitlinks.sh`](../core/orchestration/sync-child-gitlinks.sh) is the reference shape: config, then the host API, then a warning and a clean exit rather than acting on a branch named `""`. Resolving step 2 inside `load-config.sh` is a tempting simplification and a mistake. The only source available there without a token is `origin/HEAD`, a local ref that can be stale or absent, and populating the variable from it silently preempts the authoritative answer the caller was about to fetch. diff --git a/.autoducks/providers/git/github/default-branch.sh b/.autoducks/providers/git/github/default-branch.sh new file mode 100755 index 0000000..1cd5bc8 --- /dev/null +++ b/.autoducks/providers/git/github/default-branch.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# git::default_branch → the repository's default branch name, empty when the +# host cannot answer (offline, token refused, repo gone). +# +# Distinct from AUTODUCKS_BASE_BRANCH on purpose. That key says where the +# pipeline cuts branches from; this says which branch the host actually serves +# as HEAD — and therefore which copy of .github/workflows/ and .autoducks/ a +# scheduled or dispatched run executes. A repo can legitimately set them to +# different branches (see the update agent's delivery target). +git::default_branch() { + gh api "repos/$REPO" --jq '.default_branch' 2>/dev/null || true +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + case "${1:-}" in + --help) echo "Usage: git::default_branch"; echo " Print the repository's default branch (empty if unresolvable)"; echo " Requires: REPO env var"; exit 0 ;; + esac +fi diff --git a/.autoducks/providers/git/interface.sh b/.autoducks/providers/git/interface.sh index 13220b0..bb8531c 100755 --- a/.autoducks/providers/git/interface.sh +++ b/.autoducks/providers/git/interface.sh @@ -26,6 +26,12 @@ done # # git::create_branch(base, name) # git::branch_exists(name) → exit code 0/1 +# git::default_branch() → branch name, or empty +# The branch the host serves as HEAD, which is the copy of +# .github/workflows/ and .autoducks/ a scheduled or dispatched run +# executes. Not AUTODUCKS_BASE_BRANCH, which says where the pipeline cuts +# from; a repo may legitimately set the two to different branches. Empty +# when the host cannot answer — callers decide whether that is fatal. # git::create_pr(head, base, title, body, draft?) → pr_number # git::merge_pr(pr_number, when?) → 0 ok / 2 method-not-allowed / 1 other # when ∈ {now, auto}, default now. `auto` asks the host to hold the merge