Module: Adsedare
- Extended by:
- Logging
- Defined in:
- lib/adsedare.rb,
lib/adsedare/version.rb,
lib/adsedare/keychain.rb,
lib/adsedare/xcodeproj.rb,
lib/adsedare/capabilities.rb,
lib/adsedare/export_options.rb,
lib/adsedare/install_profiles.rb
Defined Under Namespace
Classes: AppGroupsCapability, Capability, Error, SimpleCapability
Constant Summary collapse
- VERSION =
"0.0.10"
- APPLE_CERTS =
[ "AppleWWDRCAG2.cer", "AppleWWDRCAG3.cer", "AppleWWDRCAG4.cer", "AppleWWDRCAG5.cer", "AppleWWDRCAG6.cer", "AppleWWDRCAG7.cer", "AppleWWDRCAG8.cer", "DeveloperIDG2CA.cer", ]
- APPLE_CERTS_URL =
"https://www.apple.com/certificateauthority"
- APPLE_WWDRCA =
"https://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
- ENTITLEMENTS_MAPPING =
{ "com.apple.security.application-groups" => "APP_GROUPS", "com.apple.developer.in-app-payments" => "APPLE_PAY", "com.apple.developer.associated-domains" => "ASSOCIATED_DOMAINS", "com.apple.developer.healthkit" => "HEALTHKIT", "com.apple.developer.homekit" => "HOMEKIT", "com.apple.developer.networking.HotspotConfiguration" => "HOTSPOT", "com.apple.developer.networking.multipath" => "MULTIPATH", "com.apple.developer.networking.networkextension" => "NETWORK_EXTENSION", "com.apple.developer.nfc.readersession.formats" => "NFC_TAG_READING", "com.apple.developer.networking.vpn.api" => "PERSONAL_VPN", "com.apple.external-accessory.wireless-configuration" => "WIRELESS_ACCESSORY_CONFIGURATION", "com.apple.developer.siri" => "SIRI", "com.apple.developer.pass-type-identifiers" => "WALLET", "com.apple.developer.icloud-services" => "ICLOUD", "com.apple.developer.icloud-container-identifiers" => "ICLOUD", "com.apple.developer.ubiquity-container-identifiers" => "ICLOUD", "com.apple.developer.ubiquity-kvstore-identifier" => "ICLOUD", "com.apple.developer.ClassKit-environment" => "CLASSKIT", "com.apple.developer.authentication-services.autofill-credential-provider" => "AUTOFILL_CREDENTIAL_PROVIDER", "com.apple.developer.applesignin" => "SIGN_IN_WITH_APPLE", "com.apple.developer.usernotifications.communication" => "COMMUNICATION_NOTIFICATIONS", "com.apple.developer.usernotifications.time-sensitive" => "USERNOTIFICATIONS_TIMESENSITIVE", "com.apple.developer.group-session" => "GROUP_ACTIVITIES", "com.apple.developer.family-controls" => "FAMILY_CONTROLS", "com.apple.developer.devicecheck.appattest-environment" => "APP_ATTEST", "com.apple.developer.game-center" => "GAME_CENTER", "com.apple.developer.carplay-maps" => "CARPLAY_NAVIGATION", }
Class Method Summary collapse
-
.create_keychain(keychain_path = nil, keychain_password = nil, make_default = true) ⇒ void
Create a build keychain with all required intermediate certificates Set following environment variables to add project specific certificates:.
- .get_bundle_map(team_id) ⇒ Object
- .get_devices(team_id) ⇒ Object
- .get_profiles_map(team_id) ⇒ Object
-
.install_profiles(project_path = nil) ⇒ void
Install provisioning profiles for a project Expects environment variables to be set:.
-
.make_export_options(project_path = nil, export_path = nil, team_id = nil, options = {}) ⇒ void
Create export options for a project Expects environment variables to be set:.
-
.patch_project(project_path, team_id = nil) ⇒ void
Patch a project with App Store Connect profiles & settings for ad-hoc distribution Will overwrite Team ID in project if provided Expects environment variables to be set:.
- .renew_bundle_id(bundle_id, team_id, entitlements_path) ⇒ Object
-
.renew_profiles(project_path = nil, certificate_id = nil, team_id = nil) ⇒ void
Renew profiles for a project Expects environment variables to be set:.
- .renew_provisioning_profile(profile, team_id) ⇒ Object
Methods included from Logging
configure_logger_for, logger, logger_for
Class Method Details
.create_keychain(keychain_path = nil, keychain_password = nil, make_default = true) ⇒ void
This method returns an undefined value.
Create a build keychain with all required intermediate certificates Set following environment variables to add project specific certificates:
-
AD_HOC_CERTIFICATE Path to the ad-hoc certificate
-
AD_HOC_PRIVATE_KEY Path to the ad-hoc private key
-
AD_HOC_KEY_PASSWORD Password for the ad-hoc private key
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/adsedare/keychain.rb', line 35 def create_keychain(keychain_path = nil, keychain_password = nil, make_default = true) raise "Keychain path is not set" unless keychain_path raise "Keychain password is not set" unless keychain_password keychain_path = File.(keychain_path) logger.info "Creating keychain at '#{keychain_path}'" FileUtils.mkdir_p(File.dirname(keychain_path)) status = system("security create-keychain -p #{keychain_password} #{keychain_path}") unless status logger.error "Failed to create keychain at '#{keychain_path}'" return end status = system("security list-keychains -d user -s #{keychain_path}") unless status logger.error "Failed to add keychain to search list" return end APPLE_CERTS.each do |cert| logger.info "Downloading certificate '#{cert}'" response = Faraday.get( "#{APPLE_CERTS_URL}/#{cert}" ) unless response.status == 200 logger.error "Failed to download certificate '#{cert}'" next end file = Tempfile.new(cert) file.write(response.body) file.close install_certificate(file.path, keychain_path) file.unlink end logger.info "Downloading certificate 'AppleWWDRCA.cer'" response = Faraday.get( APPLE_WWDRCA ) unless response.status == 200 logger.error "Failed to download certificate 'AppleWWDRCA.cer'" else file = Tempfile.new("AppleWWDRCA.cer") file.write(response.body) file.close install_certificate(file.path, keychain_path) file.unlink end ad_hoc_certificate = ENV["AD_HOC_CERTIFICATE"] ad_hoc_private_key = ENV["AD_HOC_PRIVATE_KEY"] ad_hoc_key_password = ENV["AD_HOC_KEY_PASSWORD"] unless ad_hoc_certificate || ad_hoc_private_key || ad_hoc_key_password logger.warn "AD_HOC_CERTIFICATE, AD_HOC_PRIVATE_KEY, or AD_HOC_KEY_PASSWORD is not set" return end install_certificate(ad_hoc_certificate, keychain_path, "", "cert") install_certificate(ad_hoc_private_key, keychain_path, ad_hoc_key_password, "priv") if make_default status = system("security default-keychain -d user -s #{keychain_path}") unless status logger.warn "Failed to set default keychain" return end end status = system("security set-keychain-settings #{keychain_path}") unless status logger.error "Failed to set keychain settings" return end status = system("security set-key-partition-list -S apple-tool:,apple: -k #{keychain_password} #{keychain_path}") unless status logger.error "Failed to set keychain partition list" return end status = system("security unlock-keychain -p #{keychain_password} #{keychain_path}") unless status logger.error "Failed to unlock keychain" return end logger.info "Keychain created at '#{keychain_path}'" status = system("security find-identity -p codesigning") unless status logger.error "Failed to find codesigning identity" return end end |
.get_bundle_map(team_id) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/adsedare.rb', line 123 def get_bundle_map(team_id) logger.info "Fetching bundle IDs for team ID '#{team_id}'" registered_bundles = Starship::Client.get_bundle_ids(team_id) bundle_ids = {} registered_bundles.each do |bundle| bundle_ids[bundle["attributes"]["identifier"]] = bundle["id"] end return bundle_ids end |
.get_devices(team_id) ⇒ Object
103 104 105 |
# File 'lib/adsedare.rb', line 103 def get_devices(team_id) Starship::Client.get_devices(team_id) end |
.get_profiles_map(team_id) ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/adsedare.rb', line 107 def get_profiles_map(team_id) logger.info "Fetching profiles for team ID '#{team_id}'" registered_profiles = Starship::Client.get_profiles(team_id) profiles = {} registered_profiles.each do |profile| provisioning_profile = Starship::Client::get_provisioning_profile(profile["id"], team_id) app_id = provisioning_profile["provisioningProfile"]["appIdId"] profiles[app_id] = provisioning_profile end return profiles end |
.install_profiles(project_path = nil) ⇒ void
This method returns an undefined value.
Install provisioning profiles for a project Expects environment variables to be set:
-
APPSTORE_CONNECT_KEY_ID Key ID from Apple Developer Portal
-
APPSTORE_CONNECT_ISSUER_ID Issuer ID from Apple Developer Portal
-
APPSTORE_CONNECT_KEY P8 key content from Apple Developer Portal
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/adsedare/install_profiles.rb', line 18 def install_profiles(project_path = nil) raise "Project path is not set" unless project_path project = Xcodeproj::Project.open(project_path) project_bundles = project.targets.map do |target| target.build_configurations.map do |config| config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] end end.flatten.uniq bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles) bundle_by_identifier = {} profiles_by_id = {} bundles_with_profiles["data"].each do |bundle_id| bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id end bundles_with_profiles["included"].each do |profile| profiles_by_id[profile["id"]] = profile end project_bundles.each do |bundle_identifier| bundle_id = bundle_by_identifier[bundle_identifier] unless bundle_id logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping." next end logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id["id"]}'" profiles = bundle_id["relationships"]["profiles"]["data"] unless profiles logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping." next end ad_hoc_profile = nil profiles.each do |profile| profile_id = profile["id"] profile = profiles_by_id[profile_id] profile_name = profile["attributes"]["name"] # Xcode managed profiles can't be used for signing if profile_name.start_with?("iOS Team Ad Hoc Provisioning Profile") || profile_name.start_with?("XC") next end if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE" ad_hoc_profile = profile break end end unless ad_hoc_profile logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping." next end logger.info "Profile for Bundle ID '#{bundle_id["id"]}' resolved to Profile '#{ad_hoc_profile["attributes"]["name"]}'" uuid = ad_hoc_profile["attributes"]["uuid"] profile_content = Base64.decode64(ad_hoc_profile["attributes"]["profileContent"]) profile_path = "#{Dir.home}/Library/MobileDevice/Provisioning Profiles/#{uuid}.mobileprovision" FileUtils.mkdir_p(File.dirname(profile_path)) File.write(profile_path, profile_content) logger.info "Profile '#{ad_hoc_profile["attributes"]["name"]}' installed to '#{profile_path}'" end end |
.make_export_options(project_path = nil, export_path = nil, team_id = nil, options = {}) ⇒ void
This method returns an undefined value.
Create export options for a project Expects environment variables to be set:
-
APPSTORE_CONNECT_KEY_ID Key ID from Apple Developer Portal
-
APPSTORE_CONNECT_ISSUER_ID Issuer ID from Apple Developer Portal
-
APPSTORE_CONNECT_KEY P8 key content from Apple Developer Portal
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/adsedare/export_options.rb', line 20 def (project_path = nil, export_path = nil, team_id = nil, = {}) raise "Project path is not set" unless project_path raise "Export path is not set" unless export_path logger.info "Creating export options for project" project = Xcodeproj::Project.open(project_path) = { "method" => "ad-hoc", "destination" => "export", "signingStyle" => "manual", "signingCertificate" => "Apple Distribution", "provisioningProfiles" => {}, }.merge() project_bundles = [] project.targets.each do |target| target.build_configurations.each do |config| team_id ||= config.build_settings["DEVELOPMENT_TEAM"] project_bundles << config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] end end ["teamID"] = team_id logger.info "Fetching bundles with profiles for team ID '#{team_id}'" bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles) bundle_by_identifier = {} profiles_by_id = {} bundles_with_profiles["data"].each do |bundle_id| bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id end bundles_with_profiles["included"].each do |profile| profiles_by_id[profile["id"]] = profile end project_bundles.each do |bundle_identifier| bundle_id = bundle_by_identifier[bundle_identifier] unless bundle_id logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping." next end logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id["id"]}'" profiles = bundle_id["relationships"]["profiles"]["data"] unless profiles logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping." next end ad_hoc_profile = nil profiles.each do |profile| profile_id = profile["id"] profile = profiles_by_id[profile_id] profile_name = profile["attributes"]["name"] # Xcode managed profiles can't be used for signing if profile_name.start_with?("iOS Team Ad Hoc Provisioning Profile") || profile_name.start_with?("XC") next end if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE" ad_hoc_profile = profile break end end unless ad_hoc_profile logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping." next end logger.info "Profile for Bundle ID '#{bundle_id["id"]}' resolved to Profile '#{ad_hoc_profile["attributes"]["name"]}'" profile_name = ad_hoc_profile["attributes"]["name"] ["provisioningProfiles"][bundle_identifier] = profile_name end = Plist::Emit.dump() export_path = File.(export_path) File.write(export_path, ) logger.info "Export options created at '#{export_path}'" end |
.patch_project(project_path, team_id = nil) ⇒ void
This method returns an undefined value.
Patch a project with App Store Connect profiles & settings for ad-hoc distribution Will overwrite Team ID in project if provided Expects environment variables to be set:
-
APPSTORE_CONNECT_KEY_ID Key ID from Apple Developer Portal
-
APPSTORE_CONNECT_ISSUER_ID Issuer ID from Apple Developer Portal
-
APPSTORE_CONNECT_KEY P8 key content from Apple Developer Portal
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/adsedare/xcodeproj.rb', line 19 def patch_project(project_path, team_id = nil) raise "Project path is not set" unless project_path project = Xcodeproj::Project.open(project_path) project_bundles = project.targets.map do |target| target.build_configurations.map do |config| config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] end end.flatten.uniq bundles_with_profiles = AppStoreConnect::Client.get_bundles_with_profiles(project_bundles) bundle_by_identifier = {} profiles_by_id = {} bundles_with_profiles["data"].each do |bundle_id| bundle_by_identifier[bundle_id["attributes"]["identifier"]] = bundle_id end if bundles_with_profiles["included"] bundles_with_profiles["included"].each do |profile| profiles_by_id[profile["id"]] = profile end end project.targets.each do |target| target.build_configurations.each do |config| bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] bundle_id = bundle_by_identifier[bundle_identifier] unless bundle_id logger.warn "Bundle '#{bundle_identifier}' is missing in App Store Connect. Skipping." next end logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id["id"]}'" profiles = bundle_id["relationships"]["profiles"]["data"] unless profiles logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping." next end ad_hoc_profile = nil profiles.each do |profile| profile_id = profile["id"] profile = profiles_by_id[profile_id] profile_name = profile["attributes"]["name"] # Xcode managed profiles can't be used for signing if profile_name.start_with?("iOS Team Ad Hoc Provisioning Profile") || profile_name.start_with?("XC") next end if profile["attributes"]["profileType"] == "IOS_APP_ADHOC" && profile["attributes"]["profileState"] == "ACTIVE" ad_hoc_profile = profile break end end unless ad_hoc_profile logger.warn "Profile for Bundle ID '#{bundle_id["id"]}' is missing in App Store Connect. Skipping." next end config.build_settings["CODE_SIGN_IDENTITY"] = "iPhone Distribution" config.build_settings["CODE_SIGN_STYLE"] = "Manual" if team_id config.build_settings["DEVELOPMENT_TEAM"] = team_id end config.build_settings["PROVISIONING_PROFILE_SPECIFIER"] = ad_hoc_profile["attributes"]["name"] end end project.save end |
.renew_bundle_id(bundle_id, team_id, entitlements_path) ⇒ Object
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/adsedare.rb', line 170 def renew_bundle_id(bundle_id, team_id, entitlements_path) bundle_info = Starship::Client.get_bundle_info(bundle_id, team_id) bundle_identifier = bundle_info["data"]["attributes"]["identifier"] logger.info "Checking capabilities for bundle '#{bundle_identifier}'" capabilities = parse_entitlements(entitlements_path) need_update = false capabilities.each do |capability| if !capability.check?(bundle_info) logger.warn "Bundle '#{bundle_identifier}' is missing capability '#{capability.type}'." need_update = true end end if need_update logger.warn "Bundle '#{bundle_identifier}' is missing one or more capabilities." # You can't remove IN_APP_PURCHASE capability for some reason new_capabilities = capabilities + [SimpleCapability.new("IN_APP_PURCHASE")] new_capabilities = new_capabilities.map { |capability| capability.to_bundle_capability(bundle_info, team_id) } Starship::Client.patch_bundle(bundle_info, team_id, new_capabilities) logger.info "Bundle '#{bundle_identifier}' capabilities updated." else logger.info "Bundle '#{bundle_identifier}' capabilities are up to date." end end |
.renew_profiles(project_path = nil, certificate_id = nil, team_id = nil) ⇒ void
This method returns an undefined value.
Renew profiles for a project Expects environment variables to be set:
-
APPLE_DEVELOPER_USERNAME Apple Developer Portal username
-
APPLE_DEVELOPER_PASSWORD Apple Developer Portal password
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/adsedare.rb', line 36 def renew_profiles(project_path = nil, certificate_id = nil, team_id = nil) raise "Project path is not set" unless project_path raise "Certificate ID is not set" unless certificate_id project = Xcodeproj::Project.open(project_path) project_dir = File.dirname(project_path) bundle_entitlements = {} project.targets.each do |target| target.build_configurations.each do |config| bundle_identifier = config.build_settings["PRODUCT_BUNDLE_IDENTIFIER"] entitlements_path = config.build_settings["CODE_SIGN_ENTITLEMENTS"] # If team_id is not set, use the first one from the project team_id ||= config.build_settings["DEVELOPMENT_TEAM"] if entitlements_path full_entitlements_path = File.join(project_dir, entitlements_path) bundle_entitlements[bundle_identifier] = full_entitlements_path end end end bundle_by_identifier = get_bundle_map(team_id) profiles_by_bundle = get_profiles_map(team_id) bundle_entitlements.each do |bundle_identifier, entitlements_path| bundle_id = bundle_by_identifier[bundle_identifier] unless bundle_id logger.warn "Bundle '#{bundle_identifier}' is missing in Apple Developer portal. Will create." bundle_id = Starship::Client.create_bundle( bundle_identifier, team_id, # You cannot create bundle without this capability [SimpleCapability.new("IN_APP_PURCHASE").to_bundle_capability(nil, nil)] )["data"]["id"] bundle_by_identifier[bundle_identifier] = bundle_id logger.info "Bundle '#{bundle_identifier}' created with ID '#{bundle_id}'" else logger.info "Bundle '#{bundle_identifier}' resolved to Bundle ID '#{bundle_id}'" end renew_bundle_id(bundle_id, team_id, entitlements_path) profile = profiles_by_bundle[bundle_id] unless profile logger.warn "Profile for Bundle ID '#{bundle_id}' is missing in Apple Developer portal. Will create." devices = get_devices(team_id) profile_id = Starship::Client.create_provisioning_profile( team_id, bundle_id, bundle_identifier, certificate_id, devices )["data"]["id"] profiles_by_bundle[bundle_id] = Starship::Client.get_provisioning_profile(profile_id, team_id) profile = profiles_by_bundle[bundle_id] logger.info "Profile for Bundle ID '#{bundle_id}' created with ID '#{profile_id}'" else logger.info "Bundle ID '#{bundle_id}' resolved to Profile '#{profile["provisioningProfile"]["name"]}'" end renew_provisioning_profile(profile, team_id) end end |
.renew_provisioning_profile(profile, team_id) ⇒ Object
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/adsedare.rb', line 136 def renew_provisioning_profile(profile, team_id) devices = get_devices(team_id) deviceIds = devices.map { |device| device["id"] } profileDeviceIds = profile["provisioningProfile"]["devices"].map { |device| device["deviceId"] } need_update = false deviceIds.each do |deviceId| if !profileDeviceIds.include?(deviceId) need_update = true break end end logger.info "Profile '#{profile["provisioningProfile"]["name"]}' status: '#{profile["provisioningProfile"]["status"]}'" if profile["provisioningProfile"]["status"] != "Active" need_update = true end if need_update logger.warn "Profile '#{profile["provisioningProfile"]["name"]}' is missing one or more devices." Starship::Client.regen_provisioning_profile(profile, team_id, deviceIds) logger.info "Profile '#{profile["provisioningProfile"]["name"]}' updated." else logger.info "Profile '#{profile["provisioningProfile"]["name"]}' is up to date." end end |