Skip to content
go-libfossil Cross-repo release automation Implementation Plan
Login

Cross-repo release automation Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Automate the libfossil → edgesync → bones release cascade. After all 6 PRs ship: tagging libfossil triggers an auto-bump PR in edgesync; merging + tagging edgesync triggers the same in bones. Each repo also gets a verbatim CI mirror (make ci), a fast pre-push hook (make ci-fast), and a make release VERSION=... helper.

Architecture: GitHub repository_dispatch chain with linear graph (libfossil dispatches only to edgesync; edgesync dispatches only to bones). Receiver workflow (bump-upstream.yml) is shared shape across edgesync and bones with a single EXPECTED_REPO env var distinguishing them. Local dev surface is per-repo Makefile targets and .githooks/pre-push (using existing git config core.hooksPath .githooks pattern).

Tech Stack: GitHub Actions, GoReleaser v2 (edgesync, bones only), gh CLI, GNU make, bash, Go 1.26.

Spec: docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md


File structure

libfossil (/Users/dmestas/projects/libfossil)

edgesync (/Users/dmestas/projects/edgesync, GitHub: danmestas/EdgeSync)

bones (/Users/dmestas/projects/bones)


Phase 0: Prerequisites (one-time, before PR #4 merges)

These steps are manual and produce no commits. Complete them between PR #3 merge and PR #4 PR-open so dispatch tokens are in place when needed.

Open https://github.com/settings/personal-access-tokens/new in a browser. Configure:

Generate token, copy it, then:

cd /Users/dmestas/projects/libfossil
gh secret set DISPATCH_PAT --body "<paste-token-here>"

Verify:

gh secret list | grep DISPATCH_PAT
Expected: DISPATCH_PAT Updated <date>

Same flow, configure:

cd /Users/dmestas/projects/edgesync
gh secret set DISPATCH_PAT --body "<paste-token-here>"

Verify:

gh secret list | grep DISPATCH_PAT
Expected: DISPATCH_PAT Updated <date>

Add a calendar reminder for ~10 months from today (2027-02-28) to rotate both PATs before the 1-year expiry hits.


Phase 1: PR #1 — bones local dev hygiene

Repo: /Users/dmestas/projects/bones Branch: chore/local-dev-hygiene Files added: .githooks/pre-push, .github/RELEASE_TEMPLATE.md Files modified: Makefile

Task 1.0: Branch setup

cd /Users/dmestas/projects/bones
git fetch origin
git checkout main
git pull --ff-only origin main
git checkout -b chore/local-dev-hygiene

Expected: switched to a new branch.

Task 1.1: Add make ci target

bones' CI workflow (.github/workflows/ci.yml) runs the discipline suite via make check (fmt-check + vet + lint + race + todo-check), then otel-tagged build, then otel-tagged short tests. The cross-repo leaf-binary integration step (sibling EdgeSync checkout + make leaf) is CI-only and intentionally not mirrored locally — it requires a sibling working directory layout that doesn't generalize.

Files: Modify Makefile

Append to the end of /Users/dmestas/projects/bones/Makefile:


# CI mirror — runs the bones-side portion of .github/workflows/ci.yml.
# Cross-repo leaf-binary integration (requires sibling EdgeSync checkout)
# is CI-only and not mirrored here.
.PHONY: ci ci-fast
ci:
	$(MAKE) check
	go build -tags=otel ./...
	go test -tags=otel -short ./... -count=1

# Fast subset for pre-push hook (~30-60s; no make check, no -tags=otel, no -race).
ci-fast:
	go vet ./...
	go build ./...
	go test -short -count=1 -timeout=30s ./...

Also add ci ci-fast to the .PHONY line at the top if there's a single combined .PHONY: declaration. If multiple .PHONY: lines exist (as currently), the new line above is sufficient.

make ci

Expected: exits 0. (May take 1-3 min.)

time make ci-fast

Expected: exits 0 in under 60s.

git add Makefile
git commit -m "feat(make): add ci and ci-fast targets

ci mirrors .github/workflows/ci.yml verbatim (-tags=otel -short).
ci-fast runs vet + build + short tests (~60s) for pre-push hook."

Task 1.2: Add make release VERSION=... target

Files: Modify Makefile

Append to /Users/dmestas/projects/bones/Makefile:


.PHONY: release
release:
	@test -n "$(VERSION)" || { echo "VERSION=vX.Y.Z required"; exit 1; }
	@echo "$(VERSION)" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$$' || { echo "bad version format: $(VERSION)"; exit 1; }
	@git diff --quiet || { echo "tree dirty; commit or stash first"; exit 1; }
	@git fetch origin --tags
	@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "tag $(VERSION) already exists"; exit 1; fi
	@$(MAKE) ci
	@PREV=$$(git describe --tags --abbrev=0 2>/dev/null || echo ""); \
	  TMPL=.github/RELEASE_TEMPLATE.md; \
	  TMP=$$(mktemp); \
	  { echo "Release $(VERSION)"; echo; \
	    [ -f $$TMPL ] && { cat $$TMPL; echo; }; \
	    echo "## Changes"; \
	    if [ -n "$$PREV" ]; then git log --oneline $$PREV..HEAD; else git log --oneline; fi; } > $$TMP; \
	  $${EDITOR:-vi} $$TMP; \
	  git tag -a "$(VERSION)" -F $$TMP; \
	  rm $$TMP
	@echo ""
	@echo "Tag $(VERSION) created locally. To publish:"
	@echo "  git push origin $(VERSION)"
make release          # missing VERSION

Expected: exits 1 with "VERSION=vX.Y.Z required".

make release VERSION=foo

Expected: exits 1 with "bad version format: foo".

make release VERSION=v9.9.9-test EDITOR=true

(Sets EDITOR=true so the no-op editor command exits cleanly without prompting.) Expected: tag created locally. Verify:

git tag -l 'v9.9.9*'
git tag -d v9.9.9-test

Expected: tag listed, then deleted.

git add Makefile
git commit -m "feat(make): add release VERSION=... helper

Validates clean tree, runs make ci, opens \$EDITOR with auto-populated
tag annotation template (.github/RELEASE_TEMPLATE.md + git log since
prev tag), creates annotated tag locally. Push is intentionally manual."

Task 1.3: Add setup-hooks target

bones doesn't have one yet. Use the same core.hooksPath .githooks pattern that libfossil and edgesync use.

Files: Modify Makefile

Append to /Users/dmestas/projects/bones/Makefile:


.PHONY: setup-hooks
setup-hooks:
	git config core.hooksPath .githooks
	@echo "Git hooks configured to use .githooks/ directory."
	@echo "Pre-push runs make ci-fast (~60s). Skip with: git push --no-verify"
grep -A 3 'setup-hooks' Makefile

Expected: shows the new target.

git add Makefile
git commit -m "feat(make): add setup-hooks target

Configures git core.hooksPath to .githooks/ directory.
Matches the pattern already used in libfossil and edgesync."

Task 1.4: Create .githooks/pre-push

Files: Create .githooks/pre-push

mkdir -p /Users/dmestas/projects/bones/.githooks

Create /Users/dmestas/projects/bones/.githooks/pre-push with content:

#!/usr/bin/env bash
# Pre-push hook — runs `make ci-fast` (~60s) to catch obvious breakage
# before pushing. Mirrors a fast subset of GitHub CI.
#
# Skip with: git push --no-verify

set -e
echo "[pre-push] running make ci-fast..."
cd "$(git rev-parse --show-toplevel)"
make ci-fast
chmod +x /Users/dmestas/projects/bones/.githooks/pre-push
cd /Users/dmestas/projects/bones
./.githooks/pre-push

Expected: prints [pre-push] running make ci-fast..., runs ci-fast, exits 0.

make setup-hooks
git config core.hooksPath

Expected: prints .githooks.

Make a trivial change so push has work to do:

echo "" >> .githooks/pre-push   # add trailing newline
git add .githooks/pre-push
git commit --amend --no-edit
git push --dry-run -u origin chore/local-dev-hygiene 2>&1 | head -5
Expected: see [pre-push] running make ci-fast... in output.

git add .githooks/pre-push
git commit -m "feat: add pre-push hook running make ci-fast

Catches obvious breakage (vet errors, build break, short test failures)
before push. ~60s. Skip with: git push --no-verify"

Task 1.5: Create .github/RELEASE_TEMPLATE.md

Files: Create .github/RELEASE_TEMPLATE.md

Write to /Users/dmestas/projects/bones/.github/RELEASE_TEMPLATE.md:

<!-- Edit before tagging. The `make release` target appends `git log --oneline` since the previous tag below this template, then opens $EDITOR. Save and quit to create the annotated tag. -->

## Highlights

-

## Breaking changes

None.

## Upgrade notes

-
make release VERSION=v9.9.9-test EDITOR=cat

Expected: prints "Release v9.9.9-test", then the template, then "## Changes" and a commit list.

Cleanup:

git tag -d v9.9.9-test

git add .github/RELEASE_TEMPLATE.md
git commit -m "feat: add release annotation template

Used by 'make release VERSION=...' as the body skeleton for annotated tags."

Task 1.6: Open PR

cd /Users/dmestas/projects/bones
git push -u origin chore/local-dev-hygiene
gh pr create --title "chore: add local dev hygiene targets and pre-push hook" --body "$(cat <<'EOF'
## Summary
- Add `make ci` (verbatim mirror of `.github/workflows/ci.yml`)
- Add `make ci-fast` (vet+build+short tests, ~60s for pre-push)
- Add `make release VERSION=...` helper with annotated tag template
- Add `make setup-hooks` (matches libfossil/edgesync pattern)
- Add `.githooks/pre-push` running `make ci-fast`
- Add `.github/RELEASE_TEMPLATE.md` boilerplate

No remote behavior change. Lays groundwork for the upstream-bump
workflow in PR #4 and for consistent release ergonomics.

## Test plan
- [x] `make ci` exits 0 on a clean main
- [x] `make ci-fast` runs in <60s
- [x] `make release VERSION=v9.9.9-test EDITOR=true` validates and creates a local tag, then deleted
- [x] `make setup-hooks` configures `core.hooksPath` to `.githooks/`
- [x] `git push --dry-run` triggers the pre-push hook
- [x] `make release` rejects bad version formats and missing VERSION

Spec: see libfossil docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md
EOF
)"

