diff --git a/app/assets/images/tag.svg b/app/assets/images/tag.svg new file mode 100644 index 000000000..b434c80d2 --- /dev/null +++ b/app/assets/images/tag.svg @@ -0,0 +1,6 @@ + diff --git a/app/components/commit_component.html.erb b/app/components/commit_component.html.erb index 4bbafa371..ed3ed0062 100644 --- a/app/components/commit_component.html.erb +++ b/app/components/commit_component.html.erb @@ -44,6 +44,13 @@ <% end %> + <% if tag_name.present? %> + <%= render BadgeComponent.new(kind: :badge) do |badge| %> + <% badge.with_icon("tag.svg") %> + <% badge.with_link(tag_name, tag_url) %> + <% end %> + <% end %> + <% if pull_request.present? %> <%= render BadgeComponent.new(kind: :badge) do |badge| %> <% badge.with_icon("git_pull_request.svg") %> diff --git a/app/components/commit_component.rb b/app/components/commit_component.rb index 18ee98db9..6fd14c8b6 100644 --- a/app/components/commit_component.rb +++ b/app/components/commit_component.rb @@ -8,7 +8,8 @@ def initialize(commit:, avatar: true, detailed: true) end attr_reader :commit - delegate :message, :author_name, :author_email, :author_login, :author_url, :timestamp, :short_sha, :url, to: :commit + delegate :message, :author_name, :author_email, :author_login, + :author_url, :timestamp, :short_sha, :url, :tag_name, :tag_url, to: :commit def author_link author_url || "mailto:#{author_email}" diff --git a/app/components/live_release/changeset_tracking_component.rb b/app/components/live_release/changeset_tracking_component.rb index 0f4995b7a..4e9488617 100644 --- a/app/components/live_release/changeset_tracking_component.rb +++ b/app/components/live_release/changeset_tracking_component.rb @@ -30,6 +30,10 @@ def changelog_from def apply_help_text return if change_queue_commits.blank? - "#{change_queue_commits_count} commit(s) in the queue. These will be automatically applied in #{time_in_words(build_queue&.scheduled_at)} or after #{build_queue&.build_queue_size} commits." + if release.train.trunk? + "#{change_queue_commits_count} commit(s) in the queue." + else + "#{change_queue_commits_count} commit(s) in the queue. These will be automatically applied in #{time_in_words(build_queue&.scheduled_at)} or after #{build_queue&.build_queue_size} commits." + end end end diff --git a/app/components/live_release/finalize_component.html.erb b/app/components/live_release/finalize_component.html.erb index 8569eb87b..bb739f967 100644 --- a/app/components/live_release/finalize_component.html.erb +++ b/app/components/live_release/finalize_component.html.erb @@ -31,13 +31,15 @@
- ◦ Cut tag at <%= release.last_commit&.short_sha %>
and push to <%= tag_link %> <%= checked %>
+ ◦ Cut a VCS release at <%= release.last_commit&.short_sha %>
and push to <%= tag_link %> <%= checked %>
- ◦ Ensure all unmerged changes are pushed back to <%= release.train.working_branch %> <%= checked %> -
+ <% unless train.trunk? %> ++ ◦ Ensure all unmerged changes are pushed back to <%= release.train.working_branch %> <%= checked %> +
+ <% end %>◦ Refresh all the DevOps dashboards and reports <%= checked %> diff --git a/app/controllers/trains_controller.rb b/app/controllers/trains_controller.rb index e1828c3c0..8e6033cf8 100644 --- a/app/controllers/trains_controller.rb +++ b/app/controllers/trains_controller.rb @@ -13,6 +13,8 @@ class TrainsController < SignedInApplicationController def new @train = @app.trains.new + @train.build_queue_wait_time_value = 0 + @train.build_queue_size = 0 end def edit diff --git a/app/javascript/controllers/domain/branching_selector_controller.js b/app/javascript/controllers/domain/branching_selector_controller.js index 75074e8e4..81c185f66 100644 --- a/app/javascript/controllers/domain/branching_selector_controller.js +++ b/app/javascript/controllers/domain/branching_selector_controller.js @@ -1,12 +1,14 @@ import {Controller} from "@hotwired/stimulus"; const STRATEGIES = { + trunk: "trunk", almost_trunk: "almost_trunk", release_backmerge: "release_backmerge", parallel_working: "parallel_working" } export default class extends Controller { - static targets = ["branchingStrategy", "almostTrunk", "releaseBackMerge", "parallelBranches", "backmerge"] + static targets = ["branchingStrategy", "almostTrunk", "trunk", "releaseBackMerge", "parallelBranches", "backmerge", "buildQueueToggle"] + static outlets = ["domain--build-queue-help", "domain--release-schedule-help"] initialize() { this.showCorrectInputs() @@ -14,20 +16,30 @@ export default class extends Controller { change() { this.showCorrectInputs() + if (this.hasDomainBuildQueueHelpOutlet) { + this.domainBuildQueueHelpOutlet.branchingStrategyValue = this.branchingStrategyTarget.value + } } showCorrectInputs() { this.__resetFields() const selectedBranchingStrategy = this.branchingStrategyTarget.value - if (selectedBranchingStrategy === STRATEGIES.almost_trunk) { + + if (selectedBranchingStrategy === STRATEGIES.trunk) { + this.disableBuildQueueToggle() + this.__hideBackmergeConfig() + } else if (selectedBranchingStrategy === STRATEGIES.almost_trunk) { this.almostTrunkTarget.hidden = false + this.enableBuildQueueToggle() this.__showBackmergeConfig() } else if (selectedBranchingStrategy === STRATEGIES.release_backmerge) { this.releaseBackMergeTarget.hidden = false + this.enableBuildQueueToggle() this.__hideBackmergeConfig() } else if (selectedBranchingStrategy === STRATEGIES.parallel_working) { this.parallelBranchesTarget.hidden = false + this.enableBuildQueueToggle() this.__hideBackmergeConfig() } } @@ -45,4 +57,12 @@ export default class extends Controller { __showBackmergeConfig() { this.backmergeTarget.hidden = false } + + disableBuildQueueToggle() { + this.buildQueueToggleTarget.disabled = true + } + + enableBuildQueueToggle() { + this.buildQueueToggleTarget.disabled = false; + } } diff --git a/app/javascript/controllers/domain/build_queue_help_controller.js b/app/javascript/controllers/domain/build_queue_help_controller.js index d3697dd5f..12477a77d 100644 --- a/app/javascript/controllers/domain/build_queue_help_controller.js +++ b/app/javascript/controllers/domain/build_queue_help_controller.js @@ -4,12 +4,19 @@ const BASE_HELP_TEXT = "Changes will be applied to the release every " const ERR_HELP_TEXT = "You must set a valid build queue config when it is enabled" export default class extends Controller { - static targets = ["checkbox", "size", "waitTimeValue", "waitTimeUnit", "output", "errOutput"]; + static targets = ["checkbox", "size", "waitTimeValue", "waitTimeUnit", "output", "errOutput"] + static values = { + branchingStrategy: String + } initialize() { this.change(); } + branchingStrategyValueChanged() { + this.change(); + } + change() { this.__resetContents() @@ -29,7 +36,13 @@ export default class extends Controller { const waitTimeUnit = this.waitTimeUnitTarget.value const waitTimeValue = this.waitTimeValueTarget.value - this.outputTarget.textContent = `${BASE_HELP_TEXT}${waitTimeValue} ${waitTimeUnit} OR ${size} commits` + if (this.branchingStrategyValue === "trunk") { + this.outputTarget.textContent = "Changes will be applied manually" + this.__changeInputStates(true) + } else { + this.outputTarget.textContent = `${BASE_HELP_TEXT}${waitTimeValue} ${waitTimeUnit} OR ${size} commits` + this.__changeInputStates(false) + } } __resetContents() { @@ -40,4 +53,10 @@ export default class extends Controller { __isEmptyConfig() { return this.sizeTarget.value === "" || this.waitTimeUnitTarget.value === "" || this.waitTimeValueTarget.value === "" } + + __changeInputStates(enabled) { + this.sizeTarget.disabled = enabled + this.waitTimeValueTarget.disabled = enabled + this.waitTimeUnitTarget.disabled = enabled + } } diff --git a/app/libs/coordinators/apply_commit.rb b/app/libs/coordinators/apply_commit.rb index 33c5f19d7..f8e4c9725 100644 --- a/app/libs/coordinators/apply_commit.rb +++ b/app/libs/coordinators/apply_commit.rb @@ -10,6 +10,8 @@ def initialize(release, commit) def call return unless commit.applicable? + commit.create_tag! if train.tag_applied_commits? + release.release_platform_runs.each do |run| next unless run.on_track? @@ -44,4 +46,5 @@ def apply_change?(run) end attr_reader :release, :commit + delegate :train, to: :release end diff --git a/app/libs/coordinators/finalize_release.rb b/app/libs/coordinators/finalize_release.rb index 868b17495..7417bff7e 100644 --- a/app/libs/coordinators/finalize_release.rb +++ b/app/libs/coordinators/finalize_release.rb @@ -11,6 +11,7 @@ def initialize(release, force_finalize = false) end HANDLERS = { + "trunk" => Trunk, "almost_trunk" => AlmostTrunk, "parallel_working" => ParallelBranches, "release_backmerge" => ReleaseBackMerge diff --git a/app/libs/coordinators/finalize_release/trunk.rb b/app/libs/coordinators/finalize_release/trunk.rb new file mode 100644 index 000000000..69224737f --- /dev/null +++ b/app/libs/coordinators/finalize_release/trunk.rb @@ -0,0 +1,16 @@ +class Coordinators::FinalizeRelease::Trunk + def self.call(release) + new(release).call + end + + def initialize(release) + @release = release + end + + def call + GitHub::Result.new { release.create_release_from_tag!(release.applied_commits.last.tag_name) } + end + + attr_reader :release + delegate :train, to: :release +end diff --git a/app/libs/coordinators/process_commits.rb b/app/libs/coordinators/process_commits.rb index 65882da3c..294b0fa5d 100644 --- a/app/libs/coordinators/process_commits.rb +++ b/app/libs/coordinators/process_commits.rb @@ -35,7 +35,7 @@ def call def create_head_commit! commit = Commit.find_or_create_by!(commit_params(fudge_timestamp(head_commit))) if release.queue_commit?(commit) - queue_commit!(commit) + queue_commit!(commit, can_apply: !release.train.trunk?) else Coordinators::ApplyCommit.call(release, commit) end diff --git a/app/libs/coordinators/start_release.rb b/app/libs/coordinators/start_release.rb index 201fef990..5d21749ec 100644 --- a/app/libs/coordinators/start_release.rb +++ b/app/libs/coordinators/start_release.rb @@ -69,6 +69,7 @@ def create_release end def release_branch + return train.working_branch if train.trunk? return new_branch_name(hotfix: true) if hotfix_from_new_branch? && create_branches? return existing_hotfix_branch if hotfix_from_previous_branch? return new_branch_name if create_branches? diff --git a/app/libs/installations/github/api.rb b/app/libs/installations/github/api.rb index d0cc3c524..873cb3708 100644 --- a/app/libs/installations/github/api.rb +++ b/app/libs/installations/github/api.rb @@ -202,15 +202,15 @@ def create_annotated_tag!(repo, name, branch_name, message, tagger_name, tagger_ end # creates a lightweight tag and a GitHub release simultaneously - def create_release!(repo, tag_name, branch_name, release_notes = nil) + def create_release!(repo, tag_name, branch_name = nil, release_notes = nil) options = { - target_commitish: branch_name, name: tag_name, body: release_notes.presence, generate_release_notes: release_notes.blank? }.compact + options[:target_commitish] = branch_name if branch_name.present? execute do - raise Installations::Error.new("Should not create a tag", reason: :tag_reference_already_exists) if tag_exists?(repo, tag_name) + raise Installations::Error.new("Should not create a tag", reason: :tag_reference_already_exists) if branch_name.present? && tag_exists?(repo, tag_name) @client.create_release(repo, tag_name, options) end end diff --git a/app/libs/triggers/pre_release.rb b/app/libs/triggers/pre_release.rb index a6b4d90e3..aa0cc3f7b 100644 --- a/app/libs/triggers/pre_release.rb +++ b/app/libs/triggers/pre_release.rb @@ -4,7 +4,8 @@ class Triggers::PreRelease RELEASE_HANDLERS = { "almost_trunk" => AlmostTrunk, "parallel_working" => ParallelBranches, - "release_backmerge" => ReleaseBackMerge + "release_backmerge" => ReleaseBackMerge, + "trunk" => Trunk } def self.call(release) diff --git a/app/libs/triggers/pre_release/trunk.rb b/app/libs/triggers/pre_release/trunk.rb new file mode 100644 index 000000000..1cd69545c --- /dev/null +++ b/app/libs/triggers/pre_release/trunk.rb @@ -0,0 +1,17 @@ +class Triggers::PreRelease::Trunk + def self.call(release, release_branch) + new(release, release_branch).call + end + + def initialize(release, release_branch) + @release = release + @release_branch = release_branch + end + + def call + GitHub::Result.new do + latest_commit = @release.latest_commit_hash(sha_only: false) + Coordinators::Signals.commits_have_landed!(@release, latest_commit, []) + end + end +end diff --git a/app/models/build_queue.rb b/app/models/build_queue.rb index 1adf6420a..6dfc16742 100644 --- a/app/models/build_queue.rb +++ b/app/models/build_queue.rb @@ -28,6 +28,7 @@ def add_commit!(commit, can_apply: true) end def schedule_kickoff! + return if train.trunk? BuildQueueApplicationJob.set(wait_until: scheduled_at).perform_later(id) end diff --git a/app/models/commit.rb b/app/models/commit.rb index b9712a5c5..b73e4fbb7 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -10,6 +10,7 @@ # commit_hash :string not null, indexed => [release_id] # message :string # parents :jsonb +# tag_name :string # timestamp :datetime not null, indexed => [release_id] # url :string # created_at :datetime not null @@ -23,6 +24,7 @@ class Commit < ApplicationRecord has_paper_trail include Passportable include Commitable + include Taggable self.implicit_order_column = :timestamp @@ -34,13 +36,13 @@ class Commit < ApplicationRecord scope :sequential, -> { order(timestamp: :desc) } - STAMPABLE_REASONS = ["created"] + STAMPABLE_REASONS = %w[created tag_created] validates :commit_hash, uniqueness: {scope: :release_id} after_commit -> { create_stamp!(data: {sha: short_sha}) }, on: :create - delegate :release_platform_runs, :notify!, :train, :platform, to: :release + delegate :release_platform_runs, :notify!, :train, :platform, :base_tag_name, to: :release def self.commit_messages(first_parent_only = false) Commit.commit_log(reorder("timestamp DESC"), first_parent_only)&.map(&:message) @@ -77,6 +79,17 @@ def self.between_commits(base_commit, head_commit) end end + # recursively attempt to create a release tag until a unique one gets created + # it *can* get expensive in the worst-case scenario, so ideally invoke this in a bg job + def create_tag!(input_tag_name = base_tag_name) + train.create_tag!(input_tag_name, commit_hash) + update!(tag_name: input_tag_name) + event_stamp!(reason: :tag_created, kind: :notice, data: {tag: tag_name, commit_sha: short_sha, commit_url: url}) + rescue Installations::Error => ex + raise unless ex.reason == :tag_reference_already_exists + create_tag!(unique_tag_name(input_tag_name, short_sha)) + end + def team user&.team_for(release.organization) end diff --git a/app/models/concerns/commitable.rb b/app/models/concerns/commitable.rb index 7954dbac1..27f36f24e 100644 --- a/app/models/concerns/commitable.rb +++ b/app/models/concerns/commitable.rb @@ -63,6 +63,8 @@ def pull_request = nil def backmerge_failure? = nil + def tag_name = nil + def eql?(other) commit_hash == other.commit_hash end diff --git a/app/models/concerns/taggable.rb b/app/models/concerns/taggable.rb index 52270d525..4f5f26cc8 100644 --- a/app/models/concerns/taggable.rb +++ b/app/models/concerns/taggable.rb @@ -14,4 +14,9 @@ def unique_tag_name(currently, sha) return [base_tag_name, "-", sha].join if currently.end_with?(base_tag_name) [base_tag_name, "-", sha, "-", Time.now.to_i].join end + + def tag_url + return if tag_name.blank? + train.vcs_provider&.tag_url(tag_name) + end end diff --git a/app/models/release.rb b/app/models/release.rb index d788cbfd1..b1f823fbd 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -287,7 +287,8 @@ def version_ahead?(platform_run) end def create_build_queue! - build_queues.create!(scheduled_at: (Time.current + train.build_queue_wait_time), is_active: true) + wait_time = train.build_queue_wait_time || 0 + build_queues.create!(scheduled_at: (Time.current + wait_time), is_active: true) end def applied_commits @@ -361,14 +362,23 @@ def release_branch def create_vcs_release!(input_tag_name = base_tag_name) return unless train.tag_releases? return if tag_name.present? - train.create_vcs_release!(release_branch, input_tag_name, release_diff) - update!(tag_name: input_tag_name) - event_stamp!(reason: :vcs_release_created, kind: :notice, data: {provider: vcs_provider.display, tag: tag_name}) + + train.create_vcs_release!(input_tag_name, release_branch, release_diff) + on_tag_create!(input_tag_name) rescue Installations::Error => ex raise unless [:tag_reference_already_exists, :tagged_release_already_exists].include?(ex.reason) create_vcs_release!(unique_tag_name(input_tag_name, last_commit.short_sha)) end + def create_release_from_tag!(existing_tag) + return unless train.tag_releases? + return if tag_name.present? + return if existing_tag.blank? + + train.create_vcs_release!(existing_tag, nil, release_diff) + on_tag_create!(existing_tag) + end + def release_diff changes_since_last_release = release_changelog&.commit_messages(true) changes_since_last_run = all_commits.commit_messages(true) @@ -387,10 +397,6 @@ def branch_url train.vcs_provider&.branch_url(release_branch) end - def tag_url - train.vcs_provider&.tag_url(tag_name) - end - def pull_requests_url(open = false) train.vcs_provider&.pull_requests_url(branch_name, open:) end @@ -555,8 +561,6 @@ def copy_approvals_allowed? train.previously_finished_release.present? && !hotfix? end - private - def base_tag_name tag = "v#{release_version}" tag = train.tag_prefix + "-" + tag if train.tag_prefix.present? @@ -565,6 +569,13 @@ def base_tag_name tag end + private + + def on_tag_create!(tag) + update!(tag_name: tag) + event_stamp!(reason: :vcs_release_created, kind: :notice, data: {provider: vcs_provider.display, tag: tag_name}) + end + def create_platform_runs! release_platforms.each do |release_platform| next if hotfix? && hotfix_platform.present? && hotfix_platform != release_platform.platform diff --git a/app/models/release_platform_run.rb b/app/models/release_platform_run.rb index a848294a6..1499a5151 100644 --- a/app/models/release_platform_run.rb +++ b/app/models/release_platform_run.rb @@ -302,16 +302,12 @@ def temporary_unblock_upcoming? Flipper.enabled?(:temporary_unblock_upcoming, self) end - def tag_url - train.vcs_provider&.tag_url(tag_name) - end - # recursively attempt to create a release tag until a unique one gets created # it *can* get expensive in the worst-case scenario, so ideally invoke this in a bg job def create_tag!(commit, input_tag_name = base_tag_name) train.create_tag!(input_tag_name, commit.commit_hash) update!(tag_name: input_tag_name) - event_stamp!(reason: :tag_created, kind: :notice, data: {tag: tag_name}) + event_stamp!(reason: :tag_created, kind: :notice, data: {tag: tag_name, commit_sha: commit.short_sha, commit_url: commit.url}) rescue Installations::Error => ex raise unless ex.reason == :tag_reference_already_exists create_tag!(commit, unique_tag_name(input_tag_name, commit.short_sha)) @@ -330,6 +326,7 @@ def create_tag!(commit, input_tag_name = base_tag_name) # Patch fix commit: no bump required # -- def version_bump_required? + return false if train.trunk? latest_production_release&.version_bump_required? end diff --git a/app/models/train.rb b/app/models/train.rb index 9bb0a37d3..dcaa9b090 100644 --- a/app/models/train.rb +++ b/app/models/train.rb @@ -50,6 +50,7 @@ class Train < ApplicationRecord self.ignored_columns += ["manual_release"] BRANCHING_STRATEGIES = { + trunk: "Trunk", almost_trunk: "Almost Trunk", release_backmerge: "Release with Backmerge", parallel_working: "Parallel Working and Release" @@ -106,6 +107,8 @@ class Train < ApplicationRecord after_initialize :set_backmerge_config, if: :persisted? after_initialize :set_notifications_config, if: :persisted? before_validation :set_version_seeded_with, if: :new_record? + before_create :set_release_branch, if: :trunk? + before_create :set_build_queue_values, if: :trunk? before_create :set_ci_cd_workflows before_create :set_current_version before_create :set_default_status @@ -193,6 +196,7 @@ def automatic? end def tag_platform_at_release_end? + return false if trunk? return false unless app.cross_platform? tag_platform_releases? && !tag_all_store_releases? end @@ -368,11 +372,19 @@ def almost_trunk? branching_strategy == "almost_trunk" end + def trunk? + branching_strategy == "trunk" + end + def backmerge_disabled? !almost_trunk? end - def create_vcs_release!(branch_name, tag_name, release_diff = nil) + def tag_applied_commits? + trunk? + end + + def create_vcs_release!(tag_name, branch_name = nil, release_diff = nil) return false unless active? vcs_provider.create_release!(tag_name, branch_name, release_diff) end @@ -488,6 +500,11 @@ def set_release_schedule end def set_build_queue_config + if trunk? + self.build_queue_wait_time_unit = "hours" + self.build_queue_wait_time_value = 0 + return + end return if build_queue_wait_time.blank? parts = build_queue_wait_time.parts self.build_queue_wait_time_unit = parts.keys.first.to_s @@ -498,6 +515,15 @@ def set_backmerge_config self.continuous_backmerge_enabled = continuous_backmerge? end + def set_release_branch + self.release_branch = working_branch + end + + def set_build_queue_values + self.build_queue_size = 0 + self.build_queue_enabled = true + end + def set_notifications_config self.notifications_enabled = send_notifications? end @@ -518,7 +544,7 @@ def set_version_seeded_with end def set_branching_strategy - self.branching_strategy ||= "almost_trunk" + self.branching_strategy ||= "trunk" end def set_current_version @@ -558,6 +584,7 @@ def ci_cd_workflows_presence end def build_queue_config + return if trunk? if build_queue_enabled? errors.add(:build_queue_size, :config_required) unless build_queue_size.present? && build_queue_wait_time.present? errors.add(:build_queue_size, :invalid_size) if build_queue_size && build_queue_size < 1 diff --git a/app/models/workflow_run.rb b/app/models/workflow_run.rb index b8bfd0961..01f38980c 100644 --- a/app/models/workflow_run.rb +++ b/app/models/workflow_run.rb @@ -202,7 +202,6 @@ def notification_params def trigger_external_run! deploy_action_enabled = organization.deploy_action_enabled? || app.deploy_action_enabled? || train.deploy_action_enabled? - ci_cd_provider .trigger_workflow_run!(conf.identifier, release_branch, workflow_inputs, commit_hash, deploy_action_enabled) .then { |wr| update_external_metadata!(wr) } diff --git a/app/views/trains/_build_queue_form.html.erb b/app/views/trains/_build_queue_form.html.erb index 0b9c34df3..3ba993237 100644 --- a/app/views/trains/_build_queue_form.html.erb +++ b/app/views/trains/_build_queue_form.html.erb @@ -1,11 +1,15 @@ -