Skip to content

Commit

Permalink
Jira integration for release readiness
Browse files Browse the repository at this point in the history
  • Loading branch information
gitstart_bot committed Nov 21, 2024
1 parent 600cfc7 commit b2f73a9
Show file tree
Hide file tree
Showing 25 changed files with 1,480 additions and 7 deletions.
Binary file added app/assets/images/integrations/logo_jira.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 69 additions & 1 deletion app/controllers/app_configs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def pick_category
when Integration.categories[:monitoring] then configure_monitoring
when Integration.categories[:notification] then configure_notification_channel
when Integration.categories[:build_channel] then configure_build_channel
when Integration.categories[:project_management] then configure_project_management
else raise "Invalid integration category."
end
end
Expand Down Expand Up @@ -69,6 +70,10 @@ def configure_monitoring
set_monitoring_projects if further_setup_by_category?.dig(:monitoring, :further_setup)
end

def configure_project_management
set_jira_projects if further_setup_by_category?.dig(:project_management, :further_setup)
end

def set_app_config
@config = AppConfig.find_or_initialize_by(app: @app)
end
Expand All @@ -86,7 +91,13 @@ def app_config_params
:bugsnag_ios_project_id,
:bugsnag_android_release_stage,
:bugsnag_android_project_id,
:bitbucket_workspace
:bitbucket_workspace,
jira_config: {
selected_projects: [],
project_configs: {},
release_tracking: [:track_tickets, :auto_transition],
release_filters: [[:type, :value]]
}
)
end

Expand All @@ -98,6 +109,7 @@ def parsed_app_config_params
.merge(bugsnag_config(app_config_params.slice(*BUGSNAG_CONFIG_PARAMS)))
.merge(firebase_ios_config: app_config_params[:firebase_ios_config]&.safe_json_parse)
.merge(firebase_android_config: app_config_params[:firebase_android_config]&.safe_json_parse)
.merge(jira_config: parse_jira_config(app_config_params[:jira_config]))
.except(*BUGSNAG_CONFIG_PARAMS)
.compact
end
Expand Down Expand Up @@ -133,6 +145,62 @@ def set_integration_category
end
end

def set_jira_projects
provider = @app.integrations.project_management_provider
@jira_data = provider.setup

@config.jira_config = {} if @config.jira_config.nil?

@config.jira_config = {
"selected_projects" => @config.jira_config["selected_projects"] || [],
"project_configs" => @config.jira_config["project_configs"] || {},
"release_tracking" => @config.jira_config["release_tracking"] || {
"track_tickets" => false,
"auto_transition" => false
},
"release_filters" => @config.jira_config["release_filters"] || []
}

@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") || []

@config.jira_config["project_configs"][project_key] ||= {
"done_states" => done_states
}
end

@config.save! if @config.changed?
@current_jira_config = @config.jira_config.with_indifferent_access
end

def parse_jira_config(config)
return {} if config.blank?

{
selected_projects: Array(config[:selected_projects]),
project_configs: config[:project_configs]&.transform_values do |project_config|
{
done_states: Array(project_config[:done_states])&.reject(&:blank?),
custom_done_states: Array(project_config[:custom_done_states])&.reject(&:blank?)
}
end || {},
release_tracking: {
track_tickets: ActiveModel::Type::Boolean.new.cast(config.dig(:release_tracking, :track_tickets)),
auto_transition: ActiveModel::Type::Boolean.new.cast(config.dig(:release_tracking, :auto_transition))
},
release_filters: config[:release_filters]&.values&.filter_map do |filter|
next if filter[:type].blank? || filter[:value].blank?
{
"type" => filter[:type],
"value" => filter[:value]
}
end || []
}
end

def bugsnag_config(config_params)
config = {}

Expand Down
94 changes: 94 additions & 0 deletions app/controllers/integration_listeners/jira_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
class IntegrationListeners::JiraController < IntegrationListenerController
using RefinedString

INTEGRATION_CREATE_ERROR = "Failed to create the integration, please try again."

def callback
unless valid_state?
redirect_to app_path(state_app), alert: INTEGRATION_CREATE_ERROR
return
end

begin
@integration = state_app.integrations.build(integration_params)
@integration.providable = build_providable

if @integration.providable.complete_access
@integration.save!
redirect_to app_path(state_app),
notice: t("integrations.project_management.jira.integration_created")
else
@resources = @integration.providable.available_resources

if @resources.blank?
redirect_to app_integrations_path(state_app),
alert: t("integrations.project_management.jira.no_organization")
return
end

render "jira_integration/select_organization"
end
rescue => e
Rails.logger.error("Failed to create Jira integration: #{e.message}")
redirect_to app_integrations_path(state_app),
alert: INTEGRATION_CREATE_ERROR
end
end

def set_organization
@integration = state_app.integrations.build(integration_params)
@integration.providable = build_providable
@integration.providable.cloud_id = params[:cloud_id]
@integration.providable.code = params[:code]

if @integration.save!
@integration.providable.setup
redirect_to app_path(@integration.integrable),
notice: t("integrations.project_management.jira.integration_created")
else
@resources = @integration.providable.available_resources
render "jira_integration/select_organization"
end
rescue => e
Rails.logger.error("Failed to create Jira integration: #{e.message}")
redirect_to app_integrations_path(state_app),
alert: INTEGRATION_CREATE_ERROR
end

protected

def providable_params
super.merge(
code: code,
callback_url: callback_url
)
end

private

def callback_url
host = request.host_with_port
Rails.application.routes.url_helpers.jira_callback_url(
host: host,
protocol: request.protocol.gsub("://", "")
)
end

def state
@state ||= begin
cleaned_state = params[:state].tr(" ", "+")
JSON.parse(cleaned_state.decode).with_indifferent_access
rescue ActiveSupport::MessageEncryptor::InvalidMessage => e
Rails.logger.error "Invalid state parameter: #{e.message}"
{}
end
end

def error?
params[:error].present? || state.empty?
end

def state_app
@state_app ||= App.find(state[:app_id])
end
end
37 changes: 37 additions & 0 deletions app/javascript/controllers/project_selector_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["config", "filterDivider"]

connect() {
this.toggleConfigurations()
this.toggleFilterDivider()
}

toggle(event) {
this.toggleConfigurations()
this.toggleFilterDivider()
}

toggleConfigurations() {
const configs = document.querySelectorAll('.project-config')
configs.forEach(config => {
const projectKey = config.dataset.project
const checkbox = document.querySelector(`#project_${projectKey}`)
if (checkbox) {
config.style.display = checkbox.checked ? 'block' : 'none'
}
})
}

toggleFilterDivider() {
const anyProjectSelected = Array.from(document.querySelectorAll('input[name^="app_config[jira_config][selected_projects]"]'))
.some(checkbox => checkbox.checked)

if (this.hasFilterDividerTarget) {
this.filterDividerTarget.style.display = anyProjectSelected ? 'block' : 'none'
this.filterDividerTarget.classList.add('border-t')
this.filterDividerTarget.classList.add('border-b')
}
}
}
9 changes: 9 additions & 0 deletions app/javascript/controllers/project_states_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["content"]

toggle(event) {
this.contentTarget.style.display = event.target.checked ? "block" : "none"
}
}
17 changes: 17 additions & 0 deletions app/javascript/controllers/release_filters_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["template", "container", "filter"]

add(event) {
event.preventDefault()
const content = this.templateTarget.innerHTML.replace(/__INDEX__/g, this.filterTargets.length)
this.containerTarget.insertAdjacentHTML("beforeend", content)
}

remove(event) {
event.preventDefault()
const filter = event.target.closest("[data-release-filters-target='filter']")
filter.remove()
}
}
Loading

0 comments on commit b2f73a9

Please sign in to comment.