From df2f7f9d852e94601724e34810b7e4c6446dd9d9 Mon Sep 17 00:00:00 2001 From: cafitac Date: Thu, 30 Apr 2026 22:23:49 +0900 Subject: [PATCH] ci: make release sync fallback idempotent --- .dev/status/current-handoff.md | 73 ++++++++++++++++-------------- .github/workflows/auto-release.yml | 18 +++++++- README.md | 2 +- tests/test_release_workflows.py | 12 +++++ 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/.dev/status/current-handoff.md b/.dev/status/current-handoff.md index 37001b8..b088378 100644 --- a/.dev/status/current-handoff.md +++ b/.dev/status/current-handoff.md @@ -1,7 +1,7 @@ # agent-memory current handoff Status: AI-authored draft. Not yet human-approved. -Last updated: 2026-04-30 21:49 KST +Last updated: 2026-04-30 22:21 KST ## Trigger for the next session @@ -16,9 +16,9 @@ read this file first. Do not ask the user to restate context. Verify repo state, ## Ready-to-say answer -지금 agent-memory는 OSS 기본 메모리 레이어 신뢰도 작업 Priority 1~4의 주요 truth lifecycle 조각과 retrieval-eval read-only hardening을 v0.1.31까지 완료했고, 현재 slice는 protected `main` 때문에 반복되던 release metadata sync 수동 절차를 자동 fallback PR 흐름으로 줄이는 작업이야. +지금 agent-memory는 OSS 기본 메모리 레이어 신뢰도 작업 Priority 1~4의 주요 truth lifecycle 조각, retrieval-eval read-only hardening, protected-main release fallback 자동화를 v0.1.32까지 완료했고, 현재 slice는 release fallback rerun idempotency 보강이야. -최신 검증 완료 릴리스는 v0.1.31이야. v0.1.27에서 status transition history, v0.1.28에서 npm wrapper stdin forwarding과 published Hermes hook smoke, v0.1.29에서 fact supersession/replacement relation, v0.1.30에서 `agent-memory review explain fact ...` decision explanation UX, v0.1.31에서 retrieval eval read-only behavior가 들어갔어. 로컬 Hermes hook도 v0.1.31 runtime으로 업데이트되어 doctor/hook smoke가 통과한 상태야. +최신 검증 완료 릴리스는 v0.1.32야. v0.1.27에서 status transition history, v0.1.28에서 npm wrapper stdin forwarding과 published Hermes hook smoke, v0.1.29에서 fact supersession/replacement relation, v0.1.30에서 `agent-memory review explain fact ...` decision explanation UX, v0.1.31에서 retrieval eval read-only behavior, v0.1.32에서 protected-main release-sync PR/tag/publish automation이 들어갔어. 로컬 Hermes hook도 v0.1.32 runtime으로 업데이트되어 doctor/hook smoke가 통과한 상태야. ## Current repo state @@ -35,17 +35,18 @@ Expected GitHub identity: Verified base before this slice: - branch: `main` -- HEAD: `6d955bb chore: release v0.1.31 [skip release] (#30)` -- tag/release: `v0.1.31` -- GitHub Release: `https://github.com/cafitac/agent-memory/releases/tag/v0.1.31` -- npm: `@cafitac/agent-memory@0.1.31` -- PyPI: `cafitac-agent-memory==0.1.31` -- v0.1.31 published smoke artifact: passed after a propagation retry; includes npm/uvx/pipx Hermes hook commands. +- HEAD: `654c5d8 chore: release v0.1.32 [skip release] (#32)` +- tag/release: `v0.1.32` +- GitHub Release: `https://github.com/cafitac/agent-memory/releases/tag/v0.1.32` +- npm: `@cafitac/agent-memory@0.1.32` +- PyPI: `cafitac-agent-memory==0.1.32` +- v0.1.32 published smoke artifact: passed; includes npm/uvx/pipx Hermes hook commands. +- repo Actions workflow setting: `can_approve_pull_request_reviews=true`, needed so `GITHUB_TOKEN` can create release-sync PRs. Active slice/worktree: -- branch: `ci/auto-release-sync-pr` -- worktree: `/Users/reddit/Project/agent-memory/.worktrees/auto-release-sync-pr` +- branch: `ci/release-fallback-idempotency` +- worktree: `/Users/reddit/Project/agent-memory/.worktrees/release-fallback-idempotency` Expected local untracked artifacts to preserve in the root checkout: @@ -57,7 +58,7 @@ Expected local untracked artifacts to preserve in the root checkout: Do not delete or commit these unless the user explicitly asks. -## What is complete through v0.1.31 +## What is complete through v0.1.32 ### Distribution and release automation @@ -66,12 +67,12 @@ Do not delete or commit these unless the user explicitly asks. - Publish workflow gates GitHub Release creation on `published-install-smoke` after npm/PyPI publish. - Published smoke uploads `published-install-smoke-result` JSON artifact with success/failure diagnostics. - v0.1.28+ smoke covers npm/npx/npm-exec/uvx/pipx and Hermes hook stdin payload handling. -- Known repeated pain point before this slice: protected `main` blocked auto-release metadata write-back, requiring manual release-sync PR + manual tag push. +- Protected `main` fallback is automated: auto-release creates `release-sync/vX.Y.Z` PR when direct metadata write-back is rejected; after merge, auto-release tags and dispatches publish. ### Runtime adapter readiness - Hermes bootstrap/doctor/install flow exists and defaults to the conservative preset. -- This local Hermes setup has agent-memory enabled via `/Users/reddit/.agent-memory/runtime/v0.1.31/.venv/bin/agent-memory` against `/Users/reddit/.agent-memory/memory.db`. +- This local Hermes setup has agent-memory enabled via `/Users/reddit/.agent-memory/runtime/v0.1.32/.venv/bin/agent-memory` against `/Users/reddit/.agent-memory/memory.db`. - Hermes hook fails closed: unavailable DB/schema returns `{}` and exit 0 instead of breaking prompt flow. - Conservative preset remains default: small prompt budgets, one top memory, no alternative-memory detail, no reason-code noise. - `--preset balanced` is explicit opt-in for more context/noise. @@ -89,25 +90,28 @@ Do not delete or commit these unless the user explicitly asks. - `agent-memory review explain fact ...` explains status, default retrieval visibility, same claim-slot alternatives, replacement chain, and review follow-up commands. - Retrieval eval calls the real retrieval path but suppresses retrieval bookkeeping writes (`retrieval_count`, `reinforcement_count`, `last_accessed_at`). -## Current slice: protected-main release fallback automation +## Current slice: release fallback rerun idempotency + +Why this slice exists: + +- During the first v0.1.32 live fallback run, GitHub Actions created `release-sync/v0.1.32` but failed to create the PR because the repository Actions setting initially disallowed GitHub Actions from creating PRs. +- After enabling that setting, rerunning the failed job hit a non-fast-forward branch push because the release-sync branch already existed. +- The fallback should be safe to rerun after this kind of partial success. Planned behavior: -- Main merge auto-release still tries the direct metadata write-back first. -- If `git push origin HEAD:main` is rejected by GitHub rules/protected `main`, auto-release should not fail the whole release path immediately. -- It should create a `release-sync/vX.Y.Z` branch from the already-bumped commit and open a PR titled `chore: release vX.Y.Z [skip release]`. -- The direct publish dispatch should run only when direct main push/tag push succeeds. -- After the release-sync PR is merged, a separate auto-release job should recognize the `[skip release]` release-sync commit, create/push the missing annotated tag, and dispatch `publish.yml`. -- If the tag already exists, the release-sync follow-up job should no-op rather than republishing. +- When protected-main fallback starts, check whether `release-sync/vX.Y.Z` already exists on origin. +- If the branch exists, reuse it instead of pushing and failing with non-fast-forward. +- Check whether an open PR already exists for the release-sync branch. +- If the PR exists, log the URL and exit successfully instead of opening a duplicate PR. +- If neither exists, keep the existing branch push + `gh pr create` behavior. Implementation direction: -- Update `.github/workflows/auto-release.yml` permissions to include `pull-requests: write`. -- Add `id: push_release` and a protected-main rejection branch around the direct push step. -- Add a `gh pr create` fallback step guarded by `steps.push_release.outputs.release_sync_required == 'true'`. -- Add a `tag-and-publish-release-sync` job for merged `chore: release v... [skip release]` commits. -- Keep `[skip release]` as the anti-recursion marker. -- Keep publish creation inside `publish.yml`; auto-release should only dispatch it. +- Update `.github/workflows/auto-release.yml` fallback step with `git ls-remote --heads` branch detection. +- Add `gh pr list --head ... --state open --json url --jq '.[0].url // empty'` before `gh pr create`. +- Keep the direct path and release-sync tag/publish follow-up unchanged. +- Add/keep tests in `tests/test_release_workflows.py` proving idempotency markers exist in the workflow. ## Verification checklist for this slice @@ -128,16 +132,17 @@ Before PR, run a static diff secret scan and confirm finding_count 0. ## PR/release notes -This slice changes only release automation/docs/tests, but it affects the release path and should be treated as a patch release candidate, likely v0.1.32 after PR merge. +This slice changes only release automation/docs/tests, but it affects the release path and should be treated as a patch release candidate, likely v0.1.33 after PR merge. Expected live verification after merge: -1. The auto-release run for the PR merge should bump metadata to v0.1.32. -2. If protected `main` still rejects direct write-back, the run should open `release-sync/v0.1.32` PR automatically. -3. Merge that PR. -4. Confirm the release-sync follow-up job creates tag `v0.1.32`, dispatches publish, and published smoke passes. -5. Verify GitHub Release/npm/PyPI/published-install-smoke artifact. -6. Update local Hermes runtime to v0.1.32 only after package release is verified. +1. PR merge should trigger auto-release and bump metadata to v0.1.33. +2. Protected `main` should trigger fallback. +3. Fallback should create `release-sync/v0.1.33` PR or reuse it if a partial rerun already created it. +4. Merge the release-sync PR. +5. Confirm release-sync follow-up creates tag `v0.1.33`, dispatches publish, and published smoke passes. +6. Verify GitHub Release/npm/PyPI/published-install-smoke artifact. +7. Update local Hermes runtime to v0.1.33 only after package release is verified. ## Next likely slices after this diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 547cd94..55419ec 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -101,7 +101,23 @@ jobs: run: | set -euo pipefail RELEASE_SYNC_BRANCH="release-sync/${{ steps.bump.outputs.tag }}" - git push origin "HEAD:${RELEASE_SYNC_BRANCH}" + if git ls-remote --exit-code --heads origin "${RELEASE_SYNC_BRANCH}" >/dev/null 2>&1; then + echo "Release sync branch ${RELEASE_SYNC_BRANCH} already exists; reusing it." + else + git push origin "HEAD:${RELEASE_SYNC_BRANCH}" + fi + + existing_pr_url=$(gh pr list \ + --repo "${{ github.repository }}" \ + --head "${RELEASE_SYNC_BRANCH}" \ + --state open \ + --json url \ + --jq '.[0].url // empty') + if [ -n "$existing_pr_url" ]; then + echo "Release sync PR already exists: ${existing_pr_url}" + exit 0 + fi + cat > /tmp/release-sync-pr.md <<'EOF' ## Summary - sync release metadata after protected main rejected the auto-release write-back diff --git a/README.md b/README.md index 6a81dfb..cf7610f 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ npm pack --dry-run After a release publishes, the `published-install-smoke` workflow verifies the exact npm/PyPI version through npm registry lookup, `npx`, `npm exec`, `uvx`, and `pipx`. Maintainers can also run it manually with `gh workflow run published-install-smoke.yml -f version=`. -Release automation expects protected `main`: if the auto-release workflow cannot push its bumped metadata commit directly, it opens a `release-sync/vX.Y.Z` PR instead. After that PR is merged, the same workflow tags the synced version and dispatches `publish.yml`, keeping the release path automated without requiring a permanent branch-protection bypass. +Release automation expects protected `main`: if the auto-release workflow cannot push its bumped metadata commit directly, it opens a `release-sync/vX.Y.Z` PR instead. After that PR is merged, the same workflow tags the synced version and dispatches `publish.yml`, keeping the release path automated without requiring a permanent branch-protection bypass. The fallback is safe to rerun: if the `release-sync/vX.Y.Z` branch or PR already exists, the workflow reuses it instead of failing on a non-fast-forward push or opening a duplicate PR. Useful source-checkout commands: diff --git a/tests/test_release_workflows.py b/tests/test_release_workflows.py index 32838b7..09fe526 100644 --- a/tests/test_release_workflows.py +++ b/tests/test_release_workflows.py @@ -34,6 +34,18 @@ def test_auto_release_workflow_falls_back_to_release_sync_pr_when_main_is_protec assert "Publish workflow will run after the release sync PR is merged and the tag is pushed." in workflow +def test_auto_release_fallback_is_idempotent_when_release_sync_branch_or_pr_exists() -> None: + workflow = (PROJECT_ROOT / ".github" / "workflows" / "auto-release.yml").read_text() + + assert "git ls-remote --exit-code --heads origin \"${RELEASE_SYNC_BRANCH}\"" in workflow + assert "Release sync branch ${RELEASE_SYNC_BRANCH} already exists" in workflow + assert "git push origin \"HEAD:${RELEASE_SYNC_BRANCH}\"" in workflow + assert "gh pr list" in workflow + assert "existing_pr_url" in workflow + assert "Release sync PR already exists" in workflow + assert "gh pr create" in workflow + + def test_publish_workflow_remains_tag_driven_only() -> None: workflow = (PROJECT_ROOT / ".github" / "workflows" / "publish.yml").read_text()