Skip to content

Commit

Permalink
Copy approvals from previous release
Browse files Browse the repository at this point in the history
  • Loading branch information
gitstart_bot committed Dec 6, 2024
1 parent 0bed216 commit 691b23f
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 20 deletions.
10 changes: 9 additions & 1 deletion app/controllers/releases_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class ReleasesController < SignedInApplicationController
include Tabbable
around_action :set_time_zone
before_action :require_write_access!, only: %i[create destroy post_release]
before_action :set_release, only: %i[show destroy update timeline override_approvals]
before_action :set_release, only: %i[show destroy update timeline override_approvals copy_approvals]
before_action :set_train_and_app, only: %i[show destroy update timeline]

def index
Expand Down Expand Up @@ -60,6 +60,14 @@ def override_approvals
end
end

def copy_approvals
if @release.copy_previous_approvals
redirect_to release_approval_items_path(@release), notice: "Approvals have been successfully copied."
else
redirect_back fallback_location: root_path, flash: {error: "Unable to copy approvals from previous release."}
end
end

def overview
live_release!
set_train_and_app
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/trains_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ def train_update_params
:tag_releases,
:tag_suffix,
:patch_version_bump_only,
:approvals_enabled
:approvals_enabled,
:copy_approvals
)
end

Expand Down
4 changes: 2 additions & 2 deletions app/javascript/controllers/toggle_switch_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ export default class extends Controller {
if (this.checkboxTarget.checked) {
this.outputTarget.innerHTML = this.onLabelValue
if (this.hasChildTarget) {
this.childTarget.hidden = false
this.childTargets.forEach((child) => (child.hidden = false));
}
} else {
this.outputTarget.innerHTML = this.offLabelValue
if (this.hasChildTarget) {
this.childTarget.hidden = true
this.childTargets.forEach((child) => (child.hidden = true));
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions app/jobs/releases/copy_previous_approvals_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Releases::CopyPreviousApprovalsJob < ApplicationJob
queue_as :default

def perform(release_id)
release = Release.find_by(id: release_id)
unless release
Rails.logger.error("Release with ID #{release_id} not found")
return
end

release.copy_previous_approvals
end
end
38 changes: 37 additions & 1 deletion app/models/release.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,35 @@ class Release < ApplicationRecord
after_create :create_build_queue!, if: -> { train.build_queue_enabled? }
after_commit -> { Releases::PreReleaseJob.perform_later(id) }, on: :create
after_commit -> { Releases::FetchCommitLogJob.perform_later(id) }, on: :create
after_commit -> { Releases::CopyPreviousApprovalsJob.perform_later(id) }, on: :create, if: :copy_approvals_enabled?
after_commit -> { create_stamp!(data: {version: original_release_version}) }, on: :create

attr_accessor :has_major_bump, :hotfix_platform, :custom_version
friendly_id :human_slug, use: :slugged

delegate :versioning_strategy, :patch_version_bump_only, :product_v2?, to: :train
delegate :app, :vcs_provider, :release_platforms, :notify!, :continuous_backmerge?, :approvals_enabled?, to: :train
delegate :app, :vcs_provider, :release_platforms, :notify!, :continuous_backmerge?, :approvals_enabled?, :copy_approvals?, to: :train
delegate :platform, :organization, to: :app

def copy_previous_approvals
previous_release = fetch_previous_finished_release
return if previous_release.blank?
previous_release.approval_items.find_each do |approval_item|
new_approval_item = approval_items.find_or_initialize_by(
content: approval_item.content,
author_id: approval_item.author_id
)
new_approval_item.approval_assignees = approval_item.approval_assignees.map do |approval_assignee|
new_approval_item.approval_assignees.find_or_initialize_by(
assignee_id: approval_assignee.assignee_id
)
end

new_approval_item.save!
end
approval_items.present?
end

def self.pending_release?
pending_release.exists?
end
Expand Down Expand Up @@ -556,6 +576,22 @@ def approvals_blocking?
!(approvals_overridden? || approvals_finished?)
end

def copy_approvals_enabled?
copy_approvals? && release?
end

def copy_approval_restricted?
fetch_previous_finished_release.nil? || approval_items.present? || hotfix?
end

def fetch_previous_finished_release
train.releases
.release
.finished
.order(created_at: :desc)
.first
end

private

def base_tag_name
Expand Down
6 changes: 6 additions & 0 deletions app/models/train.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# build_queue_size :integer
# build_queue_wait_time :interval
# compact_build_notes :boolean default(FALSE)
# copy_approvals :boolean default(FALSE)
# description :string
# kickoff_at :datetime
# manual_release :boolean default(FALSE)
Expand Down Expand Up @@ -113,9 +114,14 @@ class Train < ApplicationRecord
after_create :create_default_notification_settings
after_create :create_release_index
after_create -> { Flipper.enable_actor(:product_v2, self) }
before_update :disable_copy_approvals, unless: :approvals_enabled?
after_update :schedule_release!, if: -> { kickoff_at.present? && kickoff_at_previously_was.blank? }
after_update :create_default_notification_settings, if: -> { notification_channel.present? && notification_channel_previously_was.blank? }

def disable_copy_approvals
self.copy_approvals = false
end

before_destroy :ensure_deletable, prepend: true do
throw(:abort) if errors.present?
end
Expand Down
30 changes: 20 additions & 10 deletions app/views/approval_items/_items.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@

<div class="flex flex-col section-gap-default mt-2">
<%= render V2::CardComponent.new(title: "Items", separator: false, size: :full) do |card| %>
<% card.with_action do %>
<%= render V2::ButtonComponent.new(
scheme: :light,
options: root_path,
type: :link,
disabled: true,
size: :xxs,
label: "Copy from previous release") do |b|
b.with_icon("v2/square_stack.svg", size: :sm)
end %>
<% if release.approvals_editable? %>
<% card.with_action do %>
<%= render V2::ButtonComponent.new(
scheme: :light,
options: copy_approvals_release_path(release),
type: :button,
disabled: release.copy_approval_restricted?,
size: :xxs,
label: "Copy from previous release",
html_options: {
method: :post,
data: {
turbo_method: :post,
turbo_confirm: "This will copy all the release approvals from the last release. Are you sure?"
}
}
) do |b| %>
<%= b.with_icon("v2/square_stack.svg", size: :sm) %>
<% end %>
<% end %>
<% end %>

<% if release.approvals_blocking? %>
Expand Down
22 changes: 18 additions & 4 deletions app/views/trains/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,24 @@
are approved.
<% end %>

<%= render V2::Form::SwitchComponent.new(form: section.F,
field_name: :approvals_enabled,
on_label: "Submission approvals enabled",
off_label: "Submission approvals disabled") %>
<div data-controller="toggle-switch">
<%= render V2::Form::SwitchComponent.new(form: section.F,
field_name: :approvals_enabled,
on_label: "Submission approvals enabled",
off_label: "Submission approvals disabled",
hide_child: section.F.object.approvals_enabled != true,
switch_data: { action: "toggle-switch#change",
toggle_switch_target: "checkbox" }) do |component| %>
<% component.with_child do %>
<div data-toggle-switch-target="child" hidden>
<%= render V2::Form::SwitchComponent.new(form: section.F,
field_name: :copy_approvals,
on_label: "Always copy approvals from previous release",
off_label: "Do not copy approvals from previous release") %>
</div>
<% end %>
<% end %>
</div>
<% end %>

<%= f.with_advanced_section(heading: "Build Queue") do |section| %>
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
get :soak
get :wrap_up_automations
patch :override_approvals
post :copy_approvals
end

resources :approval_items, only: %i[index create update destroy], shallow: false
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20241125105657_add_copy_approvals_to_train.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddCopyApprovalsToTrain < ActiveRecord::Migration[7.2]
def change
add_column :trains, :copy_approvals, :boolean, default: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions spec/factories/app_configs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
factory :app_config do
code_repository { {id: 123, full_name: "tramline/repo", namespace: "tramline"} }
notification_channel { {id: 123, name: "build_notifications"} }
# This code is added because in some of our test cases, we need an associated record when creating a train. Without it, the spec fails.
ci_cd_workflows do
[
{id: 1, name: "Test APK"}
]
end
app factory: %i[app android without_config]
end
end
44 changes: 44 additions & 0 deletions spec/jobs/releases/copy_previous_approvals_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require "rails_helper"

RSpec.describe Releases::CopyPreviousApprovalsJob do
let(:release) { create(:release) }
let(:release_id) { release.id }

describe "#perform" do
before do
allow(release).to receive(:copy_previous_approvals).and_wrap_original do |_m, *_args|
create(:approval_item, release: release)
end

allow(Release).to receive(:find_by).with(id: release_id).and_return(release)
end

it "adds approval items to the release if none exist" do
expect(release.approval_items).to be_empty

described_class.perform_now(release_id)

release.reload
expect(release.approval_items.count).to eq(1)
expect(release.approval_items).to all(be_a(ApprovalItem))
end

it "performs the job successfully without errors" do
expect { described_class.perform_now(release_id) }.not_to raise_error
end

context "when release is not found" do
before do
allow(Release).to receive(:find_by).with(id: release_id).and_return(nil)
end

it "logs an error if release is not found" do
allow(Rails.logger).to receive(:error)

described_class.perform_now(release_id)

expect(Rails.logger).to have_received(:error).with("Release with ID #{release_id} not found")
end
end
end
end
Loading

0 comments on commit 691b23f

Please sign in to comment.