From 7f6d1075f371faa9d8e4911fe983bcf786a0d5c2 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Wed, 28 Jan 2026 17:07:30 +0530 Subject: [PATCH 1/4] Decouple sync and release actions --- .github/workflows/ci.yml | 114 ++++++----------------------- .github/workflows/ci_release.yml | 120 +++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/ci_release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 730491a89fd7..8b8a1ff924da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,33 +1,24 @@ -# LocalStack specific workflow to implement a fully-integrated continuous integration pipeline for our fork +# LocalStack specific workflow to implement a fully-integrated continuous integration pipeline for Moto-Ext # - Rebase this fork based on the latest commit on `main` of upstream -# - Build a Python source and wheel distribution of moto-ext with deterministic versioning -# - Publish the distributions to PyPi -# - Tag the commit in this fork with the new version -# - Create a GitHub release for the new version -name: Sync / Release moto-ext +name: Sync moto-ext with upstream on: - schedule: - - cron: 0 5 * * MON workflow_dispatch: inputs: dry_run: - description: 'Dry Run?' + description: 'Dry run' default: true required: true type: boolean -# limit concurrency to 1 +# Limit concurrency to 1 concurrency: group: ${{ github.workflow }} jobs: - sync-build-release-moto-ext: + sync-moto-ext: runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/project/moto-ext/ permissions: contents: write id-token: write @@ -50,35 +41,35 @@ jobs: git config --global user.name 'LocalStack Bot' git config --global user.email 'localstack-bot@users.noreply.github.com' git remote set-url origin https://git:${{ secrets.PRO_ACCESS_TOKEN }}@github.com/${{ github.repository }} - + # make sure to switch to the `localstack` branch (default / main branch of this fork) git switch localstack # add moto upstream as remote git remote add upstream https://github.com/getmoto/moto.git # rebase with latest changes git pull - + # Create a custom merge driver which prefers everything from upstream _BUT_ the name and the URL mkdir -p $HOME/.local/bin cat > $HOME/.local/bin/git-prefer-theirs-name-url << EOF #!/bin/bash set -e - + base="\$1" local="\$2" remote="\$3" - + echo "Executing custom merge driver for base \$base, local \$local, remote \$remote." - + # Define keys to keep KEYS=("name" "url") - + # Read files into arrays mapfile -t REMOTE_LINES < "\$remote" mapfile -t LOCAL_LINES < "\$local" - + echo "merging \$local + \$local + \$remote ..." - + # Function to check if a line should be kept (matches any key) keep_line() { local line="\$1" @@ -87,7 +78,7 @@ jobs: done return 1 } - + # keep key-matched lines from local, others from remote for i in "\${!LOCAL_LINES[@]}"; do if keep_line "\${REMOTE_LINES[i]}"; then @@ -96,22 +87,22 @@ jobs: echo "\${LOCAL_LINES[i]}" fi done > "\$local" - + exit 0 EOF - + # make the script executable and add it to the PATH chmod +x $HOME/.local/bin/git-prefer-theirs-name-url echo "$HOME/.local/bin" >> "$GITHUB_PATH" - + # add the merge driver to the git config cat >> .git/config << EOF - + [merge "git-prefer-theirs-name-url"] name = A driver which resolves merge conflicts on a setup.cfg such that it always takes the local name and url, and everything else from upstream driver = git-prefer-theirs-name-url %O %A %B EOF - + # define to use the custom merge driver for the setup.cfg cat > .gitattributes << EOF setup.cfg merge=git-prefer-theirs-name-url @@ -121,75 +112,12 @@ jobs: run: | git fetch upstream git rebase -f upstream/master - - - name: Determine new version - run: | - echo "Determining new version..." - cat > setuptools.cfg << EOF - [tool.setuptools_scm] - local_scheme = "no-local-version" - version_scheme = "post-release" - EOF - python3 -m venv .venv - source .venv/bin/activate - python3 -m pip install setuptools_scm - NEW_VERSION=$(python3 -m setuptools_scm -c setuptools.cfg) - NEW_VERSION="${NEW_VERSION//dev/post}" - echo "New version is: $NEW_VERSION" - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV - - - name: Build Python distributions - # FYI: Checks in this script only work because the -e flag is enabled by default in GitHub actions - run: | - python3 -m pip install build - - echo "Setting new version in setup.cfg": - # make sure setup.cfg is not dirty yet - git diff --exit-code setup.cfg - sed -i -E 's/^(version\s*=\s*)("?)[^"]+("?)/\1\2'"$NEW_VERSION"'\3/' setup.cfg - # make sure setup.cfg is dirty now - ! git diff --exit-code setup.cfg - - echo "Building new version and tagging commit..." - python3 -m build - - - name: Tag successful build - run: | - git tag -a $NEW_VERSION -m $NEW_VERSION - - - name: Clean up - run: | - git reset --hard - git clean -df - - - name: Store built distributions - uses: actions/upload-artifact@v4 - with: - name: moto-ext-dists - path: dist/*.* - - # publish the package before pushing the tag (this might fail if the version already exists on PyPI) - - name: Publish package distributions to PyPI - if: ${{ github.event.inputs.dry_run != 'true' }} - uses: pypa/gh-action-pypi-publish@release/v1 + git log --oneline --graph --abbrev-commit --max-count 20 - name: Push if: ${{ github.event.inputs.dry_run != 'true' }} run: | git push --force-with-lease - git push --atomic origin localstack $NEW_VERSION + git push --atomic origin localstack env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Add a retry to avoid issues where the GH CLI fails - # because it does not yet detect the pushed tag. - - name: Create Release - uses: nick-fields/retry@v3 - if: ${{ github.event.inputs.dry_run != 'true' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - max_attempts: 5 - retry_wait_seconds: 120 - timeout_minutes: 5 - command: gh release create $NEW_VERSION --repo localstack/moto --notes "automatic rebase sync and release" \ No newline at end of file diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml new file mode 100644 index 000000000000..dfec235366bb --- /dev/null +++ b/.github/workflows/ci_release.yml @@ -0,0 +1,120 @@ +# LocalStack specific workflow to implement a fully-integrated continuous integration pipeline for Moto-Ext +# - Guess next patch semantic version +# - Build source and wheel distributions +# - Publish the distributions to PyPi +# - Tag the commit with the new version +# - Create a GitHub release for the new version + +name: Release moto-ext + +on: + schedule: + - cron: 0 5 * * MON + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run' + default: true + required: true + type: boolean + +# Limit concurrency to 1 +concurrency: + group: ${{ github.workflow }} + +jobs: + build-release-moto-ext: + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/moto-ext/ + permissions: + contents: write + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: localstack + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Guess new version + run: | + echo "Guessing new version..." + # Only bump PATCH semver because Moto-Ext is frozen against all MAJOR and MINOR changes + cat << EOF | tee setuptools.cfg + [tool.setuptools_scm] + local_scheme = "no-local-version" + version_scheme = "python-simplified-semver" + EOF + python3 -m venv .venv + source .venv/bin/activate + python3 -m pip install setuptools_scm + NEW_VERSION=$(python3 -m setuptools_scm -c setuptools.cfg) + echo $NEW_VERSION + NEW_VERSION="${NEW_VERSION//.dev[0-9]/}" + echo "New version is: $NEW_VERSION" + echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + + - name: Ensure guessed version does not exist + run: | + ! git rev-parse $NEW_VERSION || { echo "A tag for $NEW_VERSION already exists" ; exit 1; } + + - name: Build Python distributions + # FYI: Checks in this script only work because the -e flag is enabled by default in GitHub actions + run: | + python3 -m pip install build + + # make sure setup.cfg is not dirty yet + git diff --exit-code setup.cfg + + echo "Setting new version in setup.cfg": + sed -i -E 's/^(version\s*=\s*)("?)[^"]+("?)/\1\2'"$NEW_VERSION"'\3/' setup.cfg + + # make sure setup.cfg is dirty now + ! git diff --exit-code setup.cfg + + echo "Building distributions" + python3 -m build + + - name: Create and push tag + if: ${{ github.event.inputs.dry_run != 'true' }} + run: | + git tag -a $NEW_VERSION -m $NEW_VERSION + git push --tags origin localstack + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Clean up + run: | + git reset --hard + git clean -df + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: moto-ext-dists + path: dist/*.* + + # publish the package before pushing the tag (this might fail if the version already exists on PyPI) + - name: Publish package distributions to PyPI + if: ${{ github.event.inputs.dry_run != 'true' }} + uses: pypa/gh-action-pypi-publish@release/v1 + + # Add a retry to avoid issues where the GH CLI fails because it does not yet detect the pushed tag. + - name: Create Release + uses: nick-fields/retry@v3 + if: ${{ github.event.inputs.dry_run != 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + max_attempts: 5 + retry_wait_seconds: 120 + timeout_minutes: 5 + command: gh release create $NEW_VERSION --repo localstack/moto --notes "automatic release" From a6324fe75787d437e5bd67c79d7ce3be21240bfa Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Thu, 29 Jan 2026 16:26:23 +0530 Subject: [PATCH 2/4] Use action to guess new version --- .github/workflows/ci_release.yml | 37 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml index dfec235366bb..6a3645bdbd66 100644 --- a/.github/workflows/ci_release.yml +++ b/.github/workflows/ci_release.yml @@ -44,29 +44,27 @@ jobs: with: python-version: '3.13' - - name: Guess new version - run: | - echo "Guessing new version..." - # Only bump PATCH semver because Moto-Ext is frozen against all MAJOR and MINOR changes - cat << EOF | tee setuptools.cfg - [tool.setuptools_scm] - local_scheme = "no-local-version" - version_scheme = "python-simplified-semver" - EOF - python3 -m venv .venv - source .venv/bin/activate - python3 -m pip install setuptools_scm - NEW_VERSION=$(python3 -m setuptools_scm -c setuptools.cfg) - echo $NEW_VERSION - NEW_VERSION="${NEW_VERSION//.dev[0-9]/}" - echo "New version is: $NEW_VERSION" - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV + - name: Get current latest version + id: previous + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: Guess next version + id: semver + uses: "WyriHaximus/github-action-next-semvers@v1" + with: + version: ${{ steps.previous.outputs.tag }} - name: Ensure guessed version does not exist + env: + NEW_VERSION: ${{ steps.semver.outputs.patch }} run: | ! git rev-parse $NEW_VERSION || { echo "A tag for $NEW_VERSION already exists" ; exit 1; } - name: Build Python distributions + env: + NEW_VERSION: ${{ steps.semver.outputs.patch }} # FYI: Checks in this script only work because the -e flag is enabled by default in GitHub actions run: | python3 -m pip install build @@ -84,12 +82,13 @@ jobs: python3 -m build - name: Create and push tag + env: + NEW_VERSION: ${{ steps.semver.outputs.patch }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: ${{ github.event.inputs.dry_run != 'true' }} run: | git tag -a $NEW_VERSION -m $NEW_VERSION git push --tags origin localstack - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Clean up run: | From a96d413d2e98f031eadf5e91a8847a9fc9363cd7 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Fri, 30 Jan 2026 16:27:04 +0530 Subject: [PATCH 3/4] Fail workflow if there are no commits since the previous tag --- .github/workflows/ci_release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml index 6a3645bdbd66..7b80f03ca420 100644 --- a/.github/workflows/ci_release.yml +++ b/.github/workflows/ci_release.yml @@ -62,6 +62,12 @@ jobs: run: | ! git rev-parse $NEW_VERSION || { echo "A tag for $NEW_VERSION already exists" ; exit 1; } + - name: Check for new commits since the last tag + env: + LAST_TAG: ${{ steps.previous.outputs.tag }} + run: | + [ $(git log --oneline $LAST_TAG..HEAD | wc -l) -eq "0" ] && { echo "No commits since the last tag. Nothing to release."; exit 1; } + - name: Build Python distributions env: NEW_VERSION: ${{ steps.semver.outputs.patch }} From 4e623d8c5ac728588b351effac292bb386ac7e68 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Mon, 2 Feb 2026 12:30:18 +0530 Subject: [PATCH 4/4] Rework condition into a prereq job --- .github/workflows/ci_release.yml | 47 +++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml index 7b80f03ca420..e9d91f288f33 100644 --- a/.github/workflows/ci_release.yml +++ b/.github/workflows/ci_release.yml @@ -23,8 +23,41 @@ concurrency: group: ${{ github.workflow }} jobs: + analyse-commit-log: + runs-on: ubuntu-latest + outputs: + is-release-required: ${{ steps.check.outputs.new_commits_made }} + last-tag: ${{ steps.last_tag.outputs.tag }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: localstack + persist-credentials: false + + - name: Get last tag + id: last_tag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: Check for new commits since the last tag + id: check + env: + LAST_TAG: ${{ steps.last_tag.outputs.tag }} + run: | + if [ $(git log --oneline $LAST_TAG..HEAD | wc -l) -eq "0" ]; then + echo "No commits since the last tag." + echo "new_commits_made=false" >> $GITHUB_OUTPUT + else + echo "new_commits_made=true" >> $GITHUB_OUTPUT + fi + build-release-moto-ext: runs-on: ubuntu-latest + needs: analyse-commit-log + if: ${{ needs.analyse-commit-log.outputs.is-release-required == 'true' }} environment: name: pypi url: https://pypi.org/project/moto-ext/ @@ -44,17 +77,11 @@ jobs: with: python-version: '3.13' - - name: Get current latest version - id: previous - uses: "WyriHaximus/github-action-get-previous-tag@v1" - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - - name: Guess next version id: semver uses: "WyriHaximus/github-action-next-semvers@v1" with: - version: ${{ steps.previous.outputs.tag }} + version: ${{ needs.analyse-commit-log.outputs.last-tag }} - name: Ensure guessed version does not exist env: @@ -62,12 +89,6 @@ jobs: run: | ! git rev-parse $NEW_VERSION || { echo "A tag for $NEW_VERSION already exists" ; exit 1; } - - name: Check for new commits since the last tag - env: - LAST_TAG: ${{ steps.previous.outputs.tag }} - run: | - [ $(git log --oneline $LAST_TAG..HEAD | wc -l) -eq "0" ] && { echo "No commits since the last tag. Nothing to release."; exit 1; } - - name: Build Python distributions env: NEW_VERSION: ${{ steps.semver.outputs.patch }}