Artifact 0afffeafd62a5ae3452ae83c3da2c3ea0d21959699a811d54f2f9ffbe757b168:
- File
docs/superpowers/plans/2026-04-30-cross-repo-release-automation.md
— part of check-in
[6273a0750d]
at
2026-07-22 23:03:24
on branch trunk
— fix: correct go.mod module path to go-libfossil (#76) (#117)
* fix: correct go.mod module path to go-libfossil (#76)
The module directive still declared github.com/danmestas/libfossil after the repository was renamed to go-libfossil. pkg.go.dev/godoc scraping matches the declared module path against its hosting path, so the stale path broke SDK doc generation.
Rename the module path across all four in-repo modules (root, the two db/driver submodules, observer/otel, and docs/site) and every internal import, plus the release workflow, the doc-generation script, and the docs.
No tag has been published under the go-libfossil path yet, so the cross-module requires cannot resolve from the proxy. Add replace directives pointing the in-repo requires at their local directories so GOWORK=off builds and the CI matrix resolve without a published version. These replaces are ignored by downstream consumers and can be dropped once new-path tags are cut.
* Guard releases against replace directives; regenerate SDK docs
The go-libfossil rename bridges the in-repo modules with `replace` directives. Go refuses a main module that carries them, so a tag cut while they are present breaks `go install .../cmd/libfossil@<tag>` and every `@version` fetch of the driver submodules. Add a Release workflow step that fails the job before anything is published if any tracked go.mod still contains a replace directive, so the bridge is a CI-enforced TODO rather than a silent landmine. The step comment records what unblocks a release: re-tag the submodules with the corrected module paths, bump the root requires, then drop the replace blocks.
Regenerate the SDK reference docs. The previous pass only substituted URLs over the pre-existing generated files, leaving stale signatures (Repo.Timeline was still documented as taking LogOpts rather than TimelineOpts). Note that gomarkdoc must be run outside a git worktree — inside one it silently strips every source link.
Also reword the README rename note, which read as though the import path had not changed, and re-sort the ncruces require block. (user: [email protected], size: 68981) [annotate] [blame] [check-ins using]
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)
- Create:
.github/workflows/release.yml(PR #6) - Create:
.github/RELEASE_TEMPLATE.md(PR #3) - Create:
.githooks/pre-push(PR #3) - Modify:
Makefile— addci,ci-default,ci-ncruces,ci-otel-target,ci-fast,releasetargets; existingsetup-hooksalready setscore.hooksPath(no change needed) (PR #3)
edgesync (/Users/dmestas/projects/edgesync, GitHub: danmestas/EdgeSync)
- Create:
.github/workflows/bump-upstream.yml(PR #5) - Create:
.github/RELEASE_TEMPLATE.md(PR #2) - Create:
.githooks/pre-push(PR #2) - Modify:
Makefile— addci,ci-fast,releasetargets (PR #2) - Modify:
.github/workflows/release.yml— append dispatch step (PR #5) - Modify:
.goreleaser.yml— addgroups:to existingchangelog:block (PR #5)
bones (/Users/dmestas/projects/bones)
- Create:
.github/workflows/bump-upstream.yml(PR #4) - Create:
.github/RELEASE_TEMPLATE.md(PR #1) - Create:
.githooks/pre-push(PR #1) - Modify:
Makefile— addci,ci-fast,release,setup-hookstargets (PR #1) - Modify:
.goreleaser.yml— addgroups:to existingchangelog:block (PR #4)
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.
- [ ] Step 0.1: Generate
DISPATCH_PATfor libfossil → EdgeSync
Open https://github.com/settings/personal-access-tokens/new in a browser. Configure:
- Token name:
libfossil-dispatch-to-edgesync - Expiration: 1 year
- Resource owner:
danmestas - Repository access: "Only select repositories" → select
danmestas/EdgeSync - Repository permissions:
Contents: Read and write,Metadata: Read-only(Metadata is auto-required)
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>
- [ ] Step 0.2: Generate
DISPATCH_PATfor edgesync → bones
Same flow, configure:
- Token name:
edgesync-dispatch-to-bones - Resource owner:
danmestas - Repository access:
danmestas/bonesonly - Permissions:
Contents: Read and write,Metadata: Read-only
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>
- [ ] Step 0.3: Note rotation reminder
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
- [ ] Step 1.0.1: Create feature branch
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
- [ ] Step 1.1.1: Append
citarget to 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.
- [ ] Step 1.1.2: Run
make cito verify it passes
make ci
Expected: exits 0. (May take 1-3 min.)
- [ ] Step 1.1.3: Run
make ci-fastto verify it passes and is fast
time make ci-fast
Expected: exits 0 in under 60s.
- [ ] Step 1.1.4: Commit
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
- [ ] Step 1.2.1: Append release target to 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)"
- [ ] Step 1.2.2: Verify the target rejects bad input
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".
- [ ] Step 1.2.3: Verify the target works on a test version (without committing the tag)
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.
- [ ] Step 1.2.4: Commit
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
- [ ] Step 1.3.1: Append setup-hooks target
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"
- [ ] Step 1.3.2: Verify (without running it yet — pre-push file doesn't exist)
grep -A 3 'setup-hooks' Makefile
Expected: shows the new target.
- [ ] Step 1.3.3: Commit
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
- [ ] Step 1.4.1: Create the directory and file
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
- [ ] Step 1.4.2: Make executable
chmod +x /Users/dmestas/projects/bones/.githooks/pre-push
- [ ] Step 1.4.3: Test the hook directly
cd /Users/dmestas/projects/bones
./.githooks/pre-push
Expected: prints [pre-push] running make ci-fast..., runs ci-fast, exits 0.
- [ ] Step 1.4.4: Wire it up by running setup-hooks
make setup-hooks
git config core.hooksPath
Expected: prints .githooks.
- [ ] Step 1.4.5: Verify hook fires on push (dry run)
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.
- [ ] Step 1.4.6: Commit the hook
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
- [ ] Step 1.5.1: Create the file
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
-
- [ ] Step 1.5.2: Verify
make releasepicks up the template
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
- [ ] Step 1.5.3: Commit
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
- [ ] Step 1.6.1: Push branch
cd /Users/dmestas/projects/bones
git push -u origin chore/local-dev-hygiene
- [ ] Step 1.6.2: Open PR
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.
- [ ] Step 1.6.3: Wait for CI to pass on the PR, then run local CI again
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).
- [ ] Step 1.6.4: User reviews and merges
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
- [ ] Step 2.0.1: Create feature branch
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:
- [ ] Step 2.1.1: Read CI workflow
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:
go vet ./...cd leaf && go vet ./...cd ../bridge && go vet ./...cd leaf && go test ./... -short -count=1 &cd bridge && go test ./... -short -count=1 &go build -buildvcs=false ./cmd/edgesync/go test -buildvcs=false ./cmd/edgesync/ -count=1 -timeout=60s
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
- [ ] Step 2.2.1: Append ci targets to 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.
- [ ] Step 2.2.2: Run
make ci
make ci
Expected: exits 0.
- [ ] Step 2.2.3: Run
make ci-fast
time make ci-fast
Expected: exits 0 in <60s.
- [ ] Step 2.2.4: Commit
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
- [ ] Step 2.3.1: Append release target
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)"
- [ ] Step 2.3.2: Verify rejection paths
make release # missing VERSION
make release VERSION=foo
Expected: each exits 1 with the appropriate error.
- [ ] Step 2.3.3: Verify success path on test version
make release VERSION=v9.9.9-test EDITOR=true
git tag -d v9.9.9-test
Expected: tag created, then deleted.
- [ ] Step 2.3.4: Commit
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
- [ ] Step 2.4.1: Verify .githooks dir exists
ls /Users/dmestas/projects/edgesync/.githooks/
Expected: lists existing hooks (likely pre-commit). If dir doesn't exist, mkdir -p first.
- [ ] Step 2.4.2: Create pre-push file
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
- [ ] Step 2.4.3: Make executable
chmod +x /Users/dmestas/projects/edgesync/.githooks/pre-push
- [ ] Step 2.4.4: Test the hook directly
cd /Users/dmestas/projects/edgesync
./.githooks/pre-push
Expected: prints message, runs ci-fast, exits 0.
- [ ] Step 2.4.5: Verify core.hooksPath is set (existing setup-hooks)
git config core.hooksPath
Expected: prints .githooks. If not, run make setup-hooks.
- [ ] Step 2.4.6: Commit
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
- [ ] Step 2.5.1: Create file
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
-
- [ ] Step 2.5.2: Commit
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
- [ ] Step 2.6.1: Push and 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
)"
- [ ] Step 2.6.2: Wait for CI green, user merges, cleanup
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-commitis comprehensive (~45s — modernc + ncruces + vet + otel + build + SDK drift) and already exceeds whatmake ci-fastwould do. So PR #3 does not add apre-pushhook and does not add aci-fastMakefile target. libfossil uses its existing pre-commit as the sole local gate;make ciis for manual full-CI verification before tagging. Tasks 3.3 (pre-push hook) and theci-fastrecipe in Task 3.1 below are obsolete and were not implemented. PR #19 (merged 2026-04-30) shipped onlyci/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 futuremake ci-fastwould need explicitcd <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
- [ ] Step 3.0.1: Create feature branch
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
- [ ] Step 3.1.1: Append ci targets to 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.
- [ ] Step 3.1.2: Run
make ci-default
make ci-default
Expected: exits 0.
- [ ] Step 3.1.3: Run
make ci-ncruces
make ci-ncruces
Expected: exits 0.
- [ ] Step 3.1.4: Run
make ci-otel-target
make ci-otel-target
Expected: exits 0.
- [ ] Step 3.1.5: Run
make ci-fast
time make ci-fast
Expected: exits 0 in <60s.
- [ ] Step 3.1.6: Commit
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
- [ ] Step 3.2.1: Append release target (same recipe as bones/edgesync — repeated verbatim)
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)"
- [ ] Step 3.2.2: Verify rejection and success paths
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).
- [ ] Step 3.2.3: Commit
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).
- [ ] Step 3.3.1: Verify .githooks/ exists
ls /Users/dmestas/projects/libfossil/.githooks/
Expected: lists existing hooks (likely pre-commit).
- [ ] Step 3.3.2: Create pre-push
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
- [ ] Step 3.3.3: Make executable + test directly
chmod +x /Users/dmestas/projects/libfossil/.githooks/pre-push
./.githooks/pre-push
Expected: exits 0.
- [ ] Step 3.3.4: Verify core.hooksPath set
git config core.hooksPath
Expected: .githooks. If empty, run make setup-hooks.
- [ ] Step 3.3.5: Commit
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
- [ ] Step 3.4.1: Create file (same content as other repos)
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
-
- [ ] Step 3.4.2: Commit
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
- [ ] Step 3.5.1: Push and 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
)"
- [ ] Step 3.5.2: Wait for CI green, user merges, cleanup
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
- [ ] Step 4.0.1: Create feature branch
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
- [ ] Step 4.1.1: Create the workflow file
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."
- [ ] Step 4.1.2: Validate workflow syntax
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.
- [ ] Step 4.1.3: Commit
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
- [ ] Step 4.2.1: Read current changelog block
grep -A 12 'changelog:' /Users/dmestas/projects/bones/.goreleaser.yml
Note: existing block has sort: asc, use: github, exclude: [...] per Explore snapshot.
- [ ] Step 4.2.2: Modify the changelog block
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.)
- [ ] Step 4.2.3: Validate goreleaser config
goreleaser check
Expected: config is valid.
If goreleaser isn't installed:
brew install goreleaser
- [ ] Step 4.2.4: Commit
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
- [ ] Step 4.3.1: Push and 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 )" ```
- [ ] Step 4.3.2: Wait for CI green, user merges
gh pr checks --watch
User merges manually.
Task 4.4: Post-merge smoke tests
After PR #4 merges to main:
- [ ] Step 4.4.1: Update local 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
- [ ] Step 4.4.2: Test no-op path (already-pinned version)
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.
- [ ] Step 4.4.3: Test wrong-repo path (silent ignore)
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.
- [ ] Step 4.4.4: Test bad-version path (loud failure)
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'.
- [ ] Step 4.4.5: Test workflow_dispatch fallback (UI-driven)
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
- [ ] Step 5.0.1: Verify Phase 0 complete
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.
- [ ] Step 5.0.2: Create feature branch
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
- [ ] Step 5.1.1: Create the workflow file
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."
- [ ] Step 5.1.2: Validate
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.
- [ ] Step 5.1.3: Commit
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
- [ ] Step 5.2.1: Read current release.yml
cat /Users/dmestas/projects/edgesync/.github/workflows/release.yml
Note the existing job name (release) and last step.
- [ ] Step 5.2.2: Append dispatch step to the release job
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 }}
- [ ] Step 5.2.3: Validate workflow
actionlint .github/workflows/release.yml
Expected: no errors.
- [ ] Step 5.2.4: Commit
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
- [ ] Step 5.3.1: Read current changelog block
grep -A 12 'changelog:' /Users/dmestas/projects/edgesync/.goreleaser.yml
- [ ] Step 5.3.2: Modify changelog block
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.)
- [ ] Step 5.3.3: Validate
goreleaser check
Expected: config is valid.
- [ ] Step 5.3.4: Commit
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
- [ ] Step 5.4.1: Push and 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 )" ```
- [ ] Step 5.4.2: Wait for CI green, user merges
gh pr checks --watch
User merges manually.
Task 5.5: Post-merge smoke tests
- [ ] Step 5.5.1: Update local main and clean branch
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
- [ ] Step 5.5.2: Test edgesync receiver with manual fire
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).
- [ ] Step 5.5.3: Test edgesync→bones sender by tagging an rc
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
- [ ] Step 5.5.4: Watch the cascade
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.
- [ ] Step 5.5.5: Cleanup the test cascade
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
- [ ] Step 6.0.1: Verify Phase 0 complete for libfossil
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.
- [ ] Step 6.0.2: Create feature branch
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
- [ ] Step 6.1.1: Create the workflow file
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 }}
- [ ] Step 6.1.2: Validate
actionlint .github/workflows/release.yml
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))" && echo OK
Expected: no errors; OK.
- [ ] Step 6.1.3: Commit
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
- [ ] Step 6.2.1: Push and 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 )" ```
- [ ] Step 6.2.2: Wait for CI green, user merges
gh pr checks --watch
User merges manually.
Task 6.3: End-to-end cascade validation
After PR #6 merges:
- [ ] Step 6.3.1: Update local main and clean branch
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
- [ ] Step 6.3.2: Tag a release candidate
git tag -a v0.4.6-rc1 -m "Test release for cascade validation"
git push origin v0.4.6-rc1
- [ ] Step 6.3.3: Watch libfossil release.yml
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.
- [ ] Step 6.3.4: Verify GitHub release created
gh release view v0.4.6-rc1
Expected: prerelease with auto-generated notes.
- [ ] Step 6.3.5: Verify EdgeSync bump-upstream.yml fired
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.
- [ ] Step 6.3.6: Cleanup the test cascade
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
- [ ] Step 6.4.1: Note the cascade is now live
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):
- [ ] Cascade lag: time from a libfossil tag to bones PR open is <5 min (was 1-3 days).
- [ ] PR count per cascade: bones gets exactly one bump PR per cascade (was two).
- [ ] Local CI mirror:
make ciin each repo exits 0 on a clean main; matches what GitHub CI runs. - [ ] Pre-push hook:
git push --dry-runtriggersmake ci-fast; failures block push. - [ ] Release helper:
make release VERSION=...validates clean tree, runs ci, creates annotated tag locally with template body. - [ ] Auto-changelog: edgesync v0.0.9 and bones v0.3.4 release notes are grouped (Features/Bug fixes/Other) without manual annotation work.
- [ ] PAT rotation reminder set for ~10 months out.
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.