diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index adc079346..2282a2978 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -63,7 +63,7 @@ def set_scheme def scheme_params @scheme_params ||= params.require(:scheme) - .permit(:name, :new_build_callout, channel_attributes: { name: [] }) + .permit(:name, :new_build_callout, :retained_builds, channel_attributes: { name: [] }) end def process_scheme_params diff --git a/app/jobs/retained_builds_job.rb b/app/jobs/retained_builds_job.rb new file mode 100644 index 000000000..f36efb8e8 --- /dev/null +++ b/app/jobs/retained_builds_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class RetainedBuildsJob < ApplicationJob + queue_as :schedule + + def perform(channel) + retained_builds = channel.scheme.retained_builds + return if retained_builds <= 0 + return if channel.releases.count <= retained_builds + + offset_release = channel.releases.select(:id).limit(1).offset(retained_builds).order(id: :desc).take + return if offset_release.blank? + + channel.releases.destroy_by("id <= ?", offset_release.id) + end +end diff --git a/app/models/release.rb b/app/models/release.rb index 9376f12ca..ab58a6f22 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -28,6 +28,8 @@ class Release < ApplicationRecord before_save :convert_custom_fields before_save :strip_branch + after_create :retained_build_job + delegate :scheme, to: :channel delegate :app, to: :scheme @@ -265,4 +267,8 @@ def enabled_validate_bundle_id? bundle_id = channel.bundle_id !(bundle_id.blank? || bundle_id == '*') end + + def retained_build_job + RetainedBuildsJob.perform_later(channel) + end end diff --git a/app/views/apps/show.html.slim b/app/views/apps/show.html.slim index 3ba63dd72..5821e658c 100644 --- a/app/views/apps/show.html.slim +++ b/app/views/apps/show.html.slim @@ -26,9 +26,15 @@ ruby: h3.card-title = scheme.name .card-tools + - if scheme.retained_builds > 0 + span.badge.badge-success data-toggle="tooltip" title="#{t('.retained_build')}" style="margin-right: 4px" + = scheme.retained_builds - if !scheme.new_build_callout - span.badge.rounded-pill.text-bg-secondary - i.fa.fa-bell-slash.text-success title="新上传版本提示窗" data-toggle="tooltip" + span.badge.badge-warning title="#{t('.disabled_new_build_callout')}" data-toggle="tooltip" + i.fa.fa-bell-slash + - else + span.badge.badge-success title="#{t('.enabled_new_build_callout')}" data-toggle="tooltip" + i.fa.fa-bell .card-body.p-0 section.app-section id="scheme-#{scheme.id}" table.table diff --git a/app/views/schemes/_form.html.slim b/app/views/schemes/_form.html.slim index 894972b9e..1851c601b 100644 --- a/app/views/schemes/_form.html.slim +++ b/app/views/schemes/_form.html.slim @@ -15,6 +15,7 @@ ruby: = simple_form_for(@scheme, url: form_url) do |f| = f.error_notification = f.input :name, required: true + = f.input :retained_builds = f.input :new_build_callout, value: true - if new_or_create_route? diff --git a/config/locales/simple_form/simple_form.en.yml b/config/locales/simple_form/simple_form.en.yml index 9aed30207..3be01a67d 100644 --- a/config/locales/simple_form/simple_form.en.yml +++ b/config/locales/simple_form/simple_form.en.yml @@ -27,6 +27,7 @@ en: scheme: name: 'Scheme name' new_build_callout: New build callout + retained_builds: Retained builds channel: name: 'Channel name' device_type: 'Device type' @@ -93,7 +94,8 @@ en: name: :simple_form.hints.scheme.name scheme: name: 'Apply the type of feature to target audience. for example, adhoc, test, production etc.' - new_build_callout: 'Display a new callout at the top of the page when visiting a previous build of app' + new_build_callout: 'Display a new callout at the top of the page when visiting a previous build of app.' + retained_builds: 'Set the maximum retained versions, the earlier versions will be deleted automatically if exceeded, the default is 0 which means to retain all versions.' channel: name: 'Recommended distinguishing different channels according to the application platform, single platform applications can also be the name of the distribution market.' device_type: 'Which device type of app. for example, iOS, Android, macOS etc.' diff --git a/config/locales/simple_form/simple_form.zh-CN.yml b/config/locales/simple_form/simple_form.zh-CN.yml index 071f20ced..e02f8c648 100644 --- a/config/locales/simple_form/simple_form.zh-CN.yml +++ b/config/locales/simple_form/simple_form.zh-CN.yml @@ -9,31 +9,32 @@ zh-CN: # When using html, text and mark won't be used. # html: '*' error_notification: - default_message: "请检查如下问题:" + default_message: '请检查如下问题:' labels: defaults: - name: '名称' - password: '密码' - channel: '渠道名称' + name: 名称 + password: 密码 + channel: 渠道名称 user: - username: '昵称' - email: '登录邮箱' - password: '登录密码' - password_confirmation: '密码确认' - role: '账户权限' - remember_me: '记住登录信息' + username: 昵称 + email: 登录邮箱 + password: 登录密码 + password_confirmation: 密码确认 + role: 账户权限 + remember_me: 记住登录信息 app: - name: '应用名称' + name: 应用名称 scheme: - name: '类型名称' + name: 类型名称 new_build_callout: 新上传版本提示窗 + retained_builds: 保留版本数 channel: - name: '渠道名称' - device_type: '应用平台' - bundle_id: '校验包名' - git_url: 'Git 仓库地址' - slug: '唯一地址' - password: '访问密码' + name: 渠道名称 + device_type: 应用平台 + bundle_id: 校验包名 + git_url: Git 仓库地址 + slug: 唯一地址 + password: 访问密码 release: file: 应用文件 release_version: 产品版本 @@ -44,19 +45,19 @@ zh-CN: git_commit: Git 最后提交 SHA ci_url: CI URL web_hook: - url: 'URL' - body: '自定义消息体' - upload_events: '上传事件' - download_events: '下载事件' - changelog_events: '日志变更事件' + url: URL + body: 自定义消息体 + upload_events: 上传事件 + download_events: 下载事件 + changelog_events: 日志变更事件 channels: 启用的应用渠道 debug_file: - app_id: '应用' - device_type: '应用平台' - release_version: '发布版本' - build_version: '构建版本' - file: '调试文件' - checksum: '唯一校验码' + app_id: 应用 + device_type: 应用平台 + release_version: 发布版本 + build_version: 构建版本 + file: 调试文件 + checksum: 唯一校验码 setting: value: 值 apple_key: @@ -87,20 +88,21 @@ zh-CN: hints: defaults: - channel: '应用会使用的平台' + channel: 应用会使用的平台 app: schemes: name: :'simple_form.hints.scheme.name' scheme: - name: '应用在功能、面向受众划分的类型' - new_build_callout: '用于浏览不是最新上传版本详情时在页面顶部显示一个新上传版本提示窗' + name: 应用在功能、面向受众划分的类型 + new_build_callout: 用于浏览不是最新上传版本详情时在页面顶部显示一个新上传版本提示窗 + retained_builds: 设置最大保留版本,超过的会自动删除早期的版本,默认为 0 表示保留所有版本 channel: - name: '推荐按照应用平台区分不同渠道,单平台应用也可以是分发市场的名称' - device_type: '应用设备类型' - bundle_id: '校验应用 bundle id (package name),为空或 * 为不校验' - git_url: 'Git 项目地址,填写 Github 或 Gitlab 或其他自建地址' - slug: 'URL 的唯一标识' - password: '设置后对非登录用户会要求输入密码' + name: 推荐按照应用平台区分不同渠道,单平台应用也可以是分发市场的名称 + device_type: 应用设备类型 + bundle_id: 校验应用 bundle id (package name),为空或 * 为不校验 + git_url: Git 项目地址,填写 Github 或 Gitlab 或其他自建地址 + slug: URL 的唯一标识 + password: 设置后对非登录用户会要求输入密码 release: file: 支持 iOS、Android、macOS、Windows、Linux 各种应用类型的文件 release_version: 应用对外公布的主版本号,推荐使用 X.Y.Z 语义化版本命名 @@ -113,8 +115,8 @@ zh-CN: web_hook: body: 自定义的 JSON 消息体以满足不同第三方服务消息体的要求,不填写会使用默认结构 debug_file: - device_type: '应用设备类型' - file: '必须使用 zip 文件压缩后的调试文件' + device_type: 应用设备类型 + file: 必须使用 zip 文件压缩后的调试文件 backup: key: 名称必须是唯一,不可和其他备份重复 schedule: | diff --git a/config/locales/zealot/en.yml b/config/locales/zealot/en.yml index 4e6dadfbf..57818bd5a 100644 --- a/config/locales/zealot/en.yml +++ b/config/locales/zealot/en.yml @@ -272,6 +272,9 @@ en: new_channel: New Channel edit_scheme: Edit Scheme destory_scheme: Destroy Scheme + retained_build: Retained builds + disabled_new_build_callout: Disabled new build callout + enabled_new_build_callout: Enabled new build callout not_found_channel_body_html: | Not found any channel. Click the button to new a channel. not_found_scheme: diff --git a/config/locales/zealot/zh-CN.yml b/config/locales/zealot/zh-CN.yml index 50d48aff5..f9ecbe2c9 100644 --- a/config/locales/zealot/zh-CN.yml +++ b/config/locales/zealot/zh-CN.yml @@ -267,6 +267,9 @@ zh-CN: new_channel: 新增渠道 edit_scheme: 编辑类型 destory_scheme: 删除类型 + retained_build: 最大版本保留数 + disabled_new_build_callout: 已关闭新版本提示窗 + enabled_new_build_callout: 已开启新版本提示窗 not_found_channel_body_html: | 没有发现任何渠道,选择右上角的 新增渠道 not_found_scheme: diff --git a/db/migrate/20240223040132_append_retained_builds_to_scheme.rb b/db/migrate/20240223040132_append_retained_builds_to_scheme.rb new file mode 100644 index 000000000..569bf9195 --- /dev/null +++ b/db/migrate/20240223040132_append_retained_builds_to_scheme.rb @@ -0,0 +1,5 @@ +class AppendRetainedBuildsToScheme < ActiveRecord::Migration[7.1] + def change + add_column :schemes, :retained_builds, :integer, default: 0, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f929fac00..e954c55b0 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.1].define(version: 2023_08_03_065502) do +ActiveRecord::Schema[7.1].define(version: 2024_02_23_040132) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -141,6 +141,83 @@ t.index ["release_id", "device_id"], name: "index_devices_releases_on_release_id_and_device_id" end + create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description" + t.jsonb "serialized_properties" + t.text "on_finish" + t.text "on_success" + t.text "on_discard" + t.text "callback_queue_name" + t.integer "callback_priority" + t.datetime "enqueued_at" + t.datetime "discarded_at" + t.datetime "finished_at" + end + + create_table "good_job_executions", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id", null: false + t.text "job_class" + t.text "queue_name" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.text "error" + t.integer "error_event", limit: 2 + t.index ["active_job_id", "created_at"], name: "index_good_job_executions_on_active_job_id_and_created_at" + end + + create_table "good_job_processes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.jsonb "state" + end + + create_table "good_job_settings", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "key" + t.jsonb "value" + t.index ["key"], name: "index_good_job_settings_on_key", unique: true + end + + create_table "good_jobs", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.text "queue_name" + t.integer "priority" + t.jsonb "serialized_params" + t.datetime "scheduled_at" + t.datetime "performed_at" + t.datetime "finished_at" + t.text "error" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.uuid "active_job_id" + t.text "concurrency_key" + t.text "cron_key" + t.uuid "retried_good_job_id" + t.datetime "cron_at" + t.uuid "batch_id" + t.uuid "batch_callback_id" + t.boolean "is_discrete" + t.integer "executions_count" + t.text "job_class" + t.integer "error_event", limit: 2 + t.index ["active_job_id", "created_at"], name: "index_good_jobs_on_active_job_id_and_created_at" + t.index ["active_job_id"], name: "index_good_jobs_on_active_job_id" + t.index ["batch_callback_id"], name: "index_good_jobs_on_batch_callback_id", where: "(batch_callback_id IS NOT NULL)" + t.index ["batch_id"], name: "index_good_jobs_on_batch_id", where: "(batch_id IS NOT NULL)" + t.index ["concurrency_key"], name: "index_good_jobs_on_concurrency_key_when_unfinished", where: "(finished_at IS NULL)" + t.index ["cron_key", "created_at"], name: "index_good_jobs_on_cron_key_and_created_at" + t.index ["cron_key", "cron_at"], name: "index_good_jobs_on_cron_key_and_cron_at", unique: true + t.index ["finished_at"], name: "index_good_jobs_jobs_on_finished_at", where: "((retried_good_job_id IS NULL) AND (finished_at IS NOT NULL))" + t.index ["priority", "created_at"], name: "index_good_jobs_jobs_on_priority_created_at_when_unfinished", order: { priority: "DESC NULLS LAST" }, where: "(finished_at IS NULL)" + t.index ["queue_name", "scheduled_at"], name: "index_good_jobs_on_queue_name_and_scheduled_at", where: "(finished_at IS NULL)" + t.index ["scheduled_at"], name: "index_good_jobs_on_scheduled_at", where: "(finished_at IS NULL)" + end + create_table "metadata", force: :cascade do |t| t.bigint "release_id" t.bigint "user_id" @@ -205,6 +282,7 @@ t.bigint "app_id" t.string "name", null: false t.boolean "new_build_callout", default: true + t.integer "retained_builds", default: 0, null: false t.index ["app_id"], name: "index_schemes_on_app_id" t.index ["name"], name: "index_schemes_on_name" end @@ -217,6 +295,13 @@ t.index ["var"], name: "index_settings_on_var", unique: true end + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", null: false + t.binary "value", null: false + t.datetime "created_at", null: false + t.index ["key"], name: "index_solid_cache_entries_on_key", unique: true + end + create_table "user_providers", force: :cascade do |t| t.bigint "user_id" t.string "name"