From c60c07c0b56942787e95ba89c1e38d581294e52a Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:01:56 -0600 Subject: [PATCH] fix(ci): make releases manual and gate heavy workflows --- .github/workflows/build.yml | 20 +++++---- .github/workflows/release.yml | 76 ++++++++++++++++++++++++++--------- docs/releases.md | 13 ++++-- scripts/release.py | 27 +++++++++++++ 4 files changed, 106 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57e9598..84a706d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,6 +55,7 @@ jobs: build_related: ${{ steps.filter.outputs.build_related }} template_assets_related: ${{ steps.filter.outputs.template_assets_related }} renovate_related: ${{ steps.filter.outputs.renovate_related }} + tooling_related: ${{ steps.filter.outputs.tooling_related }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -75,6 +76,7 @@ jobs: echo "build_related=true" >> "${GITHUB_OUTPUT}" echo "template_assets_related=true" >> "${GITHUB_OUTPUT}" echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + echo "tooling_related=true" >> "${GITHUB_OUTPUT}" exit 0 fi @@ -91,6 +93,7 @@ jobs: echo "build_related=true" >> "${GITHUB_OUTPUT}" echo "template_assets_related=true" >> "${GITHUB_OUTPUT}" echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + echo "tooling_related=true" >> "${GITHUB_OUTPUT}" exit 0 fi @@ -100,6 +103,7 @@ jobs: echo "build_related=true" >> "${GITHUB_OUTPUT}" echo "template_assets_related=true" >> "${GITHUB_OUTPUT}" echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + echo "tooling_related=true" >> "${GITHUB_OUTPUT}" exit 0 fi else @@ -108,6 +112,7 @@ jobs: echo "build_related=true" >> "${GITHUB_OUTPUT}" echo "template_assets_related=true" >> "${GITHUB_OUTPUT}" echo "renovate_related=true" >> "${GITHUB_OUTPUT}" + echo "tooling_related=true" >> "${GITHUB_OUTPUT}" exit 0 fi fi @@ -117,21 +122,21 @@ jobs: build_related=false template_assets_related=false renovate_related=false + tooling_related=false while IFS= read -r path; do [[ -z "${path}" ]] && continue case "${path}" in - Dockerfile|upstream.toml|rootfs/*|scripts/*|openmemory/*) + Dockerfile|upstream.toml|rootfs/*|openmemory/*) build_related=true ;; *.xml|assets/*) template_assets_related=true ;; - renovate.json) - renovate_related=true + scripts/*|.github/workflows/*) + tooling_related=true ;; - .github/workflows/*) - build_related=true + renovate.json) renovate_related=true ;; esac @@ -140,10 +145,11 @@ jobs: echo "build_related=${build_related}" >> "${GITHUB_OUTPUT}" echo "template_assets_related=${template_assets_related}" >> "${GITHUB_OUTPUT}" echo "renovate_related=${renovate_related}" >> "${GITHUB_OUTPUT}" + echo "tooling_related=${tooling_related}" >> "${GITHUB_OUTPUT}" validate-repo: needs: detect-changes - if: ${{ needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.template_assets_related == 'true' || needs.detect-changes.outputs.renovate_related == 'true' }} + if: ${{ needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.template_assets_related == 'true' || needs.detect-changes.outputs.renovate_related == 'true' || needs.detect-changes.outputs.tooling_related == 'true' }} runs-on: ubuntu-latest permissions: contents: read @@ -154,7 +160,7 @@ jobs: submodules: recursive - name: Validate shell and python scripts - if: ${{ needs.detect-changes.outputs.build_related == 'true' }} + if: ${{ needs.detect-changes.outputs.build_related == 'true' || needs.detect-changes.outputs.tooling_related == 'true' }} run: | bash -n scripts/smoke-test.sh find rootfs -type f -name '*.sh' -print0 | xargs -0 -n1 bash -n diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 83dff98..fcf301d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,20 @@ -name: Release / mem0-aio +name: Release / Mem0-AIO on: workflow_dispatch: - pull_request_target: - types: [closed] + inputs: + action: + description: "Release action to run" + required: true + default: prepare + type: choice + options: + - prepare + - publish jobs: prepare-release: - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.event.inputs.action == 'prepare' && github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest permissions: contents: write @@ -17,6 +24,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + - name: Install git-cliff env: GIT_CLIFF_VERSION: 2.12.0 @@ -25,44 +33,66 @@ jobs: curl -fsSL -o "/tmp/${archive}" "https://github.com/orhun/git-cliff/releases/download/v${GIT_CLIFF_VERSION}/${archive}" tar -xzf "/tmp/${archive}" -C /tmp install "/tmp/git-cliff-${GIT_CLIFF_VERSION}/git-cliff" /usr/local/bin/git-cliff + + - name: Check for unreleased changes + id: changes + run: | + echo "has_changes=$(python3 scripts/release.py has-unreleased-changes)" >> "${GITHUB_OUTPUT}" + - name: Compute release version id: version - run: echo "release_version=$(python3 scripts/release.py next-version)" >> "${GITHUB_OUTPUT}" + if: steps.changes.outputs.has_changes == 'true' + run: | + release_version="$(python3 scripts/release.py next-version)" + echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}" + - name: Generate changelog + if: steps.changes.outputs.has_changes == 'true' env: GITHUB_REPO: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ steps.version.outputs.release_version }} - run: git-cliff --config cliff.toml --tag "${RELEASE_VERSION}" --output CHANGELOG.md + run: | + git-cliff --config cliff.toml --tag "${RELEASE_VERSION}" --output CHANGELOG.md + - name: Create release PR + if: steps.changes.outputs.has_changes == 'true' uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: commit-message: "chore(release): ${{ steps.version.outputs.release_version }}" title: "chore(release): ${{ steps.version.outputs.release_version }}" body: | This PR prepares `${{ steps.version.outputs.release_version }}`. + + - updates `CHANGELOG.md` with `git-cliff` + - is intended to be merged to `main` + - requires a separate manual `publish` run after merge branch: "release/${{ steps.version.outputs.release_version }}" delete-branch: true - publish-release-on-merge: - if: "${{ github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' && startsWith(github.event.pull_request.title, 'chore(release): ') }}" + publish-release: + if: ${{ github.event.inputs.action == 'publish' && github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest permissions: contents: write steps: - - name: Checkout merge commit + - name: Checkout main uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 0 + - name: Determine release version id: version - env: - PR_TITLE: ${{ github.event.pull_request.title }} run: | - release_version="${PR_TITLE#chore(release): }" + release_version="$(python3 scripts/release.py latest-changelog-version)" echo "release_version=${release_version}" >> "${GITHUB_OUTPUT}" - test "$(python3 scripts/release.py latest-changelog-version)" = "${release_version}" + release_commit="$(git log --format='%H%x09%s' HEAD | awk -F '\t' -v title="chore(release): ${release_version}" '$2 == title {print $1; exit}')" + if [[ -z "${release_commit}" ]]; then + echo "Unable to find a merged release commit for ${release_version} on main." >&2 + exit 1 + fi + echo "release_commit=${release_commit}" >> "${GITHUB_OUTPUT}" + - name: Extract release notes id: notes env: @@ -73,24 +103,30 @@ jobs: python3 scripts/release.py extract-release-notes "${RELEASE_VERSION}" echo "EOF" } >> "${GITHUB_OUTPUT}" + - name: Create Git tag if missing env: RELEASE_VERSION: ${{ steps.version.outputs.release_version }} - MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }} run: | - if git rev-parse "${RELEASE_VERSION}" >/dev/null 2>&1; then exit 0; fi + if git rev-parse "${RELEASE_VERSION}" >/dev/null 2>&1; then + exit 0 + fi git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git tag "${RELEASE_VERSION}" "${MERGE_SHA}" + git tag "${RELEASE_VERSION}" "${RELEASE_COMMIT}" git push origin "${RELEASE_VERSION}" + - name: Publish GitHub release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_VERSION: ${{ steps.version.outputs.release_version }} RELEASE_NOTES: ${{ steps.notes.outputs.release_notes }} - MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + RELEASE_COMMIT: ${{ steps.version.outputs.release_commit }} run: | - if gh release view "${RELEASE_VERSION}" >/dev/null 2>&1; then exit 0; fi + if gh release view "${RELEASE_VERSION}" >/dev/null 2>&1; then + exit 0 + fi notes_file="$(mktemp)" printf '%s\n' "${RELEASE_NOTES}" > "${notes_file}" - gh release create "${RELEASE_VERSION}" --title "${RELEASE_VERSION}" --notes-file "${notes_file}" --target "${MERGE_SHA}" + gh release create "${RELEASE_VERSION}" --title "${RELEASE_VERSION}" --notes-file "${notes_file}" --target "${RELEASE_COMMIT}" diff --git a/docs/releases.md b/docs/releases.md index ac9a000..578b7bd 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -2,6 +2,12 @@ `mem0-aio` uses upstream-version-plus-AIO-revision releases such as `v1.0.9-aio.1`. +## Version format + +- first wrapper release for upstream `v1.0.9`: `v1.0.9-aio.1` +- second wrapper-only release on the same upstream: `v1.0.9-aio.2` +- first wrapper release after upgrading upstream: `v1.0.10-aio.1` + ## Published image tags Every `main` build publishes: @@ -13,7 +19,8 @@ Every `main` build publishes: ## Release flow -1. Trigger **Release / mem0-aio** from `main`. +1. Trigger **Release / Mem0-AIO** from `main` with `action=prepare`. 2. The workflow computes the next `upstream-aio.N` version and opens a release PR. -3. Merge that PR into `main`. -4. After merge, the workflow creates the Git tag and GitHub Release automatically. +3. Review and merge that PR into `main`. +4. Trigger **Release / Mem0-AIO** from `main` again with `action=publish`. +5. The workflow reads the merged `CHANGELOG.md` entry, creates the Git tag, and publishes the GitHub Release. diff --git a/scripts/release.py b/scripts/release.py index 71c8eeb..c0ba3a4 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -46,6 +46,28 @@ def git_tags() -> list[str]: return [line.strip() for line in output.splitlines() if line.strip()] +def latest_release_tag(dockerfile: pathlib.Path, upstream: pathlib.Path) -> str | None: + upstream_version = read_upstream_version(dockerfile, upstream) + pattern = re.compile(rf"^{re.escape(upstream_version)}-aio\.(\d+)$") + matches: list[tuple[int, str]] = [] + for tag in git_tags(): + match = pattern.match(tag) + if match: + matches.append((int(match.group(1)), tag)) + if not matches: + return None + matches.sort(key=lambda item: item[0]) + return matches[-1][1] + + +def has_unreleased_changes(dockerfile: pathlib.Path, upstream: pathlib.Path) -> bool: + latest_tag = latest_release_tag(dockerfile, upstream) + if latest_tag is None: + return True + output = subprocess.check_output(["git", "log", "--format=%s", f"{latest_tag}..HEAD"], cwd=ROOT, text=True) + return any(line.strip() for line in output.splitlines()) + + def next_release_version(dockerfile: pathlib.Path, upstream: pathlib.Path) -> str: upstream_version = read_upstream_version(dockerfile, upstream) pattern = re.compile(rf"^{re.escape(upstream_version)}-aio\.(\d+)$") @@ -98,6 +120,9 @@ def main() -> None: next_parser = subparsers.add_parser("next-version") next_parser.add_argument("--dockerfile", type=pathlib.Path, default=DEFAULT_DOCKERFILE) next_parser.add_argument("--upstream-config", type=pathlib.Path, default=DEFAULT_UPSTREAM) + changes_parser = subparsers.add_parser("has-unreleased-changes") + changes_parser.add_argument("--dockerfile", type=pathlib.Path, default=DEFAULT_DOCKERFILE) + changes_parser.add_argument("--upstream-config", type=pathlib.Path, default=DEFAULT_UPSTREAM) latest_parser = subparsers.add_parser("latest-changelog-version") latest_parser.add_argument("--changelog", type=pathlib.Path, default=DEFAULT_CHANGELOG) notes_parser = subparsers.add_parser("extract-release-notes") @@ -108,6 +133,8 @@ def main() -> None: print(read_upstream_version(args.dockerfile, args.upstream_config)) elif args.command == "next-version": print(next_release_version(args.dockerfile, args.upstream_config)) + elif args.command == "has-unreleased-changes": + print("true" if has_unreleased_changes(args.dockerfile, args.upstream_config) else "false") elif args.command == "latest-changelog-version": print(latest_changelog_version(args.changelog)) else: