Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b744b11e7d
commit
0a0fc6fa1a
|
@ -7,7 +7,7 @@ workflow:
|
||||||
include:
|
include:
|
||||||
- local: .gitlab/ci/version.yml
|
- local: .gitlab/ci/version.yml
|
||||||
- local: .gitlab/ci/global.gitlab-ci.yml
|
- local: .gitlab/ci/global.gitlab-ci.yml
|
||||||
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@11.9.0"
|
- component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@11.10.0"
|
||||||
inputs:
|
inputs:
|
||||||
job_name: "e2e-test-report"
|
job_name: "e2e-test-report"
|
||||||
job_stage: "report"
|
job_stage: "report"
|
||||||
|
@ -17,7 +17,7 @@ include:
|
||||||
gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
|
gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
|
||||||
allure_job_name: "${QA_RUN_TYPE}"
|
allure_job_name: "${QA_RUN_TYPE}"
|
||||||
- project: gitlab-org/quality/pipeline-common
|
- project: gitlab-org/quality/pipeline-common
|
||||||
ref: 11.9.0
|
ref: 11.10.0
|
||||||
file:
|
file:
|
||||||
- /ci/notify-slack.gitlab-ci.yml
|
- /ci/notify-slack.gitlab-ci.yml
|
||||||
- /ci/qa-report.gitlab-ci.yml
|
- /ci/qa-report.gitlab-ci.yml
|
||||||
|
|
|
@ -149,7 +149,7 @@ detect-tests:
|
||||||
if [ -n "$CI_MERGE_REQUEST_IID" ] || [ -n "$FIND_CHANGES_MERGE_REQUEST_IID" ]; then
|
if [ -n "$CI_MERGE_REQUEST_IID" ] || [ -n "$FIND_CHANGES_MERGE_REQUEST_IID" ]; then
|
||||||
mkdir -p $(dirname "$RSPEC_CHANGED_FILES_PATH")
|
mkdir -p $(dirname "$RSPEC_CHANGED_FILES_PATH")
|
||||||
|
|
||||||
tooling/bin/predictive_tests --select-tests --with-crystalball-mappings --mapping-type $MAPPING_TYPE
|
tooling/bin/predictive_tests --ci --select-tests --with-crystalball-mappings --mapping-type $MAPPING_TYPE
|
||||||
|
|
||||||
filter_rspec_matched_foss_tests ${RSPEC_MATCHING_TEST_FILES_PATH} ${RSPEC_MATCHING_TESTS_FOSS_PATH};
|
filter_rspec_matched_foss_tests ${RSPEC_MATCHING_TEST_FILES_PATH} ${RSPEC_MATCHING_TESTS_FOSS_PATH};
|
||||||
filter_rspec_matched_ee_tests ${RSPEC_MATCHING_TEST_FILES_PATH} ${RSPEC_MATCHING_TESTS_EE_PATH};
|
filter_rspec_matched_ee_tests ${RSPEC_MATCHING_TEST_FILES_PATH} ${RSPEC_MATCHING_TESTS_EE_PATH};
|
||||||
|
|
|
@ -81,27 +81,17 @@ export-predictive-test-metrics:
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
dependencies: []
|
dependencies: []
|
||||||
variables:
|
variables:
|
||||||
GLCI_CRYSTALBALL_MAPPING_DIR: "tmp/crystalball_mappings"
|
|
||||||
GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR: "tmp/predictive_tests"
|
GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR: "tmp/predictive_tests"
|
||||||
GLCI_ALL_FAILED_RSPEC_TESTS_FILE: "${GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR}/rspec_all_failed_tests.txt"
|
GLCI_ALL_FAILED_RSPEC_TESTS_FILE: "${GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR}/rspec_all_failed_tests.txt"
|
||||||
before_script:
|
before_script:
|
||||||
- apt update && apt install -y curl
|
|
||||||
- source ./scripts/utils.sh
|
- source ./scripts/utils.sh
|
||||||
- source ./scripts/rspec_helpers.sh
|
- source ./scripts/rspec_helpers.sh
|
||||||
- retrieve_failed_tests "${GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR}" "oneline" "latest"
|
- retrieve_failed_tests "${GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR}" "oneline" "latest"
|
||||||
- retrieve_frontend_fixtures_mapping
|
|
||||||
- |
|
|
||||||
mkdir -p "$GLCI_CRYSTALBALL_MAPPING_DIR/described_class"
|
|
||||||
retrieve_tests_mapping "$RSPEC_PACKED_TESTS_MAPPING_PATH" "$GLCI_CRYSTALBALL_MAPPING_DIR/described_class/mapping.json"
|
|
||||||
- |
|
|
||||||
mkdir -p "$GLCI_CRYSTALBALL_MAPPING_DIR/coverage"
|
|
||||||
retrieve_tests_mapping "$RSPEC_PACKED_TESTS_MAPPING_ALT_PATH" "$GLCI_CRYSTALBALL_MAPPING_DIR/coverage/mapping.json"
|
|
||||||
script:
|
script:
|
||||||
- tooling/bin/predictive_tests --export-predictive-backend-metrics
|
- tooling/bin/predictive_tests --export-predictive-backend-metrics
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: 7d
|
expire_in: 7d
|
||||||
paths:
|
paths:
|
||||||
- $GLCI_CRYSTALBALL_MAPPING_DIR
|
|
||||||
- $GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR
|
- $GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR
|
||||||
|
|
||||||
export-predictive-test-metrics-frontend:
|
export-predictive-test-metrics-frontend:
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -231,7 +231,7 @@ gem 'google-apis-serviceusage_v1', '~> 0.28.0', feature_category: :shared
|
||||||
gem 'google-apis-sqladmin_v1beta4', '~> 0.41.0', feature_category: :shared
|
gem 'google-apis-sqladmin_v1beta4', '~> 0.41.0', feature_category: :shared
|
||||||
gem 'google-apis-androidpublisher_v3', '~> 0.34.0', feature_category: :shared
|
gem 'google-apis-androidpublisher_v3', '~> 0.34.0', feature_category: :shared
|
||||||
|
|
||||||
gem 'googleauth', '~> 1.8.1', feature_category: :shared
|
gem 'googleauth', '~> 1.14', feature_category: :shared
|
||||||
gem 'google-cloud-artifact_registry-v1', '~> 0.11.0', feature_category: :shared
|
gem 'google-cloud-artifact_registry-v1', '~> 0.11.0', feature_category: :shared
|
||||||
gem 'google-cloud-compute-v1', '~> 2.6.0', feature_category: :shared
|
gem 'google-cloud-compute-v1', '~> 2.6.0', feature_category: :shared
|
||||||
|
|
||||||
|
|
|
@ -264,12 +264,13 @@
|
||||||
{"name":"google-cloud-common","version":"1.1.0","platform":"ruby","checksum":"738db08fd144b4fe37b4578ffd63308b64a86fd59f6979d240048f917a6fb5fb"},
|
{"name":"google-cloud-common","version":"1.1.0","platform":"ruby","checksum":"738db08fd144b4fe37b4578ffd63308b64a86fd59f6979d240048f917a6fb5fb"},
|
||||||
{"name":"google-cloud-compute-v1","version":"2.6.0","platform":"ruby","checksum":"b96059b33ffc2f25644d20161a0c1aa1331197073c2e44786b18f8b670f1141e"},
|
{"name":"google-cloud-compute-v1","version":"2.6.0","platform":"ruby","checksum":"b96059b33ffc2f25644d20161a0c1aa1331197073c2e44786b18f8b670f1141e"},
|
||||||
{"name":"google-cloud-core","version":"1.7.0","platform":"ruby","checksum":"748028a48530ea5bce159722eb7a02cd0562f1c52f0569e9ed69da3cba6b4f35"},
|
{"name":"google-cloud-core","version":"1.7.0","platform":"ruby","checksum":"748028a48530ea5bce159722eb7a02cd0562f1c52f0569e9ed69da3cba6b4f35"},
|
||||||
{"name":"google-cloud-env","version":"2.1.1","platform":"ruby","checksum":"cf4bb8c7d517ee1ea692baedf06e0b56ce68007549d8d5a66481aa9f97f46999"},
|
{"name":"google-cloud-env","version":"2.2.1","platform":"ruby","checksum":"3c6062aee0b5c863b83f3ce125ea7831507aadf1af7c0d384b74a116c4f649cf"},
|
||||||
{"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"},
|
{"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"},
|
||||||
{"name":"google-cloud-location","version":"0.6.0","platform":"ruby","checksum":"386c99ca156e5cac413731c055d7d9c55629860129ad7658a2bf39ea5004d2d0"},
|
{"name":"google-cloud-location","version":"0.6.0","platform":"ruby","checksum":"386c99ca156e5cac413731c055d7d9c55629860129ad7658a2bf39ea5004d2d0"},
|
||||||
{"name":"google-cloud-storage","version":"1.45.0","platform":"ruby","checksum":"f280abda4e608f9e91433f9dd907be4a45cdbf251ffeb275d713548e515c6300"},
|
{"name":"google-cloud-storage","version":"1.45.0","platform":"ruby","checksum":"f280abda4e608f9e91433f9dd907be4a45cdbf251ffeb275d713548e515c6300"},
|
||||||
{"name":"google-cloud-storage_transfer","version":"1.2.0","platform":"ruby","checksum":"132901f50889e02a0d378e6117c6408cbfc4fdbd15c9d31fabec4f4189ef1658"},
|
{"name":"google-cloud-storage_transfer","version":"1.2.0","platform":"ruby","checksum":"132901f50889e02a0d378e6117c6408cbfc4fdbd15c9d31fabec4f4189ef1658"},
|
||||||
{"name":"google-cloud-storage_transfer-v1","version":"0.8.0","platform":"ruby","checksum":"9dbef80275db556e046bb24139ca6559affe641d1e38b2537b8caaf2f8896176"},
|
{"name":"google-cloud-storage_transfer-v1","version":"0.8.0","platform":"ruby","checksum":"9dbef80275db556e046bb24139ca6559affe641d1e38b2537b8caaf2f8896176"},
|
||||||
|
{"name":"google-logging-utils","version":"0.1.0","platform":"ruby","checksum":"70950b1e49314273cf2e167adb47b62af7917a4691b580da7e9be67b9205fcd5"},
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"aarch64-linux","checksum":"5869d1a31f39ee3361e85f3ef3db0512c19f0e0c75cd69d7303c177e17590044"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"aarch64-linux","checksum":"5869d1a31f39ee3361e85f3ef3db0512c19f0e0c75cd69d7303c177e17590044"},
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"arm64-darwin","checksum":"e294affc4fb25c8bc7edd264f0ba490d42dce3afff08db1e08fb7bb44cc57488"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"arm64-darwin","checksum":"e294affc4fb25c8bc7edd264f0ba490d42dce3afff08db1e08fb7bb44cc57488"},
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"java","checksum":"1b8dd949116795653347f95d7975ce2897de2adf721647c10bf54d30ab87fd1e"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"java","checksum":"1b8dd949116795653347f95d7975ce2897de2adf721647c10bf54d30ab87fd1e"},
|
||||||
|
@ -282,7 +283,7 @@
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"x86_64-linux","checksum":"07783d910635e2a60eaf929fe3e45ea4d657d023be3816bda99a21c14f7be959"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"x86_64-linux","checksum":"07783d910635e2a60eaf929fe3e45ea4d657d023be3816bda99a21c14f7be959"},
|
||||||
{"name":"googleapis-common-protos","version":"1.4.0","platform":"ruby","checksum":"da2380fb5ab1563580816c74e8d684ac17512c3654c829a3ee84f6d6139de382"},
|
{"name":"googleapis-common-protos","version":"1.4.0","platform":"ruby","checksum":"da2380fb5ab1563580816c74e8d684ac17512c3654c829a3ee84f6d6139de382"},
|
||||||
{"name":"googleapis-common-protos-types","version":"1.20.0","platform":"ruby","checksum":"5e374b06bcfc7e13556e7c0d87b99f1fa3d42de6396a1de3d8fc13aefb4dd07f"},
|
{"name":"googleapis-common-protos-types","version":"1.20.0","platform":"ruby","checksum":"5e374b06bcfc7e13556e7c0d87b99f1fa3d42de6396a1de3d8fc13aefb4dd07f"},
|
||||||
{"name":"googleauth","version":"1.8.1","platform":"ruby","checksum":"814adadaaa1221dce72a67131e3ecbd6d23491a161ec84fb15fd353b87d8c9e7"},
|
{"name":"googleauth","version":"1.14.0","platform":"ruby","checksum":"62e7de11791890c3d3dc70582dfd9ab5516530e4e4f56d96451fd62c76475149"},
|
||||||
{"name":"gpgme","version":"2.0.24","platform":"ruby","checksum":"53eccd7042abb4fd5c78f30bc9ed075b1325e6450eab207f2f6a1e7e28ae3b64"},
|
{"name":"gpgme","version":"2.0.24","platform":"ruby","checksum":"53eccd7042abb4fd5c78f30bc9ed075b1325e6450eab207f2f6a1e7e28ae3b64"},
|
||||||
{"name":"grape","version":"2.0.0","platform":"ruby","checksum":"3aeff94c17e84ccead4ff98833df691e7da0c108878cc128ca31f80c1047494a"},
|
{"name":"grape","version":"2.0.0","platform":"ruby","checksum":"3aeff94c17e84ccead4ff98833df691e7da0c108878cc128ca31f80c1047494a"},
|
||||||
{"name":"grape-entity","version":"1.0.1","platform":"ruby","checksum":"e00f9e94e407aff77aa2945d741f544d07e48501927942988799913151d02634"},
|
{"name":"grape-entity","version":"1.0.1","platform":"ruby","checksum":"e00f9e94e407aff77aa2945d741f544d07e48501927942988799913151d02634"},
|
||||||
|
@ -697,7 +698,7 @@
|
||||||
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
|
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
|
||||||
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
|
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
|
||||||
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
|
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
|
||||||
{"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"},
|
{"name":"signet","version":"0.19.0","platform":"ruby","checksum":"537f3939f57f141f691e6069a97ec40f34fadafc4c7e5ba94edb06cf4350dd31"},
|
||||||
{"name":"simple_po_parser","version":"1.1.6","platform":"ruby","checksum":"122687d44d3de516a0e69e2f383a4180f5015e8c5ed5a7f2258f2b376f64cbf3"},
|
{"name":"simple_po_parser","version":"1.1.6","platform":"ruby","checksum":"122687d44d3de516a0e69e2f383a4180f5015e8c5ed5a7f2258f2b376f64cbf3"},
|
||||||
{"name":"simplecov","version":"0.22.0","platform":"ruby","checksum":"fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5"},
|
{"name":"simplecov","version":"0.22.0","platform":"ruby","checksum":"fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5"},
|
||||||
{"name":"simplecov-cobertura","version":"2.1.0","platform":"ruby","checksum":"2c6532e34df2e38a379d72cef9a05c3b16c64ce90566beebc6887801c4ad3f02"},
|
{"name":"simplecov-cobertura","version":"2.1.0","platform":"ruby","checksum":"2c6532e34df2e38a379d72cef9a05c3b16c64ce90566beebc6887801c4ad3f02"},
|
||||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -53,7 +53,7 @@ PATH
|
||||||
faraday (~> 2)
|
faraday (~> 2)
|
||||||
google-cloud-storage_transfer (~> 1.2.0)
|
google-cloud-storage_transfer (~> 1.2.0)
|
||||||
google-protobuf (~> 3.25, >= 3.25.3)
|
google-protobuf (~> 3.25, >= 3.25.3)
|
||||||
googleauth (~> 1.8.1)
|
googleauth (~> 1.14)
|
||||||
grpc (= 1.63.0)
|
grpc (= 1.63.0)
|
||||||
json (~> 2.7)
|
json (~> 2.7)
|
||||||
jwt (~> 2.5)
|
jwt (~> 2.5)
|
||||||
|
@ -885,7 +885,7 @@ GEM
|
||||||
google-cloud-core (1.7.0)
|
google-cloud-core (1.7.0)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (2.1.1)
|
google-cloud-env (2.2.1)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.3.0)
|
google-cloud-errors (1.3.0)
|
||||||
google-cloud-location (0.6.0)
|
google-cloud-location (0.6.0)
|
||||||
|
@ -905,6 +905,7 @@ GEM
|
||||||
google-cloud-storage_transfer-v1 (0.8.0)
|
google-cloud-storage_transfer-v1 (0.8.0)
|
||||||
gapic-common (>= 0.20.0, < 2.a)
|
gapic-common (>= 0.20.0, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-logging-utils (0.1.0)
|
||||||
google-protobuf (3.25.8)
|
google-protobuf (3.25.8)
|
||||||
googleapis-common-protos (1.4.0)
|
googleapis-common-protos (1.4.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
|
@ -912,8 +913,10 @@ GEM
|
||||||
grpc (~> 1.27)
|
grpc (~> 1.27)
|
||||||
googleapis-common-protos-types (1.20.0)
|
googleapis-common-protos-types (1.20.0)
|
||||||
google-protobuf (>= 3.18, < 5.a)
|
google-protobuf (>= 3.18, < 5.a)
|
||||||
googleauth (1.8.1)
|
googleauth (1.14.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.2)
|
||||||
|
google-logging-utils (~> 0.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
|
@ -1793,7 +1796,7 @@ GEM
|
||||||
globalid (>= 1.0.1)
|
globalid (>= 1.0.1)
|
||||||
sidekiq (>= 6)
|
sidekiq (>= 6)
|
||||||
sigdump (0.2.5)
|
sigdump (0.2.5)
|
||||||
signet (0.18.0)
|
signet (0.19.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
|
@ -2193,7 +2196,7 @@ DEPENDENCIES
|
||||||
google-cloud-compute-v1 (~> 2.6.0)
|
google-cloud-compute-v1 (~> 2.6.0)
|
||||||
google-cloud-storage (~> 1.45.0)
|
google-cloud-storage (~> 1.45.0)
|
||||||
google-protobuf (~> 3.25, >= 3.25.3)
|
google-protobuf (~> 3.25, >= 3.25.3)
|
||||||
googleauth (~> 1.8.1)
|
googleauth (~> 1.14)
|
||||||
gpgme (~> 2.0.24)
|
gpgme (~> 2.0.24)
|
||||||
grape (~> 2.0.0)
|
grape (~> 2.0.0)
|
||||||
grape-entity (~> 1.0.1)
|
grape-entity (~> 1.0.1)
|
||||||
|
|
|
@ -264,12 +264,13 @@
|
||||||
{"name":"google-cloud-common","version":"1.1.0","platform":"ruby","checksum":"738db08fd144b4fe37b4578ffd63308b64a86fd59f6979d240048f917a6fb5fb"},
|
{"name":"google-cloud-common","version":"1.1.0","platform":"ruby","checksum":"738db08fd144b4fe37b4578ffd63308b64a86fd59f6979d240048f917a6fb5fb"},
|
||||||
{"name":"google-cloud-compute-v1","version":"2.6.0","platform":"ruby","checksum":"b96059b33ffc2f25644d20161a0c1aa1331197073c2e44786b18f8b670f1141e"},
|
{"name":"google-cloud-compute-v1","version":"2.6.0","platform":"ruby","checksum":"b96059b33ffc2f25644d20161a0c1aa1331197073c2e44786b18f8b670f1141e"},
|
||||||
{"name":"google-cloud-core","version":"1.7.0","platform":"ruby","checksum":"748028a48530ea5bce159722eb7a02cd0562f1c52f0569e9ed69da3cba6b4f35"},
|
{"name":"google-cloud-core","version":"1.7.0","platform":"ruby","checksum":"748028a48530ea5bce159722eb7a02cd0562f1c52f0569e9ed69da3cba6b4f35"},
|
||||||
{"name":"google-cloud-env","version":"2.1.1","platform":"ruby","checksum":"cf4bb8c7d517ee1ea692baedf06e0b56ce68007549d8d5a66481aa9f97f46999"},
|
{"name":"google-cloud-env","version":"2.2.1","platform":"ruby","checksum":"3c6062aee0b5c863b83f3ce125ea7831507aadf1af7c0d384b74a116c4f649cf"},
|
||||||
{"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"},
|
{"name":"google-cloud-errors","version":"1.3.0","platform":"ruby","checksum":"450b681e24c089a20721a01acc4408bb4a7b0df28c175aaab488da917480d64b"},
|
||||||
{"name":"google-cloud-location","version":"0.6.0","platform":"ruby","checksum":"386c99ca156e5cac413731c055d7d9c55629860129ad7658a2bf39ea5004d2d0"},
|
{"name":"google-cloud-location","version":"0.6.0","platform":"ruby","checksum":"386c99ca156e5cac413731c055d7d9c55629860129ad7658a2bf39ea5004d2d0"},
|
||||||
{"name":"google-cloud-storage","version":"1.45.0","platform":"ruby","checksum":"f280abda4e608f9e91433f9dd907be4a45cdbf251ffeb275d713548e515c6300"},
|
{"name":"google-cloud-storage","version":"1.45.0","platform":"ruby","checksum":"f280abda4e608f9e91433f9dd907be4a45cdbf251ffeb275d713548e515c6300"},
|
||||||
{"name":"google-cloud-storage_transfer","version":"1.2.0","platform":"ruby","checksum":"132901f50889e02a0d378e6117c6408cbfc4fdbd15c9d31fabec4f4189ef1658"},
|
{"name":"google-cloud-storage_transfer","version":"1.2.0","platform":"ruby","checksum":"132901f50889e02a0d378e6117c6408cbfc4fdbd15c9d31fabec4f4189ef1658"},
|
||||||
{"name":"google-cloud-storage_transfer-v1","version":"0.8.0","platform":"ruby","checksum":"9dbef80275db556e046bb24139ca6559affe641d1e38b2537b8caaf2f8896176"},
|
{"name":"google-cloud-storage_transfer-v1","version":"0.8.0","platform":"ruby","checksum":"9dbef80275db556e046bb24139ca6559affe641d1e38b2537b8caaf2f8896176"},
|
||||||
|
{"name":"google-logging-utils","version":"0.1.0","platform":"ruby","checksum":"70950b1e49314273cf2e167adb47b62af7917a4691b580da7e9be67b9205fcd5"},
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"aarch64-linux","checksum":"5869d1a31f39ee3361e85f3ef3db0512c19f0e0c75cd69d7303c177e17590044"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"aarch64-linux","checksum":"5869d1a31f39ee3361e85f3ef3db0512c19f0e0c75cd69d7303c177e17590044"},
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"arm64-darwin","checksum":"e294affc4fb25c8bc7edd264f0ba490d42dce3afff08db1e08fb7bb44cc57488"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"arm64-darwin","checksum":"e294affc4fb25c8bc7edd264f0ba490d42dce3afff08db1e08fb7bb44cc57488"},
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"java","checksum":"1b8dd949116795653347f95d7975ce2897de2adf721647c10bf54d30ab87fd1e"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"java","checksum":"1b8dd949116795653347f95d7975ce2897de2adf721647c10bf54d30ab87fd1e"},
|
||||||
|
@ -282,7 +283,7 @@
|
||||||
{"name":"google-protobuf","version":"3.25.8","platform":"x86_64-linux","checksum":"07783d910635e2a60eaf929fe3e45ea4d657d023be3816bda99a21c14f7be959"},
|
{"name":"google-protobuf","version":"3.25.8","platform":"x86_64-linux","checksum":"07783d910635e2a60eaf929fe3e45ea4d657d023be3816bda99a21c14f7be959"},
|
||||||
{"name":"googleapis-common-protos","version":"1.4.0","platform":"ruby","checksum":"da2380fb5ab1563580816c74e8d684ac17512c3654c829a3ee84f6d6139de382"},
|
{"name":"googleapis-common-protos","version":"1.4.0","platform":"ruby","checksum":"da2380fb5ab1563580816c74e8d684ac17512c3654c829a3ee84f6d6139de382"},
|
||||||
{"name":"googleapis-common-protos-types","version":"1.20.0","platform":"ruby","checksum":"5e374b06bcfc7e13556e7c0d87b99f1fa3d42de6396a1de3d8fc13aefb4dd07f"},
|
{"name":"googleapis-common-protos-types","version":"1.20.0","platform":"ruby","checksum":"5e374b06bcfc7e13556e7c0d87b99f1fa3d42de6396a1de3d8fc13aefb4dd07f"},
|
||||||
{"name":"googleauth","version":"1.8.1","platform":"ruby","checksum":"814adadaaa1221dce72a67131e3ecbd6d23491a161ec84fb15fd353b87d8c9e7"},
|
{"name":"googleauth","version":"1.14.0","platform":"ruby","checksum":"62e7de11791890c3d3dc70582dfd9ab5516530e4e4f56d96451fd62c76475149"},
|
||||||
{"name":"gpgme","version":"2.0.24","platform":"ruby","checksum":"53eccd7042abb4fd5c78f30bc9ed075b1325e6450eab207f2f6a1e7e28ae3b64"},
|
{"name":"gpgme","version":"2.0.24","platform":"ruby","checksum":"53eccd7042abb4fd5c78f30bc9ed075b1325e6450eab207f2f6a1e7e28ae3b64"},
|
||||||
{"name":"grape","version":"2.0.0","platform":"ruby","checksum":"3aeff94c17e84ccead4ff98833df691e7da0c108878cc128ca31f80c1047494a"},
|
{"name":"grape","version":"2.0.0","platform":"ruby","checksum":"3aeff94c17e84ccead4ff98833df691e7da0c108878cc128ca31f80c1047494a"},
|
||||||
{"name":"grape-entity","version":"1.0.1","platform":"ruby","checksum":"e00f9e94e407aff77aa2945d741f544d07e48501927942988799913151d02634"},
|
{"name":"grape-entity","version":"1.0.1","platform":"ruby","checksum":"e00f9e94e407aff77aa2945d741f544d07e48501927942988799913151d02634"},
|
||||||
|
@ -697,7 +698,7 @@
|
||||||
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
|
{"name":"shoulda-matchers","version":"6.4.0","platform":"ruby","checksum":"9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0"},
|
||||||
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
|
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
|
||||||
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
|
{"name":"sigdump","version":"0.2.5","platform":"ruby","checksum":"bb706c1cce70458b285d2c3a57121e801ccb79f68be7f7377692eb40b5437242"},
|
||||||
{"name":"signet","version":"0.18.0","platform":"ruby","checksum":"66cda8c2edc2dde25090b792e7e6fc9598c3c2bdd64ffacd89f1ffe3cb9cea3b"},
|
{"name":"signet","version":"0.19.0","platform":"ruby","checksum":"537f3939f57f141f691e6069a97ec40f34fadafc4c7e5ba94edb06cf4350dd31"},
|
||||||
{"name":"simple_po_parser","version":"1.1.6","platform":"ruby","checksum":"122687d44d3de516a0e69e2f383a4180f5015e8c5ed5a7f2258f2b376f64cbf3"},
|
{"name":"simple_po_parser","version":"1.1.6","platform":"ruby","checksum":"122687d44d3de516a0e69e2f383a4180f5015e8c5ed5a7f2258f2b376f64cbf3"},
|
||||||
{"name":"simplecov","version":"0.22.0","platform":"ruby","checksum":"fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5"},
|
{"name":"simplecov","version":"0.22.0","platform":"ruby","checksum":"fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5"},
|
||||||
{"name":"simplecov-cobertura","version":"2.1.0","platform":"ruby","checksum":"2c6532e34df2e38a379d72cef9a05c3b16c64ce90566beebc6887801c4ad3f02"},
|
{"name":"simplecov-cobertura","version":"2.1.0","platform":"ruby","checksum":"2c6532e34df2e38a379d72cef9a05c3b16c64ce90566beebc6887801c4ad3f02"},
|
||||||
|
|
|
@ -53,7 +53,7 @@ PATH
|
||||||
faraday (~> 2)
|
faraday (~> 2)
|
||||||
google-cloud-storage_transfer (~> 1.2.0)
|
google-cloud-storage_transfer (~> 1.2.0)
|
||||||
google-protobuf (~> 3.25, >= 3.25.3)
|
google-protobuf (~> 3.25, >= 3.25.3)
|
||||||
googleauth (~> 1.8.1)
|
googleauth (~> 1.14)
|
||||||
grpc (= 1.63.0)
|
grpc (= 1.63.0)
|
||||||
json (~> 2.7)
|
json (~> 2.7)
|
||||||
jwt (~> 2.5)
|
jwt (~> 2.5)
|
||||||
|
@ -879,7 +879,7 @@ GEM
|
||||||
google-cloud-core (1.7.0)
|
google-cloud-core (1.7.0)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (2.1.1)
|
google-cloud-env (2.2.1)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.3.0)
|
google-cloud-errors (1.3.0)
|
||||||
google-cloud-location (0.6.0)
|
google-cloud-location (0.6.0)
|
||||||
|
@ -899,6 +899,7 @@ GEM
|
||||||
google-cloud-storage_transfer-v1 (0.8.0)
|
google-cloud-storage_transfer-v1 (0.8.0)
|
||||||
gapic-common (>= 0.20.0, < 2.a)
|
gapic-common (>= 0.20.0, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-logging-utils (0.1.0)
|
||||||
google-protobuf (3.25.8)
|
google-protobuf (3.25.8)
|
||||||
googleapis-common-protos (1.4.0)
|
googleapis-common-protos (1.4.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
|
@ -906,8 +907,10 @@ GEM
|
||||||
grpc (~> 1.27)
|
grpc (~> 1.27)
|
||||||
googleapis-common-protos-types (1.20.0)
|
googleapis-common-protos-types (1.20.0)
|
||||||
google-protobuf (>= 3.18, < 5.a)
|
google-protobuf (>= 3.18, < 5.a)
|
||||||
googleauth (1.8.1)
|
googleauth (1.14.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.2)
|
||||||
|
google-logging-utils (~> 0.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
|
@ -1787,7 +1790,7 @@ GEM
|
||||||
globalid (>= 1.0.1)
|
globalid (>= 1.0.1)
|
||||||
sidekiq (>= 6)
|
sidekiq (>= 6)
|
||||||
sigdump (0.2.5)
|
sigdump (0.2.5)
|
||||||
signet (0.18.0)
|
signet (0.19.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
|
@ -2188,7 +2191,7 @@ DEPENDENCIES
|
||||||
google-cloud-compute-v1 (~> 2.6.0)
|
google-cloud-compute-v1 (~> 2.6.0)
|
||||||
google-cloud-storage (~> 1.45.0)
|
google-cloud-storage (~> 1.45.0)
|
||||||
google-protobuf (~> 3.25, >= 3.25.3)
|
google-protobuf (~> 3.25, >= 3.25.3)
|
||||||
googleauth (~> 1.8.1)
|
googleauth (~> 1.14)
|
||||||
gpgme (~> 2.0.24)
|
gpgme (~> 2.0.24)
|
||||||
grape (~> 2.0.0)
|
grape (~> 2.0.0)
|
||||||
grape-entity (~> 1.0.1)
|
grape-entity (~> 1.0.1)
|
||||||
|
|
|
@ -323,11 +323,12 @@ export default {
|
||||||
<!--Timezone-->
|
<!--Timezone-->
|
||||||
<gl-form-group
|
<gl-form-group
|
||||||
:label="$options.i18n.cronTimezoneText"
|
:label="$options.i18n.cronTimezoneText"
|
||||||
label-for="schedule-timezone"
|
label-for="user_timezone"
|
||||||
class="lg:gl-w-2/3"
|
class="lg:gl-w-2/3"
|
||||||
>
|
>
|
||||||
<timezone-dropdown
|
<timezone-dropdown
|
||||||
id="schedule-timezone"
|
id="schedule-timezone"
|
||||||
|
input-id="user_timezone"
|
||||||
:value="cronTimezone"
|
:value="cronTimezone"
|
||||||
:timezone-data="timezoneData"
|
:timezone-data="timezoneData"
|
||||||
name="schedule-timezone"
|
name="schedule-timezone"
|
||||||
|
|
|
@ -136,7 +136,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useNotes, [
|
...mapState(useNotes, [
|
||||||
'getDiscussionLastNote',
|
'getDiscussionCurrentUserLastNote',
|
||||||
'getNoteableData',
|
'getNoteableData',
|
||||||
'getNotesDataByProp',
|
'getNotesDataByProp',
|
||||||
'getUserDataByProp',
|
'getUserDataByProp',
|
||||||
|
@ -215,13 +215,7 @@ export default {
|
||||||
return !this.updatedNoteBody.length || this.isSubmitting;
|
return !this.updatedNoteBody.length || this.isSubmitting;
|
||||||
},
|
},
|
||||||
isInternalNote() {
|
isInternalNote() {
|
||||||
return this.discussionNote.internal || this.discussion.confidential;
|
return this.discussion.confidential;
|
||||||
},
|
|
||||||
discussionNote() {
|
|
||||||
const discussionNote = this.discussion.id
|
|
||||||
? this.getDiscussionLastNote(this.discussion)
|
|
||||||
: this.note;
|
|
||||||
return discussionNote || {};
|
|
||||||
},
|
},
|
||||||
canSuggest() {
|
canSuggest() {
|
||||||
return (
|
return (
|
||||||
|
@ -279,7 +273,7 @@ export default {
|
||||||
},
|
},
|
||||||
editMyLastNote() {
|
editMyLastNote() {
|
||||||
if (this.updatedNoteBody === '') {
|
if (this.updatedNoteBody === '') {
|
||||||
const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
|
const lastNoteInDiscussion = this.getDiscussionCurrentUserLastNote(this.discussion);
|
||||||
|
|
||||||
if (lastNoteInDiscussion) {
|
if (lastNoteInDiscussion) {
|
||||||
eventHub.$emit('enterEditMode', {
|
eventHub.$emit('enterEditMode', {
|
||||||
|
@ -299,7 +293,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePlaceholder() {
|
updatePlaceholder() {
|
||||||
this.formFieldProps.placeholder = this.discussionNote?.internal
|
this.formFieldProps.placeholder = this.isInternalNote
|
||||||
? this.$options.i18n.bodyPlaceholderInternal
|
? this.$options.i18n.bodyPlaceholderInternal
|
||||||
: this.$options.i18n.bodyPlaceholder;
|
: this.$options.i18n.bodyPlaceholder;
|
||||||
},
|
},
|
||||||
|
@ -398,7 +392,7 @@ export default {
|
||||||
<div class="flash-container"></div>
|
<div class="flash-container"></div>
|
||||||
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
|
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
|
||||||
<comment-field-layout
|
<comment-field-layout
|
||||||
:is-internal-note="discussionNote.internal"
|
:is-internal-note="isInternalNote"
|
||||||
:note="updatedNoteBody"
|
:note="updatedNoteBody"
|
||||||
:noteable-data="getNoteableData"
|
:noteable-data="getNoteableData"
|
||||||
>
|
>
|
||||||
|
@ -409,7 +403,6 @@ export default {
|
||||||
:markdown-docs-path="markdownDocsPath"
|
:markdown-docs-path="markdownDocsPath"
|
||||||
:code-suggestions-config="codeSuggestionsConfig"
|
:code-suggestions-config="codeSuggestionsConfig"
|
||||||
:help-page-path="helpPagePath"
|
:help-page-path="helpPagePath"
|
||||||
:note="discussionNote"
|
|
||||||
:noteable-type="noteableType"
|
:noteable-type="noteableType"
|
||||||
:form-field-props="formFieldProps"
|
:form-field-props="formFieldProps"
|
||||||
:autosave-key="autosaveKey"
|
:autosave-key="autosaveKey"
|
||||||
|
|
|
@ -166,17 +166,18 @@ export function noteableType() {
|
||||||
|
|
||||||
const reverseNotes = (array) => array.slice(0).reverse();
|
const reverseNotes = (array) => array.slice(0).reverse();
|
||||||
|
|
||||||
const isLastNote = (note, state) =>
|
const isCurrentUserLastNote = (note, state) =>
|
||||||
!note.system && state.userData && note.author && note.author.id === state.userData.id;
|
!note.system && state.userData && note.author && note.author.id === state.userData.id;
|
||||||
|
|
||||||
export function getCurrentUserLastNote() {
|
export function getCurrentUserLastNote() {
|
||||||
return flattenDeep(reverseNotes(this.discussions).map((note) => reverseNotes(note.notes))).find(
|
return flattenDeep(reverseNotes(this.discussions).map((note) => reverseNotes(note.notes))).find(
|
||||||
(el) => isLastNote(el, this),
|
(el) => isCurrentUserLastNote(el, this),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDiscussionLastNote() {
|
export function getDiscussionCurrentUserLastNote() {
|
||||||
return (discussion) => reverseNotes(discussion.notes).find((el) => isLastNote(el, this));
|
return (discussion) =>
|
||||||
|
reverseNotes(discussion.notes).find((el) => isCurrentUserLastNote(el, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showJumpToNextDiscussion() {
|
export function showJumpToNextDiscussion() {
|
||||||
|
|
|
@ -165,17 +165,14 @@ export const noteableType = (state) => {
|
||||||
|
|
||||||
const reverseNotes = (array) => array.slice(0).reverse();
|
const reverseNotes = (array) => array.slice(0).reverse();
|
||||||
|
|
||||||
const isLastNote = (note, state) =>
|
const isCurrentUserLastNote = (note, state) =>
|
||||||
!note.system && state.userData && note.author && note.author.id === state.userData.id;
|
!note.system && state.userData && note.author && note.author.id === state.userData.id;
|
||||||
|
|
||||||
export const getCurrentUserLastNote = (state) =>
|
export const getCurrentUserLastNote = (state) =>
|
||||||
flattenDeep(reverseNotes(state.discussions).map((note) => reverseNotes(note.notes))).find((el) =>
|
flattenDeep(reverseNotes(state.discussions).map((note) => reverseNotes(note.notes))).find((el) =>
|
||||||
isLastNote(el, state),
|
isCurrentUserLastNote(el, state),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getDiscussionLastNote = (state) => (discussion) =>
|
|
||||||
reverseNotes(discussion.notes).find((el) => isLastNote(el, state));
|
|
||||||
|
|
||||||
export const unresolvedDiscussionsCount = (state) => state.unresolvedDiscussionsCount;
|
export const unresolvedDiscussionsCount = (state) => state.unresolvedDiscussionsCount;
|
||||||
export const resolvableDiscussionsCount = (state) => state.resolvableDiscussionsCount;
|
export const resolvableDiscussionsCount = (state) => state.resolvableDiscussionsCount;
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,7 @@ export default {
|
||||||
<div>
|
<div>
|
||||||
<p class="gl-mb-3 gl-mt-0 gl-text-subtle" data-testid="worker-cron-expression-hint">
|
<p class="gl-mb-3 gl-mt-0 gl-text-subtle" data-testid="worker-cron-expression-hint">
|
||||||
{{ sprintf($options.i18n.pipelineScheduleWorkerExplanation, { workerCronExpression }) }}
|
{{ sprintf($options.i18n.pipelineScheduleWorkerExplanation, { workerCronExpression }) }}
|
||||||
<gl-link :href="pipelineScheduleWorkerUrl" target="_blank">
|
<gl-link :href="pipelineScheduleWorkerUrl" target="_blank" variant="inline">
|
||||||
{{ $options.i18n.pipelineScheduleWorkerLink }}
|
{{ $options.i18n.pipelineScheduleWorkerLink }}
|
||||||
</gl-link>
|
</gl-link>
|
||||||
</p>
|
</p>
|
||||||
|
@ -197,7 +197,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
<p class="gl-mb-0 gl-mt-1 gl-text-subtle">
|
<p class="gl-mb-0 gl-mt-1 gl-text-subtle">
|
||||||
{{ $options.i18n.learnCronSyntax }}
|
{{ $options.i18n.learnCronSyntax }}
|
||||||
<gl-link :href="cronSyntaxUrl" target="_blank">
|
<gl-link :href="cronSyntaxUrl" target="_blank" variant="inline">
|
||||||
{{ $options.i18n.cronSyntaxLink }}
|
{{ $options.i18n.cronSyntaxLink }}
|
||||||
</gl-link>
|
</gl-link>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -32,12 +32,7 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<gl-link
|
<gl-link v-gl-tooltip :href="authorUrl" :title="showAuthorName ? null : author.name">
|
||||||
v-gl-tooltip
|
|
||||||
:href="authorUrl"
|
|
||||||
:title="showAuthorName ? null : author.name"
|
|
||||||
class="mr-widget-author"
|
|
||||||
>
|
|
||||||
<gl-avatar :src="avatarUrl" :size="16" :alt="author.name" /><span
|
<gl-avatar :src="avatarUrl" :size="16" :alt="author.name" /><span
|
||||||
v-if="showAuthorName"
|
v-if="showAuthorName"
|
||||||
class="author gl-ml-2"
|
class="author gl-ml-2"
|
||||||
|
|
|
@ -79,7 +79,7 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<state-container status="closed" :actions="actions" is-collapsible>
|
<state-container status="closed" :actions="actions">
|
||||||
<mr-widget-author-time
|
<mr-widget-author-time
|
||||||
:action-text="s__('mrWidget|Closed by')"
|
:action-text="s__('mrWidget|Closed by')"
|
||||||
:author="mr.metrics.closedBy"
|
:author="mr.metrics.closedBy"
|
||||||
|
|
|
@ -79,7 +79,8 @@ export default {
|
||||||
return label.name || label.title;
|
return label.name || label.title;
|
||||||
},
|
},
|
||||||
updateListOfAllLabels() {
|
updateListOfAllLabels() {
|
||||||
this.labels.forEach((label) => {
|
const dedupedLabels = this.dedupeLabels(this.labels);
|
||||||
|
dedupedLabels.forEach((label) => {
|
||||||
if (!this.findLabelById(label.id)) {
|
if (!this.findLabelById(label.id)) {
|
||||||
this.allLabels.push(label);
|
this.allLabels.push(label);
|
||||||
}
|
}
|
||||||
|
@ -93,7 +94,8 @@ export default {
|
||||||
// We'd want to avoid doing this check but
|
// We'd want to avoid doing this check but
|
||||||
// labels.json and /groups/:id/labels & /projects/:id/labels
|
// labels.json and /groups/:id/labels & /projects/:id/labels
|
||||||
// return response differently.
|
// return response differently.
|
||||||
this.labels = Array.isArray(res) ? res : res.data;
|
const rawLabels = Array.isArray(res) ? res : res.data;
|
||||||
|
this.labels = this.dedupeLabels(rawLabels);
|
||||||
this.updateListOfAllLabels();
|
this.updateListOfAllLabels();
|
||||||
|
|
||||||
if (this.config.fetchLatestLabels) {
|
if (this.config.fetchLatestLabels) {
|
||||||
|
@ -116,7 +118,8 @@ export default {
|
||||||
// We'd want to avoid doing this check but
|
// We'd want to avoid doing this check but
|
||||||
// labels.json and /groups/:id/labels & /projects/:id/labels
|
// labels.json and /groups/:id/labels & /projects/:id/labels
|
||||||
// return response differently.
|
// return response differently.
|
||||||
this.labels = Array.isArray(res) ? res : res.data;
|
const rawLabels = Array.isArray(res) ? res : res.data;
|
||||||
|
this.labels = this.dedupeLabels(rawLabels);
|
||||||
this.updateListOfAllLabels();
|
this.updateListOfAllLabels();
|
||||||
})
|
})
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
|
@ -125,6 +128,17 @@ export default {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
dedupeLabels(labels) {
|
||||||
|
const seen = new Set();
|
||||||
|
return labels.filter((label) => {
|
||||||
|
const labelName = this.getLabelName(label);
|
||||||
|
if (seen.has(labelName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.add(labelName);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,6 +9,11 @@ export default {
|
||||||
GlCollapsibleListbox,
|
GlCollapsibleListbox,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
inputId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'user_timezone',
|
||||||
|
},
|
||||||
headerText: {
|
headerText: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -108,7 +113,7 @@ export default {
|
||||||
<div class="gl-relative">
|
<div class="gl-relative">
|
||||||
<input
|
<input
|
||||||
v-if="name"
|
v-if="name"
|
||||||
id="user_timezone"
|
:id="inputId"
|
||||||
:name="name"
|
:name="name"
|
||||||
:value="timezoneIdentifier || value"
|
:value="timezoneIdentifier || value"
|
||||||
:required="required"
|
:required="required"
|
||||||
|
|
|
@ -32,6 +32,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -184,6 +189,7 @@ export default {
|
||||||
:searching="isLoading"
|
:searching="isLoading"
|
||||||
:selected="selectedId"
|
:selected="selectedId"
|
||||||
:toggle-text="toggleText"
|
:toggle-text="toggleText"
|
||||||
|
:disabled="disabled"
|
||||||
@reset="reset"
|
@reset="reset"
|
||||||
@search="setSearchTermDebounced"
|
@search="setSearchTermDebounced"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
|
|
|
@ -24,6 +24,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toggleText() {
|
toggleText() {
|
||||||
|
@ -51,6 +56,7 @@ export default {
|
||||||
:reset-button-label="__('Reset')"
|
:reset-button-label="__('Reset')"
|
||||||
:selected="value"
|
:selected="value"
|
||||||
:toggle-text="toggleText"
|
:toggle-text="toggleText"
|
||||||
|
:disabled="disabled"
|
||||||
@reset="reset"
|
@reset="reset"
|
||||||
@select="$emit('input', $event)"
|
@select="$emit('input', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -39,6 +39,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -177,6 +182,7 @@ export default {
|
||||||
:searching="isLoading"
|
:searching="isLoading"
|
||||||
:selected="selectedIds"
|
:selected="selectedIds"
|
||||||
:toggle-text="toggleText"
|
:toggle-text="toggleText"
|
||||||
|
:disabled="disabled"
|
||||||
@reset="handleSelect([])"
|
@reset="handleSelect([])"
|
||||||
@search="setSearchTermDebounced"
|
@search="setSearchTermDebounced"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
|
|
|
@ -28,6 +28,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -142,6 +147,7 @@ export default {
|
||||||
:searching="isLoading"
|
:searching="isLoading"
|
||||||
:selected="selectedId"
|
:selected="selectedId"
|
||||||
:toggle-text="toggleText"
|
:toggle-text="toggleText"
|
||||||
|
:disabled="disabled"
|
||||||
@reset="reset"
|
@reset="reset"
|
||||||
@search="setSearchTermDebounced"
|
@search="setSearchTermDebounced"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
|
|
|
@ -30,6 +30,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -172,6 +177,7 @@ export default {
|
||||||
:searching="isLoading"
|
:searching="isLoading"
|
||||||
:selected="selectedId"
|
:selected="selectedId"
|
||||||
:toggle-text="toggleText"
|
:toggle-text="toggleText"
|
||||||
|
:disabled="disabled"
|
||||||
@reset="reset"
|
@reset="reset"
|
||||||
@search="setSearchTermDebounced"
|
@search="setSearchTermDebounced"
|
||||||
@select="handleSelect"
|
@select="handleSelect"
|
||||||
|
|
|
@ -6,8 +6,17 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { BULK_UPDATE_UNASSIGNED } from '../../constants';
|
import {
|
||||||
|
BULK_UPDATE_UNASSIGNED,
|
||||||
|
WIDGET_TYPE_ASSIGNEES,
|
||||||
|
WIDGET_TYPE_HEALTH_STATUS,
|
||||||
|
WIDGET_TYPE_HIERARCHY,
|
||||||
|
WIDGET_TYPE_ITERATION,
|
||||||
|
WIDGET_TYPE_LABELS,
|
||||||
|
WIDGET_TYPE_MILESTONE,
|
||||||
|
} from '../../constants';
|
||||||
import workItemBulkUpdateMutation from '../../graphql/list/work_item_bulk_update.mutation.graphql';
|
import workItemBulkUpdateMutation from '../../graphql/list/work_item_bulk_update.mutation.graphql';
|
||||||
|
import getAvailableBulkEditWidgets from '../../graphql/list/get_available_bulk_edit_widgets.query.graphql';
|
||||||
import workItemParent from '../../graphql/list/work_item_parent.query.graphql';
|
import workItemParent from '../../graphql/list/work_item_parent.query.graphql';
|
||||||
import WorkItemBulkEditAssignee from './work_item_bulk_edit_assignee.vue';
|
import WorkItemBulkEditAssignee from './work_item_bulk_edit_assignee.vue';
|
||||||
import WorkItemBulkEditDropdown from './work_item_bulk_edit_dropdown.vue';
|
import WorkItemBulkEditDropdown from './work_item_bulk_edit_dropdown.vue';
|
||||||
|
@ -70,6 +79,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
availableWidgets: [],
|
||||||
addLabelIds: [],
|
addLabelIds: [],
|
||||||
assigneeId: undefined,
|
assigneeId: undefined,
|
||||||
confidentiality: undefined,
|
confidentiality: undefined,
|
||||||
|
@ -100,6 +110,21 @@ export default {
|
||||||
return !this.shouldUseGraphQLBulkEdit;
|
return !this.shouldUseGraphQLBulkEdit;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
availableWidgets: {
|
||||||
|
query: getAvailableBulkEditWidgets,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
fullPath: this.fullPath,
|
||||||
|
ids: this.workItemTypeIds,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update(data) {
|
||||||
|
return data.namespace?.workItemsWidgets || [];
|
||||||
|
},
|
||||||
|
skip() {
|
||||||
|
return this.checkedItems.length === 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
legacyBulkEditEndpoint() {
|
legacyBulkEditEndpoint() {
|
||||||
|
@ -113,6 +138,30 @@ export default {
|
||||||
isEditableUnlessEpicList() {
|
isEditableUnlessEpicList() {
|
||||||
return !this.shouldUseGraphQLBulkEdit || (this.shouldUseGraphQLBulkEdit && !this.isEpicsList);
|
return !this.shouldUseGraphQLBulkEdit || (this.shouldUseGraphQLBulkEdit && !this.isEpicsList);
|
||||||
},
|
},
|
||||||
|
workItemTypeIds() {
|
||||||
|
return [...new Set(this.checkedItems.map((item) => item.workItemType.id))];
|
||||||
|
},
|
||||||
|
hasItemsSelected() {
|
||||||
|
return this.checkedItems.length > 0;
|
||||||
|
},
|
||||||
|
canEditAssignees() {
|
||||||
|
return this.availableWidgets.includes(WIDGET_TYPE_ASSIGNEES);
|
||||||
|
},
|
||||||
|
canEditLabels() {
|
||||||
|
return this.availableWidgets.includes(WIDGET_TYPE_LABELS);
|
||||||
|
},
|
||||||
|
canEditHealthStatus() {
|
||||||
|
return this.availableWidgets.includes(WIDGET_TYPE_HEALTH_STATUS);
|
||||||
|
},
|
||||||
|
canEditIteration() {
|
||||||
|
return this.availableWidgets.includes(WIDGET_TYPE_ITERATION);
|
||||||
|
},
|
||||||
|
canEditMilestone() {
|
||||||
|
return this.availableWidgets.includes(WIDGET_TYPE_MILESTONE);
|
||||||
|
},
|
||||||
|
canEditParent() {
|
||||||
|
return this.availableWidgets.includes(WIDGET_TYPE_HIERARCHY);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async handleFormSubmitted() {
|
async handleFormSubmitted() {
|
||||||
|
@ -208,6 +257,7 @@ export default {
|
||||||
:header-text="__('Select state')"
|
:header-text="__('Select state')"
|
||||||
:items="$options.stateItems"
|
:items="$options.stateItems"
|
||||||
:label="__('State')"
|
:label="__('State')"
|
||||||
|
:disabled="!hasItemsSelected"
|
||||||
data-testid="bulk-edit-state"
|
data-testid="bulk-edit-state"
|
||||||
/>
|
/>
|
||||||
<work-item-bulk-edit-assignee
|
<work-item-bulk-edit-assignee
|
||||||
|
@ -215,12 +265,14 @@ export default {
|
||||||
v-model="assigneeId"
|
v-model="assigneeId"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
:is-group="isGroup"
|
:is-group="isGroup"
|
||||||
|
:disabled="!hasItemsSelected || !canEditAssignees"
|
||||||
/>
|
/>
|
||||||
<work-item-bulk-edit-labels
|
<work-item-bulk-edit-labels
|
||||||
:form-label="__('Add labels')"
|
:form-label="__('Add labels')"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
:is-group="isGroup"
|
:is-group="isGroup"
|
||||||
:selected-labels-ids="addLabelIds"
|
:selected-labels-ids="addLabelIds"
|
||||||
|
:disabled="!hasItemsSelected || !canEditLabels"
|
||||||
data-testid="bulk-edit-add-labels"
|
data-testid="bulk-edit-add-labels"
|
||||||
@select="addLabelIds = $event"
|
@select="addLabelIds = $event"
|
||||||
/>
|
/>
|
||||||
|
@ -230,6 +282,7 @@ export default {
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
:is-group="isGroup"
|
:is-group="isGroup"
|
||||||
:selected-labels-ids="removeLabelIds"
|
:selected-labels-ids="removeLabelIds"
|
||||||
|
:disabled="!hasItemsSelected || !canEditLabels"
|
||||||
data-testid="bulk-edit-remove-labels"
|
data-testid="bulk-edit-remove-labels"
|
||||||
@select="removeLabelIds = $event"
|
@select="removeLabelIds = $event"
|
||||||
/>
|
/>
|
||||||
|
@ -239,6 +292,7 @@ export default {
|
||||||
:header-text="__('Select health status')"
|
:header-text="__('Select health status')"
|
||||||
:items="$options.healthStatusItems"
|
:items="$options.healthStatusItems"
|
||||||
:label="__('Health status')"
|
:label="__('Health status')"
|
||||||
|
:disabled="!hasItemsSelected || !canEditHealthStatus"
|
||||||
data-testid="bulk-edit-health-status"
|
data-testid="bulk-edit-health-status"
|
||||||
/>
|
/>
|
||||||
<work-item-bulk-edit-dropdown
|
<work-item-bulk-edit-dropdown
|
||||||
|
@ -247,6 +301,7 @@ export default {
|
||||||
:header-text="__('Select subscription')"
|
:header-text="__('Select subscription')"
|
||||||
:items="$options.subscriptionItems"
|
:items="$options.subscriptionItems"
|
||||||
:label="__('Subscription')"
|
:label="__('Subscription')"
|
||||||
|
:disabled="!hasItemsSelected"
|
||||||
data-testid="bulk-edit-subscription"
|
data-testid="bulk-edit-subscription"
|
||||||
/>
|
/>
|
||||||
<work-item-bulk-edit-dropdown
|
<work-item-bulk-edit-dropdown
|
||||||
|
@ -255,6 +310,7 @@ export default {
|
||||||
:header-text="__('Select confidentiality')"
|
:header-text="__('Select confidentiality')"
|
||||||
:items="$options.confidentialityItems"
|
:items="$options.confidentialityItems"
|
||||||
:label="__('Confidentiality')"
|
:label="__('Confidentiality')"
|
||||||
|
:disabled="!hasItemsSelected"
|
||||||
data-testid="bulk-edit-confidentiality"
|
data-testid="bulk-edit-confidentiality"
|
||||||
/>
|
/>
|
||||||
<work-item-bulk-edit-iteration
|
<work-item-bulk-edit-iteration
|
||||||
|
@ -262,18 +318,21 @@ export default {
|
||||||
v-model="iterationId"
|
v-model="iterationId"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
:is-group="isGroup"
|
:is-group="isGroup"
|
||||||
|
:disabled="!hasItemsSelected || !canEditIteration"
|
||||||
/>
|
/>
|
||||||
<work-item-bulk-edit-milestone
|
<work-item-bulk-edit-milestone
|
||||||
v-if="shouldUseGraphQLBulkEdit && !isEpicsList"
|
v-if="shouldUseGraphQLBulkEdit && !isEpicsList"
|
||||||
v-model="milestoneId"
|
v-model="milestoneId"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
:is-group="isGroup"
|
:is-group="isGroup"
|
||||||
|
:disabled="!hasItemsSelected || !canEditMilestone"
|
||||||
/>
|
/>
|
||||||
<work-item-bulk-edit-parent
|
<work-item-bulk-edit-parent
|
||||||
v-if="shouldUseGraphQLBulkEdit && !isEpicsList"
|
v-if="shouldUseGraphQLBulkEdit && !isEpicsList"
|
||||||
v-model="parentId"
|
v-model="parentId"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
:is-group="isGroup"
|
:is-group="isGroup"
|
||||||
|
:disabled="!hasItemsSelected || !canEditParent"
|
||||||
/>
|
/>
|
||||||
</gl-form>
|
</gl-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
query getAvailableBulkEditWidgets($fullPath: ID!, $ids: [WorkItemsTypeID!]!) {
|
||||||
|
namespace(fullPath: $fullPath) {
|
||||||
|
id
|
||||||
|
workItemsWidgets(ids: $ids)
|
||||||
|
}
|
||||||
|
}
|
|
@ -854,8 +854,7 @@ $diff-file-header-top: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-ready-merge-related-links a,
|
.mr-ready-merge-related-links a,
|
||||||
.mr-widget-merge-details a,
|
.mr-widget-merge-details a {
|
||||||
.mr-widget-author {
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
|
|
|
@ -3,8 +3,8 @@ name: admin_groups_vue
|
||||||
description: Render Admin area -> Groups with Vue instead of HAML/vanilla JS
|
description: Render Admin area -> Groups with Vue instead of HAML/vanilla JS
|
||||||
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/17783
|
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/17783
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194642
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/194642
|
||||||
rollout_issue_url:
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/553229
|
||||||
milestone: '18.2'
|
milestone: '18.2'
|
||||||
group: group::organizations
|
group: group::organizations
|
||||||
type: wip
|
type: beta
|
||||||
default_enabled: false
|
default_enabled: false
|
|
@ -103,7 +103,10 @@ To add a broadcast message:
|
||||||
group, subgroup, and project pages, but does not display in Git remote responses.
|
group, subgroup, and project pages, but does not display in Git remote responses.
|
||||||
1. If required, select the **Target roles** to show the broadcast message to.
|
1. If required, select the **Target roles** to show the broadcast message to.
|
||||||
1. If required, add a **Target Path** to only show the broadcast message on URLs matching that path.
|
1. If required, add a **Target Path** to only show the broadcast message on URLs matching that path.
|
||||||
Use the wildcard character `*` to match multiple URLs, like `mygroup/myproject*`.
|
Use the wildcard character `*` to match multiple URLs and specify paths, for example:
|
||||||
|
- `*/-/milestones` for any group or project's **Milestones** index page.
|
||||||
|
- `*/-/milestones/*` for individual milestone pages only.
|
||||||
|
- `*/-/milestones*` for both index and individual milestone pages.
|
||||||
1. Select a date and time (UTC) for the message to start and end.
|
1. Select a date and time (UTC) for the message to start and end.
|
||||||
1. Select **Add broadcast message**.
|
1. Select **Add broadcast message**.
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,7 @@ For more information, see [epic 12978](https://gitlab.com/groups/gitlab-org/-/ep
|
||||||
{{< history >}}
|
{{< history >}}
|
||||||
|
|
||||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/513252) in GitLab 18.1 [with a flag](../../administration/feature_flags/_index.md) named `duo_rca_usage_rate`. Disabled by default.
|
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/513252) in GitLab 18.1 [with a flag](../../administration/feature_flags/_index.md) named `duo_rca_usage_rate`. Disabled by default.
|
||||||
|
- [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/543987) in GitLab 18.3.
|
||||||
|
|
||||||
{{< /history >}}
|
{{< /history >}}
|
||||||
|
|
||||||
|
@ -110,7 +111,6 @@ For more information, see [epic 12978](https://gitlab.com/groups/gitlab-org/-/ep
|
||||||
|
|
||||||
The availability of this feature is controlled by a feature flag.
|
The availability of this feature is controlled by a feature flag.
|
||||||
For more information, see the history.
|
For more information, see the history.
|
||||||
This feature is available for testing.
|
|
||||||
|
|
||||||
{{< /alert >}}
|
{{< /alert >}}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
stage: Application Security Testing
|
stage: Security Risk Management
|
||||||
group: Static Analysis
|
group: Security Platform Management
|
||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||||
title: Security configuration
|
title: Security configuration
|
||||||
description: Configuration, testing, compliance, scanning, and enablement.
|
description: Configuration, testing, compliance, scanning, and enablement.
|
||||||
|
|
|
@ -18,7 +18,7 @@ PATH
|
||||||
faraday (~> 2)
|
faraday (~> 2)
|
||||||
google-cloud-storage_transfer (~> 1.2.0)
|
google-cloud-storage_transfer (~> 1.2.0)
|
||||||
google-protobuf (~> 3.25, >= 3.25.3)
|
google-protobuf (~> 3.25, >= 3.25.3)
|
||||||
googleauth (~> 1.8.1)
|
googleauth (~> 1.14)
|
||||||
grpc (= 1.63.0)
|
grpc (= 1.63.0)
|
||||||
json (~> 2.7)
|
json (~> 2.7)
|
||||||
jwt (~> 2.5)
|
jwt (~> 2.5)
|
||||||
|
@ -88,6 +88,7 @@ GEM
|
||||||
google-cloud-storage_transfer-v1 (0.8.0)
|
google-cloud-storage_transfer-v1 (0.8.0)
|
||||||
gapic-common (>= 0.20.0, < 2.a)
|
gapic-common (>= 0.20.0, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-logging-utils (0.1.0)
|
||||||
google-protobuf (3.25.4)
|
google-protobuf (3.25.4)
|
||||||
google-protobuf (3.25.4-aarch64-linux)
|
google-protobuf (3.25.4-aarch64-linux)
|
||||||
google-protobuf (3.25.4-arm64-darwin)
|
google-protobuf (3.25.4-arm64-darwin)
|
||||||
|
@ -100,8 +101,10 @@ GEM
|
||||||
grpc (~> 1.41)
|
grpc (~> 1.41)
|
||||||
googleapis-common-protos-types (1.15.0)
|
googleapis-common-protos-types (1.15.0)
|
||||||
google-protobuf (>= 3.18, < 5.a)
|
google-protobuf (>= 3.18, < 5.a)
|
||||||
googleauth (1.8.1)
|
googleauth (1.14.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.2)
|
||||||
|
google-logging-utils (~> 0.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
|
@ -253,6 +256,7 @@ CHECKSUMS
|
||||||
google-cloud-errors (1.4.0) sha256=0b4e2e0f563db1708732ab4037421d9f26de5cbbbc04be710f2c9cf358e2de14
|
google-cloud-errors (1.4.0) sha256=0b4e2e0f563db1708732ab4037421d9f26de5cbbbc04be710f2c9cf358e2de14
|
||||||
google-cloud-storage_transfer (1.2.0) sha256=132901f50889e02a0d378e6117c6408cbfc4fdbd15c9d31fabec4f4189ef1658
|
google-cloud-storage_transfer (1.2.0) sha256=132901f50889e02a0d378e6117c6408cbfc4fdbd15c9d31fabec4f4189ef1658
|
||||||
google-cloud-storage_transfer-v1 (0.8.0) sha256=9dbef80275db556e046bb24139ca6559affe641d1e38b2537b8caaf2f8896176
|
google-cloud-storage_transfer-v1 (0.8.0) sha256=9dbef80275db556e046bb24139ca6559affe641d1e38b2537b8caaf2f8896176
|
||||||
|
google-logging-utils (0.1.0) sha256=70950b1e49314273cf2e167adb47b62af7917a4691b580da7e9be67b9205fcd5
|
||||||
google-protobuf (3.25.4) sha256=a1c594ca9d99c894e558f984d70731a8935ec639e75865f0181cab126a0aef0e
|
google-protobuf (3.25.4) sha256=a1c594ca9d99c894e558f984d70731a8935ec639e75865f0181cab126a0aef0e
|
||||||
google-protobuf (3.25.4-aarch64-linux) sha256=d155538358d03af4bcac908811d2c8b287573005f0549d8cf55354ad0c0928ff
|
google-protobuf (3.25.4-aarch64-linux) sha256=d155538358d03af4bcac908811d2c8b287573005f0549d8cf55354ad0c0928ff
|
||||||
google-protobuf (3.25.4-arm64-darwin) sha256=6d39a99a7910fc6b03479c298f38be9497938f78c0f08c89d7542bc8205be8c7
|
google-protobuf (3.25.4-arm64-darwin) sha256=6d39a99a7910fc6b03479c298f38be9497938f78c0f08c89d7542bc8205be8c7
|
||||||
|
@ -261,7 +265,7 @@ CHECKSUMS
|
||||||
google-protobuf (3.25.4-x86_64-linux) sha256=9e8e66fb5a00cf90f88f37b07e7da10ca9e176e28a3314fc80c4e7fdab120aeb
|
google-protobuf (3.25.4-x86_64-linux) sha256=9e8e66fb5a00cf90f88f37b07e7da10ca9e176e28a3314fc80c4e7fdab120aeb
|
||||||
googleapis-common-protos (1.6.0) sha256=d540114a75fd4b34fee936495d28ff7e331d546b7d7ac7898f3b4bb9f13a8d79
|
googleapis-common-protos (1.6.0) sha256=d540114a75fd4b34fee936495d28ff7e331d546b7d7ac7898f3b4bb9f13a8d79
|
||||||
googleapis-common-protos-types (1.15.0) sha256=57b1600c271fa3312096e55a3040d20d2c0f9a5d65d0fde1f16e5cd99bab156b
|
googleapis-common-protos-types (1.15.0) sha256=57b1600c271fa3312096e55a3040d20d2c0f9a5d65d0fde1f16e5cd99bab156b
|
||||||
googleauth (1.8.1) sha256=814adadaaa1221dce72a67131e3ecbd6d23491a161ec84fb15fd353b87d8c9e7
|
googleauth (1.14.0) sha256=62e7de11791890c3d3dc70582dfd9ab5516530e4e4f56d96451fd62c76475149
|
||||||
grpc (1.63.0) sha256=5f4383c4ee2886e92c31b90422261b7527f26e3baa585d877e9804e715983686
|
grpc (1.63.0) sha256=5f4383c4ee2886e92c31b90422261b7527f26e3baa585d877e9804e715983686
|
||||||
grpc (1.63.0-aarch64-linux) sha256=dc75c5fd570b819470781d9512105dddfdd11d984f38b8e60bb946f92d1f79ee
|
grpc (1.63.0-aarch64-linux) sha256=dc75c5fd570b819470781d9512105dddfdd11d984f38b8e60bb946f92d1f79ee
|
||||||
grpc (1.63.0-arm64-darwin) sha256=91b93a354508a9d1772f095554f2e4c04358c2b32d7a670e3705b7fc4695c996
|
grpc (1.63.0-arm64-darwin) sha256=91b93a354508a9d1772f095554f2e4c04358c2b32d7a670e3705b7fc4695c996
|
||||||
|
|
|
@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
|
||||||
spec.add_dependency "activerecord", ">= 7"
|
spec.add_dependency "activerecord", ">= 7"
|
||||||
spec.add_dependency "activesupport", ">= 7"
|
spec.add_dependency "activesupport", ">= 7"
|
||||||
spec.add_dependency "bigdecimal", "~> 3.1"
|
spec.add_dependency "bigdecimal", "~> 3.1"
|
||||||
spec.add_dependency "googleauth", "~> 1.8.1" # https://gitlab.com/gitlab-org/gitlab/-/issues/449019
|
spec.add_dependency "googleauth", "~> 1.14"
|
||||||
spec.add_dependency "google-cloud-storage_transfer", "~> 1.2.0"
|
spec.add_dependency "google-cloud-storage_transfer", "~> 1.2.0"
|
||||||
spec.add_dependency "mutex_m", "~> 0.3"
|
spec.add_dependency "mutex_m", "~> 0.3"
|
||||||
spec.add_dependency "pg", "~> 1.5.6"
|
spec.add_dependency "pg", "~> 1.5.6"
|
||||||
|
|
|
@ -55,7 +55,9 @@ module QA
|
||||||
Page::Main::Login.perform do |login|
|
Page::Main::Login.perform do |login|
|
||||||
login.sign_in_using_credentials(user: admin_user)
|
login.sign_in_using_credentials(user: admin_user)
|
||||||
rescue Runtime::User::ExpiredPasswordError
|
rescue Runtime::User::ExpiredPasswordError
|
||||||
login.set_up_new_password(user: admin_user)
|
Support::Retrier.retry_until(retry_on_exception: true, message: "set_up_new_password failed") do
|
||||||
|
login.set_up_new_password(user: admin_user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Page::Main::Menu.perform(&:sign_out_if_signed_in)
|
Page::Main::Menu.perform(&:sign_out_if_signed_in)
|
||||||
|
|
|
@ -192,6 +192,10 @@ module QA
|
||||||
password = user.password
|
password = user.password
|
||||||
new_password_page.set_new_password(password, password)
|
new_password_page.set_new_password(password, password)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Support::Waiter.wait_until(message: "on_login_page? failed") do
|
||||||
|
Page::Main::Login.perform(&:on_login_page?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This script deletes all projects directly under all 'gitlab-e2e-sandbox-group-<#0-7>' groups OR a group specified by
|
# This script deletes all projects directly under all 'gitlab-e2e-sandbox-group-<#1-8>' groups OR a group specified by
|
||||||
# ENV['TOP_LEVEL_GROUP_NAME']
|
# ENV['TOP_LEVEL_GROUP_NAME']
|
||||||
# - If `dry_run` is true the script will list projects to be deleted, but it won't delete them
|
# - If `dry_run` is true the script will list projects to be deleted, but it won't delete them
|
||||||
|
|
||||||
|
@ -15,18 +15,18 @@
|
||||||
# - Set PERMANENTLY_DELETE to true if you would like to permanently delete subgroups on an environment with
|
# - Set PERMANENTLY_DELETE to true if you would like to permanently delete subgroups on an environment with
|
||||||
# deletion protection enabled. Otherwise, subgroups will remain available during the retention period specified
|
# deletion protection enabled. Otherwise, subgroups will remain available during the retention period specified
|
||||||
# in admin settings. On environments with deletion protection disabled, subgroups will always be permanently deleted.
|
# in admin settings. On environments with deletion protection disabled, subgroups will always be permanently deleted.
|
||||||
# - Set DELETE_BEFORE to only delete projects that were created before a given date, otherwise defaults to 2 hours ago
|
# - Set DELETE_BEFORE to only delete projects that were created before a given date, otherwise defaults to 24 hours ago
|
||||||
|
|
||||||
# Run `rake delete_projects`
|
# Run `rake delete_projects`
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
module Tools
|
module Tools
|
||||||
class DeleteProjects < DeleteResourceBase
|
class DeleteProjects < DeleteResourceBase
|
||||||
# @example mark projects for deletion that are older than 2 hours under all gitlab-e2e-sandbox-group-<#0-7> groups
|
# @example mark projects for deletion that are older than 24 hours under gitlab-e2e-sandbox-group-<#1-8> groups
|
||||||
# GITLAB_ADDRESS=<address> \
|
# GITLAB_ADDRESS=<address> \
|
||||||
# GITLAB_QA_ACCESS_TOKEN=<token> bundle exec rake delete_projects
|
# GITLAB_QA_ACCESS_TOKEN=<token> bundle exec rake delete_projects
|
||||||
#
|
#
|
||||||
# @example permanently delete projects older than 2 hours under all gitlab-e2e-sandbox-group-<#0-7> groups
|
# @example permanently delete projects older than 24 hours under all gitlab-e2e-sandbox-group-<#1-8> groups
|
||||||
# GITLAB_ADDRESS=<address> \
|
# GITLAB_ADDRESS=<address> \
|
||||||
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
||||||
# PERMANENTLY_DELETE=true bundle exec rake delete_projects
|
# PERMANENTLY_DELETE=true bundle exec rake delete_projects
|
||||||
|
|
|
@ -12,14 +12,14 @@ module QA
|
||||||
|
|
||||||
ITEMS_PER_PAGE = '100'
|
ITEMS_PER_PAGE = '100'
|
||||||
PAGE_CUTOFF = '10'
|
PAGE_CUTOFF = '10'
|
||||||
SANDBOX_GROUPS = %w[gitlab-e2e-sandbox-group-0
|
SANDBOX_GROUPS = %w[gitlab-e2e-sandbox-group-1
|
||||||
gitlab-e2e-sandbox-group-1
|
|
||||||
gitlab-e2e-sandbox-group-2
|
gitlab-e2e-sandbox-group-2
|
||||||
gitlab-e2e-sandbox-group-3
|
gitlab-e2e-sandbox-group-3
|
||||||
gitlab-e2e-sandbox-group-4
|
gitlab-e2e-sandbox-group-4
|
||||||
gitlab-e2e-sandbox-group-5
|
gitlab-e2e-sandbox-group-5
|
||||||
gitlab-e2e-sandbox-group-6
|
gitlab-e2e-sandbox-group-6
|
||||||
gitlab-e2e-sandbox-group-7].freeze
|
gitlab-e2e-sandbox-group-7
|
||||||
|
gitlab-e2e-sandbox-group-8].freeze
|
||||||
|
|
||||||
def initialize(dry_run: false)
|
def initialize(dry_run: false)
|
||||||
%w[GITLAB_ADDRESS GITLAB_QA_ACCESS_TOKEN].each do |var|
|
%w[GITLAB_ADDRESS GITLAB_QA_ACCESS_TOKEN].each do |var|
|
||||||
|
@ -28,7 +28,7 @@ module QA
|
||||||
|
|
||||||
@api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'],
|
@api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'],
|
||||||
personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
|
personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
|
||||||
@delete_before = Time.parse(ENV['DELETE_BEFORE'] || (Time.now - (2 * 3600)).to_s).utc.iso8601(3)
|
@delete_before = Time.parse(ENV['DELETE_BEFORE'] || (Time.now - (24 * 3600)).to_s).utc.iso8601(3)
|
||||||
@dry_run = dry_run
|
@dry_run = dry_run
|
||||||
@permanently_delete = !!(ENV['PERMANENTLY_DELETE'].to_s =~ /true|1|y/i)
|
@permanently_delete = !!(ENV['PERMANENTLY_DELETE'].to_s =~ /true|1|y/i)
|
||||||
@type = nil
|
@type = nil
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This script deletes all subgroups of all 'gitlab-e2e-sandbox-group-<#0-7>' groups OR all subgroups of a group
|
# This script deletes all subgroups of all 'gitlab-e2e-sandbox-group-<#1-8>' groups OR all subgroups of a group
|
||||||
# specified by ENV['TOP_LEVEL_GROUP_NAME']
|
# specified by ENV['TOP_LEVEL_GROUP_NAME']
|
||||||
# - If `dry_run` is true the script will list subgroups to be deleted, but it won't delete them
|
# - If `dry_run` is true the script will list subgroups to be deleted, but it won't delete them
|
||||||
|
|
||||||
|
@ -10,22 +10,22 @@
|
||||||
# PERMANENTLY_DELETE (default: false),
|
# PERMANENTLY_DELETE (default: false),
|
||||||
# DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
# DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
||||||
# - Set TOP_LEVEL_GROUP_NAME to only delete subgroups under the given group.
|
# - Set TOP_LEVEL_GROUP_NAME to only delete subgroups under the given group.
|
||||||
# If not set, subgroups of all 'gitlab-e2e-sandbox-group-<#0-7>' groups will be deleted.
|
# If not set, subgroups of all 'gitlab-e2e-sandbox-group-<#1-8>' groups will be deleted.
|
||||||
# - Set PERMANENTLY_DELETE to true if you would like to permanently delete subgroups on an environment with
|
# - Set PERMANENTLY_DELETE to true if you would like to permanently delete subgroups on an environment with
|
||||||
# deletion protection enabled. Otherwise, subgroups will remain available during the retention period specified
|
# deletion protection enabled. Otherwise, subgroups will remain available during the retention period specified
|
||||||
# in admin settings. On environments with deletion protection disabled, subgroups will always be permanently deleted.
|
# in admin settings. On environments with deletion protection disabled, subgroups will always be permanently deleted.
|
||||||
# - Set DELETE_BEFORE to only delete snippets that were created before a given date, otherwise defaults to 2 hours ago
|
# - Set DELETE_BEFORE to only delete snippets that were created before a given date, otherwise defaults to 24 hours ago
|
||||||
|
|
||||||
# Run `rake delete_subgroups`
|
# Run `rake delete_subgroups`
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
module Tools
|
module Tools
|
||||||
class DeleteSubgroups < DeleteResourceBase
|
class DeleteSubgroups < DeleteResourceBase
|
||||||
# @example mark subgroups for deletion that are older than 2 hours under all gitlab-e2e-sandbox-group-<#0-7> groups
|
# @example mark subgroups for deletion that are older than 24 hours under all gitlab-e2e-sandbox-group-<#1-8> groups
|
||||||
# GITLAB_ADDRESS=<address> \
|
# GITLAB_ADDRESS=<address> \
|
||||||
# GITLAB_QA_ACCESS_TOKEN=<token> bundle exec rake delete_subgroups
|
# GITLAB_QA_ACCESS_TOKEN=<token> bundle exec rake delete_subgroups
|
||||||
#
|
#
|
||||||
# @example permanently delete subgroups older than 2 hours under all gitlab-e2e-sandbox-group-<#0-7> groups
|
# @example permanently delete subgroups older than 24 hours under all gitlab-e2e-sandbox-group-<#1-8> groups
|
||||||
# GITLAB_ADDRESS=<address> \
|
# GITLAB_ADDRESS=<address> \
|
||||||
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
||||||
# PERMANENTLY_DELETE=true bundle exec rake delete_subgroups
|
# PERMANENTLY_DELETE=true bundle exec rake delete_subgroups
|
||||||
|
|
|
@ -7,14 +7,14 @@
|
||||||
# - GITLAB_QA_ACCESS_TOKEN should have API access and belong to the user whose snippets will be deleted
|
# - GITLAB_QA_ACCESS_TOKEN should have API access and belong to the user whose snippets will be deleted
|
||||||
|
|
||||||
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
||||||
# - Set DELETE_BEFORE to only delete snippets that were created before a given date, otherwise defaults to 2 hours ago
|
# - Set DELETE_BEFORE to only delete snippets that were created before a given date, otherwise default is 24 hours ago
|
||||||
|
|
||||||
# Run `rake delete_test_snippets`
|
# Run `rake delete_test_snippets`
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
module Tools
|
module Tools
|
||||||
class DeleteTestSnippets < DeleteResourceBase
|
class DeleteTestSnippets < DeleteResourceBase
|
||||||
# @example delete snippets older than 2 hours for the user associated with the given access token
|
# @example delete snippets older than 24 hours for the user associated with the given access token
|
||||||
# GITLAB_ADDRESS=<address> \
|
# GITLAB_ADDRESS=<address> \
|
||||||
# GITLAB_QA_ACCESS_TOKEN=<token> bundle exec rake delete_test_snippets
|
# GITLAB_QA_ACCESS_TOKEN=<token> bundle exec rake delete_test_snippets
|
||||||
#
|
#
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
# - GITLAB_QA_ACCESS_TOKEN should have API access and belong to the user whose keys will be deleted
|
# - GITLAB_QA_ACCESS_TOKEN should have API access and belong to the user whose keys will be deleted
|
||||||
|
|
||||||
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
||||||
# - Set DELETE_BEFORE to only delete snippets that were created before a given date, otherwise defaults to 2 hours ago
|
# - Set DELETE_BEFORE to only delete snippets that were created before a given date, otherwise default is 24 hours ago
|
||||||
|
|
||||||
# Run `rake delete_test_ssh_keys`
|
# Run `rake delete_test_ssh_keys`
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
# - GITLAB_QA_ADMIN_ACCESS_TOKEN must have admin API access
|
# - GITLAB_QA_ADMIN_ACCESS_TOKEN must have admin API access
|
||||||
|
|
||||||
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
||||||
# - Set DELETE_BEFORE to only delete users that were created before a given date, otherwise defaults to 2 hours ago
|
# - Set DELETE_BEFORE to only delete users that were created before a given date, otherwise defaults to 24 hours ago
|
||||||
|
|
||||||
# Run `rake delete_test_users`
|
# Run `rake delete_test_users`
|
||||||
|
|
||||||
|
|
|
@ -5,21 +5,21 @@
|
||||||
|
|
||||||
# Required environment variables: GITLAB_QA_ACCESS_TOKEN, GITLAB_ADDRESS
|
# Required environment variables: GITLAB_QA_ACCESS_TOKEN, GITLAB_ADDRESS
|
||||||
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
||||||
# - Set DELETE_BEFORE to delete only groups that were created before the given date (default: 2 hours ago)
|
# - Set DELETE_BEFORE to delete only groups that were created before the given date (default: 24 hours ago)
|
||||||
|
|
||||||
# Run `rake delete_user_groups`
|
# Run `rake delete_user_groups`
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
module Tools
|
module Tools
|
||||||
class DeleteUserGroups < DeleteResourceBase
|
class DeleteUserGroups < DeleteResourceBase
|
||||||
EXCLUDE_GROUPS = %w[gitlab-e2e-sandbox-group-0
|
EXCLUDE_GROUPS = %w[gitlab-e2e-sandbox-group-1
|
||||||
gitlab-e2e-sandbox-group-1
|
|
||||||
gitlab-e2e-sandbox-group-2
|
gitlab-e2e-sandbox-group-2
|
||||||
gitlab-e2e-sandbox-group-3
|
gitlab-e2e-sandbox-group-3
|
||||||
gitlab-e2e-sandbox-group-4
|
gitlab-e2e-sandbox-group-4
|
||||||
gitlab-e2e-sandbox-group-5
|
gitlab-e2e-sandbox-group-5
|
||||||
gitlab-e2e-sandbox-group-6
|
gitlab-e2e-sandbox-group-6
|
||||||
gitlab-e2e-sandbox-group-7
|
gitlab-e2e-sandbox-group-7
|
||||||
|
gitlab-e2e-sandbox-group-8
|
||||||
quality-e2e-tests
|
quality-e2e-tests
|
||||||
quality-e2e-tests-2
|
quality-e2e-tests-2
|
||||||
quality-e2e-tests-3
|
quality-e2e-tests-3
|
||||||
|
@ -31,7 +31,7 @@ module QA
|
||||||
qa-perf-testing
|
qa-perf-testing
|
||||||
remote-development].freeze
|
remote-development].freeze
|
||||||
|
|
||||||
# @example - delete user groups older than 2 hours
|
# @example - delete user groups older than 24 hours
|
||||||
# GITLAB_ADDRESS=<address> \
|
# GITLAB_ADDRESS=<address> \
|
||||||
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
||||||
# bundle exec rake delete_user_groups
|
# bundle exec rake delete_user_groups
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
# - USER_ID to the id of the user whose projects are to be deleted.
|
# - USER_ID to the id of the user whose projects are to be deleted.
|
||||||
|
|
||||||
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
# Optional environment variables: DELETE_BEFORE - YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or YYYY-MM-DDT00:00:00Z
|
||||||
# - Set DELETE_BEFORE to delete only projects that were created before the given date (default: 2 hours ago)
|
# - Set DELETE_BEFORE to delete only projects that were created before the given date (default: 24 hours ago)
|
||||||
|
|
||||||
# Run `rake delete_user_projects`
|
# Run `rake delete_user_projects`
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ module QA
|
||||||
gitlab-qa-user5
|
gitlab-qa-user5
|
||||||
gitlab-qa-user6].freeze
|
gitlab-qa-user6].freeze
|
||||||
|
|
||||||
# @example - delete the given users projects older than 2 hours
|
# @example - delete the given users projects older than 24 hours
|
||||||
# GITLAB_ADDRESS=<address> \
|
# GITLAB_ADDRESS=<address> \
|
||||||
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
# GITLAB_QA_ACCESS_TOKEN=<token> \
|
||||||
# USER_ID=<id> bundle exec rake delete_user_projects
|
# USER_ID=<id> bundle exec rake delete_user_projects
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||||
import { useNotes } from '~/notes/store/legacy_notes';
|
import { useNotes } from '~/notes/store/legacy_notes';
|
||||||
import { useBatchComments } from '~/batch_comments/store';
|
import { useBatchComments } from '~/batch_comments/store';
|
||||||
import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
|
import { noteableDataMock, notesDataMock, discussionMock } from '../mock_data';
|
||||||
|
|
||||||
jest.mock('~/lib/utils/autosave');
|
jest.mock('~/lib/utils/autosave');
|
||||||
|
|
||||||
|
@ -138,15 +138,15 @@ describe('issue_note_form component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
internal | placeholder
|
confidential | placeholder
|
||||||
${false} | ${'Write a comment or drag your files here…'}
|
${false} | ${'Write a comment or drag your files here…'}
|
||||||
${true} | ${'Write an internal note or drag your files here…'}
|
${true} | ${'Write an internal note or drag your files here…'}
|
||||||
`(
|
`(
|
||||||
'should set correct textarea placeholder text when discussion confidentiality is $internal',
|
'should set correct textarea placeholder text when discussion confidentiality is $internal',
|
||||||
async ({ internal, placeholder }) => {
|
async ({ confidential, placeholder }) => {
|
||||||
props.note = {
|
props.discussion = {
|
||||||
...note,
|
...discussionMock,
|
||||||
internal,
|
confidential,
|
||||||
};
|
};
|
||||||
createComponentWrapper();
|
createComponentWrapper();
|
||||||
|
|
||||||
|
@ -244,9 +244,9 @@ describe('issue_note_form component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when discussion is internal', () => {
|
describe('when discussion is confidential', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponentWrapper({ note: { internal: true } });
|
createComponentWrapper({ discussion: { confidential: true } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes correct internal note information to CommentFieldLayout', () => {
|
it('passes correct internal note information to CommentFieldLayout', () => {
|
||||||
|
|
|
@ -143,6 +143,31 @@ describe('LabelToken', () => {
|
||||||
it('sets `loading` to false when request completes', () => {
|
it('sets `loading` to false when request completes', () => {
|
||||||
expect(findBaseToken().props('suggestionsLoading')).toBe(false);
|
expect(findBaseToken().props('suggestionsLoading')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('only shows each label title once', async () => {
|
||||||
|
const duplicateLabels = [
|
||||||
|
{ id: 1, title: 'Example', color: '#FF0000', textColor: '#FFFFFF' },
|
||||||
|
{ id: 2, title: 'Example', color: '#00FF00', textColor: '#000000' },
|
||||||
|
{ id: 3, title: 'Unique Label', color: '#0000FF', textColor: '#FFFFFF' },
|
||||||
|
];
|
||||||
|
|
||||||
|
wrapper = createComponent({
|
||||||
|
config: {
|
||||||
|
fetchLabels: jest.fn().mockResolvedValue({ data: duplicateLabels }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await triggerFetchLabels(searchTerm);
|
||||||
|
|
||||||
|
const suggestions = findBaseToken().props('suggestions');
|
||||||
|
const labelTitles = suggestions.map((label) => label.title);
|
||||||
|
|
||||||
|
// Should only have one "Example" label
|
||||||
|
expect(labelTitles.filter((title) => title === 'Example')).toHaveLength(1);
|
||||||
|
// Should still have the unique label
|
||||||
|
expect(labelTitles).toContain('Unique Label');
|
||||||
|
// Total count should be 2 (one deduplicated "Example" + one "Unique Label")
|
||||||
|
expect(suggestions).toHaveLength(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when request fails', () => {
|
describe('when request fails', () => {
|
||||||
|
|
|
@ -15,35 +15,73 @@ import WorkItemBulkEditParent from '~/work_items/components/work_item_bulk_edit/
|
||||||
import WorkItemBulkEditSidebar from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue';
|
import WorkItemBulkEditSidebar from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue';
|
||||||
import workItemBulkUpdateMutation from '~/work_items/graphql/list/work_item_bulk_update.mutation.graphql';
|
import workItemBulkUpdateMutation from '~/work_items/graphql/list/work_item_bulk_update.mutation.graphql';
|
||||||
import workItemParentQuery from '~/work_items/graphql/list//work_item_parent.query.graphql';
|
import workItemParentQuery from '~/work_items/graphql/list//work_item_parent.query.graphql';
|
||||||
import { workItemParentQueryResponse } from '../../mock_data';
|
import getAvailableBulkEditWidgets from '~/work_items/graphql/list/get_available_bulk_edit_widgets.query.graphql';
|
||||||
|
import {
|
||||||
|
WIDGET_TYPE_ASSIGNEES,
|
||||||
|
WIDGET_TYPE_HEALTH_STATUS,
|
||||||
|
WIDGET_TYPE_HIERARCHY,
|
||||||
|
WIDGET_TYPE_LABELS,
|
||||||
|
WIDGET_TYPE_MILESTONE,
|
||||||
|
} from '~/work_items/constants';
|
||||||
|
import {
|
||||||
|
workItemParentQueryResponse,
|
||||||
|
availableBulkEditWidgetsQueryResponse,
|
||||||
|
} from '../../mock_data';
|
||||||
|
|
||||||
jest.mock('~/alert');
|
jest.mock('~/alert');
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
|
const availableWidgetsWithout = (widgetToExclude) => {
|
||||||
|
const widgetNames = availableBulkEditWidgetsQueryResponse.data.namespace.workItemsWidgets.filter(
|
||||||
|
(name) => name !== widgetToExclude,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
namespace: {
|
||||||
|
...availableBulkEditWidgetsQueryResponse.data.namespace,
|
||||||
|
workItemsWidgets: widgetNames,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
describe('WorkItemBulkEditSidebar component', () => {
|
describe('WorkItemBulkEditSidebar component', () => {
|
||||||
let axiosMock;
|
let axiosMock;
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const checkedItems = [
|
const checkedItems = [
|
||||||
{ id: 'gid://gitlab/WorkItem/11', title: 'Work Item 11' },
|
{
|
||||||
{ id: 'gid://gitlab/WorkItem/22', title: 'Work Item 22' },
|
id: 'gid://gitlab/WorkItem/11',
|
||||||
|
title: 'Work Item 11',
|
||||||
|
workItemType: { id: 'gid://gitlab/WorkItems::Type/8' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/WorkItem/22',
|
||||||
|
title: 'Work Item 22',
|
||||||
|
workItemType: { id: 'gid://gitlab/WorkItems::Type/5' },
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const workItemParentQueryHandler = jest.fn().mockResolvedValue(workItemParentQueryResponse);
|
const workItemParentQueryHandler = jest.fn().mockResolvedValue(workItemParentQueryResponse);
|
||||||
const workItemBulkUpdateHandler = jest
|
const workItemBulkUpdateHandler = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue({ data: { workItemBulkUpdate: { updatedWorkItemCount: 1 } } });
|
.mockResolvedValue({ data: { workItemBulkUpdate: { updatedWorkItemCount: 1 } } });
|
||||||
|
const defaultAvailableWidgetsHandler = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(availableBulkEditWidgetsQueryResponse);
|
||||||
|
|
||||||
const createComponent = ({
|
const createComponent = ({
|
||||||
provide = {},
|
provide = {},
|
||||||
props = {},
|
props = {},
|
||||||
mutationHandler = workItemBulkUpdateHandler,
|
mutationHandler = workItemBulkUpdateHandler,
|
||||||
|
availableWidgetsHandler = defaultAvailableWidgetsHandler,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
wrapper = shallowMountExtended(WorkItemBulkEditSidebar, {
|
wrapper = shallowMountExtended(WorkItemBulkEditSidebar, {
|
||||||
apolloProvider: createMockApollo([
|
apolloProvider: createMockApollo([
|
||||||
[workItemParentQuery, workItemParentQueryHandler],
|
[workItemParentQuery, workItemParentQueryHandler],
|
||||||
[workItemBulkUpdateMutation, mutationHandler],
|
[workItemBulkUpdateMutation, mutationHandler],
|
||||||
|
[getAvailableBulkEditWidgets, availableWidgetsHandler],
|
||||||
]),
|
]),
|
||||||
provide: {
|
provide: {
|
||||||
hasIssuableHealthStatusFeature: false,
|
hasIssuableHealthStatusFeature: false,
|
||||||
|
@ -270,6 +308,35 @@ describe('WorkItemBulkEditSidebar component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getAvailableBulkEditWidgets query', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent({ provide: { glFeatures: { workItemsBulkEdit: true } } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is called when mounted', () => {
|
||||||
|
expect(defaultAvailableWidgetsHandler).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is called when checkedItems is updated and there is a new work item type', async () => {
|
||||||
|
await wrapper.setProps({
|
||||||
|
checkedItems: [
|
||||||
|
...checkedItems,
|
||||||
|
{
|
||||||
|
id: 'gid://gitlab/WorkItem/14',
|
||||||
|
title: 'Work Item 14',
|
||||||
|
workItemType: { id: 'gid://gitlab/WorkItems::Type/9' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
// once on initial mount, once when checked items change
|
||||||
|
expect(defaultAvailableWidgetsHandler).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('workItemParent query', () => {
|
describe('workItemParent query', () => {
|
||||||
it('is called when isEpicsList=true', () => {
|
it('is called when isEpicsList=true', () => {
|
||||||
createComponent({ props: { isEpicsList: true } });
|
createComponent({ props: { isEpicsList: true } });
|
||||||
|
@ -316,6 +383,41 @@ describe('WorkItemBulkEditSidebar component', () => {
|
||||||
|
|
||||||
expect(findAssigneeComponent().props('value')).toBe('gid://gitlab/User/5');
|
expect(findAssigneeComponent().props('value')).toBe('gid://gitlab/User/5');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('enables "Assignee" component when "Assignees" widget is available', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findAssigneeComponent().props('disabled')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Assignee" component when "Assignees" widget is unavailable', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
availableWidgetsHandler: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(availableWidgetsWithout(WIDGET_TYPE_ASSIGNEES)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findAssigneeComponent().props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('"Add labels" component', () => {
|
describe('"Add labels" component', () => {
|
||||||
|
@ -335,6 +437,41 @@ describe('WorkItemBulkEditSidebar component', () => {
|
||||||
|
|
||||||
expect(findAddLabelsComponent().props('selectedLabelsIds')).toEqual(labelIds);
|
expect(findAddLabelsComponent().props('selectedLabelsIds')).toEqual(labelIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('enables "Add labels" component when "Labels" widget is available', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findAddLabelsComponent().props('disabled')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Add labels" component when "Labels" widget is unavailable', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
availableWidgetsHandler: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(availableWidgetsWithout(WIDGET_TYPE_LABELS)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findAddLabelsComponent().props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('"Remove labels" component', () => {
|
describe('"Remove labels" component', () => {
|
||||||
|
@ -354,6 +491,41 @@ describe('WorkItemBulkEditSidebar component', () => {
|
||||||
|
|
||||||
expect(findRemoveLabelsComponent().props('selectedLabelsIds')).toEqual(labelIds);
|
expect(findRemoveLabelsComponent().props('selectedLabelsIds')).toEqual(labelIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('enables "Remove labels" component when "Labels" widget is available', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findRemoveLabelsComponent().props('disabled')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Remove labels" component when "Labels" widget is unavailable', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
availableWidgetsHandler: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(availableWidgetsWithout(WIDGET_TYPE_LABELS)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findRemoveLabelsComponent().props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('"Health status" component', () => {
|
describe('"Health status" component', () => {
|
||||||
|
@ -380,6 +552,43 @@ describe('WorkItemBulkEditSidebar component', () => {
|
||||||
|
|
||||||
expect(findHealthStatusComponent().props('value')).toBe('needs_attention');
|
expect(findHealthStatusComponent().props('value')).toBe('needs_attention');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('enables "Health status" component when "Health status" widget is available', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
hasIssuableHealthStatusFeature: true,
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findHealthStatusComponent().props('disabled')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Health status" component when "Health status" widget is unavailable', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
hasIssuableHealthStatusFeature: true,
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
availableWidgetsHandler: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(availableWidgetsWithout(WIDGET_TYPE_HEALTH_STATUS)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findHealthStatusComponent().props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('"Subscription" component', () => {
|
describe('"Subscription" component', () => {
|
||||||
|
@ -445,6 +654,41 @@ describe('WorkItemBulkEditSidebar component', () => {
|
||||||
|
|
||||||
expect(findMilestoneComponent().props('value')).toBe('gid://gitlab/Milestone/30');
|
expect(findMilestoneComponent().props('value')).toBe('gid://gitlab/Milestone/30');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('enables "Milestone" component when "Milestone" widget is available', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findMilestoneComponent().props('disabled')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Milestone" component when "Milestone" widget is unavailable', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
availableWidgetsHandler: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(availableWidgetsWithout(WIDGET_TYPE_MILESTONE)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findMilestoneComponent().props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('"Parent" component', () => {
|
describe('"Parent" component', () => {
|
||||||
|
@ -476,5 +720,40 @@ describe('WorkItemBulkEditSidebar component', () => {
|
||||||
|
|
||||||
expect(findParentComponent().props('value')).toBe('gid://gitlab/WorkItem/30');
|
expect(findParentComponent().props('value')).toBe('gid://gitlab/WorkItem/30');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('enables "Parent" component when "Hierarchy" widget is available', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findParentComponent().props('disabled')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables "Parent" component when "Hierarchy" widget is unavailable', async () => {
|
||||||
|
createComponent({
|
||||||
|
provide: {
|
||||||
|
glFeatures: {
|
||||||
|
workItemsBulkEdit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { isEpicsList: false },
|
||||||
|
availableWidgetsHandler: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(availableWidgetsWithout(WIDGET_TYPE_HIERARCHY)),
|
||||||
|
});
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(findParentComponent().props('disabled')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -8875,3 +8875,39 @@ export const namespaceWorkItemTypesWithOKRsQueryResponse = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const availableBulkEditWidgetsQueryResponse = {
|
||||||
|
data: {
|
||||||
|
namespace: {
|
||||||
|
__typename: 'Namespace',
|
||||||
|
id: 'gid://gitlab/Namespaces::ProjectNamespace/34',
|
||||||
|
workItemsWidgets: [
|
||||||
|
'ASSIGNEES',
|
||||||
|
'AWARD_EMOJI',
|
||||||
|
'CRM_CONTACTS',
|
||||||
|
'CURRENT_USER_TODOS',
|
||||||
|
'CUSTOM_FIELDS',
|
||||||
|
'DESCRIPTION',
|
||||||
|
'DESIGNS',
|
||||||
|
'DEVELOPMENT',
|
||||||
|
'EMAIL_PARTICIPANTS',
|
||||||
|
'ERROR_TRACKING',
|
||||||
|
'HEALTH_STATUS',
|
||||||
|
'HIERARCHY',
|
||||||
|
'ITERATION',
|
||||||
|
'LABELS',
|
||||||
|
'LINKED_ITEMS',
|
||||||
|
'LINKED_RESOURCES',
|
||||||
|
'MILESTONE',
|
||||||
|
'NOTES',
|
||||||
|
'NOTIFICATIONS',
|
||||||
|
'PARTICIPANTS',
|
||||||
|
'START_AND_DUE_DATE',
|
||||||
|
'STATUS',
|
||||||
|
'TIME_TRACKING',
|
||||||
|
'VULNERABILITIES',
|
||||||
|
'WEIGHT',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -8,13 +8,42 @@ RSpec.describe Tooling::PredictiveTests::MappingFetcher, :aggregate_failures, fe
|
||||||
let(:crystalball_mapping) { "test-mapping.json" }
|
let(:crystalball_mapping) { "test-mapping.json" }
|
||||||
let(:frontend_fixtures) { "ff_fixtures.json" }
|
let(:frontend_fixtures) { "ff_fixtures.json" }
|
||||||
|
|
||||||
let(:logger) { Logger.new(StringIO.new) }
|
let(:logger) { Logger.new($stdout, level: :error) }
|
||||||
let(:file_double) { instance_double(File, write: nil) }
|
let(:file_double) { instance_double(File, write: nil) }
|
||||||
let(:http_response) { double("HTTParty::Response", success?: http_success, code: 500, message: "message") } # rubocop:disable RSpec/VerifiedDoubles -- complains about message
|
|
||||||
let(:open3_status) { instance_double(Process::Status, success?: extract_success) }
|
let(:open3_status) { instance_double(Process::Status, success?: extract_success) }
|
||||||
|
|
||||||
|
let(:file_stat) do
|
||||||
|
instance_double(File::Stat, size: 123, mtime: Time.parse("Tue, 01 Jan 2024 12:34:56 GMT"))
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop:disable RSpec/VerifiedDoubles -- complains about message
|
||||||
|
let(:get_http_response) do
|
||||||
|
double(
|
||||||
|
"HTTParty::Response",
|
||||||
|
success?: http_success,
|
||||||
|
code: 500,
|
||||||
|
message: "message",
|
||||||
|
headers: { "last-modified" => upstream_last_modified.httpdate }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:head_http_response) do
|
||||||
|
double(
|
||||||
|
"HTTPParty::Response",
|
||||||
|
success?: true,
|
||||||
|
code: 200,
|
||||||
|
headers: {
|
||||||
|
"last-modified" => upstream_last_modified.httpdate,
|
||||||
|
"content-length" => upstream_content
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
# rubocop:enable RSpec/VerifiedDoubles
|
||||||
|
|
||||||
let(:extract_success) { true }
|
let(:extract_success) { true }
|
||||||
let(:http_success) { true }
|
let(:http_success) { true }
|
||||||
|
let(:upstream_last_modified) { Time.parse("Tue, 02 Jan 2024 12:34:56 GMT") }
|
||||||
|
let(:upstream_content) { 123 }
|
||||||
|
|
||||||
let(:unpacked_mapping) { JSON.generate(Tooling::TestMapPacker.new.unpack(JSON.parse(packed_mapping))) } # rubocop:disable Gitlab/Json -- non rails code
|
let(:unpacked_mapping) { JSON.generate(Tooling::TestMapPacker.new.unpack(JSON.parse(packed_mapping))) } # rubocop:disable Gitlab/Json -- non rails code
|
||||||
let(:packed_mapping) do
|
let(:packed_mapping) do
|
||||||
|
@ -41,6 +70,12 @@ RSpec.describe Tooling::PredictiveTests::MappingFetcher, :aggregate_failures, fe
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
allow(described_class).to receive(:head)
|
||||||
|
.with(
|
||||||
|
%r{https://gitlab-org.gitlab.io/gitlab/crystalball/(packed-mapping.json.gz|frontend_fixtures_mapping.json)},
|
||||||
|
timeout: 30
|
||||||
|
)
|
||||||
|
.and_return(head_http_response)
|
||||||
allow(described_class).to receive(:get)
|
allow(described_class).to receive(:get)
|
||||||
.with(
|
.with(
|
||||||
%r{https://gitlab-org.gitlab.io/gitlab/crystalball/(packed-mapping.json.gz|frontend_fixtures_mapping.json)},
|
%r{https://gitlab-org.gitlab.io/gitlab/crystalball/(packed-mapping.json.gz|frontend_fixtures_mapping.json)},
|
||||||
|
@ -48,7 +83,7 @@ RSpec.describe Tooling::PredictiveTests::MappingFetcher, :aggregate_failures, fe
|
||||||
stream_body: true
|
stream_body: true
|
||||||
)
|
)
|
||||||
.and_yield("download fragment")
|
.and_yield("download fragment")
|
||||||
.and_return(http_response)
|
.and_return(get_http_response)
|
||||||
|
|
||||||
allow(Open3).to receive(:capture3).with(/gzip -d -c \S+mapping\.gz > \S+/).and_return(["out", "err", open3_status])
|
allow(Open3).to receive(:capture3).with(/gzip -d -c \S+mapping\.gz > \S+/).and_return(["out", "err", open3_status])
|
||||||
# mock file operations only related to mapping fetcher
|
# mock file operations only related to mapping fetcher
|
||||||
|
@ -58,16 +93,25 @@ RSpec.describe Tooling::PredictiveTests::MappingFetcher, :aggregate_failures, fe
|
||||||
allow(File).to receive(:read).with(/mapping\.json/).and_return(packed_mapping)
|
allow(File).to receive(:read).with(/mapping\.json/).and_return(packed_mapping)
|
||||||
allow(File).to receive(:write).and_call_original
|
allow(File).to receive(:write).and_call_original
|
||||||
allow(File).to receive(:write).with(crystalball_mapping, unpacked_mapping)
|
allow(File).to receive(:write).with(crystalball_mapping, unpacked_mapping)
|
||||||
|
allow(File).to receive(:exist?).and_call_original
|
||||||
|
allow(File).to receive(:exist?).with(/mapping\.gz|#{frontend_fixtures}/).and_return(true)
|
||||||
|
allow(File).to receive(:utime).and_call_original
|
||||||
|
allow(File).to receive(:utime).with(upstream_last_modified, upstream_last_modified, kind_of(String))
|
||||||
|
allow(File).to receive(:stat).and_call_original
|
||||||
|
allow(File).to receive(:stat).with(/mapping\.gz|#{frontend_fixtures}/).and_return(file_stat)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fetches rspec mappings" do
|
it "fetches rspec mappings" do
|
||||||
expect(mapping_fetcher.fetch_rspec_mappings(crystalball_mapping)).to eq(crystalball_mapping)
|
expect(mapping_fetcher.fetch_rspec_mappings(crystalball_mapping)).to eq(crystalball_mapping)
|
||||||
|
expect(described_class).to have_received(:get)
|
||||||
expect(file_double).to have_received(:write).with("download fragment")
|
expect(file_double).to have_received(:write).with("download fragment")
|
||||||
|
expect(Open3).to have_received(:capture3).with(/gzip -d -c \S+mapping\.gz > \S+/)
|
||||||
expect(File).to have_received(:write).with(crystalball_mapping, unpacked_mapping)
|
expect(File).to have_received(:write).with(crystalball_mapping, unpacked_mapping)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fetches frontend fixtures" do
|
it "fetches frontend fixtures" do
|
||||||
expect(mapping_fetcher.fetch_frontend_fixtures_mappings(frontend_fixtures)).to eq(frontend_fixtures)
|
expect(mapping_fetcher.fetch_frontend_fixtures_mappings(frontend_fixtures)).to eq(frontend_fixtures)
|
||||||
|
expect(described_class).to have_received(:get)
|
||||||
expect(file_double).to have_received(:write).with("download fragment")
|
expect(file_double).to have_received(:write).with("download fragment")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -96,4 +140,18 @@ RSpec.describe Tooling::PredictiveTests::MappingFetcher, :aggregate_failures, fe
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with files being cached" do
|
||||||
|
let(:upstream_last_modified) { Time.parse("Tue, 01 Jan 2024 12:34:56 GMT") }
|
||||||
|
|
||||||
|
it "skips downloading rspec mapping archive" do
|
||||||
|
expect(mapping_fetcher.fetch_rspec_mappings(crystalball_mapping)).to eq(crystalball_mapping)
|
||||||
|
expect(described_class).not_to have_received(:get)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "skips downloading frontend fixtures" do
|
||||||
|
expect(mapping_fetcher.fetch_frontend_fixtures_mappings(frontend_fixtures)).to eq(frontend_fixtures)
|
||||||
|
expect(described_class).not_to have_received(:get)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,8 +14,6 @@ RSpec.describe Tooling::PredictiveTests::MetricsExporter, feature_category: :too
|
||||||
subject(:exporter) do
|
subject(:exporter) do
|
||||||
described_class.new(
|
described_class.new(
|
||||||
rspec_all_failed_tests_file: failed_tests_file,
|
rspec_all_failed_tests_file: failed_tests_file,
|
||||||
crystalball_mapping_dir: input_dir,
|
|
||||||
frontend_fixtures_mapping_file: frontend_fixtures_file,
|
|
||||||
output_dir: output_dir
|
output_dir: output_dir
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -24,6 +22,14 @@ RSpec.describe Tooling::PredictiveTests::MetricsExporter, feature_category: :too
|
||||||
let(:logger) { Logger.new(log_output) }
|
let(:logger) { Logger.new(log_output) }
|
||||||
let(:log_output) { StringIO.new } # useful for debugging to print out all log output
|
let(:log_output) { StringIO.new } # useful for debugging to print out all log output
|
||||||
|
|
||||||
|
let(:mapping_fetcher) do
|
||||||
|
instance_double(
|
||||||
|
Tooling::PredictiveTests::MappingFetcher,
|
||||||
|
fetch_frontend_fixtures_mappings: nil,
|
||||||
|
fetch_rspec_mappings: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
let(:test_selector_described) do
|
let(:test_selector_described) do
|
||||||
instance_double(Tooling::PredictiveTests::TestSelector, rspec_spec_list: matching_tests_described_class_specs)
|
instance_double(Tooling::PredictiveTests::TestSelector, rspec_spec_list: matching_tests_described_class_specs)
|
||||||
end
|
end
|
||||||
|
@ -39,10 +45,10 @@ RSpec.describe Tooling::PredictiveTests::MetricsExporter, feature_category: :too
|
||||||
let(:input_dir) { Dir.mktmpdir("predictive-tests-input") }
|
let(:input_dir) { Dir.mktmpdir("predictive-tests-input") }
|
||||||
let(:output_dir) { Dir.mktmpdir("predictive-tests-output") }
|
let(:output_dir) { Dir.mktmpdir("predictive-tests-output") }
|
||||||
# various input files used by MetricsExporter to create metrics output
|
# various input files used by MetricsExporter to create metrics output
|
||||||
let(:coverage_mapping_file) { File.join(input_dir, "coverage", "mapping.json") }
|
|
||||||
let(:described_class_mapping_file) { File.join(input_dir, "described_class", "mapping.json") }
|
|
||||||
let(:failed_tests_file) { File.join(input_dir, "failed_test.txt") }
|
let(:failed_tests_file) { File.join(input_dir, "failed_test.txt") }
|
||||||
let(:frontend_fixtures_file) { File.join(input_dir, "frontend_fixtures.json") }
|
let(:coverage_mapping_file) { File.join(Dir.tmpdir, "coverage", "mapping.json") }
|
||||||
|
let(:described_class_mapping_file) { File.join(Dir.tmpdir, "described_class", "mapping.json") }
|
||||||
|
let(:frontend_fixtures_file) { File.join(Dir.tmpdir, "frontend_fixtures_mapping.json") }
|
||||||
# output files created by TestSelector and used by MetricsExporter to create metrics output
|
# output files created by TestSelector and used by MetricsExporter to create metrics output
|
||||||
let(:matching_tests_coverage_file) { File.join(output_dir, "coverage", "rspec_matching_test_files.txt") }
|
let(:matching_tests_coverage_file) { File.join(output_dir, "coverage", "rspec_matching_test_files.txt") }
|
||||||
let(:matching_tests_described_class_file) do
|
let(:matching_tests_described_class_file) do
|
||||||
|
@ -101,10 +107,9 @@ RSpec.describe Tooling::PredictiveTests::MetricsExporter, feature_category: :too
|
||||||
before do
|
before do
|
||||||
stub_env({ "CI_JOB_ID" => extra_properties[:ci_job_id] })
|
stub_env({ "CI_JOB_ID" => extra_properties[:ci_job_id] })
|
||||||
|
|
||||||
# create folders for separate strategies
|
# create folders for mocked input files
|
||||||
[input_dir, output_dir].each do |dir|
|
[coverage_mapping_file, described_class_mapping_file, failed_tests_file].each do |file|
|
||||||
FileUtils.mkdir_p(File.join(dir, "coverage"))
|
FileUtils.mkdir_p(File.dirname(file))
|
||||||
FileUtils.mkdir_p(File.join(dir, "described_class"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# create files used as input for exporting selected test metrics
|
# create files used as input for exporting selected test metrics
|
||||||
|
@ -115,6 +120,7 @@ RSpec.describe Tooling::PredictiveTests::MetricsExporter, feature_category: :too
|
||||||
allow(Logger).to receive(:new).with($stdout, progname: "rspec predictive testing").and_return(logger)
|
allow(Logger).to receive(:new).with($stdout, progname: "rspec predictive testing").and_return(logger)
|
||||||
allow(Tooling::Events::TrackPipelineEvents).to receive(:new).and_return(event_tracker)
|
allow(Tooling::Events::TrackPipelineEvents).to receive(:new).and_return(event_tracker)
|
||||||
|
|
||||||
|
allow(Tooling::PredictiveTests::MappingFetcher).to receive(:new).with(logger: logger).and_return(mapping_fetcher)
|
||||||
allow(Tooling::PredictiveTests::ChangedFiles).to receive(:fetch).with(
|
allow(Tooling::PredictiveTests::ChangedFiles).to receive(:fetch).with(
|
||||||
frontend_fixtures_file: frontend_fixtures_file
|
frontend_fixtures_file: frontend_fixtures_file
|
||||||
).and_return(changed_files)
|
).and_return(changed_files)
|
||||||
|
@ -133,6 +139,17 @@ RSpec.describe Tooling::PredictiveTests::MetricsExporter, feature_category: :too
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#execute" do
|
describe "#execute" do
|
||||||
|
it "uses mapping fetcher to get mapping json files" do
|
||||||
|
exporter.execute
|
||||||
|
|
||||||
|
expect(mapping_fetcher).to have_received(:fetch_rspec_mappings)
|
||||||
|
.with(coverage_mapping_file, type: :coverage)
|
||||||
|
expect(mapping_fetcher).to have_received(:fetch_rspec_mappings)
|
||||||
|
.with(described_class_mapping_file, type: :described_class)
|
||||||
|
expect(mapping_fetcher).to have_received(:fetch_frontend_fixtures_mappings)
|
||||||
|
.with(frontend_fixtures_file)
|
||||||
|
end
|
||||||
|
|
||||||
it "exports metrics for described_class strategy", :aggregate_failures do
|
it "exports metrics for described_class strategy", :aggregate_failures do
|
||||||
exporter.execute
|
exporter.execute
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,10 @@ options = {}
|
||||||
OptionParser.new do |opts|
|
OptionParser.new do |opts|
|
||||||
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
||||||
|
|
||||||
|
opts.on('--ci', 'Use ci specific setup') do
|
||||||
|
options[:ci] = true
|
||||||
|
end
|
||||||
|
|
||||||
opts.on('--select-tests', 'Run test selection logic') do
|
opts.on('--select-tests', 'Run test selection logic') do
|
||||||
options[:select_tests] = true
|
options[:select_tests] = true
|
||||||
end
|
end
|
||||||
|
@ -39,6 +43,10 @@ OptionParser.new do |opts|
|
||||||
options[:mapping_type] = value
|
options[:mapping_type] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
opts.on('--changed-files [string]', String, 'Space separated list of changed files. Used outside CI.') do |value|
|
||||||
|
options[:changed_files] = value
|
||||||
|
end
|
||||||
|
|
||||||
opts.on('-h', '--help', 'Show this help message') do
|
opts.on('-h', '--help', 'Show this help message') do
|
||||||
puts opts
|
puts opts
|
||||||
exit
|
exit
|
||||||
|
@ -71,52 +79,70 @@ if options[:select_tests]
|
||||||
require_relative '../lib/tooling/predictive_tests/changed_files'
|
require_relative '../lib/tooling/predictive_tests/changed_files'
|
||||||
require_relative '../lib/tooling/predictive_tests/mapping_fetcher'
|
require_relative '../lib/tooling/predictive_tests/mapping_fetcher'
|
||||||
|
|
||||||
validate_required_env_variables!(%w[
|
ci = options[:ci]
|
||||||
RSPEC_MATCHING_TEST_FILES_PATH
|
|
||||||
FRONTEND_FIXTURES_MAPPING_PATH
|
|
||||||
RSPEC_MATCHING_JS_FILES_PATH
|
|
||||||
])
|
|
||||||
|
|
||||||
logger = Logger.new($stdout, progname: '[Predictive Tests]')
|
if ci
|
||||||
|
validate_required_env_variables!(%w[
|
||||||
|
RSPEC_MATCHING_TEST_FILES_PATH
|
||||||
|
FRONTEND_FIXTURES_MAPPING_PATH
|
||||||
|
RSPEC_MATCHING_JS_FILES_PATH
|
||||||
|
])
|
||||||
|
elsif options[:changed_files].nil? || options[:changed_files].strip.empty?
|
||||||
|
warn 'Please provide --changed-files with list of changed files for local run.'
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# silence all output on non ci and only output list of spec files to run so it could be used in scripts/hooks
|
||||||
|
logger = Logger.new($stdout, progname: '[Predictive Tests]', level: ci ? :info : :error)
|
||||||
logger.info("Running predictive test selection")
|
logger.info("Running predictive test selection")
|
||||||
|
|
||||||
mapping_fetcher = Tooling::PredictiveTests::MappingFetcher.new(logger: logger)
|
mapping_fetcher = Tooling::PredictiveTests::MappingFetcher.new(logger: logger)
|
||||||
test_mapping_file = if options[:with_crystalball_mappings]
|
test_mapping_file = if options[:with_crystalball_mappings]
|
||||||
mapping_fetcher.fetch_rspec_mappings(
|
mapping_fetcher.fetch_rspec_mappings(
|
||||||
'mapping.json',
|
'tmp/crystalball_mappings.json',
|
||||||
type: options[:mapping_type] || :described_class
|
type: options[:mapping_type] || :described_class
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
changed_files = Tooling::PredictiveTests::ChangedFiles.fetch(
|
changed_files = if ci
|
||||||
frontend_fixtures_file: mapping_fetcher.fetch_frontend_fixtures_mappings(ENV['FRONTEND_FIXTURES_MAPPING_PATH'])
|
Tooling::PredictiveTests::ChangedFiles.fetch(
|
||||||
)
|
frontend_fixtures_file: mapping_fetcher.fetch_frontend_fixtures_mappings(
|
||||||
|
ENV['FRONTEND_FIXTURES_MAPPING_PATH']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
options[:changed_files].split(" ")
|
||||||
|
end
|
||||||
|
|
||||||
test_selector = Tooling::PredictiveTests::TestSelector.new(
|
test_selector = Tooling::PredictiveTests::TestSelector.new(
|
||||||
changed_files: changed_files,
|
changed_files: changed_files,
|
||||||
rspec_test_mapping_path: test_mapping_file,
|
rspec_test_mapping_path: test_mapping_file,
|
||||||
logger: logger
|
logger: logger
|
||||||
)
|
)
|
||||||
|
rspec_spec_list = test_selector.rspec_spec_list
|
||||||
|
js_spec_list = test_selector.js_spec_list
|
||||||
|
|
||||||
# Used to generate predictive rspec test pipelines
|
if ci
|
||||||
File.write(ENV['RSPEC_MATCHING_TEST_FILES_PATH'], test_selector.rspec_spec_list.join(" "))
|
# Used to generate predictive rspec test pipelines
|
||||||
# Used by frontend related pipelines/jobs
|
File.write(ENV['RSPEC_MATCHING_TEST_FILES_PATH'], rspec_spec_list.join(" "))
|
||||||
File.write(ENV['RSPEC_MATCHING_JS_FILES_PATH'], test_selector.js_spec_list.join(" "))
|
# Used by frontend related pipelines/jobs
|
||||||
File.write(ENV['RSPEC_CHANGED_FILES_PATH'], changed_files.join("\n"))
|
File.write(ENV['RSPEC_MATCHING_JS_FILES_PATH'], js_spec_list.join(" "))
|
||||||
|
File.write(ENV['RSPEC_CHANGED_FILES_PATH'], changed_files.join("\n"))
|
||||||
|
else
|
||||||
|
puts (rspec_spec_list + js_spec_list).join(' ')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if options[:export_rspec_metrics]
|
if options[:export_rspec_metrics]
|
||||||
require_relative '../lib/tooling/predictive_tests/metrics_exporter'
|
require_relative '../lib/tooling/predictive_tests/metrics_exporter'
|
||||||
|
|
||||||
validate_required_env_variables!(%w[
|
validate_required_env_variables!(%w[
|
||||||
GLCI_CRYSTALBALL_MAPPING_DIR
|
|
||||||
GLCI_ALL_FAILED_RSPEC_TESTS_FILE
|
GLCI_ALL_FAILED_RSPEC_TESTS_FILE
|
||||||
GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR
|
GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR
|
||||||
])
|
])
|
||||||
|
|
||||||
Tooling::PredictiveTests::MetricsExporter.new(
|
Tooling::PredictiveTests::MetricsExporter.new(
|
||||||
rspec_all_failed_tests_file: ENV['GLCI_ALL_FAILED_RSPEC_TESTS_FILE'],
|
rspec_all_failed_tests_file: ENV['GLCI_ALL_FAILED_RSPEC_TESTS_FILE'],
|
||||||
crystalball_mapping_dir: ENV['GLCI_CRYSTALBALL_MAPPING_DIR'],
|
|
||||||
frontend_fixtures_mapping_file: ENV['FRONTEND_FIXTURES_MAPPING_PATH'],
|
|
||||||
output_dir: ENV['GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR']
|
output_dir: ENV['GLCI_PREDICTIVE_TEST_METRICS_OUTPUT_DIR']
|
||||||
).execute
|
).execute
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,7 @@ require "tmpdir"
|
||||||
require "open3"
|
require "open3"
|
||||||
require "logger"
|
require "logger"
|
||||||
require "json"
|
require "json"
|
||||||
|
require "time"
|
||||||
|
|
||||||
require_relative '../test_map_packer'
|
require_relative '../test_map_packer'
|
||||||
|
|
||||||
|
@ -30,19 +31,23 @@ module Tooling
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_rspec_mappings(unpacked_mapping_file, type: :described_class)
|
def fetch_rspec_mappings(unpacked_mapping_file, type: :described_class)
|
||||||
logger.info("Downloading spec mappings of type: #{type}")
|
logger.info("Fetching spec mappings of type: #{type}")
|
||||||
FileUtils.mkdir_p(File.dirname(unpacked_mapping_file))
|
FileUtils.mkdir_p(File.dirname(unpacked_mapping_file))
|
||||||
|
|
||||||
mapping_file = MAPPINGS.fetch(type.to_sym) do
|
mapping_file = MAPPINGS.fetch(type.to_sym) do
|
||||||
logger.warn("No mappings available for type: #{type}, defaulting to described_class")
|
logger.warn("No mappings available for type: #{type}, defaulting to described_class")
|
||||||
MAPPINGS[:described_class]
|
MAPPINGS[:described_class]
|
||||||
end
|
end
|
||||||
|
mapping_file_archive = File.join(Dir.tmpdir, "mapping.gz")
|
||||||
|
url = "#{PAGES_URL}/#{mapping_file}.gz"
|
||||||
|
logger.info("Downloading mapping archive")
|
||||||
|
download(url, mapping_file_archive) unless skip_download?(url, mapping_file_archive)
|
||||||
|
|
||||||
# tmpdir ensures all temporary files get deleted
|
# tmpdir ensures all temporary files get deleted
|
||||||
Dir.mktmpdir("test-mappings") do |dir|
|
Dir.mktmpdir("test-mappings") do |dir|
|
||||||
mapping_file_archive = File.join(dir, "mapping.gz")
|
|
||||||
packed_mapping_file = File.join(dir, "mapping.json")
|
packed_mapping_file = File.join(dir, "mapping.json")
|
||||||
|
|
||||||
download("#{PAGES_URL}/#{mapping_file}.gz", mapping_file_archive)
|
logger.info("Creating and unpacking archive")
|
||||||
extract_archive(mapping_file_archive, packed_mapping_file)
|
extract_archive(mapping_file_archive, packed_mapping_file)
|
||||||
unpack(packed_mapping_file, unpacked_mapping_file)
|
unpack(packed_mapping_file, unpacked_mapping_file)
|
||||||
|
|
||||||
|
@ -54,7 +59,8 @@ module Tooling
|
||||||
logger.info("Downloading frontend fixtures mappings")
|
logger.info("Downloading frontend fixtures mappings")
|
||||||
FileUtils.mkdir_p(File.dirname(file_path))
|
FileUtils.mkdir_p(File.dirname(file_path))
|
||||||
|
|
||||||
download("#{PAGES_URL}/#{MAPPINGS[:frontend_fixtures]}", file_path)
|
url = "#{PAGES_URL}/#{MAPPINGS[:frontend_fixtures]}"
|
||||||
|
download(url, file_path) unless skip_download?(url, file_path)
|
||||||
file_path
|
file_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -62,13 +68,62 @@ module Tooling
|
||||||
|
|
||||||
attr_reader :timeout, :logger
|
attr_reader :timeout, :logger
|
||||||
|
|
||||||
|
def skip_download?(url, file_path)
|
||||||
|
upstream_info = upstream_file_info(url)
|
||||||
|
return false unless upstream_info[:success]
|
||||||
|
|
||||||
|
local_info = local_file_info(file_path)
|
||||||
|
return false unless local_info[:success]
|
||||||
|
|
||||||
|
(upstream_info == local_info).tap do |skip|
|
||||||
|
logger.info("skipping, file exists!") if skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def upstream_file_info(url)
|
||||||
|
response = self.class.head(url, timeout: timeout)
|
||||||
|
|
||||||
|
if response.success?
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
content_length: response.headers['content-length']&.to_i,
|
||||||
|
last_modified: response.headers['last-modified']
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ success: false, error: "HEAD request failed with status #{response.code}" }
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
logger.warn("Failed to fetch upstream file info: #{e.message}")
|
||||||
|
{ success: false, error: e.message }
|
||||||
|
end
|
||||||
|
|
||||||
|
def local_file_info(file_path)
|
||||||
|
return { success: false, error: "File does not exist" } unless File.exist?(file_path)
|
||||||
|
|
||||||
|
begin
|
||||||
|
file_stat = File.stat(file_path)
|
||||||
|
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
content_length: file_stat.size,
|
||||||
|
last_modified: file_stat.mtime.httpdate
|
||||||
|
}
|
||||||
|
rescue StandardError => e
|
||||||
|
logger.warn("Failed to fetch local file info: #{e.message}")
|
||||||
|
{ success: false, error: e.message }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def download(url, destination_path)
|
def download(url, destination_path)
|
||||||
logger.debug("Downloading #{url}...")
|
logger.debug("Downloading #{url}...")
|
||||||
|
FileUtils.rm_f(destination_path) # ensure file does not exist since streaming with append mode is used
|
||||||
response = self.class.get(url, timeout: timeout, stream_body: true) do |fragment|
|
response = self.class.get(url, timeout: timeout, stream_body: true) do |fragment|
|
||||||
File.open(destination_path, 'ab') { |file| file.write(fragment) }
|
File.open(destination_path, 'ab') { |file| file.write(fragment) }
|
||||||
end
|
end
|
||||||
raise "Download failed with status #{response.code}: #{response.message}" unless response.success?
|
raise "Download failed with status #{response.code}: #{response.message}" unless response.success?
|
||||||
|
|
||||||
|
time = Time.parse(response.headers['last-modified'])
|
||||||
|
File.utime(time, time, destination_path) # preserve original last modified timestamp
|
||||||
logger.debug("Download completed: #{destination_path}")
|
logger.debug("Download completed: #{destination_path}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
require_relative "test_selector"
|
require_relative "test_selector"
|
||||||
require_relative "changed_files"
|
require_relative "changed_files"
|
||||||
|
require_relative "mapping_fetcher"
|
||||||
|
|
||||||
require_relative "../helpers/file_handler"
|
require_relative "../helpers/file_handler"
|
||||||
require_relative "../events/track_pipeline_events"
|
require_relative "../events/track_pipeline_events"
|
||||||
|
|
||||||
require "logger"
|
require "logger"
|
||||||
|
require "tmpdir"
|
||||||
|
|
||||||
module Tooling
|
module Tooling
|
||||||
module PredictiveTests
|
module PredictiveTests
|
||||||
|
@ -21,15 +23,8 @@ module Tooling
|
||||||
STRATEGIES = [:coverage, :described_class].freeze
|
STRATEGIES = [:coverage, :described_class].freeze
|
||||||
TEST_TYPE = "backend"
|
TEST_TYPE = "backend"
|
||||||
|
|
||||||
def initialize(
|
def initialize(rspec_all_failed_tests_file:, output_dir: nil)
|
||||||
rspec_all_failed_tests_file:,
|
|
||||||
crystalball_mapping_dir:,
|
|
||||||
frontend_fixtures_mapping_file:,
|
|
||||||
output_dir: nil
|
|
||||||
)
|
|
||||||
@rspec_all_failed_tests_file = rspec_all_failed_tests_file
|
@rspec_all_failed_tests_file = rspec_all_failed_tests_file
|
||||||
@crystalball_mapping_dir = crystalball_mapping_dir
|
|
||||||
@frontend_fixtures_mapping_file = frontend_fixtures_mapping_file
|
|
||||||
@output_dir = output_dir
|
@output_dir = output_dir
|
||||||
@logger = Logger.new($stdout, progname: "rspec predictive testing")
|
@logger = Logger.new($stdout, progname: "rspec predictive testing")
|
||||||
end
|
end
|
||||||
|
@ -49,7 +44,7 @@ module Tooling
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :rspec_all_failed_tests_file, :crystalball_mapping_dir, :frontend_fixtures_mapping_file, :logger
|
attr_reader :rspec_all_failed_tests_file, :logger
|
||||||
|
|
||||||
# Project root folder
|
# Project root folder
|
||||||
#
|
#
|
||||||
|
@ -84,12 +79,28 @@ module Tooling
|
||||||
@changed_files ||= ChangedFiles.fetch(frontend_fixtures_file: frontend_fixtures_mapping_file)
|
@changed_files ||= ChangedFiles.fetch(frontend_fixtures_file: frontend_fixtures_mapping_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Mapping file fetcher
|
||||||
|
#
|
||||||
|
# @return [MappingFetcher]
|
||||||
|
def mapping_fetcher
|
||||||
|
@mapping_fetcher ||= Tooling::PredictiveTests::MappingFetcher.new(logger: logger)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Frontend fixtures mapping file
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def frontend_fixtures_mapping_file
|
||||||
|
@frontend_fixtures_mapping_file ||= File.join(Dir.tmpdir, "frontend_fixtures_mapping.json").tap do |file|
|
||||||
|
mapping_fetcher.fetch_frontend_fixtures_mappings(file)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Mapping file path for specific strategy
|
# Mapping file path for specific strategy
|
||||||
#
|
#
|
||||||
# @param strategy [Symbol]
|
# @param strategy [Symbol]
|
||||||
# @return [String]
|
# @return [String]
|
||||||
def mapping_file_path(strategy)
|
def mapping_file_path(strategy)
|
||||||
File.join(crystalball_mapping_dir, strategy.to_s, "mapping.json")
|
File.join(Dir.tmpdir, strategy.to_s, "mapping.json")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Strategy specific matching rspec tests file path
|
# Strategy specific matching rspec tests file path
|
||||||
|
@ -128,6 +139,8 @@ module Tooling
|
||||||
def generate_and_record_metrics(strategy)
|
def generate_and_record_metrics(strategy)
|
||||||
logger.info("Generating metrics for mapping strategy '#{strategy}' ...")
|
logger.info("Generating metrics for mapping strategy '#{strategy}' ...")
|
||||||
|
|
||||||
|
# fetch crystalball mappings for specific strategy
|
||||||
|
fetch_crystalball_mappings!(strategy)
|
||||||
# based on the predictive test selection strategy
|
# based on the predictive test selection strategy
|
||||||
predicted_test_files = test_selector(strategy).rspec_spec_list
|
predicted_test_files = test_selector(strategy).rspec_spec_list
|
||||||
# actual failed tests from tier-3 run
|
# actual failed tests from tier-3 run
|
||||||
|
@ -149,6 +162,14 @@ module Tooling
|
||||||
logger.info("Metrics generation completed for strategy '#{strategy}'")
|
logger.info("Metrics generation completed for strategy '#{strategy}'")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fetch crystalball mappings
|
||||||
|
#
|
||||||
|
# @param strategy [Symbol]
|
||||||
|
# @return [void]
|
||||||
|
def fetch_crystalball_mappings!(strategy)
|
||||||
|
mapping_fetcher.fetch_rspec_mappings(mapping_file_path(strategy), type: strategy)
|
||||||
|
end
|
||||||
|
|
||||||
# Create metrics hash with all calculated metrics based on crystalball mapping and selected test strategy
|
# Create metrics hash with all calculated metrics based on crystalball mapping and selected test strategy
|
||||||
#
|
#
|
||||||
# @param changed_files [Array]
|
# @param changed_files [Array]
|
||||||
|
|
Loading…
Reference in New Issue