├── .circleci └── config.yml ├── .github └── workflows │ ├── add_identifiers.yml │ ├── build_loop.yml │ ├── create_certs.yml │ └── validate_secrets.yml ├── .gitignore ├── .gitmodules ├── Gemfile ├── Gemfile.lock ├── InfoCustomizations.txt ├── LoopConfigOverride.xcconfig ├── LoopWorkspace.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ ├── swiftpm │ └── Package.resolved │ └── xcschemes │ └── LoopWorkspace.xcscheme ├── OverrideAssetsLoop.xcassets ├── AppIcon.appiconset │ ├── Contents.json │ ├── Icon.png │ ├── icon_20pt.png │ ├── icon_20pt@2x-1.png │ ├── icon_20pt@2x.png │ ├── icon_20pt@3x.png │ ├── icon_29pt.png │ ├── icon_29pt@2x-1.png │ ├── icon_29pt@2x.png │ ├── icon_29pt@3x.png │ ├── icon_40pt.png │ ├── icon_40pt@2x-1.png │ ├── icon_40pt@2x.png │ ├── icon_40pt@3x.png │ ├── icon_60pt@2x.png │ ├── icon_60pt@3x.png │ ├── icon_76pt.png │ ├── icon_76pt@2x.png │ └── icon_83.5@2x.png └── Contents.json ├── OverrideAssetsWatchApp.xcassets ├── AppIcon.appiconset │ ├── Contents.json │ ├── Icon.png │ ├── icon_108pt@2x.png │ ├── icon_24pt@2x.png │ ├── icon_27.5pt@2x.png │ ├── icon_29pt@2x.png │ ├── icon_29pt@3x.png │ ├── icon_40pt@2x.png │ ├── icon_44pt@2x.png │ ├── icon_50pt@2x.png │ ├── icon_86pt@2x.png │ └── icon_98pt@2x.png └── Contents.json ├── README.md ├── Scripts ├── export_localizations.sh ├── import_localizations.sh ├── sync.swift └── update_submodule_refs.sh ├── docs └── scheme-selection.png ├── fastlane ├── Fastfile ├── Matchfile └── testflight.md └── patches └── save_patches_here.md /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | build_and_test: 3 | macos: 4 | xcode: 15.4.0 5 | steps: 6 | - checkout 7 | - run: 8 | name: Checkout submodules 9 | command: git submodule update --init --recursive --depth 1 10 | - run: 11 | name: Build Loop 12 | command: set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' build | xcpretty 13 | - run: 14 | name: Run Tests 15 | command: set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' test | xcpretty 16 | workflows: 17 | version: 2 18 | build_and_test: 19 | jobs: 20 | - build_and_test 21 | -------------------------------------------------------------------------------- /.github/workflows/add_identifiers.yml: -------------------------------------------------------------------------------- 1 | name: 2. Add Identifiers 2 | run-name: Add Identifiers (${{ github.ref_name }}) 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | validate: 8 | name: Validate 9 | uses: ./.github/workflows/validate_secrets.yml 10 | secrets: inherit 11 | 12 | identifiers: 13 | name: Add Identifiers 14 | needs: validate 15 | runs-on: macos-14 16 | steps: 17 | # Uncomment to manually select latest Xcode if needed 18 | #- name: Select Latest Xcode 19 | # run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer" 20 | 21 | # Checks-out the repo 22 | - name: Checkout Repo 23 | uses: actions/checkout@v4 24 | 25 | # Patch Fastlane Match to not print tables 26 | - name: Patch Match Tables 27 | run: | 28 | TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb 29 | if [ -f "$TABLE_PRINTER_PATH" ]; then 30 | sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH" 31 | else 32 | echo "table_printer.rb not found" 33 | exit 1 34 | fi 35 | 36 | # Install project dependencies 37 | - name: Install Project Dependencies 38 | run: bundle install 39 | 40 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996) 41 | - name: Sync clock 42 | run: sudo sntp -sS time.windows.com 43 | 44 | # Create or update identifiers for app 45 | - name: Fastlane Provision 46 | run: bundle exec fastlane identifiers 47 | env: 48 | TEAMID: ${{ secrets.TEAMID }} 49 | GH_PAT: ${{ secrets.GH_PAT }} 50 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 51 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} 52 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} 53 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} 54 | -------------------------------------------------------------------------------- /.github/workflows/build_loop.yml: -------------------------------------------------------------------------------- 1 | name: 4. Build Loop 2 | run-name: Build Loop (${{ github.ref_name }}) 3 | on: 4 | workflow_dispatch: 5 | 6 | ## Remove the "#" sign from the beginning of the line below to get automated builds on push (code changes in your repository) 7 | #push: 8 | 9 | schedule: 10 | - cron: "0 8 * * 3" # Checks for updates at 08:00 UTC every Wednesday 11 | - cron: "0 6 1 * *" # Builds the app on the 1st of every month at 06:00 UTC 12 | 13 | env: 14 | UPSTREAM_REPO: LoopKit/LoopWorkspace 15 | UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed) 16 | TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync, and target branch on upstream to be kept alive (replace with specific branch name if needed) 17 | ALIVE_BRANCH_MAIN: alive-main 18 | ALIVE_BRANCH_DEV: alive-dev 19 | 20 | jobs: 21 | validate: 22 | name: Validate 23 | uses: ./.github/workflows/validate_secrets.yml 24 | secrets: inherit 25 | 26 | # Checks if GH_PAT holds workflow permissions 27 | # Checks for existence of alive branch; if non-existent creates it 28 | check_alive_and_permissions: 29 | needs: validate 30 | runs-on: ubuntu-latest 31 | name: Check alive branch and permissions 32 | permissions: 33 | contents: write 34 | outputs: 35 | WORKFLOW_PERMISSION: ${{ steps.workflow-permission.outputs.has_permission }} 36 | 37 | steps: 38 | - name: Check for workflow permissions 39 | id: workflow-permission 40 | env: 41 | TOKEN_TO_CHECK: ${{ secrets.GH_PAT }} 42 | run: | 43 | PERMISSIONS=$(curl -sS -f -I -H "Authorization: token ${{ env.TOKEN_TO_CHECK }}" https://api.github.com | grep ^x-oauth-scopes: | cut -d' ' -f2-); 44 | 45 | if [[ $PERMISSIONS =~ "workflow" || $PERMISSIONS == "" ]]; then 46 | echo "GH_PAT holds workflow permissions or is fine-grained PAT." 47 | echo "has_permission=true" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false. 48 | else 49 | echo "GH_PAT lacks workflow permissions." 50 | echo "Automated build features will be skipped!" 51 | echo "has_permission=false" >> $GITHUB_OUTPUT # Set WORKFLOW_PERMISSION to false. 52 | fi 53 | 54 | - name: Check for alive branches 55 | if: steps.workflow-permission.outputs.has_permission == 'true' 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 58 | run: | 59 | if [[ $(gh api -H "Accept: application/vnd.github+json" /repos/${{ github.repository_owner }}/LoopWorkspace/branches | jq --raw-output '[.[] | select(.name == "alive-main" or .name == "alive-dev")] | length > 0') == "true" ]]; then 60 | echo "Branches 'alive-main' or 'alive-dev' exist." 61 | echo "ALIVE_BRANCH_EXISTS=true" >> $GITHUB_ENV 62 | else 63 | echo "Branches 'alive-main' and 'alive-dev' do not exist." 64 | echo "ALIVE_BRANCH_EXISTS=false" >> $GITHUB_ENV 65 | fi 66 | 67 | - name: Create alive branches 68 | if: env.ALIVE_BRANCH_EXISTS == 'false' 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 71 | run: | 72 | # Get ref for LoopKit/LoopWorkspace:main 73 | SHA_MAIN=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/main | jq -r '.object.sha') 74 | 75 | # Get ref for LoopKit/LoopWorkspace:dev 76 | SHA_DEV=$(curl -sS -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/repos/${{ env.UPSTREAM_REPO }}/git/refs/heads/dev | jq -r '.object.sha') 77 | 78 | # Create alive-main branch based on LoopKit/LoopWorkspace:main 79 | gh api \ 80 | --method POST \ 81 | -H "Authorization: token $GITHUB_TOKEN" \ 82 | -H "Accept: application/vnd.github.v3+json" \ 83 | /repos/${{ github.repository_owner }}/LoopWorkspace/git/refs \ 84 | -f ref='refs/heads/alive-main' \ 85 | -f sha=$SHA_MAIN 86 | 87 | # Create alive-dev branch based on LoopKit/LoopWorkspace:dev 88 | gh api \ 89 | --method POST \ 90 | -H "Authorization: token $GITHUB_TOKEN" \ 91 | -H "Accept: application/vnd.github.v3+json" \ 92 | /repos/${{ github.repository_owner }}/LoopWorkspace/git/refs \ 93 | -f ref='refs/heads/alive-dev' \ 94 | -f sha=$SHA_DEV 95 | 96 | # Checks for changes in upstream repository; if changes exist prompts sync for build 97 | # Performs keepalive to avoid stale fork 98 | check_latest_from_upstream: 99 | needs: [validate, check_alive_and_permissions] 100 | runs-on: ubuntu-latest 101 | name: Check upstream and keep alive 102 | outputs: 103 | NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }} 104 | ABORT_SYNC: ${{ steps.check_branch.outputs.ABORT_SYNC }} 105 | 106 | steps: 107 | - name: Check if running on main or dev branch 108 | if: | 109 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 110 | (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false') 111 | id: check_branch 112 | run: | 113 | if [ "${GITHUB_REF##*/}" = "main" ]; then 114 | echo "Running on main branch" 115 | echo "ALIVE_BRANCH=${ALIVE_BRANCH_MAIN}" >> $GITHUB_OUTPUT 116 | echo "ABORT_SYNC=false" >> $GITHUB_OUTPUT 117 | elif [ "${GITHUB_REF##*/}" = "dev" ]; then 118 | echo "Running on dev branch" 119 | echo "ALIVE_BRANCH=${ALIVE_BRANCH_DEV}" >> $GITHUB_OUTPUT 120 | echo "ABORT_SYNC=false" >> $GITHUB_OUTPUT 121 | else 122 | echo "Not running on main or dev branch" 123 | echo "ABORT_SYNC=true" >> $GITHUB_OUTPUT 124 | fi 125 | 126 | - name: Checkout target repo 127 | if: | 128 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 129 | (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false') 130 | uses: actions/checkout@v4 131 | with: 132 | token: ${{ secrets.GH_PAT }} 133 | ref: ${{ steps.check_branch.outputs.ALIVE_BRANCH }} 134 | 135 | - name: Sync upstream changes 136 | if: | # do not run the upstream sync action on the upstream repository 137 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 138 | vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'LoopKit' && steps.check_branch.outputs.ABORT_SYNC == 'false' 139 | id: sync 140 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1 141 | with: 142 | target_sync_branch: ${{ steps.check_branch.outputs.ALIVE_BRANCH }} 143 | shallow_since: 6 months ago 144 | target_repo_token: ${{ secrets.GH_PAT }} 145 | upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }} 146 | upstream_sync_repo: ${{ env.UPSTREAM_REPO }} 147 | 148 | # Display a sample message based on the sync output var 'has_new_commits' 149 | - name: New commits found 150 | if: | 151 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 152 | vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true' 153 | run: echo "New commits were found to sync." 154 | 155 | - name: No new commits 156 | if: | 157 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 158 | vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false' 159 | run: echo "There were no new commits." 160 | 161 | - name: Show value of 'has_new_commits' 162 | if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && vars.SCHEDULED_SYNC != 'false' && steps.check_branch.outputs.ABORT_SYNC == 'false' 163 | run: | 164 | echo ${{ steps.sync.outputs.has_new_commits }} 165 | echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT 166 | 167 | # Keep repository "alive": add empty commits to ALIVE_BRANCH after "time_elapsed" days of inactivity to avoid inactivation of scheduled workflows 168 | - name: Keep alive 169 | if: | 170 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 171 | (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false') 172 | uses: gautamkrishnar/keepalive-workflow@v1 # using the workflow with default settings 173 | with: 174 | time_elapsed: 20 # Time elapsed from the previous commit to trigger a new automated commit (in days) 175 | 176 | - name: Show scheduled build configuration message 177 | if: needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION != 'true' 178 | run: | 179 | echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY 180 | echo "You have not yet configured the scheduled sync and build for Loop's browser build." >> $GITHUB_STEP_SUMMARY 181 | echo "Synchronizing your fork of LoopWorkspace with the upstream repository LoopKit/LoopWorkspace will be skipped." >> $GITHUB_STEP_SUMMARY 182 | echo "If you want to enable automatic builds and updates for your Loop, please follow the instructions \ 183 | under the following path LoopWorkspace/fastlane/testflight.md." >> $GITHUB_STEP_SUMMARY 184 | 185 | # Builds Loop 186 | build: 187 | name: Build 188 | needs: [validate, check_alive_and_permissions, check_latest_from_upstream] 189 | runs-on: macos-14 190 | permissions: 191 | contents: write 192 | if: 193 | | # runs if started manually, or if sync schedule is set and enabled and scheduled on the first Saturday each month, or if sync schedule is set and enabled and new commits were found 194 | github.event_name == 'workflow_dispatch' || 195 | (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 196 | (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '0 6 1 * *') || 197 | (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' ) 198 | ) 199 | steps: 200 | - name: Select Xcode version 201 | run: "sudo xcode-select --switch /Applications/Xcode_15.4.app/Contents/Developer" 202 | 203 | - name: Checkout Repo for syncing 204 | if: | 205 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 206 | vars.SCHEDULED_SYNC != 'false' 207 | uses: actions/checkout@v4 208 | with: 209 | token: ${{ secrets.GH_PAT }} 210 | ref: ${{ env.TARGET_BRANCH }} 211 | 212 | - name: Sync upstream changes 213 | if: | # do not run the upstream sync action on the upstream repository 214 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 215 | vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'LoopKit' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false' 216 | id: sync 217 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1 218 | with: 219 | target_sync_branch: ${{ env.TARGET_BRANCH }} 220 | shallow_since: 6 months ago 221 | target_repo_token: ${{ secrets.GH_PAT }} 222 | upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }} 223 | upstream_sync_repo: ${{ env.UPSTREAM_REPO }} 224 | 225 | # Display a sample message based on the sync output var 'has_new_commits' 226 | - name: New commits found 227 | if: | 228 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 229 | vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false' 230 | run: echo "New commits were found to sync." 231 | 232 | - name: No new commits 233 | if: | 234 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' && 235 | vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false' 236 | run: echo "There were no new commits." 237 | 238 | - name: Show value of 'has_new_commits' 239 | if: | 240 | needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' 241 | && vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.ABORT_SYNC == 'false' 242 | run: | 243 | echo ${{ steps.sync.outputs.has_new_commits }} 244 | echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT 245 | 246 | - name: Checkout Repo for building 247 | uses: actions/checkout@v4 248 | with: 249 | token: ${{ secrets.GH_PAT }} 250 | submodules: recursive 251 | ref: ${{ env.TARGET_BRANCH }} 252 | 253 | # Customize Loop: Download and apply patches 254 | - name: Customize Loop 255 | run: | 256 | 257 | # LoopWorkspace patches 258 | # -applies any patches located in the LoopWorkspace/patches/ directory 259 | if $(ls ./patches/* &> /dev/null); then 260 | git apply ./patches/* --allow-empty -v --whitespace=fix 261 | fi 262 | 263 | # Submodule Loop patches: 264 | # Template for customizing submodule Loop (changes Loop app name to "CustomLoop") 265 | # Remove the "#" sign from the beginning of the line below to activate: 266 | #curl https://github.com/loopnlearn/Loop/commit/d206432b024279ef710df462b20bd464cd9682d4.patch | git apply --directory=Loop -v --whitespace=fix 267 | 268 | # Submodule LoopKit patches: 269 | # General template for customizing submodule LoopKit 270 | # Copy url from a GitHub commit or pull request and insert below, and remove the "#" sign from the beginning of the line to activate: 271 | #curl url_to_github_commit.patch | git apply --directory=LoopKit -v --whitespace=fix 272 | 273 | # Submodule xxxxx patches: 274 | 275 | # Add patches for customization of additional submodules by following the templates above, 276 | # and make sure to specify the submodule by setting "--directory=(submodule_name)". 277 | # Several patches may be added per submodule. 278 | # Adding comments (#) may be useful to easily tell the individual patches apart. 279 | 280 | # Patch Fastlane Match to not print tables 281 | - name: Patch Match Tables 282 | run: | 283 | TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb 284 | if [ -f "$TABLE_PRINTER_PATH" ]; then 285 | sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH" 286 | else 287 | echo "table_printer.rb not found" 288 | exit 1 289 | fi 290 | 291 | # Install project dependencies 292 | - name: Install Project Dependencies 293 | run: bundle install 294 | 295 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996) 296 | - name: Sync clock 297 | run: sudo sntp -sS time.windows.com 298 | 299 | # Build signed Loop IPA file 300 | - name: Fastlane Build & Archive 301 | run: bundle exec fastlane build_loop 302 | env: 303 | TEAMID: ${{ secrets.TEAMID }} 304 | GH_PAT: ${{ secrets.GH_PAT }} 305 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} 306 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} 307 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} 308 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 309 | 310 | # Upload to TestFlight 311 | - name: Fastlane upload to TestFlight 312 | run: bundle exec fastlane release 313 | env: 314 | TEAMID: ${{ secrets.TEAMID }} 315 | GH_PAT: ${{ secrets.GH_PAT }} 316 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} 317 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} 318 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} 319 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 320 | 321 | # Upload Build artifacts 322 | - name: Upload build log, IPA and Symbol artifacts 323 | if: always() 324 | uses: actions/upload-artifact@v4 325 | with: 326 | name: build-artifacts 327 | path: | 328 | artifacts 329 | buildlog -------------------------------------------------------------------------------- /.github/workflows/create_certs.yml: -------------------------------------------------------------------------------- 1 | name: 3. Create Certificates 2 | run-name: Create Certificates (${{ github.ref_name }}) 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | validate: 8 | name: Validate 9 | uses: ./.github/workflows/validate_secrets.yml 10 | secrets: inherit 11 | 12 | certificates: 13 | name: Create Certificates 14 | needs: validate 15 | runs-on: macos-14 16 | steps: 17 | # Uncomment to manually select latest Xcode if needed 18 | #- name: Select Latest Xcode 19 | # run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer" 20 | 21 | # Checks-out the repo 22 | - name: Checkout Repo 23 | uses: actions/checkout@v4 24 | 25 | # Patch Fastlane Match to not print tables 26 | - name: Patch Match Tables 27 | run: | 28 | TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb 29 | if [ -f "$TABLE_PRINTER_PATH" ]; then 30 | sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH" 31 | else 32 | echo "table_printer.rb not found" 33 | exit 1 34 | fi 35 | 36 | # Install project dependencies 37 | - name: Install Project Dependencies 38 | run: bundle install 39 | 40 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996) 41 | - name: Sync clock 42 | run: sudo sntp -sS time.windows.com 43 | 44 | # Create or update certificates for app 45 | - name: Create Certificates 46 | run: bundle exec fastlane certs 47 | env: 48 | TEAMID: ${{ secrets.TEAMID }} 49 | GH_PAT: ${{ secrets.GH_PAT }} 50 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 51 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} 52 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} 53 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} 54 | -------------------------------------------------------------------------------- /.github/workflows/validate_secrets.yml: -------------------------------------------------------------------------------- 1 | name: 1. Validate Secrets 2 | run-name: Validate Secrets (${{ github.ref_name }}) 3 | on: [workflow_call, workflow_dispatch] 4 | 5 | jobs: 6 | validate-access-token: 7 | name: Access 8 | runs-on: macos-14 9 | env: 10 | GH_PAT: ${{ secrets.GH_PAT }} 11 | GH_TOKEN: ${{ secrets.GH_PAT }} 12 | outputs: 13 | HAS_WORKFLOW_PERMISSION: ${{ steps.access-token.outputs.has_workflow_permission }} 14 | steps: 15 | - name: Validate Access Token 16 | id: access-token 17 | run: | 18 | # Validate Access Token 19 | 20 | # Ensure that gh exit codes are handled when output is piped. 21 | set -o pipefail 22 | 23 | # Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens. 24 | GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$' 25 | GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$' 26 | 27 | # Validate Access Token (GH_PAT) 28 | if [ -z "$GH_PAT" ]; then 29 | failed=true 30 | echo "::error::The GH_PAT secret is unset or empty. Set it and try again." 31 | else 32 | if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then 33 | provides_scopes=true 34 | echo "The GH_PAT secret is a structurally valid classic token." 35 | elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then 36 | echo "The GH_PAT secret is a structurally valid fine-grained token." 37 | else 38 | unknown_format=true 39 | echo "The GH_PAT secret does not have a known token format." 40 | fi 41 | 42 | # Attempt to capture the x-oauth-scopes scopes of the token. 43 | 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 44 | failed=true 45 | if [ $unknown_format ]; then 46 | 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." 47 | else 48 | 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." 49 | fi 50 | elif [[ $scopes =~ workflow ]]; then 51 | echo "The GH_PAT secret has repo and workflow permissions." 52 | echo "has_workflow_permission=true" >> $GITHUB_OUTPUT 53 | elif [[ $scopes =~ repo ]]; then 54 | echo "The GH_PAT secret has repo (but not workflow) permissions." 55 | elif [ $provides_scopes ]; then 56 | failed=true 57 | if [ -z "$scopes" ]; then 58 | echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes." 59 | else 60 | echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes" 61 | fi 62 | 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." 63 | else 64 | 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." 65 | echo "has_workflow_permission=true" >> $GITHUB_OUTPUT 66 | fi 67 | fi 68 | 69 | # Exit unsuccessfully if secret validation failed. 70 | if [ $failed ]; then 71 | exit 2 72 | fi 73 | 74 | validate-match-secrets: 75 | name: Match-Secrets 76 | needs: validate-access-token 77 | runs-on: macos-14 78 | env: 79 | GH_TOKEN: ${{ secrets.GH_PAT }} 80 | steps: 81 | - name: Validate Match-Secrets 82 | run: | 83 | # Validate Match-Secrets 84 | 85 | # Ensure that gh exit codes are handled when output is piped. 86 | set -o pipefail 87 | 88 | # If a Match-Secrets repository does not exist, attempt to create one. 89 | if ! visibility=$(gh repo view ${{ github.repository_owner }}/Match-Secrets --json visibility | jq --raw-output '.visibility | ascii_downcase'); then 90 | echo "A '${{ github.repository_owner }}/Match-Secrets' repository could not be found using the GH_PAT secret. Attempting to create one..." 91 | 92 | # Create a private Match-Secrets repository and verify that it exists and that it is private. 93 | if gh repo create ${{ github.repository_owner }}/Match-Secrets --private >/dev/null && [ "$(gh repo view ${{ github.repository_owner }}/Match-Secrets --json visibility | jq --raw-output '.visibility | ascii_downcase')" == "private" ]; then 94 | echo "Created a private '${{ github.repository_owner }}/Match-Secrets' repository." 95 | else 96 | failed=true 97 | echo "::error::Unable to create a private '${{ github.repository_owner }}/Match-Secrets' repository. Create a private 'Match-Secrets' repository manually and try again. If a private 'Match-Secrets' repository already exists, verify that the token permissions of the GH_PAT are set correctly (or update them) at https://github.com/settings/tokens and try again." 98 | fi 99 | # Otherwise, if a Match-Secrets repository exists, but it is public, cause validation to fail. 100 | elif [[ "$visibility" == "public" ]]; then 101 | failed=true 102 | echo "::error::A '${{ github.repository_owner }}/Match-Secrets' repository was found, but it is public. Change the repository visibility to private (or delete it) and try again. If necessary, a private repository will be created for you." 103 | else 104 | echo "Found a private '${{ github.repository_owner }}/Match-Secrets' repository to use." 105 | fi 106 | 107 | # Exit unsuccessfully if secret validation failed. 108 | if [ $failed ]; then 109 | exit 2 110 | fi 111 | 112 | validate-fastlane-secrets: 113 | name: Fastlane 114 | needs: [validate-access-token, validate-match-secrets] 115 | runs-on: macos-14 116 | env: 117 | GH_PAT: ${{ secrets.GH_PAT }} 118 | GH_TOKEN: ${{ secrets.GH_PAT }} 119 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} 120 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} 121 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }} 122 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 123 | TEAMID: ${{ secrets.TEAMID }} 124 | steps: 125 | - name: Checkout Repo 126 | uses: actions/checkout@v4 127 | 128 | - name: Install Project Dependencies 129 | run: bundle install 130 | 131 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996) 132 | - name: Sync clock 133 | run: sudo sntp -sS time.windows.com 134 | 135 | - name: Validate Fastlane Secrets 136 | run: | 137 | # Validate Fastlane Secrets 138 | 139 | # Validate TEAMID 140 | if [ -z "$TEAMID" ]; then 141 | failed=true 142 | echo "::error::The TEAMID secret is unset or empty. Set it and try again." 143 | elif [ ${#TEAMID} -ne 10 ]; then 144 | failed=true 145 | echo "::error::The TEAMID secret is set but has wrong length. Verify that it is set correctly and try again." 146 | elif ! [[ $TEAMID =~ ^[A-Z0-9]+$ ]]; then 147 | failed=true 148 | echo "::error::The TEAMID secret is set but invalid. Verify that it is set correctly (only uppercase letters and numbers) and try again." 149 | fi 150 | 151 | # Validate MATCH_PASSWORD 152 | if [ -z "$MATCH_PASSWORD" ]; then 153 | failed=true 154 | echo "::error::The MATCH_PASSWORD secret is unset or empty. Set it and try again." 155 | fi 156 | 157 | # Ensure that fastlane exit codes are handled when output is piped. 158 | set -o pipefail 159 | 160 | # Validate FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY 161 | FASTLANE_KEY_ID_PATTERN='^[A-Z0-9]+$' 162 | FASTLANE_ISSUER_ID_PATTERN='^\{?[A-F0-9a-f]{8}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{4}-[A-F0-9a-f]{12}\}?$' 163 | 164 | if [ -z "$FASTLANE_ISSUER_ID" ] || [ -z "$FASTLANE_KEY_ID" ] || [ -z "$FASTLANE_KEY" ]; then 165 | failed=true 166 | [ -z "$FASTLANE_ISSUER_ID" ] && echo "::error::The FASTLANE_ISSUER_ID secret is unset or empty. Set it and try again." 167 | [ -z "$FASTLANE_KEY_ID" ] && echo "::error::The FASTLANE_KEY_ID secret is unset or empty. Set it and try again." 168 | [ -z "$FASTLANE_KEY" ] && echo "::error::The FASTLANE_KEY secret is unset or empty. Set it and try again." 169 | elif [ ${#FASTLANE_KEY_ID} -ne 10 ]; then 170 | failed=true 171 | echo "::error::The FASTLANE_KEY_ID secret is set but has wrong length. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/integrations/api and try again." 172 | elif ! [[ $FASTLANE_KEY_ID =~ $FASTLANE_KEY_ID_PATTERN ]]; then 173 | failed=true 174 | echo "::error::The FASTLANE_KEY_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/integrations/api and try again." 175 | elif ! [[ $FASTLANE_ISSUER_ID =~ $FASTLANE_ISSUER_ID_PATTERN ]]; then 176 | failed=true 177 | echo "::error::The FASTLANE_ISSUER_ID secret is set but invalid. Verify that you copied it correctly from the 'Keys' tab at https://appstoreconnect.apple.com/access/integrations/api and try again." 178 | elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then 179 | failed=true 180 | echo "::error::The FASTLANE_KEY secret is set but invalid. Verify that you copied it correctly from the API Key file (*.p8) you downloaded and try again." 181 | elif ! bundle exec fastlane validate_secrets 2>&1 | tee fastlane.log; then 182 | if grep -q "bad decrypt" fastlane.log; then 183 | failed=true 184 | echo "::error::Unable to decrypt the Match-Secrets repository using the MATCH_PASSWORD secret. Verify that it is set correctly and try again." 185 | elif grep -q -e "required agreement" -e "license agreement" fastlane.log; then 186 | failed=true 187 | echo "::error::Unable to create a valid authorization token for the App Store Connect API. Verify that the latest developer program license agreement has been accepted at https://developer.apple.com/account (review and accept any updated agreement), then wait a few minutes for changes to propagate and try again." 188 | elif ! grep -q -e "No code signing identity found" -e "Could not install WWDR certificate" fastlane.log; then 189 | failed=true 190 | echo "::error::Unable to create a valid authorization token for the App Store Connect API. Verify that the FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY secrets are set correctly and try again." 191 | fi 192 | fi 193 | 194 | # Exit unsuccessfully if secret validation failed. 195 | if [ $failed ]; then 196 | exit 2 197 | fi 198 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Build 2 | DerivedData/ 3 | 4 | ## Settings 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata/ 14 | 15 | ## Other 16 | *.moved-aside 17 | *.xccheckout 18 | *.xcscmblueprint 19 | *.xcuserstate 20 | .DS_Store 21 | 22 | ## Obj-C/Swift specific 23 | *.hmap 24 | *.ipa 25 | 26 | ## Playgrounds 27 | *.playground 28 | playground.xcworkspace 29 | timeline.xctimeline 30 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Loop"] 2 | path = Loop 3 | url = https://github.com/LoopKit/Loop.git 4 | [submodule "LoopKit"] 5 | path = LoopKit 6 | url = https://github.com/LoopKit/LoopKit.git 7 | [submodule "CGMBLEKit"] 8 | path = CGMBLEKit 9 | url = https://github.com/LoopKit/CGMBLEKit.git 10 | [submodule "dexcom-share-client-swift"] 11 | path = dexcom-share-client-swift 12 | url = https://github.com/LoopKit/dexcom-share-client-swift.git 13 | [submodule "RileyLinkKit"] 14 | path = RileyLinkKit 15 | url = https://github.com/LoopKit/RileyLinkKit 16 | [submodule "NightscoutService"] 17 | path = NightscoutService 18 | url = https://github.com/LoopKit/NightscoutService.git 19 | [submodule "Minizip"] 20 | path = Minizip 21 | url = https://github.com/LoopKit/Minizip.git 22 | [submodule "TrueTime.swift"] 23 | path = TrueTime.swift 24 | url = https://github.com/LoopKit/TrueTime.swift.git 25 | [submodule "LoopOnboarding"] 26 | path = LoopOnboarding 27 | url = https://github.com/LoopKit/LoopOnboarding.git 28 | [submodule "AmplitudeService"] 29 | path = AmplitudeService 30 | url = https://github.com/LoopKit/AmplitudeService.git 31 | [submodule "LogglyService"] 32 | path = LogglyService 33 | url = https://github.com/LoopKit/LogglyService.git 34 | [submodule "OmniBLE"] 35 | path = OmniBLE 36 | url = https://github.com/LoopKit/OmniBLE.git 37 | [submodule "NightscoutRemoteCGM"] 38 | path = NightscoutRemoteCGM 39 | url = https://github.com/LoopKit/NightscoutRemoteCGM.git 40 | [submodule "LoopSupport"] 41 | path = LoopSupport 42 | url = https://github.com/LoopKit/LoopSupport 43 | [submodule "G7SensorKit"] 44 | path = G7SensorKit 45 | url = https://github.com/LoopKit/G7SensorKit.git 46 | [submodule "TidepoolService"] 47 | path = TidepoolService 48 | url = https://github.com/LoopKit/TidepoolService.git 49 | [submodule "OmniKit"] 50 | path = OmniKit 51 | url = https://github.com/LoopKit/OmniKit.git 52 | [submodule "MinimedKit"] 53 | path = MinimedKit 54 | url = https://github.com/LoopKit/MinimedKit.git 55 | [submodule "MixpanelService"] 56 | path = MixpanelService 57 | url = https://github.com/LoopKit/MixpanelService 58 | [submodule "LibreTransmitter"] 59 | path = LibreTransmitter 60 | url = https://github.com/LoopKit/LibreTransmitter.git 61 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | addressable (2.8.7) 9 | public_suffix (>= 2.0.2, < 7.0) 10 | artifactory (3.0.17) 11 | atomos (0.1.3) 12 | aws-eventstream (1.3.0) 13 | aws-partitions (1.949.0) 14 | aws-sdk-core (3.200.0) 15 | aws-eventstream (~> 1, >= 1.3.0) 16 | aws-partitions (~> 1, >= 1.651.0) 17 | aws-sigv4 (~> 1.8) 18 | jmespath (~> 1, >= 1.6.1) 19 | aws-sdk-kms (1.87.0) 20 | aws-sdk-core (~> 3, >= 3.199.0) 21 | aws-sigv4 (~> 1.1) 22 | aws-sdk-s3 (1.155.0) 23 | aws-sdk-core (~> 3, >= 3.199.0) 24 | aws-sdk-kms (~> 1) 25 | aws-sigv4 (~> 1.8) 26 | aws-sigv4 (1.8.0) 27 | aws-eventstream (~> 1, >= 1.0.2) 28 | babosa (1.0.4) 29 | base64 (0.2.0) 30 | claide (1.1.0) 31 | colored (1.2) 32 | colored2 (3.1.2) 33 | commander (4.6.0) 34 | highline (~> 2.0.0) 35 | declarative (0.0.20) 36 | digest-crc (0.6.5) 37 | rake (>= 12.0.0, < 14.0.0) 38 | domain_name (0.5.20190701) 39 | unf (>= 0.0.5, < 1.0.0) 40 | dotenv (2.8.1) 41 | emoji_regex (3.2.3) 42 | excon (0.109.0) 43 | faraday (1.10.3) 44 | faraday-em_http (~> 1.0) 45 | faraday-em_synchrony (~> 1.0) 46 | faraday-excon (~> 1.1) 47 | faraday-httpclient (~> 1.0) 48 | faraday-multipart (~> 1.0) 49 | faraday-net_http (~> 1.0) 50 | faraday-net_http_persistent (~> 1.0) 51 | faraday-patron (~> 1.0) 52 | faraday-rack (~> 1.0) 53 | faraday-retry (~> 1.0) 54 | ruby2_keywords (>= 0.0.4) 55 | faraday-cookie_jar (0.0.7) 56 | faraday (>= 0.8.0) 57 | http-cookie (~> 1.0.0) 58 | faraday-em_http (1.0.0) 59 | faraday-em_synchrony (1.0.0) 60 | faraday-excon (1.1.0) 61 | faraday-httpclient (1.0.1) 62 | faraday-multipart (1.0.4) 63 | multipart-post (~> 2) 64 | faraday-net_http (1.0.1) 65 | faraday-net_http_persistent (1.2.0) 66 | faraday-patron (1.0.0) 67 | faraday-rack (1.0.0) 68 | faraday-retry (1.0.3) 69 | faraday_middleware (1.2.0) 70 | faraday (~> 1.0) 71 | fastimage (2.3.1) 72 | fastlane (2.221.1) 73 | CFPropertyList (>= 2.3, < 4.0.0) 74 | addressable (>= 2.8, < 3.0.0) 75 | artifactory (~> 3.0) 76 | aws-sdk-s3 (~> 1.0) 77 | babosa (>= 1.0.3, < 2.0.0) 78 | bundler (>= 1.12.0, < 3.0.0) 79 | colored (~> 1.2) 80 | commander (~> 4.6) 81 | dotenv (>= 2.1.1, < 3.0.0) 82 | emoji_regex (>= 0.1, < 4.0) 83 | excon (>= 0.71.0, < 1.0.0) 84 | faraday (~> 1.0) 85 | faraday-cookie_jar (~> 0.0.6) 86 | faraday_middleware (~> 1.0) 87 | fastimage (>= 2.1.0, < 3.0.0) 88 | gh_inspector (>= 1.1.2, < 2.0.0) 89 | google-apis-androidpublisher_v3 (~> 0.3) 90 | google-apis-playcustomapp_v1 (~> 0.1) 91 | google-cloud-env (>= 1.6.0, < 2.0.0) 92 | google-cloud-storage (~> 1.31) 93 | highline (~> 2.0) 94 | http-cookie (~> 1.0.5) 95 | json (< 3.0.0) 96 | jwt (>= 2.1.0, < 3) 97 | mini_magick (>= 4.9.4, < 5.0.0) 98 | multipart-post (>= 2.0.0, < 3.0.0) 99 | naturally (~> 2.2) 100 | optparse (>= 0.1.1, < 1.0.0) 101 | plist (>= 3.1.0, < 4.0.0) 102 | rubyzip (>= 2.0.0, < 3.0.0) 103 | security (= 0.1.5) 104 | simctl (~> 1.6.3) 105 | terminal-notifier (>= 2.0.0, < 3.0.0) 106 | terminal-table (~> 3) 107 | tty-screen (>= 0.6.3, < 1.0.0) 108 | tty-spinner (>= 0.8.0, < 1.0.0) 109 | word_wrap (~> 1.0.0) 110 | xcodeproj (>= 1.13.0, < 2.0.0) 111 | xcpretty (~> 0.3.0) 112 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) 113 | gh_inspector (1.1.3) 114 | google-apis-androidpublisher_v3 (0.54.0) 115 | google-apis-core (>= 0.11.0, < 2.a) 116 | google-apis-core (0.11.3) 117 | addressable (~> 2.5, >= 2.5.1) 118 | googleauth (>= 0.16.2, < 2.a) 119 | httpclient (>= 2.8.1, < 3.a) 120 | mini_mime (~> 1.0) 121 | representable (~> 3.0) 122 | retriable (>= 2.0, < 4.a) 123 | rexml 124 | google-apis-iamcredentials_v1 (0.17.0) 125 | google-apis-core (>= 0.11.0, < 2.a) 126 | google-apis-playcustomapp_v1 (0.13.0) 127 | google-apis-core (>= 0.11.0, < 2.a) 128 | google-apis-storage_v1 (0.29.0) 129 | google-apis-core (>= 0.11.0, < 2.a) 130 | google-cloud-core (1.6.1) 131 | google-cloud-env (>= 1.0, < 3.a) 132 | google-cloud-errors (~> 1.0) 133 | google-cloud-env (1.6.0) 134 | faraday (>= 0.17.3, < 3.0) 135 | google-cloud-errors (1.3.1) 136 | google-cloud-storage (1.45.0) 137 | addressable (~> 2.8) 138 | digest-crc (~> 0.4) 139 | google-apis-iamcredentials_v1 (~> 0.1) 140 | google-apis-storage_v1 (~> 0.29.0) 141 | google-cloud-core (~> 1.6) 142 | googleauth (>= 0.16.2, < 2.a) 143 | mini_mime (~> 1.0) 144 | googleauth (1.8.1) 145 | faraday (>= 0.17.3, < 3.a) 146 | jwt (>= 1.4, < 3.0) 147 | multi_json (~> 1.11) 148 | os (>= 0.9, < 2.0) 149 | signet (>= 0.16, < 2.a) 150 | highline (2.0.3) 151 | http-cookie (1.0.6) 152 | domain_name (~> 0.5) 153 | httpclient (2.8.3) 154 | jmespath (1.6.2) 155 | json (2.7.2) 156 | jwt (2.8.2) 157 | base64 158 | mini_magick (4.13.1) 159 | mini_mime (1.1.5) 160 | multi_json (1.15.0) 161 | multipart-post (2.4.1) 162 | nanaimo (0.3.0) 163 | naturally (2.2.1) 164 | nkf (0.2.0) 165 | optparse (0.5.0) 166 | os (1.1.4) 167 | plist (3.7.1) 168 | public_suffix (5.1.1) 169 | rake (13.2.1) 170 | representable (3.2.0) 171 | declarative (< 0.1.0) 172 | trailblazer-option (>= 0.1.1, < 0.2.0) 173 | uber (< 0.2.0) 174 | retriable (3.1.2) 175 | rexml (3.2.9) 176 | strscan 177 | rouge (2.0.7) 178 | ruby2_keywords (0.0.5) 179 | rubyzip (2.3.2) 180 | security (0.1.5) 181 | signet (0.18.0) 182 | addressable (~> 2.8) 183 | faraday (>= 0.17.5, < 3.a) 184 | jwt (>= 1.5, < 3.0) 185 | multi_json (~> 1.10) 186 | simctl (1.6.10) 187 | CFPropertyList 188 | naturally 189 | strscan (3.1.0) 190 | terminal-notifier (2.0.0) 191 | terminal-table (3.0.2) 192 | unicode-display_width (>= 1.1.1, < 3) 193 | trailblazer-option (0.1.2) 194 | tty-cursor (0.7.1) 195 | tty-screen (0.8.2) 196 | tty-spinner (0.9.3) 197 | tty-cursor (~> 0.7) 198 | uber (0.1.0) 199 | unf (0.1.4) 200 | unf_ext 201 | unf_ext (0.0.9.1) 202 | unicode-display_width (2.5.0) 203 | word_wrap (1.0.0) 204 | xcodeproj (1.24.0) 205 | CFPropertyList (>= 2.3.3, < 4.0) 206 | atomos (~> 0.1.3) 207 | claide (>= 1.0.2, < 2.0) 208 | colored2 (~> 3.1) 209 | nanaimo (~> 0.3.0) 210 | rexml (~> 3.2.4) 211 | xcpretty (0.3.0) 212 | rouge (~> 2.0.7) 213 | xcpretty-travis-formatter (1.0.1) 214 | xcpretty (~> 0.2, >= 0.0.7) 215 | 216 | PLATFORMS 217 | arm64-darwin-21 218 | arm64-darwin-22 219 | arm64-darwin-23 220 | x86_64-darwin-19 221 | x86_64-linux 222 | 223 | DEPENDENCIES 224 | fastlane 225 | 226 | BUNDLED WITH 227 | 2.4.19 228 | -------------------------------------------------------------------------------- /InfoCustomizations.txt: -------------------------------------------------------------------------------- 1 | TidepoolServiceClientId=diy-loop 2 | -------------------------------------------------------------------------------- /LoopConfigOverride.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "../../LoopConfigOverride.xcconfig" 2 | 3 | // Override this if you don't want the default com.${DEVELOPMENT_TEAM}.loopkit that loop uses 4 | // MAIN_APP_BUNDLE_IDENTIFIER = com.myname.loop 5 | 6 | // Customize this to change the app name displayed 7 | //MAIN_APP_DISPLAY_NAME = Loop 8 | 9 | // Customize this to change the URL to open Loop to something other than the display name 10 | //URL_SCHEME_NAME = $(MAIN_APP_DISPLAY_NAME) 11 | 12 | // Features 13 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) EXPERIMENTAL_FEATURES_ENABLED SIMULATORS_ENABLED ALLOW_ALGORITHM_EXPERIMENTS DEBUG_FEATURES_ENABLED 14 | 15 | // Put your team id here for signing 16 | //LOOP_DEVELOPMENT_TEAM = UY678SP37Q 17 | -------------------------------------------------------------------------------- /LoopWorkspace.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 26 | 28 | 29 | 31 | 32 | 34 | 35 | 36 | 38 | 39 | 41 | 42 | 44 | 45 | 47 | 48 | 50 | 51 | 53 | 54 | 56 | 57 | 59 | 60 | 62 | 63 | 65 | 66 | 68 | 69 | 71 | 72 | 74 | 75 | 77 | 78 | 80 | 81 | 83 | 84 | 86 | 87 | 89 | 90 | 92 | 93 | 95 | 96 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /LoopWorkspace.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LoopWorkspace.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | PreviewsEnabled 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LoopWorkspace.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "f8d1e9c237647ab612da7f2bd3ae26946f39410508314c00cf54509a673f147e", 3 | "pins" : [ 4 | { 5 | "identity" : "amplitude-ios", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/amplitude/Amplitude-iOS.git", 8 | "state" : { 9 | "branch" : "main", 10 | "revision" : "e818b182f5c3d5ce5035deab90bca108175b3561" 11 | } 12 | }, 13 | { 14 | "identity" : "analytics-connector-ios", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/amplitude/analytics-connector-ios.git", 17 | "state" : { 18 | "revision" : "d3d682a26ca6f4947ece2c2e627971bb41b940fa", 19 | "version" : "1.0.1" 20 | } 21 | }, 22 | { 23 | "identity" : "base32", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/mattrubin/Base32.git", 26 | "state" : { 27 | "branch" : "1.1.2+spm", 28 | "revision" : "d185e44c8b355d34d5c6c6ad502c60cba4599f69" 29 | } 30 | }, 31 | { 32 | "identity" : "cryptoswift", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/krzyzanowskim/CryptoSwift", 35 | "state" : { 36 | "revision" : "eee9ad754926c40a0f7e73f152357d37b119b7fa", 37 | "version" : "1.7.1" 38 | } 39 | }, 40 | { 41 | "identity" : "mixpanel-swift", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/mixpanel/mixpanel-swift.git", 44 | "state" : { 45 | "branch" : "master", 46 | "revision" : "c676a9737c76e127e3ae5776247b226bc6d7652d" 47 | } 48 | }, 49 | { 50 | "identity" : "mkringprogressview", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/maxkonovalov/MKRingProgressView.git", 53 | "state" : { 54 | "branch" : "master", 55 | "revision" : "660888aab1d2ab0ed7eb9eb53caec12af4955fa7" 56 | } 57 | }, 58 | { 59 | "identity" : "nightscoutkit", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/LoopKit/NightscoutKit", 62 | "state" : { 63 | "branch" : "main", 64 | "revision" : "ca8e2cea82ab465282cd180ce01d64c1cf25478d" 65 | } 66 | }, 67 | { 68 | "identity" : "onetimepassword", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/mattrubin/OneTimePassword", 71 | "state" : { 72 | "revision" : "8e4022f2852d77240d0a17482cbfe325354aac70" 73 | } 74 | }, 75 | { 76 | "identity" : "slidebutton", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/no-comment/SlideButton", 79 | "state" : { 80 | "branch" : "main", 81 | "revision" : "5eacebba4d7deeb693592bc9a62ab2d2181e133b" 82 | } 83 | }, 84 | { 85 | "identity" : "swiftcharts", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/ivanschuetz/SwiftCharts", 88 | "state" : { 89 | "branch" : "master", 90 | "revision" : "c354c1945bb35a1f01b665b22474f6db28cba4a2" 91 | } 92 | }, 93 | { 94 | "identity" : "tidepoolkit", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/tidepool-org/TidepoolKit", 97 | "state" : { 98 | "branch" : "dev", 99 | "revision" : "54045c2e7d720dcd8a0909037772dcd6f54f0158" 100 | } 101 | }, 102 | { 103 | "identity" : "zipfoundation", 104 | "kind" : "remoteSourceControl", 105 | "location" : "https://github.com/LoopKit/ZIPFoundation.git", 106 | "state" : { 107 | "branch" : "stream-entry", 108 | "revision" : "c67b7509ec82ee2b4b0ab3f97742b94ed9692494" 109 | } 110 | } 111 | ], 112 | "version" : 3 113 | } 114 | -------------------------------------------------------------------------------- /LoopWorkspace.xcworkspace/xcshareddata/xcschemes/LoopWorkspace.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 71 | 77 | 78 | 79 | 85 | 91 | 92 | 93 | 99 | 105 | 106 | 107 | 113 | 119 | 120 | 121 | 127 | 133 | 134 | 135 | 141 | 147 | 148 | 149 | 155 | 161 | 162 | 163 | 169 | 175 | 176 | 177 | 183 | 189 | 190 | 191 | 197 | 203 | 204 | 205 | 211 | 217 | 218 | 219 | 225 | 231 | 232 | 233 | 239 | 245 | 246 | 247 | 253 | 259 | 260 | 261 | 267 | 273 | 274 | 275 | 281 | 287 | 288 | 289 | 295 | 301 | 302 | 303 | 309 | 315 | 316 | 317 | 323 | 329 | 330 | 331 | 337 | 343 | 344 | 345 | 351 | 357 | 358 | 359 | 365 | 371 | 372 | 373 | 379 | 385 | 386 | 387 | 393 | 399 | 400 | 401 | 407 | 413 | 414 | 415 | 416 | 417 | 422 | 423 | 429 | 430 | 431 | 432 | 434 | 440 | 441 | 442 | 444 | 450 | 451 | 452 | 454 | 460 | 461 | 462 | 464 | 470 | 471 | 472 | 474 | 480 | 481 | 482 | 484 | 490 | 491 | 492 | 494 | 500 | 501 | 502 | 504 | 510 | 511 | 512 | 514 | 520 | 521 | 522 | 524 | 530 | 531 | 532 | 534 | 540 | 541 | 542 | 543 | 544 | 554 | 556 | 562 | 563 | 564 | 565 | 571 | 573 | 579 | 580 | 581 | 582 | 584 | 585 | 588 | 589 | 590 | -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon_20pt@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon_20pt@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon_29pt@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon_29pt@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "icon_40pt@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon_40pt@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "icon_60pt@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon_60pt@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "icon_20pt.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "icon_20pt@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "icon_29pt.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "icon_29pt@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "icon_40pt.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "icon_40pt@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "icon_76pt.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon_76pt@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "icon_83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /OverrideAssetsLoop.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_24pt@2x.png", 5 | "idiom" : "watch", 6 | "role" : "notificationCenter", 7 | "scale" : "2x", 8 | "size" : "24x24", 9 | "subtype" : "38mm" 10 | }, 11 | { 12 | "filename" : "icon_27.5pt@2x.png", 13 | "idiom" : "watch", 14 | "role" : "notificationCenter", 15 | "scale" : "2x", 16 | "size" : "27.5x27.5", 17 | "subtype" : "42mm" 18 | }, 19 | { 20 | "filename" : "icon_29pt@2x.png", 21 | "idiom" : "watch", 22 | "role" : "companionSettings", 23 | "scale" : "2x", 24 | "size" : "29x29" 25 | }, 26 | { 27 | "filename" : "icon_29pt@3x.png", 28 | "idiom" : "watch", 29 | "role" : "companionSettings", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "idiom" : "watch", 35 | "role" : "notificationCenter", 36 | "scale" : "2x", 37 | "size" : "33x33", 38 | "subtype" : "45mm" 39 | }, 40 | { 41 | "filename" : "icon_40pt@2x.png", 42 | "idiom" : "watch", 43 | "role" : "appLauncher", 44 | "scale" : "2x", 45 | "size" : "40x40", 46 | "subtype" : "38mm" 47 | }, 48 | { 49 | "filename" : "icon_44pt@2x.png", 50 | "idiom" : "watch", 51 | "role" : "appLauncher", 52 | "scale" : "2x", 53 | "size" : "44x44", 54 | "subtype" : "40mm" 55 | }, 56 | { 57 | "idiom" : "watch", 58 | "role" : "appLauncher", 59 | "scale" : "2x", 60 | "size" : "46x46", 61 | "subtype" : "41mm" 62 | }, 63 | { 64 | "filename" : "icon_50pt@2x.png", 65 | "idiom" : "watch", 66 | "role" : "appLauncher", 67 | "scale" : "2x", 68 | "size" : "50x50", 69 | "subtype" : "44mm" 70 | }, 71 | { 72 | "idiom" : "watch", 73 | "role" : "appLauncher", 74 | "scale" : "2x", 75 | "size" : "51x51", 76 | "subtype" : "45mm" 77 | }, 78 | { 79 | "idiom" : "watch", 80 | "role" : "appLauncher", 81 | "scale" : "2x", 82 | "size" : "54x54", 83 | "subtype" : "49mm" 84 | }, 85 | { 86 | "filename" : "icon_86pt@2x.png", 87 | "idiom" : "watch", 88 | "role" : "quickLook", 89 | "scale" : "2x", 90 | "size" : "86x86", 91 | "subtype" : "38mm" 92 | }, 93 | { 94 | "filename" : "icon_98pt@2x.png", 95 | "idiom" : "watch", 96 | "role" : "quickLook", 97 | "scale" : "2x", 98 | "size" : "98x98", 99 | "subtype" : "42mm" 100 | }, 101 | { 102 | "filename" : "icon_108pt@2x.png", 103 | "idiom" : "watch", 104 | "role" : "quickLook", 105 | "scale" : "2x", 106 | "size" : "108x108", 107 | "subtype" : "44mm" 108 | }, 109 | { 110 | "idiom" : "watch", 111 | "role" : "quickLook", 112 | "scale" : "2x", 113 | "size" : "117x117", 114 | "subtype" : "45mm" 115 | }, 116 | { 117 | "idiom" : "watch", 118 | "role" : "quickLook", 119 | "scale" : "2x", 120 | "size" : "129x129", 121 | "subtype" : "49mm" 122 | }, 123 | { 124 | "filename" : "Icon.png", 125 | "idiom" : "watch-marketing", 126 | "scale" : "1x", 127 | "size" : "1024x1024" 128 | } 129 | ], 130 | "info" : { 131 | "author" : "xcode", 132 | "version" : 1 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_108pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_108pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_24pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_24pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_27.5pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_27.5pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_44pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_44pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_50pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_50pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_86pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_86pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_98pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_98pt@2x.png -------------------------------------------------------------------------------- /OverrideAssetsWatchApp.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LoopWorkspace 2 | 3 | The Loop app can be built using GitHub in a browser on any computer or using a Mac with Xcode. 4 | 5 | * Non-developers may prefer the GitHub method 6 | * Developers or Loopers who want full build control may prefer the Mac/Xcode method 7 | 8 | ## GitHub Build Instructions 9 | 10 | The GitHub Build Instructions are at this [link](fastlane/testflight.md) and further expanded in [LoopDocs: Browser Build](https://loopkit.github.io/loopdocs/gh-actions/gh-overview/). 11 | 12 | ## Mac/Xcode Build Instructions 13 | 14 | The rest of this README contains information needed for Mac/Xcode build. Additonal instructions are found in [LoopDocs: Mac/Xcode Build](https://loopkit.github.io/loopdocs/build/overview/). 15 | 16 | ### Clone 17 | 18 | This repository uses git submodules to pull in the various workspace dependencies. 19 | 20 | To clone this repo: 21 | 22 | ``` 23 | git clone --branch= --recurse-submodules https://github.com/LoopKit/LoopWorkspace 24 | ``` 25 | 26 | Replace `` with the initial LoopWorkspace repository branch you wish to checkout. 27 | 28 | ### Open 29 | 30 | Change to the cloned directory and open the workspace in Xcode: 31 | 32 | ``` 33 | cd LoopWorkspace 34 | xed . 35 | ``` 36 | 37 | ### Input your development team 38 | 39 | You should be able to build to a simulator without changing anything. But if you wish to build to a real device, you'll need a developer account, and you'll need to tell Xcode about your team id, which you can find at https://developer.apple.com/. 40 | 41 | Select the LoopConfigOverride file in Xcode's project navigator, uncomment the `LOOP_DEVELOPMENT_TEAM`, and replace the existing team id with your own id. 42 | 43 | ### Build 44 | 45 | Select the "LoopWorkspace" scheme (not the "Loop" scheme) and Build, Run, or Test. 46 | -------------------------------------------------------------------------------- /Scripts/export_localizations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | set -e 4 | set -u 5 | 6 | : "$LOKALISE_TOKEN" 7 | 8 | LANGUAGES=(ar cs ru en zh-Hans nl fr de it nb pl es ja pt-BR vi da sv fi ro tr he sk hi) 9 | 10 | argstring="${LANGUAGES[@]/#/-exportLanguage }" 11 | IFS=" "; args=( $=argstring ) 12 | 13 | xcodebuild -scheme LoopWorkspace -exportLocalizations -localizationPath xclocs $args 14 | 15 | mkdir -p xliff_out 16 | find xclocs -name '*.xliff' -exec cp {} xliff_out \; 17 | 18 | -------------------------------------------------------------------------------- /Scripts/import_localizations.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Install the Lokalise command line tools from https://github.com/lokalise/lokalise-cli-2-go 4 | # Generate an API Token (not an SDK Token!) following the instructions here: https://docs.lokalise.com/en/articles/1929556-api-tokens 5 | # export LOKALISE_TOKEN="" 6 | # export GH_TOKEN="" 7 | 8 | set -e 9 | set -u 10 | 11 | : "$LOKALISE_TOKEN" 12 | : "$GH_TOKEN" 13 | 14 | date=`date` 15 | 16 | # Fetch translations from Lokalise 17 | rm -rf xliff_in 18 | lokalise2 \ 19 | --token "$LOKALISE_TOKEN" \ 20 | --project-id "414338966417c70d7055e2.75119857" \ 21 | file download \ 22 | --format xliff \ 23 | --bundle-structure "%LANG_ISO%.%FORMAT%" \ 24 | --original-filenames=false \ 25 | --placeholder-format ios \ 26 | --export-empty-as skip \ 27 | --replace-breaks=false \ 28 | --unzip-to ./xliff_in 29 | 30 | projects=(LoopKit:AmplitudeService:dev LoopKit:CGMBLEKit:dev LoopKit:G7SensorKit:main LoopKit:LogglyService:dev LoopKit:Loop:dev LoopKit:LoopKit:dev LoopKit:LoopOnboarding:dev LoopKit:LoopSupport:dev LoopKit:NightscoutRemoteCGM:dev LoopKit:NightscoutService:dev LoopKit:OmniBLE:dev LoopKit:TidepoolService:dev LoopKit:dexcom-share-client-swift:dev LoopKit:RileyLinkKit:dev LoopKit:OmniKit:main LoopKit:MinimedKit:main LoopKit:LibreTransmitter:main) 31 | 32 | for project in ${projects}; do 33 | echo "Prepping $project" 34 | IFS=":" read user dir branch <<< "$project" 35 | echo "parts = $user $dir $branch" 36 | cd $dir 37 | git checkout $branch 38 | git pull 39 | git branch -D translations || true 40 | git checkout -b translations || true 41 | cd - 42 | done 43 | 44 | # Build Loop 45 | set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' build | xcpretty 46 | 47 | 48 | # Apply translations 49 | foreach file in xliff_in/*.xliff 50 | xcodebuild -workspace LoopWorkspace.xcworkspace -scheme "LoopWorkspace" -importLocalizations -localizationPath $file 51 | end 52 | 53 | 54 | # Generate branches, commit and push. 55 | for project in ${projects}; do 56 | echo "Commiting $project" 57 | IFS=":" read user dir branch <<< "$project" 58 | echo "parts = $user $dir $branch" 59 | cd $dir 60 | git add . 61 | if git commit -am "Updated translations from Lokalise on ${date}"; then 62 | git push -f 63 | pr=$(gh pr create -B $branch -R $user/$dir --fill 2>&1 | grep http) 64 | echo "PR = $pr" 65 | open $pr 66 | fi 67 | cd - 68 | done 69 | 70 | # Reset 71 | #for project in ${projects}; do 72 | # echo "Commiting $project" 73 | # IFS=":" read user dir branch <<< "$project" 74 | # echo "parts = $user $dir $branch" 75 | # cd $dir 76 | # git checkout $branch 77 | # git pull 78 | # cd - 79 | #done 80 | -------------------------------------------------------------------------------- /Scripts/sync.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/swift sh 2 | 3 | // Depends on swift-sh. Install with: `brew install swift-sh` 4 | 5 | import Foundation 6 | import Cocoa 7 | 8 | import AsyncSwiftGit // @bdewey 9 | import OctoKit // nerdishbynature/octokit.swift == main 10 | 11 | let createPRs = true 12 | 13 | guard CommandLine.arguments.count == 3 else { 14 | print("usage: sync.swift ") 15 | exit(1) 16 | } 17 | let pullRequestName = CommandLine.arguments[1] // example: "LOOP-4688 DIY Sync" 18 | let syncBranch = CommandLine.arguments[2] // example: "ps/LOOP-4688/diy-sync" 19 | 20 | enum EnvError: Error { 21 | case missing(String) 22 | } 23 | 24 | func getEnv(_ name: String) throws -> String { 25 | guard let value = ProcessInfo.processInfo.environment[name] else { 26 | throw EnvError.missing(name) 27 | } 28 | return value 29 | } 30 | 31 | let ghUsername = try getEnv("GH_USERNAME") 32 | let ghToken = try getEnv("GH_TOKEN") 33 | let ghCommitterName = try getEnv("GH_COMMITTER_NAME") 34 | let ghCommitterEmail = try getEnv("GH_COMMITTER_EMAIL") 35 | 36 | struct Project { 37 | let project: String 38 | let branch: String 39 | let subdir: String 40 | 41 | init(_ project: String, _ branch: String, _ subdir: String = "") { 42 | self.project = project 43 | self.branch = branch 44 | self.subdir = subdir 45 | } 46 | 47 | var path: String { 48 | if subdir.isEmpty { 49 | return project 50 | } else { 51 | return subdir + "/" + project 52 | } 53 | } 54 | } 55 | 56 | let projects = [ 57 | Project("Loop", "dev"), 58 | Project("LoopKit", "dev"), 59 | Project("TidepoolService", "dev"), 60 | Project("CGMBLEKit", "dev"), 61 | Project("dexcom-share-client-swift", "dev"), 62 | Project("RileyLinkKit", "dev"), 63 | Project("NightscoutService", "dev"), 64 | Project("LoopOnboarding", "dev"), 65 | Project("AmplitudeService", "dev"), 66 | Project("LogglyService", "dev"), 67 | Project("MixpanelService", "main"), 68 | Project("OmniBLE", "dev"), 69 | Project("NightscoutRemoteCGM", "dev"), 70 | Project("LoopSupport", "dev"), 71 | Project("G7SensorKit", "main"), 72 | Project("OmniKit", "main"), 73 | Project("MinimedKit", "main"), 74 | Project("LibreTransmitter", "main") 75 | ] 76 | 77 | let fm = FileManager.default 78 | let loopkit = URL(string: "https://github.com/LoopKit")! 79 | let tidepool = URL(string: "https://github.com/tidepool-org")! 80 | let incomingRemote = "tidepool" 81 | 82 | let octokit = Octokit(TokenConfiguration(ghToken)) 83 | 84 | let credentials = Credentials.plaintext(username: ghUsername, password: ghToken) 85 | let signature = try! Signature(name: ghCommitterName, email: ghCommitterEmail) 86 | 87 | for project in projects { 88 | let dest = URL(string: fm.currentDirectoryPath)!.appendingPathComponent(project.path) 89 | let repository: AsyncSwiftGit.Repository 90 | if !fm.fileExists(atPath: dest.path) { 91 | print("Cloning \(project.project)") 92 | let url = loopkit.appendingPathComponent(project.project) 93 | repository = try await Repository.clone(from: url, to: dest) 94 | print("Cloned \(project.project)") 95 | } else { 96 | print("Already Exists: \(project.path)") 97 | repository = try Repository(openAt: dest) 98 | } 99 | 100 | let incomingRemoteURL = tidepool.appendingPathComponent(project.project) 101 | 102 | // Add remote if it doesn't exist, and fetch latest changes 103 | if (try? repository.remoteURL(for: incomingRemote)) == nil { 104 | try repository.addRemote(incomingRemote, url: incomingRemoteURL) 105 | } 106 | try await repository.fetch(remote: incomingRemote) 107 | 108 | // Create and checkout the branch where sync changesets will go ("tidepool-sync") 109 | if !(try repository.branchExists(named: syncBranch)) { 110 | try repository.createBranch(named: syncBranch, target: "origin/\(project.branch)") 111 | } 112 | try await repository.checkout(revspec: syncBranch) 113 | 114 | // Merge changes from tidepool to diy 115 | try await repository.merge(revisionSpecification: "\(incomingRemote)/\(project.branch)", signature: signature) 116 | 117 | let originTree = try repository.lookupTree(for: "origin/\(project.branch)") 118 | let diff = try repository.diff(originTree, repository.headTree) 119 | 120 | guard diff.count > 0 else { 121 | print("No incoming changes; skipping PR creation.") 122 | try await repository.checkout(revspec: project.branch) 123 | continue 124 | } 125 | print("Found diffs: \(diff)") 126 | 127 | // Push changes up to origin 128 | let refspec = "refs/heads/" + syncBranch + ":refs/heads/" + syncBranch 129 | print("Pushing \(refspec) to \(project.project)") 130 | try await repository.push(remoteName: "origin", refspecs: [refspec], credentials: credentials) 131 | 132 | if createPRs { 133 | // Make sure a PR exists, or create it 134 | 135 | let prs = try await octokit.pullRequests(owner: "LoopKit", repository: project.project, base: project.branch, head:"LoopKit:" + syncBranch) 136 | let pr: PullRequest 137 | if prs.count == 0 { 138 | pr = try await octokit.createPullRequest(owner: "LoopKit", repo: project.project, title: pullRequestName, head: "LoopKit:" + syncBranch, base: project.branch, body: "") 139 | print("PR = \(pr)") 140 | } else { 141 | pr = prs.first! 142 | } 143 | if let url = pr.htmlURL { 144 | if NSWorkspace.shared.open(url) { 145 | print("default browser was successfully opened") 146 | } 147 | } 148 | } else { 149 | print("Skipping PR creation") 150 | } 151 | } 152 | 153 | -------------------------------------------------------------------------------- /Scripts/update_submodule_refs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | projects=(LoopKit:AmplitudeService:dev LoopKit:CGMBLEKit:dev LoopKit:G7SensorKit:main LoopKit:LogglyService:dev LoopKit:Loop:dev LoopKit:LoopKit:dev LoopKit:LoopOnboarding:dev LoopKit:LoopSupport:dev LoopKit:NightscoutRemoteCGM:dev LoopKit:NightscoutService:dev LoopKit:OmniBLE:dev LoopKit:TidepoolService:dev LoopKit:dexcom-share-client-swift:dev ps2:RileyLinkKit:dev LoopKit:OmniKit:main LoopKit:MinimedKit:main LoopKit:LibreTransmitter:main LoopKit:MixpanelService:main) 4 | 5 | for project in ${projects}; do 6 | echo "Updating to $project" 7 | IFS=":" read user dir branch <<< "$project" 8 | echo "Updating to $branch on $user/$project" 9 | cd $dir 10 | git checkout $branch 11 | #git branch -D tidepool-sync 12 | git pull 13 | cd - 14 | done 15 | 16 | -------------------------------------------------------------------------------- /docs/scheme-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopnlearn/LoopWorkspace/8060718e78b44ef45797082817392c1c4b7a7dab/docs/scheme-selection.png -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | default_platform(:ios) 14 | 15 | TEAMID = ENV["TEAMID"] 16 | GH_PAT = ENV["GH_PAT"] 17 | GITHUB_WORKSPACE = ENV["GITHUB_WORKSPACE"] 18 | GITHUB_REPOSITORY_OWNER = ENV["GITHUB_REPOSITORY_OWNER"] 19 | FASTLANE_KEY_ID = ENV["FASTLANE_KEY_ID"] 20 | FASTLANE_ISSUER_ID = ENV["FASTLANE_ISSUER_ID"] 21 | FASTLANE_KEY = ENV["FASTLANE_KEY"] 22 | DEVICE_NAME = ENV["DEVICE_NAME"] 23 | DEVICE_ID = ENV["DEVICE_ID"] 24 | ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120" 25 | 26 | platform :ios do 27 | desc "Build Loop" 28 | lane :build_loop do 29 | setup_ci if ENV['CI'] 30 | 31 | update_project_team( 32 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 33 | teamid: "#{TEAMID}" 34 | ) 35 | 36 | api_key = app_store_connect_api_key( 37 | key_id: "#{FASTLANE_KEY_ID}", 38 | issuer_id: "#{FASTLANE_ISSUER_ID}", 39 | key_content: "#{FASTLANE_KEY}" 40 | ) 41 | 42 | previous_build_number = latest_testflight_build_number( 43 | app_identifier: "com.#{TEAMID}.loopkit.Loop", 44 | api_key: api_key, 45 | ) 46 | 47 | current_build_number = previous_build_number + 1 48 | 49 | increment_build_number( 50 | xcodeproj: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 51 | build_number: current_build_number 52 | ) 53 | 54 | match( 55 | type: "appstore", 56 | git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), 57 | app_identifier: [ 58 | "com.#{TEAMID}.loopkit.Loop", 59 | "com.#{TEAMID}.loopkit.Loop.statuswidget", 60 | "com.#{TEAMID}.loopkit.Loop.LoopWatch.watchkitextension", 61 | "com.#{TEAMID}.loopkit.Loop.LoopWatch", 62 | "com.#{TEAMID}.loopkit.Loop.Loop-Intent-Extension", 63 | "com.#{TEAMID}.loopkit.Loop.LoopWidgetExtension" 64 | ] 65 | ) 66 | 67 | previous_build_number = latest_testflight_build_number( 68 | app_identifier: "com.#{TEAMID}.loopkit.Loop", 69 | api_key: api_key, 70 | ) 71 | 72 | current_build_number = previous_build_number + 1 73 | 74 | increment_build_number( 75 | xcodeproj: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 76 | build_number: current_build_number 77 | ) 78 | 79 | mapping = Actions.lane_context[ 80 | SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING 81 | ] 82 | 83 | update_code_signing_settings( 84 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 85 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop"], 86 | code_sign_identity: "iPhone Distribution", 87 | targets: ["Loop"] 88 | ) 89 | 90 | update_code_signing_settings( 91 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 92 | code_sign_identity: "iPhone Distribution", 93 | targets: ["LoopCore", "LoopCore-watchOS", "LoopUI"] 94 | ) 95 | 96 | update_code_signing_settings( 97 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 98 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.statuswidget"], 99 | code_sign_identity: "iPhone Distribution", 100 | targets: ["Loop Status Extension"] 101 | ) 102 | 103 | update_code_signing_settings( 104 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 105 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.LoopWatch.watchkitextension"], 106 | code_sign_identity: "iPhone Distribution", 107 | targets: ["WatchApp Extension"] 108 | ) 109 | 110 | update_code_signing_settings( 111 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 112 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.LoopWatch"], 113 | code_sign_identity: "iPhone Distribution", 114 | targets: ["WatchApp"] 115 | ) 116 | 117 | update_code_signing_settings( 118 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 119 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.Loop-Intent-Extension"], 120 | code_sign_identity: "iPhone Distribution", 121 | targets: ["Loop Intent Extension"] 122 | ) 123 | 124 | update_code_signing_settings( 125 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj", 126 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.LoopWidgetExtension"], 127 | code_sign_identity: "iPhone Distribution", 128 | targets: ["Loop Widget Extension"] 129 | ) 130 | 131 | gym( 132 | export_method: "app-store", 133 | scheme: "LoopWorkspace", 134 | output_name: "Loop.ipa", 135 | configuration: "Release", 136 | destination: 'generic/platform=iOS', 137 | buildlog_path: 'buildlog' 138 | ) 139 | 140 | copy_artifacts( 141 | target_path: "artifacts", 142 | artifacts: ["*.mobileprovision", "*.ipa", "*.dSYM.zip"] 143 | ) 144 | end 145 | 146 | desc "Push to TestFlight" 147 | lane :release do 148 | api_key = app_store_connect_api_key( 149 | key_id: "#{FASTLANE_KEY_ID}", 150 | issuer_id: "#{FASTLANE_ISSUER_ID}", 151 | key_content: "#{FASTLANE_KEY}" 152 | ) 153 | 154 | upload_to_testflight( 155 | api_key: api_key, 156 | skip_submission: false, 157 | ipa: "Loop.ipa", 158 | skip_waiting_for_build_processing: true, 159 | ) 160 | end 161 | 162 | desc "Provision Identifiers and Certificates" 163 | lane :identifiers do 164 | setup_ci if ENV['CI'] 165 | ENV["MATCH_READONLY"] = false.to_s 166 | 167 | app_store_connect_api_key( 168 | key_id: "#{FASTLANE_KEY_ID}", 169 | issuer_id: "#{FASTLANE_ISSUER_ID}", 170 | key_content: "#{FASTLANE_KEY}" 171 | ) 172 | 173 | def configure_bundle_id(name, identifier, capabilities) 174 | bundle_id = Spaceship::ConnectAPI::BundleId.find(identifier) || Spaceship::ConnectAPI::BundleId.create(name: name, identifier: identifier) 175 | capabilities.each { |capability| 176 | bundle_id.create_capability(capability) 177 | } 178 | end 179 | 180 | configure_bundle_id("Loop", "com.#{TEAMID}.loopkit.Loop", [ 181 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS, 182 | Spaceship::ConnectAPI::BundleIdCapability::Type::HEALTHKIT, 183 | Spaceship::ConnectAPI::BundleIdCapability::Type::PUSH_NOTIFICATIONS, 184 | Spaceship::ConnectAPI::BundleIdCapability::Type::SIRIKIT, 185 | Spaceship::ConnectAPI::BundleIdCapability::Type::USERNOTIFICATIONS_TIMESENSITIVE, 186 | Spaceship::ConnectAPI::BundleIdCapability::Type::NFC_TAG_READING 187 | ]) 188 | 189 | configure_bundle_id("Loop Intent Extension", "com.#{TEAMID}.loopkit.Loop.Loop-Intent-Extension", [ 190 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS 191 | ]) 192 | 193 | configure_bundle_id("Loop Status Extension", "com.#{TEAMID}.loopkit.Loop.statuswidget", [ 194 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS 195 | ]) 196 | 197 | configure_bundle_id("WatchApp", "com.#{TEAMID}.loopkit.Loop.LoopWatch", []) 198 | 199 | configure_bundle_id("WatchApp Extension", "com.#{TEAMID}.loopkit.Loop.LoopWatch.watchkitextension", [ 200 | Spaceship::ConnectAPI::BundleIdCapability::Type::HEALTHKIT, 201 | Spaceship::ConnectAPI::BundleIdCapability::Type::SIRIKIT 202 | ]) 203 | 204 | configure_bundle_id("Loop Widget Extension", "com.#{TEAMID}.loopkit.Loop.LoopWidgetExtension", [ 205 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS 206 | ]) 207 | 208 | end 209 | 210 | desc "Provision Certificates" 211 | lane :certs do 212 | setup_ci if ENV['CI'] 213 | ENV["MATCH_READONLY"] = false.to_s 214 | 215 | app_store_connect_api_key( 216 | key_id: "#{FASTLANE_KEY_ID}", 217 | issuer_id: "#{FASTLANE_ISSUER_ID}", 218 | key_content: "#{FASTLANE_KEY}" 219 | ) 220 | 221 | match( 222 | type: "appstore", 223 | force: true, 224 | git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), 225 | app_identifier: [ 226 | "com.#{TEAMID}.loopkit.Loop", 227 | "com.#{TEAMID}.loopkit.Loop.statuswidget", 228 | "com.#{TEAMID}.loopkit.Loop.LoopWatch.watchkitextension", 229 | "com.#{TEAMID}.loopkit.Loop.LoopWatch", 230 | "com.#{TEAMID}.loopkit.Loop.Loop-Intent-Extension", 231 | "com.#{TEAMID}.loopkit.Loop.LoopWidgetExtension", 232 | ] 233 | ) 234 | end 235 | 236 | desc "Validate Secrets" 237 | lane :validate_secrets do 238 | setup_ci if ENV['CI'] 239 | ENV["MATCH_READONLY"] = true.to_s 240 | 241 | app_store_connect_api_key( 242 | key_id: "#{FASTLANE_KEY_ID}", 243 | issuer_id: "#{FASTLANE_ISSUER_ID}", 244 | key_content: "#{FASTLANE_KEY}" 245 | ) 246 | 247 | def find_bundle_id(identifier) 248 | bundle_id = Spaceship::ConnectAPI::BundleId.find(identifier) 249 | end 250 | 251 | find_bundle_id("com.#{TEAMID}.loopkit.Loop") 252 | 253 | match( 254 | type: "appstore", 255 | git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"), 256 | app_identifier: [], 257 | ) 258 | 259 | end 260 | 261 | desc "Nuke Certs" 262 | lane :nuke_certs do 263 | setup_ci if ENV['CI'] 264 | ENV["MATCH_READONLY"] = false.to_s 265 | 266 | app_store_connect_api_key( 267 | key_id: "#{FASTLANE_KEY_ID}", 268 | issuer_id: "#{FASTLANE_ISSUER_ID}", 269 | key_content: "#{FASTLANE_KEY}" 270 | ) 271 | 272 | match_nuke( 273 | type: "appstore", 274 | team_id: "#{TEAMID}", 275 | skip_confirmation: true, 276 | git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}") 277 | ) 278 | end 279 | end 280 | -------------------------------------------------------------------------------- /fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | 2 | GITHUB_REPOSITORY_OWNER ||= ENV["GITHUB_REPOSITORY_OWNER"] 3 | 4 | git_url("https://github.com/#{GITHUB_REPOSITORY_OWNER}/Match-Secrets.git") 5 | 6 | storage_mode("git") 7 | 8 | type("appstore") 9 | 10 | # The docs are available on https://docs.fastlane.tools/actions/match 11 | -------------------------------------------------------------------------------- /fastlane/testflight.md: -------------------------------------------------------------------------------- 1 | # Using GitHub Actions + FastLane to deploy to TestFlight 2 | 3 | These instructions allow you to build Loop without having access to a Mac. 4 | 5 | * You can install Loop on phones via TestFlight that are not connected to your computer 6 | * You can send builds and updates to those you care for 7 | * You can install Loop on your phone using only the TestFlight app if a phone was lost or the app is accidentally deleted 8 | * You do not need to worry about specific Xcode/Mac versions for a given iOS 9 | 10 | ## **Automatic Builds** 11 | > 12 | > This new version of the browser build **defaults to** automatically updating and building a new version of Loop according to this schedule: 13 | > - automatically checks for updates weekly on Wednesdays and if updates are found, it will build a new version of the app 14 | > - automatically builds once a month regardless of whether there are updates on the first of the month 15 | > - with each scheduled run (weekly or monthly), a successful Build Loop log appears - if the time is very short, it did not need to build - only the long actions (>20 minutes) built a new Loop app 16 | > 17 | > It also creates an alive branch, if you don't already have one. See [Why do I have an alive branch?](#why-do-i-have-an-alive-branch). 18 | > 19 | > The [**Optional**](#optional) section provides instructions to modify the default behavior if desired. 20 | 21 | > **Repeat Builders** 22 | > - to enable automatic build, your `GH_PAT` token must have `workflow` scope 23 | > - if you previously configured your `GH_PAT` without that scope, see [`GH_PAT` `workflow` permission](#gh_pat-workflow-permission) 24 | 25 | ## Introduction 26 | 27 | The setup steps are somewhat involved, but nearly all are one time steps. Subsequent builds are trivial. Your app must be updated once every 90 days, but it's a simple click to make a new build and can be done from anywhere. The 90-day update is a TestFlight requirement, and with this version of Loop, the build process (once you've successfully built once) is automated to update and build at least once a month. 28 | 29 | There are more detailed instructions in LoopDocs for using GitHub for Browser Builds of Loop, including troubleshooting and build errors. Please refer to: 30 | 31 | * [LoopDocs: GitHub Overview](https://loopkit.github.io/loopdocs/gh-actions/gh-overview/) 32 | * [LoopDocs: GitHub Errors](https://loopkit.github.io/loopdocs/gh-actions/gh-errors/) 33 | 34 | Note that installing with TestFlight, (in the US), requires the Apple ID account holder to be 13 years or older. For younger Loopers, an adult must log into Media & Purchase on the child's phone to install Loop. More details on this can be found in [LoopDocs](https://loopkit.github.io/loopdocs/gh-actions/gh-deploy/#install-testflight-loop-for-child). 35 | 36 | ## Prerequisites 37 | 38 | * A [GitHub account](https://github.com/signup). The free level comes with plenty of storage and free compute time to build loop, multiple times a day, if you wanted to. 39 | * A paid [Apple Developer account](https://developer.apple.com). 40 | * Some time. Set aside a couple of hours to perform the setup. 41 | 42 | ## Save 6 Secrets 43 | 44 | You require 6 Secrets (alphanumeric items) to use the GitHub build method and if you use the GitHub method to build more than Loop, e.g., Loop Follow or LoopCaregiver, you will use the same 6 Secrets for each app you build with this method. Each secret is indentified below by `ALL_CAPITAL_LETTER_NAMES`. 45 | 46 | * Four Secrets are from your Apple Account 47 | * Two Secrets are from your GitHub account 48 | * Be sure to save the 6 Secrets in a text file using a text editor 49 | - Do **NOT** use a smart editor, which might auto-correct and change case, because these Secrets are case sensitive 50 | 51 | ## Generate App Store Connect API Key 52 | 53 | This step is common for all GitHub Browser Builds; do this step only once. You will be saving 4 Secrets from your Apple Account in this step. 54 | 55 | 1. Sign in to the [Apple developer portal page](https://developer.apple.com/account/resources/certificates/list). 56 | 1. Copy the Team ID from the upper right of the screen. Record this as your `TEAMID`. 57 | 1. Go to the [App Store Connect](https://appstoreconnect.apple.com/access/integrations/api) interface, click the "Integrations" tab, and create a new key with "Admin" access. Give it the name: "FastLane API Key". 58 | 1. Record the issuer id; this will be used for `FASTLANE_ISSUER_ID`. 59 | 1. Record the key id; this will be used for `FASTLANE_KEY_ID`. 60 | 1. Download the API key itself, and open it in a text editor. The contents of this file will be used for `FASTLANE_KEY`. Copy the full text, including the "-----BEGIN PRIVATE KEY-----" and "-----END PRIVATE KEY-----" lines. 61 | 62 | ## Create GitHub Personal Access Token 63 | 64 | Log into your GitHub account to create a personal access token; this is one of two GitHub secrets needed for your build. 65 | 66 | 1. Create a [new personal access token](https://github.com/settings/tokens/new): 67 | * Enter a name for your token, use "FastLane Access Token". 68 | * Change the Expiration selection to `No expiration`. 69 | * Select the `workflow` permission scope - this also selects `repo` scope. 70 | * Click "Generate token". 71 | * Copy the token and record it. It will be used below as `GH_PAT`. 72 | 73 | ## Make up a Password 74 | 75 | This is the second one of two GitHub secrets needed for your build. 76 | 77 | The first time you build with the GitHub Browser Build method for any DIY app, you will make up a password and record it as `MATCH_PASSWORD`. Note, if you later lose `MATCH_PASSWORD`, you will need to delete and make a new Match-Secrets repository (next step). 78 | 79 | ## Setup GitHub Match-Secrets Repository 80 | 81 | The creation of the Match-Secrets repository is a common step for all GitHub Browser Builds; do this step only once. You must be logged into your GitHub account. 82 | 83 | 1. Create a [new empty repository](https://github.com/new) titled `Match-Secrets`. It should be private. 84 | 85 | Once created, you will not take any direct actions with this repository; it needs to be there for the GitHub to use as you progress through the steps. 86 | 87 | ## Setup GitHub LoopWorkspace Repository 88 | 89 | 1. Fork https://github.com/LoopKit/LoopWorkspace into your account. 90 | 1. In the forked LoopWorkspace repo, go to Settings -> Secrets and variables -> Actions. 91 | 1. For each of the following secrets, tap on "New repository secret", then add the name of the secret, along with the value you recorded for it: 92 | * `TEAMID` 93 | * `FASTLANE_ISSUER_ID` 94 | * `FASTLANE_KEY_ID` 95 | * `FASTLANE_KEY` 96 | * `GH_PAT` 97 | * `MATCH_PASSWORD` 98 | 99 | ## Validate repository secrets 100 | 101 | This step validates most of your six Secrets and provides error messages if it detects an issue with one or more. 102 | 103 | 1. Click on the "Actions" tab of your LoopWorkspace repository and enable workflows if needed 104 | 1. On the left side, select "1. Validate Secrets". 105 | 1. On the right side, click "Run Workflow", and tap the green `Run workflow` button. 106 | 1. Wait, and within a minute or two you should see a green checkmark indicating the workflow succeeded. 107 | 1. The workflow will check if the required secrets are added and that they are correctly formatted. If errors are detected, please check the run log for details. 108 | 109 | ## Add Identifiers for Loop App 110 | 111 | 1. Click on the "Actions" tab of your LoopWorkspace repository. 112 | 1. On the left side, select "2. Add Identifiers". 113 | 1. On the right side, click "Run Workflow", and tap the green `Run workflow` button. 114 | 1. Wait, and within a minute or two you should see a green checkmark indicating the workflow succeeded. 115 | 116 | ## Create App Group 117 | 118 | If you have already built Loop via Xcode using this Apple ID, you can skip on to [Add App Group to Bundle Identifiers](#add-app-group-to-bundle-identifiers). 119 | 120 | 1. Go to [Register an App Group](https://developer.apple.com/account/resources/identifiers/applicationGroup/add/) on the apple developer site. 121 | 1. For Description, use "Loop App Group". 122 | 1. For Identifier, enter "group.com.TEAMID.loopkit.LoopGroup", subsituting your team id for `TEAMID`. 123 | 1. Click "Continue" and then "Register". 124 | 125 | ## Add App Group to Bundle Identifiers 126 | 127 | Note 1 - If you previously built with Xcode, the `Names` listed below may be different, but the `Identifiers` will match. A table is provided below the steps to assist. The Add Identifier Action that you completed above generates 6 identifiers, but only 4 need to be modified as indicated in this step. 128 | 129 | Note 2 - Depending on your build history, you may find some of the Identifiers are already configured - and you are just verifying the status; but in other cases, you will need to configure the Identifiers. 130 | 131 | 1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) on the apple developer site. 132 | 1. For each of the following identifier names: 133 | * Loop 134 | * Loop Intent Extension 135 | * Loop Status Extension 136 | * Loop Widget Extension 137 | 1. Click on the identifier's name. 138 | 1. On the "App Groups" capabilies, click on the "Configure" button. 139 | 1. Select the "Loop App Group" 140 | 1. Click "Continue". 141 | 1. Click "Save". 142 | 1. Click "Confirm". 143 | 1. Remember to do this for each of the identifiers above. 144 | 145 | #### Table with Name and Identifier for Loop 3 146 | 147 | | NAME | IDENTIFIER | 148 | |-------|------------| 149 | | Loop | com.TEAMID.loopkit.Loop | 150 | | Loop Intent Extension | com.TEAMID.loopkit.Loop.Loop-Intent-Extension | 151 | | Loop Status Extension | com.TEAMID.loopkit.Loop.statuswidget | 152 | | Loop Widget Extension | com.TEAMID.loopkit.Loop.LoopWidgetExtension | 153 | | WatchApp | com.TEAMID.loopkit.Loop.LoopWatch | 154 | | WatchAppExtension | com.TEAMID.loopkit.Loop.LoopWatch.watchkitextension | 155 | 156 | 157 | ## Create Loop App in App Store Connect 158 | 159 | If you have created a Loop app in App Store Connect before, you can skip this section. 160 | 161 | 1. Go to the [apps list](https://appstoreconnect.apple.com/apps) on App Store Connect and click the blue "plus" icon to create a New App. 162 | * Select "iOS". 163 | * Select a name: this will have to be unique, so you may have to try a few different names here, but it will not be the name you see on your phone, so it's not that important. 164 | * Select your primary language. 165 | * Choose the bundle ID that matches `com.TEAMID.loopkit.Loop`, with TEAMID matching your team id. 166 | * SKU can be anything; e.g. "123". 167 | * Select "Full Access". 168 | 1. Click Create 169 | 170 | You do not need to fill out the next form. That is for submitting to the app store. 171 | 172 | ## Create Building Certificates 173 | 174 | 1. Go back to the "Actions" tab of your LoopWorkspace repository in GitHub. 175 | 1. On the left side, select "3. Create Certificates". 176 | 1. On the right side, click "Run Workflow", and tap the green `Run workflow` button. 177 | 1. Wait, and within a minute or two you should see a green checkmark indicating the workflow succeeded. 178 | 179 | ## Build Loop 180 | 181 | 1. Click on the "Actions" tab of your LoopWorkspace repository. 182 | 1. On the left side, select "4. Build Loop". 183 | 1. On the right side, click "Run Workflow", and tap the green `Run workflow` button. 184 | 1. You have some time now. Go enjoy a coffee. The build should take about 20-30 minutes. 185 | 1. Your app should eventually appear on [App Store Connect](https://appstoreconnect.apple.com/apps). 186 | 1. For each phone/person you would like to support Loop on: 187 | * Add them in [Users and Access](https://appstoreconnect.apple.com/access/users) on App Store Connect. 188 | * Add them to your TestFlight Internal Testing group. 189 | 190 | ## TestFlight and Deployment Details 191 | 192 | Please refer to [LoopDocs: Set Up Users](https://loopkit.github.io/loopdocs/gh-actions/gh-first-time/#set-up-users-and-access-testflight) and [LoopDocs: Deploy](https://loopkit.github.io/loopdocs/gh-actions/gh-deploy/) 193 | 194 | ## Automatic Build FAQs 195 | 196 | ### Why do I have an `alive` branch? 197 | 198 | If a GitHub repository has no activity (no commits are made) in 60 days, then GitHub disables the ability to use automated actions for that repository. We need to take action more frequently than that or the automated build process won't work. 199 | 200 | The updated `build_loop.yml` file uses a special branch called `alive` and adds a dummy commit to the `alive` branch at regular intervals. This "trick" keeps the Actions enabled so the automated build works. 201 | 202 | The branch `alive` is created automatically for you. Do not delete or rename it! Do not modify `alive` yourself; it is not used for building the app. 203 | 204 | ## OPTIONAL 205 | 206 | What if you don't want to allow automated updates of the repository or automatic builds? 207 | 208 | You can affect the default behavior: 209 | 210 | 1. [`GH_PAT` `workflow` permission](#gh_pat-workflow-permission) 211 | 1. [Modify scheduled building and synchronization](#modify-scheduled-building-and-synchronization) 212 | 213 | ### `GH_PAT` `workflow` permission 214 | 215 | To enable the scheduled build and sync, the `GH_PAT` must hold the `workflow` permission scopes. This permission serves as the enabler for automatic and scheduled builds with browser build. To verify your token holds this permission, follow these steps. 216 | 217 | 1. Go to your [FastLane Access Token](https://github.com/settings/tokens) 218 | 2. It should say `repo`, `workflow` next to the `FastLane Access Token` link 219 | 3. If it does not, click on the link to open the token detail view 220 | 4. Click to check the `workflow` box. You will see that the checked boxes for the `repo` scope become disabled (change color to dark gray and are not clickable) 221 | 5. Scroll all the way down to and click the green `Update token` button 222 | 6. Your token now holds both required permissions 223 | 224 | If you choose not to have automatic building enabled, be sure the `GH_PAT` has `repo` scope or you won't be able to manually build. 225 | 226 | ### Modify scheduled building and synchronization 227 | 228 | You can modify the automation by creating and using some variables. 229 | 230 | To configure the automated build more granularly involves creating up to two environment variables: `SCHEDULED_BUILD` and/or `SCHEDULED_SYNC`. See [How to configure a variable](#how-to-configure-a-variable). 231 | 232 | Note that the weekly and monthly Build Loop actions will continue, but the actions are modified if one or more of these variables is set to false. **A successful Action Log will still appear, even if no automatic activity happens**. 233 | 234 | * If you want to manually decide when to update your repository to the latest commit, but you want the monthly builds and keep-alive to continue: set `SCHEDULED_SYNC` to false and either do not create `SCHEDULED_BUILD` or set it to true 235 | * If you want to only build when an update has been found: set `SCHEDULED_BUILD` to false and either do not create `SCHEDULED_SYNC` or set it to true 236 | * **Warning**: if no updates to your default branch are detected within 90 days, your previous TestFlight build may expire requiring a manual build 237 | 238 | |`SCHEDULED_SYNC`|`SCHEDULED_BUILD`|Automatic Actions| 239 | |---|---|---| 240 | | `true` (or NA) | `true` (or NA) | keep-alive, weekly update check (auto update/build), monthly build with auto update| 241 | | `true` (or NA) | `false` | keep-alive, weekly update check with auto update, only builds if update detected| 242 | | `false` | `true` (or NA) | keep-alive, monthly build, no auto update | 243 | | `false` | `false` | no automatic activity, no keep-alive| 244 | 245 | ### How to configure a variable 246 | 247 | 1. Go to the "Settings" tab of your LoopWorkspace repository. 248 | 2. Click on `Secrets and Variables`. 249 | 3. Click on `Actions` 250 | 4. You will now see a page titled *Actions secrets and variables*. Click on the `Variables` tab 251 | 5. To disable ONLY scheduled building, do the following: 252 | - Click on the green `New repository variable` button (upper right) 253 | - Type `SCHEDULED_BUILD` in the "Name" field 254 | - Type `false` in the "Value" field 255 | - Click the green `Add variable` button to save. 256 | 7. To disable scheduled syncing, add a variable: 257 | - Click on the green `New repository variable` button (upper right) 258 | - - Type `SCHEDULED_SYNC` in the "Name" field 259 | - Type `false` in the "Value" field 260 | - Click the green `Add variable` button to save 261 | 262 | Your build will run on the following conditions: 263 | - Default behaviour: 264 | - Run weekly, every Wednesday at 08:00 UTC to check for changes; if there are changes, it will update your repository and build 265 | - Run monthly, every first of the month at 06:00 UTC, if there are changes, it will update your repository; regardless of changes, it will build 266 | - Each time the action runs, it makes a keep-alive commit to the `alive` branch if necessary 267 | - If you disable any automation (both variables set to `false`), no updates, keep-alive or building happens when Build Loop runs 268 | - If you disabled just scheduled synchronization (`SCHEDULED_SYNC` set to`false`), it will only run once a month, on the first of the month, no update will happen; keep-alive will run 269 | - If you disabled just scheduled build (`SCHEDULED_BUILD` set to`false`), it will run once weekly, every Wednesday, to check for changes; if there are changes, it will update and build; keep-alive will run 270 | -------------------------------------------------------------------------------- /patches/save_patches_here.md: -------------------------------------------------------------------------------- 1 | LoopWorkspace-level patches can be saved in this directory (LoopWorkspace/patches/) 2 | --------------------------------------------------------------------------------