Expected: PR URL printed.

gh pr checks --watch

Wait for green. Then on local main:

git checkout main
make ci
Expected: exits 0 (still passes locally — sanity check before merge).

User performs the merge manually per CLAUDE.md ("wait for me to merge"). After merge:

git checkout main
git pull --ff-only origin main
git branch -d chore/local-dev-hygiene
git push origin --delete chore/local-dev-hygiene

Phase 2: PR #2 — edgesync local dev hygiene

Repo: /Users/dmestas/projects/edgesync Branch: chore/local-dev-hygiene Files added: .githooks/pre-push, .github/RELEASE_TEMPLATE.md Files modified: Makefile

Task 2.0: Branch setup

cd /Users/dmestas/projects/edgesync
git fetch origin
git checkout main
git pull --ff-only origin main
git checkout -b chore/local-dev-hygiene

Task 2.1: Inspect edgesync's CI workflows for the verbatim ci command

edgesync has both ci.yml and test.yml. Read them to extract the exact run: blocks:

cat /Users/dmestas/projects/edgesync/.github/workflows/ci.yml
cat /Users/dmestas/projects/edgesync/.github/workflows/test.yml

Note the exact commands. Per Explore snapshot, ci.yml runs:

And test.yml adds the sim/ tests. The make ci target should mirror ci.yml only (sim/ DST is its own thing run separately by test.yml; if a test-sim target makes sense, add it as well).

If actual workflow content differs from the Explore snapshot, use the actual workflow content as the source of truth.

Task 2.2: Add ci and ci-fast targets

Files: Modify Makefile

Append to /Users/dmestas/projects/edgesync/Makefile (preserving existing content):


# CI mirror — must match .github/workflows/ci.yml verbatim.
.PHONY: ci ci-vet ci-leaf-bridge ci-cmd ci-fast

ci: ci-vet ci-leaf-bridge ci-cmd

ci-vet:
	go vet ./...
	cd leaf && go vet ./...
	cd bridge && go vet ./...

ci-leaf-bridge:
	cd leaf && go test ./... -short -count=1
	cd bridge && go test ./... -short -count=1

ci-cmd:
	go build -buildvcs=false ./cmd/edgesync/
	go test -buildvcs=false ./cmd/edgesync/ -count=1 -timeout=60s

# Fast subset for pre-push hook.
ci-fast:
	go vet ./...
	go build ./...
	go test -short -count=1 -timeout=30s ./...

Note: ci-leaf-bridge runs leaf and bridge tests sequentially; ci.yml runs them in background (&) which is a parallelism trick that doesn't translate cleanly to make. Sequential is acceptable for local mirror.

make ci

Expected: exits 0.

time make ci-fast

Expected: exits 0 in <60s.

git add Makefile
git commit -m "feat(make): add ci and ci-fast targets

ci mirrors .github/workflows/ci.yml verbatim (vet + leaf/bridge tests + cmd build/test).
ci-fast runs vet + build + short tests (~60s) for pre-push hook."

Task 2.3: Add make release VERSION=... target

Files: Modify Makefile

Append to /Users/dmestas/projects/edgesync/Makefile (use the same recipe as bones Step 1.2.1 — repeating verbatim):


.PHONY: release
release:
	@test -n "$(VERSION)" || { echo "VERSION=vX.Y.Z required"; exit 1; }
	@echo "$(VERSION)" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$$' || { echo "bad version format: $(VERSION)"; exit 1; }
	@git diff --quiet || { echo "tree dirty; commit or stash first"; exit 1; }
	@git fetch origin --tags
	@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "tag $(VERSION) already exists"; exit 1; fi
	@$(MAKE) ci
	@PREV=$$(git describe --tags --abbrev=0 2>/dev/null || echo ""); \
	  TMPL=.github/RELEASE_TEMPLATE.md; \
	  TMP=$$(mktemp); \
	  { echo "Release $(VERSION)"; echo; \
	    [ -f $$TMPL ] && { cat $$TMPL; echo; }; \
	    echo "## Changes"; \
	    if [ -n "$$PREV" ]; then git log --oneline $$PREV..HEAD; else git log --oneline; fi; } > $$TMP; \
	  $${EDITOR:-vi} $$TMP; \
	  git tag -a "$(VERSION)" -F $$TMP; \
	  rm $$TMP
	@echo ""
	@echo "Tag $(VERSION) created locally. To publish:"
	@echo "  git push origin $(VERSION)"
make release          # missing VERSION
make release VERSION=foo

Expected: each exits 1 with the appropriate error.

make release VERSION=v9.9.9-test EDITOR=true
git tag -d v9.9.9-test

Expected: tag created, then deleted.

git add Makefile
git commit -m "feat(make): add release VERSION=... helper

Validates clean tree, runs make ci, opens \$EDITOR with auto-populated
tag annotation template, creates annotated tag locally. Push is intentionally manual."

Task 2.4: Create .githooks/pre-push

edgesync already has .githooks/ configured (existing setup-hooks target sets core.hooksPath). Just add the pre-push file.

