From a1f59eedfee75fe783ae9cb90266ac5e2ee7f41b Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Tue, 9 Sep 2025 03:09:15 +0530 Subject: [PATCH 01/26] feat: Add separate write paths for configuring `version_control` integrations Update the read path on `AppConfig#further_setup_by_category?` for `code_repository`. This set of changes has a few TODO items that should be resolved once the remaining configurable integrations are brought under the fold of the refactor. --- .../integration_card_component.html.erb | 43 ++++++++++----- app/components/integration_card_component.rb | 33 +++++++++++ app/components/integration_list_component.rb | 3 +- app/controllers/apps_controller.rb | 7 ++- .../bitbucket_configs_controller.rb | 55 +++++++++++++++++++ .../github_configs_controller.rb | 53 ++++++++++++++++++ .../gitlab_configs_controller.rb | 53 ++++++++++++++++++ app/models/app_config.rb | 6 +- app/models/bitbucket_integration.rb | 2 + app/models/github_integration.rb | 11 ++-- app/models/gitlab_integration.rb | 1 + .../edit.html+turbo_frame.erb | 34 ++++++++++++ .../github_configs/edit.html+turbo_frame.erb | 22 ++++++++ .../gitlab_configs/edit.html+turbo_frame.erb | 22 ++++++++ config/locales/en.yml | 10 ++++ config/routes.rb | 6 ++ ...d_repository_config_to_vcs_integrations.rb | 8 +++ db/schema.rb | 6 +- 18 files changed, 352 insertions(+), 23 deletions(-) create mode 100644 app/controllers/version_control/bitbucket_configs_controller.rb create mode 100644 app/controllers/version_control/github_configs_controller.rb create mode 100644 app/controllers/version_control/gitlab_configs_controller.rb create mode 100644 app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb create mode 100644 app/views/version_control/github_configs/edit.html+turbo_frame.erb create mode 100644 app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb create mode 100644 db/migrate/20250908020424_add_repository_config_to_vcs_integrations.rb diff --git a/app/components/integration_card_component.html.erb b/app/components/integration_card_component.html.erb index c87fba73c..86a6f75d4 100644 --- a/app/components/integration_card_component.html.erb +++ b/app/components/integration_card_component.html.erb @@ -20,19 +20,36 @@ <% if connected? %>
<%= render BadgeComponent.new(text: "Connected", status: :success) %> - <% if disconnectable_categories? %> - <%= render ButtonComponent.new( - scheme: :danger, - options: app_integration_path(@app, integration), - type: :button, - size: :xxs, - turbo: false, - disabled: !disconnectable?, - html_options: {method: :delete, data: {turbo_method: :delete, turbo_confirm: "Are you sure you want disconnect the integration?"}} - ) do |b| - b.with_icon("trash.svg", size: :sm) - end %> - <% end %> +
+ <% if further_setup? %> + <%= render ModalComponent.new(title: category_title, open: pre_open_category?) do |modal| %> + <% modal.with_button(scheme: :light, type: :action, size: :xxs) + .with_icon("cog.svg", size: :sm) %> + <% modal.with_body do %> + <%= tag.turbo_frame id: category_config_turbo_frame_id, + src: edit_config_path, + loading: :lazy, + class: "with-turbo-frame-loader" do %> + <%= render LoadingIndicatorComponent.new(skeleton_only: true, turbo_frame: true) %> + <% end %> + <% end %> + <% end %> + <% end %> + + <% if disconnectable_categories? %> + <%= render ButtonComponent.new( + scheme: :danger, + options: app_integration_path(@app, integration), + type: :button, + size: :xxs, + turbo: false, + disabled: !disconnectable?, + html_options: {method: :delete, data: {turbo_method: :delete, turbo_confirm: "Are you sure you want disconnect the integration?"}} + ) do |b| + b.with_icon("trash.svg", size: :sm) + end %> + <% end %> +
<% end %> diff --git a/app/components/integration_card_component.rb b/app/components/integration_card_component.rb index 71d1de2b0..3a748a503 100644 --- a/app/components/integration_card_component.rb +++ b/app/components/integration_card_component.rb @@ -77,4 +77,37 @@ def reusable_integrations_form_partial(existing_integrations) def disconnectable? integration.disconnectable? && disconnectable_categories? end + + def category_title + "Configure #{Integration.human_enum_name(:category, @category)}" + end + + def pre_open_category? + @pre_open_category == @category + end + + def category_config_turbo_frame_id + "#{@category}_config" + end + + def further_setup? + # TODO: delegate to Integration properly + integration.version_control? + end + + def edit_config_path + # TODO: find a potentially better way to route this + if integration.version_control? + case integration.providable_type + when "GithubIntegration" + edit_app_version_control_github_config_path(@app) + when "GitlabIntegration" + edit_app_version_control_gitlab_config_path(@app) + when "BitbucketIntegration" + edit_app_version_control_bitbucket_config_path(@app) + else + raise TypeError, "Unknown providable_type: #{integration.providable_type}" + end + end + end end diff --git a/app/components/integration_list_component.rb b/app/components/integration_list_component.rb index d1b87df28..e22f76556 100644 --- a/app/components/integration_list_component.rb +++ b/app/components/integration_list_component.rb @@ -14,6 +14,7 @@ def pre_open?(category) end def connected_integrations?(integrations) - integrations.any? { |i| i.connected? && i.further_setup? } + # TODO: Move away from checking integration category later + integrations.any? { |i| i.connected? && i.further_setup? && !i.version_control? } end end diff --git a/app/controllers/apps_controller.rb b/app/controllers/apps_controller.rb index b6a35e49f..2ea20686b 100644 --- a/app/controllers/apps_controller.rb +++ b/app/controllers/apps_controller.rb @@ -24,8 +24,11 @@ def show redirect_to app_train_releases_path(@app, selected_train) end - @train_in_creation = @app.train_in_creation - @app_setup_instructions = @app.app_setup_instructions + if @app.ready? + @train_in_creation = @app.train_in_creation + else + @app_setup_instructions = @app.app_setup_instructions + end end def edit diff --git a/app/controllers/version_control/bitbucket_configs_controller.rb b/app/controllers/version_control/bitbucket_configs_controller.rb new file mode 100644 index 000000000..8ec52913b --- /dev/null +++ b/app/controllers/version_control/bitbucket_configs_controller.rb @@ -0,0 +1,55 @@ +class VersionControl::BitbucketConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_bitbucket_integration + around_action :set_time_zone + + def edit + set_code_repositories + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + format.turbo_stream { render :edit } + end + end + + def update + if @bitbucket_integration.update(parsed_bitbucket_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @bitbucket_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_bitbucket_integration + @bitbucket_integration = @app.vcs_provider + unless @bitbucket_integration + redirect_to app_integrations_path(@app), flash: {error: "Version control integration not found."} + end + end + + def set_code_repositories + @workspaces = @bitbucket_integration.workspaces || [] + workspace = params[:workspace] || @workspaces.first + @code_repositories = @bitbucket_integration.repos(workspace) + end + + def parsed_bitbucket_config_params + bitbucket_config_params = params.require(:bitbucket_integration) + .permit(:repository_config, :workspace) + bitbucket_config_params.merge( + repository_config: bitbucket_config_params[:repository_config]&.safe_json_parse + ) + end +end diff --git a/app/controllers/version_control/github_configs_controller.rb b/app/controllers/version_control/github_configs_controller.rb new file mode 100644 index 000000000..fe693d735 --- /dev/null +++ b/app/controllers/version_control/github_configs_controller.rb @@ -0,0 +1,53 @@ +class VersionControl::GithubConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_github_integration + around_action :set_time_zone + + def edit + set_code_repositories + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + format.turbo_stream { render :edit } # NOTE: probably not needed for GithubIntegration + end + end + + def update + if @github_integration.update(parsed_github_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @github_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_github_integration + @github_integration = @app.vcs_provider + unless @github_integration + redirect_to app_integrations_path(@app), flash: {error: "Version control integration not found."} + end + end + + def set_code_repositories + @code_repositories = @github_integration.repos + end + + def parsed_github_config_params + github_config_params = params.require(:github_integration) + .permit(:repository_config) + github_config_params.merge( + repository_config: github_config_params[:repository_config]&.safe_json_parse + ) + end +end diff --git a/app/controllers/version_control/gitlab_configs_controller.rb b/app/controllers/version_control/gitlab_configs_controller.rb new file mode 100644 index 000000000..608e818fc --- /dev/null +++ b/app/controllers/version_control/gitlab_configs_controller.rb @@ -0,0 +1,53 @@ +class VersionControl::GitlabConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_gitlab_integration + around_action :set_time_zone + + def edit + set_code_repositories + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + format.turbo_stream { render :edit } # NOTE: probably not needed for GitlabIntegration + end + end + + def update + if @gitlab_integration.update(parsed_gitlab_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @gitlab_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_gitlab_integration + @gitlab_integration = @app.vcs_provider + unless @gitlab_integration + redirect_to app_integrations_path(@app), flash: {error: "Version control integration not found."} + end + end + + def set_code_repositories + @code_repositories = @gitlab_integration.repos + end + + def parsed_gitlab_config_params + gitlab_config_params = params.require(:gitlab_integration) + .permit(:repository_config) + gitlab_config_params.merge( + repository_config: gitlab_config_params[:repository_config]&.safe_json_parse + ) + end +end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 80b77bca7..a95dc6fa0 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -73,7 +73,7 @@ def further_setup_by_category? if integrations.version_control.present? categories[:version_control] = { further_setup: integrations.version_control.any?(&:further_setup?), - ready: code_repository.present? + ready: integrations.version_control.any? { _1.providable&.repository_config.present? } # TODO: perhaps &:ready? makes sense? } end @@ -140,6 +140,10 @@ def disconnect!(integration) save! end + def code_repository + app.vcs_provider&.repository_config + end + private def set_bugsnag_config diff --git a/app/models/bitbucket_integration.rb b/app/models/bitbucket_integration.rb index d0600c879..e658d9a5f 100644 --- a/app/models/bitbucket_integration.rb +++ b/app/models/bitbucket_integration.rb @@ -5,6 +5,8 @@ # id :uuid not null, primary key # oauth_access_token :string # oauth_refresh_token :string +# repository_config :jsonb +# workspace :string # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/models/github_integration.rb b/app/models/github_integration.rb index fd968cbcc..48a89741e 100644 --- a/app/models/github_integration.rb +++ b/app/models/github_integration.rb @@ -2,10 +2,11 @@ # # Table name: github_integrations # -# id :uuid not null, primary key -# created_at :datetime not null -# updated_at :datetime not null -# installation_id :string not null +# id :uuid not null, primary key +# repository_config :jsonb +# created_at :datetime not null +# updated_at :datetime not null +# installation_id :string not null # class GithubIntegration < ApplicationRecord has_paper_trail @@ -119,7 +120,7 @@ def install_path def workspaces = nil - def repos(_) + def repos(_ = nil) installation.list_repos(REPOS_TRANSFORMATIONS) end diff --git a/app/models/gitlab_integration.rb b/app/models/gitlab_integration.rb index 5fe09e23f..c7a7abf5d 100644 --- a/app/models/gitlab_integration.rb +++ b/app/models/gitlab_integration.rb @@ -5,6 +5,7 @@ # id :uuid not null, primary key # oauth_access_token :string # oauth_refresh_token :string +# repository_config :jsonb # created_at :datetime not null # updated_at :datetime not null # diff --git a/app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb b/app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..f9f680d73 --- /dev/null +++ b/app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb @@ -0,0 +1,34 @@ +<%= render EnhancedTurboFrameComponent.new(:version_control_config) do %> + <%= render FormComponent.new(model: [@app, @bitbucket_integration], url: app_version_control_bitbucket_config_path(@app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Repository") do |section| %> + <% section.with_description do %> + Primary working code repository. + <% end %> + +
+ <%= section.F.labeled_select :workspace, + "Workspace", + options_for_select(@workspaces, selected: @bitbucket_integration.workspace), + {}, + disabled: @workspaces.blank?, + class: EnhancedFormHelper::AuthzForm::SELECT_CLASSES, + data: {action: "change->stream-effect#fetch", stream_effect_target: "dispatch"} %> +
+ +
+ <%= section.F.labeled_select :repository_config, + "Code Repository", + options_for_select( + display_channels(@code_repositories) { |repo| repo[:full_name] }, + @code_repository.to_json + ) %> +
+ <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/version_control/github_configs/edit.html+turbo_frame.erb b/app/views/version_control/github_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..5fb51741c --- /dev/null +++ b/app/views/version_control/github_configs/edit.html+turbo_frame.erb @@ -0,0 +1,22 @@ +<%= render EnhancedTurboFrameComponent.new(:version_control_config) do %> + <%= render FormComponent.new(model: [@app, @github_integration], url: app_version_control_github_config_path(@app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Repository") do |section| %> + <% section.with_description do %> + Primary working code repository. + <% end %> + +
+ <%= section.F.labeled_select :repository_config, + "Code Repository", + options_for_select( + display_channels(@code_repositories) { |repo| repo[:full_name] }, + @code_repository.to_json + ) %> +
+ <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb b/app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..3e6cc0401 --- /dev/null +++ b/app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb @@ -0,0 +1,22 @@ +<%= render EnhancedTurboFrameComponent.new(:version_control_config) do %> + <%= render FormComponent.new(model: [@app, @gitlab_integration], url: app_version_control_gitlab_config_path(@app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Repository") do |section| %> + <% section.with_description do %> + Primary working code repository. + <% end %> + +
+ <%= section.F.labeled_select :repository_config, + "Code Repository", + options_for_select( + display_channels(@code_repositories) { |repo| repo[:full_name] }, + @code_repository.to_json + ) %> +
+ <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 6e71d1e49..5272ba553 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -776,3 +776,13 @@ en: done_states: select: "Select Done States" help_text: "Choose which states represent completed work" + + github_configs: + update: + success: "Version control configuration was successfully updated." + gitlab_configs: + update: + success: "Version control configuration was successfully updated." + bitbucket_configs: + update: + success: "Version control configuration was successfully updated." diff --git a/config/routes.rb b/config/routes.rb index 10bd2f42d..476236223 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,6 +69,12 @@ resources :app_variants, only: %i[index edit create update destroy] end + namespace :version_control do + resource :github_config, only: %i[edit update] + resource :gitlab_config, only: %i[edit update] + resource :bitbucket_config, only: %i[edit update] + end + member do get :all_builds get :search diff --git a/db/migrate/20250908020424_add_repository_config_to_vcs_integrations.rb b/db/migrate/20250908020424_add_repository_config_to_vcs_integrations.rb new file mode 100644 index 000000000..48798c335 --- /dev/null +++ b/db/migrate/20250908020424_add_repository_config_to_vcs_integrations.rb @@ -0,0 +1,8 @@ +class AddRepositoryConfigToVcsIntegrations < ActiveRecord::Migration[7.2] + def change + add_column :github_integrations, :repository_config, :jsonb + add_column :gitlab_integrations, :repository_config, :jsonb + add_column :bitbucket_integrations, :repository_config, :jsonb + add_column :bitbucket_integrations, :workspace, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 79306a168..ba223ea15 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_08_26_110821) do +ActiveRecord::Schema[7.2].define(version: 2025_09_08_020424) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -133,6 +133,8 @@ t.string "oauth_refresh_token" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "repository_config" + t.string "workspace" end create_table "bitrise_integrations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -363,6 +365,7 @@ t.string "installation_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "repository_config" end create_table "gitlab_integrations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -370,6 +373,7 @@ t.string "oauth_refresh_token" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "repository_config" end create_table "google_firebase_integrations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| From 253321ffac13e84bea4f1547595e52f6705e0780 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 14 Sep 2025 11:35:58 +0530 Subject: [PATCH 02/26] fix: Make Bitbucket workspace dropdown switcher work Bitbucket version control configuration requires the user to select a workspace to select the code repository. The workspace dropdown is supposed work by re-rendering the configuration form via Turbo stream. However, the partial for the TURBO_STREAM format was missing, which would break the interaction, along with raising a server-side error. These changes refactor the HTML partial to render the form in both the HTML and the TURBO_STREAM formats, to make the drop-down UX work. --- .../integration_card_component.html.erb | 4 +-- .../github_configs_controller.rb | 1 - .../gitlab_configs_controller.rb | 1 - .../bitbucket_configs/_form.html.erb | 32 ++++++++++++++++++ .../edit.html+turbo_frame.erb | 33 +------------------ .../bitbucket_configs/edit.turbo_stream.erb | 1 + 6 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 app/views/version_control/bitbucket_configs/_form.html.erb create mode 100644 app/views/version_control/bitbucket_configs/edit.turbo_stream.erb diff --git a/app/components/integration_card_component.html.erb b/app/components/integration_card_component.html.erb index 86a6f75d4..fb08d37ea 100644 --- a/app/components/integration_card_component.html.erb +++ b/app/components/integration_card_component.html.erb @@ -23,8 +23,8 @@
<% if further_setup? %> <%= render ModalComponent.new(title: category_title, open: pre_open_category?) do |modal| %> - <% modal.with_button(scheme: :light, type: :action, size: :xxs) - .with_icon("cog.svg", size: :sm) %> + <% button = modal.with_button(scheme: :light, type: :action, size: :xxs) %> + <% button.with_icon("cog.svg", size: :sm) %> <% modal.with_body do %> <%= tag.turbo_frame id: category_config_turbo_frame_id, src: edit_config_path, diff --git a/app/controllers/version_control/github_configs_controller.rb b/app/controllers/version_control/github_configs_controller.rb index fe693d735..aa5fbafb9 100644 --- a/app/controllers/version_control/github_configs_controller.rb +++ b/app/controllers/version_control/github_configs_controller.rb @@ -13,7 +13,6 @@ def edit format.html do |variant| variant.turbo_frame { render :edit } end - format.turbo_stream { render :edit } # NOTE: probably not needed for GithubIntegration end end diff --git a/app/controllers/version_control/gitlab_configs_controller.rb b/app/controllers/version_control/gitlab_configs_controller.rb index 608e818fc..41543bc23 100644 --- a/app/controllers/version_control/gitlab_configs_controller.rb +++ b/app/controllers/version_control/gitlab_configs_controller.rb @@ -13,7 +13,6 @@ def edit format.html do |variant| variant.turbo_frame { render :edit } end - format.turbo_stream { render :edit } # NOTE: probably not needed for GitlabIntegration end end diff --git a/app/views/version_control/bitbucket_configs/_form.html.erb b/app/views/version_control/bitbucket_configs/_form.html.erb new file mode 100644 index 000000000..caf642a3e --- /dev/null +++ b/app/views/version_control/bitbucket_configs/_form.html.erb @@ -0,0 +1,32 @@ +<%= render FormComponent.new(model: [app, bitbucket_integration], url: app_version_control_bitbucket_config_path(app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Repository") do |section| %> + <% section.with_description do %> + Primary working code repository. + <% end %> + +
+ <%= section.F.labeled_select :workspace, + "Workspace", + options_for_select(workspaces, selected: bitbucket_integration.workspace), + {}, + disabled: workspaces.blank?, + class: EnhancedFormHelper::AuthzForm::SELECT_CLASSES, + data: {action: "change->stream-effect#fetch", stream_effect_target: "dispatch"} %> +
+ +
+ <%= section.F.labeled_select :repository_config, + "Code Repository", + options_for_select( + display_channels(code_repositories) { |repo| repo[:full_name] }, + code_repository.to_json + ) %> +
+ <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> +<% end %> diff --git a/app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb b/app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb index f9f680d73..e2680b7d1 100644 --- a/app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb +++ b/app/views/version_control/bitbucket_configs/edit.html+turbo_frame.erb @@ -1,34 +1,3 @@ <%= render EnhancedTurboFrameComponent.new(:version_control_config) do %> - <%= render FormComponent.new(model: [@app, @bitbucket_integration], url: app_version_control_bitbucket_config_path(@app), method: :patch) do |f| %> - <% f.with_section(heading: "Select Repository") do |section| %> - <% section.with_description do %> - Primary working code repository. - <% end %> - -
- <%= section.F.labeled_select :workspace, - "Workspace", - options_for_select(@workspaces, selected: @bitbucket_integration.workspace), - {}, - disabled: @workspaces.blank?, - class: EnhancedFormHelper::AuthzForm::SELECT_CLASSES, - data: {action: "change->stream-effect#fetch", stream_effect_target: "dispatch"} %> -
- -
- <%= section.F.labeled_select :repository_config, - "Code Repository", - options_for_select( - display_channels(@code_repositories) { |repo| repo[:full_name] }, - @code_repository.to_json - ) %> -
- <% end %> - - <% f.with_action do %> - <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> - <% end %> - <% end %> + <%= render partial: "form", locals: {app: @app, bitbucket_integration: @bitbucket_integration, workspaces: @workspaces, code_repositories: @code_repositories} %> <% end %> diff --git a/app/views/version_control/bitbucket_configs/edit.turbo_stream.erb b/app/views/version_control/bitbucket_configs/edit.turbo_stream.erb new file mode 100644 index 000000000..784719a81 --- /dev/null +++ b/app/views/version_control/bitbucket_configs/edit.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.update :version_control_config, partial: "form", locals: {app: @app, bitbucket_integration: @bitbucket_integration, workspaces: @workspaces, code_repositories: @code_repositories} %> From 15e09a0cc2d7662e2f0a16d24daa9cc00d38656c Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Wed, 17 Sep 2025 09:23:27 +0530 Subject: [PATCH 03/26] feat: Add separate write paths for configuring `ci_cd` integrations Update read path on `AppConfig#further_setup_by_category?` for `ci_cd`. Fix config update translations. --- .../integration_card_component.html.erb | 4 +- app/components/integration_card_component.rb | 11 +++- app/components/integration_list_component.rb | 2 +- .../ci_cd/bitrise_configs_controller.rb | 53 +++++++++++++++++++ app/models/app_config.rb | 4 +- app/models/bitrise_integration.rb | 9 ++-- .../bitrise_configs/edit.html+turbo_frame.erb | 27 ++++++++++ config/locales/en.yml | 24 +++++---- config/routes.rb | 4 ++ ...d_project_config_to_bitrise_integration.rb | 5 ++ db/schema.rb | 3 +- 11 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 app/controllers/ci_cd/bitrise_configs_controller.rb create mode 100644 app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb create mode 100644 db/migrate/20250909193500_add_project_config_to_bitrise_integration.rb diff --git a/app/components/integration_card_component.html.erb b/app/components/integration_card_component.html.erb index fb08d37ea..59812bbf1 100644 --- a/app/components/integration_card_component.html.erb +++ b/app/components/integration_card_component.html.erb @@ -62,8 +62,8 @@ <% if creatable? %> <%= render ModalComponent.new(title: creatable_modal_title) do |modal| %> - <% modal.with_button(label: "Connect", scheme: :light, type: :action, size: :xxs, arrow: :none) - .with_icon("plus.svg", size: :md) %> + <% button = modal.with_button(label: "Connect", scheme: :light, type: :action, size: :xxs, arrow: :none) %> + <% button.with_icon("plus.svg", size: :md) %> <% modal.with_body do %> <%= creatable_form_partial %> <% end %> diff --git a/app/components/integration_card_component.rb b/app/components/integration_card_component.rb index 3a748a503..6d03815d3 100644 --- a/app/components/integration_card_component.rb +++ b/app/components/integration_card_component.rb @@ -92,7 +92,7 @@ def category_config_turbo_frame_id def further_setup? # TODO: delegate to Integration properly - integration.version_control? + integration.version_control? || integration.ci_cd? end def edit_config_path @@ -108,6 +108,15 @@ def edit_config_path else raise TypeError, "Unknown providable_type: #{integration.providable_type}" end + elsif integration.ci_cd? + case integration.providable_type + when "BitriseIntegration" + edit_app_ci_cd_bitrise_config_path(@app) + else + raise TypeError, "Unknown providable_type: #{integration.providable_type}" + end + else + raise TypeError, "further_setup? should be true only for version_control or ci_cd integrations" end end end diff --git a/app/components/integration_list_component.rb b/app/components/integration_list_component.rb index e22f76556..c2aab0dd5 100644 --- a/app/components/integration_list_component.rb +++ b/app/components/integration_list_component.rb @@ -15,6 +15,6 @@ def pre_open?(category) def connected_integrations?(integrations) # TODO: Move away from checking integration category later - integrations.any? { |i| i.connected? && i.further_setup? && !i.version_control? } + integrations.any? { |i| i.connected? && i.further_setup? && !(i.version_control? || i.ci_cd?) } end end diff --git a/app/controllers/ci_cd/bitrise_configs_controller.rb b/app/controllers/ci_cd/bitrise_configs_controller.rb new file mode 100644 index 000000000..d5e580644 --- /dev/null +++ b/app/controllers/ci_cd/bitrise_configs_controller.rb @@ -0,0 +1,53 @@ +class CiCd::BitriseConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_bitrise_integration + around_action :set_time_zone + + def edit + set_ci_cd_projects + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + end + end + + def update + if @bitrise_integration.update(parsed_bitrise_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @bitrise_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_bitrise_integration + @bitrise_integration = @app.ci_cd_provider + unless @bitrise_integration + redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} + end + end + + def set_ci_cd_projects + @ci_cd_apps = @bitrise_integration.setup + @project_config = @bitrise_integration.project_config + end + + def parsed_bitrise_config_params + bitrise_config_params = params.require(:bitrise_integration) + .permit(:project_config) + bitrise_config_params.merge( + project_config: bitrise_config_params[:project_config]&.safe_json_parse + ) + end +end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index a95dc6fa0..a66e7bafe 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -63,7 +63,7 @@ def code_repo_name_only end def bitrise_project - bitrise_project_id&.fetch("id", nil) + app.ci_cd_provider&.project_config&.fetch("id", nil) end def further_setup_by_category? @@ -160,7 +160,7 @@ def firebase_ready? def bitrise_ready? return true unless app.bitrise_connected? - bitrise_project.present? + app.ci_cd_provider.project_config.present? end def bugsnag_ready? diff --git a/app/models/bitrise_integration.rb b/app/models/bitrise_integration.rb index f9b8ba891..9ac51a8b4 100644 --- a/app/models/bitrise_integration.rb +++ b/app/models/bitrise_integration.rb @@ -2,10 +2,11 @@ # # Table name: bitrise_integrations # -# id :uuid not null, primary key -# access_token :string -# created_at :datetime not null -# updated_at :datetime not null +# id :uuid not null, primary key +# access_token :string +# project_config :jsonb +# created_at :datetime not null +# updated_at :datetime not null # class BitriseIntegration < ApplicationRecord has_paper_trail diff --git a/app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb b/app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..26bce27b0 --- /dev/null +++ b/app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb @@ -0,0 +1,27 @@ +<%= render EnhancedTurboFrameComponent.new(:ci_cd_config) do %> + <%= render FormComponent.new(model: [@app, @bitrise_integration], url: app_ci_cd_bitrise_config_path(@app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Bitrise Project") do |section| %> + <% section.with_description do %> + Choose the Bitrise project for CI/CD builds. + <%= image_tag "integrations/logo_bitrise.png", title: "Bitrise", width: 22, class: "my-2" %> + <% end %> + + <% if @ci_cd_apps.present? %> +
+ <%= section.F.labeled_select :project_config, + "App Name", + options_for_select( + display_channels(@ci_cd_apps) { |app| "#{app[:name]} (#{app[:id]})" }, + @project_config.to_json + ), + {}, + data: {controller: "input-select"} %> +
+ <% end %> + <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 5272ba553..44ac79e2f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -777,12 +777,18 @@ en: select: "Select Done States" help_text: "Choose which states represent completed work" - github_configs: - update: - success: "Version control configuration was successfully updated." - gitlab_configs: - update: - success: "Version control configuration was successfully updated." - bitbucket_configs: - update: - success: "Version control configuration was successfully updated." + version_control: + github_configs: + update: + success: "Version control configuration was successfully updated." + gitlab_configs: + update: + success: "Version control configuration was successfully updated." + bitbucket_configs: + update: + success: "Version control configuration was successfully updated." + + ci_cd: + bitrise_configs: + update: + success: "CI/CD configuration was successfully updated." diff --git a/config/routes.rb b/config/routes.rb index 476236223..788e57bd3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,6 +75,10 @@ resource :bitbucket_config, only: %i[edit update] end + namespace :ci_cd do + resource :bitrise_config, only: %i[edit update] + end + member do get :all_builds get :search diff --git a/db/migrate/20250909193500_add_project_config_to_bitrise_integration.rb b/db/migrate/20250909193500_add_project_config_to_bitrise_integration.rb new file mode 100644 index 000000000..85b16a3ae --- /dev/null +++ b/db/migrate/20250909193500_add_project_config_to_bitrise_integration.rb @@ -0,0 +1,5 @@ +class AddProjectConfigToBitriseIntegration < ActiveRecord::Migration[7.2] + def change + add_column :bitrise_integrations, :project_config, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index ba223ea15..7304db0e0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_09_08_020424) do +ActiveRecord::Schema[7.2].define(version: 2025_09_09_193500) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -141,6 +141,7 @@ t.string "access_token" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "project_config" end create_table "bugsnag_integrations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| From 73ac1b934d205cf23a288ce0fa6450eb86208d16 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Fri, 19 Sep 2025 08:24:11 +0530 Subject: [PATCH 04/26] Add separate write paths for configuring `build_channel` integrations Update read path on `AppConfig#further_setup_by_category?` for `build_channel`. --- app/components/integration_card_component.rb | 11 +++- app/components/integration_list_component.rb | 2 +- .../google_firebase_configs_controller.rb | 54 +++++++++++++++++++ app/models/app_config.rb | 10 +++- app/models/google_firebase_integration.rb | 2 + .../_firebase_integration_form.html.erb | 24 +++++++++ .../edit.html+turbo_frame.erb | 9 ++++ config/locales/en.yml | 5 ++ config/routes.rb | 4 ++ ...d_config_to_google_firebase_integration.rb | 6 +++ db/schema.rb | 4 +- 11 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 app/controllers/build_channel/google_firebase_configs_controller.rb create mode 100644 app/views/build_channel/_firebase_integration_form.html.erb create mode 100644 app/views/build_channel/google_firebase_configs/edit.html+turbo_frame.erb create mode 100644 db/migrate/20250918025642_add_config_to_google_firebase_integration.rb diff --git a/app/components/integration_card_component.rb b/app/components/integration_card_component.rb index 6d03815d3..6955f9f57 100644 --- a/app/components/integration_card_component.rb +++ b/app/components/integration_card_component.rb @@ -92,7 +92,7 @@ def category_config_turbo_frame_id def further_setup? # TODO: delegate to Integration properly - integration.version_control? || integration.ci_cd? + integration.version_control? || integration.ci_cd? || integration.build_channel? end def edit_config_path @@ -115,8 +115,15 @@ def edit_config_path else raise TypeError, "Unknown providable_type: #{integration.providable_type}" end + elsif integration.build_channel? + case integration.providable_type + when "GoogleFirebaseIntegration" + edit_app_build_channel_google_firebase_config_path(@app) + else + raise TypeError, "Unknown providable_type: #{integration.providable_type}" + end else - raise TypeError, "further_setup? should be true only for version_control or ci_cd integrations" + raise TypeError, "further_setup? should be true only for version_control, ci_cd, or build_channel integrations" end end end diff --git a/app/components/integration_list_component.rb b/app/components/integration_list_component.rb index c2aab0dd5..317e39788 100644 --- a/app/components/integration_list_component.rb +++ b/app/components/integration_list_component.rb @@ -15,6 +15,6 @@ def pre_open?(category) def connected_integrations?(integrations) # TODO: Move away from checking integration category later - integrations.any? { |i| i.connected? && i.further_setup? && !(i.version_control? || i.ci_cd?) } + integrations.any? { |i| i.connected? && i.further_setup? && !(i.version_control? || i.ci_cd? || i.build_channel?) } end end diff --git a/app/controllers/build_channel/google_firebase_configs_controller.rb b/app/controllers/build_channel/google_firebase_configs_controller.rb new file mode 100644 index 000000000..52e1bc47a --- /dev/null +++ b/app/controllers/build_channel/google_firebase_configs_controller.rb @@ -0,0 +1,54 @@ +class BuildChannel::GoogleFirebaseConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_google_firebase_integration + around_action :set_time_zone + + def edit + set_firebase_apps + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + end + end + + def update + if @google_firebase_integration.update(parsed_google_firebase_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @google_firebase_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_google_firebase_integration + @google_firebase_integration = @app.integrations.firebase_build_channel_provider + unless @google_firebase_integration + redirect_to app_integrations_path(@app), flash: {error: "Firebase build channel integration not found."} + end + end + + def set_firebase_apps + config = @google_firebase_integration.setup + @firebase_android_apps, @firebase_ios_apps = config[:android], config[:ios] + end + + def parsed_google_firebase_config_params + google_firebase_config_params = params.require(:google_firebase_integration) + .permit(:android_config, :ios_config) + google_firebase_config_params.merge( + android_config: google_firebase_config_params[:android_config]&.safe_json_parse, + ios_config: google_firebase_config_params[:ios_config]&.safe_json_parse + ) + end +end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index a66e7bafe..332cc7b3e 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -155,7 +155,15 @@ def set_bugsnag_config def firebase_ready? return true unless app.firebase_connected? - configs_ready?(firebase_ios_config, firebase_android_config) + + firebase_build_channel = app.integrations.firebase_build_channel_provider + return firebase_build_channel.ios_config.present? if app.ios? + return firebase_build_channel.android_config.present? if app.android? + + if app.cross_platform? + firebase_build_channel.ios_config.present? && + firebase_build_channel.android_config.present? + end end def bitrise_ready? diff --git a/app/models/google_firebase_integration.rb b/app/models/google_firebase_integration.rb index d46629f27..86f13f67d 100644 --- a/app/models/google_firebase_integration.rb +++ b/app/models/google_firebase_integration.rb @@ -3,6 +3,8 @@ # Table name: google_firebase_integrations # # id :uuid not null, primary key +# android_config :jsonb +# ios_config :jsonb # json_key :string # project_number :string # created_at :datetime not null diff --git a/app/views/build_channel/_firebase_integration_form.html.erb b/app/views/build_channel/_firebase_integration_form.html.erb new file mode 100644 index 000000000..ffd353f9f --- /dev/null +++ b/app/views/build_channel/_firebase_integration_form.html.erb @@ -0,0 +1,24 @@ +<% f.with_section(heading: "Select Firebase Apps") do |section| %> + <% section.with_description do %> + Apps against your connected Firebase project. + <%= image_tag "integrations/logo_firebase.png", title: "Firebase", width: 24, class: "py-2" %> + <% end %> + + <% if firebase_android_apps.present? %> +
+ <%= section.F.labeled_select :android_config, + "Android App", + options_for_select(display_channels(firebase_android_apps) { |app| "#{app[:display_name]} #{app[:app_id]}" }, + integration.android_config.to_json) %> +
+ <% end %> + + <% if firebase_ios_apps.present? %> +
+ <%= section.F.labeled_select :ios_config, + "iOS App", + options_for_select(display_channels(firebase_ios_apps) { |app| "#{app[:display_name]} #{app[:app_id]}" }, + integration.ios_config.to_json) %> +
+ <% end %> +<% end %> diff --git a/app/views/build_channel/google_firebase_configs/edit.html+turbo_frame.erb b/app/views/build_channel/google_firebase_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..8a5066d46 --- /dev/null +++ b/app/views/build_channel/google_firebase_configs/edit.html+turbo_frame.erb @@ -0,0 +1,9 @@ +<%= render EnhancedTurboFrameComponent.new(:build_channel_config) do %> + <%= render FormComponent.new(model: [@app, @google_firebase_integration], url: app_build_channel_google_firebase_config_path(@app), method: :patch) do |f| %> + <%= render partial: "build_channel/firebase_integration_form", locals: {f:, integration: @google_firebase_integration, firebase_ios_apps: @firebase_ios_apps, firebase_android_apps: @firebase_android_apps} %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 44ac79e2f..f137152e9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -792,3 +792,8 @@ en: bitrise_configs: update: success: "CI/CD configuration was successfully updated." + + build_channel: + google_firebase_configs: + update: + success: "Build channel configuration was successfully updated." diff --git a/config/routes.rb b/config/routes.rb index 788e57bd3..c27920e91 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,6 +79,10 @@ resource :bitrise_config, only: %i[edit update] end + namespace :build_channel do + resource :google_firebase_config, only: %i[edit update] + end + member do get :all_builds get :search diff --git a/db/migrate/20250918025642_add_config_to_google_firebase_integration.rb b/db/migrate/20250918025642_add_config_to_google_firebase_integration.rb new file mode 100644 index 000000000..77a145c05 --- /dev/null +++ b/db/migrate/20250918025642_add_config_to_google_firebase_integration.rb @@ -0,0 +1,6 @@ +class AddConfigToGoogleFirebaseIntegration < ActiveRecord::Migration[7.2] + def change + add_column :google_firebase_integrations, :android_config, :jsonb + add_column :google_firebase_integrations, :ios_config, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 7304db0e0..4429b3e3d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_09_09_193500) do +ActiveRecord::Schema[7.2].define(version: 2025_09_18_025642) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -383,6 +383,8 @@ t.string "app_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "android_config" + t.jsonb "ios_config" end create_table "google_play_store_integrations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| From 379adce9b53f247fe7568e5046e5aa523c094abb Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Tue, 23 Sep 2025 00:45:03 +0530 Subject: [PATCH 05/26] fix: Pre-select persisted integration config value The persisted config value, when serialized, could generate a JSON string that could be different from the one generated by the view helper for the drop-down options' values, which would make it so that the persisted config value is not pre-selected by default. Thus, we explicitly find the correct config and use that as the default, rather than using the persisted value. --- app/views/build_channel/_firebase_integration_form.html.erb | 4 ++-- app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb | 2 +- app/views/version_control/bitbucket_configs/_form.html.erb | 4 ++-- .../version_control/github_configs/edit.html+turbo_frame.erb | 2 +- .../version_control/gitlab_configs/edit.html+turbo_frame.erb | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/build_channel/_firebase_integration_form.html.erb b/app/views/build_channel/_firebase_integration_form.html.erb index ffd353f9f..9bd2a6033 100644 --- a/app/views/build_channel/_firebase_integration_form.html.erb +++ b/app/views/build_channel/_firebase_integration_form.html.erb @@ -9,7 +9,7 @@ <%= section.F.labeled_select :android_config, "Android App", options_for_select(display_channels(firebase_android_apps) { |app| "#{app[:display_name]} #{app[:app_id]}" }, - integration.android_config.to_json) %> + firebase_android_apps.find { |app| app[:app_id] == integration.android_config&.dig("app_id") }&.to_json) %>
<% end %> @@ -18,7 +18,7 @@ <%= section.F.labeled_select :ios_config, "iOS App", options_for_select(display_channels(firebase_ios_apps) { |app| "#{app[:display_name]} #{app[:app_id]}" }, - integration.ios_config.to_json) %> + firebase_ios_apps.find { |app| app[:app_id] == integration.ios_config&.dig("app_id") }&.to_json) %> <% end %> <% end %> diff --git a/app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb b/app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb index 26bce27b0..97d03a28e 100644 --- a/app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb +++ b/app/views/ci_cd/bitrise_configs/edit.html+turbo_frame.erb @@ -12,7 +12,7 @@ "App Name", options_for_select( display_channels(@ci_cd_apps) { |app| "#{app[:name]} (#{app[:id]})" }, - @project_config.to_json + @ci_cd_apps.find { |app| app[:id] == @project_config&.dig("id") }&.to_json ), {}, data: {controller: "input-select"} %> diff --git a/app/views/version_control/bitbucket_configs/_form.html.erb b/app/views/version_control/bitbucket_configs/_form.html.erb index caf642a3e..da11e409c 100644 --- a/app/views/version_control/bitbucket_configs/_form.html.erb +++ b/app/views/version_control/bitbucket_configs/_form.html.erb @@ -9,7 +9,7 @@ data-stream-effect-param-value="workspace"> <%= section.F.labeled_select :workspace, "Workspace", - options_for_select(workspaces, selected: bitbucket_integration.workspace), + options_for_select(workspaces, bitbucket_integration.workspace), {}, disabled: workspaces.blank?, class: EnhancedFormHelper::AuthzForm::SELECT_CLASSES, @@ -21,7 +21,7 @@ "Code Repository", options_for_select( display_channels(code_repositories) { |repo| repo[:full_name] }, - code_repository.to_json + code_repositories.find { |repo| repo[:full_name] == bitbucket_integration.repository_config&.dig("full_name") }&.to_json ) %> <% end %> diff --git a/app/views/version_control/github_configs/edit.html+turbo_frame.erb b/app/views/version_control/github_configs/edit.html+turbo_frame.erb index 5fb51741c..49e684882 100644 --- a/app/views/version_control/github_configs/edit.html+turbo_frame.erb +++ b/app/views/version_control/github_configs/edit.html+turbo_frame.erb @@ -10,7 +10,7 @@ "Code Repository", options_for_select( display_channels(@code_repositories) { |repo| repo[:full_name] }, - @code_repository.to_json + @code_repositories.find { |repo| repo[:id] == @github_integration.repository_config&.dig("id") }&.to_json ) %> <% end %> diff --git a/app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb b/app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb index 3e6cc0401..9ac473feb 100644 --- a/app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb +++ b/app/views/version_control/gitlab_configs/edit.html+turbo_frame.erb @@ -10,7 +10,7 @@ "Code Repository", options_for_select( display_channels(@code_repositories) { |repo| repo[:full_name] }, - @code_repository.to_json + @code_repositories.find { |repo| repo[:id] == @gitlab_integration.repository_config&.dig("id") }&.to_json ) %> <% end %> From a5d2f57d5b650498d456d96f75559caa023008bf Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Tue, 23 Sep 2025 01:02:40 +0530 Subject: [PATCH 06/26] chore: Fix integration card pre-opening UX Simplify some config read paths in `AppConfig`. --- app/components/integration_card_component.rb | 3 ++- app/components/integration_list_component.html.erb | 2 +- app/models/app_config.rb | 13 +++---------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/components/integration_card_component.rb b/app/components/integration_card_component.rb index 6955f9f57..e3d1f0f62 100644 --- a/app/components/integration_card_component.rb +++ b/app/components/integration_card_component.rb @@ -10,10 +10,11 @@ class IntegrationCardComponent < BaseComponent bitrise: "Access Token" } - def initialize(app, integration, category) + def initialize(app, integration, category, pre_open_category = nil) @app = app @integration = integration @category = category + @pre_open_category = pre_open_category end attr_reader :integration diff --git a/app/components/integration_list_component.html.erb b/app/components/integration_list_component.html.erb index 5a3fa5d37..aa7db068a 100644 --- a/app/components/integration_list_component.html.erb +++ b/app/components/integration_list_component.html.erb @@ -5,7 +5,7 @@
<% integrations.each do |integration| %> - <%= render IntegrationCardComponent.new(@app, integration, category) %> + <%= render IntegrationCardComponent.new(@app, integration, category, @pre_open_category) %> <% end %>
diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 332cc7b3e..8b999f271 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -73,7 +73,7 @@ def further_setup_by_category? if integrations.version_control.present? categories[:version_control] = { further_setup: integrations.version_control.any?(&:further_setup?), - ready: integrations.version_control.any? { _1.providable&.repository_config.present? } # TODO: perhaps &:ready? makes sense? + ready: code_repository.present? } end @@ -155,20 +155,13 @@ def set_bugsnag_config def firebase_ready? return true unless app.firebase_connected? - firebase_build_channel = app.integrations.firebase_build_channel_provider - return firebase_build_channel.ios_config.present? if app.ios? - return firebase_build_channel.android_config.present? if app.android? - - if app.cross_platform? - firebase_build_channel.ios_config.present? && - firebase_build_channel.android_config.present? - end + configs_ready?(firebase_build_channel.ios_config, firebase_build_channel.android_config) end def bitrise_ready? return true unless app.bitrise_connected? - app.ci_cd_provider.project_config.present? + bitrise_project.present? end def bugsnag_ready? From 04abfa04f9c2d489ffe5ba3c6b377ca19e93078d Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Tue, 23 Sep 2025 02:40:50 +0530 Subject: [PATCH 07/26] feat: Add separate write paths for configuring `monitoring` integrations Update read paths for `monitoring` config in `AppConfig` for: - `#further_setup_by_category?` - bugsnag related attributes --- app/components/integration_card_component.rb | 11 ++- app/components/integration_list_component.rb | 2 +- .../monitoring/bugsnag_configs_controller.rb | 86 +++++++++++++++++++ app/models/app_config.rb | 26 ++---- app/models/bugsnag_integration.rb | 42 +++++++-- .../monitoring/_bugsnag_project_form.html.erb | 31 +++++++ .../bugsnag_configs/edit.html+turbo_frame.erb | 33 +++++++ config/locales/en.yml | 5 ++ config/routes.rb | 4 + ...24803_add_config_to_bugsnag_integration.rb | 6 ++ db/schema.rb | 4 +- 11 files changed, 222 insertions(+), 28 deletions(-) create mode 100644 app/controllers/monitoring/bugsnag_configs_controller.rb create mode 100644 app/views/monitoring/_bugsnag_project_form.html.erb create mode 100644 app/views/monitoring/bugsnag_configs/edit.html+turbo_frame.erb create mode 100644 db/migrate/20250919124803_add_config_to_bugsnag_integration.rb diff --git a/app/components/integration_card_component.rb b/app/components/integration_card_component.rb index e3d1f0f62..a8d47413b 100644 --- a/app/components/integration_card_component.rb +++ b/app/components/integration_card_component.rb @@ -93,7 +93,7 @@ def category_config_turbo_frame_id def further_setup? # TODO: delegate to Integration properly - integration.version_control? || integration.ci_cd? || integration.build_channel? + integration.version_control? || integration.ci_cd? || integration.build_channel? || integration.monitoring? end def edit_config_path @@ -123,8 +123,15 @@ def edit_config_path else raise TypeError, "Unknown providable_type: #{integration.providable_type}" end + elsif integration.monitoring? + case integration.providable_type + when "BugsnagIntegration" + edit_app_monitoring_bugsnag_config_path(@app) + else + raise TypeError, "Unknown providable_type: #{integration.providable_type}" + end else - raise TypeError, "further_setup? should be true only for version_control, ci_cd, or build_channel integrations" + raise TypeError, "further_setup? should be true only for version_control, ci_cd, build_channel, or monitoring integrations" end end end diff --git a/app/components/integration_list_component.rb b/app/components/integration_list_component.rb index 317e39788..3fc4aa69f 100644 --- a/app/components/integration_list_component.rb +++ b/app/components/integration_list_component.rb @@ -15,6 +15,6 @@ def pre_open?(category) def connected_integrations?(integrations) # TODO: Move away from checking integration category later - integrations.any? { |i| i.connected? && i.further_setup? && !(i.version_control? || i.ci_cd? || i.build_channel?) } + integrations.any? { |i| i.connected? && i.further_setup? && !(i.version_control? || i.ci_cd? || i.build_channel? || i.monitoring?) } end end diff --git a/app/controllers/monitoring/bugsnag_configs_controller.rb b/app/controllers/monitoring/bugsnag_configs_controller.rb new file mode 100644 index 000000000..b6aea3a0b --- /dev/null +++ b/app/controllers/monitoring/bugsnag_configs_controller.rb @@ -0,0 +1,86 @@ +class Monitoring::BugsnagConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_bugsnag_integration + around_action :set_time_zone + + def edit + set_monitoring_projects + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + end + end + + def update + if @bugsnag_integration.update(parsed_bugsnag_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @bugsnag_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_bugsnag_integration + @bugsnag_integration = @app.monitoring_provider + unless @bugsnag_integration + redirect_to app_integrations_path(@app), flash: {error: "Monitoring integration not found."} + end + end + + def set_monitoring_projects + @monitoring_projects = @bugsnag_integration.setup + end + + def bugsnag_integration_params + params + .require(:bugsnag_integration) + .permit( + :ios_project_id, + :ios_release_stage, + :android_project_id, + :android_release_stage + ) + end + + def parsed_bugsnag_config_params + bugsnag_config_params = params.require(:bugsnag_integration) + .permit( + :ios_project_id, + :ios_release_stage, + :android_project_id, + :android_release_stage + ) + bugsnag_config_params.merge(bugsnag_config(bugsnag_config_params)) + end + + def bugsnag_config(config_params) + config = {} + + if config_params[:ios_release_stage].present? + config[:ios_config] = { + project_id: config_params[:ios_project_id]&.safe_json_parse, + release_stage: config_params[:ios_release_stage] + } + end + + if config_params[:android_release_stage].present? + config[:android_config] = { + project_id: config_params[:android_project_id]&.safe_json_parse, + release_stage: config_params[:android_release_stage] + } + end + + config + end +end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 8b999f271..9b10f55fa 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -109,21 +109,11 @@ def further_setup_by_category? end def bugsnag_project(platform) - case platform - when "android" then bugsnag_android_config["project_id"] - when "ios" then bugsnag_ios_config["project_id"] - else - raise ArgumentError, INVALID_PLATFORM_ERROR - end + app.monitoring_provider.project(platform) end def bugsnag_release_stage(platform) - case platform - when "android" then bugsnag_android_config["release_stage"] - when "ios" then bugsnag_ios_config["release_stage"] - else - raise ArgumentError, INVALID_PLATFORM_ERROR - end + app.monitoring_provider.release_stage(platform) end def ci_cd_workflows @@ -147,10 +137,11 @@ def code_repository private def set_bugsnag_config - self.bugsnag_ios_release_stage = bugsnag_ios_config&.fetch("release_stage", nil) - self.bugsnag_ios_project_id = bugsnag_ios_config&.fetch("project_id", nil) - self.bugsnag_android_release_stage = bugsnag_android_config&.fetch("release_stage", nil) - self.bugsnag_android_project_id = bugsnag_android_config&.fetch("project_id", nil) + monitoring_provider = app.monitoring_provider + self.bugsnag_ios_release_stage = monitoring_provider.ios_release_stage + self.bugsnag_ios_project_id = monitoring_provider.ios_project_id + self.bugsnag_android_release_stage = monitoring_provider.android_release_stage + self.bugsnag_android_project_id = monitoring_provider.android_project_id end def firebase_ready? @@ -166,7 +157,8 @@ def bitrise_ready? def bugsnag_ready? return true unless app.bugsnag_connected? - configs_ready?(bugsnag_ios_config, bugsnag_android_config) + monitoring_provider = app.monitoring_provider + configs_ready?(monitoring_provider.ios_config, monitoring_provider.android_config) end def configs_ready?(ios, android) diff --git a/app/models/bugsnag_integration.rb b/app/models/bugsnag_integration.rb index d1e3702ab..ee1682deb 100644 --- a/app/models/bugsnag_integration.rb +++ b/app/models/bugsnag_integration.rb @@ -2,10 +2,12 @@ # # Table name: bugsnag_integrations # -# id :uuid not null, primary key -# access_token :string -# created_at :datetime not null -# updated_at :datetime not null +# id :uuid not null, primary key +# access_token :string +# android_config :jsonb +# ios_config :jsonb +# created_at :datetime not null +# updated_at :datetime not null # class BugsnagIntegration < ApplicationRecord has_paper_trail @@ -14,6 +16,8 @@ class BugsnagIntegration < ApplicationRecord include Displayable include Rails.application.routes.url_helpers + attr_accessor :ios_release_stage, :android_release_stage, :ios_project_id, :android_project_id + CACHE_EXPIRY = 1.month API = Installations::Bugsnag::Api @@ -46,12 +50,11 @@ class BugsnagIntegration < ApplicationRecord validate :correct_key, on: :create validates :access_token, presence: true + after_initialize :set_bugsnag_config, if: :persisted? + encrypts :access_token, deterministic: true delegate :cache, to: Rails delegate :integrable, to: :integration - delegate :bugsnag_project, :bugsnag_release_stage, to: :app_config - alias_method :project, :bugsnag_project - alias_method :release_stage, :bugsnag_release_stage def installation API.new(access_token) @@ -110,8 +113,33 @@ def dashboard_url(platform:, release_id:) "#{project_url(platform)}/overview?release_stage=#{release_stage(platform)}" end + def project(platform) + case platform + when "android" then android_config&.dig("project_id") + when "ios" then ios_config&.dig("project_id") + else + raise ArgumentError, "Invalid platform: #{platform}" + end + end + + def release_stage(platform) + case platform + when "android" then android_config&.dig("release_stage") + when "ios" then ios_config&.dig("release_stage") + else + raise ArgumentError, "Invalid platform: #{platform}" + end + end + private + def set_bugsnag_config + self.ios_release_stage = ios_config&.fetch("release_stage", nil) + self.ios_project_id = ios_config&.fetch("project_id", nil) + self.android_release_stage = android_config&.fetch("release_stage", nil) + self.android_project_id = android_config&.fetch("project_id", nil) + end + def project_url(platform) project(platform)&.fetch("url", nil) end diff --git a/app/views/monitoring/_bugsnag_project_form.html.erb b/app/views/monitoring/_bugsnag_project_form.html.erb new file mode 100644 index 000000000..40de90318 --- /dev/null +++ b/app/views/monitoring/_bugsnag_project_form.html.erb @@ -0,0 +1,31 @@ +<% form.with_section(heading: platform) do |section| %> + <% section.with_description do %> + For your connected Bugsnag organization. + <%= image_tag "integrations/logo_bugsnag.png", title: "Bugsnag", width: 22, class: "mt-2" %> + <% end %> + +
+
+ <%= section.F.labeled_select "#{platform.downcase}_project_id", + "Project", + options_for_select( + display_channels(projects.map { |p| p.except(:release_stages) }) { |project| "#{project[:name]} (#{project[:id]})" }, + projects.find { |proj| proj[:id] == project&.dig("id") }&.except(:release_stages)&.to_json + ), + {}, + {data: {nested_select_target: "primary", action: "nested-select#updateNestedOptions"}} %> +
+
+ <%= section.F.labeled_select "#{platform.downcase}_release_stage", + "Release Stage", + {}, + {}, + {data: {nested_select_target: "nested"}} %> +
+
+<% end %> diff --git a/app/views/monitoring/bugsnag_configs/edit.html+turbo_frame.erb b/app/views/monitoring/bugsnag_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..a8f2ce0d8 --- /dev/null +++ b/app/views/monitoring/bugsnag_configs/edit.html+turbo_frame.erb @@ -0,0 +1,33 @@ +<%= render EnhancedTurboFrameComponent.new(:monitoring_config) do %> + <% if @monitoring_projects.present? %> + <%= render FormComponent.new(model: [@app, @bugsnag_integration], url: app_monitoring_bugsnag_config_path(@app), method: :patch) do |f| %> + <% if @app.bugsnag_connected? %> + <% if @app.ios? || @app.cross_platform? %> + <%= render partial: "monitoring/bugsnag_project_form", + locals: { + form: f, + platform: "iOS", + projects: @monitoring_projects, + project: @bugsnag_integration.ios_project_id, + stage: @bugsnag_integration.ios_release_stage + } %> + <% end %> + + <% if @app.android? || @app.cross_platform? %> + <%= render partial: "monitoring/bugsnag_project_form", + locals: { + form: f, + platform: "Android", + projects: @monitoring_projects, + project: @bugsnag_integration.android_project_id, + stage: @bugsnag_integration.android_release_stage + } %> + <% end %> + <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index f137152e9..cb265fa57 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -797,3 +797,8 @@ en: google_firebase_configs: update: success: "Build channel configuration was successfully updated." + + monitoring: + bugsnag_configs: + update: + success: "Monitoring configuration was successfully updated." diff --git a/config/routes.rb b/config/routes.rb index c27920e91..a643c170b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -83,6 +83,10 @@ resource :google_firebase_config, only: %i[edit update] end + namespace :monitoring do + resource :bugsnag_config, only: %i[edit update] + end + member do get :all_builds get :search diff --git a/db/migrate/20250919124803_add_config_to_bugsnag_integration.rb b/db/migrate/20250919124803_add_config_to_bugsnag_integration.rb new file mode 100644 index 000000000..556fc3370 --- /dev/null +++ b/db/migrate/20250919124803_add_config_to_bugsnag_integration.rb @@ -0,0 +1,6 @@ +class AddConfigToBugsnagIntegration < ActiveRecord::Migration[7.2] + def change + add_column :bugsnag_integrations, :android_config, :jsonb + add_column :bugsnag_integrations, :ios_config, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 4429b3e3d..6dd97f95a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_09_18_025642) do +ActiveRecord::Schema[7.2].define(version: 2025_09_19_124803) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -148,6 +148,8 @@ t.string "access_token" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "android_config" + t.jsonb "ios_config" end create_table "build_artifacts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| From ce9d69632c4189de0ae88991ba37dcdfffd1c807 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Fri, 26 Sep 2025 08:36:52 +0530 Subject: [PATCH 08/26] feat: Add separate write paths for configuring `project_management` integrations Update read path for `project_management` config in `AppConfig#further_setup_by_category?`. Remove unnecessary code and markup from `IntegrationCard` and `IntegrationList` components. Remove unused strong params method in `Monitoring::BugsnagConfigsController` controller. --- app/components/integration_card_component.rb | 96 ++++++++------- .../integration_list_component.html.erb | 16 --- app/components/integration_list_component.rb | 5 - .../monitoring/bugsnag_configs_controller.rb | 11 -- .../jira_configs_controller.rb | 109 ++++++++++++++++++ .../linear_configs_controller.rb | 100 ++++++++++++++++ app/models/app_config.rb | 20 ++-- app/models/jira_integration.rb | 7 +- app/models/linear_integration.rb | 7 +- .../_project_selection.html.erb | 4 +- .../_release_filter_form.html.erb | 12 +- .../_release_filter_form.html.erb | 2 +- .../_team_selection.html.erb | 4 +- .../jira_configs/edit.html+turbo_frame.erb | 28 +++++ .../linear_configs/edit.html+turbo_frame.erb | 28 +++++ config/locales/en.yml | 8 ++ config/routes.rb | 5 + ...nfig_to_project_management_integrations.rb | 6 + db/schema.rb | 4 +- 19 files changed, 371 insertions(+), 101 deletions(-) create mode 100644 app/controllers/project_management/jira_configs_controller.rb create mode 100644 app/controllers/project_management/linear_configs_controller.rb create mode 100644 app/views/project_management/jira_configs/edit.html+turbo_frame.erb create mode 100644 app/views/project_management/linear_configs/edit.html+turbo_frame.erb create mode 100644 db/migrate/20250924035033_add_config_to_project_management_integrations.rb diff --git a/app/components/integration_card_component.rb b/app/components/integration_card_component.rb index a8d47413b..260a2a8e6 100644 --- a/app/components/integration_card_component.rb +++ b/app/components/integration_card_component.rb @@ -23,7 +23,8 @@ def initialize(app, integration, category, pre_open_category = nil) :providable, :connection_data, :providable_type, - :disconnectable_categories?, to: :integration, allow_nil: true + :disconnectable_categories?, + :further_setup?, to: :integration, allow_nil: true delegate :creatable?, :connectable?, to: :provider alias_method :provider, :providable @@ -91,47 +92,62 @@ def category_config_turbo_frame_id "#{@category}_config" end - def further_setup? - # TODO: delegate to Integration properly - integration.version_control? || integration.ci_cd? || integration.build_channel? || integration.monitoring? + def edit_config_path + case integration.category + when "version_control" then edit_app_version_control_config_path + when "ci_cd" then edit_app_ci_cd_config_path + when "build_channel" then edit_app_build_channel_config_path + when "monitoring" then edit_app_monitoring_config_path + when "project_management" then edit_app_project_management_config_path + else unsupported_integration_category + end end - def edit_config_path - # TODO: find a potentially better way to route this - if integration.version_control? - case integration.providable_type - when "GithubIntegration" - edit_app_version_control_github_config_path(@app) - when "GitlabIntegration" - edit_app_version_control_gitlab_config_path(@app) - when "BitbucketIntegration" - edit_app_version_control_bitbucket_config_path(@app) - else - raise TypeError, "Unknown providable_type: #{integration.providable_type}" - end - elsif integration.ci_cd? - case integration.providable_type - when "BitriseIntegration" - edit_app_ci_cd_bitrise_config_path(@app) - else - raise TypeError, "Unknown providable_type: #{integration.providable_type}" - end - elsif integration.build_channel? - case integration.providable_type - when "GoogleFirebaseIntegration" - edit_app_build_channel_google_firebase_config_path(@app) - else - raise TypeError, "Unknown providable_type: #{integration.providable_type}" - end - elsif integration.monitoring? - case integration.providable_type - when "BugsnagIntegration" - edit_app_monitoring_bugsnag_config_path(@app) - else - raise TypeError, "Unknown providable_type: #{integration.providable_type}" - end - else - raise TypeError, "further_setup? should be true only for version_control, ci_cd, build_channel, or monitoring integrations" + private + + def edit_app_version_control_config_path + case integration.providable_type + when "GithubIntegration" then edit_app_version_control_github_config_path(@app) + when "GitlabIntegration" then edit_app_version_control_gitlab_config_path(@app) + when "BitbucketIntegration" then edit_app_version_control_bitbucket_config_path(@app) + else unsupported_integration_type + end + end + + def edit_app_ci_cd_config_path + case integration.providable_type + when "BitriseIntegration" then edit_app_ci_cd_bitrise_config_path(@app) + else unsupported_integration_type + end + end + + def edit_app_build_channel_config_path + case integration.providable_type + when "GoogleFirebaseIntegration" then edit_app_build_channel_google_firebase_config_path(@app) + else unsupported_integration_type end end + + def edit_app_monitoring_config_path + case integration.providable_type + when "BugsnagIntegration" then edit_app_monitoring_bugsnag_config_path(@app) + else unsupported_integration_type + end + end + + def edit_app_project_management_config_path + case integration.providable_type + when "JiraIntegration" then edit_app_project_management_jira_config_path(@app) + when "LinearIntegration" then edit_app_project_management_linear_config_path(@app) + else unsupported_integration_type + end + end + + def unsupported_integration_category + raise TypeError, "Unsupported integration category: #{integration.category}" + end + + def unsupported_integration_type + raise TypeError, "Unsupported integration type: #{integration.providable_type}" + end end diff --git a/app/components/integration_list_component.html.erb b/app/components/integration_list_component.html.erb index aa7db068a..c99db525c 100644 --- a/app/components/integration_list_component.html.erb +++ b/app/components/integration_list_component.html.erb @@ -9,22 +9,6 @@ <% end %> - <% if connected_integrations?(integrations) %> - <% sc.with_sidenote do %> - <%= render ModalComponent.new(title: title(category), open: pre_open?(category)) do |modal| %> - <% modal.with_button(label: "Configure", scheme: :light, type: :action, size: :xxs, arrow: :none) - .with_icon("cog.svg", size: :md) %> - <% modal.with_body do %> - <%= tag.turbo_frame id: "#{category}_config", - src: edit_app_app_config_path(@app, integration_category: category), - loading: :lazy, - class: "with-turbo-frame-loader" do %> - <%= render LoadingIndicatorComponent.new(skeleton_only: true, turbo_frame: true) %> - <% end %> - <% end %> - <% end %> - <% end %> - <% end %> <% end %> <% end %> <%= render SectionComponent.new(style: :titled, title: "Coming Soon") do %> diff --git a/app/components/integration_list_component.rb b/app/components/integration_list_component.rb index 3fc4aa69f..7f6bee7f3 100644 --- a/app/components/integration_list_component.rb +++ b/app/components/integration_list_component.rb @@ -12,9 +12,4 @@ def title(category) def pre_open?(category) @pre_open_category == category end - - def connected_integrations?(integrations) - # TODO: Move away from checking integration category later - integrations.any? { |i| i.connected? && i.further_setup? && !(i.version_control? || i.ci_cd? || i.build_channel? || i.monitoring?) } - end end diff --git a/app/controllers/monitoring/bugsnag_configs_controller.rb b/app/controllers/monitoring/bugsnag_configs_controller.rb index b6aea3a0b..0ab8960f4 100644 --- a/app/controllers/monitoring/bugsnag_configs_controller.rb +++ b/app/controllers/monitoring/bugsnag_configs_controller.rb @@ -42,17 +42,6 @@ def set_monitoring_projects @monitoring_projects = @bugsnag_integration.setup end - def bugsnag_integration_params - params - .require(:bugsnag_integration) - .permit( - :ios_project_id, - :ios_release_stage, - :android_project_id, - :android_release_stage - ) - end - def parsed_bugsnag_config_params bugsnag_config_params = params.require(:bugsnag_integration) .permit( diff --git a/app/controllers/project_management/jira_configs_controller.rb b/app/controllers/project_management/jira_configs_controller.rb new file mode 100644 index 000000000..4584bfa1c --- /dev/null +++ b/app/controllers/project_management/jira_configs_controller.rb @@ -0,0 +1,109 @@ +class ProjectManagement::JiraConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_jira_integration + around_action :set_time_zone + + def edit + set_jira_projects + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + end + end + + def update + if @jira_integration.update(parsed_jira_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @jira_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_jira_integration + project_management_integration = @app.project_management_provider + unless project_management_integration&.is_a?(JiraIntegration) + redirect_to app_integrations_path(@app), flash: {error: "Jira integration not found."} + end + @jira_integration = project_management_integration + end + + def set_jira_projects + @jira_data = @jira_integration.setup + + # Initialize project_config structure + @jira_integration.project_config = {} if @jira_integration.project_config.blank? + @jira_integration.project_config = { + "selected_projects" => @jira_integration.project_config["selected_projects"] || [], + "project_configs" => @jira_integration.project_config["project_configs"] || {}, + "release_tracking" => @jira_integration.project_config["release_tracking"] || { + "track_tickets" => false, + "auto_transition" => false + }, + "release_filters" => @jira_integration.project_config["release_filters"] || [] + } + + # Initialize project configs + @jira_data[:projects]&.each do |project| + project_key = project["key"] + statuses = @jira_data[:project_statuses][project_key] + done_states = statuses&.select { |status| status["name"] == "Done" }&.pluck("name") || [] + + @jira_integration.project_config["project_configs"][project_key] ||= { + "done_states" => done_states + } + end + + @jira_integration.save! if @jira_integration.changed? + @current_jira_config = @jira_integration.project_config.with_indifferent_access + end + + def parsed_jira_config_params + jira_config_params = params.require(:jira_integration) + .permit( + project_config: { + selected_projects: [], + project_configs: {}, + release_tracking: [:track_tickets, :auto_transition], + release_filters: [[:type, :value, :_destroy]] + } + ) + + jira_config_params.merge(project_config: parse_jira_config(jira_config_params[:project_config])) + end + + def parse_jira_config(config_params) + return {} if config_params.blank? + + { + selected_projects: Array(config_params[:selected_projects]), + project_configs: config_params[:project_configs]&.transform_values do |project_config| + { + done_states: Array(project_config[:done_states]).compact_blank + } + end || {}, + release_tracking: { + track_tickets: ActiveModel::Type::Boolean.new.cast(config_params.dig(:release_tracking, :track_tickets)), + auto_transition: ActiveModel::Type::Boolean.new.cast(config_params.dig(:release_tracking, :auto_transition)) + }, + release_filters: config_params[:release_filters]&.values&.filter_map do |filter| + next if filter[:type].blank? || filter[:value].blank? || filter[:_destroy] == "1" + { + "type" => filter[:type], + "value" => filter[:value] + } + end || [] + } + end +end diff --git a/app/controllers/project_management/linear_configs_controller.rb b/app/controllers/project_management/linear_configs_controller.rb new file mode 100644 index 000000000..9e8991c17 --- /dev/null +++ b/app/controllers/project_management/linear_configs_controller.rb @@ -0,0 +1,100 @@ +class ProjectManagement::LinearConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_linear_integration + around_action :set_time_zone + + def edit + set_linear_projects + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + end + end + + def update + if @linear_integration.update(parsed_linear_config_params) + redirect_to app_integrations_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @linear_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_linear_integration + project_management_integration = @app.project_management_provider + unless project_management_integration&.is_a?(LinearIntegration) + redirect_to app_integrations_path(@app), flash: {error: "Linear integration not found."} + end + @linear_integration = project_management_integration + end + + def set_linear_projects + @linear_data = @linear_integration.setup + + # Initialize project_config structure + @linear_integration.project_config = {} if @linear_integration.project_config.blank? + @linear_integration.project_config = { + "selected_teams" => @linear_integration.project_config["selected_teams"] || [], + "team_configs" => @linear_integration.project_config["team_configs"] || {}, + "release_filters" => @linear_integration.project_config["release_filters"] || [] + } + + # Initialize team configs + @linear_data[:teams]&.each do |team| + team_id = team["id"] + workflow_states = @linear_data[:workflow_states] + done_states = workflow_states&.select { |state| state["type"] == "completed" }&.pluck("name") || [] + + @linear_integration.project_config["team_configs"][team_id] ||= { + "done_states" => done_states + } + end + + @linear_integration.save! if @linear_integration.changed? + @current_linear_config = @linear_integration.project_config.with_indifferent_access + end + + def parsed_linear_config_params + linear_config_params = params.require(:linear_integration) + .permit( + project_config: { + selected_teams: [], + team_configs: {}, + release_filters: [[:type, :value, :_destroy]] + } + ) + linear_config_params.merge(project_config: parse_linear_config(linear_config_params[:project_config])) + end + + def parse_linear_config(config_params) + return {} if config_params.blank? + + { + selected_teams: Array(config_params[:selected_teams]), + team_configs: config_params[:team_configs]&.transform_values do |team_config| + { + done_states: Array(team_config[:done_states]).compact_blank, + custom_done_states: Array(team_config[:custom_done_states]).compact_blank + } + end || {}, + release_filters: config_params[:release_filters]&.values&.filter_map do |filter| + next if filter[:type].blank? || filter[:value].blank? || filter[:_destroy] == "1" + { + "type" => filter[:type], + "value" => filter[:value] + } + end || [] + } + end +end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 9b10f55fa..3fbc8cb59 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -34,8 +34,8 @@ class AppConfig < ApplicationRecord validates :firebase_android_config, allow_blank: true, json: {message: ->(errors) { errors }, schema: PLATFORM_AWARE_CONFIG_SCHEMA} - validate :jira_release_filters, if: -> { jira_config&.dig("release_filters").present? } - validate :linear_release_filters, if: -> { linear_config&.dig("release_filters").present? } + validate :jira_release_filters, if: -> { jira_config&.dig("release_filters").present? } # TODO: remove + validate :linear_release_filters, if: -> { linear_config&.dig("release_filters").present? } # TODO: remove after_initialize :set_bugsnag_config, if: :persisted? @@ -174,17 +174,17 @@ def project_management_ready? linear = app.integrations.project_management.find(&:linear_integration?)&.providable if jira - return jira_config.present? && - jira_config["selected_projects"].present? && - jira_config["selected_projects"].any? && - jira_config["project_configs"].present? + return jira.project_config.present? && + jira.project_config["selected_projects"].present? && + jira.project_config["selected_projects"].any? && + jira.project_config["project_configs"].present? end if linear - return linear_config.present? && - linear_config["selected_teams"].present? && - linear_config["selected_teams"].any? && - linear_config["team_configs"].present? + return linear.project_config.present? && + linear.project_config["selected_teams"].present? && + linear.project_config["selected_teams"].any? && + linear.project_config["team_configs"].present? end false diff --git a/app/models/jira_integration.rb b/app/models/jira_integration.rb index a757c6b0c..3f5148632 100644 --- a/app/models/jira_integration.rb +++ b/app/models/jira_integration.rb @@ -7,6 +7,7 @@ # oauth_refresh_token :string # organization_name :string # organization_url :string +# project_config :jsonb not null # created_at :datetime not null # updated_at :datetime not null # cloud_id :string indexed @@ -156,10 +157,10 @@ def connection_data end def fetch_tickets_for_release - return [] if app.config.jira_config.blank? + return [] if project_config.blank? - project_key = app.config.jira_config["selected_projects"]&.last - release_filters = app.config.jira_config["release_filters"] + project_key = project_config["selected_projects"]&.last + release_filters = project_config["release_filters"] return [] if project_key.blank? || release_filters.blank? with_api_retries do diff --git a/app/models/linear_integration.rb b/app/models/linear_integration.rb index 442a0614b..9e7e0e0c6 100644 --- a/app/models/linear_integration.rb +++ b/app/models/linear_integration.rb @@ -5,6 +5,7 @@ # id :uuid not null, primary key # oauth_access_token :string # oauth_refresh_token :string +# project_config :jsonb not null # workspace_name :string # workspace_url_key :string # created_at :datetime not null @@ -142,10 +143,10 @@ def connection_data end def fetch_issues_for_release - return [] if app.config.linear_config.blank? + return [] if project_config.blank? - team_id = app.config.linear_config["selected_teams"]&.last - release_filters = app.config.linear_config["release_filters"] + team_id = project_config["selected_teams"]&.last + release_filters = project_config["release_filters"] return [] if team_id.blank? || release_filters.blank? with_api_retries do diff --git a/app/views/jira_integration/_project_selection.html.erb b/app/views/jira_integration/_project_selection.html.erb index b10e37a83..c1aa6e4b7 100644 --- a/app/views/jira_integration/_project_selection.html.erb +++ b/app/views/jira_integration/_project_selection.html.erb @@ -16,7 +16,7 @@ <% project_key = project["key"] %>
- <%= form.fields_for :jira_config do |sf| %> + <%= form.fields_for :project_config do |sf| %> <% checked = current_jira_config &.dig("selected_projects") @@ -70,7 +70,7 @@ <% status_name = "status_#{project_key}_#{status["name"].parameterize}" %>
- <%= form.fields_for :jira_config do |sf| %> + <%= form.fields_for :project_config do |sf| %> <%= sf.fields_for :project_configs do |pf| %> <%= pf.fields_for project_key do |pk| %> <% checked = diff --git a/app/views/jira_integration/_release_filter_form.html.erb b/app/views/jira_integration/_release_filter_form.html.erb index 2d68bbb75..a7fb86edc 100644 --- a/app/views/jira_integration/_release_filter_form.html.erb +++ b/app/views/jira_integration/_release_filter_form.html.erb @@ -1,13 +1,11 @@ <%# locals: (form:, filter: {}, index: 0) %>
- <%= form.fields_for :jira_config do |sf| %> - <%= sf.fields_for :release_filters do |pf| %> - <%= pf.fields_for index.to_s do |rf| %> - <%= rf.select_without_label :type, options_for_select([["Label", "label"], ["Fix Version", "fix_version"]], filter&.dig("type")) %> - <%= rf.text_field_without_label :value, "e.g., release-1.0.0", value: filter&.dig("value") %> - <%= rf.hidden_field :_destroy %> - <% end %> + <%= form.fields_for :project_config do |sf| %> + <%= sf.fields_for :release_filters, index: index do |rf| %> + <%= rf.select_without_label :type, options_for_select([["Label", "label"], ["Fix Version", "fix_version"]], filter&.dig("type")) %> + <%= rf.text_field_without_label :value, "e.g., release-1.0.0", value: filter&.dig("value") %> + <%= rf.hidden_field :_destroy %> <% end %> <% end %> diff --git a/app/views/linear_integration/_release_filter_form.html.erb b/app/views/linear_integration/_release_filter_form.html.erb index a0d36cb54..b5976c20c 100644 --- a/app/views/linear_integration/_release_filter_form.html.erb +++ b/app/views/linear_integration/_release_filter_form.html.erb @@ -1,7 +1,7 @@ <%# locals: (form:, filter: nil, index:) %>
- <%= form.fields_for :linear_config do |sf| %> + <%= form.fields_for :project_config do |sf| %> <%= sf.fields_for :release_filters, index: index do |rf| %> <%= rf.select_without_label :type, options_for_select([["Label", "label"]], filter&.dig("type")) %> <%= rf.text_field_without_label :value, "e.g., release-1.0.0", value: filter&.dig("value") %> diff --git a/app/views/linear_integration/_team_selection.html.erb b/app/views/linear_integration/_team_selection.html.erb index b099199c0..100e6ebaf 100644 --- a/app/views/linear_integration/_team_selection.html.erb +++ b/app/views/linear_integration/_team_selection.html.erb @@ -15,7 +15,7 @@ <% linear_data[:teams].each do |team| %> <% team_id = team["id"] %>
- <%= form.fields_for :linear_config do |sf| %> + <%= form.fields_for :project_config do |sf| %> <% checked = current_linear_config &.dig("selected_teams") @@ -69,7 +69,7 @@ <% state_name = "state_#{team_id}_#{state["name"].parameterize}" %>
- <%= form.fields_for :linear_config do |sf| %> + <%= form.fields_for :project_config do |sf| %> <%= sf.fields_for :team_configs do |tf| %> <%= tf.fields_for team_id do |tk| %> <% checked = diff --git a/app/views/project_management/jira_configs/edit.html+turbo_frame.erb b/app/views/project_management/jira_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..ab2e60805 --- /dev/null +++ b/app/views/project_management/jira_configs/edit.html+turbo_frame.erb @@ -0,0 +1,28 @@ +<%= render EnhancedTurboFrameComponent.new(:project_management_config) do %> + <% if @jira_data && @jira_data[:projects].present? %> + <%= render FormComponent.new(model: @jira_integration, + url: app_project_management_jira_config_path(@app), + method: :patch, + data: {turbo_frame: "_top"}, + builder: EnhancedFormHelper::AuthzForm, + free_form: true) do |f| %> + <%= render CardComponent.new(title: "Select Jira Projects", + subtitle: "Pick projects, add release filters and done states for tracking releases", + separator: false, + size: :full) do %> + <%= render partial: "jira_integration/project_selection", + locals: {form: f.F, jira_data: @jira_data, current_jira_config: @current_jira_config} %> + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :sm %> + <% end %> + <% end %> + <% end %> + <% else %> + <%= render EmptyStateComponent.new( + title: "No Jira projects found", + text: "Please try loading this page again or check your configured projects.", + banner_image: "folder_open.svg", + type: :subdued + ) %> + <% end %> +<% end %> diff --git a/app/views/project_management/linear_configs/edit.html+turbo_frame.erb b/app/views/project_management/linear_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..f8630464b --- /dev/null +++ b/app/views/project_management/linear_configs/edit.html+turbo_frame.erb @@ -0,0 +1,28 @@ +<%= render EnhancedTurboFrameComponent.new(:project_management_config) do %> + <% if @linear_data && @linear_data[:teams].present? %> + <%= render FormComponent.new(model: @linear_integration, + url: app_project_management_linear_config_path(@app), + method: :patch, + data: {turbo_frame: "_top"}, + builder: EnhancedFormHelper::AuthzForm, + free_form: true) do |f| %> + <%= render CardComponent.new(title: "Select Linear Teams", + subtitle: "Pick teams, add release filters and done states for tracking releases", + separator: false, + size: :full) do %> + <%= render partial: "linear_integration/team_selection", + locals: {form: f.F, linear_data: @linear_data, current_linear_config: @current_linear_config} %> + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :sm %> + <% end %> + <% end %> + <% end %> + <% else %> + <%= render EmptyStateComponent.new( + title: "No Linear teams found", + text: "Please try loading this page again or check your configured teams.", + banner_image: "folder_open.svg", + type: :subdued + ) %> + <% end %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index cb265fa57..b12974bd3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -802,3 +802,11 @@ en: bugsnag_configs: update: success: "Monitoring configuration was successfully updated." + + project_management: + jira_configs: + update: + success: "Project management configuration was successfully updated." + linear_configs: + update: + success: "Project management configuration was successfully updated." diff --git a/config/routes.rb b/config/routes.rb index a643c170b..426ead6fc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -87,6 +87,11 @@ resource :bugsnag_config, only: %i[edit update] end + namespace :project_management do + resource :jira_config, only: %i[edit update] + resource :linear_config, only: %i[edit update] + end + member do get :all_builds get :search diff --git a/db/migrate/20250924035033_add_config_to_project_management_integrations.rb b/db/migrate/20250924035033_add_config_to_project_management_integrations.rb new file mode 100644 index 000000000..b12e02aed --- /dev/null +++ b/db/migrate/20250924035033_add_config_to_project_management_integrations.rb @@ -0,0 +1,6 @@ +class AddConfigToProjectManagementIntegrations < ActiveRecord::Migration[7.2] + def change + add_column :jira_integrations, :project_config, :jsonb, default: {}, null: false + add_column :linear_integrations, :project_config, :jsonb, default: {}, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 6dd97f95a..7cc3f22ac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_09_19_124803) do +ActiveRecord::Schema[7.2].define(version: 2025_09_24_035033) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" @@ -435,6 +435,7 @@ t.datetime "updated_at", null: false t.string "organization_name" t.string "organization_url" + t.jsonb "project_config", default: {}, null: false t.index ["cloud_id"], name: "index_jira_integrations_on_cloud_id" end @@ -446,6 +447,7 @@ t.datetime "updated_at", null: false t.string "workspace_name" t.string "workspace_url_key" + t.jsonb "project_config", default: {}, null: false t.index ["workspace_id"], name: "index_linear_integrations_on_workspace_id" end From d0534adfea8a371e19106743044de731c0d1088d Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Thu, 2 Oct 2025 19:28:21 +0530 Subject: [PATCH 09/26] feat: Copy `AppConfig` validations to the respective `Integration`s Removing model validations from `AppConfig` has to be done as part of deprecating the model. --- app/models/app_config.rb | 5 +-- app/models/google_firebase_integration.rb | 8 +++++ app/models/jira_integration.rb | 11 ++++++ app/models/linear_integration.rb | 11 ++++++ spec/models/jira_integration_spec.rb | 42 +++++++++++++++++++++-- spec/models/linear_integration_spec.rb | 34 ++++++++++++++++++ 6 files changed, 106 insertions(+), 5 deletions(-) diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 3fbc8cb59..ee397d30a 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -28,14 +28,15 @@ class AppConfig < ApplicationRecord attr_accessor :bugsnag_ios_release_stage, :bugsnag_android_release_stage, :bugsnag_ios_project_id, :bugsnag_android_project_id + # TODO: migrate validations to the appropriate integrations validates :firebase_ios_config, allow_blank: true, json: {message: ->(errors) { errors }, schema: PLATFORM_AWARE_CONFIG_SCHEMA} validates :firebase_android_config, allow_blank: true, json: {message: ->(errors) { errors }, schema: PLATFORM_AWARE_CONFIG_SCHEMA} - validate :jira_release_filters, if: -> { jira_config&.dig("release_filters").present? } # TODO: remove - validate :linear_release_filters, if: -> { linear_config&.dig("release_filters").present? } # TODO: remove + validate :jira_release_filters, if: -> { jira_config&.dig("release_filters").present? } + validate :linear_release_filters, if: -> { linear_config&.dig("release_filters").present? } after_initialize :set_bugsnag_config, if: :persisted? diff --git a/app/models/google_firebase_integration.rb b/app/models/google_firebase_integration.rb index 86f13f67d..b40673301 100644 --- a/app/models/google_firebase_integration.rb +++ b/app/models/google_firebase_integration.rb @@ -23,7 +23,15 @@ class GoogleFirebaseIntegration < ApplicationRecord encrypts :json_key, deterministic: true + PLATFORM_AWARE_CONFIG_SCHEMA = Rails.root.join("config/schema/platform_aware_integration_config.json") + validate :correct_key, on: :create + validates :ios_config, + allow_blank: true, + json: {message: ->(errors) { errors }, schema: PLATFORM_AWARE_CONFIG_SCHEMA} + validates :android_config, + allow_blank: true, + json: {message: ->(errors) { errors }, schema: PLATFORM_AWARE_CONFIG_SCHEMA} delegate :cache, to: Rails delegate :integrable, to: :integration diff --git a/app/models/jira_integration.rb b/app/models/jira_integration.rb index 3f5148632..2df70c2f4 100644 --- a/app/models/jira_integration.rb +++ b/app/models/jira_integration.rb @@ -62,6 +62,7 @@ class JiraIntegration < ApplicationRecord delegate :app, to: :integration delegate :cache, to: Rails validates :cloud_id, presence: true + validate :release_filters_are_valid, if: -> { project_config&.dig("release_filters").present? } def install_path BASE_INSTALLATION_URL @@ -261,4 +262,14 @@ def fetch_project_statuses(projects) elog("Failed to fetch Jira project statuses for cloud_id #{cloud_id}: #{e}", level: :warn) {} end + + private + + def release_filters_are_valid + project_config["release_filters"].each do |filter| + unless filter.is_a?(Hash) && VALID_FILTER_TYPES.include?(filter["type"]) && filter["value"].present? + errors.add(:project_config, "release filters must contain valid type and value") + end + end + end end diff --git a/app/models/linear_integration.rb b/app/models/linear_integration.rb index 9e7e0e0c6..399ac1626 100644 --- a/app/models/linear_integration.rb +++ b/app/models/linear_integration.rb @@ -60,6 +60,7 @@ class LinearIntegration < ApplicationRecord delegate :app, to: :integration delegate :cache, to: Rails validates :workspace_id, presence: true + validate :release_filters_are_valid, if: -> { project_config&.dig("release_filters").present? } def install_path BASE_INSTALLATION_URL @@ -239,4 +240,14 @@ def fetch_workflow_states elog("Failed to fetch Linear workflow states for workspace_id #{workspace_id}: #{e}", level: :warn) {} end + + private + + def release_filters_are_valid + project_config["release_filters"].each do |filter| + unless filter.is_a?(Hash) && VALID_FILTER_TYPES.include?(filter["type"]) && filter["value"].present? + errors.add(:project_config, "release filters must contain valid type and value") + end + end + end end diff --git a/spec/models/jira_integration_spec.rb b/spec/models/jira_integration_spec.rb index dfc351b72..30a353463 100644 --- a/spec/models/jira_integration_spec.rb +++ b/spec/models/jira_integration_spec.rb @@ -6,6 +6,42 @@ let(:sample_release_label) { "release-1.0" } let(:sample_version) { "v1.0.0" } + describe "validations" do + describe "release filters" do + context "with invalid filter type" do + it "is invalid" do + integration.project_config = { + "release_filters" => [{"type" => "invalid", "value" => "test"}] + } + expect(integration).not_to be_valid + expect(integration.errors[:project_config]).to include("release filters must contain valid type and value") + end + end + + context "with empty filter value" do + it "is invalid" do + integration.project_config = { + "release_filters" => [{"type" => "label", "value" => ""}] + } + expect(integration).not_to be_valid + expect(integration.errors[:project_config]).to include("release filters must contain valid type and value") + end + end + + context "with valid filters" do + it "is valid" do + integration.project_config = { + "release_filters" => [ + {"type" => "label", "value" => sample_release_label}, + {"type" => "fix_version", "value" => sample_version} + ] + } + expect(integration).to be_valid + end + end + end + end + describe "#installation" do it "returns a new API instance with correct credentials" do api = integration.installation @@ -65,7 +101,7 @@ end before do - app.config.update!(jira_config: { + integration.update!(project_config: { "selected_projects" => ["PROJ"], "release_filters" => [{"type" => "label", "value" => sample_release_label}] }) @@ -88,14 +124,14 @@ context "when missing required configuration" do it "returns empty array when no selected projects" do - app.config.update!(jira_config: { + integration.update!(project_config: { "release_filters" => [{"type" => "label", "value" => sample_release_label}] }) expect(integration.fetch_tickets_for_release).to eq([]) end it "returns empty array when no release filters" do - app.config.update!(jira_config: { + integration.update!(project_config: { "selected_projects" => ["PROJ"] }) expect(integration.fetch_tickets_for_release).to eq([]) diff --git a/spec/models/linear_integration_spec.rb b/spec/models/linear_integration_spec.rb index a23267e2c..daf92f39b 100644 --- a/spec/models/linear_integration_spec.rb +++ b/spec/models/linear_integration_spec.rb @@ -9,6 +9,40 @@ expect(linear_integration).not_to be_valid expect(linear_integration.errors[:workspace_id]).to include("can't be blank") end + + describe "release filters" do + context "with invalid filter type" do + it "is invalid" do + linear_integration.project_config = { + "release_filters" => [{"type" => "invalid", "value" => "test"}] + } + expect(linear_integration).not_to be_valid + expect(linear_integration.errors[:project_config]).to include("release filters must contain valid type and value") + end + end + + context "with empty filter value" do + it "is invalid" do + linear_integration.project_config = { + "release_filters" => [{"type" => "label", "value" => ""}] + } + expect(linear_integration).not_to be_valid + expect(linear_integration.errors[:project_config]).to include("release filters must contain valid type and value") + end + end + + context "with valid filters" do + it "is valid" do + linear_integration.project_config = { + "release_filters" => [ + {"type" => "label", "value" => "release-1.0"}, + {"type" => "state", "value" => "v1.0.0"} + ] + } + expect(linear_integration).to be_valid + end + end + end end describe "#install_path" do From 1ba11ce49ae762aecd7d32f2acc1eb80d614c889 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 12:24:39 +0530 Subject: [PATCH 10/26] feat: Move `AppConfig#ready?` to `Integration` --- app/models/app.rb | 6 +- app/models/app_config.rb | 49 ---------------- app/models/integration.rb | 118 +++++++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 54 deletions(-) diff --git a/app/models/app.rb b/app/models/app.rb index 0766ef15e..7605a01c4 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -126,9 +126,7 @@ def project_management_connected? integrations.project_management.connected.any? end - def ready? - integrations.ready? and config&.ready? - end + delegate :ready?, to: :integrations def guided_train_setup? trains.none? || train_in_creation.present? @@ -193,7 +191,7 @@ def app_setup_instructions } } - config.further_setup_by_category?.each do |category, status_map| + integrations.further_setup_by_category.each do |category, status_map| app_config_setup[:app_config][:integrations][category] = { visible: true, completed: status_map[:ready] } diff --git a/app/models/app_config.rb b/app/models/app_config.rb index ee397d30a..89fd9e44a 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,13 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - def ready? - further_setup_by_category? - .values - .pluck(:ready) - .all? - end - def code_repository_name code_repository&.fetch("full_name", nil) end @@ -67,48 +60,6 @@ def bitrise_project app.ci_cd_provider&.project_config&.fetch("id", nil) end - def further_setup_by_category? - integrations = app.integrations.connected - categories = {}.with_indifferent_access - - if integrations.version_control.present? - categories[:version_control] = { - further_setup: integrations.version_control.any?(&:further_setup?), - ready: code_repository.present? - } - end - - if integrations.ci_cd.present? - categories[:ci_cd] = { - further_setup: integrations.ci_cd.any?(&:further_setup?), - ready: bitrise_ready? - } - end - - if integrations.build_channel.present? - categories[:build_channel] = { - further_setup: integrations.build_channel.map(&:providable).any?(&:further_setup?), - ready: firebase_ready? - } - end - - if integrations.monitoring.present? - categories[:monitoring] = { - further_setup: integrations.monitoring.any?(&:further_setup?), - ready: bugsnag_ready? - } - end - - if integrations.project_management.present? - categories[:project_management] = { - further_setup: integrations.project_management.map(&:providable).any?(&:further_setup?), - ready: project_management_ready? - } - end - - categories - end - def bugsnag_project(platform) app.monitoring_provider.project(platform) end diff --git a/app/models/integration.rb b/app/models/integration.rb index c42b8aa09..028928ddb 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -158,7 +158,7 @@ def category_ready?(category) end def ready? - MINIMUM_REQUIRED_SET.all? { |category| category_ready?(category) } + minimum_required_set_ready? && integration_configs_ready? end def slack_notifications? @@ -211,11 +211,127 @@ def build_channels_for_platform(platform) kept.build_channel.filter { |b| ALLOWED_INTEGRATIONS_FOR_APP[platform.to_sym][:build_channel].include?(b.providable_type) } end + def further_setup_by_category + connected_integrations = connected + categories = {}.with_indifferent_access + + if connected_integrations.version_control.present? + categories[:version_control] = { + further_setup: connected_integrations.version_control.any?(&:further_setup?), + ready: code_repository.present? + } + end + + if connected_integrations.ci_cd.present? + categories[:ci_cd] = { + further_setup: connected_integrations.ci_cd.any?(&:further_setup?), + ready: bitrise_ready? + } + end + + if connected_integrations.build_channel.present? + categories[:build_channel] = { + further_setup: connected_integrations.build_channel.map(&:providable).any?(&:further_setup?), + ready: firebase_ready? + } + end + + if connected_integrations.monitoring.present? + categories[:monitoring] = { + further_setup: connected_integrations.monitoring.any?(&:further_setup?), + ready: bugsnag_ready? + } + end + + if connected_integrations.project_management.present? + categories[:project_management] = { + further_setup: connected_integrations.project_management.map(&:providable).any?(&:further_setup?), + ready: project_management_ready? + } + end + + categories + end + private + def minimum_required_set_ready? + MINIMUM_REQUIRED_SET.all? { |category| category_ready?(category) } + end + + # Configuration readiness checks (migrated from AppConfig) + def integration_configs_ready? + return false if none? # need at least one integration + + further_setup_by_category + .values + .pluck(:ready) + .all? + end + def providable_error_message(meta) meta[:value].errors.full_messages[0] end + + def code_repository + vcs_provider&.repository_config + end + + def bitrise_ready? + app = first&.integrable + return true unless app&.bitrise_connected? + + bitrise_project.present? + end + + def bitrise_project + ci_cd_provider&.project_config&.fetch("id", nil) + end + + def firebase_ready? + app = first&.integrable + return true unless app&.firebase_connected? + + firebase_build_channel = firebase_build_channel_provider + configs_ready?(app, firebase_build_channel&.android_config, firebase_build_channel&.ios_config) + end + + def bugsnag_ready? + app = first&.integrable + return true unless app&.bugsnag_connected? + + monitoring = monitoring_provider + configs_ready?(app, monitoring&.android_config, monitoring&.ios_config) + end + + def project_management_ready? + return false if project_management.blank? + + jira = project_management.find(&:jira_integration?)&.providable + linear = project_management.find(&:linear_integration?)&.providable + + if jira + return jira.project_config.present? && + jira.project_config["selected_projects"].present? && + jira.project_config["selected_projects"].any? && + jira.project_config["project_configs"].present? + end + + if linear + return linear.project_config.present? && + linear.project_config["selected_teams"].present? && + linear.project_config["selected_teams"].any? && + linear.project_config["team_configs"].present? + end + + false + end + + def configs_ready?(app, android, ios) + return ios.present? if app&.ios? + return android.present? if app&.android? + ios.present? && android.present? if app&.cross_platform? + end end def disconnectable? From a96cd2b413a917dfa29003ef7a9a558229295a01 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 12:51:08 +0530 Subject: [PATCH 11/26] feat: Move `AppConfig#code_repository_name` to `vcs_provider` Integrations TODO: fix failing specs. --- .../coordinators/webhooks/pull_request.rb | 2 +- app/libs/coordinators/webhooks/push.rb | 2 +- app/models/app_config.rb | 4 --- app/models/bitbucket_integration.rb | 9 +++-- app/models/github_integration.rb | 6 +++- app/models/gitlab_integration.rb | 8 +++-- spec/libs/triggers/pull_request_spec.rb | 2 +- spec/models/gitlab_integration_spec.rb | 33 ++++++++----------- 8 files changed, 32 insertions(+), 34 deletions(-) diff --git a/app/libs/coordinators/webhooks/pull_request.rb b/app/libs/coordinators/webhooks/pull_request.rb index 048eeafcb..d77f0bcb4 100644 --- a/app/libs/coordinators/webhooks/pull_request.rb +++ b/app/libs/coordinators/webhooks/pull_request.rb @@ -28,6 +28,6 @@ def process end def valid_repo? - (train.app.config&.code_repository_name == repository_name) + (train.app.vcs_provider&.code_repository_name == repository_name) end end diff --git a/app/libs/coordinators/webhooks/push.rb b/app/libs/coordinators/webhooks/push.rb index 3599ce835..1cf2612c9 100644 --- a/app/libs/coordinators/webhooks/push.rb +++ b/app/libs/coordinators/webhooks/push.rb @@ -25,6 +25,6 @@ def relevant_commit? end def valid_repo_and_branch? - (train.app.config&.code_repository_name == repository_name) if branch_name + (train.app.vcs_provider&.code_repository_name == repository_name) if branch_name end end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 89fd9e44a..9195d09c2 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,10 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - def code_repository_name - code_repository&.fetch("full_name", nil) - end - def code_repo_url code_repository&.fetch("repo_url", nil) end diff --git a/app/models/bitbucket_integration.rb b/app/models/bitbucket_integration.rb index e658d9a5f..f88b84204 100644 --- a/app/models/bitbucket_integration.rb +++ b/app/models/bitbucket_integration.rb @@ -28,9 +28,12 @@ class BitbucketIntegration < ApplicationRecord attr_accessor :code before_create :complete_access delegate :integrable, to: :integration - delegate :code_repository_name, to: :app_config delegate :cache, to: Rails + def code_repository_name + repository_config&.fetch("full_name", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { @@ -382,10 +385,6 @@ def set_tokens(tokens) assign_attributes(oauth_access_token: tokens.access_token, oauth_refresh_token: tokens.refresh_token) if tokens end - def app_config - integrable.config - end - def redirect_uri bitbucket_callback_url(link_params) end diff --git a/app/models/github_integration.rb b/app/models/github_integration.rb index 48a89741e..9eb1d1416 100644 --- a/app/models/github_integration.rb +++ b/app/models/github_integration.rb @@ -19,7 +19,7 @@ class GithubIntegration < ApplicationRecord validates :installation_id, presence: true - delegate :code_repository_name, :code_repo_namespace, :code_repo_name_only, to: :app_config + delegate :code_repo_namespace, :code_repo_name_only, to: :app_config delegate :integrable, to: :integration delegate :organization, to: :integrable delegate :cache, to: Rails @@ -111,6 +111,10 @@ class GithubIntegration < ApplicationRecord generated_at: :created_at } + def code_repository_name + repository_config&.fetch("full_name", nil) + end + def install_path BASE_INSTALLATION_URL .expand(app_name: creds.integrations.github.app_name, params: { diff --git a/app/models/gitlab_integration.rb b/app/models/gitlab_integration.rb index c7a7abf5d..c7a5c4ebf 100644 --- a/app/models/gitlab_integration.rb +++ b/app/models/gitlab_integration.rb @@ -25,7 +25,7 @@ class GitlabIntegration < ApplicationRecord before_validation :complete_access, if: :new_record? delegate :integrable, to: :integration delegate :organization, to: :integrable - delegate :code_repository_name, :code_repo_namespace, to: :app_config + delegate :code_repo_namespace, to: :app_config delegate :cache, to: Rails validate :correct_key, on: :create @@ -157,6 +157,10 @@ class GitlabIntegration < ApplicationRecord generated_at: :created_at } + def code_repository_name + repository_config&.fetch("full_name", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { @@ -347,7 +351,7 @@ def connection_data end def get_commit(sha) - with_api_retries { installation.get_commit(app_config.code_repository["id"], sha, COMMITS_TRANSFORMATIONS) } + with_api_retries { installation.get_commit(repository_config["id"], sha, COMMITS_TRANSFORMATIONS) } end def create_pr!(to_branch_ref, from_branch_ref, title, description) diff --git a/spec/libs/triggers/pull_request_spec.rb b/spec/libs/triggers/pull_request_spec.rb index 0bd5ccc7c..e5a89d90b 100644 --- a/spec/libs/triggers/pull_request_spec.rb +++ b/spec/libs/triggers/pull_request_spec.rb @@ -9,7 +9,7 @@ let(:pr_title) { Faker::Lorem.word } let(:pr_description) { Faker::Lorem.word } let(:repo_integration) { instance_double(Installations::Github::Api) } - let(:repo_name) { app.config.code_repository_name } + let(:repo_name) { app.vcs_provider.code_repository_name } let(:no_diff_error) { Installations::Error.new("Should not create a Pull Request without a diff", reason: :pull_request_without_commits) } before do diff --git a/spec/models/gitlab_integration_spec.rb b/spec/models/gitlab_integration_spec.rb index cd25ebf23..19490aecc 100644 --- a/spec/models/gitlab_integration_spec.rb +++ b/spec/models/gitlab_integration_spec.rb @@ -3,19 +3,14 @@ describe GitlabIntegration do let(:app) { create(:app, :android) } let(:integration) { create(:integration, integrable: app) } - let(:gitlab_integration) { create(:gitlab_integration, :without_callbacks_and_validations, integration: integration) } - let(:app_config) { app.config } + let(:gitlab_integration) { create(:gitlab_integration, :without_callbacks_and_validations, integration:) } let(:installation) { instance_double(Installations::Gitlab::Api) } - before do - allow(gitlab_integration).to receive_messages(app_config: app_config, installation: installation) - end - describe "#create_release!" do it "calls the GitLab API to create a release" do allow(installation).to receive(:create_release!).and_return({"tag_name" => "v1.0.0"}) result = gitlab_integration.create_release!("v1.0.0", "main", anything, "Release notes") - expect(installation).to have_received(:create_release!).with(app_config.code_repository_name, "v1.0.0", "main", "Release notes") + expect(installation).to have_received(:create_release!).with(gitlab_integration.code_repository_name, "v1.0.0", "main", "Release notes") expect(result).to eq({"tag_name" => "v1.0.0"}) end @@ -31,7 +26,7 @@ it "calls the GitLab API to get file content" do allow(installation).to receive(:get_file_content).and_return("file content") result = gitlab_integration.get_file_content("main", "path/to/file.txt") - expect(installation).to have_received(:get_file_content).with(app_config.code_repository_name, "main", "path/to/file.txt") + expect(installation).to have_received(:get_file_content).with(gitlab_integration.code_repository_name, "main", "path/to/file.txt") expect(result).to eq("file content") end @@ -55,7 +50,7 @@ it "calls the GitLab API to update a file" do allow(installation).to receive(:update_file!).and_return(true) result = gitlab_integration.update_file!("main", "path/to/file.txt", "new content", "commit message") - expect(installation).to have_received(:update_file!).with(app_config.code_repository_name, "main", "path/to/file.txt", "new content", "commit message", author_name: nil, author_email: nil) + expect(installation).to have_received(:update_file!).with(gitlab_integration.code_repository_name, "main", "path/to/file.txt", "new content", "commit message", author_name: nil, author_email: nil) expect(result).to be true end @@ -63,7 +58,7 @@ it "passes author details to update_file!" do allow(installation).to receive(:update_file!).and_return(true) gitlab_integration.update_file!("main", "path/to/file.txt", "new content", "commit message", author_name: "Test User", author_email: "test@example.com") - expect(installation).to have_received(:update_file!).with(app_config.code_repository_name, "main", "path/to/file.txt", "new content", "commit message", author_name: "Test User", author_email: "test@example.com") + expect(installation).to have_received(:update_file!).with(gitlab_integration.code_repository_name, "main", "path/to/file.txt", "new content", "commit message", author_name: "Test User", author_email: "test@example.com") end end @@ -83,14 +78,14 @@ trigger_job!: nil ) gitlab_integration.trigger_workflow_run!("ci_cd_channel", "main", {key: "value"}) - expect(installation).to have_received(:run_pipeline_with_job!).with(app_config.code_repository_name, "main", {key: "value"}, "ci_cd_channel", nil, GitlabIntegration::WORKFLOW_RUN_TRANSFORMATIONS) + expect(installation).to have_received(:run_pipeline_with_job!).with(gitlab_integration.code_repository_name, "main", {key: "value"}, "ci_cd_channel", nil, GitlabIntegration::WORKFLOW_RUN_TRANSFORMATIONS) end context "with commit SHA" do it "passes commit SHA to run_pipeline_with_job!" do allow(installation).to receive(:run_pipeline_with_job!).and_return({ci_ref: "123", ci_link: "http://example.com"}) gitlab_integration.trigger_workflow_run!("ci_cd_channel", "main", {key: "value"}, "abc123") - expect(installation).to have_received(:run_pipeline_with_job!).with(app_config.code_repository_name, "main", {key: "value"}, "ci_cd_channel", "abc123", GitlabIntegration::WORKFLOW_RUN_TRANSFORMATIONS) + expect(installation).to have_received(:run_pipeline_with_job!).with(gitlab_integration.code_repository_name, "main", {key: "value"}, "ci_cd_channel", "abc123", GitlabIntegration::WORKFLOW_RUN_TRANSFORMATIONS) end end @@ -106,7 +101,7 @@ it "calls the GitLab API to cancel a pipeline" do allow(installation).to receive(:cancel_job!) gitlab_integration.cancel_workflow_run!("123") - expect(installation).to have_received(:cancel_job!).with(app_config.code_repository_name, "123") + expect(installation).to have_received(:cancel_job!).with(gitlab_integration.code_repository_name, "123") end end @@ -114,7 +109,7 @@ it "calls the GitLab API to retry a pipeline" do allow(installation).to receive(:retry_job!) gitlab_integration.retry_workflow_run!("123") - expect(installation).to have_received(:retry_job!).with(app_config.code_repository_name, "123", GitlabIntegration::JOB_RUN_TRANSFORMATIONS) + expect(installation).to have_received(:retry_job!).with(gitlab_integration.code_repository_name, "123", GitlabIntegration::JOB_RUN_TRANSFORMATIONS) end end @@ -122,7 +117,7 @@ it "calls the GitLab API to get a pipeline" do allow(installation).to receive(:get_job) gitlab_integration.get_workflow_run("123") - expect(installation).to have_received(:get_job).with(app_config.code_repository_name, "123") + expect(installation).to have_received(:get_job).with(gitlab_integration.code_repository_name, "123") end end @@ -130,7 +125,7 @@ it "calls the GitLab API to create a tag" do allow(installation).to receive(:create_tag!).and_return({"name" => "v1.0.0"}) result = gitlab_integration.create_tag!("v1.0.0", "abcdef") - expect(installation).to have_received(:create_tag!).with(app_config.code_repository_name, "v1.0.0", "abcdef") + expect(installation).to have_received(:create_tag!).with(gitlab_integration.code_repository_name, "v1.0.0", "abcdef") expect(result).to eq({"name" => "v1.0.0"}) end @@ -153,14 +148,14 @@ it "calls the GitLab API to cherry pick a commit" do allow(installation).to receive(:cherry_pick_pr).and_return({}) gitlab_integration.create_patch_pr!("main", "patch-branch", "abcdef", "PR Title") - expect(installation).to have_received(:cherry_pick_pr).with(app_config.code_repository_name, "main", "abcdef", "patch-branch", "PR Title", "", GitlabIntegration::PR_TRANSFORMATIONS) + expect(installation).to have_received(:cherry_pick_pr).with(gitlab_integration.code_repository_name, "main", "abcdef", "patch-branch", "PR Title", "", GitlabIntegration::PR_TRANSFORMATIONS) end context "with custom description" do it "passes description to cherry_pick_pr" do allow(installation).to receive(:cherry_pick_pr).and_return({}) gitlab_integration.create_patch_pr!("main", "patch-branch", "abcdef", "PR Title", "Custom description") - expect(installation).to have_received(:cherry_pick_pr).with(app_config.code_repository_name, "main", "abcdef", "patch-branch", "PR Title", "Custom description", GitlabIntegration::PR_TRANSFORMATIONS) + expect(installation).to have_received(:cherry_pick_pr).with(gitlab_integration.code_repository_name, "main", "abcdef", "patch-branch", "PR Title", "Custom description", GitlabIntegration::PR_TRANSFORMATIONS) end end @@ -176,7 +171,7 @@ it "calls the GitLab API to enable auto merge" do allow(installation).to receive(:enable_auto_merge).and_return(true) result = gitlab_integration.enable_auto_merge!(123) - expect(installation).to have_received(:enable_auto_merge).with(app_config.code_repository_name, 123) + expect(installation).to have_received(:enable_auto_merge).with(gitlab_integration.code_repository_name, 123) expect(result).to be true end From cf8de8471c6270e4181c728aaaf4f6d92e918d32 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 12:55:26 +0530 Subject: [PATCH 12/26] feat: Move `AppConfig#code_repository_url` to `vcs_provider` Integrations --- app/components/live_release/finalize_component.rb | 2 +- app/models/app_config.rb | 4 ---- app/models/bitbucket_integration.rb | 4 ++++ app/models/github_integration.rb | 4 ++++ app/models/gitlab_integration.rb | 4 ++++ 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/components/live_release/finalize_component.rb b/app/components/live_release/finalize_component.rb index cd9154e78..7f81cbdbf 100644 --- a/app/components/live_release/finalize_component.rb +++ b/app/components/live_release/finalize_component.rb @@ -72,7 +72,7 @@ def subtitle end def tag_link - link = release.tag_url || release.app.config&.code_repo_url + link = release.tag_url || release.app.vcs_provider&.code_repo_url return NOT_AVAILABLE if link.blank? link_to_external train.vcs_provider.display, link, class: "underline" end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 9195d09c2..4578957f8 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,10 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - def code_repo_url - code_repository&.fetch("repo_url", nil) - end - def code_repo_namespace code_repository&.fetch("namespace", nil) end diff --git a/app/models/bitbucket_integration.rb b/app/models/bitbucket_integration.rb index f88b84204..4bf736c2d 100644 --- a/app/models/bitbucket_integration.rb +++ b/app/models/bitbucket_integration.rb @@ -34,6 +34,10 @@ def code_repository_name repository_config&.fetch("full_name", nil) end + def code_repo_url + repository_config&.fetch("repo_url", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { diff --git a/app/models/github_integration.rb b/app/models/github_integration.rb index 9eb1d1416..7abd1b633 100644 --- a/app/models/github_integration.rb +++ b/app/models/github_integration.rb @@ -115,6 +115,10 @@ def code_repository_name repository_config&.fetch("full_name", nil) end + def code_repo_url + repository_config&.fetch("repo_url", nil) + end + def install_path BASE_INSTALLATION_URL .expand(app_name: creds.integrations.github.app_name, params: { diff --git a/app/models/gitlab_integration.rb b/app/models/gitlab_integration.rb index c7a5c4ebf..4b23c7a2d 100644 --- a/app/models/gitlab_integration.rb +++ b/app/models/gitlab_integration.rb @@ -161,6 +161,10 @@ def code_repository_name repository_config&.fetch("full_name", nil) end + def code_repo_url + repository_config&.fetch("repo_url", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { From 397e9d7fdb435641401fbb195487fcd09c2bb89c Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 13:03:36 +0530 Subject: [PATCH 13/26] feat: Move `AppConfig#code_repo_namespace` to `vcs_provider` Integrations --- app/models/app_config.rb | 4 ---- app/models/bitbucket_integration.rb | 4 ++++ app/models/github_integration.rb | 6 +++++- app/models/gitlab_integration.rb | 9 ++++----- spec/libs/triggers/pull_request_spec.rb | 8 ++++---- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 4578957f8..a00524996 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,10 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - def code_repo_namespace - code_repository&.fetch("namespace", nil) - end - def code_repo_name_only code_repository&.fetch("name", nil) end diff --git a/app/models/bitbucket_integration.rb b/app/models/bitbucket_integration.rb index 4bf736c2d..b2a812799 100644 --- a/app/models/bitbucket_integration.rb +++ b/app/models/bitbucket_integration.rb @@ -38,6 +38,10 @@ def code_repo_url repository_config&.fetch("repo_url", nil) end + def code_repo_namespace + repository_config&.fetch("namespace", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { diff --git a/app/models/github_integration.rb b/app/models/github_integration.rb index 7abd1b633..59f5d119e 100644 --- a/app/models/github_integration.rb +++ b/app/models/github_integration.rb @@ -19,7 +19,7 @@ class GithubIntegration < ApplicationRecord validates :installation_id, presence: true - delegate :code_repo_namespace, :code_repo_name_only, to: :app_config + delegate :code_repo_name_only, to: :app_config delegate :integrable, to: :integration delegate :organization, to: :integrable delegate :cache, to: Rails @@ -119,6 +119,10 @@ def code_repo_url repository_config&.fetch("repo_url", nil) end + def code_repo_namespace + repository_config&.fetch("namespace", nil) + end + def install_path BASE_INSTALLATION_URL .expand(app_name: creds.integrations.github.app_name, params: { diff --git a/app/models/gitlab_integration.rb b/app/models/gitlab_integration.rb index 4b23c7a2d..ec9653e42 100644 --- a/app/models/gitlab_integration.rb +++ b/app/models/gitlab_integration.rb @@ -25,7 +25,6 @@ class GitlabIntegration < ApplicationRecord before_validation :complete_access, if: :new_record? delegate :integrable, to: :integration delegate :organization, to: :integrable - delegate :code_repo_namespace, to: :app_config delegate :cache, to: Rails validate :correct_key, on: :create @@ -165,6 +164,10 @@ def code_repo_url repository_config&.fetch("repo_url", nil) end + def code_repo_namespace + repository_config&.fetch("namespace", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { @@ -484,10 +487,6 @@ def reset_tokens! reload end - def app_config - integrable.config - end - def redirect_uri gitlab_callback_url(link_params) end diff --git a/spec/libs/triggers/pull_request_spec.rb b/spec/libs/triggers/pull_request_spec.rb index e5a89d90b..4e1ba0861 100644 --- a/spec/libs/triggers/pull_request_spec.rb +++ b/spec/libs/triggers/pull_request_spec.rb @@ -31,7 +31,7 @@ title: pr_title, description: pr_description ) - namespaced_release_branch = "#{release.train.app.config.code_repo_namespace}:#{release_branch}" + namespaced_release_branch = "#{release.train.app.vcs_provider.code_repo_namespace}:#{release_branch}" expect(repo_integration).to have_received(:create_pr!).with(repo_name, working_branch, namespaced_release_branch, pr_title, pr_description, GithubIntegration::PR_TRANSFORMATIONS) expect(result.ok?).to be(true) @@ -96,7 +96,7 @@ expect(repo_integration).to have_received(:cherry_pick_pr).with(repo_name, working_branch, commit.commit_hash, patch_branch, pr_title, pr_description, GithubIntegration::PR_TRANSFORMATIONS) expect(repo_integration).to have_received(:merge_pr!).with(repo_name, created_pr.number, GithubIntegration::PR_TRANSFORMATIONS) - expect(repo_integration).to have_received(:enable_auto_merge).with(app.config.code_repo_namespace, app.config.code_repo_name_only, created_pr.number) + expect(repo_integration).to have_received(:enable_auto_merge).with(app.vcs_provider.code_repo_namespace, app.vcs_provider.code_repo_name_only, created_pr.number) expect(result.ok?).to be(true) expect(created_pr.closed?).to be(false) end @@ -113,7 +113,7 @@ description: pr_description, allow_without_diff: true ) - namespaced_release_branch = "#{release.train.app.config.code_repo_namespace}:#{release_branch}" + namespaced_release_branch = "#{release.train.app.vcs_provider.code_repo_namespace}:#{release_branch}" expect(repo_integration).to have_received(:create_pr!).with(repo_name, working_branch, namespaced_release_branch, pr_title, pr_description, GithubIntegration::PR_TRANSFORMATIONS) expect(result.ok?).to be(true) @@ -132,7 +132,7 @@ description: pr_description, allow_without_diff: false ) - namespaced_release_branch = "#{release.train.app.config.code_repo_namespace}:#{release_branch}" + namespaced_release_branch = "#{release.train.app.vcs_provider.code_repo_namespace}:#{release_branch}" expect(repo_integration).to have_received(:create_pr!).with(repo_name, working_branch, namespaced_release_branch, pr_title, pr_description, GithubIntegration::PR_TRANSFORMATIONS) expect(result.ok?).to be(false) From 08adced649983d69bf3826855f7512e24ee2b93e Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 13:09:10 +0530 Subject: [PATCH 14/26] feat: Move `AppConfig#code_repo_name_only` to `vcs_provider` Integrations --- app/models/app_config.rb | 4 ---- app/models/bitbucket_integration.rb | 4 ++++ app/models/github_integration.rb | 9 ++++----- app/models/gitlab_integration.rb | 4 ++++ 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/models/app_config.rb b/app/models/app_config.rb index a00524996..b2588579a 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,10 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - def code_repo_name_only - code_repository&.fetch("name", nil) - end - def bitrise_project app.ci_cd_provider&.project_config&.fetch("id", nil) end diff --git a/app/models/bitbucket_integration.rb b/app/models/bitbucket_integration.rb index b2a812799..924553675 100644 --- a/app/models/bitbucket_integration.rb +++ b/app/models/bitbucket_integration.rb @@ -42,6 +42,10 @@ def code_repo_namespace repository_config&.fetch("namespace", nil) end + def code_repo_name_only + repository_config&.fetch("name", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { diff --git a/app/models/github_integration.rb b/app/models/github_integration.rb index 59f5d119e..72cab2c77 100644 --- a/app/models/github_integration.rb +++ b/app/models/github_integration.rb @@ -19,7 +19,6 @@ class GithubIntegration < ApplicationRecord validates :installation_id, presence: true - delegate :code_repo_name_only, to: :app_config delegate :integrable, to: :integration delegate :organization, to: :integrable delegate :cache, to: Rails @@ -123,6 +122,10 @@ def code_repo_namespace repository_config&.fetch("namespace", nil) end + def code_repo_name_only + repository_config&.fetch("name", nil) + end + def install_path BASE_INSTALLATION_URL .expand(app_name: creds.integrations.github.app_name, params: { @@ -388,10 +391,6 @@ def update_webhook!(id, url_params) installation.update_repo_webhook!(code_repository_name, id, events_url(url_params), WEBHOOK_TRANSFORMATIONS) end - def app_config - integrable.config - end - def events_url(params) if Rails.env.development? github_events_url(host: ENV["WEBHOOK_HOST_NAME"], **params) diff --git a/app/models/gitlab_integration.rb b/app/models/gitlab_integration.rb index ec9653e42..80567a0da 100644 --- a/app/models/gitlab_integration.rb +++ b/app/models/gitlab_integration.rb @@ -168,6 +168,10 @@ def code_repo_namespace repository_config&.fetch("namespace", nil) end + def code_repo_name_only + repository_config&.fetch("name", nil) + end + def install_path BASE_INSTALLATION_URL .expand(params: { From 9d9d8dd044820398c36fb8722fc5e656dc9a5c43 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 13:31:29 +0530 Subject: [PATCH 15/26] feat: Move `AppConfig#bitrise_project` to `BitriseIntegration` --- app/models/app_config.rb | 10 +--------- app/models/bitrise_integration.rb | 7 +++++-- app/models/integration.rb | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/models/app_config.rb b/app/models/app_config.rb index b2588579a..12f7ec366 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,10 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - def bitrise_project - app.ci_cd_provider&.project_config&.fetch("id", nil) - end - def bugsnag_project(platform) app.monitoring_provider.project(platform) end @@ -66,10 +62,6 @@ def disconnect!(integration) save! end - def code_repository - app.vcs_provider&.repository_config - end - private def set_bugsnag_config @@ -88,7 +80,7 @@ def firebase_ready? def bitrise_ready? return true unless app.bitrise_connected? - bitrise_project.present? + app.ci_cd_provider&.project_config&.fetch("id", nil).present? end def bugsnag_ready? diff --git a/app/models/bitrise_integration.rb b/app/models/bitrise_integration.rb index 9ac51a8b4..4796e3805 100644 --- a/app/models/bitrise_integration.rb +++ b/app/models/bitrise_integration.rb @@ -59,10 +59,13 @@ class BitriseIntegration < ApplicationRecord encrypts :access_token, deterministic: true delegate :integrable, to: :integration - delegate :bitrise_project, to: :app_config - alias_method :project, :bitrise_project delegate :cache, to: Rails + def bitrise_project + project_config&.fetch("id", nil) + end + alias_method :project, :bitrise_project + def installation API.new(access_token) end diff --git a/app/models/integration.rb b/app/models/integration.rb index 028928ddb..4a98de8cc 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -285,7 +285,7 @@ def bitrise_ready? end def bitrise_project - ci_cd_provider&.project_config&.fetch("id", nil) + ci_cd.find(&:bitrise_integration?)&.providable&.bitrise_project end def firebase_ready? From 5ba3372f70b69ca86956687915ba785f12d6a4ad Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 17:51:03 +0530 Subject: [PATCH 16/26] chore: Fix failing specs for `GitlabIntegration` --- spec/models/gitlab_integration_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/models/gitlab_integration_spec.rb b/spec/models/gitlab_integration_spec.rb index 19490aecc..7c3b16ac9 100644 --- a/spec/models/gitlab_integration_spec.rb +++ b/spec/models/gitlab_integration_spec.rb @@ -6,6 +6,10 @@ let(:gitlab_integration) { create(:gitlab_integration, :without_callbacks_and_validations, integration:) } let(:installation) { instance_double(Installations::Gitlab::Api) } + before do + allow(gitlab_integration).to receive_messages(installation:) + end + describe "#create_release!" do it "calls the GitLab API to create a release" do allow(installation).to receive(:create_release!).and_return({"tag_name" => "v1.0.0"}) From 7d32eb4896df3f9b406cb92ca7e249ab6aa7d5c4 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 18:39:38 +0530 Subject: [PATCH 17/26] chore: Attempt to fix the remaining failing specs --- spec/factories/apps.rb | 2 +- spec/factories/bitbucket_integrations.rb | 1 + spec/factories/github_integrations.rb | 1 + spec/factories/gitlab_integrations.rb | 1 + spec/factories/integrations.rb | 2 +- spec/factories/jira_integrations.rb | 5 ++--- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spec/factories/apps.rb b/spec/factories/apps.rb index ee1f6c1af..c99a1eccd 100644 --- a/spec/factories/apps.rb +++ b/spec/factories/apps.rb @@ -7,7 +7,7 @@ build_number { Faker::Number.number(digits: 4) } after(:build) do |app| - app.config = build(:app_config, app: app) + app.config = build(:app_config, app: app) # TODO: try removing this end trait :android do diff --git a/spec/factories/bitbucket_integrations.rb b/spec/factories/bitbucket_integrations.rb index bdde0d198..e58caa630 100644 --- a/spec/factories/bitbucket_integrations.rb +++ b/spec/factories/bitbucket_integrations.rb @@ -1,6 +1,7 @@ FactoryBot.define do factory :bitbucket_integration do oauth_access_token { Faker::Lorem.sentence } + repository_config { {id: 123, full_name: "tramline/repo", namespace: "tramline"} } trait :without_callbacks_and_validations do to_create { |instance| instance.save(validate: false) } diff --git a/spec/factories/github_integrations.rb b/spec/factories/github_integrations.rb index 546e3cbe7..d627dff09 100644 --- a/spec/factories/github_integrations.rb +++ b/spec/factories/github_integrations.rb @@ -1,5 +1,6 @@ FactoryBot.define do factory :github_integration do installation_id { 1 } + repository_config { {id: 123, full_name: "tramline/repo", namespace: "tramline"} } end end diff --git a/spec/factories/gitlab_integrations.rb b/spec/factories/gitlab_integrations.rb index ce44ad37a..5ac70fcaf 100644 --- a/spec/factories/gitlab_integrations.rb +++ b/spec/factories/gitlab_integrations.rb @@ -2,6 +2,7 @@ factory :gitlab_integration do oauth_access_token { Faker::Lorem.sentence } oauth_refresh_token { Faker::Lorem.sentence } + repository_config { {id: 123, full_name: "tramline/repo", namespace: "tramline"} } trait :without_callbacks_and_validations do to_create { |instance| instance.save(validate: false) } after(:build) do |integration| diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index e1849a1d9..1d9ede5de 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -49,7 +49,7 @@ trait :with_jira do category { "project_management" } - providable factory: %i[jira_integration with_app_config] + providable factory: %i[jira_integration with_config] end trait :with_crashlytics do diff --git a/spec/factories/jira_integrations.rb b/spec/factories/jira_integrations.rb index 21969a481..44a1beb94 100644 --- a/spec/factories/jira_integrations.rb +++ b/spec/factories/jira_integrations.rb @@ -7,10 +7,9 @@ organization_url { "https://testorg.atlassian.net" } integration - trait :with_app_config do + trait :with_config do after(:create) do |jira_integration| - app = jira_integration.integration.integrable - app.config.update!(jira_config: { + jira_integration.update!(project_config: { "release_filters" => [ {"type" => "label", "value" => "release-1.0"}, {"type" => "fix_version", "value" => "v1.0.0"} From d019bd2dcd00d9606ed489a047ad2238338038bd Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 23:24:33 +0530 Subject: [PATCH 18/26] feat: Remove migrated `bugsnag_*` public methods from `AppConfig` --- app/models/app_config.rb | 8 -------- spec/factories/apps.rb | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 12f7ec366..98035e510 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,14 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - def bugsnag_project(platform) - app.monitoring_provider.project(platform) - end - - def bugsnag_release_stage(platform) - app.monitoring_provider.release_stage(platform) - end - def ci_cd_workflows super&.map(&:with_indifferent_access) end diff --git a/spec/factories/apps.rb b/spec/factories/apps.rb index c99a1eccd..a6f974512 100644 --- a/spec/factories/apps.rb +++ b/spec/factories/apps.rb @@ -7,7 +7,7 @@ build_number { Faker::Number.number(digits: 4) } after(:build) do |app| - app.config = build(:app_config, app: app) # TODO: try removing this + app.config = build(:app_config, app: app) # NOTE: this can be removed end trait :android do From 2b0628dc0716e58f024f9748829ac70add5398df Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 23:32:09 +0530 Subject: [PATCH 19/26] chore: Annotate mystery methods --- app/models/app_config.rb | 1 + app/models/concerns/app_configurable.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 98035e510..107bbae87 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,6 +40,7 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? + # NOTE: not sure where this is being called from, if at all def ci_cd_workflows super&.map(&:with_indifferent_access) end diff --git a/app/models/concerns/app_configurable.rb b/app/models/concerns/app_configurable.rb index dd2b12aaa..fc2e75420 100644 --- a/app/models/concerns/app_configurable.rb +++ b/app/models/concerns/app_configurable.rb @@ -1,6 +1,8 @@ module AppConfigurable INVALID_PLATFORM_ERROR = "platform must be valid" + # NOTE: not being used by AppVariant, only by GoogleFirebaseIntegration via AppConfig + # Can probably be moved into GoogleFirebaseIntegration def firebase_app(platform) case platform when "android" then firebase_android_config["app_id"] From 69df925f35644ee919008e73ea6e7f4eae8e8689 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Sun, 5 Oct 2025 23:39:16 +0530 Subject: [PATCH 20/26] feat: Move `AppConfig#firebase_app` to `GoogleFirebaseIntegration` --- app/models/bitrise_integration.rb | 4 ---- app/models/bugsnag_integration.rb | 4 ---- app/models/google_firebase_integration.rb | 13 +++++++++++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/models/bitrise_integration.rb b/app/models/bitrise_integration.rb index 4796e3805..9bfdb8d57 100644 --- a/app/models/bitrise_integration.rb +++ b/app/models/bitrise_integration.rb @@ -197,10 +197,6 @@ def load_custom_bitrise_pipelines [] end - def app_config - integrable.config - end - def correct_key if access_token.present? errors.add(:access_token, :no_apps) if list_apps.size < 1 diff --git a/app/models/bugsnag_integration.rb b/app/models/bugsnag_integration.rb index ee1682deb..880670608 100644 --- a/app/models/bugsnag_integration.rb +++ b/app/models/bugsnag_integration.rb @@ -148,10 +148,6 @@ def project_id(platform) project(platform)&.fetch("id", nil) end - def app_config - integrable.config - end - def correct_key if access_token.present? && list_organizations.blank? errors.add(:access_token, :no_orgs) diff --git a/app/models/google_firebase_integration.rb b/app/models/google_firebase_integration.rb index b40673301..c0d4e5fbd 100644 --- a/app/models/google_firebase_integration.rb +++ b/app/models/google_firebase_integration.rb @@ -35,8 +35,6 @@ class GoogleFirebaseIntegration < ApplicationRecord delegate :cache, to: Rails delegate :integrable, to: :integration - delegate :config, to: :integrable - delegate :firebase_app, to: :config after_create_commit :fetch_channels @@ -58,6 +56,17 @@ class GoogleFirebaseIntegration < ApplicationRecord CACHE_EXPIRY = 1.month + INVALID_PLATFORM_ERROR = "platform must be valid" + + def firebase_app(platform) + case platform + when "android" then android_config["app_id"] + when "ios" then ios_config["app_id"] + else + raise ArgumentError, INVALID_PLATFORM_ERROR + end + end + def installation Installations::Google::Firebase::Api.new(project_number, access_key) end From dbff8e33430ec742ef95e5b15e2e5d48153a10a8 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Wed, 8 Oct 2025 07:17:09 +0530 Subject: [PATCH 21/26] feat: Remove remaining public methods from `AppConfig` --- .../integration_card_component.html.erb | 1 - app/models/app_config.rb | 15 --------------- app/models/bitbucket_integration.rb | 10 +++------- app/models/gitlab_integration.rb | 8 ++------ app/models/integration.rb | 1 - 5 files changed, 5 insertions(+), 30 deletions(-) diff --git a/app/components/integration_card_component.html.erb b/app/components/integration_card_component.html.erb index 59812bbf1..cb4319072 100644 --- a/app/components/integration_card_component.html.erb +++ b/app/components/integration_card_component.html.erb @@ -42,7 +42,6 @@ options: app_integration_path(@app, integration), type: :button, size: :xxs, - turbo: false, disabled: !disconnectable?, html_options: {method: :delete, data: {turbo_method: :delete, turbo_confirm: "Are you sure you want disconnect the integration?"}} ) do |b| diff --git a/app/models/app_config.rb b/app/models/app_config.rb index 107bbae87..f1938702d 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -40,21 +40,6 @@ class AppConfig < ApplicationRecord after_initialize :set_bugsnag_config, if: :persisted? - # NOTE: not sure where this is being called from, if at all - def ci_cd_workflows - super&.map(&:with_indifferent_access) - end - - def disconnect!(integration) - if integration.version_control? - self.code_repository = nil - elsif integration.ci_cd? - self.bitrise_project_id = nil - end - - save! - end - private def set_bugsnag_config diff --git a/app/models/bitbucket_integration.rb b/app/models/bitbucket_integration.rb index 924553675..3fd60817a 100644 --- a/app/models/bitbucket_integration.rb +++ b/app/models/bitbucket_integration.rb @@ -35,16 +35,12 @@ def code_repository_name end def code_repo_url - repository_config&.fetch("repo_url", nil) + repository_config&.dig("repo_url", "href") end - def code_repo_namespace - repository_config&.fetch("namespace", nil) - end + def code_repo_namespace = nil - def code_repo_name_only - repository_config&.fetch("name", nil) - end + def code_repo_name_only = nil def install_path BASE_INSTALLATION_URL diff --git a/app/models/gitlab_integration.rb b/app/models/gitlab_integration.rb index 80567a0da..e519fe4e8 100644 --- a/app/models/gitlab_integration.rb +++ b/app/models/gitlab_integration.rb @@ -164,13 +164,9 @@ def code_repo_url repository_config&.fetch("repo_url", nil) end - def code_repo_namespace - repository_config&.fetch("namespace", nil) - end + def code_repo_namespace = nil - def code_repo_name_only - repository_config&.fetch("name", nil) - end + def code_repo_name_only = nil def install_path BASE_INSTALLATION_URL diff --git a/app/models/integration.rb b/app/models/integration.rb index 4a98de8cc..35cd34566 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -345,7 +345,6 @@ def disconnectable_categories? def disconnect return unless disconnectable? transaction do - integrable.config.disconnect!(self) update!(status: :disconnected, discarded_at: Time.current) true end From 11eb9dfcd4c37b6a640874c81c78345312f8c7a1 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Wed, 8 Oct 2025 07:34:11 +0530 Subject: [PATCH 22/26] chore: Fix Bitrise provider readiness check --- app/models/integration.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/integration.rb b/app/models/integration.rb index 35cd34566..eca31f170 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -173,6 +173,10 @@ def ci_cd_provider kept.ci_cd.connected.first&.providable end + def bitrise_ci_cd_provider + kept.ci_cd.find(&:bitrise_integration?)&.providable + end + def monitoring_provider kept.monitoring.first&.providable end @@ -285,7 +289,7 @@ def bitrise_ready? end def bitrise_project - ci_cd.find(&:bitrise_integration?)&.providable&.bitrise_project + bitrise_ci_cd_provider&.bitrise_project end def firebase_ready? From a40b6b3b95314b38541917a855eb26cdb7b912ce Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Wed, 8 Oct 2025 23:32:54 +0530 Subject: [PATCH 23/26] fix: Allow build servers to be configurable Since factoring out the `code_repository` to individual integration providers, creating trains would fail since CI/CD integrations wouldn't have the necessary `repository_config` present. This was observed only for Github, Gitlab and Bitbucket integrations. Thus, we now allow CI/CD integrations to now be configurable. --- app/components/integration_card_component.rb | 3 + .../ci_cd/bitbucket_configs_controller.rb | 55 +++++++++++++++++++ .../ci_cd/github_configs_controller.rb | 52 ++++++++++++++++++ .../ci_cd/gitlab_configs_controller.rb | 52 ++++++++++++++++++ app/models/bitbucket_integration.rb | 2 +- app/models/github_integration.rb | 2 +- app/models/gitlab_integration.rb | 5 +- .../ci_cd/bitbucket_configs/_form.html.erb | 33 +++++++++++ .../edit.html+turbo_frame.erb | 3 + .../bitbucket_configs/edit.turbo_stream.erb | 1 + .../github_configs/edit.html+turbo_frame.erb | 23 ++++++++ .../gitlab_configs/edit.html+turbo_frame.erb | 23 ++++++++ config/locales/en.yml | 9 +++ config/routes.rb | 3 + 14 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 app/controllers/ci_cd/bitbucket_configs_controller.rb create mode 100644 app/controllers/ci_cd/github_configs_controller.rb create mode 100644 app/controllers/ci_cd/gitlab_configs_controller.rb create mode 100644 app/views/ci_cd/bitbucket_configs/_form.html.erb create mode 100644 app/views/ci_cd/bitbucket_configs/edit.html+turbo_frame.erb create mode 100644 app/views/ci_cd/bitbucket_configs/edit.turbo_stream.erb create mode 100644 app/views/ci_cd/github_configs/edit.html+turbo_frame.erb create mode 100644 app/views/ci_cd/gitlab_configs/edit.html+turbo_frame.erb diff --git a/app/components/integration_card_component.rb b/app/components/integration_card_component.rb index 260a2a8e6..c5c42b457 100644 --- a/app/components/integration_card_component.rb +++ b/app/components/integration_card_component.rb @@ -116,6 +116,9 @@ def edit_app_version_control_config_path def edit_app_ci_cd_config_path case integration.providable_type + when "GithubIntegration" then edit_app_ci_cd_github_config_path(@app) + when "GitlabIntegration" then edit_app_ci_cd_gitlab_config_path(@app) + when "BitbucketIntegration" then edit_app_ci_cd_bitbucket_config_path(@app) when "BitriseIntegration" then edit_app_ci_cd_bitrise_config_path(@app) else unsupported_integration_type end diff --git a/app/controllers/ci_cd/bitbucket_configs_controller.rb b/app/controllers/ci_cd/bitbucket_configs_controller.rb new file mode 100644 index 000000000..f1b7622f2 --- /dev/null +++ b/app/controllers/ci_cd/bitbucket_configs_controller.rb @@ -0,0 +1,55 @@ +class CiCd::BitbucketConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_bitbucket_integration + around_action :set_time_zone + + def edit + set_code_repositories + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + format.turbo_stream { render :edit } + end + end + + def update + if @bitbucket_integration.update(parsed_bitbucket_config_params) + redirect_to app_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @bitbucket_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_bitbucket_integration + @bitbucket_integration = @app.ci_cd_provider + unless @bitbucket_integration + redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} + end + end + + def set_code_repositories + @workspaces = @bitbucket_integration.workspaces || [] + workspace = params[:workspace] || @workspaces.first + @code_repositories = @bitbucket_integration.repos(workspace) + end + + def parsed_bitbucket_config_params + bitbucket_config_params = params.require(:bitbucket_integration) + .permit(:repository_config, :workspace) + bitbucket_config_params.merge( + repository_config: bitbucket_config_params[:repository_config]&.safe_json_parse + ) + end +end diff --git a/app/controllers/ci_cd/github_configs_controller.rb b/app/controllers/ci_cd/github_configs_controller.rb new file mode 100644 index 000000000..03096c5cd --- /dev/null +++ b/app/controllers/ci_cd/github_configs_controller.rb @@ -0,0 +1,52 @@ +class CiCd::GithubConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_github_integration + around_action :set_time_zone + + def edit + set_code_repositories + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + end + end + + def update + if @github_integration.update(parsed_github_config_params) + redirect_to app_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @github_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_github_integration + @github_integration = @app.ci_cd_provider + unless @github_integration + redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} + end + end + + def set_code_repositories + @code_repositories = @github_integration.repos + end + + def parsed_github_config_params + github_config_params = params.require(:github_integration) + .permit(:repository_config) + github_config_params.merge( + repository_config: github_config_params[:repository_config]&.safe_json_parse + ) + end +end diff --git a/app/controllers/ci_cd/gitlab_configs_controller.rb b/app/controllers/ci_cd/gitlab_configs_controller.rb new file mode 100644 index 000000000..deb66e1ff --- /dev/null +++ b/app/controllers/ci_cd/gitlab_configs_controller.rb @@ -0,0 +1,52 @@ +class CiCd::GitlabConfigsController < SignedInApplicationController + using RefinedString + + before_action :require_write_access! + before_action :set_app + before_action :set_gitlab_integration + around_action :set_time_zone + + def edit + set_code_repositories + + respond_to do |format| + format.html do |variant| + variant.turbo_frame { render :edit } + end + end + end + + def update + if @gitlab_integration.update(parsed_gitlab_config_params) + redirect_to app_path(@app), notice: t(".success") + else + redirect_back fallback_location: app_integrations_path(@app), + flash: {error: @gitlab_integration.errors.full_messages.to_sentence} + end + end + + private + + def set_app + @app = current_organization.apps.friendly.find(params[:app_id]) + end + + def set_gitlab_integration + @gitlab_integration = @app.ci_cd_provider + unless @gitlab_integration + redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} + end + end + + def set_code_repositories + @code_repositories = @gitlab_integration.repos + end + + def parsed_gitlab_config_params + gitlab_config_params = params.require(:gitlab_integration) + .permit(:repository_config) + gitlab_config_params.merge( + repository_config: gitlab_config_params[:repository_config]&.safe_json_parse + ) + end +end diff --git a/app/models/bitbucket_integration.rb b/app/models/bitbucket_integration.rb index 3fd60817a..fce8ccf44 100644 --- a/app/models/bitbucket_integration.rb +++ b/app/models/bitbucket_integration.rb @@ -72,7 +72,7 @@ def store? = false def project_link = nil def further_setup? - false + true end def enable_auto_merge? = false diff --git a/app/models/github_integration.rb b/app/models/github_integration.rb index 72cab2c77..cc6b78743 100644 --- a/app/models/github_integration.rb +++ b/app/models/github_integration.rb @@ -221,7 +221,7 @@ def store? end def further_setup? - false + true end def enable_auto_merge? = true diff --git a/app/models/gitlab_integration.rb b/app/models/gitlab_integration.rb index e519fe4e8..54e1a2e3e 100644 --- a/app/models/gitlab_integration.rb +++ b/app/models/gitlab_integration.rb @@ -186,7 +186,8 @@ def complete_access def correct_key if integration.ci_cd? - errors.add(:base, :workflows) if workflows(bust_cache: true).blank? + # NOTE: relaxing this validation temporarily since it depends on config that's not yet setup + # errors.add(:base, :workflows) if workflows(bust_cache: true).blank? elsif integration.version_control? errors.add(:base, :repos) if repos.blank? end @@ -265,7 +266,7 @@ def workflow_retriable? = true def workflow_retriable_in_place? = false def further_setup? - false + true end def enable_auto_merge? = true diff --git a/app/views/ci_cd/bitbucket_configs/_form.html.erb b/app/views/ci_cd/bitbucket_configs/_form.html.erb new file mode 100644 index 000000000..d7545f745 --- /dev/null +++ b/app/views/ci_cd/bitbucket_configs/_form.html.erb @@ -0,0 +1,33 @@ +<%= render FormComponent.new(model: [app, bitbucket_integration], url: app_ci_cd_bitbucket_config_path(app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Repository") do |section| %> + <% section.with_description do %> + Primary working code repository for CI/CD builds. + <%= image_tag "integrations/logo_bitbucket.png", title: "Bitbucket", width: 22, class: "my-2" %> + <% end %> + +
+ <%= section.F.labeled_select :workspace, + "Workspace", + options_for_select(workspaces, bitbucket_integration.workspace), + {}, + disabled: workspaces.blank?, + class: EnhancedFormHelper::AuthzForm::SELECT_CLASSES, + data: {action: "change->stream-effect#fetch", stream_effect_target: "dispatch"} %> +
+ +
+ <%= section.F.labeled_select :repository_config, + "Code Repository", + options_for_select( + display_channels(code_repositories) { |repo| repo[:full_name] }, + code_repositories.find { |repo| repo[:full_name] == bitbucket_integration.repository_config&.dig("full_name") }&.to_json + ) %> +
+ <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> +<% end %> diff --git a/app/views/ci_cd/bitbucket_configs/edit.html+turbo_frame.erb b/app/views/ci_cd/bitbucket_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..6c1c5c0bc --- /dev/null +++ b/app/views/ci_cd/bitbucket_configs/edit.html+turbo_frame.erb @@ -0,0 +1,3 @@ +<%= render EnhancedTurboFrameComponent.new(:ci_cd_config) do %> + <%= render partial: "form", locals: {app: @app, bitbucket_integration: @bitbucket_integration, workspaces: @workspaces, code_repositories: @code_repositories} %> +<% end %> diff --git a/app/views/ci_cd/bitbucket_configs/edit.turbo_stream.erb b/app/views/ci_cd/bitbucket_configs/edit.turbo_stream.erb new file mode 100644 index 000000000..b965fba13 --- /dev/null +++ b/app/views/ci_cd/bitbucket_configs/edit.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.update :ci_cd_config, partial: "form", locals: {app: @app, bitbucket_integration: @bitbucket_integration, workspaces: @workspaces, code_repositories: @code_repositories} %> diff --git a/app/views/ci_cd/github_configs/edit.html+turbo_frame.erb b/app/views/ci_cd/github_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..c6ec044e3 --- /dev/null +++ b/app/views/ci_cd/github_configs/edit.html+turbo_frame.erb @@ -0,0 +1,23 @@ +<%= render EnhancedTurboFrameComponent.new(:ci_cd_config) do %> + <%= render FormComponent.new(model: [@app, @github_integration], url: app_ci_cd_github_config_path(@app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Repository") do |section| %> + <% section.with_description do %> + Primary working code repository for CI/CD builds. + <%= image_tag "integrations/logo_github.png", title: "GitHub", width: 22, class: "my-2" %> + <% end %> + +
+ <%= section.F.labeled_select :repository_config, + "Code Repository", + options_for_select( + display_channels(@code_repositories) { |repo| repo[:full_name] }, + @code_repositories.find { |repo| repo[:id] == @github_integration.repository_config&.dig("id") }&.to_json + ) %> +
+ <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> +<% end %> diff --git a/app/views/ci_cd/gitlab_configs/edit.html+turbo_frame.erb b/app/views/ci_cd/gitlab_configs/edit.html+turbo_frame.erb new file mode 100644 index 000000000..68f9649a6 --- /dev/null +++ b/app/views/ci_cd/gitlab_configs/edit.html+turbo_frame.erb @@ -0,0 +1,23 @@ +<%= render EnhancedTurboFrameComponent.new(:ci_cd_config) do %> + <%= render FormComponent.new(model: [@app, @gitlab_integration], url: app_ci_cd_gitlab_config_path(@app), method: :patch) do |f| %> + <% f.with_section(heading: "Select Repository") do |section| %> + <% section.with_description do %> + Primary working code repository for CI/CD builds. + <%= image_tag "integrations/logo_gitlab.png", title: "GitLab", width: 22, class: "my-2" %> + <% end %> + +
+ <%= section.F.labeled_select :repository_config, + "Code Repository", + options_for_select( + display_channels(@code_repositories) { |repo| repo[:full_name] }, + @code_repositories.find { |repo| repo[:id] == @gitlab_integration.repository_config&.dig("id") }&.to_json + ) %> +
+ <% end %> + + <% f.with_action do %> + <%= f.F.authz_submit "Update", "plus.svg", size: :xs %> + <% end %> + <% end %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index b12974bd3..5c6e26052 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -792,6 +792,15 @@ en: bitrise_configs: update: success: "CI/CD configuration was successfully updated." + github_configs: + update: + success: "CI/CD configuration was successfully updated." + gitlab_configs: + update: + success: "CI/CD configuration was successfully updated." + bitbucket_configs: + update: + success: "CI/CD configuration was successfully updated." build_channel: google_firebase_configs: diff --git a/config/routes.rb b/config/routes.rb index 426ead6fc..6c1fe7311 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,6 +77,9 @@ namespace :ci_cd do resource :bitrise_config, only: %i[edit update] + resource :github_config, only: %i[edit update] + resource :gitlab_config, only: %i[edit update] + resource :bitbucket_config, only: %i[edit update] end namespace :build_channel do From 43f82628a3b6171f7311ab176332ad2f769ce169 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Wed, 8 Oct 2025 23:40:50 +0530 Subject: [PATCH 24/26] fix: Make the app config wizard work again Update the new integration config controllers to redirect to the app's show page. Separate integration connection-readiness and integration config-readiness checks so that the wizard isn't stuck at just showing the "Complete your app setup" section. Update CI/CD integration readiness check. --- .../google_firebase_configs_controller.rb | 2 +- .../ci_cd/bitrise_configs_controller.rb | 2 +- .../monitoring/bugsnag_configs_controller.rb | 2 +- .../jira_configs_controller.rb | 2 +- .../linear_configs_controller.rb | 2 +- .../bitbucket_configs_controller.rb | 2 +- .../github_configs_controller.rb | 2 +- .../gitlab_configs_controller.rb | 2 +- app/models/app.rb | 4 +- app/models/integration.rb | 46 ++++++++++++------- 10 files changed, 40 insertions(+), 26 deletions(-) diff --git a/app/controllers/build_channel/google_firebase_configs_controller.rb b/app/controllers/build_channel/google_firebase_configs_controller.rb index 52e1bc47a..c6651d5bb 100644 --- a/app/controllers/build_channel/google_firebase_configs_controller.rb +++ b/app/controllers/build_channel/google_firebase_configs_controller.rb @@ -18,7 +18,7 @@ def edit def update if @google_firebase_integration.update(parsed_google_firebase_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @google_firebase_integration.errors.full_messages.to_sentence} diff --git a/app/controllers/ci_cd/bitrise_configs_controller.rb b/app/controllers/ci_cd/bitrise_configs_controller.rb index d5e580644..a088ce6ef 100644 --- a/app/controllers/ci_cd/bitrise_configs_controller.rb +++ b/app/controllers/ci_cd/bitrise_configs_controller.rb @@ -18,7 +18,7 @@ def edit def update if @bitrise_integration.update(parsed_bitrise_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @bitrise_integration.errors.full_messages.to_sentence} diff --git a/app/controllers/monitoring/bugsnag_configs_controller.rb b/app/controllers/monitoring/bugsnag_configs_controller.rb index 0ab8960f4..2b8b1fb85 100644 --- a/app/controllers/monitoring/bugsnag_configs_controller.rb +++ b/app/controllers/monitoring/bugsnag_configs_controller.rb @@ -18,7 +18,7 @@ def edit def update if @bugsnag_integration.update(parsed_bugsnag_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @bugsnag_integration.errors.full_messages.to_sentence} diff --git a/app/controllers/project_management/jira_configs_controller.rb b/app/controllers/project_management/jira_configs_controller.rb index 4584bfa1c..3b4e26807 100644 --- a/app/controllers/project_management/jira_configs_controller.rb +++ b/app/controllers/project_management/jira_configs_controller.rb @@ -18,7 +18,7 @@ def edit def update if @jira_integration.update(parsed_jira_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @jira_integration.errors.full_messages.to_sentence} diff --git a/app/controllers/project_management/linear_configs_controller.rb b/app/controllers/project_management/linear_configs_controller.rb index 9e8991c17..7cf5e449e 100644 --- a/app/controllers/project_management/linear_configs_controller.rb +++ b/app/controllers/project_management/linear_configs_controller.rb @@ -18,7 +18,7 @@ def edit def update if @linear_integration.update(parsed_linear_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @linear_integration.errors.full_messages.to_sentence} diff --git a/app/controllers/version_control/bitbucket_configs_controller.rb b/app/controllers/version_control/bitbucket_configs_controller.rb index 8ec52913b..5fbad58b1 100644 --- a/app/controllers/version_control/bitbucket_configs_controller.rb +++ b/app/controllers/version_control/bitbucket_configs_controller.rb @@ -19,7 +19,7 @@ def edit def update if @bitbucket_integration.update(parsed_bitbucket_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @bitbucket_integration.errors.full_messages.to_sentence} diff --git a/app/controllers/version_control/github_configs_controller.rb b/app/controllers/version_control/github_configs_controller.rb index aa5fbafb9..9f9ee5ca1 100644 --- a/app/controllers/version_control/github_configs_controller.rb +++ b/app/controllers/version_control/github_configs_controller.rb @@ -18,7 +18,7 @@ def edit def update if @github_integration.update(parsed_github_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @github_integration.errors.full_messages.to_sentence} diff --git a/app/controllers/version_control/gitlab_configs_controller.rb b/app/controllers/version_control/gitlab_configs_controller.rb index 41543bc23..d505459da 100644 --- a/app/controllers/version_control/gitlab_configs_controller.rb +++ b/app/controllers/version_control/gitlab_configs_controller.rb @@ -18,7 +18,7 @@ def edit def update if @gitlab_integration.update(parsed_gitlab_config_params) - redirect_to app_integrations_path(@app), notice: t(".success") + redirect_to app_path(@app), notice: t(".success") else redirect_back fallback_location: app_integrations_path(@app), flash: {error: @gitlab_integration.errors.full_messages.to_sentence} diff --git a/app/models/app.rb b/app/models/app.rb index 7605a01c4..78bec0f97 100644 --- a/app/models/app.rb +++ b/app/models/app.rb @@ -126,7 +126,9 @@ def project_management_connected? integrations.project_management.connected.any? end - delegate :ready?, to: :integrations + def ready? + integrations.ready? && integrations.configured? + end def guided_train_setup? trains.none? || train_in_creation.present? diff --git a/app/models/integration.rb b/app/models/integration.rb index eca31f170..a6a8fbf4b 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -158,7 +158,16 @@ def category_ready?(category) end def ready? - minimum_required_set_ready? && integration_configs_ready? + MINIMUM_REQUIRED_SET.all? { |category| category_ready?(category) } + end + + def configured? + return false if none? # need at least one integration + + further_setup_by_category + .values + .pluck(:ready) + .all? end def slack_notifications? @@ -170,7 +179,7 @@ def vcs_provider end def ci_cd_provider - kept.ci_cd.connected.first&.providable + kept.ci_cd.first&.providable end def bitrise_ci_cd_provider @@ -229,7 +238,7 @@ def further_setup_by_category if connected_integrations.ci_cd.present? categories[:ci_cd] = { further_setup: connected_integrations.ci_cd.any?(&:further_setup?), - ready: bitrise_ready? + ready: ci_cd_ready? } end @@ -259,20 +268,6 @@ def further_setup_by_category private - def minimum_required_set_ready? - MINIMUM_REQUIRED_SET.all? { |category| category_ready?(category) } - end - - # Configuration readiness checks (migrated from AppConfig) - def integration_configs_ready? - return false if none? # need at least one integration - - further_setup_by_category - .values - .pluck(:ready) - .all? - end - def providable_error_message(meta) meta[:value].errors.full_messages[0] end @@ -281,6 +276,23 @@ def code_repository vcs_provider&.repository_config end + def ci_cd_code_repository + ci_cd_provider&.repository_config + end + + def ci_cd_ready? + return false if ci_cd_provider.blank? + + case ci_cd_provider + when GithubIntegration, GitlabIntegration, BitbucketIntegration + ci_cd_code_repository.present? + when BitriseIntegration + bitrise_ready? + else + false + end + end + def bitrise_ready? app = first&.integrable return true unless app&.bitrise_connected? From 7ebdd8ac042f504f86fbf9e25bfe5ac56e7fab43 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Thu, 9 Oct 2025 22:37:25 +0530 Subject: [PATCH 25/26] fix: Address AI review comments --- app/components/integration_card_component.html.erb | 2 +- .../build_channel/google_firebase_configs_controller.rb | 2 +- app/controllers/ci_cd/bitbucket_configs_controller.rb | 2 +- app/controllers/ci_cd/bitrise_configs_controller.rb | 2 +- app/controllers/ci_cd/github_configs_controller.rb | 2 +- app/controllers/ci_cd/gitlab_configs_controller.rb | 2 +- app/controllers/monitoring/bugsnag_configs_controller.rb | 2 +- app/controllers/project_management/jira_configs_controller.rb | 2 -- .../version_control/bitbucket_configs_controller.rb | 2 +- app/controllers/version_control/github_configs_controller.rb | 2 +- app/controllers/version_control/gitlab_configs_controller.rb | 2 +- app/models/app_config.rb | 2 ++ app/models/concerns/app_configurable.rb | 4 ++-- app/models/jira_integration.rb | 4 ++-- 14 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/components/integration_card_component.html.erb b/app/components/integration_card_component.html.erb index cb4319072..fb6791d89 100644 --- a/app/components/integration_card_component.html.erb +++ b/app/components/integration_card_component.html.erb @@ -43,7 +43,7 @@ type: :button, size: :xxs, disabled: !disconnectable?, - html_options: {method: :delete, data: {turbo_method: :delete, turbo_confirm: "Are you sure you want disconnect the integration?"}} + html_options: {method: :delete, data: {turbo_confirm: "Are you sure you want to disconnect this integration?"}} ) do |b| b.with_icon("trash.svg", size: :sm) end %> diff --git a/app/controllers/build_channel/google_firebase_configs_controller.rb b/app/controllers/build_channel/google_firebase_configs_controller.rb index c6651d5bb..f628f38e8 100644 --- a/app/controllers/build_channel/google_firebase_configs_controller.rb +++ b/app/controllers/build_channel/google_firebase_configs_controller.rb @@ -33,7 +33,7 @@ def set_app def set_google_firebase_integration @google_firebase_integration = @app.integrations.firebase_build_channel_provider - unless @google_firebase_integration + unless @google_firebase_integration.is_a?(GoogleFirebaseIntegration) redirect_to app_integrations_path(@app), flash: {error: "Firebase build channel integration not found."} end end diff --git a/app/controllers/ci_cd/bitbucket_configs_controller.rb b/app/controllers/ci_cd/bitbucket_configs_controller.rb index f1b7622f2..b8dce7ba0 100644 --- a/app/controllers/ci_cd/bitbucket_configs_controller.rb +++ b/app/controllers/ci_cd/bitbucket_configs_controller.rb @@ -34,7 +34,7 @@ def set_app def set_bitbucket_integration @bitbucket_integration = @app.ci_cd_provider - unless @bitbucket_integration + unless @bitbucket_integration.is_a?(BitbucketIntegration) redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} end end diff --git a/app/controllers/ci_cd/bitrise_configs_controller.rb b/app/controllers/ci_cd/bitrise_configs_controller.rb index a088ce6ef..504308bb7 100644 --- a/app/controllers/ci_cd/bitrise_configs_controller.rb +++ b/app/controllers/ci_cd/bitrise_configs_controller.rb @@ -33,7 +33,7 @@ def set_app def set_bitrise_integration @bitrise_integration = @app.ci_cd_provider - unless @bitrise_integration + unless @bitrise_integration.is_a?(BitriseIntegration) redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} end end diff --git a/app/controllers/ci_cd/github_configs_controller.rb b/app/controllers/ci_cd/github_configs_controller.rb index 03096c5cd..2bde77b6a 100644 --- a/app/controllers/ci_cd/github_configs_controller.rb +++ b/app/controllers/ci_cd/github_configs_controller.rb @@ -33,7 +33,7 @@ def set_app def set_github_integration @github_integration = @app.ci_cd_provider - unless @github_integration + unless @github_integration.is_a?(GithubIntegration) redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} end end diff --git a/app/controllers/ci_cd/gitlab_configs_controller.rb b/app/controllers/ci_cd/gitlab_configs_controller.rb index deb66e1ff..0ff45df60 100644 --- a/app/controllers/ci_cd/gitlab_configs_controller.rb +++ b/app/controllers/ci_cd/gitlab_configs_controller.rb @@ -33,7 +33,7 @@ def set_app def set_gitlab_integration @gitlab_integration = @app.ci_cd_provider - unless @gitlab_integration + unless @gitlab_integration.is_a?(GitlabIntegration) redirect_to app_integrations_path(@app), flash: {error: "CI/CD integration not found."} end end diff --git a/app/controllers/monitoring/bugsnag_configs_controller.rb b/app/controllers/monitoring/bugsnag_configs_controller.rb index 2b8b1fb85..f5cb8d42b 100644 --- a/app/controllers/monitoring/bugsnag_configs_controller.rb +++ b/app/controllers/monitoring/bugsnag_configs_controller.rb @@ -33,7 +33,7 @@ def set_app def set_bugsnag_integration @bugsnag_integration = @app.monitoring_provider - unless @bugsnag_integration + unless @bugsnag_integration.is_a?(BugsnagIntegration) redirect_to app_integrations_path(@app), flash: {error: "Monitoring integration not found."} end end diff --git a/app/controllers/project_management/jira_configs_controller.rb b/app/controllers/project_management/jira_configs_controller.rb index 3b4e26807..1221e7ca7 100644 --- a/app/controllers/project_management/jira_configs_controller.rb +++ b/app/controllers/project_management/jira_configs_controller.rb @@ -1,6 +1,4 @@ class ProjectManagement::JiraConfigsController < SignedInApplicationController - using RefinedString - before_action :require_write_access! before_action :set_app before_action :set_jira_integration diff --git a/app/controllers/version_control/bitbucket_configs_controller.rb b/app/controllers/version_control/bitbucket_configs_controller.rb index 5fbad58b1..49d4bdd8a 100644 --- a/app/controllers/version_control/bitbucket_configs_controller.rb +++ b/app/controllers/version_control/bitbucket_configs_controller.rb @@ -34,7 +34,7 @@ def set_app def set_bitbucket_integration @bitbucket_integration = @app.vcs_provider - unless @bitbucket_integration + unless @bitbucket_integration.is_a?(BitbucketIntegration) redirect_to app_integrations_path(@app), flash: {error: "Version control integration not found."} end end diff --git a/app/controllers/version_control/github_configs_controller.rb b/app/controllers/version_control/github_configs_controller.rb index 9f9ee5ca1..da859b1c8 100644 --- a/app/controllers/version_control/github_configs_controller.rb +++ b/app/controllers/version_control/github_configs_controller.rb @@ -33,7 +33,7 @@ def set_app def set_github_integration @github_integration = @app.vcs_provider - unless @github_integration + unless @github_integration.is_a?(GithubIntegration) redirect_to app_integrations_path(@app), flash: {error: "Version control integration not found."} end end diff --git a/app/controllers/version_control/gitlab_configs_controller.rb b/app/controllers/version_control/gitlab_configs_controller.rb index d505459da..68a01684c 100644 --- a/app/controllers/version_control/gitlab_configs_controller.rb +++ b/app/controllers/version_control/gitlab_configs_controller.rb @@ -33,7 +33,7 @@ def set_app def set_gitlab_integration @gitlab_integration = @app.vcs_provider - unless @gitlab_integration + unless @gitlab_integration.is_a?(GitlabIntegration) redirect_to app_integrations_path(@app), flash: {error: "Version control integration not found."} end end diff --git a/app/models/app_config.rb b/app/models/app_config.rb index f1938702d..f7ab59a3e 100644 --- a/app/models/app_config.rb +++ b/app/models/app_config.rb @@ -38,8 +38,10 @@ class AppConfig < ApplicationRecord validate :jira_release_filters, if: -> { jira_config&.dig("release_filters").present? } validate :linear_release_filters, if: -> { linear_config&.dig("release_filters").present? } + # TODO: remove callback after_initialize :set_bugsnag_config, if: :persisted? + # TODO: remove private method private def set_bugsnag_config diff --git a/app/models/concerns/app_configurable.rb b/app/models/concerns/app_configurable.rb index fc2e75420..5d09f7f59 100644 --- a/app/models/concerns/app_configurable.rb +++ b/app/models/concerns/app_configurable.rb @@ -1,8 +1,8 @@ module AppConfigurable INVALID_PLATFORM_ERROR = "platform must be valid" - # NOTE: not being used by AppVariant, only by GoogleFirebaseIntegration via AppConfig - # Can probably be moved into GoogleFirebaseIntegration + # NOTE: not being used by AppVariant, was only being used by GoogleFirebaseIntegration via AppConfig + # Has been moved into GoogleFirebaseIntegration def firebase_app(platform) case platform when "android" then firebase_android_config["app_id"] diff --git a/app/models/jira_integration.rb b/app/models/jira_integration.rb index 2df70c2f4..e2bd0c396 100644 --- a/app/models/jira_integration.rb +++ b/app/models/jira_integration.rb @@ -97,8 +97,8 @@ def complete_access if resources.length == 1 self.cloud_id = resources.first["id"] - self.organization_url = resource["url"] - self.organization_name = resource["name"] + self.organization_url = resources.first["url"] + self.organization_name = resources.first["name"] true else @available_resources = resources From d4c2e5b746b307865acd56e3d38cbf70865a4a27 Mon Sep 17 00:00:00 2001 From: Animesh-Ghosh Date: Fri, 10 Oct 2025 01:02:47 +0530 Subject: [PATCH 26/26] feat: Remove safe-nav operator Safe-nav felt unnecessary since Integration depends on App. --- app/models/integration.rb | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/models/integration.rb b/app/models/integration.rb index a6a8fbf4b..78db9cc16 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -143,6 +143,7 @@ def find_build_channels(id, with_production: false) end def category_ready?(category) + # Does the following need to change to `ready.first.integrable` since app seems like an optional `belongs_to` association? app = ready.first&.app if category != :build_channel || !app&.cross_platform? @@ -294,8 +295,8 @@ def ci_cd_ready? end def bitrise_ready? - app = first&.integrable - return true unless app&.bitrise_connected? + app = first.integrable + return true unless app.bitrise_connected? bitrise_project.present? end @@ -305,16 +306,16 @@ def bitrise_project end def firebase_ready? - app = first&.integrable - return true unless app&.firebase_connected? + app = first.integrable + return true unless app.firebase_connected? firebase_build_channel = firebase_build_channel_provider configs_ready?(app, firebase_build_channel&.android_config, firebase_build_channel&.ios_config) end def bugsnag_ready? - app = first&.integrable - return true unless app&.bugsnag_connected? + app = first.integrable + return true unless app.bugsnag_connected? monitoring = monitoring_provider configs_ready?(app, monitoring&.android_config, monitoring&.ios_config) @@ -343,10 +344,11 @@ def project_management_ready? false end + # NOTE: Could be moved to App perhaps? def configs_ready?(app, android, ios) - return ios.present? if app&.ios? - return android.present? if app&.android? - ios.present? && android.present? if app&.cross_platform? + return ios.present? if app.ios? + return android.present? if app.android? + ios.present? && android.present? if app.cross_platform? end end