1+ # LocalStack specific workflow to implement a fully-integrated continuous integration pipeline for our fork
2+ # - Rebase this fork based on the latest commit on `main` of upstream
3+ # - Build a Python source and wheel distribution of moto-ext with deterministic versioning
4+ # - Publish the distributions to PyPi
5+ # - Tag the commit in this fork with the new version
6+ # - Create a GitHub release for the new version
7+
8+ name : Sync / Release moto-ext
9+
10+ on :
11+ schedule :
12+ - cron : 0 5 * * MON
13+ workflow_dispatch :
14+ inputs :
15+ dry_run :
16+ description : ' Dry Run?'
17+ default : true
18+ required : true
19+ type : boolean
20+
21+ # limit concurrency to 1
22+ concurrency :
23+ group : ${{ github.workflow }}
24+
25+ jobs :
26+ sync-build-release-moto-ext :
27+ runs-on : ubuntu-latest
28+ environment :
29+ name : pypi
30+ url : https://pypi.org/project/moto-ext/
31+ permissions :
32+ contents : write
33+ id-token : write
34+ steps :
35+ - name : Checkout
36+ uses : actions/checkout@v4
37+ with :
38+ fetch-depth : 0
39+ ref : localstack
40+ persist-credentials : false
41+
42+ - name : Setup Python
43+ uses : actions/setup-python@v6
44+ with :
45+ python-version : ' 3.13'
46+
47+ - name : Configure Git
48+ run : |
49+ # Configure git
50+ git config --global user.name 'LocalStack Bot'
51+ git config --global user.email 'localstack-bot@users.noreply.github.com'
52+ git remote set-url origin https://git:${{ secrets.PRO_ACCESS_TOKEN }}@github.com/${{ github.repository }}
53+
54+ # make sure to switch to the `localstack` branch (default / main branch of this fork)
55+ git switch localstack
56+ # add moto upstream as remote
57+ git remote add upstream https://github.com/getmoto/moto.git
58+ # rebase with latest changes
59+ git pull
60+
61+ # Create a custom merge driver which prefers everything from upstream _BUT_ the name and the URL
62+ mkdir -p $HOME/.local/bin
63+ cat > $HOME/.local/bin/git-prefer-theirs-name-url << EOF
64+ #!/bin/bash
65+ set -e
66+
67+ base="\$1"
68+ local="\$2"
69+ remote="\$3"
70+
71+ echo "Executing custom merge driver for base \$base, local \$local, remote \$remote."
72+
73+ # Define keys to keep
74+ KEYS=("name" "url")
75+
76+ # Read files into arrays
77+ mapfile -t REMOTE_LINES < "\$remote"
78+ mapfile -t LOCAL_LINES < "\$local"
79+
80+ echo "merging \$local + \$local + \$remote ..."
81+
82+ # Function to check if a line should be kept (matches any key)
83+ keep_line() {
84+ local line="\$1"
85+ for key in "\${KEYS[@]}"; do
86+ [[ "\$line" == *"\$key"* ]] && return 0
87+ done
88+ return 1
89+ }
90+
91+ # keep key-matched lines from local, others from remote
92+ for i in "\${!LOCAL_LINES[@]}"; do
93+ if keep_line "\${REMOTE_LINES[i]}"; then
94+ echo "\${REMOTE_LINES[i]}"
95+ else
96+ echo "\${LOCAL_LINES[i]}"
97+ fi
98+ done > "\$local"
99+
100+ exit 0
101+ EOF
102+
103+ # make the script executable and add it to the PATH
104+ chmod +x $HOME/.local/bin/git-prefer-theirs-name-url
105+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
106+
107+ # add the merge driver to the git config
108+ cat >> .git/config << EOF
109+
110+ [merge "git-prefer-theirs-name-url"]
111+ 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
112+ driver = git-prefer-theirs-name-url %O %A %B
113+ EOF
114+
115+ # define to use the custom merge driver for the setup.cfg
116+ cat > .gitattributes << EOF
117+ setup.cfg merge=git-prefer-theirs-name-url
118+ EOF
119+
120+ - name : Rebase localstack branch with latest master from upstream
121+ run : |
122+ git fetch upstream
123+ git rebase upstream/master
124+
125+ - name : Determine new version
126+ run : |
127+ echo "Determining new version..."
128+ cat > setuptools.cfg << EOF
129+ [tool.setuptools_scm]
130+ local_scheme = "no-local-version"
131+ version_scheme = "post-release"
132+ EOF
133+ python3 -m venv .venv
134+ source .venv/bin/activate
135+ python3 -m pip install setuptools_scm
136+ NEW_VERSION=$(python3 -m setuptools_scm -c setuptools.cfg)
137+ NEW_VERSION="${NEW_VERSION//dev/post}"
138+ echo "New version is: $NEW_VERSION"
139+ echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
140+
141+ - name : Build Python distributions
142+ # FYI: Checks in this script only work because the -e flag is enabled by default in GitHub actions
143+ run : |
144+ python3 -m pip install build
145+
146+ echo "Setting new version in setup.cfg":
147+ # make sure setup.cfg is not dirty yet
148+ git diff --exit-code setup.cfg
149+ sed -i -E 's/^(version\s*=\s*)("?)[^"]+("?)/\1\2'"$NEW_VERSION"'\3/' setup.cfg
150+ # make sure setup.cfg is dirty now
151+ ! git diff --exit-code setup.cfg
152+
153+ echo "Building new version and tagging commit..."
154+ python3 -m build
155+
156+ - name : Tag successful build
157+ run : |
158+ git tag -a $NEW_VERSION -m $NEW_VERSION
159+
160+ - name : Clean up
161+ run : |
162+ git reset --hard
163+ git clean -df
164+
165+ - name : Store built distributions
166+ uses : actions/upload-artifact@v4
167+ with :
168+ name : moto-ext-dists
169+ path : dist/*.*
170+
171+ # publish the package before pushing the tag (this might fail if the version already exists on PyPI)
172+ - name : Publish package distributions to PyPI
173+ if : ${{ github.event.inputs.dry_run != 'true' }}
174+ uses : pypa/gh-action-pypi-publish@release/v1
175+
176+ - name : Push
177+ if : ${{ github.event.inputs.dry_run != 'true' }}
178+ run : |
179+ git push --force-with-lease
180+ git push --atomic origin localstack $NEW_VERSION
181+ env :
182+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
183+
184+ # Add a retry to avoid issues where the GH CLI fails
185+ # because it does not yet detect the pushed tag.
186+ - name : Create Release
187+ uses : nick-fields/retry@v3
188+ if : ${{ github.event.inputs.dry_run != 'true' }}
189+ env :
190+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
191+ with :
192+ max_attempts : 5
193+ retry_wait_seconds : 120
194+ timeout_minutes : 5
195+ command : gh release create $NEW_VERSION --repo localstack/moto --notes "automatic rebase sync and release"
0 commit comments