Files: Create .githooks/pre-push

ls /Users/dmestas/projects/edgesync/.githooks/

Expected: lists existing hooks (likely pre-commit). If dir doesn't exist, mkdir -p first.

Write to /Users/dmestas/projects/edgesync/.githooks/pre-push:

#!/usr/bin/env bash
# Pre-push hook — runs `make ci-fast` (~60s) to catch obvious breakage
# before pushing. Mirrors a fast subset of GitHub CI.
#
# Skip with: git push --no-verify

set -e
echo "[pre-push] running make ci-fast..."
cd "$(git rev-parse --show-toplevel)"
make ci-fast
chmod +x /Users/dmestas/projects/edgesync/.githooks/pre-push
cd /Users/dmestas/projects/edgesync
./.githooks/pre-push

Expected: prints message, runs ci-fast, exits 0.

git config core.hooksPath

Expected: prints .githooks. If not, run make setup-hooks.

git add .githooks/pre-push
git commit -m "feat: add pre-push hook running make ci-fast

Catches obvious breakage before push. ~60s. Skip with: git push --no-verify"

Task 2.5: Create .github/RELEASE_TEMPLATE.md

Write to /Users/dmestas/projects/edgesync/.github/RELEASE_TEMPLATE.md (same content as bones — repeating verbatim):

<!-- Edit before tagging. The `make release` target appends `git log --oneline` since the previous tag below this template, then opens $EDITOR. Save and quit to create the annotated tag. -->

## Highlights

-

## Breaking changes

None.

## Upgrade notes

-
git add .github/RELEASE_TEMPLATE.md
git commit -m "feat: add release annotation template

Used by 'make release VERSION=...' as the body skeleton for annotated tags."

Task 2.6: Open PR

cd /Users/dmestas/projects/edgesync
git push -u origin chore/local-dev-hygiene
gh pr create --title "chore: add local dev hygiene targets and pre-push hook" --body "$(cat <<'EOF'
## Summary
- Add `make ci` (mirrors `.github/workflows/ci.yml` verbatim)
- Add `make ci-fast` (vet+build+short tests, ~60s for pre-push)
- Add `make release VERSION=...` helper
- Add `.githooks/pre-push` (existing core.hooksPath already configured)
- Add `.github/RELEASE_TEMPLATE.md`

No remote behavior change. Sets up local dev surface to match libfossil
and bones; lays groundwork for upstream-bump workflow in PR #5.

## Test plan
- [x] `make ci` exits 0
- [x] `make ci-fast` runs in <60s
- [x] `make release VERSION=v9.9.9-test EDITOR=true` works and is reverted
- [x] `git push --dry-run` triggers the pre-push hook
- [x] `make release` rejects bad input

Spec: see libfossil docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md
EOF
)"
gh pr checks --watch

After user merges:

git checkout main && git pull --ff-only origin main
git branch -d chore/local-dev-hygiene
git push origin --delete chore/local-dev-hygiene


Phase 3: PR #3 — libfossil local dev hygiene

Repo: /Users/dmestas/projects/libfossil Branch: chore/local-dev-hygiene Files added: .github/RELEASE_TEMPLATE.md Files modified: Makefile

Deliberate deviation from PR #1/#2 (decided 2026-04-30, option A): libfossil's existing .githooks/pre-commit is comprehensive (~45s — modernc + ncruces + vet + otel + build + SDK drift) and already exceeds what make ci-fast would do. So PR #3 does not add a pre-push hook and does not add a ci-fast Makefile target. libfossil uses its existing pre-commit as the sole local gate; make ci is for manual full-CI verification before tagging. Tasks 3.3 (pre-push hook) and the ci-fast recipe in Task 3.1 below are obsolete and were not implemented. PR #19 (merged 2026-04-30) shipped only ci/ci-default/ci-ncruces/ci-otel-target + release + RELEASE_TEMPLATE.

Multi-module note for any future libfossil pre-push: libfossil has 3 sub-modules with their own go.mod (db/driver/modernc/, db/driver/ncruces/, observer/otel/). go test ./... from repo root does not descend into them. Any future make ci-fast would need explicit cd <subdir> && go test -short ./... per sub-module — same gap edgesync hit and fixed.

libfossil has the most complex make ci because its CI runs three jobs: default driver (modernc), ncruces driver, and otel.

Task 3.0: Branch setup

cd /Users/dmestas/projects/libfossil
git fetch origin
git checkout main
git pull --ff-only origin main
git checkout -b chore/local-dev-hygiene

Task 3.1: Add make ci target (verbatim mirror of test.yml)

libfossil's test.yml has two driver jobs (modernc default, ncruces) plus an otel job. The existing test, test-drivers, test-otel targets in the Makefile do not use GOWORK=off; the CI workflow does. So we add a new ci target rather than aliasing.

Files: Modify Makefile

Append to /Users/dmestas/projects/libfossil/Makefile:


# CI mirror — must match .github/workflows/test.yml verbatim.
# Note: GOWORK=off is critical; the existing test/test-drivers/test-otel
# targets do NOT use it but CI does, leading to historical drift.
.PHONY: ci ci-default ci-ncruces ci-otel-target ci-fast

ci: ci-default ci-ncruces ci-otel-target

ci-default:
	GOWORK=off go test $$(GOWORK=off go list ./... | grep -v '/dst') -count=1 -timeout=120s
	GOWORK=off go test ./dst/... -count=1 -timeout=300s
	cd db/driver/modernc && GOWORK=off go test ./... -count=1
	GOWORK=off go vet ./...
	GOWORK=off go build ./cmd/libfossil/

ci-ncruces:
	GOWORK=off go test -tags test_ncruces $$(GOWORK=off go list ./... | grep -v '/dst' | grep -v 'cmd/libfossil') -count=1 -timeout=120s
	GOWORK=off go test -tags test_ncruces ./dst/... -count=1 -timeout=300s
	cd db/driver/ncruces && GOWORK=off go test ./... -count=1

ci-otel-target:
	cd observer/otel && GOWORK=off go test ./... -count=1

# Fast subset for pre-push hook (~30-60s).
ci-fast:
	GOWORK=off go vet ./...
	GOWORK=off go build ./cmd/libfossil/
	GOWORK=off go test -short -count=1 -timeout=30s $$(GOWORK=off go list ./... | grep -v '/dst')

Note: target name is ci-otel-target not ci-otel to avoid potential collision with the existing test-otel shape if the Makefile evolves.

make ci-default

Expected: exits 0.

make ci-ncruces

Expected: exits 0.

make ci-otel-target

Expected: exits 0.

time make ci-fast

Expected: exits 0 in <60s.

git add Makefile
git commit -m "feat(make): add ci and ci-fast targets

ci mirrors .github/workflows/test.yml verbatim across all three jobs:
  ci-default     — modernc driver tests + DST + vet + cmd build
  ci-ncruces     — ncruces driver tests with -tags test_ncruces
  ci-otel-target — observer/otel tests

All use GOWORK=off (matching CI). Existing test/test-drivers/test-otel
targets are left alone — they don't use GOWORK=off and have drifted
from CI; ci is the single source of truth going forward.

ci-fast runs vet + build + short tests (~60s) for pre-push hook."

Task 3.2: Add make release VERSION=... target

Files: Modify Makefile

Append to /Users/dmestas/projects/libfossil/Makefile:


.PHONY: release
release:
	@test -n "$(VERSION)" || { echo "VERSION=vX.Y.Z required"; exit 1; }
	@echo "$(VERSION)" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$$' || { echo "bad version format: $(VERSION)"; exit 1; }
	@git diff --quiet || { echo "tree dirty; commit or stash first"; exit 1; }
	@git fetch origin --tags
	@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "tag $(VERSION) already exists"; exit 1; fi
	@$(MAKE) ci
	@PREV=$$(git describe --tags --abbrev=0 2>/dev/null || echo ""); \
	  TMPL=.github/RELEASE_TEMPLATE.md; \
	  TMP=$$(mktemp); \
	  { echo "Release $(VERSION)"; echo; \
	    [ -f $$TMPL ] && { cat $$TMPL; echo; }; \
	    echo "## Changes"; \
	    if [ -n "$$PREV" ]; then git log --oneline $$PREV..HEAD; else git log --oneline; fi; } > $$TMP; \
	  $${EDITOR:-vi} $$TMP; \
	  git tag -a "$(VERSION)" -F $$TMP; \
	  rm $$TMP
	@echo ""
	@echo "Tag $(VERSION) created locally. To publish:"
	@echo "  git push origin $(VERSION)"
make release
make release VERSION=foo
make release VERSION=v9.9.9-test EDITOR=true
git tag -d v9.9.9-test

Expected: first two exit 1; third creates tag (after running ci which is slow — be patient or test on a smaller scope by setting EDITOR=true to skip prompt).

git add Makefile
git commit -m "feat(make): add release VERSION=... helper

Validates clean tree, runs make ci, opens \$EDITOR with auto-populated
tag annotation template, creates annotated tag locally. Push is intentionally manual."

Task 3.3: Create .githooks/pre-push

libfossil already has .githooks/ (existing setup-hooks target sets core.hooksPath).

ls /Users/dmestas/projects/libfossil/.githooks/

Expected: lists existing hooks (likely pre-commit).

Write to /Users/dmestas/projects/libfossil/.githooks/pre-push:

#!/usr/bin/env bash
# Pre-push hook — runs `make ci-fast` (~60s) to catch obvious breakage
# before pushing. Mirrors a fast subset of GitHub CI.
#
# Skip with: git push --no-verify

set -e
echo "[pre-push] running make ci-fast..."
cd "$(git rev-parse --show-toplevel)"
make ci-fast
chmod +x /Users/dmestas/projects/libfossil/.githooks/pre-push
./.githooks/pre-push

Expected: exits 0.

git config core.hooksPath

Expected: .githooks. If empty, run make setup-hooks.

git add .githooks/pre-push
git commit -m "feat: add pre-push hook running make ci-fast

Catches obvious breakage (vet errors, build break, short test failures)
before push. ~60s. Skip with: git push --no-verify"

Task 3.4: Create .github/RELEASE_TEMPLATE.md

Write to /Users/dmestas/projects/libfossil/.github/RELEASE_TEMPLATE.md:

<!-- Edit before tagging. The `make release` target appends `git log --oneline` since the previous tag below this template, then opens $EDITOR. Save and quit to create the annotated tag. -->

## Highlights

-

## Breaking changes

None.

## Upgrade notes

-
git add .github/RELEASE_TEMPLATE.md
git commit -m "feat: add release annotation template

Used by 'make release VERSION=...' as the body skeleton for annotated tags."

Task 3.5: Open PR

cd /Users/dmestas/projects/libfossil
git push -u origin chore/local-dev-hygiene
gh pr create --title "chore: add local dev hygiene targets and pre-push hook" --body "$(cat <<'EOF'
## Summary
- Add `make ci` (verbatim mirror of `.github/workflows/test.yml`):
  - `ci-default` — modernc driver + DST + vet + build
  - `ci-ncruces` — ncruces driver
  - `ci-otel-target` — observer/otel tests
  All use `GOWORK=off` matching CI. Existing `test/test-drivers/test-otel`
  targets left untouched (they don't use GOWORK=off; preserved for
  alternate use cases).
- Add `make ci-fast` (vet+build+short tests, ~60s for pre-push)
- Add `make release VERSION=...` helper
- Add `.githooks/pre-push`
- Add `.github/RELEASE_TEMPLATE.md`

No remote behavior change.

## Test plan
- [x] `make ci-default` exits 0
- [x] `make ci-ncruces` exits 0
- [x] `make ci-otel-target` exits 0
- [x] `make ci-fast` runs in <60s
- [x] `make release VERSION=v9.9.9-test EDITOR=true` works and is reverted
- [x] `git push --dry-run` triggers the pre-push hook
- [x] `make release` rejects bad input

Spec: docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md
EOF
)"
gh pr checks --watch

After user merges:

git checkout main && git pull --ff-only origin main
git branch -d chore/local-dev-hygiene
git push origin --delete chore/local-dev-hygiene


Phase 4: PR #4 — bones receiver (bump-upstream.yml) + goreleaser changelog groups

Repo: /Users/dmestas/projects/bones Branch: feat/upstream-bump-receiver Files added: .github/workflows/bump-upstream.yml Files modified: .goreleaser.yml

Prerequisite: Phase 0 (DISPATCH_PAT setup) does not need to be complete for this PR — bones is only a receiver, no PAT needed in bones itself. Phase 0 must be complete before PR #5 merges.

Task 4.0: Branch setup

cd /Users/dmestas/projects/bones
git fetch origin
git checkout main
git pull --ff-only origin main
git checkout -b feat/upstream-bump-receiver

Task 4.1: Add bump-upstream.yml receiver workflow

Files: Create .github/workflows/bump-upstream.yml

Write to /Users/dmestas/projects/bones/.github/workflows/bump-upstream.yml:

name: Bump upstream
on:
  repository_dispatch:
    types: [upstream-bump]
  workflow_dispatch:
    inputs:
      repo:        { required: true,  type: string }
      module_path: { required: true,  type: string }
      version:     { required: true,  type: string }
      release_url: { required: false, type: string }
      tag_sha:     { required: false, type: string }
      actor:       { required: false, type: string }

permissions:
  contents: write
  pull-requests: write

