Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ jobs:
run: exit 1

publish:
if: ${{ (needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.xml_related == 'true') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-changes.outputs.publish_requested == 'true' && needs.integration-tests.result == 'success' }}
if: ${{ (needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.xml_related == 'true') && github.ref == 'refs/heads/main' && needs.integration-tests.result == 'success' && ((github.event_name == 'push' && needs.detect-changes.outputs.publish_requested == 'true') || github.event_name == 'workflow_dispatch') }}
needs:
- detect-changes
- validate-template
Expand Down Expand Up @@ -448,7 +448,6 @@ jobs:
- name: Compute image tags
id: prep
env:
RELEASE_TAG_OVERRIDE: ""
DOCKERHUB_ENABLED: ${{ steps.dockerhub.outputs.enabled }}
DOCKERHUB_IMAGE_NAME: ${{ steps.dockerhub.outputs.image_name }}
run: |
Expand All @@ -463,19 +462,21 @@ jobs:
version_label="${upstream_version}"
wrapper_track="main"
wrapper_version=""
if [[ -n "${RELEASE_TAG_OVERRIDE}" ]]; then
release_package_tag="${RELEASE_TAG_OVERRIDE}"
else
changelog_version="$(python3 scripts/release.py latest-changelog-version 2>/dev/null || true)"
release_commit_pattern="^chore\\(release\\): $(python3 -c 'import re, sys; print(re.escape(sys.argv[1]))' "${changelog_version}")( \\(#[0-9]+\\))?$"
case "${changelog_version}" in
"${upstream_version}"-aio.*)

changelog_version="$(python3 scripts/release.py latest-changelog-version 2>/dev/null || true)"
case "${changelog_version}" in
"${upstream_version}"-aio.*)
release_target="$(python3 scripts/release.py find-release-target-commit "${changelog_version}" 2>/dev/null || true)"
if [[ "${release_target}" == "${GITHUB_SHA}" || "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep manual publish runs pinned to the release target SHA

This condition makes every workflow_dispatch run on main assign release_package_tag even when GITHUB_SHA is not the resolved release target commit. If someone manually reruns CI after newer commits land, the existing *-aio.N tag can be republished from non-release code, breaking release tag immutability and reproducibility for users pulling that version tag.

Useful? React with 👍 / 👎.

release_package_tag="${changelog_version}"
else
release_commit_pattern="^chore\\(release\\): $(python3 -c 'import re, sys; print(re.escape(sys.argv[1]))' "${changelog_version}")( \\(#[0-9]+\\))?$"
if printf '%s\n' "${commit_message}" | grep -Eq "${release_commit_pattern}"; then
release_package_tag="${changelog_version}"
fi
;;
esac
fi
fi
;;
esac
if [[ -n "${release_package_tag}" ]]; then
version_label="${release_package_tag}"
wrapper_track="${release_package_tag}"
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ jobs:
echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}"
release_commit="$(python3 scripts/release.py find-release-commit "${release_version}")"
echo "release_commit=${release_commit}" >> "${GITHUB_OUTPUT}"
echo "Matched release commit ${release_commit} for ${release_version}"
release_target="$(python3 scripts/release.py find-release-target-commit "${release_version}")"
echo "release_target=${release_target}" >> "${GITHUB_OUTPUT}"
echo "Matched release commit ${release_commit} and target ${release_target} for ${release_version}"
changelog_version="$(python3 scripts/release.py latest-changelog-version)"
if [[ "${changelog_version}" != "${release_version}" ]]; then
echo "CHANGELOG top entry ${changelog_version} does not match ${release_version}" >&2
Expand All @@ -37,7 +39,7 @@ jobs:
- name: Require successful CI for release commit
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
WORKFLOW_SELECTOR: build.yml
run: |
runs_json="$(gh run list \
Expand Down Expand Up @@ -89,7 +91,7 @@ jobs:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
if git rev-parse "${RELEASE_VERSION}" >/dev/null 2>&1; then
Expand All @@ -115,7 +117,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.release_version }}
RELEASE_NOTES: ${{ steps.notes.outputs.release_notes }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }}
RELEASE_COMMIT: ${{ steps.version.outputs.release_target }}
run: |
export GITHUB_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN}}"

Expand Down
6 changes: 4 additions & 2 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ When Docker Hub credentials are configured, the same tag set is pushed to Docker
1. Trigger **Prepare Release / Mem0-AIO** from `main`.
2. The workflow computes the next `upstream-aio.N` version, updates `CHANGELOG.md`, syncs the XML `<Changes>` block, and opens a release PR.
3. Review and merge that PR into `main`.
4. Wait for the `CI / Mem0-AIO` run on the release commit to finish green. That same `main` push also publishes the updated package tags automatically.
4. Wait for the `CI / Mem0-AIO` run on the release target commit to finish green. That same `main` push also publishes the updated package tags automatically.
5. Trigger **Publish Release / Mem0-AIO** from `main`.
6. The workflow verifies CI on the exact release commit, creates the Git tag if needed, and publishes the GitHub Release.
6. The workflow verifies CI on the exact release target commit, creates the Git tag if needed, and publishes the GitHub Release.

