Module: CodexNotify::ConfigSupport

Included in:
Config, EnvSourceLoader, HookConfig
Defined in:
lib/codex_notify/config_support.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

DEFAULT_ENV_PATH =
'.env'
DEFAULT_ENV_POLICY =
'restricted'
ENV_POLICIES =
%w[legacy restricted].freeze
REPOSITORY_ALLOWED_KEYS =
%w[
  CODEX_NOTIFY_DESTINATION
  CODEX_NOTIFY_TITLE
  CODEX_NOTIFY_USER_NAME
  CODEX_NOTIFY_MODE
].freeze
REPOSITORY_CREDENTIAL_PATTERN =
/\ASLACK_(?:BOT_TOKEN|CHANNEL)(?:__.*)?\z/

Instance Method Summary collapse

Instance Method Details

#add_common_options(parser, options) ⇒ Object



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
# File 'lib/codex_notify/config_support.rb', line 56

def add_common_options(parser, options)
  parser.on('--config PATH') do |value|
    options.config_file = value
  end
  parser.on('--migrate-config', 'Create trusted YAML from a legacy env file') do
    options.migrate_config = true
  end
  parser.on('--env-file PATH') do |value|
    options.env_file = value
    options.env_file_explicit = true if options.respond_to?(:env_file_explicit=)
  end
  parser.on('--token TOKEN', 'Deprecated: use the XDG config file or SLACK_BOT_TOKEN') do |value|
    options.token = value
    options.token_from_cli = true
  end
  parser.on('--channel CHANNEL') { |value| options.channel = value }
  parser.on('--destination NAME') { |value| options.destination = value } if options.respond_to?(:destination=)
  parser.on('--user-name NAME') { |value| options.user_name = value }
  parser.on('--title TITLE') { |value| options.title = value }
  if options.respond_to?(:outbox_action=)
    parser.on('--outbox-status') { options.outbox_action = :status }
    parser.on('--drain-outbox') { options.outbox_action = :drain }
    parser.on('--retry-outbox ID') do |value|
      options.outbox_action = :retry
      options.outbox_id = value
    end
  end
end

#apply_destination(options, sources, policy:, stderr: $stderr) ⇒ Object



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
# File 'lib/codex_notify/config_support.rb', line 94

def apply_destination(options, sources, policy:, stderr: $stderr)
  eligible = eligible_sources(sources, policy:)
  resolution = DestinationResolver.new(
    selection_sources: sources,
    profile_sources: trusted_sources(sources),
    default_sources: eligible
  ).resolve(destination: options.destination, token: options.token, channel: options.channel)

  options.destination = resolution.destination
  options.token = resolution.token
  options.channel = resolution.channel

  used_sources = [resolution.token_source, resolution.channel_source].compact
  source_keys = [
    [resolution.token_source, 'SLACK_BOT_TOKEN'],
    [resolution.channel_source, 'SLACK_CHANNEL']
  ]
  repository_keys = source_keys.filter_map { |source, key| key if source&.kind == :repository }
  if repository_keys.any?
    source = used_sources.find { |candidate| candidate.kind == :repository }
    ConfigDiagnostics.warn_deprecated_repository_credentials(source.path, repository_keys, stderr:)
  end

  tool_keys = [
    [resolution.token_source, resolution.destination ? "destination #{resolution.destination} token" : 'SLACK_BOT_TOKEN'],
    [resolution.channel_source, resolution.destination ? "destination #{resolution.destination} channel" : 'SLACK_CHANNEL']
  ].filter_map { |source, key| key if source&.kind == :tool }
  warn_if_legacy_tool_source(used_sources.find { |source| source.kind == :tool }, tool_keys, stderr:)
end

#eligible_sources(sources, policy:) ⇒ Object



124
125
126
127
128
# File 'lib/codex_notify/config_support.rb', line 124

def eligible_sources(sources, policy:)
  return sources unless policy == 'restricted'

  sources.restrict_kind(:repository, keys: REPOSITORY_ALLOWED_KEYS)
end

#getenv_any(keys) ⇒ Object



48
49
50
51
52
53
54
# File 'lib/codex_notify/config_support.rb', line 48