jobs:
  bump:
    runs-on: ubuntu-latest
    env:
      EXPECTED_REPO: EdgeSync
      EXPECTED_MODULE: github.com/danmestas/EdgeSync
    steps:
      - name: Resolve inputs
        id: in
        env:
          PAYLOAD_REPO: ${{ github.event.client_payload.repo }}
          PAYLOAD_MODULE: ${{ github.event.client_payload.module_path }}
          PAYLOAD_VERSION: ${{ github.event.client_payload.version }}
          PAYLOAD_RELEASE_URL: ${{ github.event.client_payload.release_url }}
          PAYLOAD_TAG_SHA: ${{ github.event.client_payload.tag_sha }}
          PAYLOAD_ACTOR: ${{ github.event.client_payload.actor }}
          INPUT_REPO: ${{ inputs.repo }}
          INPUT_MODULE: ${{ inputs.module_path }}
          INPUT_VERSION: ${{ inputs.version }}
          INPUT_RELEASE_URL: ${{ inputs.release_url }}
          INPUT_TAG_SHA: ${{ inputs.tag_sha }}
          INPUT_ACTOR: ${{ inputs.actor }}
        run: |
          REPO="${PAYLOAD_REPO:-$INPUT_REPO}"
          MODULE_PATH="${PAYLOAD_MODULE:-$INPUT_MODULE}"
          VERSION="${PAYLOAD_VERSION:-$INPUT_VERSION}"
          RELEASE_URL="${PAYLOAD_RELEASE_URL:-$INPUT_RELEASE_URL}"
          TAG_SHA="${PAYLOAD_TAG_SHA:-$INPUT_TAG_SHA}"
          ACTOR="${PAYLOAD_ACTOR:-${INPUT_ACTOR:-${{ github.actor }}}}"

          if [[ "$REPO" != "$EXPECTED_REPO" ]]; then
            echo "::notice::Ignoring dispatch for repo=$REPO (expected $EXPECTED_REPO)"
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          if [[ "$MODULE_PATH" != "$EXPECTED_MODULE" ]]; then
            echo "::error::module_path mismatch: got '$MODULE_PATH', expected '$EXPECTED_MODULE'"
            exit 1
          fi
          if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then
            echo "::error::bad version: '$VERSION'"
            exit 1
          fi

          {
            echo "repo=$REPO"
            echo "module=$MODULE_PATH"
            echo "version=$VERSION"
            echo "release_url=$RELEASE_URL"
            echo "tag_sha=$TAG_SHA"
            echo "actor=$ACTOR"
            echo "branch=chore/bump-${REPO}-${VERSION}"
          } >> "$GITHUB_OUTPUT"

      - name: Idempotency check
        if: steps.in.outputs.skip != 'true'
        id: idem
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: ${{ steps.in.outputs.branch }}
        run: |
          if gh pr list --repo "${{ github.repository }}" --head "$BRANCH" --state open --json number -q '.[0].number' | grep -q .; then
            echo "exists=true" >> "$GITHUB_OUTPUT"
            echo "::notice::PR already open for $BRANCH; skipping"
          fi

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true'
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true'
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true'
        name: Bump module
        id: bump
        env:
          BRANCH: ${{ steps.in.outputs.branch }}
          MODULE: ${{ steps.in.outputs.module }}
          VERSION: ${{ steps.in.outputs.version }}
        run: |
          git switch -c "$BRANCH"
          # GOPROXY can lag the upstream tag push by a few seconds; retry
          # before giving up so transient propagation lag self-recovers.
          for i in 1 2 3; do
            if go get "${MODULE}@${VERSION}"; then
              break
            fi
            if [ "$i" -lt 3 ]; then
              echo "::notice::go get failed (attempt $i/3) — likely GOPROXY propagation race; retrying in 15s..."
              sleep 15
            else
              echo "::error::go get failed after 3 attempts"
              exit 1
            fi
          done
          go mod tidy
          if git diff --quiet go.mod go.sum; then
            echo "::notice::No-op bump — version already pinned"
            echo "noop=true" >> "$GITHUB_OUTPUT"
          fi

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true' && steps.bump.outputs.noop != 'true'
        name: Verify build
        run: go build ./...

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true' && steps.bump.outputs.noop != 'true'
        name: Commit + push + open PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: ${{ steps.in.outputs.branch }}
          REPO: ${{ steps.in.outputs.repo }}
          VERSION: ${{ steps.in.outputs.version }}
          MODULE: ${{ steps.in.outputs.module }}
          RELEASE_URL: ${{ steps.in.outputs.release_url }}
          TAG_SHA: ${{ steps.in.outputs.tag_sha }}
          ACTOR: ${{ steps.in.outputs.actor }}
        run: |
          git config user.name  "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add go.mod go.sum
          git commit -m "chore: bump ${REPO} → ${VERSION}"
          git push -u origin "$BRANCH"
          gh pr create \
            --title "chore: bump ${REPO} → ${VERSION}" \
            --body "Auto-bump from upstream release.

          - Module: \`${MODULE}\`
          - Version: \`${VERSION}\`
          - Upstream release: ${RELEASE_URL}
          - Tag SHA: \`${TAG_SHA}\`
          - Triggered by: @${ACTOR}

          CI must pass before merge. Review go.sum diff for transitive surprises."

If actionlint is installed:

actionlint .github/workflows/bump-upstream.yml
Expected: no errors.

If actionlint isn't installed:

brew install actionlint   # macOS
Or skip locally and rely on GitHub's syntax check after push.

Also verify YAML is at least parseable:

python3 -c "import yaml; yaml.safe_load(open('.github/workflows/bump-upstream.yml'))" && echo OK
Expected: OK.

git add .github/workflows/bump-upstream.yml
git commit -m "feat(ci): add bump-upstream receiver workflow

Listens for repository_dispatch event_type=upstream-bump from EdgeSync's
release workflow. Validates payload, checks for existing open PR
(idempotency), runs go get + go mod tidy + go build, opens a chore PR.

Falls back to workflow_dispatch with the same inputs for manual recovery
if a dispatch is ever missed."

Task 4.2: Add groups: to existing .goreleaser.yml changelog

Files: Modify .goreleaser.yml

grep -A 12 'changelog:' /Users/dmestas/projects/bones/.goreleaser.yml

Note: existing block has sort: asc, use: github, exclude: [...] per Explore snapshot.

Open /Users/dmestas/projects/bones/.goreleaser.yml. Replace the existing changelog: block with:

changelog:
  sort: asc
  use: github
  groups:
    - title: 'Features'
      regexp: '^.*?feat(\(.+\))??!?:.+$'
      order: 0
    - title: 'Bug fixes'
      regexp: '^.*?fix(\(.+\))??!?:.+$'
      order: 1
    - title: 'Other'
      order: 999
  filters:
    exclude:
      - '^docs:'
      - '^chore:'
      - '^test:'
      - '^ci:'
      - 'merge conflict'
      - 'Merge PR'
      - 'Merge branch'

(Adjust filters.exclude to match the existing block's actual exclude patterns — preserve any project-specific entries.)

goreleaser check

Expected: config is valid.

If goreleaser isn't installed:

brew install goreleaser

git add .goreleaser.yml
git commit -m "feat(release): add changelog groups (Features/Bug fixes/Other)

Replaces flat commit list with conventional-commit-grouped sections in
the auto-generated GitHub release notes."

Task 4.3: Open PR

cd /Users/dmestas/projects/bones
git push -u origin feat/upstream-bump-receiver
gh pr create --title "feat(ci): add bump-upstream receiver and changelog groups" --body "$(cat <<'EOF'
## Summary
- Add `.github/workflows/bump-upstream.yml` — listens for repository_dispatch
  events from EdgeSync's release workflow, opens a chore PR bumping
  `github.com/danmestas/EdgeSync` to the new version. Includes payload
  validation, idempotency check, no-op detection, and workflow_dispatch
  fallback for manual triggers.
- Modify `.goreleaser.yml` — add `groups:` to changelog config so release
  notes are split into Features / Bug fixes / Other sections.

No external trigger yet — EdgeSync's dispatch step is added in PR #5.
This PR is testable by manual `gh api` fire (see "Test plan" below).

## Test plan
- [x] `actionlint` accepts the workflow
- [x] `goreleaser check` accepts the config
- [ ] After merge: manual fire test (see DEPLOY NOTES)

## Deploy notes
After merge, smoke-test the receiver:

bash gh api -X POST repos/danmestas/bones/dispatches -f event_type=upstream-bump -f client_payload[repo]=EdgeSync -f client_payload[module_path]=github.com/danmestas/EdgeSync -f client_payload[version]=v0.0.7 -f client_payload[release_url]=https://github.com/danmestas/EdgeSync/releases/tag/v0.0.7 -f client_payload[tag_sha]=$(gh api repos/danmestas/EdgeSync/git/refs/tags/v0.0.7 -q .object.sha) -f client_payload[actor]=danmestas ```

Expected: noop=true (already pinned), no PR created. Idempotency: re-fire same command, expect "PR already open" notice (also no PR). Bad input test: fire with version=vbogus, expect workflow failure.

Spec: see libfossil docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md EOF )" ```

gh pr checks --watch

User merges manually.

Task 4.4: Post-merge smoke tests

After PR #4 merges to main:

cd /Users/dmestas/projects/bones
git checkout main
git pull --ff-only origin main
git branch -d feat/upstream-bump-receiver
git push origin --delete feat/upstream-bump-receiver
gh api -X POST repos/danmestas/bones/dispatches \
  -f event_type=upstream-bump \
  -f client_payload[repo]=EdgeSync \
  -f client_payload[module_path]=github.com/danmestas/EdgeSync \
  -f client_payload[version]=v0.0.7 \
  -f client_payload[release_url]=https://github.com/danmestas/EdgeSync/releases/tag/v0.0.7 \
  -f client_payload[tag_sha]=$(gh api repos/danmestas/EdgeSync/git/refs/tags/v0.0.7 -q .object.sha) \
  -f client_payload[actor]=danmestas

Watch the run:

gh run list --workflow bump-upstream.yml --limit 1
gh run view --log $(gh run list --workflow bump-upstream.yml --limit 1 --json databaseId -q '.[0].databaseId')

Expected: workflow succeeds with ::notice::No-op bump — version already pinned. No PR created.

gh api -X POST repos/danmestas/bones/dispatches \
  -f event_type=upstream-bump \
  -f client_payload[repo]=somethingelse \
  -f client_payload[module_path]=github.com/danmestas/somethingelse \
  -f client_payload[version]=v1.0.0 \
  -f client_payload[release_url]=https://example.com \
  -f client_payload[tag_sha]=deadbeef \
  -f client_payload[actor]=danmestas

Expected: workflow succeeds with ::notice::Ignoring dispatch for repo=somethingelse (expected EdgeSync). No PR created.

gh api -X POST repos/danmestas/bones/dispatches \
  -f event_type=upstream-bump \
  -f client_payload[repo]=EdgeSync \
  -f client_payload[module_path]=github.com/danmestas/EdgeSync \
  -f client_payload[version]=vbogus \
  -f client_payload[release_url]=https://example.com \
  -f client_payload[tag_sha]=deadbeef \
  -f client_payload[actor]=danmestas

Expected: workflow fails with ::error::bad version: 'vbogus'.

gh workflow run bump-upstream.yml \
  -f repo=EdgeSync \
  -f module_path=github.com/danmestas/EdgeSync \
  -f version=v0.0.7 \
  -f release_url=https://github.com/danmestas/EdgeSync/releases/tag/v0.0.7 \
  -f tag_sha=$(gh api repos/danmestas/EdgeSync/git/refs/tags/v0.0.7 -q .object.sha) \
  -f actor=danmestas

Expected: same no-op result as Step 4.4.2.

If any test fails: investigate the run log, fix the workflow on a follow-up PR before proceeding to Phase 5.


Phase 5: PR #5 — edgesync receiver + dispatcher + changelog groups

Repo: /Users/dmestas/projects/edgesync Branch: feat/upstream-bump-and-dispatch Files added: .github/workflows/bump-upstream.yml Files modified: .github/workflows/release.yml, .goreleaser.yml

Prerequisite: Phase 0 (DISPATCH_PAT secret in edgesync repo) must be complete before merging this PR.

Task 5.0: Branch setup

cd /Users/dmestas/projects/edgesync
gh secret list | grep DISPATCH_PAT

Expected: DISPATCH_PAT Updated <date>. If absent, return to Phase 0 Step 0.2.

git fetch origin
git checkout main
git pull --ff-only origin main
git checkout -b feat/upstream-bump-and-dispatch

Task 5.1: Add bump-upstream.yml receiver

Files: Create .github/workflows/bump-upstream.yml

Write to /Users/dmestas/projects/edgesync/.github/workflows/bump-upstream.yml — same structure as bones (Step 4.1.1) but with different EXPECTED_REPO / EXPECTED_MODULE:

name: Bump upstream
on:
  repository_dispatch:
    types: [upstream-bump]
  workflow_dispatch:
    inputs:
      repo:        { required: true,  type: string }
      module_path: { required: true,  type: string }
      version:     { required: true,  type: string }
      release_url: { required: false, type: string }
      tag_sha:     { required: false, type: string }
      actor:       { required: false, type: string }

permissions:
  contents: write
  pull-requests: write