If the latest release image tag must be repaired after the release merge, manually run **CI / Mem0-AIO** from `main`. The workflow derives the release tag from the top `CHANGELOG.md` entry and still runs the normal integration gate before publishing it.
36 changes: 36 additions & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,38 @@ def find_release_commit(version: str) -> str:
)


def git_is_ancestor(ancestor: str, descendant: str) -> bool:
return (
git_completed("merge-base", "--is-ancestor", ancestor, descendant).returncode
== 0
)


def find_release_target_commit(version: str) -> str:
release_commit = find_release_commit(version)
head = git_output("rev-parse", "HEAD").strip()

if release_commit == head:
return release_commit

if not git_is_ancestor(release_commit, head):
raise SystemExit(
f"Release commit {release_commit} for {version} is not reachable from HEAD."
)

first_parent_commits = git_output("rev-list", "--first-parent", "HEAD").splitlines()
if release_commit in first_parent_commits:
return release_commit

first_parent_path = git_output(
"rev-list", "--first-parent", "--reverse", f"{release_commit}..HEAD"
).splitlines()
if first_parent_path:
return first_parent_path[0]
Comment on lines +182 to +185
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Select merge commit as release target

When main has additional commits after the release branch was cut, rev-list --first-parent --reverse "${release_commit}..HEAD" yields all intervening first-parent commits and this code picks the first one, which is typically an older main commit rather than the merge commit that actually brought the release in. In that case Publish Release will validate CI and create the release tag/GitHub release against the wrong SHA (which may not contain the release changes).

Useful? React with 👍 / 👎.


return release_commit


def main() -> None:
parser = argparse.ArgumentParser(description="Release helpers for AIO repos.")
subparsers = parser.add_subparsers(dest="command", required=True)
Expand Down Expand Up @@ -191,6 +223,8 @@ def main() -> None:
)
commit_parser = subparsers.add_parser("find-release-commit")
commit_parser.add_argument("version")
target_parser = subparsers.add_parser("find-release-target-commit")
target_parser.add_argument("version")
args = parser.parse_args()
if args.command == "upstream-version":
print(read_upstream_version(args.dockerfile, args.upstream_config))
Expand All @@ -210,6 +244,8 @@ def main() -> None:
print(latest_changelog_version(args.changelog))
elif args.command == "find-release-commit":
print(find_release_commit(args.version))
elif args.command == "find-release-target-commit":
print(find_release_target_commit(args.version))
else:
print(extract_release_notes(args.version, args.changelog))

Expand Down
63 changes: 63 additions & 0 deletions tests/unit/test_release_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from subprocess import ( # nosec B404 - tests construct return objects only
CompletedProcess,
)

import pytest

from scripts import release


def test_find_release_target_commit_returns_squash_release_commit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_git_output(*args: str) -> str:
if args == ("log", "--format=%H\t%s", "HEAD"):
return "release-sha\tchore(release): v2.0.0-aio.3\n"
if args == ("rev-parse", "HEAD"):
return "release-sha\n"
raise AssertionError(f"unexpected git_output args: {args}")

monkeypatch.setattr(release, "git_output", fake_git_output)

assert (
release.find_release_target_commit("v2.0.0-aio.3") == "release-sha"
) # nosec B101


def test_find_release_target_commit_returns_merge_commit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_git_output(*args: str) -> str:
if args == ("log", "--format=%H\t%s", "HEAD"):
return "\n".join(
[
"merge-sha\tMerge pull request #43 from JSONbored/release/v2.0.0-aio.3",
"release-sha\tchore(release): v2.0.0-aio.3",
]
)
if args == ("rev-parse", "HEAD"):
return "merge-sha\n"
if args == ("rev-list", "--first-parent", "HEAD"):
return "merge-sha\nprevious-main-sha\n"
if args == (
"rev-list",
"--first-parent",
"--reverse",
"release-sha..HEAD",
):
return "merge-sha\n"
raise AssertionError(f"unexpected git_output args: {args}")

def fake_git_completed(*args: str) -> CompletedProcess[str]:
if args == ("merge-base", "--is-ancestor", "release-sha", "merge-sha"):
return CompletedProcess(args=args, returncode=0)
raise AssertionError(f"unexpected git_completed args: {args}")

monkeypatch.setattr(release, "git_output", fake_git_output)
monkeypatch.setattr(release, "git_completed", fake_git_completed)

assert (
release.find_release_target_commit("v2.0.0-aio.3") == "merge-sha"
) # nosec B101
Loading