1+ name : 4. Build Loop Auto
2+ run-name : Build Loop Auto (${{ github.ref_name }})
3+ on :
4+ workflow_dispatch :
5+ schedule :
6+ # Check for updates every Sunday
7+ # Later logic builds if there are updates or if it is the 2nd Sunday of the month
8+ - cron : " 33 7 * * 0" # Sunday at UTC 7:33
9+
10+ env :
11+ GH_PAT : ${{ secrets.GH_PAT }}
12+ UPSTREAM_REPO : LoopKit/LoopWorkspace
13+ UPSTREAM_BRANCH : ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed)
14+ TARGET_BRANCH : ${{ github.ref_name }} # target branch on fork to be kept in sync
15+
16+ jobs :
17+ # use a single runner for these sequential steps
18+ check_status :
19+ runs-on : ubuntu-latest
20+ name : Check status to decide whether to build
21+ permissions :
22+ contents : write
23+ outputs :
24+ NEW_COMMITS : ${{ steps.sync.outputs.has_new_commits }}
25+ IS_SECOND_IN_MONTH : ${{ steps.date-check.outputs.is_second_instance }}
26+
27+ # Check GH_PAT, sync repository, check day in month
28+ steps :
29+
30+ - name : Access
31+ id : workflow-permission
32+ run : |
33+ # Validate Access Token
34+
35+ # Ensure that gh exit codes are handled when output is piped.
36+ set -o pipefail
37+
38+ # Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens.
39+ GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$'
40+ GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$'
41+
42+ # Validate Access Token (GH_PAT)
43+ if [ -z "$GH_PAT" ]; then
44+ failed=true
45+ echo "::error::The GH_PAT secret is unset or empty. Set it and try again."
46+ else
47+ if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then
48+ provides_scopes=true
49+ echo "The GH_PAT secret is a structurally valid classic token."
50+ elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then
51+ echo "The GH_PAT secret is a structurally valid fine-grained token."
52+ else
53+ unknown_format=true
54+ echo "The GH_PAT secret does not have a known token format."
55+ fi
56+
57+ # Attempt to capture the x-oauth-scopes scopes of the token.
58+ if ! scopes=$(curl -sS -f -I -H "Authorization: token $GH_PAT" https://api.github.com | { grep -i '^x-oauth-scopes:' || true; } | cut -d ' ' -f2- | tr -d '\r'); then
59+ failed=true
60+ if [ $unknown_format ]; then
61+ echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that it is set correctly (including the 'ghp_' or 'github_pat_' prefix) and try again."
62+ else
63+ echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that the token exists and has not expired at https://github.com/settings/tokens. If necessary, regenerate or create a new token (and update the secret), then try again."
64+ fi
65+ elif [[ $scopes =~ workflow ]]; then
66+ echo "The GH_PAT secret has repo and workflow permissions."
67+ echo "has_permission=true" >> $GITHUB_OUTPUT
68+ elif [[ $scopes =~ repo ]]; then
69+ echo "The GH_PAT secret has repo (but not workflow) permissions."
70+ elif [ $provides_scopes ]; then
71+ failed=true
72+ if [ -z "$scopes" ]; then
73+ echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes."
74+ else
75+ echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes"
76+ fi
77+ echo "::error::The GH_PAT secret is lacking at least the 'repo' permission scope required to access the Match-Secrets repository. Update the token permissions at https://github.com/settings/tokens (to include the 'repo' and 'workflow' scopes) and try again."
78+ else
79+ echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide inspectable scopes. Assuming that the 'repo' and 'workflow' permission scopes required to access the Match-Secrets repository and perform automations are present."
80+ echo "has_permission=true" >> $GITHUB_OUTPUT
81+ fi
82+ fi
83+
84+ # Exit unsuccessfully if secret validation failed.
85+ if [ $failed ]; then
86+ exit 2
87+ fi
88+
89+ - name : Checkout target repo
90+ if : |
91+ steps.workflow-permission.outputs.has_permission == 'true' &&
92+ (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
93+ uses : actions/checkout@v5
94+ with :
95+ token : ${{ secrets.GH_PAT }}
96+
97+ # This syncs any target branch to upstream branch of the same name
98+ - name : Sync upstream changes
99+ if : | # do not run the upstream sync action on the upstream repository
100+ steps.workflow-permission.outputs.has_permission == 'true' &&
101+ vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'LoopKit'
102+ id : sync
103+ uses : aormsby/Fork-Sync-With-Upstream-action@v3.4.2
104+ with :
105+ target_sync_branch : ${{ env.TARGET_BRANCH }}
106+ shallow_since : 6 months ago
107+ target_repo_token : ${{ secrets.GH_PAT }}
108+ upstream_sync_branch : ${{ env.UPSTREAM_BRANCH }}
109+ upstream_sync_repo : ${{ env.UPSTREAM_REPO }}
110+
111+ # Display a sample message based on the sync output var 'has_new_commits'
112+ - name : New commits found
113+ if : |
114+ steps.workflow-permission.outputs.has_permission == 'true' &&
115+ vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true'
116+ run : echo "New commits were found to sync."
117+
118+ - name : No new commits
119+ if : |
120+ steps.workflow-permission.outputs.has_permission == 'true' &&
121+ vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false'
122+ run : echo "There were no new commits."
123+
124+ - name : Show value of 'has_new_commits'
125+ if : steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false'
126+ run : |
127+ echo ${{ steps.sync.outputs.has_new_commits }}
128+ echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
129+
130+ - name : Show scheduled build configuration message
131+ if : steps.workflow-permission.outputs.has_permission != 'true'
132+ run : |
133+ echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY
134+ echo "You have not yet configured the scheduled sync and build for Loop's browser build." >> $GITHUB_STEP_SUMMARY
135+ echo "Synchronizing your fork of <code>LoopWorkspace</code> with the upstream repository <code>LoopKit/LoopWorkspace</code> will be skipped." >> $GITHUB_STEP_SUMMARY
136+ echo "If you want to enable automatic builds and updates for your Loop, please follow the instructions \
137+ under the following path <code>LoopWorkspace/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
138+
139+ # Set a logic flag if this is the second instance of this day-of-week in this month
140+ - name : Check if this is the second time this day-of-week happens this month
141+ id : date-check
142+ run : |
143+ DAY_OF_MONTH=$(date +%-d)
144+ WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 ))
145+ if [[ $WEEK_OF_MONTH -eq 2 ]]; then
146+ echo "is_second_instance=true" >> "$GITHUB_OUTPUT"
147+ else
148+ echo "is_second_instance=false" >> "$GITHUB_OUTPUT"
149+ fi
150+
151+ # Checks if Distribution certificate is present and valid, optionally nukes and
152+ # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
153+ # only run if a build is planned
154+ check_certs :
155+ needs : [check_status]
156+ name : Check certificates
157+ uses : ./.github/workflows/create_certs.yml
158+ secrets : inherit
159+ if : |
160+ github.event_name == 'workflow_dispatch' ||
161+ (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') ||
162+ (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
163+
164+ # Builds Loop
165+ build :
166+ name : Build
167+ needs : [check_certs, check_status]
168+ runs-on : macos-26
169+ permissions :
170+ contents : write
171+ if :
172+ | # builds with manual start; if scheduled: once a month or when new commits are found
173+ github.event_name == 'workflow_dispatch' ||
174+ (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') ||
175+ (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
176+ steps :
177+ - name : Select Xcode version
178+ run : " sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer"
179+
180+ - name : Checkout Repo for building
181+ uses : actions/checkout@v5
182+ with :
183+ token : ${{ secrets.GH_PAT }}
184+ submodules : recursive
185+ ref : ${{ env.TARGET_BRANCH }}
186+
187+ # Customize Loop: Download and apply patches
188+ - name : Customize Loop
189+ run : |
190+
191+ # LoopWorkspace patches
192+ # -applies any patches located in the LoopWorkspace/patches/ directory
193+ if $(ls ./patches/* &> /dev/null); then
194+ git apply ./patches/* --allow-empty -v --whitespace=fix
195+ fi
196+
197+ # Submodule Loop patches:
198+ # Template for customizing submodule Loop (changes Loop app name to "CustomLoop")
199+ # Remove the "#" sign from the beginning of the line below to activate:
200+ #curl https://github.com/loopnlearn/Loop/commit/d206432b024279ef710df462b20bd464cd9682d4.patch | git apply --directory=Loop -v --whitespace=fix
201+
202+ # Submodule LoopKit patches:
203+ # General template for customizing submodule LoopKit
204+ # Copy url from a GitHub commit or pull request and insert below, and remove the "#" sign from the beginning of the line to activate:
205+ #curl url_to_github_commit.patch | git apply --directory=LoopKit -v --whitespace=fix
206+
207+ # Submodule xxxxx patches:
208+
209+ # Add patches for customization of additional submodules by following the templates above,
210+ # and make sure to specify the submodule by setting "--directory=(submodule_name)".
211+ # Several patches may be added per submodule.
212+ # Adding comments (#) may be useful to easily tell the individual patches apart.
213+
214+ # Patch Fastlane Match to not print tables
215+ - name : Patch Match Tables
216+ run : |
217+ TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb
218+ if [ -f "$TABLE_PRINTER_PATH" ]; then
219+ sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH"
220+ else
221+ echo "table_printer.rb not found"
222+ exit 1
223+ fi
224+
225+ # Install project dependencies
226+ - name : Install Project Dependencies
227+ run : bundle install
228+
229+ # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
230+ - name : Sync clock
231+ run : sudo sntp -sS time.windows.com
232+
233+ # Build signed Loop IPA file
234+ - name : Fastlane Build & Archive
235+ run : bundle exec fastlane build_loop
236+ env :
237+ TEAMID : ${{ secrets.TEAMID }}
238+ GH_PAT : ${{ secrets.GH_PAT }}
239+ FASTLANE_KEY_ID : ${{ secrets.FASTLANE_KEY_ID }}
240+ FASTLANE_ISSUER_ID : ${{ secrets.FASTLANE_ISSUER_ID }}
241+ FASTLANE_KEY : ${{ secrets.FASTLANE_KEY }}
242+ MATCH_PASSWORD : ${{ secrets.MATCH_PASSWORD }}
243+
244+ # Upload to TestFlight
245+ - name : Fastlane upload to TestFlight
246+ run : bundle exec fastlane release
247+ env :
248+ TEAMID : ${{ secrets.TEAMID }}
249+ GH_PAT : ${{ secrets.GH_PAT }}
250+ FASTLANE_KEY_ID : ${{ secrets.FASTLANE_KEY_ID }}
251+ FASTLANE_ISSUER_ID : ${{ secrets.FASTLANE_ISSUER_ID }}
252+ FASTLANE_KEY : ${{ secrets.FASTLANE_KEY }}
253+ MATCH_PASSWORD : ${{ secrets.MATCH_PASSWORD }}
254+
255+ # Upload Build artifacts
256+ - name : Upload build log, IPA and Symbol artifacts
257+ if : always()
258+ uses : actions/upload-artifact@v6
259+ with :
260+ name : build-artifacts
261+ path : |
262+ artifacts
263+ buildlog
0 commit comments