jobs:
  bump:
    runs-on: ubuntu-latest
    env:
      EXPECTED_REPO: libfossil
      EXPECTED_MODULE: github.com/danmestas/go-libfossil
    steps:
      - name: Resolve inputs
        id: in
        env:
          PAYLOAD_REPO: ${{ github.event.client_payload.repo }}
          PAYLOAD_MODULE: ${{ github.event.client_payload.module_path }}
          PAYLOAD_VERSION: ${{ github.event.client_payload.version }}
          PAYLOAD_RELEASE_URL: ${{ github.event.client_payload.release_url }}
          PAYLOAD_TAG_SHA: ${{ github.event.client_payload.tag_sha }}
          PAYLOAD_ACTOR: ${{ github.event.client_payload.actor }}
          INPUT_REPO: ${{ inputs.repo }}
          INPUT_MODULE: ${{ inputs.module_path }}
          INPUT_VERSION: ${{ inputs.version }}
          INPUT_RELEASE_URL: ${{ inputs.release_url }}
          INPUT_TAG_SHA: ${{ inputs.tag_sha }}
          INPUT_ACTOR: ${{ inputs.actor }}
        run: |
          REPO="${PAYLOAD_REPO:-$INPUT_REPO}"
          MODULE_PATH="${PAYLOAD_MODULE:-$INPUT_MODULE}"
          VERSION="${PAYLOAD_VERSION:-$INPUT_VERSION}"
          RELEASE_URL="${PAYLOAD_RELEASE_URL:-$INPUT_RELEASE_URL}"
          TAG_SHA="${PAYLOAD_TAG_SHA:-$INPUT_TAG_SHA}"
          ACTOR="${PAYLOAD_ACTOR:-${INPUT_ACTOR:-${{ github.actor }}}}"

          if [[ "$REPO" != "$EXPECTED_REPO" ]]; then
            echo "::notice::Ignoring dispatch for repo=$REPO (expected $EXPECTED_REPO)"
            echo "skip=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          if [[ "$MODULE_PATH" != "$EXPECTED_MODULE" ]]; then
            echo "::error::module_path mismatch: got '$MODULE_PATH', expected '$EXPECTED_MODULE'"
            exit 1
          fi
          if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-.+)?$ ]]; then
            echo "::error::bad version: '$VERSION'"
            exit 1
          fi

          {
            echo "repo=$REPO"
            echo "module=$MODULE_PATH"
            echo "version=$VERSION"
            echo "release_url=$RELEASE_URL"
            echo "tag_sha=$TAG_SHA"
            echo "actor=$ACTOR"
            echo "branch=chore/bump-${REPO}-${VERSION}"
          } >> "$GITHUB_OUTPUT"

      - name: Idempotency check
        if: steps.in.outputs.skip != 'true'
        id: idem
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: ${{ steps.in.outputs.branch }}
        run: |
          if gh pr list --repo "${{ github.repository }}" --head "$BRANCH" --state open --json number -q '.[0].number' | grep -q .; then
            echo "exists=true" >> "$GITHUB_OUTPUT"
            echo "::notice::PR already open for $BRANCH; skipping"
          fi

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true'
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true'
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true'
        name: Bump module
        id: bump
        env:
          BRANCH: ${{ steps.in.outputs.branch }}
          MODULE: ${{ steps.in.outputs.module }}
          VERSION: ${{ steps.in.outputs.version }}
        run: |
          git switch -c "$BRANCH"
          # GOPROXY can lag the upstream tag push by a few seconds; retry
          # before giving up so transient propagation lag self-recovers.
          for i in 1 2 3; do
            if go get "${MODULE}@${VERSION}"; then
              break
            fi
            if [ "$i" -lt 3 ]; then
              echo "::notice::go get failed (attempt $i/3) — likely GOPROXY propagation race; retrying in 15s..."
              sleep 15
            else
              echo "::error::go get failed after 3 attempts"
              exit 1
            fi
          done
          go mod tidy
          if git diff --quiet go.mod go.sum; then
            echo "::notice::No-op bump — version already pinned"
            echo "noop=true" >> "$GITHUB_OUTPUT"
          fi

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true' && steps.bump.outputs.noop != 'true'
        name: Verify build
        run: go build ./...

      - if: steps.in.outputs.skip != 'true' && steps.idem.outputs.exists != 'true' && steps.bump.outputs.noop != 'true'
        name: Commit + push + open PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          BRANCH: ${{ steps.in.outputs.branch }}
          REPO: ${{ steps.in.outputs.repo }}
          VERSION: ${{ steps.in.outputs.version }}
          MODULE: ${{ steps.in.outputs.module }}
          RELEASE_URL: ${{ steps.in.outputs.release_url }}
          TAG_SHA: ${{ steps.in.outputs.tag_sha }}
          ACTOR: ${{ steps.in.outputs.actor }}
        run: |
          git config user.name  "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add go.mod go.sum
          git commit -m "chore: bump ${REPO} → ${VERSION}"
          git push -u origin "$BRANCH"
          gh pr create \
            --title "chore: bump ${REPO} → ${VERSION}" \
            --body "Auto-bump from upstream release.

          - Module: \`${MODULE}\`
          - Version: \`${VERSION}\`
          - Upstream release: ${RELEASE_URL}
          - Tag SHA: \`${TAG_SHA}\`
          - Triggered by: @${ACTOR}

          CI must pass before merge. Review go.sum diff for transitive surprises."
actionlint .github/workflows/bump-upstream.yml
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/bump-upstream.yml'))" && echo OK

Expected: no errors; OK.

git add .github/workflows/bump-upstream.yml
git commit -m "feat(ci): add bump-upstream receiver workflow

Listens for repository_dispatch event_type=upstream-bump from libfossil's
release workflow. Validates payload, checks for existing open PR
(idempotency), runs go get + go mod tidy + go build, opens a chore PR.

EXPECTED_REPO=libfossil; rejects dispatches from other sources."

Task 5.2: Modify release.yml to dispatch on tag

Files: Modify .github/workflows/release.yml

cat /Users/dmestas/projects/edgesync/.github/workflows/release.yml

Note the existing job name (release) and last step.

Edit /Users/dmestas/projects/edgesync/.github/workflows/release.yml. Add a new step at the end of the release job's steps: list:

      - name: Dispatch downstream bump
        env:
          GH_TOKEN: ${{ secrets.DISPATCH_PAT }}
          DOWNSTREAM: bones
          MODULE_PATH: github.com/danmestas/EdgeSync
        run: |
          gh api -X POST "repos/danmestas/${DOWNSTREAM}/dispatches" \
            -f event_type=upstream-bump \
            -f client_payload[repo]=${{ github.event.repository.name }} \
            -f client_payload[module_path]=${MODULE_PATH} \
            -f client_payload[version]=${{ github.ref_name }} \
            -f client_payload[release_url]=${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }} \
            -f client_payload[tag_sha]=${{ github.sha }} \
            -f client_payload[actor]=${{ github.actor }}
actionlint .github/workflows/release.yml

Expected: no errors.

git add .github/workflows/release.yml
git commit -m "feat(ci): dispatch upstream-bump to bones on tag

After goreleaser publishes a release, fires repository_dispatch
to danmestas/bones so it can open a chore-bump PR. Uses DISPATCH_PAT
(fine-grained, scoped to bones contents:write)."

Task 5.3: Add groups: to .goreleaser.yml changelog

Files: Modify .goreleaser.yml

grep -A 12 'changelog:' /Users/dmestas/projects/edgesync/.goreleaser.yml

Replace the existing changelog: block in /Users/dmestas/projects/edgesync/.goreleaser.yml with:

changelog:
  sort: asc
  use: github
  groups:
    - title: 'Features'
      regexp: '^.*?feat(\(.+\))??!?:.+$'
      order: 0
    - title: 'Bug fixes'
      regexp: '^.*?fix(\(.+\))??!?:.+$'
      order: 1
    - title: 'Other'
      order: 999
  filters:
    exclude:
      - '^docs:'
      - '^chore:'
      - '^test:'
      - '^ci:'
      - 'merge conflict'
      - 'Merge PR'
      - 'Merge branch'

(Preserve any project-specific filters.exclude patterns from the existing block.)

goreleaser check

Expected: config is valid.

git add .goreleaser.yml
git commit -m "feat(release): add changelog groups (Features/Bug fixes/Other)

Replaces flat commit list with conventional-commit-grouped sections in
the auto-generated GitHub release notes."

Task 5.4: Open PR

cd /Users/dmestas/projects/edgesync
git push -u origin feat/upstream-bump-and-dispatch
gh pr create --title "feat(ci): receive libfossil dispatches, dispatch to bones, group changelog" --body "$(cat <<'EOF'
## Summary
- Add `.github/workflows/bump-upstream.yml` (receiver listening for libfossil dispatches)
- Modify `.github/workflows/release.yml` (dispatches to bones on tag)
- Modify `.goreleaser.yml` (group changelog into Features/Bug fixes/Other)

Requires `DISPATCH_PAT` secret already in place (Phase 0 prerequisite).

## Test plan
- [x] `actionlint` accepts both workflow files
- [x] `goreleaser check` accepts the config
- [ ] After merge: receiver smoke test via manual `gh api` fire to edgesync
- [ ] After merge: full sender test by tagging edgesync v0.0.8-rc1 and verifying bones receives a PR; close PR; delete rc tag

## Deploy notes
After merge, smoke-test the receiver:

bash gh api -X POST repos/danmestas/EdgeSync/dispatches -f event_type=upstream-bump -f client_payload[repo]=libfossil -f client_payload[module_path]=github.com/danmestas/go-libfossil -f client_payload[version]=v0.4.5 -f client_payload[release_url]=https://github.com/danmestas/go-libfossil/releases/tag/v0.4.5 -f client_payload[tag_sha]=$(gh api repos/danmestas/libfossil/git/refs/tags/v0.4.5 -q .object.sha) -f client_payload[actor]=danmestas ```

Expected: noop (already pinned). Then test the sender by tagging edgesync v0.0.8-rc1 and watching bones open a bump PR (close PR without merging; delete the rc tag and prerelease GitHub release).

Spec: see libfossil docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md EOF )" ```

gh pr checks --watch

User merges manually.

Task 5.5: Post-merge smoke tests

cd /Users/dmestas/projects/edgesync
git checkout main && git pull --ff-only origin main
git branch -d feat/upstream-bump-and-dispatch
git push origin --delete feat/upstream-bump-and-dispatch
gh api -X POST repos/danmestas/EdgeSync/dispatches \
  -f event_type=upstream-bump \
  -f client_payload[repo]=libfossil \
  -f client_payload[module_path]=github.com/danmestas/go-libfossil \
  -f client_payload[version]=v0.4.5 \
  -f client_payload[release_url]=https://github.com/danmestas/go-libfossil/releases/tag/v0.4.5 \
  -f client_payload[tag_sha]=$(gh api repos/danmestas/libfossil/git/refs/tags/v0.4.5 -q .object.sha) \
  -f client_payload[actor]=danmestas

Watch:

gh run list --workflow bump-upstream.yml --limit 1
Expected: succeeds with no-op (libfossil v0.4.5 already pinned).

Make sure tree is clean and on main:

git checkout main && git status

Tag and push:

git tag -a v0.0.8-rc1 -m "Test release for cascade validation"
git push origin v0.0.8-rc1

In one terminal:

