summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js1
-rw-r--r--app/finders/projects/export_job_finder.rb29
-rw-r--r--app/models/ci/bridge.rb1
-rw-r--r--app/models/project.rb23
-rw-r--r--app/models/project_export_job.rb26
-rw-r--r--app/services/ci/create_cross_project_pipeline_service.rb5
-rw-r--r--app/services/ci/pipeline_bridge_status_service.rb9
-rw-r--r--app/workers/all_queues.yml7
-rw-r--r--app/workers/concerns/project_export_options.rb25
-rw-r--r--app/workers/project_export_worker.rb9
-rw-r--r--app/workers/stuck_export_jobs_worker.rb54
-rw-r--r--changelogs/unreleased/204774-quick-actions-executed-in-multiline-inline-code.yml5
-rw-r--r--changelogs/unreleased/26712-Update-GitLab-codeclimate-to-head.yml5
-rw-r--r--changelogs/unreleased/fix-export-state-logic.yml5
-rw-r--r--changelogs/unreleased/fix-mermaid-flow-chart-width.yml5
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--db/migrate/20200311165635_create_project_export_jobs.rb19
-rw-r--r--db/schema.rb15
-rw-r--r--doc/administration/integration/plantuml.md4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json83
-rw-r--r--doc/api/project_import_export.md16
-rw-r--r--doc/ci/jenkins/index.md21
-rw-r--r--doc/development/contributing/issue_workflow.md4
-rw-r--r--doc/development/documentation/styleguide.md12
-rw-r--r--doc/user/application_security/img/outdated_report_branch_v12_9.pngbin0 -> 15172 bytes
-rw-r--r--doc/user/application_security/img/outdated_report_pipeline_v12_9.pngbin0 -> 16694 bytes
-rw-r--r--doc/user/application_security/index.md29
-rw-r--r--doc/user/asciidoc.md4
-rw-r--r--doc/user/markdown.md42
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/quick_actions/extractor.rb11
-rw-r--r--lib/tasks/gitlab/graphql.rake19
-rw-r--r--locale/gitlab.pot4
-rw-r--r--package.json2
-rw-r--r--spec/controllers/projects_controller_spec.rb6
-rw-r--r--spec/factories/project_export_jobs.rb8
-rw-r--r--spec/features/markdown/mermaid_spec.rb27
-rw-r--r--spec/finders/projects/export_job_finder_spec.rb51
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/project/export_status.json3
-rw-r--r--spec/frontend/blob/blob_file_dropzone_spec.js50
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js39
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb27
-rw-r--r--spec/models/project_export_job_spec.rb19
-rw-r--r--spec/models/project_spec.rb86
-rw-r--r--spec/requests/api/project_export_spec.rb53
-rw-r--r--spec/services/ci/create_cross_project_pipeline_service_spec.rb20
-rw-r--r--spec/services/ci/pipeline_bridge_status_service_spec.rb18
-rw-r--r--spec/workers/concerns/project_export_options_spec.rb41
-rw-r--r--spec/workers/project_export_worker_spec.rb46
-rw-r--r--spec/workers/stuck_export_jobs_worker_spec.rb75
-rw-r--r--yarn.lock18
55 files changed, 960 insertions, 141 deletions
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 688cfb5033e..6437f40f29f 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -20,7 +20,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
- CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/security-products/codequality:0.85.6"
+ CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/security-products/codequality:0.85.9"
script:
- |
if ! docker info &>/dev/null; then
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 3856832de90..b5e17a0587d 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -35,6 +35,7 @@ function renderMermaids($els) {
// mermaidAPI options
theme: 'neutral',
flowchart: {
+ useMaxWidth: true,
htmlLabels: false,
},
securityLevel: 'strict',
diff --git a/app/finders/projects/export_job_finder.rb b/app/finders/projects/export_job_finder.rb
new file mode 100644
index 00000000000..c26a7a3f1a6
--- /dev/null
+++ b/app/finders/projects/export_job_finder.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Projects
+ class ExportJobFinder
+ InvalidExportJobStatusError = Class.new(StandardError)
+ attr_reader :project, :params
+
+ def initialize(project, params = {})
+ @project = project
+ @params = params
+ end
+
+ def execute
+ export_jobs = project.export_jobs
+ export_jobs = by_status(export_jobs)
+
+ export_jobs
+ end
+
+ private
+
+ def by_status(export_jobs)
+ return export_jobs unless params[:status]
+ raise InvalidExportJobStatusError, 'Invalid export job status' unless ProjectExportJob.state_machines[:status].states.map(&:name).include?(params[:status])
+
+ export_jobs.with_status(params[:status])
+ end
+ end
+end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index d62aa09e432..955b16ad50d 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -10,6 +10,7 @@ module Ci
include HasRef
InvalidBridgeTypeError = Class.new(StandardError)
+ InvalidTransitionError = Class.new(StandardError)
belongs_to :project
belongs_to :trigger_request
diff --git a/app/models/project.rb b/app/models/project.rb
index daae6544ad5..890768ccf58 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -186,6 +186,7 @@ class Project < ApplicationRecord
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :export_jobs, class_name: 'ProjectExportJob'
has_one :project_repository, inverse_of: :project
has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
@@ -1850,10 +1851,12 @@ class Project < ApplicationRecord
end
def export_status
- if export_in_progress?
+ if regeneration_in_progress?
+ :regeneration_in_progress
+ elsif export_enqueued?
+ :queued
+ elsif export_in_progress?
:started
- elsif after_export_in_progress?
- :after_export_action
elsif export_file_exists?
:finished
else
@@ -1862,11 +1865,19 @@ class Project < ApplicationRecord
end
def export_in_progress?
- import_export_shared.active_export_count > 0
+ strong_memoize(:export_in_progress) do
+ ::Projects::ExportJobFinder.new(self, { status: :started }).execute.present?
+ end
+ end
+
+ def export_enqueued?
+ strong_memoize(:export_enqueued) do
+ ::Projects::ExportJobFinder.new(self, { status: :queued }).execute.present?
+ end
end
- def after_export_in_progress?
- import_export_shared.after_export_in_progress?
+ def regeneration_in_progress?
+ (export_enqueued? || export_in_progress?) && export_file_exists?
end
def remove_exports
diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb
new file mode 100644
index 00000000000..c7fe3d7bc10
--- /dev/null
+++ b/app/models/project_export_job.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class ProjectExportJob < ApplicationRecord
+ belongs_to :project
+
+ validates :project, :jid, :status, presence: true
+
+ state_machine :status, initial: :queued do
+ event :start do
+ transition [:queued] => :started
+ end
+
+ event :finish do
+ transition [:started] => :finished
+ end
+
+ event :fail_op do
+ transition [:queued, :started] => :failed
+ end
+
+ state :queued, value: 0
+ state :started, value: 1
+ state :finished, value: 2
+ state :failed, value: 3
+ end
+end
diff --git a/app/services/ci/create_cross_project_pipeline_service.rb b/app/services/ci/create_cross_project_pipeline_service.rb
index 99f232bc892..3a2cc3f9d32 100644
--- a/app/services/ci/create_cross_project_pipeline_service.rb
+++ b/app/services/ci/create_cross_project_pipeline_service.rb
@@ -52,6 +52,11 @@ module Ci
subject.drop!(:downstream_pipeline_creation_failed)
end
end
+ rescue StateMachines::InvalidTransition => e
+ Gitlab::ErrorTracking.track_exception(
+ Ci::Bridge::InvalidTransitionError.new(e.message),
+ bridge_id: bridge.id,
+ downstream_pipeline_id: pipeline.id)
end
def ensure_preconditions!(target_ref)
diff --git a/app/services/ci/pipeline_bridge_status_service.rb b/app/services/ci/pipeline_bridge_status_service.rb
index 19ed5026a3a..e2e5dd386f2 100644
--- a/app/services/ci/pipeline_bridge_status_service.rb
+++ b/app/services/ci/pipeline_bridge_status_service.rb
@@ -5,7 +5,14 @@ module Ci
def execute(pipeline)
return unless pipeline.bridge_triggered?
- pipeline.source_bridge.inherit_status_from_downstream!(pipeline)
+ begin
+ pipeline.source_bridge.inherit_status_from_downstream!(pipeline)
+ rescue StateMachines::InvalidTransition => e
+ Gitlab::ErrorTracking.track_exception(
+ Ci::Bridge::InvalidTransitionError.new(e.message),
+ bridge_id: pipeline.source_bridge.id,
+ downstream_pipeline_id: pipeline.id)
+ end
end
end
end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 890f38aa26b..71f7c1bac3c 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -234,6 +234,13 @@
:resource_boundary: :cpu
:weight: 1
:idempotent:
+- :name: cronjob:stuck_export_jobs
+ :feature_category: :importers
+ :has_external_dependencies:
+ :urgency: :default
+ :resource_boundary: :cpu
+ :weight: 1
+ :idempotent:
- :name: cronjob:stuck_import_jobs
:feature_category: :importers
:has_external_dependencies:
diff --git a/app/workers/concerns/project_export_options.rb b/app/workers/concerns/project_export_options.rb
new file mode 100644
index 00000000000..e9318c1ba43
--- /dev/null
+++ b/app/workers/concerns/project_export_options.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module ProjectExportOptions
+ extend ActiveSupport::Concern
+
+ EXPORT_RETRY_COUNT = 3
+
+ included do
+ sidekiq_options retry: EXPORT_RETRY_COUNT, status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+
+ # We mark the project export as failed once we have exhausted all retries
+ sidekiq_retries_exhausted do |job|
+ project = Project.find(job['args'][1])
+ # rubocop: disable CodeReuse/ActiveRecord
+ job = project.export_jobs.find_by(jid: job["jid"])
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ if job&.fail_op
+ Sidekiq.logger.info "Job #{job['jid']} for project #{project.id} has been set to failed state"
+ else
+ Sidekiq.logger.error "Failed to set Job #{job['jid']} for project #{project.id} to failed state"
+ end
+ end
+ end
+end
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index eefba6d25c7..aaaf70f09b5 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -3,17 +3,24 @@
class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
include ExceptionBacktrace
+ include ProjectExportOptions
- sidekiq_options retry: 3
feature_category :importers
worker_resource_boundary :memory
def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
project = Project.find(project_id)
+ export_job = project.export_jobs.safe_find_or_create_by(jid: self.jid)
after_export = build!(after_export_strategy)
+ export_job&.start
+
::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
+
+ export_job&.finish
+ rescue ActiveRecord::RecordNotFound, Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError => e
+ logger.error("Failed to export project #{project_id}: #{e.message}")
end
private
diff --git a/app/workers/stuck_export_jobs_worker.rb b/app/workers/stuck_export_jobs_worker.rb
new file mode 100644
index 00000000000..6d8d60d2fc0
--- /dev/null
+++ b/app/workers/stuck_export_jobs_worker.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+# rubocop:disable Scalability/IdempotentWorker
+class StuckExportJobsWorker
+ include ApplicationWorker
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker updates export states inline and does not schedule
+ # other jobs.
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ feature_category :importers
+ worker_resource_boundary :cpu
+
+ EXPORT_JOBS_EXPIRATION = 6.hours.to_i
+
+ def perform
+ failed_jobs_count = mark_stuck_jobs_as_failed!
+
+ Gitlab::Metrics.add_event(:stuck_export_jobs,
+ failed_jobs_count: failed_jobs_count)
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def mark_stuck_jobs_as_failed!
+ jids_and_ids = enqueued_exports.pluck(:jid, :id).to_h
+
+ completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
+ return unless completed_jids.any?
+
+ completed_ids = jids_and_ids.values_at(*completed_jids)
+
+ # We select the export states again, because they may have transitioned from
+ # started to finished while we were looking up their Sidekiq status.
+ completed_jobs = enqueued_exports.where(id: completed_ids)
+
+ Sidekiq.logger.info(
+ message: 'Marked stuck export jobs as failed',
+ job_ids: completed_jobs.map(&:jid)
+ )
+
+ completed_jobs.each do |job|
+ job.fail_op
+ end.count
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def enqueued_exports
+ ProjectExportJob.with_status([:started, :queued])
+ end
+end
+# rubocop:enable Scalability/IdempotentWorker
diff --git a/changelogs/unreleased/204774-quick-actions-executed-in-multiline-inline-code.yml b/changelogs/unreleased/204774-quick-actions-executed-in-multiline-inline-code.yml
new file mode 100644
index 00000000000..d626875a47d
--- /dev/null
+++ b/changelogs/unreleased/204774-quick-actions-executed-in-multiline-inline-code.yml
@@ -0,0 +1,5 @@
+---
+title: Fix quick actions executing in multiline inline code when placed on its own line
+merge_request: 24933
+author: Pavlo Dudchenko
+type: fixed
diff --git a/changelogs/unreleased/26712-Update-GitLab-codeclimate-to-head.yml b/changelogs/unreleased/26712-Update-GitLab-codeclimate-to-head.yml
new file mode 100644
index 00000000000..8c9aca822d4
--- /dev/null
+++ b/changelogs/unreleased/26712-Update-GitLab-codeclimate-to-head.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab's codeclimate to 0.85.9
+merge_request: 26712
+author: Eddie Stubbington
+type: other
diff --git a/changelogs/unreleased/fix-export-state-logic.yml b/changelogs/unreleased/fix-export-state-logic.yml
new file mode 100644
index 00000000000..7b4bc2186a0
--- /dev/null
+++ b/changelogs/unreleased/fix-export-state-logic.yml
@@ -0,0 +1,5 @@
+---
+title: Fix logic to determine project export state and add regeneration_in_progress state
+merge_request: 23664
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-mermaid-flow-chart-width.yml b/changelogs/unreleased/fix-mermaid-flow-chart-width.yml
new file mode 100644
index 00000000000..20258d40728
--- /dev/null
+++ b/changelogs/unreleased/fix-mermaid-flow-chart-width.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Mermaid flowchart width
+merge_request: 26848
+author: julien MILLAU
+type: fixed
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index fdf3ec67be7..f22ddc7f081 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -453,6 +453,9 @@ Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'Rem
Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *'
Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker'
+Settings.cron_jobs['stuck_export_jobs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['stuck_export_jobs_worker']['cron'] ||= '30 * * * *'
+Settings.cron_jobs['stuck_export_jobs_worker']['job_class'] = 'StuckExportJobsWorker'
Settings.cron_jobs['gitlab_usage_ping_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['gitlab_usage_ping_worker']['cron'] ||= nil # This is dynamically loaded in the sidekiq initializer
Settings.cron_jobs['gitlab_usage_ping_worker']['job_class'] = 'GitlabUsagePingWorker'
diff --git a/db/migrate/20200311165635_create_project_export_jobs.rb b/db/migrate/20200311165635_create_project_export_jobs.rb
new file mode 100644
index 00000000000..026ad2cd771
--- /dev/null
+++ b/db/migrate/20200311165635_create_project_export_jobs.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateProjectExportJobs < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :project_export_jobs do |t|
+ t.references :project, index: false, null: false, foreign_key: { on_delete: :cascade }
+ t.timestamps_with_timezone null: false
+ t.integer :status, limit: 2, null: false, default: 0
+ t.string :jid, limit: 100, null: false, unique: true
+
+ t.index [:project_id, :jid]
+ t.index [:jid], unique: true
+ t.index [:status]
+ t.index [:project_id, :status]
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3128317ec99..52bb18dda68 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.define(version: 2020_03_10_135823) do
+ActiveRecord::Schema.define(version: 2020_03_11_165635) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -3242,6 +3242,18 @@ ActiveRecord::Schema.define(version: 2020_03_10_135823) do
t.string "organization_name"
end
+ create_table "project_export_jobs", force: :cascade do |t|
+ t.bigint "project_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", limit: 2, default: 0, null: false
+ t.string "jid", limit: 100, null: false
+ t.index ["jid"], name: "index_project_export_jobs_on_jid", unique: true
+ t.index ["project_id", "jid"], name: "index_project_export_jobs_on_project_id_and_jid"
+ t.index ["project_id", "status"], name: "index_project_export_jobs_on_project_id_and_status"
+ t.index ["status"], name: "index_project_export_jobs_on_status"
+ end
+
create_table "project_feature_usages", primary_key: "project_id", id: :integer, default: nil, force: :cascade do |t|
t.datetime "jira_dvcs_cloud_last_sync_at"
t.datetime "jira_dvcs_server_last_sync_at"
@@ -5017,6 +5029,7 @@ ActiveRecord::Schema.define(version: 2020_03_10_135823) do
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
add_foreign_key "project_error_tracking_settings", "projects", on_delete: :cascade
+ add_foreign_key "project_export_jobs", "projects", on_delete: :cascade
add_foreign_key "project_feature_usages", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 144076ccb79..009a1a247c0 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -122,12 +122,12 @@ our AsciiDoc snippets, wikis and repos using delimited blocks:
- **Markdown**
- ~~~markdown
+ ````markdown
```plantuml
Bob -> Alice : hello
Alice -> Bob : hi
```
- ~~~
+ ````
- **AsciiDoc**
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 0f0abf25047..3c0835a3605 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -4988,6 +4988,20 @@
"deprecationReason": null
},
{
+ "name": "descendantWeightSum",
+ "description": "Total weight of open and closed issues in the epic and its descendants. Available only when feature flag `unfiltered_epic_aggregates` is enabled.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicDescendantWeights",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "description",
"description": "Description of the epic",
"args": [
@@ -9738,6 +9752,20 @@
"deprecationReason": null
},
{
+ "name": "healthStatus",
+ "description": "Current health status. Available only when feature flag `save_issuable_health_status` is enabled.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "HealthStatus",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "id",
"description": "Global ID of the epic-issue relation",
"args": [
@@ -11118,6 +11146,20 @@
"deprecationReason": null
},
{
+ "name": "healthStatus",
+ "description": "Current health status. Available only when feature flag `save_issuable_health_status` is enabled.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "HealthStatus",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "iid",
"description": "Internal ID of the issue",
"args": [
@@ -13100,6 +13142,47 @@
},
{
"kind": "OBJECT",
+ "name": "EpicDescendantWeights",
+ "description": "Total weight of open and closed descendant issues",
+ "fields": [
+ {
+ "name": "closedIssues",
+ "description": "Total weight of completed (closed) issues in this epic, including epic descendants",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "openedIssues",
+ "description": "Total weight of opened issues in this epic, including epic descendants",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "EpicHealthStatus",
"description": "Health status of child issues",
"fields": [
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index d1aaa01d37c..476abc18835 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -61,14 +61,20 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
Status can be one of:
- `none`
+- `queued`
- `started`
-- `after_export_action`
- `finished`
+- `regeneration_in_progress`
-The `after_export_action` state represents that the export process has been completed successfully and
-the platform is performing some actions on the resulted file. For example, sending
-an email notifying the user to download the file, uploading the exported file
-to a web server, etc.
+`queued` state represents the request for export is received, and is currently in the queue to be processed.
+
+The `started` state represents that the export process has started and is currently in progress.
+It includes the process of exporting, actions performed on the resultant file such as sending
+an email notifying the user to download the file, uploading the exported file to a web server, etc.
+
+`finished` state is after the export process has completed and the user has been notified.
+
+`regeneration_in_progress` is when an export file is available to download, and a request to generate a new export is in process.
`_links` are only present when export has finished.
diff --git a/doc/ci/jenkins/index.md b/doc/ci/jenkins/index.md
index 3caea124351..fe937bb1f3a 100644
--- a/doc/ci/jenkins/index.md
+++ b/doc/ci/jenkins/index.md
@@ -19,7 +19,26 @@ to GitLab!
If you have questions that are not answered here, the [GitLab community forum](https://forum.gitlab.com/)
can be a great resource.
-## Important differences
+## Managing the organizational transition
+
+An important part of transitioning from Jenkins to GitLab is the cultural and organizational
+changes that comes with the move, and successfully managing them. There are a few
+things we have found that helps this:
+
+- Setting and communicating a clear vision of what your migration goals are helps
+ your users understand why the effort is worth it. The value will be clear when
+ the work is done, but people need to be aware while it's in progress too.
+- Sponsorship and alignment from the relevant leadership team helps with the point above.
+- Spending time educating your users on what's different, sharing this document with them,
+ and so on will help ensure you are successful.
+- Finding ways to sequence or delay parts of the migration can help a lot, but you
+ don't want to leave things in a non-migrated (or partially-migrated) state for too
+ long. To gain all the benefits of GitLab, moving your existing Jenkins setup over
+ as-is, including any current problems, will not be enough. You need to take advantage
+ of the improvements that GitLab offers, and this requires (eventually) updating
+ your implementation as part of the transition.
+
+## Important product differences
There are some high level differences between the products worth mentioning:
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index ab9c543f507..a00f7cff651 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -56,7 +56,7 @@ All labels, their meaning and priority are defined on the
[labels page](https://gitlab.com/gitlab-org/gitlab/-/labels).
If you come across an issue that has none of these, and you're allowed to set
-labels, you can _always_ add the team and type, and often also the subject.
+labels, you can _always_ add the type, stage, group, and often the category/feature labels.
### Type labels
@@ -75,7 +75,7 @@ A number of type labels have a priority assigned to them, which automatically
makes them float to the top, depending on their importance.
Type labels are always lowercase, and can have any color, besides blue (which is
-already reserved for subject labels).
+already reserved for category labels).
The descriptions on the [labels page](https://gitlab.com/groups/gitlab-org/-/labels)
explain what falls under each type label.
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 9381082af85..2ae3cd0a290 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -476,7 +476,7 @@ as the list item. This can be done with:
Items nested in lists should always align with the first character of the list item.
In unordered lists (using `-`), this means two spaces for each level of indentation:
-~~~md
+````markdown
- Unordered list item 1
A line nested using 2 spaces to align with the `U` above.
@@ -495,11 +495,11 @@ In unordered lists (using `-`), this means two spaces for each level of indentat
- Unordered list item 4
![an image that will nest inside list item 4](image.png)
-~~~
+````
For ordered lists, use three spaces for each level of indentation:
-~~~md
+````markdown
1. Ordered list item 1
A line nested using 3 spaces to align with the `O` above.
@@ -518,7 +518,7 @@ For ordered lists, use three spaces for each level of indentation:
1. Ordered list item 4
![an image that will nest inside list item 4](image.png)
-~~~
+````
You can nest full lists inside other lists using the same rules as above. If you wish
to mix types, that is also possible, as long as you don't mix items at the same level:
@@ -1364,7 +1364,7 @@ on this document. Further explanation is given below.
The following can be used as a template to get started:
-~~~md
+````markdown
## Descriptive title
One or two sentence description of what endpoint does.
@@ -1392,7 +1392,7 @@ Example response:
}
]
```
-~~~
+````
### Fake tokens
diff --git a/doc/user/application_security/img/outdated_report_branch_v12_9.png b/doc/user/application_security/img/outdated_report_branch_v12_9.png
new file mode 100644
index 00000000000..6e23cf04b26
--- /dev/null
+++ b/doc/user/application_security/img/outdated_report_branch_v12_9.png
Binary files differ
diff --git a/doc/user/application_security/img/outdated_report_pipeline_v12_9.png b/doc/user/application_security/img/outdated_report_pipeline_v12_9.png
new file mode 100644
index 00000000000..2bb1fcaa302
--- /dev/null
+++ b/doc/user/application_security/img/outdated_report_pipeline_v12_9.png
Binary files differ
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 8d7891fb973..4382c69a9ac 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -198,6 +198,35 @@ An approval is optional when a license report:
- Contains no software license violations.
- Contains only new licenses that are `approved` or unknown.
+## Outdated security reports
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4913) in GitLab 12.7.
+
+When a security report generated for a merge request becomes outdated, the merge request shows a warning
+message in the security widget and prompts you to take an appropriate action.
+
+This can happen in two scenarios:
+
+1. Your [source branch is behind the target branch](#source-branch-is-behind-the-target-branch).
+1. The [target branch security report is out of date](#target-branch-security-report-is-out-of-date).
+
+### Source branch is behind the target branch
+
+This means the most recent common ancestor commit between the target branch and the source branch is
+not the most recent commit on the target branch. This is by far the most common situation.
+
+In this case you must rebase or merge to incorporate the changes from the target branch.
+
+![Incorporate target branch changes](img/outdated_report_branch_v12_9.png)
+
+### Target branch security report is out of date
+
+This can happen for many reasons, including failed jobs or new advisories. When the merge request shows that a
+security report is out of date, you must run a new pipeline on the target branch.
+You can do it quickly by following the hyperlink given to run a new pipeline.
+
+![Run a new pipeline](img/outdated_report_pipeline_v12_9.png)
+
## Troubleshooting
### Getting error message `sast job: stage parameter should be [some stage name here]`
diff --git a/doc/user/asciidoc.md b/doc/user/asciidoc.md
index da6bf287955..b9b346d3be4 100644
--- a/doc/user/asciidoc.md
+++ b/doc/user/asciidoc.md
@@ -282,11 +282,11 @@ source - a listing that is embellished with (colorized) syntax highlighting
----
```
-~~~asciidoc
+````asciidoc
\```language
fenced code - a shorthand syntax for the source block
\```
-~~~
+````
```asciidoc
[,attribution,citetitle]
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c8484380127..72570848a61 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -165,7 +165,7 @@ Visit the [official page](https://mermaidjs.github.io/) for more details. If you
In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block:
-~~~
+````markdown
```mermaid
graph TD;
A-->B;
@@ -173,7 +173,7 @@ graph TD;
B-->D;
C-->D;
```
-~~~
+````
```mermaid
graph TD;
@@ -185,7 +185,7 @@ graph TD;
Subgraphs can also be included:
-~~~
+````markdown
```mermaid
graph TB
@@ -202,7 +202,7 @@ graph TB
SubGraph1 --> FinalThing[Final Thing]
end
```
-~~~
+````
```mermaid
graph TB
@@ -280,27 +280,27 @@ The following delimiters are supported:
- YAML (`---`):
- ~~~yaml
+ ```yaml
---
title: About Front Matter
example:
language: yaml
---
- ~~~
+ ```
- TOML (`+++`):
- ~~~toml
+ ```toml
+++
title = "About Front Matter"
[example]
language = "toml"
+++
- ~~~
+ ```
- JSON (`;;;`):
- ~~~json
+ ```json
;;;
{
"title": "About Front Matter"
@@ -309,7 +309,7 @@ The following delimiters are supported:
}
}
;;;
- ~~~
+ ```
Other languages are supported by adding a specifier to any of the existing
delimiters. For example:
@@ -364,7 +364,7 @@ Math written between dollar signs `$` will be rendered inline with the text. Mat
inside a [code block](#code-spans-and-blocks) with the language declared as `math`, will be rendered
on a separate line:
-~~~
+````markdown
This math is inline $`a^2+b^2=c^2`$.
This is on a separate line
@@ -372,7 +372,7 @@ This is on a separate line
```math
a^2+b^2=c^2
```
-~~~
+````
This math is inline $`a^2+b^2=c^2`$.
@@ -613,12 +613,12 @@ Inline `code` has `back-ticks around` it.
---
-Similarly, a whole block of code can be fenced with triple backticks ```` ``` ````,
+Similarly, a whole block of code can be fenced with triple backticks (```` ``` ````),
triple tildes (`~~~`), or indented 4 or more spaces to achieve a similar effect for
a larger body of code.
-~~~
-```
+````markdown
+```python
def function():
#indenting works just fine in the fenced code block
s = "Python code"
@@ -628,7 +628,7 @@ def function():
Using 4 spaces
is like using
3-backtick fences.
-~~~
+````
```plaintext
~~~
@@ -651,9 +651,9 @@ is like using
3-backtick fences.
```
-~~~plaintext
+```plaintext
Tildes are OK too.
-~~~
+```
#### Colored code and syntax highlighting
@@ -665,10 +665,10 @@ highlighting in code blocks. For a list of supported languages visit the
Syntax highlighting is only supported in code blocks, it is not possible to highlight
code when it is inline.
-Blocks of code are fenced by lines with three back-ticks ```` ``` ```` or three tildes `~~~`, and have
+Blocks of code are fenced by lines with three back-ticks (```` ``` ````) or three tildes (`~~~`), and have
the language identified at the end of the first fence:
-~~~markdown
+````markdown
```javascript
var s = "JavaScript syntax highlighting";
alert(s);
@@ -692,7 +692,7 @@ No language indicated, so no syntax highlighting.
s = "There is no highlighting for this."
But let's throw in a <b>tag</b>.
```
-~~~
+````
The four examples above render as:
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 5b8d41bbd10..934ce2f9871 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -7,7 +7,7 @@ code_quality:
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
- CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/security-products/codequality:0.85.6"
+ CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/security-products/codequality:0.85.9"
script:
- |
if ! docker info &>/dev/null; then
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index 11f5df08e48..cd07122ffd9 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -106,6 +106,17 @@ module Gitlab
\n```$
)
|
+ (?<inline_code>
+ # Inline code on separate rows:
+ # `
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # `
+
+ ^.*`\n*
+ .+?
+ \n*`$
+ )
+ |
(?<html>
# HTML block:
# <tag>
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index 568761edb33..5a583183924 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -8,13 +8,14 @@ namespace :gitlab do
OUTPUT_DIR = Rails.root.join("doc/api/graphql/reference")
TEMPLATES_DIR = 'lib/gitlab/graphql/docs/templates/'
- # Consider all feature flags disabled
- # to avoid pipeline failures in case developer
- # dumps schema with flags enabled locally before pushing
- task disable_feature_flags: :environment do
+ # Make all feature flags enabled so that all feature flag
+ # controlled fields are considered visible and are output.
+ # Also avoids pipeline failures in case developer
+ # dumps schema with flags disabled locally before pushing
+ task enable_feature_flags: :environment do
class Feature
def self.enabled?(*args)
- false
+ true
end
end
end
@@ -25,7 +26,7 @@ namespace :gitlab do
# - gitlab:graphql:schema:json
GraphQL::RakeTask.new(
schema_name: 'GitlabSchema',
- dependencies: [:environment, :disable_feature_flags],
+ dependencies: [:environment, :enable_feature_flags],
directory: OUTPUT_DIR,
idl_outfile: "gitlab_schema.graphql",
json_outfile: "gitlab_schema.json"
@@ -33,7 +34,7 @@ namespace :gitlab do
namespace :graphql do
desc 'GitLab | GraphQL | Generate GraphQL docs'
- task compile_docs: :environment do
+ task compile_docs: [:environment, :enable_feature_flags] do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
renderer.write
@@ -42,7 +43,7 @@ namespace :gitlab do
end
desc 'GitLab | GraphQL | Check if GraphQL docs are up to date'
- task check_docs: :environment do
+ task check_docs: [:environment, :enable_feature_flags] do
renderer = Gitlab::Graphql::Docs::Renderer.new(GitlabSchema.graphql_definition, render_options)
doc = File.read(Rails.root.join(OUTPUT_DIR, 'index.md'))
@@ -56,7 +57,7 @@ namespace :gitlab do
end
desc 'GitLab | GraphQL | Check if GraphQL schemas are up to date'
- task check_schema: :environment do
+ task check_schema: [:environment, :enable_feature_flags] do
idl_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.graphql'))
json_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.json'))
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 739f705e398..4f079323f4a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7015,7 +7015,7 @@ msgstr ""
msgid "Display source"
msgstr ""
-msgid "Displays dependencies and known vulnerabilities, based on the %{linkStart}latest pipeline%{linkEnd} scan"
+msgid "Displays dependencies and known vulnerabilities, based on the %{linkStart}latest successful%{linkEnd} scan"
msgstr ""
msgid "Do not display offers from third parties within GitLab"
@@ -11724,7 +11724,7 @@ msgstr ""
msgid "Licenses|Detected in Project"
msgstr ""
-msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest pipeline%{linkEnd} scan"
+msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest successful%{linkEnd} scan"
msgstr ""
msgid "Licenses|Error fetching the license list. Please check your network connection and try again."
diff --git a/package.json b/package.json
index 44577a5aa6d..f8f00b67f6b 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"apollo-link-batch-http": "^1.2.11",
"apollo-upload-client": "^10.0.0",
"autosize": "^4.0.2",
- "aws-sdk": "^2.526.0",
+ "aws-sdk": "^2.637.0",
"axios": "^0.19.0",
"babel-loader": "^8.0.6",
"babel-plugin-lodash": "^3.3.4",
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 67e24841dee..53a57937e9b 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1140,7 +1140,7 @@ describe ProjectsController do
end
it 'prevents requesting project export' do
- get action, params: { namespace_id: project.namespace, id: project }
+ post action, params: { namespace_id: project.namespace, id: project }
expect(flash[:alert]).to eq('This endpoint has been requested too many times. Try again later.')
expect(response).to have_gitlab_http_status(:found)
@@ -1152,7 +1152,7 @@ describe ProjectsController do
context 'when project export is enabled' do
it 'returns 302' do
- get action, params: { namespace_id: project.namespace, id: project }
+ post action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(:found)
end
@@ -1164,7 +1164,7 @@ describe ProjectsController do
end
it 'returns 404' do
- get action, params: { namespace_id: project.namespace, id: project }
+ post action, params: { namespace_id: project.namespace, id: project }
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/factories/project_export_jobs.rb b/spec/factories/project_export_jobs.rb
new file mode 100644
index 00000000000..b2666555ea8
--- /dev/null
+++ b/spec/factories/project_export_jobs.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_export_job do
+ project
+ jid { SecureRandom.hex(8) }
+ end
+end
diff --git a/spec/features/markdown/mermaid_spec.rb b/spec/features/markdown/mermaid_spec.rb
index 542caccb18d..1cd5760c30e 100644
--- a/spec/features/markdown/mermaid_spec.rb
+++ b/spec/features/markdown/mermaid_spec.rb
@@ -94,8 +94,31 @@ describe 'Mermaid rendering', :js do
page.find('summary').click
svg = page.find('svg.mermaid')
- expect(svg[:width].to_i).to be_within(5).of(120)
- expect(svg[:height].to_i).to be_within(5).of(220)
+ expect(svg[:style]).to match(/max-width/)
+ expect(svg[:width].to_i).to eq(100)
+ expect(svg[:height].to_i).to eq(0)
end
end
+
+ it 'correctly sizes mermaid diagram block', :js do
+ description = <<~MERMAID
+ ```mermaid
+ graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+ ```
+ MERMAID
+
+ project = create(:project, :public)
+ issue = create(:issue, project: project, description: description)
+
+ visit project_issue_path(project, issue)
+
+ svg = page.find('svg.mermaid')
+ expect(svg[:style]).to match(/max-width/)
+ expect(svg[:width].to_i).to eq(100)
+ expect(svg[:height].to_i).to eq(0)
+ end
end
diff --git a/spec/finders/projects/export_job_finder_spec.rb b/spec/finders/projects/export_job_finder_spec.rb
new file mode 100644
index 00000000000..31b68717d13
--- /dev/null
+++ b/spec/finders/projects/export_job_finder_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::ExportJobFinder do
+ let(:project) { create(:project) }
+ let(:project_export_job1) { create(:project_export_job, project: project) }
+ let(:project_export_job2) { create(:project_export_job, project: project) }
+
+ describe '#execute' do
+ subject { described_class.new(project, params).execute }
+
+ context 'when queried for a project' do
+ let(:params) { {} }
+
+ it 'scopes to the project' do
+ expect(subject).to contain_exactly(
+ project_export_job1, project_export_job2
+ )
+ end
+ end
+
+ context 'when queried by job id' do
+ let(:params) { { jid: project_export_job1.jid } }
+
+ it 'filters records' do
+ expect(subject).to contain_exactly(project_export_job1)
+ end
+ end
+
+ context 'when queried by status' do
+ let(:params) { { status: :started } }
+
+ before do
+ project_export_job2.start!
+ end
+
+ it 'filters records' do
+ expect(subject).to contain_exactly(project_export_job2)
+ end
+ end
+
+ context 'when queried by invalid status' do
+ let(:params) { { status: '1234ad' } }
+
+ it 'raises exception' do
+ expect { subject }.to raise_error(described_class::InvalidExportJobStatusError, 'Invalid export job status')
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
index 81c8815caf6..fd35ba34b49 100644
--- a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
+++ b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
@@ -13,9 +13,10 @@
"type": "string",
"enum": [
"none",
+ "queued",
"started",
"finished",
- "after_export_action"
+ "regeneration_in_progress"
]
}
}
diff --git a/spec/frontend/blob/blob_file_dropzone_spec.js b/spec/frontend/blob/blob_file_dropzone_spec.js
new file mode 100644
index 00000000000..4e9a05418df
--- /dev/null
+++ b/spec/frontend/blob/blob_file_dropzone_spec.js
@@ -0,0 +1,50 @@
+import $ from 'jquery';
+import BlobFileDropzone from '~/blob/blob_file_dropzone';
+
+describe('BlobFileDropzone', () => {
+ preloadFixtures('blob/show.html');
+ let dropzone;
+ let replaceFileButton;
+ const jQueryMock = {
+ enable: jest.fn(),
+ disable: jest.fn(),
+ };
+
+ beforeEach(() => {
+ loadFixtures('blob/show.html');
+ const form = $('.js-upload-blob-form');
+ // eslint-disable-next-line no-new
+ new BlobFileDropzone(form, 'POST');
+ dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone;
+ dropzone.processQueue = jest.fn();
+ replaceFileButton = $('#submit-all');
+ $.fn.extend(jQueryMock);
+ });
+
+ describe('submit button', () => {
+ it('requires file', () => {
+ jest.spyOn(window, 'alert').mockImplementation(() => {});
+
+ replaceFileButton.click();
+
+ expect(window.alert).toHaveBeenCalled();
+ });
+
+ it('is disabled while uploading', () => {
+ jest.spyOn(window, 'alert').mockImplementation(() => {});
+
+ const file = new File([], 'some-file.jpg');
+ const fakeEvent = $.Event('drop', {
+ dataTransfer: { files: [file] },
+ });
+
+ dropzone.listeners[0].events.drop(fakeEvent);
+
+ replaceFileButton.click();
+
+ expect(window.alert).not.toHaveBeenCalled();
+ expect(jQueryMock.enable).toHaveBeenCalled();
+ expect(dropzone.processQueue).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
deleted file mode 100644
index fe03775ec4d..00000000000
--- a/spec/javascripts/blob/blob_file_dropzone_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import $ from 'jquery';
-import BlobFileDropzone from '~/blob/blob_file_dropzone';
-
-describe('BlobFileDropzone', function() {
- preloadFixtures('blob/show.html');
-
- beforeEach(() => {
- loadFixtures('blob/show.html');
- const form = $('.js-upload-blob-form');
- this.blobFileDropzone = new BlobFileDropzone(form, 'POST');
- this.dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone;
- this.replaceFileButton = $('#submit-all');
- });
-
- describe('submit button', () => {
- it('requires file', () => {
- spyOn(window, 'alert');
-
- this.replaceFileButton.click();
-
- expect(window.alert).toHaveBeenCalled();
- });
-
- it('is disabled while uploading', () => {
- spyOn(window, 'alert');
-
- const file = new File([], 'some-file.jpg');
- const fakeEvent = $.Event('drop', {
- dataTransfer: { files: [file] },
- });
-
- this.dropzone.listeners[0].events.drop(fakeEvent);
- this.replaceFileButton.click();
-
- expect(window.alert).not.toHaveBeenCalled();
- expect(this.replaceFileButton.is(':disabled')).toEqual(true);
- });
- });
-});
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
index 86ceb97b250..1631de393b5 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -3,6 +3,12 @@
require 'spec_helper'
describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
+ before do
+ allow_next_instance_of(ProjectExportWorker) do |job|
+ allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
+ end
+ end
+
let!(:service) { described_class.new }
let!(:project) { create(:project, :with_export) }
let(:shared) { project.import_export_shared }
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 95c47d15f8f..7792daed99c 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -5,6 +5,12 @@ require 'spec_helper'
describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
include StubRequests
+ before do
+ allow_next_instance_of(ProjectExportWorker) do |job|
+ allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
+ end
+ end
+
let(:example_url) { 'http://www.example.com' }
let(:strategy) { subject.new(url: example_url, http_method: 'post') }
let!(:project) { create(:project, :with_export) }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e579c8474b7..37b3e4a4a22 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -469,6 +469,7 @@ project:
- autoclose_referenced_issues
- status_page_setting
- requirements
+- export_jobs
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index 1843acb0cc0..6ea597bf01e 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -291,6 +291,33 @@ describe Gitlab::QuickActions::Extractor do
expect(msg).to eq expected
end
+ it 'does not extract commands in multiline inline code on seperated rows' do
+ msg = "Hello\r\n`\r\nThis is some text\r\n/close\r\n/assign @user\r\n`\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+
+ it 'does not extract commands in multiline inline code starting from text' do
+ msg = "Hello `This is some text\r\n/close\r\n/assign @user\r\n`\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+
+ it 'does not extract commands in inline code' do
+ msg = "`This is some text\r\n/close\r\n/assign @user\r\n`\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+
it 'limits to passed commands when they are passed' do
msg = <<~MSG.strip
Hello, we should only extract the commands passed
diff --git a/spec/models/project_export_job_spec.rb b/spec/models/project_export_job_spec.rb
new file mode 100644
index 00000000000..dc39d0e401d
--- /dev/null
+++ b/spec/models/project_export_job_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectExportJob, type: :model do
+ let(:project) { create(:project) }
+ let!(:job1) { create(:project_export_job, project: project, status: 0) }
+ let!(:job2) { create(:project_export_job, project: project, status: 2) }
+
+ describe 'associations' do
+ it { expect(job1).to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { expect(job1).to validate_presence_of(:project) }
+ it { expect(job1).to validate_presence_of(:jid) }
+ it { expect(job1).to validate_presence_of(:status) }
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 782e526b69d..6d9b46c9941 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3957,6 +3957,12 @@ describe Project do
describe '#remove_export' do
let(:project) { create(:project, :with_export) }
+ before do
+ allow_next_instance_of(ProjectExportWorker) do |job|
+ allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
+ end
+ end
+
it 'removes the export' do
project.remove_exports
@@ -5813,6 +5819,86 @@ describe Project do
end
end
+ describe '#add_export_job' do
+ context 'if not already present' do
+ it 'starts project export job' do
+ user = create(:user)
+ project = build(:project)
+
+ expect(ProjectExportWorker).to receive(:perform_async).with(user.id, project.id, nil, {})
+
+ project.add_export_job(current_user: user)
+ end
+ end
+ end
+
+ describe '#export_in_progress?' do
+ let(:project) { build(:project) }
+ let!(:project_export_job ) { create(:project_export_job, project: project) }
+
+ context 'when project export is enqueued' do
+ it { expect(project.export_in_progress?).to be false }
+ end
+
+ context 'when project export is in progress' do
+ before do
+ project_export_job.start!
+ end
+
+ it { expect(project.export_in_progress?).to be true }
+ end
+
+ context 'when project export is completed' do
+ before do
+ finish_job(project_export_job)
+ end
+
+ it { expect(project.export_in_progress?).to be false }
+ end
+ end
+
+ describe '#export_status' do
+ let(:project) { build(:project) }
+ let!(:project_export_job ) { create(:project_export_job, project: project) }
+
+ context 'when project export is enqueued' do
+ it { expect(project.export_status).to eq :queued }
+ end
+
+ context 'when project export is in progress' do
+ before do
+ project_export_job.start!
+ end
+
+ it { expect(project.export_status).to eq :started }
+ end
+
+ context 'when project export is completed' do
+ before do
+ finish_job(project_export_job)
+ allow(project).to receive(:export_file).and_return(double(ImportExportUploader, file: 'exists.zip'))
+ end
+
+ it { expect(project.export_status).to eq :finished }
+ end
+
+ context 'when project export is being regenerated' do
+ let!(:new_project_export_job ) { create(:project_export_job, project: project) }
+
+ before do
+ finish_job(project_export_job)
+ allow(project).to receive(:export_file).and_return(double(ImportExportUploader, file: 'exists.zip'))
+ end
+
+ it { expect(project.export_status).to eq :regeneration_in_progress }
+ end
+ end
+
+ def finish_job(export_job)
+ export_job.start
+ export_job.finish
+ end
+
def rugged_config
rugged_repo(project.repository).config
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index d5c822385da..859a3cca44f 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -27,12 +27,9 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
-
- # simulate exporting work directory
- FileUtils.mkdir_p File.join(project_started.export_path, 'securerandom-hex')
-
- # simulate in after export action
- FileUtils.touch File.join(project_after_export.import_export_shared.lock_files_path, SecureRandom.hex)
+ allow_next_instance_of(ProjectExportWorker) do |job|
+ allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
+ end
end
after do
@@ -82,28 +79,42 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do
expect(json_response['export_status']).to eq('none')
end
- it 'is started' do
- get api(path_started, user)
+ context 'when project export has started' do
+ before do
+ create(:project_export_job, project: project_started, status: 1)
+ end
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/project/export_status')
- expect(json_response['export_status']).to eq('started')
+ it 'returns status started' do
+ get api(path_started, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/project/export_status')
+ expect(json_response['export_status']).to eq('started')
+ end
end
- it 'is after_export' do
- get api(path_after_export, user)
+ context 'when project export has finished' do
+ it 'returns status finished' do
+ get api(path_finished, user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/project/export_status')
- expect(json_response['export_status']).to eq('after_export_action')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/project/export_status')
+ expect(json_response['export_status']).to eq('finished')
+ end
end
- it 'is finished' do
- get api(path_finished, user)
+ context 'when project export is being regenerated' do
+ before do
+ create(:project_export_job, project: project_finished, status: 1)
+ end
+
+ it 'returns status regeneration_in_progress' do
+ get api(path_finished, user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('public_api/v4/project/export_status')
- expect(json_response['export_status']).to eq('finished')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/project/export_status')
+ expect(json_response['export_status']).to eq('regeneration_in_progress')
+ end
end
end
diff --git a/spec/services/ci/create_cross_project_pipeline_service_spec.rb b/spec/services/ci/create_cross_project_pipeline_service_spec.rb
index 667ad532fb0..99c44c3aa17 100644
--- a/spec/services/ci/create_cross_project_pipeline_service_spec.rb
+++ b/spec/services/ci/create_cross_project_pipeline_service_spec.rb
@@ -362,6 +362,26 @@ describe Ci::CreateCrossProjectPipelineService, '#execute' do
end
end
+ context 'when bridge job status update raises state machine errors' do
+ let(:stub_config) { false }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(invalid: { yaml: 'error' }))
+ bridge.drop!
+ end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ instance_of(Ci::Bridge::InvalidTransitionError),
+ bridge_id: bridge.id,
+ downstream_pipeline_id: kind_of(Numeric))
+
+ service.execute(bridge)
+ end
+ end
+
context 'when bridge job has YAML variables defined' do
before do
bridge.yaml_variables = [{ key: 'BRIDGE', value: 'var', public: true }]
diff --git a/spec/services/ci/pipeline_bridge_status_service_spec.rb b/spec/services/ci/pipeline_bridge_status_service_spec.rb
index 95f16af3af9..0b6ae976d97 100644
--- a/spec/services/ci/pipeline_bridge_status_service_spec.rb
+++ b/spec/services/ci/pipeline_bridge_status_service_spec.rb
@@ -22,6 +22,24 @@ describe Ci::PipelineBridgeStatusService do
subject
end
+
+ context 'when bridge job status raises state machine errors' do
+ before do
+ pipeline.drop!
+ bridge.drop!
+ end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ instance_of(Ci::Bridge::InvalidTransitionError),
+ bridge_id: bridge.id,
+ downstream_pipeline_id: pipeline.id)
+
+ subject
+ end
+ end
end
end
end
diff --git a/spec/workers/concerns/project_export_options_spec.rb b/spec/workers/concerns/project_export_options_spec.rb
new file mode 100644
index 00000000000..985afaaf11e
--- /dev/null
+++ b/spec/workers/concerns/project_export_options_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectExportOptions do
+ let(:project) { create(:project) }
+ let(:project_export_job) { create(:project_export_job, project: project, jid: '123', status: 1) }
+ let(:job) { { 'args' => [project.owner.id, project.id, nil, nil], 'jid' => '123' } }
+ let(:worker_class) do
+ Class.new do
+ include Sidekiq::Worker
+ include ProjectExportOptions
+ end
+ end
+
+ it 'sets default retry limit' do
+ expect(worker_class.sidekiq_options['retry']).to eq(ProjectExportOptions::EXPORT_RETRY_COUNT)
+ end
+
+ it 'sets default status expiration' do
+ expect(worker_class.sidekiq_options['status_expiration']).to eq(StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION)
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'marks status as failed' do
+ expect { worker_class.sidekiq_retries_exhausted_block.call(job) }.to change { project_export_job.reload.status }.from(1).to(3)
+ end
+
+ context 'when status update fails' do
+ before do
+ project_export_job.update(status: 2)
+ end
+
+ it 'logs an error' do
+ expect(Sidekiq.logger).to receive(:error).with("Failed to set Job #{job['jid']} for project #{project.id} to failed state")
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+ end
+ end
+ end
+end
diff --git a/spec/workers/project_export_worker_spec.rb b/spec/workers/project_export_worker_spec.rb
index 8065087796c..d0d52e0df2d 100644
--- a/spec/workers/project_export_worker_spec.rb
+++ b/spec/workers/project_export_worker_spec.rb
@@ -9,21 +9,59 @@ describe ProjectExportWorker do
subject { described_class.new }
describe '#perform' do
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
+ end
+ end
+
context 'when it succeeds' do
it 'calls the ExportService' do
expect_any_instance_of(::Projects::ImportExport::ExportService).to receive(:execute)
subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' })
end
+
+ context 'export job' do
+ before do
+ allow_any_instance_of(::Projects::ImportExport::ExportService).to receive(:execute)
+ end
+
+ it 'creates an export job record for the project' do
+ expect { subject.perform(user.id, project.id, {}) }.to change { project.export_jobs.count }.from(0).to(1)
+ end
+
+ it 'sets the export job status to started' do
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:start)
+ end
+
+ subject.perform(user.id, project.id, {})
+ end
+
+ it 'sets the export job status to finished' do
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:finish)
+ end
+
+ subject.perform(user.id, project.id, {})
+ end
+ end
end
context 'when it fails' do
- it 'raises an exception when params are invalid' do
+ it 'does not raise an exception when strategy is invalid' do
expect_any_instance_of(::Projects::ImportExport::ExportService).not_to receive(:execute)
- expect { subject.perform(1234, project.id, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
- expect { subject.perform(user.id, 1234, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
- expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.to raise_exception(Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError)
+ expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error
+ end
+
+ it 'does not raise error when project cannot be found' do
+ expect { subject.perform(user.id, -234, {}) }.not_to raise_error
+ end
+
+ it 'does not raise error when user cannot be found' do
+ expect { subject.perform(-863, project.id, {}) }.not_to raise_error
end
end
end
diff --git a/spec/workers/stuck_export_jobs_worker_spec.rb b/spec/workers/stuck_export_jobs_worker_spec.rb
new file mode 100644
index 00000000000..fc5758fdadf
--- /dev/null
+++ b/spec/workers/stuck_export_jobs_worker_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe StuckExportJobsWorker do
+ let(:worker) { described_class.new }
+
+ shared_examples 'project export job detection' do
+ context 'when the job has completed' do
+ context 'when the export status was already updated' do
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do
+ project_export_job.start
+ project_export_job.finish
+
+ [project_export_job.jid]
+ end
+ end
+
+ it 'does not mark the export as failed' do
+ worker.perform
+
+ expect(project_export_job.reload.finished?).to be true
+ end
+ end
+
+ context 'when the export status was not updated' do
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do
+ project_export_job.start
+
+ [project_export_job.jid]
+ end
+ end
+
+ it 'marks the project as failed' do
+ worker.perform
+
+ expect(project_export_job.reload.failed?).to be true
+ end
+ end
+
+ context 'when the job is not in queue and db record in queued state' do
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([project_export_job.jid])
+ end
+
+ it 'marks the project as failed' do
+ expect(project_export_job.queued?).to be true
+
+ worker.perform
+
+ expect(project_export_job.reload.failed?).to be true
+ end
+ end
+ end
+
+ context 'when the job is running in Sidekiq' do
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
+ end
+
+ it 'does not mark the project export as failed' do
+ expect { worker.perform }.not_to change { project_export_job.reload.status }
+ end
+ end
+ end
+
+ describe 'with started export status' do
+ it_behaves_like 'project export job detection' do
+ let(:project) { create(:project) }
+ let!(:project_export_job) { create(:project_export_job, project: project, jid: '123') }
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index b6691bf0a86..b93a588c992 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1866,14 +1866,14 @@ autosize@^4.0.2:
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.2.tgz#073cfd07c8bf45da4b9fd153437f5bafbba1e4c9"
integrity sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA==
-aws-sdk@^2.526.0:
- version "2.526.0"
- resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.526.0.tgz#e0f899be59edb7d50eb8cca7978bcd401a5d48c2"
- integrity sha512-ZZqf8AnD9A8ZJd/4oU711R8taxm8sV7wcAOvT0HhrZxv8zASAzoz2lpZ19QAil6uJ52IOkq4ij/zGy7VBXEgPA==
+aws-sdk@^2.637.0:
+ version "2.637.0"
+ resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.637.0.tgz#810e25e53acf2250d35fc74498f9d4492e154217"
+ integrity sha512-e7EYX5rNtQyEaleQylUtLSNKXOmvOwfifQ4bYkfF80mFsVI3DSydczLHXrqPzXoEJaS/GI/9HqVnlQcPs6Q3ew==
dependencies:
buffer "4.9.1"
events "1.1.1"
- ieee754 "1.1.8"
+ ieee754 "1.1.13"
jmespath "0.15.0"
querystring "0.2.0"
sax "1.2.1"
@@ -5752,10 +5752,10 @@ icss-utils@^2.1.0:
dependencies:
postcss "^6.0.1"
-ieee754@1.1.8, ieee754@^1.1.4:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
- integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=
+ieee754@1.1.13, ieee754@^1.1.4:
+ version "1.1.13"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
+ integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
iferr@^0.1.5:
version "0.1.5"