├── InfoCustomizations.txt
├── docs
└── scheme-selection.png
├── patches
└── save_patches_here.md
├── OverrideAssetsLoop.xcassets
├── Contents.json
└── AppIcon.appiconset
│ ├── Icon.png
│ ├── icon_20pt.png
│ ├── icon_29pt.png
│ ├── icon_40pt.png
│ ├── icon_76pt.png
│ ├── icon_20pt@2x.png
│ ├── icon_20pt@3x.png
│ ├── icon_29pt@2x.png
│ ├── icon_29pt@3x.png
│ ├── icon_40pt@2x.png
│ ├── icon_40pt@3x.png
│ ├── icon_60pt@2x.png
│ ├── icon_60pt@3x.png
│ ├── icon_76pt@2x.png
│ ├── icon_83.5@2x.png
│ ├── icon_20pt@2x-1.png
│ ├── icon_29pt@2x-1.png
│ ├── icon_40pt@2x-1.png
│ └── Contents.json
├── OverrideAssetsWatchApp.xcassets
├── Contents.json
└── AppIcon.appiconset
│ ├── Icon.png
│ ├── icon_108pt@2x.png
│ ├── icon_24pt@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
│ ├── icon_27.5pt@2x.png
│ └── Contents.json
├── Gemfile
├── fastlane
├── Matchfile
├── Fastfile
└── testflight.md
├── LoopWorkspace.xcworkspace
├── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ ├── WorkspaceSettings.xcsettings
│ ├── swiftpm
│ │ └── Package.resolved
│ └── xcschemes
│ │ └── LoopWorkspace.xcscheme
└── contents.xcworkspacedata
├── VersionOverride.xcconfig
├── Scripts
├── update_submodule_refs.sh
├── export_localizations.sh
├── manual_export_localizations.sh
├── archive_translations.sh
├── manual_review_translations.sh
├── manual_upload_to_lokalise.sh
├── manual_finalize_translations.sh
├── manual_download_from_lokalise.sh
├── manual_cleanup.sh
├── manual_LoopWorkspace_prepare_pr.sh
├── import_localizations.sh
├── manual_import_localizations.sh
├── define_common.sh
├── sync.swift
└── LocalizationInstructions.md
├── .gitignore
├── LoopConfigOverride.xcconfig
├── .circleci
└── config.yml
├── README.md
├── .github
└── workflows
│ ├── add_identifiers.yml
│ ├── create_certs.yml
│ ├── validate_secrets.yml
│ └── build_loop.yml
├── .gitmodules
└── Gemfile.lock
/InfoCustomizations.txt:
--------------------------------------------------------------------------------
1 | TidepoolServiceClientId=diy-loop
2 |
--------------------------------------------------------------------------------
/docs/scheme-selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/docs/scheme-selection.png
--------------------------------------------------------------------------------
/patches/save_patches_here.md:
--------------------------------------------------------------------------------
1 | LoopWorkspace-level patches can be saved in this directory (LoopWorkspace/patches/)
2 |
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@3x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@3x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@3x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_60pt@3x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_76pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_83.5@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png
--------------------------------------------------------------------------------
/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsLoop.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_108pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_108pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_24pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_24pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_29pt@3x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_40pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_40pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_44pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_44pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_50pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_50pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_86pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_86pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_98pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_98pt@2x.png
--------------------------------------------------------------------------------
/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_27.5pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoopKit/LoopWorkspace/HEAD/OverrideAssetsWatchApp.xcassets/AppIcon.appiconset/icon_27.5pt@2x.png
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # gem "fastlane"
4 |
5 | # This branch uses fastlane 2.228.0 plus pr 29596
6 | gem "fastlane", git: "https://github.com/loopandlearn/fastlane.git", ref: "a670d4b092b274d58ebb5497126e47fc6a84f533"
7 | gem "rexml", ">=3.4.2"
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LoopWorkspace.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/VersionOverride.xcconfig:
--------------------------------------------------------------------------------
1 | //
2 | // VersionOverride.xcconfig
3 | // LoopWorkspace
4 | //
5 | // Created 3/31/2025
6 | // Copyright © 2020 LoopKit Authors. All rights reserved.
7 | //
8 |
9 | // Version [for DIY Loop]
10 | // configure the version number in LoopWorkspace
11 | LOOP_MARKETING_VERSION = 3.8.2
12 | CURRENT_PROJECT_VERSION = 57
13 |
--------------------------------------------------------------------------------
/Scripts/update_submodule_refs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | source Scripts/define_common.sh
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 |
--------------------------------------------------------------------------------
/LoopWorkspace.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 | PreviewsEnabled
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/Scripts/manual_export_localizations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # This script creates the xliff files suitable to upload to lokalise
4 |
5 | # You must be in the LoopWorkspace folder before executing with:
6 | # ./Scripts/manual_export_localizations.sh
7 |
8 | set -e
9 | set -u
10 |
11 | source Scripts/define_common.sh
12 |
13 | argstring="${LANGUAGES[@]/#/-exportLanguage }"
14 | IFS=" "; args=( $=argstring )
15 |
16 | xcodebuild -scheme LoopWorkspace -exportLocalizations -localizationPath xclocs $args
17 |
18 | mkdir -p xliff_out
19 | find xclocs -name '*.xliff' -exec cp {} xliff_out \;
20 |
21 | echo ""
22 | echo "Next step is to upload the xliff_out files to lokalise with"
23 | echo "./Scripts/manual_upload_to_lokalise.sh"
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | jobs:
2 | build_and_test:
3 | macos:
4 | xcode: 16.4
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 16,OS=18.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 16,OS=18.5' test | xcpretty
16 | workflows:
17 | version: 2
18 | build_and_test:
19 | jobs:
20 | - build_and_test
21 |
--------------------------------------------------------------------------------
/Scripts/archive_translations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # archive previously created translation branches as a "reset" action
4 | # you can edit branch names in Scripts/define_common.sh prior to running
5 |
6 | set -e
7 | set -u
8 |
9 | source Scripts/define_common.sh
10 |
11 | # use a common message with the time at which xliff files were downloaded from lokalise
12 | if [[ -e "${MESSAGE_FILE}" ]]; then
13 | message_string=$(<"${MESSAGE_FILE}")
14 | else
15 | message_string="message not defined"
16 | fi
17 | echo "message_string = ${message_string}"
18 |
19 | for project in ${PROJECTS}; do
20 | echo "Archive ${TRANSLATION_BRANCH} branch for $project"
21 | IFS=":" read user dir branch <<< "$project"
22 | echo "parts = $user $dir $branch"
23 | cd $dir
24 | if git switch ${TRANSLATION_BRANCH}; then
25 | echo "in $dir, configure $ARCHIVE_BRANCH"
26 | git branch -D ${ARCHIVE_BRANCH} || true
27 | git switch -c ${ARCHIVE_BRANCH}
28 | git add .
29 | if git commit -m "${message_string}"; then
30 | echo "updated $dir with ${message_string} in ${ARCHIVE_BRANCH} branch"
31 | fi
32 | git branch -D ${TRANSLATION_BRANCH}
33 | fi
34 | cd -
35 | done
36 |
37 | git submodule update
38 | git status
39 |
40 | echo "You may need to manually clean branches not in the project list"
41 |
--------------------------------------------------------------------------------
/Scripts/manual_review_translations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # This script assists in reviewing translations for each submodule after running
4 | # ./Scripts/manual_import_localizations.sh
5 | # and before running
6 | # ./Scripts/manual_finalize_translations.sh
7 | # You must be in the LoopWorkspace folder
8 |
9 | set -e
10 | set -u
11 |
12 | source Scripts/define_common.sh
13 |
14 | NO_CHANGES="nothing to commit"
15 |
16 | section_divider
17 | echo "You are running ${0}"
18 | echo " Each submodule will have 'git status' displayed for the '${TRANSLATION_BRANCH}' branch"
19 | echo " Use a separate terminal in the submodule folder if you want to make adjustments"
20 |
21 | continue_or_quit ${0}
22 |
23 | for project in ${PROJECTS}; do
24 | section_divider
25 | IFS=":" read user dir branch <<< "$project"
26 | cd $dir
27 | current_branch=$(git branch --show-current 2>/dev/null)
28 | if [[ "${current_branch}" == "${TRANSLATION_BRANCH}" ]]; then
29 | echo "Review diffs for $dir"
30 | result=$(git status)
31 | echo "${result}"
32 | folder_path="${PWD}"
33 | echo ""
34 | echo "This folder is $folder_path"
35 | if [[ ${result} == *"$NO_CHANGES"* ]]; then
36 | cd -
37 | continue
38 | fi
39 | section_divider
40 | echo " Hit return when ready to continue"
41 | read query
42 | else
43 | echo " $dir does not have a ${TRANSLATION_BRANCH} branch"
44 | fi
45 | cd -
46 | done
47 |
48 | section_divider
49 | echo "Done reviewing diffs by submodule"
50 | echo
51 | echo "Next step is to create/update PRs for each modified submodule by executing"
52 | next_script "./Scripts/manual_finalize_translations.sh"
53 | section_divider
54 |
--------------------------------------------------------------------------------
/Scripts/manual_upload_to_lokalise.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # This script will upload the xliff files from LoopWorkspace and submodules to lokalise
4 |
5 | # Install the lokalise command line tools from https://github.com/lokalise/lokalise-cli-2-go
6 | # Generate an API Token (not an SDK Token!) following the instructions here: https://docs.lokalise.com/en/articles/1929556-api-tokens
7 | # export LOKALISE_TOKEN=""
8 |
9 | # The token must have read/write access or this script will fail
10 |
11 | # This script should be run first:
12 | # ./Scripts/manual_export_localizations.sh
13 |
14 | # You must be in the LoopWorkspace folder before executing with:
15 | # ./Scripts/manual_upload_to_lokalise.sh
16 |
17 | set -e
18 | set -u
19 |
20 | : "$LOKALISE_TOKEN"
21 |
22 | source Scripts/define_common.sh
23 |
24 | section_divider
25 | echo "You are running ${0}"
26 | echo " It will upload an xliff file for each language to lokalise"
27 | echo " from the xliff_out folder created by manual_export_localizations."
28 | echo
29 | echo " Each uploaded file will be queued and processed"
30 |
31 | continue_or_quit ${0}
32 |
33 | cd xliff_out
34 |
35 | foreach lang in $LANGUAGES
36 |
37 | # modify the hyphen to underscore to support lokalise lang-iso expectation
38 | lang_iso=$(sed "s/zh-Hans/zh_Hans/g; s/pt-BR/pt_BR/g" <<<"$lang")
39 |
40 | lokalise2 \
41 | --token $LOKALISE_TOKEN \
42 | --convert-placeholders=false \
43 | --project-id 414338966417c70d7055e2.75119857 \
44 | file upload \
45 | --file ${lang}.xliff \
46 | --cleanup-mode \
47 | --lang-iso ${lang_iso}
48 | end
49 |
50 | section_divider
51 | echo "Reminder: At lokalise, wait until all uploaded files are processed"
52 | section_divider
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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-15
16 | steps:
17 | # Checks-out the repo
18 | - name: Checkout Repo
19 | uses: actions/checkout@v4
20 |
21 | # Patch Fastlane Match to not print tables
22 | - name: Patch Match Tables
23 | run: |
24 | TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb
25 | if [ -f "$TABLE_PRINTER_PATH" ]; then
26 | sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH"
27 | else
28 | echo "table_printer.rb not found"
29 | exit 1
30 | fi
31 |
32 | # Install project dependencies
33 | - name: Install Project Dependencies
34 | run: bundle install
35 |
36 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
37 | - name: Sync clock
38 | run: sudo sntp -sS time.windows.com
39 |
40 | # Create or update identifiers for app
41 | - name: Fastlane Provision
42 | run: bundle exec fastlane identifiers
43 | env:
44 | TEAMID: ${{ secrets.TEAMID }}
45 | GH_PAT: ${{ secrets.GH_PAT }}
46 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
47 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
48 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
49 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
50 |
--------------------------------------------------------------------------------
/Scripts/manual_finalize_translations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | set -e
4 | set -u
5 |
6 | # this script commits the changes to translations branch, pushes and opens PR
7 |
8 | source Scripts/define_common.sh
9 |
10 | section_divider
11 | echo "You are running ${0}"
12 | echo " All differences for submodule files, including untracked files, will be committed."
13 | echo " If you did not just review all the changes, quit, execute command below and come back"
14 | next_script "./Scripts/manual_review_translations.sh"
15 |
16 | continue_or_quit ${0}
17 |
18 | LOOPKIT_USER="LoopKit"
19 |
20 | for project in ${PROJECTS}; do
21 | echo "Committing updates to $project"
22 | IFS=":" read user dir branch <<< "$project"
23 | cd $dir
24 | git add .
25 | # skip repositories with no changes
26 | if git commit -F "../${MESSAGE_FILE}"; then
27 | git push --set-upstream origin ${TRANSLATION_BRANCH}
28 | # Only open PR if the owner is LoopKit
29 | # the loopandlearn branch should be created or updated
30 | # then manually create the PR to the source repository
31 | if [[ ${user} == ${LOOPKIT_USER} ]]; then
32 | # If PR already exists, this just opens it
33 | pr=$(gh pr create -B $branch -R $user/$dir --fill 2>&1 | grep http)
34 | echo "PR = $pr"
35 | open $pr
36 | else
37 | echo "Automatic PR creation is only provided for LoopKit"
38 | echo " The branch ${TRANSLATION_BRANCH} was created or updated at $user/$dir"
39 | echo " Create the appropriate PR to the source repository"
40 | echo " After that PR is approved and merged, then sync $user/$dir"
41 | fi
42 | fi
43 | cd -
44 | done
45 |
46 | section_divider
47 | echo "Review and get approvals for the submodule PRs"
48 | echo "Once all are merged, then create/update the LoopWorkspace PR"
49 | section_divider
--------------------------------------------------------------------------------
/Scripts/manual_download_from_lokalise.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # This script will import the latest translations from lokalise and
4 | # generate a standard commit message for subsequent pull requires
5 |
6 | # Install the lokalise command line tools from https://github.com/lokalise/lokalise-cli-2-go
7 | # Generate an API Token (not an SDK Token!) following the instructions here: https://docs.lokalise.com/en/articles/1929556-api-tokens
8 | # export LOKALISE_TOKEN=""
9 |
10 | # You must be in the LoopWorkspace folder before executing with:
11 | # ./Scripts/manual_download_from_lokalise.sh
12 |
13 | set -e
14 | set -u
15 |
16 | : "$LOKALISE_TOKEN"
17 |
18 | date=`date`
19 |
20 | source Scripts/define_common.sh
21 |
22 | section_divider
23 | echo "You are running ${0}"
24 | echo " This requests localization files from lokalise"
25 |
26 | # Fetch translations from lokalise
27 | rm -rf xliff_in
28 | lokalise2 \
29 | --token "$LOKALISE_TOKEN" \
30 | --project-id "414338966417c70d7055e2.75119857" \
31 | file download \
32 | --async\
33 | --format xliff \
34 | --bundle-structure "%LANG_ISO%.%FORMAT%" \
35 | --original-filenames=false \
36 | --placeholder-format ios \
37 | --export-empty-as skip \
38 | --replace-breaks=false \
39 | --unzip-to ./xliff_in
40 |
41 | # create xlate_pr_title.txt using the date of the import from localize
42 | # this overwrites any existing file because we want to capture the date of the latest download
43 |
44 | section_divider
45 | echo "Updated translations from lokalise on ${date}" > "${MESSAGE_FILE}"
46 | echo "The standard translation commit message is stored in ${MESSAGE_FILE}"
47 |
48 | section_divider
49 | echo "To import from the xliff_in folder for each submodule, execute"
50 | echo "./Scripts/manual_import_localizations.sh"
51 | echo
52 | echo "If you prefer to use a path other than '${DEFAULT_TRANSLATION_BRANCH}',"
53 | echo " add that as the first argument on the import script"
54 | section_divider
55 |
--------------------------------------------------------------------------------
/Scripts/manual_cleanup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # This script deletes temporary files and directories created during the translation process
4 | # You must be in the LoopWorkspace folder
5 |
6 | # ensure you really want to do this before executing with:
7 | # ./Scripts/manual_cleanup.sh
8 |
9 | set -e
10 | set -u
11 |
12 | source Scripts/define_common.sh
13 |
14 | section_divider
15 | echo "You are running ${0}"
16 | echo " Be sure you are completely done with the translation process"
17 | echo " or that you want to discard all your work to date"
18 | echo
19 | echo " /////////// WARNING ///////////"
20 | echo
21 | echo " This deletes the xclocs, xliff_in, xliff_out folders"
22 | echo " This deletes the file, ${MESSAGE_FILE}, with the lokalise download timestamp"
23 | echo " This restores all submodules to their current branch (reset, clean)"
24 | echo " If '${TRANSLATION_BRANCH}' branch exists and submodule is NOT on that branch:"
25 | echo " then '${TRANSLATION_BRANCH}' branch is deleted"
26 |
27 | continue_or_quit ${0}
28 |
29 | rm -rf xclocs
30 | rm -rf xliff_in
31 | rm -rf xliff_out
32 | rm -f "${MESSAGE_FILE}"
33 |
34 | for project in ${PROJECTS}; do
35 | IFS=":" read user dir branch <<< "$project"
36 | echo
37 | echo " *** Reset and clean $dir"
38 | cd $dir
39 | git reset --hard; git clean -fd;
40 | current_branch=$(git branch --show-current 2>/dev/null)
41 | if [[ "${current_branch}" == "${TRANSLATION_BRANCH}" ]]; then
42 | echo " already on $TRANSLATION_BRANCH, take no action"
43 | elif [ -n "$(git branch --list "$TRANSLATION_BRANCH")" ]; then
44 | echo " Local branch '$TRANSLATION_BRANCH' exists, deleting it."
45 | git branch -D "${TRANSLATION_BRANCH}"
46 | else
47 | echo " no branch named $TRANSLATION_BRANCH exists, take no action"
48 | fi
49 | cd -
50 | done
51 |
52 |
53 | section_divider
54 | echo "Temporary folders and ${MESSAGE_FILE} removed from LoopWorkspace"
55 | echo "All folders in PROJECTS reset and cleaned"
56 | section_divider
57 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/Scripts/manual_LoopWorkspace_prepare_pr.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | set -e
4 | set -u
5 |
6 | # this script prepares a PR for LoopWorkspace based on current local branch
7 |
8 | source Scripts/define_common.sh
9 |
10 | section_divider
11 | echo "You are running ${0}"
12 | echo " You must be in the LoopWorkspace folder ready to commit changes to"
13 | echo " tracked files in this clone, see 'git status' results below:"
14 | section_divider
15 | git status
16 | section_divider
17 | echo "This script will prepare a PR to LoopWorkspace '${TARGET_LOOPWORKSPACE_BRANCH}' branch"
18 | echo
19 | echo "1. If the local clone LoopWorkspace branch name is not already '${TRANSLATION_BRANCH}', then"
20 | echo " that branch will be created and used for this PR"
21 | echo "2. The commit message in the ${MESSAGE_FILE} will be used"
22 | cat ${MESSAGE_FILE}
23 | echo "3. Once the PR is prepared, additional commits can be added as needed"
24 |
25 | continue_or_quit ${0}
26 |
27 | current_branch=$(git branch --show-current 2>/dev/null)
28 | echo "current_branch = $current_branch"
29 |
30 | if [[ "${current_branch}" == "${TRANSLATION_BRANCH}" ]]; then
31 | echo "already on $TRANSLATION_BRANCH, ok to continue"
32 |
33 | elif [ -n "$(git branch --list "$TRANSLATION_BRANCH")" ]; then
34 | echo "Local branch '$TRANSLATION_BRANCH' exists."
35 | echo "You are on '$current_branch' and '$TRANSLATION_BRANCH' already exists"
36 | echo "quitting"
37 | exit 1 # exit with failure
38 |
39 | else
40 | echo "Local branch $TRANSLATION_BRANCH does not exist,"
41 | echo "creating $TRANSLATION_BRANCH from the current branch, $current_branch."
42 | git switch -c "${TRANSLATION_BRANCH}"
43 | fi
44 |
45 | continue_or_quit ${0}
46 |
47 | # only create a PR if there are changes
48 | if git commit -a -F "${MESSAGE_FILE}"; then
49 | git push --set-upstream origin ${TRANSLATION_BRANCH}
50 | pr=$(gh pr create -B ${TARGET_LOOPWORKSPACE_BRANCH} --fill 2>&1 | grep http)
51 | echo "PR = $pr"
52 | open $pr
53 |
54 | section_divider
55 | echo "After you review, ${pr}, get approvals and merge the PR"
56 | echo " be sure to trim the '${TRANSLATION_BRANCH}' branch,"
57 | echo " and then run the export and upload scripts again from the updated '${TARGET_LOOPWORKSPACE_BRANCH}' branch"
58 | section_divider
59 |
60 | else
61 | section_divider
62 | echo "No changes were found, no PR created"
63 | section_divider
64 | fi
65 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/Scripts/manual_import_localizations.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # This script imports localizations from xliff files into the users local clone of LoopWorkspace
4 | # You must be in the LoopWorkspace folder
5 |
6 | # Fetch translations from lokalise before running this script
7 | # ./Scripts/manual_download_from_lokalise.sh
8 |
9 | # Then execute script:
10 | # ./Scripts/manual_import_localizations.sh
11 |
12 | set -e
13 | set -u
14 |
15 | source Scripts/define_common.sh
16 |
17 | section_divider
18 | echo "You are running ${0}"
19 | echo " You must be in the LoopWorkspace folder ready to bring in "
20 | echo " localizations from the xliff_in files downloaded from lokalise."
21 | echo
22 | echo "All submodules will use '${TRANSLATION_BRANCH}' as the branch name:"
23 | echo " If that branch does not exist, it will be created from current submodule branch."
24 | echo " If that branch exists, it will continue to be used."
25 | echo
26 | echo "You are responsible for configuring your clone before running ${0}."
27 | echo " Typically, you run ./Scripts/update_submodule_refs.sh before using this script."
28 | echo " You can also update in-progress submodule localization using '${TRANSLATION_BRANCH}'."
29 | echo
30 | echo "If you are not updating an in-progress localization, you can clean up with"
31 | echo " ./Scripts/manual_cleanup.sh"
32 | echo "before running this script"
33 | echo
34 | echo "This script takes a long time to run. Wait to make sure there is not an early error."
35 | echo " Then take a break and return when all languages have been processed by Xcode"
36 |
37 | continue_or_quit ${0}
38 |
39 | for project in ${PROJECTS}; do
40 | echo "Prepping $project"
41 | IFS=":" read user dir branch <<< "$project"
42 | echo "parts = $user $dir $branch"
43 | cd $dir
44 | current_branch=$(git branch --show-current 2>/dev/null)
45 | echo "current_branch = $current_branch"
46 | if [[ "${current_branch}" == "${TRANSLATION_BRANCH}" ]]; then
47 | echo "already on $TRANSLATION_BRANCH"
48 |
49 | elif [ -n "$(git branch --list "$TRANSLATION_BRANCH")" ]; then
50 | echo "Local branch '$TRANSLATION_BRANCH' exists, switching to it."
51 | git switch "${TRANSLATION_BRANCH}"
52 |
53 | else
54 | echo "Local branch $TRANSLATION_BRANCH does not exist,"
55 | echo "creating $TRANSLATION_BRANCH from the current branch, $current_branch."
56 | git switch -c "${TRANSLATION_BRANCH}"
57 | fi
58 |
59 | cd -
60 | done
61 |
62 | # Build Loop
63 | set -o pipefail && time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme 'LoopWorkspace' build | xcpretty
64 |
65 | # Apply translations
66 | foreach file in xliff_in/*.xliff
67 | section_divider
68 | echo " importing ${file}"
69 | section_divider
70 | /usr/bin/time xcodebuild -workspace LoopWorkspace.xcworkspace -scheme "LoopWorkspace" -importLocalizations -localizationPath $file
71 | end
72 |
73 | section_divider
74 | echo "Continue by reviewing the differences for each submodule with command:"
75 | next_script "./Scripts/manual_review_translations.sh"
76 | section_divider
77 |
--------------------------------------------------------------------------------
/Scripts/define_common.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 |
3 | # define parameters and arrays used by more than one script
4 | # These are always capitalized
5 | # First two can be replaced with arguments
6 | # TRANSLATION_BRANCH (arg 1)
7 | # TARGET_LOOPWORKSPACE_BRANCH (arg 2)
8 | # MESSAGE_FILE
9 | # ARCHIVE_BRANCH
10 | # PROJECTS
11 | # LANGUAGES
12 |
13 | # include this file in each script using
14 | # source Scripts/define_commont.sh
15 |
16 | # define the branch names used by the translation scripts
17 | # Any script that uses define_common can be called with one or two optional arguments
18 | # first argument replaces default for TRANSLATION_BRANCH
19 | # second argument replaces default for TARGET_LOOPWORKSPACE_BRANCH
20 | # Note: went for simplicity here - if you want to modify TARGET_LOOPWORKSPACE_BRANCH
21 | # via argument, you must also include TRANSLATION_BRANCH as an argument
22 | DEFAULT_TRANSLATION_BRANCH="translations"
23 | DEFAULT_TARGET_LOOPWORKSPACE_BRANCH="dev"
24 |
25 | TRANSLATION_BRANCH=${1:-$DEFAULT_TRANSLATION_BRANCH}
26 | TARGET_LOOPWORKSPACE_BRANCH=${2:-$DEFAULT_TARGET_LOOPWORKSPACE_BRANCH}
27 |
28 | ARCHIVE_BRANCH="archive_translations"
29 |
30 | # define name of file used to save the commit message and title for pull requests
31 | MESSAGE_FILE="xlate_message_file.txt"
32 |
33 | # define the languages used by the translation scripts
34 | # matches lokalise order, en plus alphabetical order by language name in English
35 | LANGUAGES=(en \
36 | ar \
37 | ce \
38 | zh-Hans \
39 | cs \
40 | da \
41 | nl \
42 | fi \
43 | fr \
44 | de \
45 | he \
46 | hi \
47 | hu \
48 | it \
49 | ja \
50 | nb \
51 | pl \
52 | pt-BR \
53 | ro \
54 | ru \
55 | sk \
56 | es \
57 | sv \
58 | tr \
59 | uk \
60 | vi \
61 | )
62 |
63 | # define the PROJECTS used by the translation scripts
64 | PROJECTS=( \
65 | LoopKit:AmplitudeService:dev \
66 | LoopKit:CGMBLEKit:dev \
67 | LoopKit:dexcom-share-client-swift:dev \
68 | loopandlearn:DanaKit:dev \
69 | LoopKit:G7SensorKit:main \
70 | LoopKit:LibreTransmitter:main \
71 | LoopKit:LogglyService:dev \
72 | LoopKit:Loop:dev \
73 | LoopKit:LoopKit:dev \
74 | LoopKit:LoopOnboarding:dev \
75 | LoopKit:LoopSupport:dev \
76 | LoopKit:MinimedKit:main \
77 | LoopKit:NightscoutRemoteCGM:dev \
78 | LoopKit:NightscoutService:dev \
79 | LoopKit:OmniBLE:dev \
80 | LoopKit:OmniKit:main \
81 | LoopKit:RileyLinkKit:dev \
82 | LoopKit:TidepoolService:dev \
83 | )
84 |
85 | function section_divider() {
86 | echo -e ""
87 | echo -e "--------------------------------"
88 | echo -e ""
89 | }
90 |
91 | function continue_or_quit() {
92 | local script_name=$1
93 | section_divider
94 | echo "Enter y to proceed, any other character exits"
95 | read query
96 |
97 | if [[ ${query} != "y" ]]; then
98 | section_divider
99 | echo "User opted to exit ${script_name}."
100 | section_divider
101 | exit 1
102 | fi
103 | }
104 |
105 | function next_script() {
106 | local next_script_name=$1
107 | if [[ ${TRANSLATION_BRANCH} == ${DEFAULT_TRANSLATION_BRANCH} ]]; then
108 | echo "$next_script_name"
109 | else
110 | echo "$next_script_name ${TRANSLATION_BRANCH}"
111 | fi
112 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
9 |
10 |
12 |
13 |
15 |
16 |
18 |
19 |
21 |
22 |
24 |
25 |
27 |
28 |
30 |
31 |
33 |
34 |
36 |
37 |
39 |
40 |
42 |
43 |
45 |
46 |
48 |
49 |
51 |
52 |
53 |
55 |
56 |
58 |
59 |
62 |
64 |
65 |
67 |
68 |
70 |
71 |
72 |
74 |
75 |
77 |
78 |
80 |
81 |
83 |
84 |
86 |
87 |
89 |
90 |
92 |
93 |
95 |
96 |
98 |
99 |
101 |
102 |
104 |
105 |
107 |
108 |
110 |
111 |
113 |
114 |
116 |
117 |
119 |
120 |
122 |
123 |
125 |
126 |
128 |
129 |
131 |
132 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/create_certs.yml:
--------------------------------------------------------------------------------
1 | name: 3. Create Certificates
2 | run-name: Create Certificates (${{ github.ref_name }})
3 |
4 | on: [workflow_call, workflow_dispatch]
5 |
6 | env:
7 | TEAMID: ${{ secrets.TEAMID }}
8 | GH_PAT: ${{ secrets.GH_PAT }}
9 | GH_TOKEN: ${{ secrets.GH_PAT }}
10 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
11 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
12 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
13 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
14 |
15 | jobs:
16 | validate:
17 | name: Validate
18 | uses: ./.github/workflows/validate_secrets.yml
19 | secrets: inherit
20 |
21 |
22 | create_certs:
23 | name: Certificates
24 | needs: validate
25 | runs-on: macos-15
26 | outputs:
27 | new_certificate_needed: ${{ steps.set_output.outputs.new_certificate_needed }}
28 |
29 | steps:
30 | # Checks-out the repo
31 | - name: Checkout Repo
32 | uses: actions/checkout@v4
33 |
34 | # Patch Fastlane Match to not print tables
35 | - name: Patch Match Tables
36 | run: |
37 | TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb
38 | if [ -f "$TABLE_PRINTER_PATH" ]; then
39 | sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH"
40 | else
41 | echo "table_printer.rb not found"
42 | exit 1
43 | fi
44 |
45 | # Install project dependencies
46 | - name: Install Project Dependencies
47 | run: bundle install
48 |
49 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
50 | - name: Sync clock
51 | run: sudo sntp -sS time.windows.com
52 |
53 | # Create or update Distribution certificate and provisioning profiles
54 | - name: Check and create or update Distribution certificate and profiles if needed
55 | run: |
56 | echo "Running Fastlane certs lane..."
57 | bundle exec fastlane certs || true # ignore and continue on errors without annotating an exit code
58 | - name: Check Distribution certificate and launch Nuke certificates if needed
59 | run: bundle exec fastlane check_and_renew_certificates
60 | id: check_certs
61 |
62 | - name: Set output and annotations based on Fastlane result
63 | id: set_output
64 | run: |
65 | CERT_STATUS_FILE="${{ github.workspace }}/fastlane/new_certificate_needed.txt"
66 | ENABLE_NUKE_CERTS=${{ vars.ENABLE_NUKE_CERTS }}
67 |
68 | if [ -f "$CERT_STATUS_FILE" ]; then
69 | CERT_STATUS=$(cat "$CERT_STATUS_FILE" | tr -d '\n' | tr -d '\r') # Read file content and strip newlines
70 | echo "new_certificate_needed: $CERT_STATUS"
71 | echo "new_certificate_needed=$CERT_STATUS" >> $GITHUB_OUTPUT
72 | else
73 | echo "Certificate status file not found. Defaulting to false."
74 | echo "new_certificate_needed=false" >> $GITHUB_OUTPUT
75 | fi
76 | # Check if ENABLE_NUKE_CERTS is not set to true when certs are valid
77 | if [ "$CERT_STATUS" != "true" ] && [ "$ENABLE_NUKE_CERTS" != "true" ]; then
78 | echo "::notice::🔔 Automated renewal of certificates is disabled because the repository variable ENABLE_NUKE_CERTS is not set to 'true'."
79 | fi
80 | # Check if ENABLE_NUKE_CERTS is not set to true when certs are not valid
81 | if [ "$CERT_STATUS" = "true" ] && [ "$ENABLE_NUKE_CERTS" != "true" ]; then
82 | echo "::error::❌ No valid distribution certificate found. Automated renewal of certificates was skipped because the repository variable ENABLE_NUKE_CERTS is not set to 'true'."
83 | exit 1
84 | fi
85 | # Check if vars.FORCE_NUKE_CERTS is not set to true
86 | if [ vars.FORCE_NUKE_CERTS = "true" ]; then
87 | echo "::warning::‼️ Nuking of certificates was forced because the repository variable FORCE_NUKE_CERTS is set to 'true'."
88 | fi
89 | # Nuke Certs if needed, and if the repository variable ENABLE_NUKE_CERTS is set to 'true', or if FORCE_NUKE_CERTS is set to 'true', which will always force certs to be nuked
90 | nuke_certs:
91 | name: Nuke certificates
92 | needs: [validate, create_certs]
93 | runs-on: macos-15
94 | if: ${{ (needs.create_certs.outputs.new_certificate_needed == 'true' && vars.ENABLE_NUKE_CERTS == 'true') || vars.FORCE_NUKE_CERTS == 'true' }}
95 | steps:
96 | - name: Output from step id 'check_certs'
97 | run: echo "new_certificate_needed=${{ needs.create_certs.outputs.new_certificate_needed }}"
98 |
99 | - name: Checkout repository
100 | uses: actions/checkout@v4
101 |
102 | - name: Install dependencies
103 | run: bundle install
104 |
105 | - name: Run Fastlane nuke_certs
106 | run: |
107 | set -e # Set error immediately after this step if error occurs
108 | bundle exec fastlane nuke_certs
109 | - name: Recreate Distribution certificate after nuking
110 | run: |
111 | set -e # Set error immediately after this step if error occurs
112 | bundle exec fastlane certs
113 | - name: Add success annotations for nuke and certificate recreation
114 | if: ${{ success() }}
115 | run: |
116 | echo "::warning::⚠️ All Distribution certificates and TestFlight profiles have been revoked and recreated."
117 | echo "::warning::❗️ If you have other apps being distributed by GitHub Actions / Fastlane / TestFlight that does not renew certificates automatically, please run the '3. Create Certificates' workflow for each of these apps to allow these apps to be built."
118 | echo "::warning::✅ But don't worry about your existing TestFlight builds, they will keep working!"
119 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/loopandlearn/fastlane.git
3 | revision: a670d4b092b274d58ebb5497126e47fc6a84f533
4 | ref: a670d4b092b274d58ebb5497126e47fc6a84f533
5 | specs:
6 | fastlane (2.228.0)
7 | CFPropertyList (>= 2.3, < 4.0.0)
8 | addressable (>= 2.8, < 3.0.0)
9 | artifactory (~> 3.0)
10 | aws-sdk-s3 (~> 1.0)
11 | babosa (>= 1.0.3, < 2.0.0)
12 | bundler (>= 1.12.0, < 3.0.0)
13 | colored (~> 1.2)
14 | commander (~> 4.6)
15 | dotenv (>= 2.1.1, < 3.0.0)
16 | emoji_regex (>= 0.1, < 4.0)
17 | excon (>= 0.71.0, < 1.0.0)
18 | faraday (~> 1.0)
19 | faraday-cookie_jar (~> 0.0.6)
20 | faraday_middleware (~> 1.0)
21 | fastimage (>= 2.1.0, < 3.0.0)
22 | fastlane-sirp (>= 1.0.0)
23 | gh_inspector (>= 1.1.2, < 2.0.0)
24 | google-apis-androidpublisher_v3 (~> 0.3)
25 | google-apis-playcustomapp_v1 (~> 0.1)
26 | google-cloud-env (>= 1.6.0, < 2.0.0)
27 | google-cloud-storage (~> 1.31)
28 | highline (~> 2.0)
29 | http-cookie (~> 1.0.5)
30 | json (< 3.0.0)
31 | jwt (>= 2.1.0, < 3)
32 | mini_magick (>= 4.9.4, < 5.0.0)
33 | multipart-post (>= 2.0.0, < 3.0.0)
34 | naturally (~> 2.2)
35 | optparse (>= 0.1.1, < 1.0.0)
36 | plist (>= 3.1.0, < 4.0.0)
37 | rubyzip (>= 2.0.0, < 3.0.0)
38 | security (= 0.1.5)
39 | simctl (~> 1.6.3)
40 | terminal-notifier (>= 2.0.0, < 3.0.0)
41 | terminal-table (~> 3)
42 | tty-screen (>= 0.6.3, < 1.0.0)
43 | tty-spinner (>= 0.8.0, < 1.0.0)
44 | word_wrap (~> 1.0.0)
45 | xcodeproj (>= 1.13.0, < 2.0.0)
46 | xcpretty (~> 0.4.1)
47 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
48 |
49 | GEM
50 | remote: https://rubygems.org/
51 | specs:
52 | CFPropertyList (3.0.7)
53 | base64
54 | nkf
55 | rexml
56 | addressable (2.8.7)
57 | public_suffix (>= 2.0.2, < 7.0)
58 | artifactory (3.0.17)
59 | atomos (0.1.3)
60 | aws-eventstream (1.4.0)
61 | aws-partitions (1.1163.0)
62 | aws-sdk-core (3.232.0)
63 | aws-eventstream (~> 1, >= 1.3.0)
64 | aws-partitions (~> 1, >= 1.992.0)
65 | aws-sigv4 (~> 1.9)
66 | base64
67 | bigdecimal
68 | jmespath (~> 1, >= 1.6.1)
69 | logger
70 | aws-sdk-kms (1.112.0)
71 | aws-sdk-core (~> 3, >= 3.231.0)
72 | aws-sigv4 (~> 1.5)
73 | aws-sdk-s3 (1.199.0)
74 | aws-sdk-core (~> 3, >= 3.231.0)
75 | aws-sdk-kms (~> 1)
76 | aws-sigv4 (~> 1.5)
77 | aws-sigv4 (1.12.1)
78 | aws-eventstream (~> 1, >= 1.0.2)
79 | babosa (1.0.4)
80 | base64 (0.3.0)
81 | bigdecimal (3.2.3)
82 | claide (1.1.0)
83 | colored (1.2)
84 | colored2 (3.1.2)
85 | commander (4.6.0)
86 | highline (~> 2.0.0)
87 | declarative (0.0.20)
88 | digest-crc (0.7.0)
89 | rake (>= 12.0.0, < 14.0.0)
90 | domain_name (0.6.20240107)
91 | dotenv (2.8.1)
92 | emoji_regex (3.2.3)
93 | excon (0.112.0)
94 | faraday (1.10.4)
95 | faraday-em_http (~> 1.0)
96 | faraday-em_synchrony (~> 1.0)
97 | faraday-excon (~> 1.1)
98 | faraday-httpclient (~> 1.0)
99 | faraday-multipart (~> 1.0)
100 | faraday-net_http (~> 1.0)
101 | faraday-net_http_persistent (~> 1.0)
102 | faraday-patron (~> 1.0)
103 | faraday-rack (~> 1.0)
104 | faraday-retry (~> 1.0)
105 | ruby2_keywords (>= 0.0.4)
106 | faraday-cookie_jar (0.0.7)
107 | faraday (>= 0.8.0)
108 | http-cookie (~> 1.0.0)
109 | faraday-em_http (1.0.0)
110 | faraday-em_synchrony (1.0.1)
111 | faraday-excon (1.1.0)
112 | faraday-httpclient (1.0.1)
113 | faraday-multipart (1.1.1)
114 | multipart-post (~> 2.0)
115 | faraday-net_http (1.0.2)
116 | faraday-net_http_persistent (1.2.0)
117 | faraday-patron (1.0.0)
118 | faraday-rack (1.0.0)
119 | faraday-retry (1.0.3)
120 | faraday_middleware (1.2.1)
121 | faraday (~> 1.0)
122 | fastimage (2.4.0)
123 | fastlane-sirp (1.0.0)
124 | sysrandom (~> 1.0)
125 | gh_inspector (1.1.3)
126 | google-apis-androidpublisher_v3 (0.54.0)
127 | google-apis-core (>= 0.11.0, < 2.a)
128 | google-apis-core (0.11.3)
129 | addressable (~> 2.5, >= 2.5.1)
130 | googleauth (>= 0.16.2, < 2.a)
131 | httpclient (>= 2.8.1, < 3.a)
132 | mini_mime (~> 1.0)
133 | representable (~> 3.0)
134 | retriable (>= 2.0, < 4.a)
135 | rexml
136 | google-apis-iamcredentials_v1 (0.17.0)
137 | google-apis-core (>= 0.11.0, < 2.a)
138 | google-apis-playcustomapp_v1 (0.13.0)
139 | google-apis-core (>= 0.11.0, < 2.a)
140 | google-apis-storage_v1 (0.31.0)
141 | google-apis-core (>= 0.11.0, < 2.a)
142 | google-cloud-core (1.8.0)
143 | google-cloud-env (>= 1.0, < 3.a)
144 | google-cloud-errors (~> 1.0)
145 | google-cloud-env (1.6.0)
146 | faraday (>= 0.17.3, < 3.0)
147 | google-cloud-errors (1.5.0)
148 | google-cloud-storage (1.47.0)
149 | addressable (~> 2.8)
150 | digest-crc (~> 0.4)
151 | google-apis-iamcredentials_v1 (~> 0.1)
152 | google-apis-storage_v1 (~> 0.31.0)
153 | google-cloud-core (~> 1.6)
154 | googleauth (>= 0.16.2, < 2.a)
155 | mini_mime (~> 1.0)
156 | googleauth (1.8.1)
157 | faraday (>= 0.17.3, < 3.a)
158 | jwt (>= 1.4, < 3.0)
159 | multi_json (~> 1.11)
160 | os (>= 0.9, < 2.0)
161 | signet (>= 0.16, < 2.a)
162 | highline (2.0.3)
163 | http-cookie (1.0.8)
164 | domain_name (~> 0.5)
165 | httpclient (2.9.0)
166 | mutex_m
167 | jmespath (1.6.2)
168 | json (2.15.0)
169 | jwt (2.10.2)
170 | base64
171 | logger (1.7.0)
172 | mini_magick (4.13.2)
173 | mini_mime (1.1.5)
174 | multi_json (1.17.0)
175 | multipart-post (2.4.1)
176 | mutex_m (0.3.0)
177 | nanaimo (0.4.0)
178 | naturally (2.3.0)
179 | nkf (0.2.0)
180 | optparse (0.6.0)
181 | os (1.1.4)
182 | plist (3.7.2)
183 | public_suffix (6.0.2)
184 | rake (13.3.0)
185 | representable (3.2.0)
186 | declarative (< 0.1.0)
187 | trailblazer-option (>= 0.1.1, < 0.2.0)
188 | uber (< 0.2.0)
189 | retriable (3.1.2)
190 | rexml (3.4.4)
191 | rouge (3.28.0)
192 | ruby2_keywords (0.0.5)
193 | rubyzip (2.4.1)
194 | security (0.1.5)
195 | signet (0.21.0)
196 | addressable (~> 2.8)
197 | faraday (>= 0.17.5, < 3.a)
198 | jwt (>= 1.5, < 4.0)
199 | multi_json (~> 1.10)
200 | simctl (1.6.10)
201 | CFPropertyList
202 | naturally
203 | sysrandom (1.0.5)
204 | terminal-notifier (2.0.0)
205 | terminal-table (3.0.2)
206 | unicode-display_width (>= 1.1.1, < 3)
207 | trailblazer-option (0.1.2)
208 | tty-cursor (0.7.1)
209 | tty-screen (0.8.2)
210 | tty-spinner (0.9.3)
211 | tty-cursor (~> 0.7)
212 | uber (0.1.0)
213 | unicode-display_width (2.6.0)
214 | word_wrap (1.0.0)
215 | xcodeproj (1.27.0)
216 | CFPropertyList (>= 2.3.3, < 4.0)
217 | atomos (~> 0.1.3)
218 | claide (>= 1.0.2, < 2.0)
219 | colored2 (~> 3.1)
220 | nanaimo (~> 0.4.0)
221 | rexml (>= 3.3.6, < 4.0)
222 | xcpretty (0.4.1)
223 | rouge (~> 3.28.0)
224 | xcpretty-travis-formatter (1.0.1)
225 | xcpretty (~> 0.2, >= 0.0.7)
226 |
227 | PLATFORMS
228 | arm64-darwin-21
229 | arm64-darwin-22
230 | arm64-darwin-23
231 | arm64-darwin-24
232 | x86_64-darwin-19
233 | x86_64-darwin-24
234 | x86_64-linux
235 |
236 | DEPENDENCIES
237 | fastlane!
238 | rexml (>= 3.4.2)
239 |
240 | BUNDLED WITH
241 | 2.6.2
242 |
--------------------------------------------------------------------------------
/.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: ubuntu-latest
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 | - name: Validate Match-Secrets
75 | run: |
76 | # Validate Match-Secrets
77 |
78 | # Ensure that gh exit codes are handled when output is piped.
79 | set -o pipefail
80 |
81 | # If a Match-Secrets repository does not exist, attempt to create one.
82 | if ! visibility=$(gh repo view ${{ github.repository_owner }}/Match-Secrets --json visibility | jq --raw-output '.visibility | ascii_downcase'); then
83 | echo "A '${{ github.repository_owner }}/Match-Secrets' repository could not be found using the GH_PAT secret. Attempting to create one..."
84 |
85 | # Create a private Match-Secrets repository and verify that it exists and that it is private.
86 | 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
87 | echo "Created a private '${{ github.repository_owner }}/Match-Secrets' repository."
88 | else
89 | failed=true
90 | 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."
91 | fi
92 | # Otherwise, if a Match-Secrets repository exists, but it is public, cause validation to fail.
93 | elif [[ "$visibility" == "public" ]]; then
94 | failed=true
95 | 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."
96 | else
97 | echo "Found a private '${{ github.repository_owner }}/Match-Secrets' repository to use."
98 | fi
99 |
100 | # Exit unsuccessfully if secret validation failed.
101 | if [ $failed ]; then
102 | exit 2
103 | fi
104 |
105 | validate-fastlane-secrets:
106 | name: Fastlane
107 | needs: [validate-access-token]
108 | runs-on: macos-15
109 | env:
110 | GH_PAT: ${{ secrets.GH_PAT }}
111 | GH_TOKEN: ${{ secrets.GH_PAT }}
112 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
113 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
114 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
115 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
116 | TEAMID: ${{ secrets.TEAMID }}
117 | steps:
118 | - name: Checkout Repo
119 | uses: actions/checkout@v4
120 |
121 | - name: Install Project Dependencies
122 | run: bundle install
123 |
124 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
125 | - name: Sync clock
126 | run: sudo sntp -sS time.windows.com
127 |
128 | - name: Validate Fastlane Secrets
129 | run: |
130 | # Validate Fastlane Secrets
131 |
132 | # Validate TEAMID
133 | if [ -z "$TEAMID" ]; then
134 | failed=true
135 | echo "::error::The TEAMID secret is unset or empty. Set it and try again."
136 | elif [ ${#TEAMID} -ne 10 ]; then
137 | failed=true
138 | echo "::error::The TEAMID secret is set but has wrong length. Verify that it is set correctly and try again."
139 | elif ! [[ $TEAMID =~ ^[A-Z0-9]+$ ]]; then
140 | failed=true
141 | echo "::error::The TEAMID secret is set but invalid. Verify that it is set correctly (only uppercase letters and numbers) and try again."
142 | fi
143 |
144 | # Validate MATCH_PASSWORD
145 | if [ -z "$MATCH_PASSWORD" ]; then
146 | failed=true
147 | echo "::error::The MATCH_PASSWORD secret is unset or empty. Set it and try again."
148 | fi
149 |
150 | # Ensure that fastlane exit codes are handled when output is piped.
151 | set -o pipefail
152 |
153 | # Validate FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY
154 | FASTLANE_KEY_ID_PATTERN='^[A-Z0-9]+$'
155 | 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}\}?$'
156 |
157 | if [ -z "$FASTLANE_ISSUER_ID" ] || [ -z "$FASTLANE_KEY_ID" ] || [ -z "$FASTLANE_KEY" ]; then
158 | failed=true
159 | [ -z "$FASTLANE_ISSUER_ID" ] && echo "::error::The FASTLANE_ISSUER_ID secret is unset or empty. Set it and try again."
160 | [ -z "$FASTLANE_KEY_ID" ] && echo "::error::The FASTLANE_KEY_ID secret is unset or empty. Set it and try again."
161 | [ -z "$FASTLANE_KEY" ] && echo "::error::The FASTLANE_KEY secret is unset or empty. Set it and try again."
162 | elif [ ${#FASTLANE_KEY_ID} -ne 10 ]; then
163 | failed=true
164 | 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."
165 | elif ! [[ $FASTLANE_KEY_ID =~ $FASTLANE_KEY_ID_PATTERN ]]; then
166 | failed=true
167 | 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."
168 | elif ! [[ $FASTLANE_ISSUER_ID =~ $FASTLANE_ISSUER_ID_PATTERN ]]; then
169 | failed=true
170 | 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."
171 | elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
172 | failed=true
173 | 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."
174 | elif ! (bundle exec fastlane validate_secrets 2>&1 || true) | tee fastlane.log; then # ignore "fastlane validate_secrets" errors and continue on errors without annotating an exit code
175 | if grep -q "bad decrypt" fastlane.log; then
176 | failed=true
177 | echo "::error::Unable to decrypt the Match-Secrets repository using the MATCH_PASSWORD secret. Verify that it is set correctly and try again."
178 | elif grep -q -e "required agreement" -e "license agreement" fastlane.log; then
179 | failed=true
180 | echo "::error::❗️ 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 take effect and try again."
181 | elif grep -q "Your certificate .* is not valid" fastlane.log; then
182 | echo "::notice::Your Distribution certificate is invalid or expired. Automated renewal of the certificate will be attempted."
183 | fi
184 | fi
185 |
186 | # Exit unsuccessfully if secret validation failed.
187 | if [ $failed ]; then
188 | exit 2
189 | fi
190 |
--------------------------------------------------------------------------------
/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 | mapping = Actions.lane_context[
68 | SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING
69 | ]
70 |
71 | update_code_signing_settings(
72 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj",
73 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop"],
74 | code_sign_identity: "iPhone Distribution",
75 | targets: ["Loop"]
76 | )
77 |
78 | update_code_signing_settings(
79 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj",
80 | code_sign_identity: "iPhone Distribution",
81 | targets: ["LoopCore", "LoopCore-watchOS", "LoopUI"]
82 | )
83 |
84 | update_code_signing_settings(
85 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj",
86 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.statuswidget"],
87 | code_sign_identity: "iPhone Distribution",
88 | targets: ["Loop Status Extension"]
89 | )
90 |
91 | update_code_signing_settings(
92 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj",
93 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.LoopWatch.watchkitextension"],
94 | code_sign_identity: "iPhone Distribution",
95 | targets: ["WatchApp Extension"]
96 | )
97 |
98 | update_code_signing_settings(
99 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj",
100 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.LoopWatch"],
101 | code_sign_identity: "iPhone Distribution",
102 | targets: ["WatchApp"]
103 | )
104 |
105 | update_code_signing_settings(
106 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj",
107 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.Loop-Intent-Extension"],
108 | code_sign_identity: "iPhone Distribution",
109 | targets: ["Loop Intent Extension"]
110 | )
111 |
112 | update_code_signing_settings(
113 | path: "#{GITHUB_WORKSPACE}/Loop/Loop.xcodeproj",
114 | profile_name: mapping["com.#{TEAMID}.loopkit.Loop.LoopWidgetExtension"],
115 | code_sign_identity: "iPhone Distribution",
116 | targets: ["Loop Widget Extension"]
117 | )
118 |
119 | gym(
120 | export_method: "app-store",
121 | scheme: "LoopWorkspace",
122 | output_name: "Loop.ipa",
123 | configuration: "Release",
124 | destination: 'generic/platform=iOS',
125 | buildlog_path: 'buildlog'
126 | )
127 |
128 | copy_artifacts(
129 | target_path: "artifacts",
130 | artifacts: ["*.mobileprovision", "*.ipa", "*.dSYM.zip"]
131 | )
132 | end
133 |
134 | desc "Push to TestFlight"
135 | lane :release do
136 | api_key = app_store_connect_api_key(
137 | key_id: "#{FASTLANE_KEY_ID}",
138 | issuer_id: "#{FASTLANE_ISSUER_ID}",
139 | key_content: "#{FASTLANE_KEY}"
140 | )
141 |
142 | upload_to_testflight(
143 | api_key: api_key,
144 | skip_submission: false,
145 | ipa: "Loop.ipa",
146 | skip_waiting_for_build_processing: true,
147 | )
148 | end
149 |
150 | desc "Provision Identifiers and Certificates"
151 | lane :identifiers do
152 | setup_ci if ENV['CI']
153 | ENV["MATCH_READONLY"] = false.to_s
154 |
155 | app_store_connect_api_key(
156 | key_id: "#{FASTLANE_KEY_ID}",
157 | issuer_id: "#{FASTLANE_ISSUER_ID}",
158 | key_content: "#{FASTLANE_KEY}"
159 | )
160 |
161 | def configure_bundle_id(name, identifier, capabilities)
162 | bundle_id = Spaceship::ConnectAPI::BundleId.find(identifier) || Spaceship::ConnectAPI::BundleId.create(
163 | name: name,
164 | identifier: identifier,
165 | platform: "IOS"
166 | )
167 | existing = bundle_id.get_capabilities.map(&:capability_type)
168 | capabilities.reject { |c| existing.include?(c) }.each do |cap|
169 | bundle_id.create_capability(cap)
170 | end
171 | end
172 |
173 | configure_bundle_id("Loop", "com.#{TEAMID}.loopkit.Loop", [
174 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS,
175 | Spaceship::ConnectAPI::BundleIdCapability::Type::HEALTHKIT,
176 | Spaceship::ConnectAPI::BundleIdCapability::Type::PUSH_NOTIFICATIONS,
177 | Spaceship::ConnectAPI::BundleIdCapability::Type::SIRIKIT,
178 | Spaceship::ConnectAPI::BundleIdCapability::Type::NFC_TAG_READING
179 | ])
180 |
181 | configure_bundle_id("Loop Intent Extension", "com.#{TEAMID}.loopkit.Loop.Loop-Intent-Extension", [
182 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS
183 | ])
184 |
185 | configure_bundle_id("Loop Status Extension", "com.#{TEAMID}.loopkit.Loop.statuswidget", [
186 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS
187 | ])
188 |
189 | configure_bundle_id("WatchApp", "com.#{TEAMID}.loopkit.Loop.LoopWatch", [])
190 |
191 | configure_bundle_id("WatchApp Extension", "com.#{TEAMID}.loopkit.Loop.LoopWatch.watchkitextension", [
192 | Spaceship::ConnectAPI::BundleIdCapability::Type::HEALTHKIT,
193 | Spaceship::ConnectAPI::BundleIdCapability::Type::SIRIKIT
194 | ])
195 |
196 | configure_bundle_id("Loop Widget Extension", "com.#{TEAMID}.loopkit.Loop.LoopWidgetExtension", [
197 | Spaceship::ConnectAPI::BundleIdCapability::Type::APP_GROUPS
198 | ])
199 |
200 | end
201 |
202 | desc "Provision Certificates"
203 | lane :certs do
204 | setup_ci if ENV['CI']
205 | ENV["MATCH_READONLY"] = false.to_s
206 |
207 | app_store_connect_api_key(
208 | key_id: "#{FASTLANE_KEY_ID}",
209 | issuer_id: "#{FASTLANE_ISSUER_ID}",
210 | key_content: "#{FASTLANE_KEY}"
211 | )
212 |
213 | match(
214 | type: "appstore",
215 | force: false,
216 | verbose: true,
217 | git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"),
218 | app_identifier: [
219 | "com.#{TEAMID}.loopkit.Loop",
220 | "com.#{TEAMID}.loopkit.Loop.statuswidget",
221 | "com.#{TEAMID}.loopkit.Loop.LoopWatch.watchkitextension",
222 | "com.#{TEAMID}.loopkit.Loop.LoopWatch",
223 | "com.#{TEAMID}.loopkit.Loop.Loop-Intent-Extension",
224 | "com.#{TEAMID}.loopkit.Loop.LoopWidgetExtension",
225 | ]
226 | )
227 | end
228 |
229 | desc "Validate Secrets"
230 | lane :validate_secrets do
231 | setup_ci if ENV['CI']
232 | ENV["MATCH_READONLY"] = true.to_s
233 |
234 | app_store_connect_api_key(
235 | key_id: "#{FASTLANE_KEY_ID}",
236 | issuer_id: "#{FASTLANE_ISSUER_ID}",
237 | key_content: "#{FASTLANE_KEY}"
238 | )
239 |
240 | def find_bundle_id(identifier)
241 | bundle_id = Spaceship::ConnectAPI::BundleId.find(identifier)
242 | end
243 |
244 | find_bundle_id("com.#{TEAMID}.loopkit.Loop")
245 |
246 | match(
247 | type: "appstore",
248 | git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}"),
249 | app_identifier: [],
250 | )
251 |
252 | end
253 |
254 | desc "Nuke Certs"
255 | lane :nuke_certs do
256 | setup_ci if ENV['CI']
257 | ENV["MATCH_READONLY"] = false.to_s
258 |
259 | app_store_connect_api_key(
260 | key_id: "#{FASTLANE_KEY_ID}",
261 | issuer_id: "#{FASTLANE_ISSUER_ID}",
262 | key_content: "#{FASTLANE_KEY}"
263 | )
264 |
265 | match_nuke(
266 | type: "appstore",
267 | team_id: "#{TEAMID}",
268 | skip_confirmation: true,
269 | git_basic_authorization: Base64.strict_encode64("#{GITHUB_REPOSITORY_OWNER}:#{GH_PAT}")
270 | )
271 | end
272 |
273 | desc "Check Certificates and Trigger Workflow for Expired or Missing Certificates"
274 | lane :check_and_renew_certificates do
275 | setup_ci if ENV['CI']
276 | ENV["MATCH_READONLY"] = false.to_s
277 |
278 | # Authenticate using App Store Connect API Key
279 | api_key = app_store_connect_api_key(
280 | key_id: ENV["FASTLANE_KEY_ID"],
281 | issuer_id: ENV["FASTLANE_ISSUER_ID"],
282 | key_content: ENV["FASTLANE_KEY"] # Ensure valid key content
283 | )
284 |
285 | # Initialize flag to track if renewal of certificates is needed
286 | new_certificate_needed = false
287 |
288 | # Fetch all certificates
289 | certificates = Spaceship::ConnectAPI::Certificate.all
290 |
291 | # Filter for Distribution Certificates
292 | distribution_certs = certificates.select { |cert| cert.certificate_type == "DISTRIBUTION" }
293 |
294 | # Handle case where no distribution certificates are found
295 | if distribution_certs.empty?
296 | puts "No Distribution certificates found! Triggering action to create certificate."
297 | new_certificate_needed = true
298 | else
299 | # Check for expiration
300 | distribution_certs.each do |cert|
301 | expiration_date = Time.parse(cert.expiration_date)
302 |
303 | puts "Current Distribution Certificate: #{cert.id}, Expiration date: #{expiration_date}"
304 |
305 | if expiration_date < Time.now
306 | puts "Distribution Certificate #{cert.id} is expired! Triggering action to renew certificate."
307 | new_certificate_needed = true
308 | else
309 | puts "Distribution certificate #{cert.id} is valid. No action required."
310 | end
311 | end
312 | end
313 |
314 | # Write result to new_certificate_needed.txt
315 | file_path = File.expand_path('new_certificate_needed.txt')
316 | File.write(file_path, new_certificate_needed ? 'true' : 'false')
317 |
318 | # Log the absolute path and contents of the new_certificate_needed.txt file
319 | puts ""
320 | puts "Absolute path of new_certificate_needed.txt: #{file_path}"
321 | new_certificate_needed_content = File.read(file_path)
322 | puts "Certificate creation or renewal needed: #{new_certificate_needed_content}"
323 | end
324 | end
--------------------------------------------------------------------------------
/.github/workflows/build_loop.yml:
--------------------------------------------------------------------------------
1 | name: 4. Build Loop
2 | run-name: Build Loop (${{ github.ref_name }})
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | # Check for updates every Sunday
7 | # Later logic builds if there are updates or if it is the 2nd Sunday of the month
8 | - cron: "33 7 * * 0" # Sunday at UTC 7:33
9 |
10 | env:
11 | GH_PAT: ${{ secrets.GH_PAT }}
12 | UPSTREAM_REPO: LoopKit/LoopWorkspace
13 | UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed)
14 | TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync
15 |
16 | jobs:
17 | # use a single runner for these sequential steps
18 | check_status:
19 | runs-on: ubuntu-latest
20 | name: Check status to decide whether to build
21 | permissions:
22 | contents: write
23 | outputs:
24 | NEW_COMMITS: ${{ steps.sync.outputs.has_new_commits }}
25 | IS_SECOND_IN_MONTH: ${{ steps.date-check.outputs.is_second_instance }}
26 |
27 | # Check GH_PAT, sync repository, check day in month
28 | steps:
29 |
30 | - name: Access
31 | id: workflow-permission
32 | run: |
33 | # Validate Access Token
34 |
35 | # Ensure that gh exit codes are handled when output is piped.
36 | set -o pipefail
37 |
38 | # Define patterns to validate the access token (GH_PAT) and distinguish between classic and fine-grained tokens.
39 | GH_PAT_CLASSIC_PATTERN='^ghp_[a-zA-Z0-9]{36}$'
40 | GH_PAT_FINE_GRAINED_PATTERN='^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$'
41 |
42 | # Validate Access Token (GH_PAT)
43 | if [ -z "$GH_PAT" ]; then
44 | failed=true
45 | echo "::error::The GH_PAT secret is unset or empty. Set it and try again."
46 | else
47 | if [[ $GH_PAT =~ $GH_PAT_CLASSIC_PATTERN ]]; then
48 | provides_scopes=true
49 | echo "The GH_PAT secret is a structurally valid classic token."
50 | elif [[ $GH_PAT =~ $GH_PAT_FINE_GRAINED_PATTERN ]]; then
51 | echo "The GH_PAT secret is a structurally valid fine-grained token."
52 | else
53 | unknown_format=true
54 | echo "The GH_PAT secret does not have a known token format."
55 | fi
56 |
57 | # Attempt to capture the x-oauth-scopes scopes of the token.
58 | if ! scopes=$(curl -sS -f -I -H "Authorization: token $GH_PAT" https://api.github.com | { grep -i '^x-oauth-scopes:' || true; } | cut -d ' ' -f2- | tr -d '\r'); then
59 | failed=true
60 | if [ $unknown_format ]; then
61 | echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that it is set correctly (including the 'ghp_' or 'github_pat_' prefix) and try again."
62 | else
63 | echo "::error::Unable to connect to GitHub using the GH_PAT secret. Verify that the token exists and has not expired at https://github.com/settings/tokens. If necessary, regenerate or create a new token (and update the secret), then try again."
64 | fi
65 | elif [[ $scopes =~ workflow ]]; then
66 | echo "The GH_PAT secret has repo and workflow permissions."
67 | echo "has_permission=true" >> $GITHUB_OUTPUT
68 | elif [[ $scopes =~ repo ]]; then
69 | echo "The GH_PAT secret has repo (but not workflow) permissions."
70 | elif [ $provides_scopes ]; then
71 | failed=true
72 | if [ -z "$scopes" ]; then
73 | echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide any permission scopes."
74 | else
75 | echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it only provides the following permission scopes: $scopes"
76 | fi
77 | echo "::error::The GH_PAT secret is lacking at least the 'repo' permission scope required to access the Match-Secrets repository. Update the token permissions at https://github.com/settings/tokens (to include the 'repo' and 'workflow' scopes) and try again."
78 | else
79 | echo "The GH_PAT secret is valid and can be used to connect to GitHub, but it does not provide inspectable scopes. Assuming that the 'repo' and 'workflow' permission scopes required to access the Match-Secrets repository and perform automations are present."
80 | echo "has_permission=true" >> $GITHUB_OUTPUT
81 | fi
82 | fi
83 |
84 | # Exit unsuccessfully if secret validation failed.
85 | if [ $failed ]; then
86 | exit 2
87 | fi
88 |
89 | - name: Checkout target repo
90 | if: |
91 | steps.workflow-permission.outputs.has_permission == 'true' &&
92 | (vars.SCHEDULED_BUILD != 'false' || vars.SCHEDULED_SYNC != 'false')
93 | uses: actions/checkout@v4
94 | with:
95 | token: ${{ secrets.GH_PAT }}
96 |
97 | # This syncs any target branch to upstream branch of the same name
98 | - name: Sync upstream changes
99 | if: | # do not run the upstream sync action on the upstream repository
100 | steps.workflow-permission.outputs.has_permission == 'true' &&
101 | vars.SCHEDULED_SYNC != 'false' && github.repository_owner != 'LoopKit'
102 | id: sync
103 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4.1
104 | with:
105 | target_sync_branch: ${{ env.TARGET_BRANCH }}
106 | shallow_since: 6 months ago
107 | target_repo_token: ${{ secrets.GH_PAT }}
108 | upstream_sync_branch: ${{ env.UPSTREAM_BRANCH }}
109 | upstream_sync_repo: ${{ env.UPSTREAM_REPO }}
110 |
111 | # Display a sample message based on the sync output var 'has_new_commits'
112 | - name: New commits found
113 | if: |
114 | steps.workflow-permission.outputs.has_permission == 'true' &&
115 | vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'true'
116 | run: echo "New commits were found to sync."
117 |
118 | - name: No new commits
119 | if: |
120 | steps.workflow-permission.outputs.has_permission == 'true' &&
121 | vars.SCHEDULED_SYNC != 'false' && steps.sync.outputs.has_new_commits == 'false'
122 | run: echo "There were no new commits."
123 |
124 | - name: Show value of 'has_new_commits'
125 | if: steps.workflow-permission.outputs.has_permission == 'true' && vars.SCHEDULED_SYNC != 'false'
126 | run: |
127 | echo ${{ steps.sync.outputs.has_new_commits }}
128 | echo "NEW_COMMITS=${{ steps.sync.outputs.has_new_commits }}" >> $GITHUB_OUTPUT
129 |
130 | - name: Show scheduled build configuration message
131 | if: steps.workflow-permission.outputs.has_permission != 'true'
132 | run: |
133 | echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY
134 | echo "You have not yet configured the scheduled sync and build for Loop's browser build." >> $GITHUB_STEP_SUMMARY
135 | echo "Synchronizing your fork of LoopWorkspace with the upstream repository LoopKit/LoopWorkspace will be skipped." >> $GITHUB_STEP_SUMMARY
136 | echo "If you want to enable automatic builds and updates for your Loop, please follow the instructions \
137 | under the following path LoopWorkspace/fastlane/testflight.md." >> $GITHUB_STEP_SUMMARY
138 |
139 | # Set a logic flag if this is the second instance of this day-of-week in this month
140 | - name: Check if this is the second time this day-of-week happens this month
141 | id: date-check
142 | run: |
143 | DAY_OF_MONTH=$(date +%-d)
144 | WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 ))
145 | if [[ $WEEK_OF_MONTH -eq 2 ]]; then
146 | echo "is_second_instance=true" >> "$GITHUB_OUTPUT"
147 | else
148 | echo "is_second_instance=false" >> "$GITHUB_OUTPUT"
149 | fi
150 |
151 | # Checks if Distribution certificate is present and valid, optionally nukes and
152 | # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
153 | # only run if a build is planned
154 | check_certs:
155 | needs: [check_status]
156 | name: Check certificates
157 | uses: ./.github/workflows/create_certs.yml
158 | secrets: inherit
159 | if: |
160 | github.event_name == 'workflow_dispatch' ||
161 | (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') ||
162 | (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
163 |
164 | # Builds Loop
165 | build:
166 | name: Build
167 | needs: [check_certs, check_status]
168 | runs-on: macos-15
169 | permissions:
170 | contents: write
171 | if:
172 | | # builds with manual start; if scheduled: once a month or when new commits are found
173 | github.event_name == 'workflow_dispatch' ||
174 | (vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') ||
175 | (vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
176 | steps:
177 | - name: Select Xcode version
178 | run: "sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer"
179 |
180 | - name: Checkout Repo for building
181 | uses: actions/checkout@v4
182 | with:
183 | token: ${{ secrets.GH_PAT }}
184 | submodules: recursive
185 | ref: ${{ env.TARGET_BRANCH }}
186 |
187 | # Customize Loop: Download and apply patches
188 | - name: Customize Loop
189 | run: |
190 |
191 | # LoopWorkspace patches
192 | # -applies any patches located in the LoopWorkspace/patches/ directory
193 | if $(ls ./patches/* &> /dev/null); then
194 | git apply ./patches/* --allow-empty -v --whitespace=fix
195 | fi
196 |
197 | # Submodule Loop patches:
198 | # Template for customizing submodule Loop (changes Loop app name to "CustomLoop")
199 | # Remove the "#" sign from the beginning of the line below to activate:
200 | #curl https://github.com/loopnlearn/Loop/commit/d206432b024279ef710df462b20bd464cd9682d4.patch | git apply --directory=Loop -v --whitespace=fix
201 |
202 | # Submodule LoopKit patches:
203 | # General template for customizing submodule LoopKit
204 | # Copy url from a GitHub commit or pull request and insert below, and remove the "#" sign from the beginning of the line to activate:
205 | #curl url_to_github_commit.patch | git apply --directory=LoopKit -v --whitespace=fix
206 |
207 | # Submodule xxxxx patches:
208 |
209 | # Add patches for customization of additional submodules by following the templates above,
210 | # and make sure to specify the submodule by setting "--directory=(submodule_name)".
211 | # Several patches may be added per submodule.
212 | # Adding comments (#) may be useful to easily tell the individual patches apart.
213 |
214 | # Patch Fastlane Match to not print tables
215 | - name: Patch Match Tables
216 | run: |
217 | TABLE_PRINTER_PATH=$(ruby -e 'puts Gem::Specification.find_by_name("fastlane").gem_dir')/match/lib/match/table_printer.rb
218 | if [ -f "$TABLE_PRINTER_PATH" ]; then
219 | sed -i "" "/puts(Terminal::Table.new(params))/d" "$TABLE_PRINTER_PATH"
220 | else
221 | echo "table_printer.rb not found"
222 | exit 1
223 | fi
224 |
225 | # Install project dependencies
226 | - name: Install Project Dependencies
227 | run: bundle install
228 |
229 | # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
230 | - name: Sync clock
231 | run: sudo sntp -sS time.windows.com
232 |
233 | # Build signed Loop IPA file
234 | - name: Fastlane Build & Archive
235 | run: bundle exec fastlane build_loop
236 | env:
237 | TEAMID: ${{ secrets.TEAMID }}
238 | GH_PAT: ${{ secrets.GH_PAT }}
239 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
240 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
241 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
242 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
243 |
244 | # Upload to TestFlight
245 | - name: Fastlane upload to TestFlight
246 | run: bundle exec fastlane release
247 | env:
248 | TEAMID: ${{ secrets.TEAMID }}
249 | GH_PAT: ${{ secrets.GH_PAT }}
250 | FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
251 | FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
252 | FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
253 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
254 |
255 | # Upload Build artifacts
256 | - name: Upload build log, IPA and Symbol artifacts
257 | if: always()
258 | uses: actions/upload-artifact@v4
259 | with:
260 | name: build-artifacts
261 | path: |
262 | artifacts
263 | buildlog
264 |
--------------------------------------------------------------------------------
/Scripts/LocalizationInstructions.md:
--------------------------------------------------------------------------------
1 | # Manual Localization Instructions
2 |
3 | Table of Contents:
4 |
5 | * [Overview](#overview)
6 | * [Overview: From lokalise to LoopWorkspace](#overview-from-lokalise-to-loopworkspace)
7 | * [Overview: From LoopWorkspace to lokalise](#overview-from-loopworkspace-to-lokalise)
8 | * [Loop Dashboard at lokalise](#loop-dashboard-at-lokalise)
9 | * [Script Usage](#script-usage)
10 | * Translations
11 | * From lokalise to LoopWorkspace
12 | * [Download from lokalise](#download-from-lokalise)
13 | * [Import xliff files into LoopWorkspace](#import-xliff-files-into-loopworkspace)
14 | * [Review Differences](#review-differences)
15 | * [Commit Submodule Changes and Create PRs](#commit-submodule-changes-and-create-prs)
16 | * [Review the Open PR and merge](#review-the-open-pr-and-merge)
17 | * [Finalize with PR to LoopWorkspace](#finalize-with-pr-to-loopworkspace)
18 | * From LoopWorkspace to lokalise
19 | * [Prepare xliff_out folder](#prepare-xliff_out-folder)
20 | * [Update lokalise strings](#update-lokalise-strings)
21 | * [Utility Scripts](#utility-scripts)
22 | * [Questions and notes](#questions-and-notes)
23 |
24 | ## Overview
25 |
26 | Translations for Loop are performed by volunteers at [lokalise](https://app.lokalise.com/projects).
27 | Several scripts were added to assist in bringing those translations into the repositories and updating keys when strings are added or modified.
28 |
29 | To volunteer, join [Loop zulipchat](https://loop.zulipchat.com/) and send a direct message to Marion Barker with your email address and the language(s) you can translate.
30 |
31 | The first set of scripts were created in 2023 to automate the localization process. (Refer to these as the original scripts.)
32 |
33 | * Scripts/import_localizations.sh
34 | * Scripts/export_localizations.sh
35 |
36 | About the naming:
37 |
38 | * The "import" in the original script name refers to importing xliff files from lokalise to provide updated localization strings for LoopWorkspace and associated submodules
39 | * This script was used to bring in new translations into the LoopWorkspace submodules and autocreate PR
40 | * The "export" in the original script name refers to exporting localization from LoopWorkspace and associated submodules into xliff files and uploading them to the lokalise site
41 | * This script was used to upload the strings in any of the workspace submodules
42 |
43 | New scripts were created in 2025 to provide smaller steps and to allow review before the modifications are committed and PR are opened.
44 |
45 | These new scripts have "manual" in the script name.
46 |
47 | ### Overview: From lokalise to LoopWorkspace
48 |
49 | For details, see [From lokalise to LoopWorkspace](#from-lokalise-to-loopworkspace)
50 |
51 | These scripts break the original import_localizations script into smaller components:
52 |
53 | * manual_download_from_lokalise.sh
54 | * manual_import_localizations.sh
55 | * manual_review_translations.sh
56 | * manual_finalize_translations.sh
57 |
58 | ### Overview: From LoopWorkspace to lokalise
59 |
60 | For details, see [From LoopWorkspace to lokalise](#from-loopworkspace-to-lokalise)
61 |
62 | This script prepares xliff files for each language (for all repositories) from LoopWorkspace suitable to be uploaded to lokalise:
63 |
64 | * manual_export_localizations.sh
65 | * manual_upload_to_lokalise.sh
66 |
67 | ## Loop Dashboard at lokalise
68 |
69 | When you log into the [lokalise portal](https://app.lokalise.com/projects), navigate to the Loop dashboard, you see all the languages and the % complete for translation.
70 |
71 | ## Translations
72 |
73 | The translations are performed by volunteers. To volunteer, join [Loop zulipchat](https://loop.zulipchat.com/) and send a direct message to Marion Barker with your email address and the language(s) you can translate.
74 |
75 | ## Script Usage
76 |
77 | Many scripts import parameters from Scripts/define_common.sh. These are always capitalized. The first two can be replaced with arguments
78 | * TRANSLATION_BRANCH (optional arg 1)
79 | * TARGET_LOOPWORKSPACE_BRANCH (optional arg 2)
80 | * MESSAGE_FILE
81 | * ARCHIVE_BRANCH
82 | * PROJECTS
83 | * LANGUAGES
84 |
85 | The PROJECTS array lists all the submodules that are handled by these import/export scripts.
86 |
87 | The LANGUAGES array lists all the languages that are handled by the Loop project.
88 |
89 | Some scripts require a LOKALISE_TOKEN.
90 |
91 | When the user is a manager for the Loop project at lokalise, they create a LOKALISE_TOKEN (API token) with read/write privileges.
92 |
93 | * API tokens can be created and recovered by going to : https://app.lokalise.com/profile/?refresh6656#apitokens
94 |
95 | Once the token is created, export the token, e.g.,
96 |
97 | ```
98 | export LOKALISE_TOKEN=
99 | ```
100 |
101 | Make sure the scripts are executable. If not, apply `chmod +x` to the scripts.
102 |
103 | ## From lokalise to LoopWorkspace
104 |
105 | This has been broken into 4 separate scripts to allow review at each step.
106 |
107 | ### Download from lokalise
108 |
109 | The `manual_download_from_lokalise.sh` script requires a LOKALISE_TOKEN with at least read privileges, see [Script Usage](#script-usage).
110 |
111 | This script:
112 |
113 | * deletes any existing xliff_in folder
114 | * downloads the localization information from lokalise into a new xliff_in folder
115 | * generates a temporary `xlate_pr_title.txt` file used for the commit message and titles for PRs to the submodules and LoopWorkspace
116 | * final message provides information about next script to execute
117 |
118 | | | |
119 | |:--|:--|
120 | |**Optional arguments**: | none |
121 | | **Products**: | `xliff_in` folder with xliff files and `xlate_pr_title.txt` with download timestamp |
122 | | **Warnings**: | the previous `xliff_in` folder and `xlate_pr_title.txt` file are replaced |
123 | | | |
124 |
125 | ### Import xliff files into LoopWorkspace
126 |
127 | **Bullet summary** of the `manual_import_translations.sh` script:
128 |
129 | * create `translations` branch for each submodule (project) if it does not already exist
130 | * command-line Xcode build for each language importing from the associated xliff file
131 | * after completion, LoopWorkspace may have uncommitted changes in submodules
132 | * final message provides information about next script to execute
133 | * this script can be repeated with a fresh download from localize to add to an existing translation session
134 |
135 | | | |
136 | |:--|:--|
137 | |**Optional arguments**: | the name of the `translations` branch can be modified with an optional argument |
138 | | **Products**: | any of the submodules associated with LoopWorkspace may be modifed if any new translations are imported for that submodule |
139 | | **Warnings**: | - The first time you run this for a given translation session, be sure you start from version of LoopWorkspace you want to update
- Subsequent runs will add additional translations to the same branch names |
140 | | | |
141 |
142 | **Descriptive summary** of the `manual_import_translations.sh` script.
143 |
144 | Typically, when preparing to update from LoopWorkspace dev, Script/update_submodule_ref.sh is run to prepare the submodules so each one is configured for the subsequent submodules PRs to bring in the translations back to GitHub.
145 |
146 | However, the script can be repeated for more than one download. In this case, the new import is added on top of existing changes.
147 |
148 | The `manual_import_translations.sh` script goes through each submodule in the PROJECTS list.
149 |
150 | Each submodule branch is examined and set to the `translations` branch:
151 | * if the branch does not exist it is created from the current branch
152 |
153 | Then an xcodebuild command is executed to import each language in turn. This can take a very long time, so be patient.
154 |
155 | The result is that any updated localizations shows up as a diff in each submodule.
156 |
157 | Execute this script:
158 |
159 | ```
160 | ./Scripts/manual_import_localizations.sh
161 | ```
162 |
163 | The final message from the script includes the command needed to execute the next script.
164 | * if this script was called with an optional argument, the next script suggestion includes the same argument for you to copy and paste.
165 |
166 |
167 | ### Review Differences
168 |
169 | Use the `manual_review_translations.sh` script in one terminal and open another terminal if you want to look in detail at a particular submodule:
170 |
171 | | | |
172 | |:--|:--|
173 | |**Optional arguments**: | the name of the `translations` branch can be changed to the first argument |
174 | | **Products**: | there are no changes - this is used to review changes before committing them |
175 | | **Warnings**: | none |
176 | | | |
177 |
178 | Execute this script:
179 |
180 | ```
181 | ./Scripts/manual_review_translations.sh
182 | ```
183 |
184 | For each submodule, if any differences are detected, the script pauses with the summary of files changed (`git status`) and allows time to do detailed review (`git diff`) (in another terminal). Hit return when ready to continue the script.
185 |
186 | Examine the diffs for each submodule to make sure they are appropriate.
187 |
188 | ### Commit Submodule Changes and Create PRs
189 |
190 | > Before running this script, ensure that code builds using Mac-Xcode GUI.
191 |
192 | **Bullet summary** of action for each submodule by the `manual_finalize_translations.sh` script:
193 |
194 | * if there are no changes, no action is taken
195 | * if there are changes
196 | * git add .; commit all with automated message
197 | * push the `translations` branch to origin
198 | * create a PR from `translations` branch to default branch for that repository
199 | * open the URL for the PR
200 |
201 | | | |
202 | |:--|:--|
203 | |**Optional arguments**: | the name of the `translations` branch can be changed to the first argument |
204 | | **Products**: | a PR will be opened, or updated, for every submodule for which new localizations are imported |
205 | | **Warnings**: | If there are out-of-date `translations` branches on submodule GitHub repositories from an older translation session, you will get an error
**However**, current branches can be used and will accept updated commits if more than one download is used for this session. |
206 | | | |
207 |
208 | **Descriptive summary** of action for each submodule by the `manual_finalize_translations.sh` script.
209 |
210 | Once all the diffs have been reviewed and you are ready to proceed, run this script:
211 |
212 | ```
213 | ./Scripts/manual_finalize_translations.sh
214 | ```
215 |
216 | Assuming the permission are ok for each repository that is being changed, this should run without errors, create the PRs and open each one.
217 |
218 | If the person running the script does not have appropriate permissions to push the branch or if the branch exists at GitHub and is out of date, the commits are already made for that repository before attempting to push, so the user can just run the script again to proceed to the next repository.
219 |
220 | The skipped PR need to be handled separately. But really the person running the script should have permissions to open new PR and the `translations` branches should all be trimmed when the PR are merged so there won't be a conflict next time.
221 |
222 | If an error is seen with this hint - you need to go to GitHub and trim the translations branch and then push and create the pr manually:
223 |
224 | > Updates were rejected because the tip of your current branch is behind its remote counterpart.
225 |
226 | ### Review the Open PR and merge
227 |
228 | At this point, get someone to approve each of the open PR and merge them. Be sure to trim the `translations` branch once the PR are merged.
229 |
230 | ## Finalize with PR to LoopWorkspace
231 |
232 | Once all the PR submodules are merged, prepare your local LoopWorkspace clone to use the submodule PR that were just merged; `Scripts/update_submodules_ref.sh` can do this for you.
233 |
234 | * The only changes to LoopWorkspace when running this script should be the localization changes in the submodules
235 | * You can include additional changes, but they need to be committed either before or after running this script
236 |
237 | > Before running this script, ensure that code builds using Mac-Xcode GUI.
238 |
239 | Run the script to prepare the PR to update LoopWorkspace.
240 |
241 | **Bullet summary** `manual_LoopWorkspace_prepare_pr.sh` script:
242 |
243 | * create `translations` branch for LoopWorkspace (if one does not exist)
244 | * commit all changes in tracked files for LoopWorkspace and prepare
245 | * `git commit -a -F` using the automated commit message file
246 | * push the `translations` branch to origin
247 | * create a PR from `translations` branch to `dev` branch for LoopWorkspace
248 | * open the URL for the PR
249 |
250 | Update the version number and add that commit to the PR before merging.
251 |
252 | Allow time for testing and be sure Mac Xcode Build and Browser Build are successful.
253 |
254 | | | |
255 | |:--|:--|
256 | |**Optional arguments**: | - the name of the `translations` branch can be changed to the first argument
- the name of the target branch (`dev`) can be changed to the second argument|
257 | | **Products**: | a PR will be opened with the modified version of LoopWorkspace with all modified submodules updated |
258 | | **Warnings**: | this should be run only once after all submodule PRs are merged and LoopWorkspace diffs should be limited to submodule updates
Additional changes should be pushed as separate commit |
259 | | | |
260 |
261 | ```
262 | ./Scripts/manual_LoopWorkspace_prepare_pr.sh
263 | ```
264 |
265 | ## From LoopWorkspace to lokalise
266 |
267 | ### Prepare xliff_out folder
268 |
269 | The `manual_export_translations.sh` script is used to prepare xliff files to be uploaded to lokalise for translation.
270 |
271 | It is normally required for any code updates that add or modify the strings that require localization.
272 |
273 | First navigate to the LoopWorkspace directory in the appropriate branch, normally this is the `dev` branch. Make sure it is fully up to date with GitHub.
274 |
275 | Make sure the Xcode workspace is **not** open on your Mac or this will fail.
276 |
277 | ```
278 | ./Scripts/manual_export_localizations.sh
279 | ```
280 |
281 | This creates an xliff_out folder filled with xliff files, one for each language, that contains all the keys and strings for the entire clone (including all submodules).
282 |
283 | ### Update lokalise strings
284 |
285 | This script requires Read/Write token for lokalise. It uploads the xliff file for each language in the Xliff_out folder.
286 |
287 | ```
288 | ./Scripts/manual_upload_to_lokalise.sh
289 | ```
290 |
291 | ## Utility Scripts
292 |
293 | Once the import and export process is completed, you can delete temporary files and folders using:
294 |
295 | ```
296 | ./Scripts/manual_cleanup.sh
297 | ```
298 |
299 | The define_common.sh is used by other scripts to provide a single source for the list of:
300 |
301 | * filename with message indicating download time from lokalise for commit messages and PR titles
302 | * branch names used by some of the scripts for output and input
303 | * LANGUAGES (list of all languages to be included)
304 | * PROJECTS (all the submodules for LoopWorkspace to localize with owners and branches)
305 |
306 | If you need to start over but don't want to lose prior work, use archive_translations.sh. However, this is probably no longer necessary with the optional arguments available with the manual scripts.
307 |
308 | ## Questions and notes
309 |
310 | Most of the questions were worked through while developing the new scripts.
311 |
312 | #### Keys uploaded that not require translation
313 |
314 | * **Answer** Mark them as not visible to translators.
315 |
316 | * **Details**
317 |
318 | > The current method uploads some keys that do not need to be translated. Initially, a few keys were deleted from lokalise, but on the next upload, they were restored. So the next modification was to mark the keys as not visible to the translators.
319 |
320 | > Items already translated are brought down one time - go on and include those diffs and then next cycle, these should no longer be a problem.
321 |
322 | > Keys that were deleted on 2025-07-27, then later are restored as empty, CFBundleGetInfoString, CFBundleNames, NSHumanReadableCopyright
323 |
324 | #### White space changes
325 |
326 | * **Details** removed from this file
327 |
328 | * **Follow up** This is no longer an issue with String Catalogs.
329 |
330 | #### Downloaded Translations duplicated in Xcode
331 |
332 | * **Details** removed from this file
333 |
334 | * **Follow up** This is no longer an issue with String Catalogs.
335 |
336 | #### Status on 2025-08-10
337 |
338 | Updated the LocalizationInstructions.md file after running through the sequence documented in this file:
339 |
340 | 1. Download from lokalise (manual_download_from_lokalise.sh)
341 | 2. Import into LoopWorkspace (manual_import_localizations.sh)
342 | 3. Review Differences (manual_review_translations.sh)
343 | 4. Commit Submodule Changes and Create PRs (manual_finalize_translations.md)
344 |
345 | Only 4 PR were opened for this test, which were subsequently closed without merging. They helped with the testing process.
346 |
347 | #### Status on 2025-08-24
348 |
349 | Additional changes were made to the scripts and translations were merged into PR for 15 repositories from the download on 2025-08-24.
350 |
351 | #### Status on 2025-08-30
352 |
353 | Another cycle was completed, that included an upload to lokalise from the in-progress translations changes. Then a new download was processed.
354 |
355 | The final step to test is the creation of the PR for LoopWorkspace dev branch. To do this, the final script will be tested.
356 |
357 | #### Status on 2025-09-07
358 |
359 | The transition to String Catalogs is in process using the branch name `convert_to_xcstrings`. Several commits will be added to the submodules PRs before they are finally merged.
360 |
361 | While doing that work, a temporary LoopWorkspace branch is in use for testing. Once completed, this branch will be trimmed.
362 | * https://github.com/LoopKit/LoopWorkspace/commits/prepare_workspace_convert_to_xcstrings/
363 |
364 | **Summary**:
365 | 1. The uploaded files to lokalise have all been converted to String Catalogs.
366 |
367 | 2. The duplicate finder tool was run at lokalise to capture translations that already existed by linking terms.
368 |
369 | 3. Some additional strings were identified (or removed from) localization for the Loop submodule and added to the in-process PR.
370 |
371 | 4. Some additional Xcode settings may be required and will also be added to the open PRs.
372 |
--------------------------------------------------------------------------------
/fastlane/testflight.md:
--------------------------------------------------------------------------------
1 | # Using GitHub Actions + FastLane to deploy to TestFlight
2 |
3 | These instructions allow you to build your app without having access to a Mac.
4 |
5 | * You can install your app on phones using TestFlight that are not connected to your computer
6 | * You can send builds and updates to those you care for
7 | * You can install your app 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 | > The browser build **defaults to** automatically updating and building a new version of Loop according to this schedule:
13 | > - automatically checks for updates weekly and if updates are found, it will build a new version of the app
14 | > - even when there are no updates, it builds on the second Sunday of the month
15 | > - with each scheduled weekly run, a successful build log appears - if the time is very short, it did not need to build - only the long actions (>20 minutes) built a new app
16 | >
17 | > The [**Optional**](#optional) section provides instructions to modify the default behavior if desired.
18 |
19 | > **Repeat Builders**
20 | > - to enable automatic build, your `GH_PAT` token must have `workflow` scope
21 | > - if you previously configured your `GH_PAT` without that scope, see [`GH_PAT` `workflow` permission](#gh_pat-workflow-permission)
22 |
23 | ## Introduction
24 |
25 | 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.
26 |
27 | There are more detailed instructions in LoopDocs for using GitHub for Browser Builds, including troubleshooting and build errors. Please refer to:
28 |
29 | * [LoopDocs: Browser Overview](https://loopkit.github.io/loopdocs/browser/bb-overview/)
30 | * [LoopDocs: Errors with Browser](https://loopkit.github.io/loopdocs/browser/bb-errors/)
31 |
32 | 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/browser/phone-install/#testflight-for-a-child).
33 |
34 | If you build multiple apps, it is strongly recommended that you configure a free *GitHub* organization and do all your building in the organization. This means you enter items one time for the organization (6 SECRETS required to build and 1 VARIABLE required to automatically update your certificates annually). Otherwise, those 6 SECRETS must be entered for every repository. Please refer to [LoopDocs: Create a *GitHub* Organization](https://loopkit.github.io/loopdocs/browser/secrets/#create-a-free-github-organization).
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 identified 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 | Refer to [LoopDocs: Make a Secrets Reference File](https://loopkit.github.io/loopdocs/browser/intro-summary/#make-a-secrets-reference-file) for a handy template to use when saving your Secrets.
52 |
53 | ## Generate App Store Connect API Key
54 |
55 | 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.
56 |
57 | 1. Sign in to the [Apple developer portal page](https://developer.apple.com/account/resources/certificates/list).
58 | 1. Copy the Team ID from the upper right of the screen. Record this as your `TEAMID`.
59 | 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".
60 | 1. Record the issuer id; this will be used for `FASTLANE_ISSUER_ID`.
61 | 1. Record the key id; this will be used for `FASTLANE_KEY_ID`.
62 | 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.
63 |
64 | ## Create GitHub Personal Access Token
65 |
66 | If you have previously built another app using the "browser build" method, you use the same personal access token (`GH_PAT`), so skip this step. If you use a free GitHub organization to build, you still use the same personal access token. This is created using your personal GitHub username.
67 |
68 | Log into your GitHub account to create a personal access token; this is one of two GitHub secrets needed for your build.
69 |
70 | 1. Create a [new personal access token](https://github.com/settings/tokens/new):
71 | * Enter a name for your token, use "FastLane Access Token".
72 | * Change the Expiration selection to `No expiration`.
73 | * Select the `workflow` permission scope - this also selects `repo` scope.
74 | * Click "Generate token".
75 | * Copy the token and record it. It will be used below as `GH_PAT`.
76 |
77 | ## Make up a Password
78 |
79 | This is the second one of two GitHub secrets needed for your build.
80 |
81 | 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).
82 |
83 | ## GitHub Match-Secrets Repository
84 |
85 | A private Match-Secrets repository is automatically created under your GitHub username the first time you run a GitHub Action. Because it is a private repository - only you can see it. You will not take any direct actions with this repository; it needs to be there for 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 GitHub username (using your organization if you have one). If you already have a fork of LoopWorkspace in that username, you should not make another one. Do not rename the repository. You can continue to work with your existing fork, or delete that from GitHub and then fork again.
90 | 1. If you are using an organization, do this step at the organization level, e.g., username-org. If you are not using an organization, do this step at the repository level, e.g., username/LoopWorkspace:
91 | * Go to Settings -> Secrets and variables -> Actions and make sure the Secrets tab is open
92 | 1. For each of the following secrets, tap on "New organization secret" or "New repository secret", then add the name of the secret, along with the value you recorded for it:
93 | * `TEAMID`
94 | * `FASTLANE_ISSUER_ID`
95 | * `FASTLANE_KEY_ID`
96 | * `FASTLANE_KEY`
97 | * `GH_PAT`
98 | * `MATCH_PASSWORD`
99 | 1. If you are using an organization, do this step at the organization level, e.g., username-org. If you are not using an organization, do this step at the repository level, e.g., username/LoopWorkspace:
100 | * Go to Settings -> Secrets and variables -> Actions and make sure the Variables tab is open
101 | 1. Tap on "Create new organization variable" or "Create new repository variable", then add the name below and enter the value true. Unlike secrets, variables are visible and can be edited.
102 | * `ENABLE_NUKE_CERTS`
103 |
104 | ## Validate repository secrets
105 |
106 | This step validates most of your six Secrets and provides error messages if it detects an issue with one or more.
107 |
108 | 1. Click on the "Actions" tab of your LoopWorkspace repository and enable workflows if needed
109 | 1. On the left side, select "1. Validate Secrets".
110 | 1. On the right side, click "Run Workflow", and tap the green `Run workflow` button.
111 | 1. Wait, and within a minute or two you should see a green checkmark indicating the workflow succeeded.
112 | 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.
113 |
114 | There can be a delay after you start a workflow before the screen changes. Refresh your browser to see if it started. And if it seems to take a long time to finish - refresh your browser to see if it is done.
115 |
116 | ## Add Identifiers for Loop App
117 |
118 | 1. Click on the "Actions" tab of your LoopWorkspace repository.
119 | 1. On the left side, select "2. Add Identifiers".
120 | 1. On the right side, click "Run Workflow", and tap the green `Run workflow` button.
121 | 1. Wait, and within a minute or two you should see a green checkmark indicating the workflow succeeded.
122 |
123 | > New with changes *Apple* instituted in May 2025. There is one capability for the main Loop Identifier that has to be manually added. The Add Identifiers action cannot do it for you. Details are found at [Add Time Sensitive Capability](#add-time-sensitive-capability).
124 |
125 | ## Create App Group
126 |
127 | If you have already built Loop via Xcode using this Apple ID, you can skip ahead to [Add App Group to Bundle Identifiers](#add-app-group-to-bundle-identifiers).
128 |
129 | 1. Go to [Register an App Group](https://developer.apple.com/account/resources/identifiers/applicationGroup/add/) on the Apple Developer site.
130 | 1. For Description, use "Loop App Group".
131 | 1. For Identifier, enter "group.com.TEAMID.loopkit.LoopGroup", substituting your team id for `TEAMID`.
132 | 1. Click "Continue" and then "Register".
133 |
134 | ## Add App Group to Bundle Identifiers
135 |
136 | 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.
137 |
138 | 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.
139 |
140 | 1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) on the Apple Developer site.
141 | 1. For each of the following identifier names:
142 | * Loop (see Add Time Sensitive Capability)
143 | * Loop Intent Extension
144 | * Loop Status Extension
145 | * Loop Widget Extension
146 | 1. Click on the identifier's name.
147 | 1. On the "App Groups" capabilities, click on the "Configure" button.
148 | 1. Select the "Loop App Group"
149 | 1. Click "Continue".
150 | 1. Click "Save".
151 | 1. Click "Confirm".
152 | 1. Remember to do this for each of the identifiers above.
153 |
154 | ### Add Time Sensitive Capability
155 |
156 | For the Loop Identifier, you must manually add a capability if it is not already selected.
157 |
158 | 1. Go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) on the Apple Developer site.
159 | 1. Click on the Loop identifier name
160 | 1, Scroll down on the screen looking at Capabilities until you reach `Time Sensitive Notifications`
161 | 1. Make sure the Enable box to the left is selected - if you make a change, then you need to save the Identifier
162 |
163 | #### Table with Name and Identifier for Loop
164 |
165 | | NAME | IDENTIFIER |
166 | |-------|------------|
167 | | Loop | com.TEAMID.loopkit.Loop |
168 | | Loop Intent Extension | com.TEAMID.loopkit.Loop.Loop-Intent-Extension |
169 | | Loop Status Extension | com.TEAMID.loopkit.Loop.statuswidget |
170 | | Loop Widget Extension | com.TEAMID.loopkit.Loop.LoopWidgetExtension |
171 | | WatchApp | com.TEAMID.loopkit.Loop.LoopWatch |
172 | | WatchAppExtension | com.TEAMID.loopkit.Loop.LoopWatch.watchkitextension |
173 |
174 |
175 | ## Create Loop App in App Store Connect
176 |
177 | If you have created a Loop app in App Store Connect before, you can skip this section.
178 |
179 | 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.
180 | * Select "iOS".
181 | * 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.
182 | * Select your primary language.
183 | * Choose the bundle ID that matches `com.TEAMID.loopkit.Loop`, with TEAMID matching your team id.
184 | * SKU can be anything; e.g. "123".
185 | * Select "Full Access".
186 | 1. Click Create
187 |
188 | You do not need to fill out the next form. That is for submitting to the app store.
189 |
190 | ## Create Building Certificates
191 |
192 | This step is no longer required. The build action now takes care of this for you. It does not hurt to run it but is not needed.
193 |
194 | Once a year, you will get an email from Apple indicating your certificate will expire in 30 days. You can ignore that email. When it does expire, the next time an automatic or manual build happens, the expired certificate information will be removed (nuked) from your Match-Secrets repository and a new one created. This should happen without you needing to take any action.
195 |
196 | ## Build Loop
197 |
198 | 1. Click on the "Actions" tab of your LoopWorkspace repository.
199 | 1. On the left side, select "4. Build Loop".
200 | 1. On the right side, click "Run Workflow", and tap the green `Run workflow` button.
201 | 1. You have some time now. Go enjoy a coffee. The build should take about 20-30 minutes.
202 | 1. Your app should eventually appear on [App Store Connect](https://appstoreconnect.apple.com/apps).
203 | 1. For each phone/person you would like to support Loop on:
204 | * Add them in [Users and Access](https://appstoreconnect.apple.com/access/users) on App Store Connect.
205 | * Add them to your TestFlight Internal Testing group.
206 |
207 | ## TestFlight and Deployment Details
208 |
209 | Please refer to [LoopDocs: TestFlight Overview](https://loopkit.github.io/loopdocs/browser/tf-users) and [LoopDocs: Install on Phone](https://loopkit.github.io/loopdocs/browser/phone-install/)
210 |
211 | ## Automatic Build FAQs
212 |
213 | 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. You may need to manually enable your build action and manually execute it when your fork becomes stale.
214 |
215 | ## OPTIONAL
216 |
217 | What if you don't want to allow automated updates of the repository or automatic builds?
218 |
219 | You can affect the default behavior:
220 |
221 | 1. [`GH_PAT` `workflow` permission](#gh_pat-workflow-permission)
222 | 1. [Modify scheduled building and synchronization](#modify-scheduled-building-and-synchronization)
223 |
224 | ### `GH_PAT` `workflow` permission
225 |
226 | 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.
227 |
228 | 1. Go to your [FastLane Access Token](https://github.com/settings/tokens)
229 | 2. It should say `repo`, `workflow` next to the `FastLane Access Token` link
230 | 3. If it does not, click on the link to open the token detail view
231 | 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)
232 | 5. Scroll all the way down to and click the green `Update token` button
233 | 6. Your token now holds both required permissions
234 |
235 | 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.
236 |
237 | ### Modify scheduled building and synchronization
238 |
239 | You can modify the automation by creating and using some variables.
240 |
241 | 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).
242 |
243 | Note that the weekly build 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**.
244 |
245 | * If you want to manually decide when to update your repository to the latest commit, but you want the monthly builds to continue: set `SCHEDULED_SYNC` to false and either do not create `SCHEDULED_BUILD` or set it to true
246 | * 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
247 | * **Warning**: if no updates to your default branch are detected within 90 days, your previous TestFlight build may expire requiring a manual build
248 |
249 | |`SCHEDULED_SYNC`|`SCHEDULED_BUILD`|Automatic Actions|
250 | |---|---|---|
251 | | `true` (or NA) | `true` (or NA) | weekly update check (auto update/build), monthly build with auto update|
252 | | `true` (or NA) | `false` | weekly update check with auto update, only builds if update detected|
253 | | `false` | `true` (or NA) | monthly build, no auto update |
254 | | `false` | `false` | no automatic activity|
255 |
256 | ### How to configure a variable
257 |
258 | 1. Go to the "Settings" tab of your repository (to modify a single repository schedule) or your organization to affect all repositories.
259 | 2. Click on `Secrets and Variables`.
260 | 3. Click on `Actions`
261 | 4. You will now see a page titled *Actions secrets and variables*. Click on the `Variables` tab
262 | 5. To disable ONLY scheduled building, do the following:
263 | - Click on the green `New repository variable` button (upper right)
264 | - Type `SCHEDULED_BUILD` in the "Name" field
265 | - Type `false` in the "Value" field
266 | - Click the green `Add variable` button to save.
267 | 7. To disable scheduled syncing, add a variable:
268 | - Click on the green `New repository variable` button (upper right)
269 | - - Type `SCHEDULED_SYNC` in the "Name" field
270 | - Type `false` in the "Value" field
271 | - Click the green `Add variable` button to save
272 |
273 | Your build will run on the following conditions:
274 | - Default behaviour:
275 | - Run weekly every Sunday
276 | - If updates are detected, it will update your repository and build
277 | - If it is the second Sunday of the month, it will build even when no changes are detected
278 | - If you disable any automation (both variables set to `false`), no updates or building happens when the build action runs
279 | - If you disabled just scheduled synchronization (`SCHEDULED_SYNC` set to`false`), it will still build once a month, but no update will happen
280 | - If you disabled just scheduled build (`SCHEDULED_BUILD` set to`false`), it will run once weekly, to check for changes; if there are changes, it will update and build
281 |
282 | ## What if I build using more than one GitHub username
283 |
284 | This is not typical. But if you do use more than one GitHub username, follow these steps at the time of the annual certificate renewal.
285 |
286 | 1. After the certificates were removed (nuked) from username1 Match-Secrets storage, you need to switch to username2
287 | 1. Add the variable FORCE_NUKE_CERTS=true to the username2/LoopWorkspace repository
288 | 1. Run the action Create Certificate (or Build, but Create is faster)
289 | 1. Immediately set FORCE_NUKE_CERTS=false or delete the variable
290 |
291 | Now certificates for username2 have been cleared out of Match-Secrets storage for username2. Building can proceed as usual for both username1 and username2.
292 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------