Files
self/app/fastlane/Fastfile
Justin Hernandez b6d526e5f8 chore: update dev with staging 09/06/25 (#1007)
* update CI

* bump iOS version

* update readme

* update mobile-deploy ci

* bump version iOS

* update workflow to use workload identity federation (#933)

* update workflow to use workload identity federation

* add token permissions

* correct provider name

* chore: incrementing android build version for version 2.6.4 [github action]

---------

Co-authored-by: Self GitHub Actions <action@github.com>

* update ci

* update ci

* update ci

* update ci

* update ci

* fix ci

* fix ci

* fix ci

* remove fastlane use for android

* bump iOS build version

* update CI python script

* iterate on CI

* iterate on CI

* iterate on CI

* Dev (#941)

* SDK Go version (#920)

* feat: helper functions and constant for go-sdk

* feat: formatRevealedDataPacked in go

* chore: refactor

* feat: define struct for selfBackendVerifier

* feat: verify function for selfBackendVerifier

* feat(wip): custom hasher

* feat: SelfVerifierBacked in go

* test(wip): scope and userContextHash is failing

* test: zk proof verified

* fix: MockConfigStore getactionId function

* chore: refactor

* chore: remove abi duplicate files

* chore: move configStore to utils

* chore: modified VcAndDiscloseProof struct

* chore: more review changes

* feat: impl DefaultConfig and InMemoryConfigStore

* chore: refactor and export functions

* fix: module import and README

* chore: remove example folder

* chore: remove pointers from VerificationConfig

* chore: coderabbit review fixes

* chore: more coderabbit review fix

* chore: add license

* fix: convert attestationIdd to int

* chore: remove duplicate code

---------

Co-authored-by: ayman <aymanshaik1015@gmail.com>

* Moving proving Utils to common (#935)

* remove react dom

* moves proving utils to the common

* need to use rn components

* fix imports

* add proving-utils and dedeuplicate entry configs for esm and cjs.

* must wrap in text component

* fix metro bundling

* fix mock import

* fix builds and tests

* please save me

* solution?

* fix test

* Move proving inputs to the common package (#937)

* create ofactTree type to share

* move proving inputs from app to register inputs in common

* missed reexport

* ok

* add some validations as suggested by our ai overlords

* Fix mock passport flow (#942)

* fix dev screens

* add hint

* rename

* fix path

* fix mobile-ci path

* fix: extractMRZ (#938)

* fix: extractMRZ

* yarn nice && yarn types

* fix test: remove unused

* fix mobile ci

* add script

---------

Co-authored-by: Justin Hernandez <transphorm@gmail.com>

* Move Proving attest and cose (#950)

* moved attest and cose utils to common

with cursor converted tests in common to use vitest and converted coseVerify.test to vitest after moving from app to common

what does cryptoLoader do?

* moved away

* get buff

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* SELF-253 feat: add user email feedback (#889)

* feat: add sentry feedback

* add sentry feedback to web

* feat: add custom feedback modal & fix freeze on IOS

* yarn nice

* update lock

* feat: show feedback widget on NFC scan issues (#948)

* feat: show feedback widget on NFC scan issues

* fix ref

* clean up

* fix report issue screen

* abstract send user feedback email logic

* fixes

* change text to Report Issue

* sanitize email and track event messge

* remove unnecessary sanitization

* add sanitize error message tests

* fix tests

* save wip. almost done

* fix screen test

* fix screen test

* remove non working test

---------

Co-authored-by: Justin Hernandez <transphorm@gmail.com>
Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>

* chore: centralize license header checks (#952)

* chore: centralize license header scripts

* chore: run license header checks from root

* add header to other files

* add header to bundle

* add migration script and update check license headers

* convert license to mobile sdk

* migrate license headers

* remove headers from common; convert remaining

* fix headers

* add license header checks

* update unsupported passport screen (#953)

* update unsupported passport screen

* yarn nice

---------

Co-authored-by: Vishalkulkarni45 <109329073+Vishalkulkarni45@users.noreply.github.com>
Co-authored-by: ayman <aymanshaik1015@gmail.com>
Co-authored-by: Aaron DeRuvo <aaron.deruvo@clabs.co>
Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>
Co-authored-by: Seshanth.S🐺 <35675963+seshanthS@users.noreply.github.com>
Co-authored-by: Justin Hernandez <transphorm@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* bump version

* bump yarn.lock

* update ci (#966)

* chore: Manually bump and release v2.6.4 (#961)

* update lock files

* bump and build android

* update build artifacts

* show generate mock document button

* update lock

* fix formatting and update failing e2e test

* revert podfile

* fixes

* fix cold start of the app with deeplink

* update ci

* update ci

* Sync MARKETING_VERSION to iOS project files after version bump

* chore: incrementing android build version for version 2.6.4 [github action] (#976)

Co-authored-by: remicolin <98749896+remicolin@users.noreply.github.com>

* chore: add build dependencies step for iOS and Android in mobile deploy workflow

* chore: enhance mobile deploy workflow by adding CMake installation step

* bump android build version

* chore: incrementing android build version for version 2.6.4 [github action] (#985)

Co-authored-by: remicolin <98749896+remicolin@users.noreply.github.com>

* chore: configure Metro bundler for production compatibility in mobile deploy workflow

* chore: incrementing android build version for version 2.6.4 [github action] (#987)

Co-authored-by: remicolin <98749896+remicolin@users.noreply.github.com>

* Revert "chore: configure Metro bundler for production compatibility in mobile deploy workflow"

This reverts commit 60fc1f2580.

* reduce max old space size in mobile-deploy ci

* fix android french id card (#957)

* fix android french id card

* fix common ci cache

* feat: log apdu (#988)

---------

Co-authored-by: Justin Hernandez <transphorm@gmail.com>
Co-authored-by: Seshanth.S🐺 <35675963+seshanthS@users.noreply.github.com>

* unblock ci

* fix merge

* merge fixes

* fix tests

* make ci happy

---------

Co-authored-by: turnoffthiscomputer <colin.remi07@gmail.com>
Co-authored-by: pputman-clabs <99900942+pputman-clabs@users.noreply.github.com>
Co-authored-by: Self GitHub Actions <action@github.com>
Co-authored-by: turnoffthiscomputer <98749896+remicolin@users.noreply.github.com>
Co-authored-by: Vishalkulkarni45 <109329073+Vishalkulkarni45@users.noreply.github.com>
Co-authored-by: ayman <aymanshaik1015@gmail.com>
Co-authored-by: Aaron DeRuvo <aaron.deruvo@clabs.co>
Co-authored-by: Seshanth.S🐺 <35675963+seshanthS@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-07 11:19:59 -07:00

498 lines
18 KiB
Ruby

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
# https://docs.fastlane.tools/actions
#
opt_out_usage
require "bundler/setup"
require "base64"
require "xcodeproj"
require_relative "helpers"
# load secrets before project configuration
Fastlane::Helpers.dev_load_dotenv_secrets
is_ci = Fastlane::Helpers.is_ci_environment?
local_development = !is_ci
# checks after calling Dotenv.load
android_has_permissions = false
# Project configuration
PROJECT_NAME = ENV["IOS_PROJECT_NAME"]
APP_NAME = ENV["IOS_PROJECT_NAME"] || begin
app_json_path = File.expand_path("../app.json", __dir__)
if File.exist?(app_json_path)
app_config = JSON.parse(File.read(app_json_path))
app_config["displayName"] if app_config.is_a?(Hash)
end
rescue JSON::ParserError, Errno::ENOENT
UI.important("Could not read app.json or invalid JSON format, using default app name")
nil
end || "MobileApp"
PROJECT_SCHEME = ENV["IOS_PROJECT_SCHEME"]
SIGNING_CERTIFICATE = ENV["IOS_SIGNING_CERTIFICATE"]
# Environment setup
package_version = JSON.parse(File.read("../package.json"))["version"]
# most of these values are for local development
android_aab_path = "android/app/build/outputs/bundle/release/app-release.aab"
android_gradle_file_path = "../android/app/build.gradle"
android_keystore_path = "../android/app/upload-keystore.jks"
android_play_store_json_key_path = "../android/app/play-store-key.json"
ios_connect_api_key_path = "../ios/certs/connect_api_key.p8"
ios_provisioning_profile_directory = "~/Library/MobileDevice/Provisioning\ Profiles"
ios_xcode_profile_path = "../ios/#{PROJECT_NAME}.xcodeproj"
default_platform(:ios)
platform :ios do
desc "Sync ios version"
lane :sync_version do
increment_version_number(
xcodeproj: "ios/#{PROJECT_NAME}.xcodeproj",
version_number: package_version,
)
end
desc "Push a new build to TestFlight Internal Testing"
lane :internal_test do |options|
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
result = prepare_ios_build(prod_release: false)
if test_mode
UI.important("🧪 TEST MODE: Skipping TestFlight upload")
UI.success("✅ Build completed successfully!")
UI.message("📦 IPA path: #{result[:ipa_path]}")
else
upload_to_testflight(
api_key: result[:api_key],
distribute_external: true,
# Only external TestFlight groups are valid here
groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","),
changelog: "",
skip_waiting_for_build_processing: false,
) if result[:should_upload]
end
# Update deployment timestamp in version.json
if result[:should_upload]
Fastlane::Helpers.update_deployment_timestamp("ios")
end
# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"] && !test_mode
deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Workflow" : "Local Deploy"
Fastlane::Helpers.upload_file_to_slack(
file_path: result[:ipa_path],
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "🍎 iOS v#{package_version} (Build #{result[:build_number]}) deployed to TestFlight via #{deploy_source}",
title: "#{APP_NAME}-#{package_version}-#{result[:build_number]}.ipa",
)
elsif test_mode
UI.important("🧪 TEST MODE: Skipping Slack notification")
else
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
end
end
desc "Deploy iOS app with automatic version management"
lane :deploy_auto do |options|
deployment_track = options[:deployment_track] || "internal"
version_bump = options[:version_bump] || "build"
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
UI.message("🚀 Starting iOS deployment")
UI.message(" Track: #{deployment_track}")
UI.message(" Version bump: #{version_bump}")
UI.message(" Test mode: #{test_mode}")
# Handle version bumping
if !test_mode
require_relative "helpers/version_manager"
case version_bump
when "major", "minor", "patch"
# VersionManager doesn't handle semantic versions, use npm
sh("cd .. && npm version #{version_bump} --no-git-tag-version")
UI.success("✅ Bumped #{version_bump} version")
# Sync the new version to iOS project files
sync_version
UI.success("✅ Synced MARKETING_VERSION to iOS project")
when "build"
# Build number is handled in prepare_ios_build
UI.message("📦 Build number will be incremented during build")
end
end
# Prepare and build
result = prepare_ios_build(prod_release: deployment_track == "production")
# Handle deployment based on track
if test_mode
UI.important("🧪 TEST MODE: Skipping App Store upload")
UI.success("✅ Build completed successfully!")
elsif deployment_track == "internal"
upload_to_testflight(
api_key: result[:api_key],
distribute_external: false,
skip_waiting_for_build_processing: false,
) if result[:should_upload]
elsif deployment_track == "production"
# For production, upload to TestFlight first, then promote
upload_to_testflight(
api_key: result[:api_key],
distribute_external: false,
skip_waiting_for_build_processing: true,
) if result[:should_upload]
# TODO: Add app store submission when ready
UI.important("⚠️ Production deployment uploaded to TestFlight. Manual App Store submission required.")
end
# Update deployment info
if result[:should_upload] && !test_mode
Fastlane::Helpers.update_deployment_timestamp("ios")
end
# Slack notification
if ENV["SLACK_CHANNEL_ID"] && !test_mode
deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Actions (Auto)" : "Local Deploy"
track_emoji = deployment_track == "production" ? "🚀" : "🧪"
Fastlane::Helpers.upload_file_to_slack(
file_path: result[:ipa_path],
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "#{track_emoji} iOS v#{package_version} (Build #{result[:build_number]}) deployed to #{deployment_track} via #{deploy_source}",
title: "#{APP_NAME}-#{package_version}-#{result[:build_number]}.ipa",
)
end
end
private_lane :prepare_ios_build do |options|
if local_development
# app breaks with Xcode 16.3
xcode_select "/Applications/Xcode.app"
# Set up API key, profile, and potentially certificate for local dev
Fastlane::Helpers.ios_dev_setup_connect_api_key(ios_connect_api_key_path)
Fastlane::Helpers.ios_dev_setup_provisioning_profile(ios_provisioning_profile_directory)
Fastlane::Helpers.ios_dev_setup_certificate
else
# we need this for building ios apps in CI
# else build will hang on "[CP] Embed Pods Frameworks"
setup_ci(
keychain_name: "build.keychain",
)
end
required_env_vars = [
"IOS_APP_IDENTIFIER",
"IOS_CONNECT_API_KEY_BASE64",
"IOS_CONNECT_API_KEY_PATH",
"IOS_CONNECT_ISSUER_ID",
"IOS_CONNECT_KEY_ID",
"IOS_PROJECT_NAME",
"IOS_PROJECT_SCHEME",
"IOS_PROV_PROFILE_NAME",
"IOS_PROV_PROFILE_PATH",
"IOS_TEAM_ID",
"IOS_TEAM_NAME",
]
target_platform = options[:prod_release] ? "App Store" : "TestFlight"
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
workspace_path = File.expand_path("../ios/#{PROJECT_NAME}.xcworkspace", Dir.pwd)
ios_signing_certificate_name = "iPhone Distribution: #{ENV["IOS_TEAM_NAME"]} (#{ENV["IOS_TEAM_ID"]})"
Fastlane::Helpers.verify_env_vars(required_env_vars)
# Get build number from version.json and increment it
build_number = Fastlane::Helpers.bump_ios_build_number
# Update Xcode project with new build number
increment_build_number(
build_number: build_number,
xcodeproj: "ios/#{ENV["IOS_PROJECT_NAME"]}.xcodeproj",
)
# Verify build number is higher than TestFlight
Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path)
Fastlane::Helpers.ios_verify_provisioning_profile
api_key = app_store_connect_api_key(
key_id: ENV["IOS_CONNECT_KEY_ID"],
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
in_house: false,
)
# Update project to use manual code signing
update_code_signing_settings(
use_automatic_signing: false,
path: "ios/#{PROJECT_NAME}.xcodeproj",
team_id: ENV["IOS_TEAM_ID"],
targets: [PROJECT_NAME],
code_sign_identity: ios_signing_certificate_name,
profile_name: ENV["IOS_PROV_PROFILE_NAME"],
bundle_identifier: ENV["IOS_APP_IDENTIFIER"],
build_configurations: ["Release"],
)
clear_derived_data
# Print final build settings before archiving
sh "xcodebuild -showBuildSettings -workspace #{workspace_path} " \
"-scheme #{PROJECT_SCHEME} -configuration Release " \
"| grep 'CODE_SIGN_STYLE\|PROVISIONING_PROFILE_SPECIFIER\|CODE_SIGN_IDENTITY\|DEVELOPMENT_TEAM' || true"
cocoapods(
podfile: "ios/Podfile",
clean_install: true,
deployment: true,
)
ipa_path = build_app({
workspace: "#{workspace_path}",
scheme: PROJECT_SCHEME,
export_method: "app-store",
output_directory: "build",
clean: true,
export_options: {
method: "app-store",
signingStyle: "manual",
provisioningProfiles: {
ENV["IOS_APP_IDENTIFIER"] => ENV["IOS_PROV_PROFILE_NAME"],
},
signingCertificate: ios_signing_certificate_name,
teamID: ENV["IOS_TEAM_ID"],
},
})
{
api_key: api_key,
build_number: build_number,
ipa_path: ipa_path,
should_upload: should_upload,
}
end
end
platform :android do
desc "Sync android version"
lane :sync_version do
android_set_version_name(
version_name: package_version,
gradle_file: android_gradle_file_path.gsub("../", ""),
)
end
desc "Push a new build to Google Play Internal Testing"
lane :internal_test do |options|
upload_android_build(track: "internal", test_mode: options[:test_mode])
end
desc "Push a new build to Google Play Store"
lane :deploy do
upload_android_build(track: "production")
end
desc "Build Android app without uploading"
lane :build_only do |options|
deployment_track = options[:deployment_track] || "internal"
version_bump = options[:version_bump] || "build"
UI.message("🔨 Building Android app (build only)")
UI.message(" Track: #{deployment_track}")
UI.message(" Version bump: #{version_bump}")
upload_android_build(options.merge(skip_upload: true))
end
desc "Deploy Android app with automatic version management"
lane :deploy_auto do |options|
deployment_track = options[:deployment_track] || "internal"
version_bump = options[:version_bump] || "build"
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
UI.message("🚀 Starting Android deployment")
UI.message(" Track: #{deployment_track}")
UI.message(" Version bump: #{version_bump}")
UI.message(" Test mode: #{test_mode}")
# Handle version bumping
if !test_mode
require_relative "helpers/version_manager"
case version_bump
when "major", "minor", "patch"
# For semantic version bumps, we need to use npm version
sh("cd .. && npm version #{version_bump} --no-git-tag-version")
UI.success("✅ Bumped #{version_bump} version")
# Get the new version and sync to build.gradle
new_version = Fastlane::Helpers::VersionManager.get_current_version
android_set_version_name(
version_name: new_version,
gradle_file: android_gradle_file_path.gsub("../", ""),
)
when "build"
# Build number is automatically incremented during build
UI.message("📦 Build number will be incremented")
end
end
# Map deployment track to Play Store track
play_store_track = deployment_track == "production" ? "production" : "internal"
# Build and deploy
upload_android_build(track: play_store_track, test_mode: test_mode, deployment_track: deployment_track)
end
private_lane :upload_android_build do |options|
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
skip_upload = options[:skip_upload] == true || options[:skip_upload] == "true"
if local_development
if ENV["ANDROID_KEYSTORE_PATH"].nil?
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
end
if ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"].nil?
ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"] = Fastlane::Helpers.android_create_play_store_key(android_play_store_json_key_path)
end
end
required_env_vars = [
"ANDROID_KEYSTORE",
"ANDROID_KEYSTORE_PASSWORD",
"ANDROID_KEYSTORE_PATH",
"ANDROID_KEY_ALIAS",
"ANDROID_KEY_PASSWORD",
"ANDROID_PACKAGE_NAME",
]
# Only require JSON key path when not running in CI (local development)
required_env_vars << "ANDROID_PLAY_STORE_JSON_KEY_PATH" if local_development
Fastlane::Helpers.verify_env_vars(required_env_vars)
# Get version code from version.json and increment it
version_code = Fastlane::Helpers.bump_android_build_number
# Update build.gradle with new version code
increment_version_code(
version_code: version_code,
gradle_file_path: android_gradle_file_path.gsub("../", ""),
)
# TODO: uncomment when we have the permissions to run this action
# Fastlane::Helpers.android_verify_version_code(android_gradle_file_path)
target_platform = options[:track] == "production" ? "Google Play" : "Internal Testing"
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
# Validate JSON key only in local development; CI uses Workload Identity Federation (ADC)
if local_development
validate_play_store_json_key(
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
)
end
Fastlane::Helpers.with_retry(max_retries: 3, delay: 10) do
gradle(
task: "clean bundleRelease --stacktrace --info",
project_dir: "android/",
properties: {
"MYAPP_UPLOAD_STORE_FILE" => ENV["ANDROID_KEYSTORE_PATH"],
"MYAPP_UPLOAD_STORE_PASSWORD" => ENV["ANDROID_KEYSTORE_PASSWORD"],
"MYAPP_UPLOAD_KEY_ALIAS" => ENV["ANDROID_KEY_ALIAS"],
"MYAPP_UPLOAD_KEY_PASSWORD" => ENV["ANDROID_KEY_PASSWORD"] == "EMPTY" ? "" : ENV["ANDROID_KEY_PASSWORD"],
},
)
end
if test_mode || skip_upload
if skip_upload
UI.important("🔨 BUILD ONLY: Skipping Play Store upload")
else
UI.important("🧪 TEST MODE: Skipping Play Store upload")
end
UI.success("✅ Build completed successfully!")
UI.message("📦 AAB path: #{android_aab_path}")
else
if should_upload
begin
upload_options = {
track: options[:track],
package_name: ENV["ANDROID_PACKAGE_NAME"],
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
track_promote_release_status: "completed",
aab: android_aab_path,
}
# In local development, use the JSON key file; in CI rely on ADC
if local_development
upload_options[:json_key] = ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"]
else
# In CI, try to use ADC credentials file directly
adc_creds_path = ENV["GOOGLE_APPLICATION_CREDENTIALS"]
if adc_creds_path && File.exist?(adc_creds_path)
UI.message("🔑 Using ADC credentials file: #{adc_creds_path}")
begin
# Try passing the credentials file content as json_key_data
creds_content = File.read(adc_creds_path)
upload_options[:json_key_data] = creds_content
rescue => e
UI.error("Failed to read ADC credentials: #{e.message}")
# Fallback: let supply try to use ADC automatically
UI.message("🔄 Falling back to automatic ADC detection")
end
else
UI.error("❌ ADC credentials not found at: #{adc_creds_path}")
end
end
upload_to_play_store(upload_options)
rescue => e
if e.message.include?("forbidden") || e.message.include?("403") || e.message.include?("insufficientPermissions")
UI.error("❌ Play Store upload failed: Insufficient permissions")
UI.error("Please fix permissions in Google Play Console")
UI.important("Build saved at: #{android_aab_path}")
else
# Re-raise if it's a different error
raise e
end
end
else
UI.message("Skipping Play Store upload (should_upload: false)")
end
end
# Update deployment timestamp in version.json
if should_upload
Fastlane::Helpers.update_deployment_timestamp("android")
end
# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"] && !test_mode
deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Actions (Auto)" : "Local Deploy"
deployment_track = options[:deployment_track] || options[:track]
track_emoji = deployment_track == "production" ? "🚀" : "🧪"
Fastlane::Helpers.upload_file_to_slack(
file_path: android_aab_path,
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "#{track_emoji} Android v#{package_version} (Build #{version_code}) deployed to #{deployment_track || target_platform} via #{deploy_source}",
title: "#{APP_NAME}-#{package_version}-#{version_code}.aab",
)
elsif test_mode
UI.important("🧪 TEST MODE: Skipping Slack notification")
else
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
end
end
end