gh run list --workflow release.yml --repo danmestas/EdgeSync --limit 1
gh run watch --repo danmestas/EdgeSync $(gh run list --workflow release.yml --repo danmestas/EdgeSync --limit 1 --json databaseId -q '.[0].databaseId')

After release.yml completes, check bones:

gh run list --workflow bump-upstream.yml --repo danmestas/bones --limit 3
Expected: a recent bump-upstream run dispatched. Check it opened a PR:
gh pr list --repo danmestas/bones --state open --head 'chore/bump-EdgeSync-v0.0.8-rc1'
Expected: one PR.

Close the test PR without merging:

gh pr close --repo danmestas/bones $(gh pr list --repo danmestas/bones --state open --head 'chore/bump-EdgeSync-v0.0.8-rc1' --json number -q '.[0].number') --delete-branch

Delete the rc tag locally and remotely:

cd /Users/dmestas/projects/edgesync
git tag -d v0.0.8-rc1
git push origin --delete v0.0.8-rc1

Delete the prerelease GitHub release:

gh release delete v0.0.8-rc1 --yes --repo danmestas/EdgeSync

If any test fails: investigate, fix on a follow-up PR before proceeding to Phase 6.


Phase 6: PR #6 — libfossil release.yml + dispatch to edgesync

Repo: /Users/dmestas/projects/libfossil Branch: feat/release-workflow Files added: .github/workflows/release.yml

This is the smallest PR but the most consequential — it creates libfossil's first-ever release pipeline.

Prerequisite: DISPATCH_PAT secret in libfossil repo (Phase 0 Step 0.1) must be in place.

Task 6.0: Branch setup

cd /Users/dmestas/projects/libfossil
gh secret list | grep DISPATCH_PAT

Expected: DISPATCH_PAT Updated <date>. If absent, return to Phase 0 Step 0.1.

git fetch origin
git checkout main
git pull --ff-only origin main
git checkout -b feat/release-workflow

Task 6.1: Create release.yml

Files: Create .github/workflows/release.yml

Write to /Users/dmestas/projects/libfossil/.github/workflows/release.yml:

name: Release
on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release create "${{ github.ref_name }}" \
            --generate-notes \
            --verify-tag

      - name: Dispatch downstream bump
        env:
          GH_TOKEN: ${{ secrets.DISPATCH_PAT }}
          DOWNSTREAM: EdgeSync
          MODULE_PATH: github.com/danmestas/go-libfossil
        run: |
          gh api -X POST "repos/danmestas/${DOWNSTREAM}/dispatches" \
            -f event_type=upstream-bump \
            -f client_payload[repo]=${{ github.event.repository.name }} \
            -f client_payload[module_path]=${MODULE_PATH} \
            -f client_payload[version]=${{ github.ref_name }} \
            -f client_payload[release_url]=${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }} \
            -f client_payload[tag_sha]=${{ github.sha }} \
            -f client_payload[actor]=${{ github.actor }}
actionlint .github/workflows/release.yml
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo OK

Expected: no errors; OK.

git add .github/workflows/release.yml
git commit -m "feat(ci): add release workflow with downstream dispatch

On tag push (v*):
  1. Create GitHub release with auto-generated notes from PRs/commits
     since previous tag. Source tarball auto-attached.
  2. Dispatch repository_dispatch event to danmestas/EdgeSync to trigger
     a chore-bump PR there.

libfossil is a library — no binaries are published, so goreleaser is
overkill. 'gh release create --generate-notes' provides equivalent
auto-changelog and source archives in 5 lines."

Task 6.2: Open PR

git push -u origin feat/release-workflow
gh pr create --title "feat(ci): add release workflow with downstream dispatch" --body "$(cat <<'EOF'
## Summary
First release pipeline for libfossil. On tag push:
- Creates GitHub release with auto-generated notes (`gh release --generate-notes`)
- Dispatches `repository_dispatch` to `danmestas/EdgeSync` for downstream bump

Closes the cascade chain. Once merged, the next libfossil tag will fire
PRs in EdgeSync (and via PR #5 also in bones).

## Test plan
- [x] `actionlint` accepts the workflow
- [ ] After merge: tag `v0.4.6-rc1` and watch:
  - libfossil release.yml runs both steps
  - GitHub release `v0.4.6-rc1` created (prerelease)
  - EdgeSync's bump-upstream.yml runs and opens PR `chore/bump-libfossil-v0.4.6-rc1`
- [ ] Cleanup: close EdgeSync PR; delete rc tag from libfossil + EdgeSync; delete libfossil prerelease

## Deploy notes
Test cascade after merge:

bash cd /Users/dmestas/projects/libfossil git checkout main && git pull git tag -a v0.4.6-rc1 -m "Test release for cascade validation" git push origin v0.4.6-rc1 ```

Then watch:

gh run list --workflow release.yml --limit 1

After release.yml succeeds, watch EdgeSync:

gh run list --workflow bump-upstream.yml --repo danmestas/EdgeSync --limit 1
gh pr list --repo danmestas/EdgeSync --head 'chore/bump-libfossil-v0.4.6-rc1'

Cleanup (do not merge the rc PR):

gh pr close --repo danmestas/EdgeSync $(gh pr list --repo danmestas/EdgeSync --head 'chore/bump-libfossil-v0.4.6-rc1' --json number -q '.[0].number') --delete-branch
git tag -d v0.4.6-rc1
git push origin --delete v0.4.6-rc1
gh release delete v0.4.6-rc1 --yes

Spec: docs/superpowers/specs/2026-04-30-cross-repo-release-automation-design.md EOF )" ```

gh pr checks --watch

User merges manually.

Task 6.3: End-to-end cascade validation

After PR #6 merges:

cd /Users/dmestas/projects/libfossil
git checkout main && git pull --ff-only origin main
git branch -d feat/release-workflow
git push origin --delete feat/release-workflow
git tag -a v0.4.6-rc1 -m "Test release for cascade validation"
git push origin v0.4.6-rc1
gh run list --workflow release.yml --limit 1
gh run watch $(gh run list --workflow release.yml --limit 1 --json databaseId -q '.[0].databaseId')

Expected: succeeds; both Create GitHub release and Dispatch downstream bump steps complete.

gh release view v0.4.6-rc1

Expected: prerelease with auto-generated notes.

gh run list --workflow bump-upstream.yml --repo danmestas/EdgeSync --limit 1
gh pr list --repo danmestas/EdgeSync --head 'chore/bump-libfossil-v0.4.6-rc1' --json number,title,url

Expected: a recent bump-upstream run; one open PR titled chore: bump libfossil → v0.4.6-rc1.

gh pr close --repo danmestas/EdgeSync $(gh pr list --repo danmestas/EdgeSync --head 'chore/bump-libfossil-v0.4.6-rc1' --json number -q '.[0].number') --delete-branch
git tag -d v0.4.6-rc1
git push origin --delete v0.4.6-rc1
gh release delete v0.4.6-rc1 --yes

Verify cleanup:

gh release list --limit 5
gh pr list --repo danmestas/EdgeSync --state open
Expected: no v0.4.6-rc1 artifacts remain.

Task 6.4: Update memory with cascade-active state

The deferred CI improvements memory file (/Users/dmestas/.claude/projects/-Users-dmestas-projects-libfossil/memory/project_pending_ci_improvements.md) now has additional context: the cascade automation has shipped, the -race/SDK-drift/DST-sweep work can be added to make ci rather than just CI.

This is a memory-update step; perform via the auto-memory system in conversation rather than as a code commit.


Self-review checklist

After plan completes (all 6 PRs merged + post-merge tests passed):

If any item fails after the rollout: investigate that piece in isolation; the cascade can be repaired one workflow file at a time without touching the others.