def getenv_any(keys)
  keys.each do |key|
    value = ENV[key]
    return value if value && !value.empty?
  end
  nil
end

#load_env_file(path = DEFAULT_ENV_PATH, override: false, stderr: $stderr, legacy_checkout_root: nil) ⇒ Object



33
34
35
36
37
38
39
40
# File 'lib/codex_notify/config_support.rb', line 33

def load_env_file(path = DEFAULT_ENV_PATH, override: false, stderr: $stderr, legacy_checkout_root: nil)
  env_paths = resolve_env_paths(path, legacy_checkout_root:)
  return if env_paths.empty?

  env_paths.each { |env_path| ConfigDiagnostics.warn_if_env_file_insecure(env_path, stderr:) }
  loader = override ? Dotenv.method(:overload) : Dotenv.method(:load)
  loader.call(*env_paths.map(&:to_s))
end

#resolve_env_paths(path = DEFAULT_ENV_PATH, legacy_checkout_root: nil) ⇒ Object



22
23
24
25
26
27
28
29
30
31
# File 'lib/codex_notify/config_support.rb', line 22

def resolve_env_paths(path = DEFAULT_ENV_PATH, legacy_checkout_root: nil)
  env_path = Pathname(path)
  return [env_path] if env_path.absolute?

  candidates = [Pathname(Dir.pwd).join(env_path)]
  candidates << Pathname(legacy_checkout_root).join(env_path) if legacy_checkout_root
  candidates.map(&:expand_path).select(&:exist?).each_with_object([]) do |candidate, paths|
    paths << candidate unless paths.any? { |path| File.identical?(path, candidate) }
  end
end

#resolve_policy(sources, stderr: $stderr) ⇒ Object

Raises:



85
86
87
88
89
90
91
92
# File 'lib/codex_notify/config_support.rb', line 85

def resolve_policy(sources, stderr: $stderr)
  result = trusted_sources(sources).lookup('CODEX_NOTIFY_ENV_POLICY')
  policy = result ? result.value.strip.downcase : DEFAULT_ENV_POLICY
  raise Error, "environment policy must be one of: #{ENV_POLICIES.join(', ')}" unless ENV_POLICIES.include?(policy)

  warn_if_legacy_tool_source(result&.source, ['CODEX_NOTIFY_ENV_POLICY'], stderr:)
  policy
end

#system_user_nameObject



42
43
44
45
46
# File 'lib/codex_notify/config_support.rb', line 42

def system_user_name
  ENV['USER'] || ENV['USERNAME'] || Etc.getlogin || 'user'
rescue StandardError
  ENV['USER'] || ENV['USERNAME'] || 'user'
end

#trusted_sources(sources) ⇒ Object



130
131
132
# File 'lib/codex_notify/config_support.rb', line 130

def trusted_sources(sources)
  sources.excluding_kind(:repository)
end

#warn_if_legacy_tool_source(source, keys, stderr:) ⇒ Object



150
151
152
153
154
# File 'lib/codex_notify/config_support.rb', line 150

def warn_if_legacy_tool_source(source, keys, stderr:)
  return unless source&.kind == :tool && keys.any?

  ConfigDiagnostics.warn_deprecated_tool_config(source.path, keys, stderr:)
end

#warn_ignored_repository_values(sources, policy:, stderr:) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/codex_notify/config_support.rb', line 134

def warn_ignored_repository_values(sources, policy:, stderr:)
  sources.select { |source| source.kind == :repository }.each do |source|
    policy_value = source.values['CODEX_NOTIFY_ENV_POLICY']
    if policy_value && !policy_value.empty?
      ConfigDiagnostics.warn_ignored_repository_policy(source.path, stderr:)
    end

    ignored = source.values.keys.grep(REPOSITORY_CREDENTIAL_PATTERN)
    ignored.select! { |key| key.include?('__') } if policy == 'legacy'
    next if ignored.empty?

    reason = policy == 'restricted' ? policy : nil
    ConfigDiagnostics.warn_ignored_repository_credentials(source.path, ignored.sort, policy: reason, stderr:)
  end
end