diff options
40 files changed, 855 insertions, 198 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index acee30867d9..78645f48b6f 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -94,11 +94,11 @@ setup-test-env: rspec unit pg9: extends: .rspec-base-pg9 - parallel: 20 + parallel: 24 rspec unit pg9-foss: extends: .rspec-base-pg9-foss - parallel: 20 + parallel: 24 rspec integration pg9: extends: .rspec-base-pg9 diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md index b468d266c07..b409dc3df4b 100644 --- a/CHANGELOG-EE.md +++ b/CHANGELOG-EE.md @@ -1,5 +1,17 @@ Please view this file on the master branch, on stable branches it's out of date. +## 12.5.1 + +### Security (6 changes) + +- Protect Jira integration endpoints from guest users. +- Fix private comment Elasticsearch leak on project search scope. +- Filter snippet search results by feature visibility. +- Hide AWS secret on Admin Integration page. +- Fail pull mirror when mirror user is blocked. +- Prevent IDOR when adding users to protected environments. + + ## 12.5.0 ### Security (5 changes) @@ -224,6 +236,18 @@ Please view this file on the master branch, on stable branches it's out of date. - Docs for protected branch code owner approval API. !17132 +## 12.3.7 + +### Security (6 changes) + +- Protect Jira integration endpoints from guest users. +- Fix private comment Elasticsearch leak on project search scope. +- Filter snippet search results by feature visibility. +- Hide AWS secret on Admin Integration page. +- Fail pull mirror when mirror user is blocked. +- Prevent IDOR when adding users to protected environments. + + ## 12.3.4 ### Fixed (2 changes) diff --git a/CHANGELOG.md b/CHANGELOG.md index bea21092b43..f22601325d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,19 @@ entry. ## 12.5.1 -### Security (8 changes) +### Security (11 changes) -- Check permissions before showing a forked project's source. +- Do not create todos for approvers without access. !1442 +- Hide commit counts from guest users in Cycle Analytics. - Encrypt application setting tokens. - Update Workhorse and Gitaly to fix a security issue. -- Hide commit counts from guest users in Cycle Analytics. +- Add maven file_name regex validation on incoming files. +- Check permissions before showing a forked project's source. - Limit potential for DNS rebind SSRF in chat notifications. - Ensure are cleaned by ImportExport::AttributeCleaner. - Remove notes regarding Related Branches from Issue activity feeds for guest users. - Escape namespace in label references to prevent XSS. +- Add authorization to using filter vulnerable in Dependency List. ## 12.5.0 @@ -367,21 +370,6 @@ entry. - Change selects from default browser style to custom style. -## 12.4.4 - -### Security (9 changes) - -- Check permissions before showing a forked project's source. -- Encrypt application setting tokens. -- Update Workhorse and Gitaly to fix a security issue. -- Hide commit counts from guest users in Cycle Analytics. -- Limit potential for DNS rebind SSRF in chat notifications. -- Fix 500 error caused by invalid byte sequences in links. -- Ensure are cleaned by ImportExport::AttributeCleaner. -- Remove notes regarding Related Branches from Issue activity feeds for guest users. -- Escape namespace in label references to prevent XSS. - - ## 12.4.3 ### Fixed (2 changes) @@ -752,17 +740,20 @@ entry. ## 12.3.7 -### Security (9 changes) +### Security (12 changes) -- Check permissions before showing a forked project's source. +- Do not create todos for approvers without access. !1442 +- Limit potential for DNS rebind SSRF in chat notifications. - Encrypt application setting tokens. - Update Workhorse and Gitaly to fix a security issue. +- Add maven file_name regex validation on incoming files. - Hide commit counts from guest users in Cycle Analytics. -- Limit potential for DNS rebind SSRF in chat notifications. +- Check permissions before showing a forked project's source. - Fix 500 error caused by invalid byte sequences in links. - Ensure are cleaned by ImportExport::AttributeCleaner. - Remove notes regarding Related Branches from Issue activity feeds for guest users. - Escape namespace in label references to prevent XSS. +- Add authorization to using filter vulnerable in Dependency List. ## 12.3.4 diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index dfd4d5474ff..748673f05bb 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,7 +1,7 @@ <script> /* eslint-disable @gitlab/vue-i18n/no-bare-strings */ import { __, sprintf } from '~/locale'; -import Timeago from 'timeago.js'; +import { format } from 'timeago.js'; import _ from 'underscore'; import { GlTooltipDirective } from '@gitlab/ui'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -23,7 +23,6 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; * * Renders a table row for each environment. */ -const timeagoInstance = new Timeago(); export default { components: { @@ -123,7 +122,7 @@ export default { */ deployedDate() { if (this.canShowDate) { - return timeagoInstance.format(this.model.last_deployment.deployed_at); + return format(this.model.last_deployment.deployed_at); } return ''; }, diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 1ab11892f61..bc742179279 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import _ from 'underscore'; -import timeago from 'timeago.js'; +import * as timeago from 'timeago.js'; import dateFormat from 'dateformat'; import { languageCode, s__, __, n__ } from '../../locale'; @@ -92,90 +92,80 @@ export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => { */ const timeagoLanguageCode = languageCode().replace(/-/g, '_'); -let timeagoInstance; - /** - * Sets a timeago Instance + * Registers timeago locales */ -export const getTimeago = () => { - if (!timeagoInstance) { - const memoizedLocaleRemaining = () => { - const cache = []; - - const timeAgoLocaleRemaining = [ - () => [s__('Timeago|just now'), s__('Timeago|right now')], - () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')], - () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], - () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], - () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], - () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')], - () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')], - () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], - () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')], - () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], - () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], - () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], - ]; - - return (number, index) => { - if (cache[index]) { - return cache[index]; - } - cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index](); - return cache[index]; - }; - }; - - const memoizedLocale = () => { - const cache = []; - - const timeAgoLocale = [ - () => [s__('Timeago|just now'), s__('Timeago|right now')], - () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')], - () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], - () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], - () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], - () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')], - () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')], - () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], - () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')], - () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], - () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], - () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], - ]; - - return (number, index) => { - if (cache[index]) { - return cache[index]; - } - cache[index] = timeAgoLocale[index] && timeAgoLocale[index](); - return cache[index]; - }; - }; - - timeago.register(timeagoLanguageCode, memoizedLocale()); - timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining()); - - timeagoInstance = timeago(); - } +const memoizedLocaleRemaining = () => { + const cache = []; + + const timeAgoLocaleRemaining = [ + () => [s__('Timeago|just now'), s__('Timeago|right now')], + () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')], + () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], + () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], + () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], + () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')], + () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')], + () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], + () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')], + () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], + () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], + () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], + () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], + () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], + ]; + + return (number, index) => { + if (cache[index]) { + return cache[index]; + } + cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index](); + return cache[index]; + }; +}; - return timeagoInstance; +const memoizedLocale = () => { + const cache = []; + + const timeAgoLocale = [ + () => [s__('Timeago|just now'), s__('Timeago|right now')], + () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')], + () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], + () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], + () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], + () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')], + () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')], + () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], + () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')], + () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], + () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], + () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], + () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], + () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], + ]; + + return (number, index) => { + if (cache[index]) { + return cache[index]; + } + cache[index] = timeAgoLocale[index] && timeAgoLocale[index](); + return cache[index]; + }; }; +timeago.register(timeagoLanguageCode, memoizedLocale()); +timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining()); + +export const getTimeago = () => timeago; + /** * For the given elements, sets a tooltip with a formatted date. * @param {JQuery} $timeagoEls * @param {Boolean} setTimeago */ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { - getTimeago(); - $timeagoEls.each((i, el) => { - $(el).text(timeagoInstance.format($(el).attr('datetime'), timeagoLanguageCode)); + $(el).text(timeago.format($(el).attr('datetime'), timeagoLanguageCode)); }); if (!setTimeago) { @@ -207,9 +197,7 @@ export const timeFor = (time, expiredLabel) => { if (new Date(time) < new Date()) { return expiredLabel || s__('Timeago|Past due'); } - return getTimeago() - .format(time, `${timeagoLanguageCode}-remaining`) - .trim(); + return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim(); }; export const getDayDifference = (a, b) => { diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index f51d0fa4f52..7e5b8f14b96 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,4 +1,4 @@ -import Timeago from 'timeago.js'; +import { format } from 'timeago.js'; import _ from 'underscore'; import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; import { stateKey } from './state_maps'; @@ -213,9 +213,7 @@ export default class MergeRequestStore { return ''; } - const timeagoInstance = new Timeago(); - - return timeagoInstance.format(date); + return format(date); } static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) { diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 1fbc61cd950..0ecf5f50e86 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -53,6 +53,7 @@ module Resolvers # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 args[:project_id] = project.id args[:iids] ||= [args[:iid]].compact + args[:attempt_project_search_optimizations] = args[:search].present? IssuesFinder.new(context[:current_user], args).execute end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4679e8b74d7..7d3cb62e4ee 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -764,7 +764,7 @@ module Ci # find all jobs that are needed if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && needs.exists? - depended_jobs = depended_jobs.where(name: needs.select(:name)) + depended_jobs = depended_jobs.where(name: needs.artifacts.select(:name)) end # find all jobs that are dependent on @@ -772,6 +772,8 @@ module Ci depended_jobs = depended_jobs.where(name: options[:dependencies]) end + # if both needs and dependencies are used, + # the end result will be an intersection between them depended_jobs end diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 6531dfd332f..0b243c20e67 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -10,5 +10,6 @@ module Ci validates :name, presence: true, length: { maximum: 128 } scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') } + scope :artifacts, -> { where(artifacts: true) } end end diff --git a/app/models/milestone_release.rb b/app/models/milestone_release.rb index f7127df339d..713c8ef7b94 100644 --- a/app/models/milestone_release.rb +++ b/app/models/milestone_release.rb @@ -6,9 +6,6 @@ class MilestoneRelease < ApplicationRecord validate :same_project_between_milestone_and_release - # Keep until 2019-11-29 - self.ignored_columns += %i[id] - private def same_project_between_milestone_and_release diff --git a/changelogs/unreleased/34958-update-timeago-to-the-latest-release.yml b/changelogs/unreleased/34958-update-timeago-to-the-latest-release.yml new file mode 100644 index 00000000000..d9696e27820 --- /dev/null +++ b/changelogs/unreleased/34958-update-timeago-to-the-latest-release.yml @@ -0,0 +1,5 @@ +--- +title: Update timeago to the latest release +merge_request: 19407 +author: +type: other diff --git a/changelogs/unreleased/36905-create-a-rake-task-to-gather-license-info.yml b/changelogs/unreleased/36905-create-a-rake-task-to-gather-license-info.yml new file mode 100644 index 00000000000..abf46acbcb2 --- /dev/null +++ b/changelogs/unreleased/36905-create-a-rake-task-to-gather-license-info.yml @@ -0,0 +1,5 @@ +--- +title: Create a license info rake task +merge_request: 20501 +author: Jason Colyer +type: added diff --git a/changelogs/unreleased/ci-merge-dependencies-and-artifacts-with-needs.yml b/changelogs/unreleased/ci-merge-dependencies-and-artifacts-with-needs.yml new file mode 100644 index 00000000000..d9ba35b44c4 --- /dev/null +++ b/changelogs/unreleased/ci-merge-dependencies-and-artifacts-with-needs.yml @@ -0,0 +1,5 @@ +--- +title: Control passing artifacts from CI DAG needs +merge_request: 19943 +author: +type: added diff --git a/changelogs/unreleased/feat-increase-start-in.yml b/changelogs/unreleased/feat-increase-start-in.yml new file mode 100644 index 00000000000..d92983eeb5f --- /dev/null +++ b/changelogs/unreleased/feat-increase-start-in.yml @@ -0,0 +1,5 @@ +--- +title: Increase upper limit of start_in attribute to 1 week +merge_request: 20323 +author: Will Layton +type: changed diff --git a/changelogs/unreleased/fix-job-log-default-colors.yml b/changelogs/unreleased/fix-job-log-default-colors.yml new file mode 100644 index 00000000000..b30eac4bc4b --- /dev/null +++ b/changelogs/unreleased/fix-job-log-default-colors.yml @@ -0,0 +1,5 @@ +--- +title: Fix change to default foreground and backgorund colors in job log +merge_request: 20787 +author: +type: fixed diff --git a/changelogs/unreleased/issue_34226.yml b/changelogs/unreleased/issue_34226.yml new file mode 100644 index 00000000000..eb0e2b27b87 --- /dev/null +++ b/changelogs/unreleased/issue_34226.yml @@ -0,0 +1,5 @@ +--- +title: Improve issues search performance on GraphQL +merge_request: 20784 +author: +type: performance diff --git a/changelogs/unreleased/security-394-path-traversal-package-bug.yml b/changelogs/unreleased/security-394-path-traversal-package-bug.yml new file mode 100644 index 00000000000..887f1cded25 --- /dev/null +++ b/changelogs/unreleased/security-394-path-traversal-package-bug.yml @@ -0,0 +1,5 @@ +--- +title: Add maven file_name regex validation on incoming files +merge_request: +author: +type: security diff --git a/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb b/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb new file mode 100644 index 00000000000..2fbd003b2e5 --- /dev/null +++ b/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddArtifactsToCiBuildNeed < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:ci_build_needs, :artifacts, + :boolean, + default: true, + allow_null: false) + end + + def down + remove_column(:ci_build_needs, :artifacts) + end +end diff --git a/db/schema.rb b/db/schema.rb index 090e9586686..57d05abd980 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -601,6 +601,7 @@ ActiveRecord::Schema.define(version: 2019_11_24_150431) do create_table "ci_build_needs", id: :serial, force: :cascade do |t| t.integer "build_id", null: false t.text "name", null: false + t.boolean "artifacts", default: true, null: false t.index ["build_id", "name"], name: "index_ci_build_needs_on_build_id_and_name", unique: true end diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index fb9dd7792c3..12c009d9e90 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1245,11 +1245,12 @@ Delayed job are for executing scripts after a certain period. This is useful if you want to avoid jobs entering `pending` state immediately. You can set the period with `start_in` key. The value of `start_in` key is an elapsed time in seconds, unless a unit is -provided. `start_in` key must be less than or equal to one hour. Examples of valid values include: +provided. `start_in` key must be less than or equal to one week. Examples of valid values include: - `10 seconds` - `30 minutes` -- `1 hour` +- `1 day` +- `1 week` When there is a delayed job in a stage, the pipeline will not progress until the delayed job has finished. This means this keyword can also be used for inserting delays between different stages. @@ -2232,6 +2233,49 @@ This example creates three paths of execution: - Related to the above, stages must be explicitly defined for all jobs that have the keyword `needs:` or are referred to by one. +#### Artifact downloads with `needs` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14311) in GitLab v12.6. + +When using `needs`, artifact downloads are controlled with `artifacts: true` or `artifacts: false`. +The `dependencies` keyword should not be used with `needs`, as this is deprecated since GitLab 12.6. + +In the example below, the `rspec` job will download the `build_job` artifacts, while the +`rubocop` job will not: + +```yaml +build_job: + stage: build + artifacts: + paths: + - binaries/ + +rspec: + stage: test + needs: + - job: build_job + artifacts: true + +rubocop: + stage: test + needs: + - job: build_job + artifacts: false +``` + +Additionally, in the three syntax examples below, the `rspec` job will download the artifacts +from all three `build_jobs`, as `artifacts` is true for `build_job_1`, and will +**default** to true for both `build_job_2` and `build_job_3`. + +```yaml +rspec: + needs: + - job: build_job_1 + artifacts: true + - job: build_job_2 + - build_job_3 +``` + ### `coverage` > [Introduced][ce-7447] in GitLab 8.17. diff --git a/lib/gitlab/ci/ansi2json/parser.rb b/lib/gitlab/ci/ansi2json/parser.rb index d428680fb2a..79b42a5f5bf 100644 --- a/lib/gitlab/ci/ansi2json/parser.rb +++ b/lib/gitlab/ci/ansi2json/parser.rb @@ -94,7 +94,7 @@ module Gitlab def on_38(stack) { fg: fg_color_256(stack) } end - def on_39(_) { fg: fg_color(9) } end + def on_39(_) { fg: nil } end def on_40(_) { bg: bg_color(0) } end @@ -114,8 +114,7 @@ module Gitlab def on_48(stack) { bg: bg_color_256(stack) } end - # TODO: all the x9 never get called? - def on_49(_) { fg: fg_color(9) } end + def on_49(_) { bg: nil } end def on_90(_) { fg: fg_color(0, 'l') } end diff --git a/lib/gitlab/ci/ansi2json/style.rb b/lib/gitlab/ci/ansi2json/style.rb index 77f61178b37..4d38ea55866 100644 --- a/lib/gitlab/ci/ansi2json/style.rb +++ b/lib/gitlab/ci/ansi2json/style.rb @@ -61,9 +61,9 @@ module Gitlab case when changes[:reset] reset! - when changes[:fg] + when changes.key?(:fg) @fg = changes[:fg] - when changes[:bg] + when changes.key?(:bg) @bg = changes[:bg] when changes[:enable] @mask |= changes[:enable] diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 7c01e6ffbe8..1aa2d6d0bcb 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -51,7 +51,7 @@ module Gitlab validates :rules, array_of_hashes: true end - validates :start_in, duration: { limit: '1 day' }, if: :delayed? + validates :start_in, duration: { limit: '1 week' }, if: :delayed? validates :start_in, absence: true, if: -> { has_rules? || !delayed? } validate do diff --git a/lib/gitlab/ci/config/entry/need.rb b/lib/gitlab/ci/config/entry/need.rb index b6db546d8ff..61bd09fd5f3 100644 --- a/lib/gitlab/ci/config/entry/need.rb +++ b/lib/gitlab/ci/config/entry/need.rb @@ -5,9 +5,10 @@ module Gitlab class Config module Entry class Need < ::Gitlab::Config::Entry::Simplifiable - strategy :Job, if: -> (config) { config.is_a?(String) } + strategy :JobString, if: -> (config) { config.is_a?(String) } + strategy :JobHash, if: -> (config) { config.is_a?(Hash) && config.key?(:job) } - class Job < ::Gitlab::Config::Entry::Node + class JobString < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable validations do @@ -20,7 +21,30 @@ module Gitlab end def value - { name: @config } + { name: @config, artifacts: true } + end + end + + class JobHash < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[job artifacts].freeze + attributes :job, :artifacts + + validations do + validates :config, presence: true + validates :config, allowed_keys: ALLOWED_KEYS + validates :job, type: String, presence: true + validates :artifacts, boolean: true, allow_nil: true + end + + def type + :job + end + + def value + { name: job, artifacts: artifacts || artifacts.nil? } end end diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb index e714ef225f5..1139efee9e8 100644 --- a/lib/gitlab/ci/config/normalizer.rb +++ b/lib/gitlab/ci/config/normalizer.rb @@ -44,7 +44,7 @@ module Gitlab if all_job_names = parallelized_jobs[job_need_name] all_job_names.map do |job_name| - { name: job_name } + job_need.merge(name: job_name) end else job_need diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1a229a72165..0518b7450b3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6452,6 +6452,9 @@ msgstr "" msgid "Enter merge request URLs" msgstr "" +msgid "Enter new AWS Secret Access Key" +msgstr "" + msgid "Enter the issue description" msgstr "" diff --git a/package.json b/package.json index e5f36099833..b92b2c9b06e 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,7 @@ "three": "^0.84.0", "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", - "timeago.js": "^3.0.2", + "timeago.js": "^4.0.1", "tiptap": "^1.8.0", "tiptap-commands": "^1.4.0", "tiptap-extensions": "^1.8.0", diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js index fd75c9aa0cd..872779299d2 100644 --- a/spec/frontend/lib/utils/datetime_utility_spec.js +++ b/spec/frontend/lib/utils/datetime_utility_spec.js @@ -1,15 +1,16 @@ +import { __, s__ } from '~/locale'; import * as datetimeUtility from '~/lib/utils/datetime_utility'; describe('Date time utils', () => { describe('timeFor', () => { - it('returns `past due` when in past', () => { + it('returns localize `past due` when in past', () => { const date = new Date(); date.setFullYear(date.getFullYear() - 1); - expect(datetimeUtility.timeFor(date)).toBe('Past due'); + expect(datetimeUtility.timeFor(date)).toBe(s__('Timeago|Past due')); }); - it('returns remaining time when in the future', () => { + it('returns localized remaining time when in the future', () => { const date = new Date(); date.setFullYear(date.getFullYear() + 1); @@ -17,51 +18,51 @@ describe('Date time utils', () => { // short of a full year, timeFor will return '11 months remaining' date.setDate(date.getDate() + 1); - expect(datetimeUtility.timeFor(date)).toBe('1 year remaining'); + expect(datetimeUtility.timeFor(date)).toBe(s__('Timeago|1 year remaining')); }); }); - describe('get day name', () => { + describe('get localized day name', () => { it('should return Sunday', () => { const day = datetimeUtility.getDayName(new Date('07/17/2016')); - expect(day).toBe('Sunday'); + expect(day).toBe(__('Sunday')); }); it('should return Monday', () => { const day = datetimeUtility.getDayName(new Date('07/18/2016')); - expect(day).toBe('Monday'); + expect(day).toBe(__('Monday')); }); it('should return Tuesday', () => { const day = datetimeUtility.getDayName(new Date('07/19/2016')); - expect(day).toBe('Tuesday'); + expect(day).toBe(__('Tuesday')); }); it('should return Wednesday', () => { const day = datetimeUtility.getDayName(new Date('07/20/2016')); - expect(day).toBe('Wednesday'); + expect(day).toBe(__('Wednesday')); }); it('should return Thursday', () => { const day = datetimeUtility.getDayName(new Date('07/21/2016')); - expect(day).toBe('Thursday'); + expect(day).toBe(__('Thursday')); }); it('should return Friday', () => { const day = datetimeUtility.getDayName(new Date('07/22/2016')); - expect(day).toBe('Friday'); + expect(day).toBe(__('Friday')); }); it('should return Saturday', () => { const day = datetimeUtility.getDayName(new Date('07/23/2016')); - expect(day).toBe('Saturday'); + expect(day).toBe(__('Saturday')); }); }); @@ -114,10 +115,10 @@ describe('Date time utils', () => { describe('timeIntervalInWords', () => { it('should return string with number of minutes and seconds', () => { - expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds'); - expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second'); - expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); - expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual(s__('Timeago|9 seconds')); + expect(datetimeUtility.timeIntervalInWords(1)).toEqual(s__('Timeago|1 second')); + expect(datetimeUtility.timeIntervalInWords(200)).toEqual(s__('Timeago|3 minutes 20 seconds')); + expect(datetimeUtility.timeIntervalInWords(6008)).toEqual(s__('Timeago|100 minutes 8 seconds')); }); }); @@ -125,15 +126,15 @@ describe('dateInWords', () => { const date = new Date('07/01/2016'); it('should return date in words', () => { - expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016'); + expect(datetimeUtility.dateInWords(date)).toEqual(s__('July 1, 2016')); }); it('should return abbreviated month name', () => { - expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); + expect(datetimeUtility.dateInWords(date, true)).toEqual(s__('Jul 1, 2016')); }); it('should return date in words without year', () => { - expect(datetimeUtility.dateInWords(date, true, true)).toEqual('Jul 1'); + expect(datetimeUtility.dateInWords(date, true, true)).toEqual(s__('Jul 1')); }); }); @@ -141,11 +142,11 @@ describe('monthInWords', () => { const date = new Date('2017-01-20'); it('returns month name from provided date', () => { - expect(datetimeUtility.monthInWords(date)).toBe('January'); + expect(datetimeUtility.monthInWords(date)).toBe(s__('January')); }); it('returns abbreviated month name from provided date', () => { - expect(datetimeUtility.monthInWords(date, true)).toBe('Jan'); + expect(datetimeUtility.monthInWords(date, true)).toBe(s__('Jan')); }); }); diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index bf9106643eb..9e75a6cad60 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -68,8 +68,22 @@ describe Resolvers::IssuesResolver do end end - it 'searches issues' do - expect(resolve_issues(search: 'foo')).to contain_exactly(issue2) + context 'when searching issues' do + it 'returns correct issues' do + expect(resolve_issues(search: 'foo')).to contain_exactly(issue2) + end + + it 'uses project search optimization' do + expected_arguments = { + search: 'foo', + attempt_project_search_optimizations: true, + iids: [], + project_id: project.id + } + expect(IssuesFinder).to receive(:new).with(anything, expected_arguments).and_call_original + + resolve_issues(search: 'foo') + end end describe 'sorting' do diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index f9ee4648128..4ab9a3998c0 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -1,4 +1,4 @@ -import 'timeago.js'; +import { format } from 'timeago.js'; import Vue from 'vue'; import environmentItemComp from '~/environments/components/environment_item.vue'; @@ -139,8 +139,7 @@ describe('Environment item', () => { }); it('should render last deployment date', () => { - const timeagoInstance = new timeago(); // eslint-disable-line - const formatedDate = timeagoInstance.format(environment.last_deployment.deployed_at); + const formatedDate = format(environment.last_deployment.deployed_at); expect( component.$el.querySelector('.environment-created-date-timeago').textContent, diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb index 5110c215415..ad05aa03e83 100644 --- a/spec/lib/gitlab/ci/ansi2json/style_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb @@ -147,6 +147,10 @@ describe Gitlab::Ci::Ansi2json::Style do [%w[1], %w[0], '', 'resets style from format bold'], [%w[1 3], %w[0], '', 'resets style from format bold and italic'], [%w[1 3 term-fg-l-red term-bg-yellow], %w[0], '', 'resets all formats and colors'], + # default foreground + [%w[31 42], %w[39], 'term-bg-green', 'set foreground from red to default leaving background unchanged'], + # default background + [%w[31 42], %w[49], 'term-fg-red', 'set background from green to default leaving foreground unchanged'], # misc [[], %w[1 30 42 3], 'term-fg-l-black term-bg-green term-bold term-italic', 'adds fg color, bg color and formats from no style'], [%w[3 31], %w[23 1 43], 'term-fg-l-red term-bg-yellow term-bold', 'replaces format italic with bold and adds a yellow background'] diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index b0e08e49d78..4d6245c2d86 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -93,7 +93,7 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when delayed job' do context 'when start_in is specified' do - let(:config) { { script: 'echo', when: 'delayed', start_in: '1 day' } } + let(:config) { { script: 'echo', when: 'delayed', start_in: '1 week' } } it { expect(entry).to be_valid } end @@ -232,11 +232,9 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when delayed job' do context 'when start_in is specified' do - let(:config) { { script: 'echo', when: 'delayed', start_in: '1 day' } } + let(:config) { { script: 'echo', when: 'delayed', start_in: '1 week' } } - it 'returns error about invalid type' do - expect(entry).to be_valid - end + it { expect(entry).to be_valid } end context 'when start_in is empty' do @@ -257,8 +255,8 @@ describe Gitlab::Ci::Config::Entry::Job do end end - context 'when start_in is longer than one day' do - let(:config) { { when: 'delayed', start_in: '2 days' } } + context 'when start_in is longer than one week' do + let(:config) { { when: 'delayed', start_in: '8 days' } } it 'returns error about exceeding the limit' do expect(entry).not_to be_valid diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb index d119e604900..92b71c5f6cc 100644 --- a/spec/lib/gitlab/ci/config/entry/need_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb @@ -5,31 +5,177 @@ require 'spec_helper' describe ::Gitlab::Ci::Config::Entry::Need do subject(:need) { described_class.new(config) } - context 'when job is specified' do - let(:config) { 'job_name' } + shared_examples 'job type' do + describe '#type' do + subject(:need_type) { need.type } - describe '#valid?' do - it { is_expected.to be_valid } + it { is_expected.to eq(:job) } + end + end + + context 'with simple config' do + context 'when job is specified' do + let(:config) { 'job_name' } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'when need is empty' do + let(:config) { '' } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about an empty config' do + expect(need.errors) + .to contain_exactly("job string config can't be blank") + end + end + + it_behaves_like 'job type' end + end + + context 'with complex config' do + context 'with job name and artifacts true' do + let(:config) { { job: 'job_name', artifacts: true } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'with job name and artifacts false' do + let(:config) { { job: 'job_name', artifacts: false } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: false) + end + end + + it_behaves_like 'job type' + end + + context 'with job name and artifacts nil' do + let(:config) { { job: 'job_name', artifacts: nil } } - describe '#value' do - it 'returns job needs configuration' do - expect(need.value).to eq(name: 'job_name') + describe '#valid?' do + it { is_expected.to be_valid } end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'without artifacts key' do + let(:config) { { job: 'job_name' } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'when job name is empty' do + let(:config) { { job: '', artifacts: true } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about an empty config' do + expect(need.errors) + .to contain_exactly("job hash job can't be blank") + end + end + + it_behaves_like 'job type' + end + + context 'when job name is not a string' do + let(:config) { { job: :job_name, artifacts: false } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about job type' do + expect(need.errors) + .to contain_exactly('job hash job should be a string') + end + end + + it_behaves_like 'job type' + end + + context 'when job has unknown keys' do + let(:config) { { job: 'job_name', artifacts: false, some: :key } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about job type' do + expect(need.errors) + .to contain_exactly('job hash config contains unknown keys: some') + end + end + + it_behaves_like 'job type' end end - context 'when need is empty' do - let(:config) { '' } + context 'when need config is not a string or a hash' do + let(:config) { :job_name } describe '#valid?' do it { is_expected.not_to be_valid } end describe '#errors' do - it 'is returns an error about an empty config' do + it 'is returns an error about job type' do expect(need.errors) - .to contain_exactly("job config can't be blank") + .to contain_exactly('unknown strategy has an unsupported type') end end end diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb index f4a76b52d30..b8b84b5efd2 100644 --- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb @@ -51,9 +51,34 @@ describe ::Gitlab::Ci::Config::Entry::Needs do end end end + + context 'when wrong needs type is used' do + let(:config) { [{ job: 'job_name', artifacts: true, some: :key }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(needs.errors).to contain_exactly( + 'need config contains unknown keys: some') + end + end + end end describe '.compose!' do + shared_examples 'entry with descendant nodes' do + describe '#descendants' do + it 'creates valid descendant nodes' do + expect(needs.descendants.count).to eq 2 + expect(needs.descendants) + .to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need)) + end + end + end + context 'when valid job entries composed' do let(:config) { %w[first_job_name second_job_name] } @@ -65,18 +90,80 @@ describe ::Gitlab::Ci::Config::Entry::Needs do it 'returns key value' do expect(needs.value).to eq( job: [ - { name: 'first_job_name' }, - { name: 'second_job_name' } + { name: 'first_job_name', artifacts: true }, + { name: 'second_job_name', artifacts: true } ] ) end end - describe '#descendants' do - it 'creates valid descendant nodes' do - expect(needs.descendants.count).to eq 2 - expect(needs.descendants) - .to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need)) + it_behaves_like 'entry with descendant nodes' + end + + context 'with complex job entries composed' do + let(:config) do + [ + { job: 'first_job_name', artifacts: true }, + { job: 'second_job_name', artifacts: false } + ] + end + + before do + needs.compose! + end + + describe '#value' do + it 'returns key value' do + expect(needs.value).to eq( + job: [ + { name: 'first_job_name', artifacts: true }, + { name: 'second_job_name', artifacts: false } + ] + ) + end + end + + it_behaves_like 'entry with descendant nodes' + end + + context 'with mixed job entries composed' do + let(:config) do + [ + 'first_job_name', + { job: 'second_job_name', artifacts: false } + ] + end + + before do + needs.compose! + end + + describe '#value' do + it 'returns key value' do + expect(needs.value).to eq( + job: [ + { name: 'first_job_name', artifacts: true }, + { name: 'second_job_name', artifacts: false } + ] + ) + end + end + + it_behaves_like 'entry with descendant nodes' + end + + context 'with empty config' do + let(:config) do + [] + end + + before do + needs.compose! + end + + describe '#value' do + it 'returns empty value' do + expect(needs.value).to eq({}) end end end diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index bf880478387..db62fb7524d 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -105,7 +105,7 @@ describe Gitlab::Ci::Config::Normalizer do context 'for needs' do let(:expanded_job_attributes) do expanded_job_names.map do |job_name| - { name: job_name } + { name: job_name, extra: :key } end end @@ -117,7 +117,7 @@ describe Gitlab::Ci::Config::Normalizer do script: 'echo 1', needs: { job: [ - { name: job_name.to_s } + { name: job_name.to_s, extra: :key } ] } } @@ -140,8 +140,8 @@ describe Gitlab::Ci::Config::Normalizer do script: 'echo 1', needs: { job: [ - { name: job_name.to_s }, - { name: "other_job" } + { name: job_name.to_s, extra: :key }, + { name: "other_job", extra: :key } ] } } @@ -153,7 +153,7 @@ describe Gitlab::Ci::Config::Normalizer do end it "includes the regular job in dependencies" do - expect(subject.dig(:final_job, :needs, :job)).to include(name: 'other_job') + expect(subject.dig(:final_job, :needs, :job)).to include(name: 'other_job', extra: :key) end end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 5dc51f83b3c..ed2d97b1a38 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1525,8 +1525,48 @@ module Gitlab name: "test1", options: { script: ["test"] }, needs_attributes: [ - { name: "build1" }, - { name: "build2" } + { name: "build1", artifacts: true }, + { name: "build2", artifacts: true } + ], + when: "on_success", + allow_failure: false, + yaml_variables: [] + ) + end + end + + context 'needs two builds' do + let(:needs) do + [ + { job: 'parallel', artifacts: false }, + { job: 'build1', artifacts: true }, + 'build2' + ] + end + + it "does create jobs with valid specification" do + expect(subject.builds.size).to eq(7) + expect(subject.builds[0]).to eq( + stage: "build", + stage_idx: 1, + name: "build1", + options: { + script: ["test"] + }, + when: "on_success", + allow_failure: false, + yaml_variables: [] + ) + expect(subject.builds[4]).to eq( + stage: "test", + stage_idx: 2, + name: "test1", + options: { script: ["test"] }, + needs_attributes: [ + { name: "parallel 1/2", artifacts: false }, + { name: "parallel 2/2", artifacts: false }, + { name: "build1", artifacts: true }, + { name: "build2", artifacts: true } ], when: "on_success", allow_failure: false, @@ -1546,8 +1586,37 @@ module Gitlab name: "test1", options: { script: ["test"] }, needs_attributes: [ - { name: "parallel 1/2" }, - { name: "parallel 2/2" } + { name: "parallel 1/2", artifacts: true }, + { name: "parallel 2/2", artifacts: true } + ], + when: "on_success", + allow_failure: false, + yaml_variables: [] + ) + end + end + + context 'needs dependencies artifacts' do + let(:needs) do + [ + "build1", + { job: "build2" }, + { job: "parallel", artifacts: true } + ] + end + + it "does create jobs with valid specification" do + expect(subject.builds.size).to eq(7) + expect(subject.builds[4]).to eq( + stage: "test", + stage_idx: 2, + name: "test1", + options: { script: ["test"] }, + needs_attributes: [ + { name: "build1", artifacts: true }, + { name: "build2", artifacts: true }, + { name: "parallel 1/2", artifacts: true }, + { name: "parallel 2/2", artifacts: true } ], when: "on_success", allow_failure: false, diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb index 450dd550a8f..d1186fa981d 100644 --- a/spec/models/ci/build_need_spec.rb +++ b/spec/models/ci/build_need_spec.rb @@ -10,4 +10,11 @@ describe Ci::BuildNeed, model: true do it { is_expected.to validate_presence_of(:build) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(128) } + + describe '.artifacts' do + let_it_be(:with_artifacts) { create(:ci_build_need, artifacts: true) } + let_it_be(:without_artifacts) { create(:ci_build_need, artifacts: false) } + + it { expect(described_class.artifacts).to contain_exactly(with_artifacts) } + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index d491300add2..916f5536ebd 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -741,20 +741,26 @@ describe Ci::Build do before do needs.to_a.each do |need| - create(:ci_build_need, build: final, name: need) + create(:ci_build_need, build: final, **need) end end subject { final.dependencies } - context 'when depedencies are defined' do + context 'when dependencies are defined' do let(:dependencies) { %w(rspec staging) } it { is_expected.to contain_exactly(rspec_test, staging) } end context 'when needs are defined' do - let(:needs) { %w(build rspec staging) } + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: true }, + { name: 'staging', artifacts: true } + ] + end it { is_expected.to contain_exactly(build, rspec_test, staging) } @@ -767,13 +773,44 @@ describe Ci::Build do end end + context 'when need artifacts are defined' do + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: false }, + { name: 'staging', artifacts: true } + ] + end + + it { is_expected.to contain_exactly(build, staging) } + end + context 'when needs and dependencies are defined' do let(:dependencies) { %w(rspec staging) } - let(:needs) { %w(build rspec staging) } + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: true }, + { name: 'staging', artifacts: true } + ] + end it { is_expected.to contain_exactly(rspec_test, staging) } end + context 'when needs and dependencies contradict' do + let(:dependencies) { %w(rspec staging) } + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: false }, + { name: 'staging', artifacts: true } + ] + end + + it { is_expected.to contain_exactly(staging) } + end + context 'when nor dependencies or needs are defined' do it { is_expected.to contain_exactly(build, rspec_test, rubocop_test, staging) } end diff --git a/spec/services/ci/create_pipeline_service/needs_spec.rb b/spec/services/ci/create_pipeline_service/needs_spec.rb new file mode 100644 index 00000000000..5ef7e592b36 --- /dev/null +++ b/spec/services/ci/create_pipeline_service/needs_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::CreatePipelineService do + context 'needs' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository, creator: user) } + + let(:ref) { 'refs/heads/master' } + let(:source) { :push } + let(:service) { described_class.new(project, user, { ref: ref }) } + let(:pipeline) { service.execute(source) } + + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'with a valid config' do + let(:config) do + <<~YAML + build_a: + stage: build + script: + - make + artifacts: + paths: + - binaries/ + build_b: + stage: build + script: + - make + artifacts: + paths: + - other_binaries/ + build_c: + stage: build + script: + - make + build_d: + stage: build + script: + - make + parallel: 3 + + test_a: + stage: test + script: + - ls + needs: + - build_a + - job: build_b + artifacts: true + - job: build_c + artifacts: false + dependencies: + - build_a + + test_b: + stage: test + script: + - ls + parallel: 2 + needs: + - build_a + - job: build_b + artifacts: true + - job: build_d + artifacts: false + + test_c: + stage: test + script: + - ls + needs: + - build_a + - job: build_b + - job: build_c + artifacts: true + YAML + end + + let(:test_a_build) { pipeline.builds.find_by!(name: 'test_a') } + + it 'creates a pipeline with builds' do + expected_builds = [ + 'build_a', 'build_b', 'build_c', 'build_d 1/3', 'build_d 2/3', + 'build_d 3/3', 'test_a', 'test_b 1/2', 'test_b 2/2', 'test_c' + ] + + expect(pipeline).to be_persisted + expect(pipeline.builds.pluck(:name)).to contain_exactly(*expected_builds) + end + + it 'saves needs' do + expect(test_a_build.needs.map(&:attributes)) + .to contain_exactly( + a_hash_including('name' => 'build_a', 'artifacts' => true), + a_hash_including('name' => 'build_b', 'artifacts' => true), + a_hash_including('name' => 'build_c', 'artifacts' => false) + ) + end + + it 'saves dependencies' do + expect(test_a_build.options) + .to match(a_hash_including('dependencies' => ['build_a'])) + end + + it 'artifacts default to true' do + test_job = pipeline.builds.find_by!(name: 'test_c') + + expect(test_job.needs.map(&:attributes)) + .to contain_exactly( + a_hash_including('name' => 'build_a', 'artifacts' => true), + a_hash_including('name' => 'build_b', 'artifacts' => true), + a_hash_including('name' => 'build_c', 'artifacts' => true) + ) + end + + it 'saves parallel jobs' do + ['1/2', '2/2'].each do |part| + test_job = pipeline.builds.find_by(name: "test_b #{part}") + + expect(test_job.needs.map(&:attributes)) + .to contain_exactly( + a_hash_including('name' => 'build_a', 'artifacts' => true), + a_hash_including('name' => 'build_b', 'artifacts' => true), + a_hash_including('name' => 'build_d 1/3', 'artifacts' => false), + a_hash_including('name' => 'build_d 2/3', 'artifacts' => false), + a_hash_including('name' => 'build_d 3/3', 'artifacts' => false) + ) + end + end + end + + context 'with an invalid config' do + let(:config) do + <<~YAML + build_a: + stage: build + script: + - make + artifacts: + paths: + - binaries/ + + build_b: + stage: build + script: + - make + artifacts: + paths: + - other_binaries/ + + test_a: + stage: test + script: + - ls + needs: + - build_a + - job: build_b + artifacts: string + YAML + end + + it { expect(pipeline).to be_persisted } + it { expect(pipeline.builds.any?).to be_falsey } + + it 'assigns an error to the pipeline' do + expect(pipeline.yaml_errors) + .to eq('jobs:test_a:needs:need artifacts should be a boolean value') + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 9414587c3a3..df27000b93f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1047,11 +1047,6 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jquery@^2.0.40": - version "2.0.48" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef" - integrity sha512-nNLzUrVjaRV/Ds1eHZLYTd7IZxs38cwwLSaqMJj8OTXY8xNUbxSK69bi9cMLvQ7dm/IBeQ1wHwQ0S1uYa0rd2w== - "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -10877,12 +10872,10 @@ thunky@^0.1.0: resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e" integrity sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4= -timeago.js@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-3.0.2.tgz#32a67e7c0d887ea42ca588d3aae26f77de5e76cc" - integrity sha1-MqZ+fA2IfqQspYjTquJvd95edsw= - dependencies: - "@types/jquery" "^2.0.40" +timeago.js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-4.0.1.tgz#4be4aa19565ceaeb0da31fe14e01ce6ca4742da6" + integrity sha512-ePzZuMoJqUc44hJbUYtY1qtzU7IammxooDCcFKogLkS5Nj+iCabR0ZlmNOFX8Dm1r5EpvR5q/PotOJli/mEPew== timed-out@^4.0.0: version "4.0.1" |