diff --git a/.dive-ci b/.dive-ci new file mode 100644 index 0000000000000..29131b7bb43e7 --- /dev/null +++ b/.dive-ci @@ -0,0 +1,13 @@ +rules: + # If the efficiency is measured below X%, mark as failed. + # Expressed as a ratio between 0-1. + lowestEfficiency: 0.80 + + # If the amount of wasted space is at least X or larger than X, mark as failed. + # Expressed in B, KB, MB, and GB. + highestWastedBytes: 1GB + + # If the amount of wasted space makes up for X% or more of the image, mark as failed. + # Note: the base image layer is NOT included in the total image size. + # Expressed as a ratio between 0-1; fails if the threshold is met or crossed. + highestUserWastedPercent: 0.20 diff --git a/.dockerignore b/.dockerignore index a3501acca3fec..ad73ed04a7800 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,7 @@ build/ !build/vendor/woff-code-latest.zip !build/gergich/ -!build/new-jenkins/package-translations +!build/new-jenkins/ docker-compose/ !docker-compose/config/* docker-compose.local.*.yml diff --git a/.gitignore b/.gitignore index b6b48153e4500..3809f94db23c9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,10 +25,10 @@ /db/schema.rb /db/*.sqlite* /db/*sql +docker-compose.override.yml /exports/ Gemfile.lock Gemfile.lock.next -knapsack_rspec_report.json /log/ mkmf.log /node_modules @@ -81,3 +81,6 @@ docker-compose.local.* .nyc_output coverage-jest coverage-karma + +# generated graphql schema +schema.graphql diff --git a/.rubocop.yml b/.rubocop.yml index 7d5d79585bd4d..055c2e7279f81 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -104,8 +104,6 @@ Layout/DotPosition: EnforcedStyle: trailing Layout/AlignHash: Enabled: false -Layout/AlignParameters: - Enabled: false Style/Lambda: Enabled: false Style/WhileUntilModifier: diff --git a/Dockerfile b/Dockerfile index af8fc929a103c..f8370d9d182ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,19 +2,18 @@ # To update this file please edit the relevant template and run the generation # task `build/dockerfile_writer.rb` -# See doc/docker/README.md or https://github.com/instructure/canvas-lms/tree/master/doc/docker -FROM instructure/ruby-passenger:2.4-xenial +# For documentation, please check doc/docker/README.md in +# this local repo which is also published at: +# https://github.com/instructure/canvas-lms/tree/master/doc/docker +ARG RUBY_PASSENGER=2.4-xenial +FROM instructure/ruby-passenger:$RUBY_PASSENGER +ARG POSTGRES_VERSION=9.5 ENV APP_HOME /usr/src/app/ ENV RAILS_ENV "production" ENV NGINX_MAX_UPLOAD_SIZE 10g ENV YARN_VERSION 1.19.1-1 -# Work around github.com/zertosh/v8-compile-cache/issues/2 -# This can be removed once yarn pushes a release including the fixed version -# of v8-compile-cache. -ENV DISABLE_V8_COMPILE_CACHE 1 - USER root WORKDIR /root RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \ @@ -30,11 +29,11 @@ RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \ libxmlsec1-dev \ python-lxml \ libicu-dev \ - postgresql-client-9.5 \ + parallel \ + postgresql-client-$POSTGRES_VERSION \ unzip \ pbzip2 \ fontforge \ - && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /home/docker/.gem/ruby/$RUBY_MAJOR.0 @@ -44,7 +43,7 @@ RUN if [ -e /var/lib/gems/$RUBY_MAJOR.0/gems/bundler-* ]; then BUNDLER_INSTALL=" && find $GEM_HOME ! -user docker | xargs chown docker:docker # We will need sfnt2woff in order to build fonts -COPY build/vendor/woff-code-latest.zip ./ +COPY --chown=docker:docker build/vendor/woff-code-latest.zip ./ RUN unzip woff-code-latest.zip -d woff \ && cd woff \ && make \ @@ -54,23 +53,23 @@ RUN unzip woff-code-latest.zip -d woff \ WORKDIR $APP_HOME -COPY Gemfile ${APP_HOME} -COPY Gemfile.d ${APP_HOME}Gemfile.d -COPY config ${APP_HOME}config -COPY --chown=docker:docker gems ${APP_HOME}gems -COPY --chown=docker:docker packages ${APP_HOME}packages -COPY script ${APP_HOME}script -COPY package.json ${APP_HOME} -COPY yarn.lock ${APP_HOME} -COPY babel.config.js ${APP_HOME} +COPY --chown=docker:docker Gemfile ${APP_HOME} +COPY --chown=docker:docker Gemfile.d ${APP_HOME}Gemfile.d +COPY --chown=docker:docker config ${APP_HOME}config +COPY --chown=docker:docker gems ${APP_HOME}gems +COPY --chown=docker:docker packages ${APP_HOME}packages +COPY --chown=docker:docker script ${APP_HOME}script +COPY --chown=docker:docker package.json ${APP_HOME} +COPY --chown=docker:docker yarn.lock ${APP_HOME} +COPY --chown=docker:docker babel.config.js ${APP_HOME} +COPY --chown=docker:docker build/new-jenkins ${APP_HOME}build/new-jenkins -# Install deps as docker to avoid sadness w/ npm lifecycle hooks USER docker +# if yarn hits a snag try one more time with concurrency set to 1 +# https://github.com/yarnpkg/yarn/issues/2629 RUN bundle install --jobs 8 \ - && yarn install --pure-lockfile -USER root - -COPY . $APP_HOME + && (yarn install --pure-lockfile || yarn install --pure-lockfile --network-concurrency 1) +COPY --chown=docker:docker . $APP_HOME RUN mkdir -p .yardoc \ app/stylesheets/brandable_css_brands \ app/views/info \ @@ -94,11 +93,9 @@ RUN mkdir -p .yardoc \ tmp \ /home/docker/.bundler/ \ /home/docker/.cache/yarn \ - /home/docker/.gem/ \ - && find ${APP_HOME} /home/docker ! -user docker -print0 | xargs -0 chown -h docker:docker + /home/docker/.gem/ -USER docker -# update Gemfile.lock in cases where a lock file was pulled in during the `COPY . $APP_HOME` step +# update Gemfile.lock in cases where a lock file was pulled in during the `COPY --chown=docker:docker . $APP_HOME` step RUN bundle lock --local --conservative # TODO: switch to canvas:compile_assets_dev once we stop using this Dockerfile in production/e2e diff --git a/Dockerfile-production b/Dockerfile-production index 396430dcaf79f..d588faf7347b4 100644 --- a/Dockerfile-production +++ b/Dockerfile-production @@ -2,7 +2,9 @@ # To update this file please edit the relevant template and run the generation # task `build/dockerfile_writer.rb` -# See doc/docker/README.md or https://github.com/instructure/canvas-lms/tree/master/doc/docker +# For documentation, please check doc/docker/README.md in +# this local repo which is also published at: +# https://github.com/instructure/canvas-lms/tree/master/doc/docker FROM instructure/ruby-passenger:2.4-xenial ENV APP_HOME /usr/src/app/ @@ -10,11 +12,6 @@ ENV RAILS_ENV "production" ENV NGINX_MAX_UPLOAD_SIZE 10g ENV YARN_VERSION 1.19.1-1 -# Work around github.com/zertosh/v8-compile-cache/issues/2 -# This can be removed once yarn pushes a release including the fixed version -# of v8-compile-cache. -ENV DISABLE_V8_COMPILE_CACHE 1 - USER root WORKDIR /root RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \ @@ -28,7 +25,6 @@ RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \ libxmlsec1-dev \ python-lxml \ libicu-dev \ - && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /home/docker/.gem/ruby/$RUBY_MAJOR.0 @@ -40,7 +36,7 @@ RUN if [ -e /var/lib/gems/$RUBY_MAJOR.0/gems/bundler-* ]; then BUNDLER_INSTALL=" WORKDIR $APP_HOME -COPY . $APP_HOME +COPY --chown=docker:docker . $APP_HOME # optimizing for size here ... get all the dev dependencies so we can # compile assets, then throw away everything we don't need diff --git a/Gemfile.d/_before.rb b/Gemfile.d/_before.rb index 511ff21f6d5eb..9290eeeb41fae 100644 --- a/Gemfile.d/_before.rb +++ b/Gemfile.d/_before.rb @@ -16,7 +16,7 @@ # with this program. If not, see . # -gem 'bundler', '>= 1.13.3', '<= 2.0.2' +gem 'bundler', '>= 1.13.3', '<= 2.1.4' if Gem::Version.new(Bundler::VERSION) >= Gem::Version.new('1.14.0') && Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.6.9') @@ -24,10 +24,10 @@ end # NOTE: this has to use 1.8.7 hash syntax to not raise a parser exception on 1.8.7 -if RUBY_VERSION >= "2.4.0" && RUBY_VERSION < "2.6" +if RUBY_VERSION >= "2.4.0" && RUBY_VERSION < "2.7" ruby RUBY_VERSION, :engine => 'ruby', :engine_version => RUBY_VERSION -elsif RUBY_VERSION >= "2.6.0" && RUBY_VERSION < "2.7" - $stderr.puts "Ruby 2.6 support is untested" unless ENV['SUPPRESS_RUBY_WARNING'] +elsif RUBY_VERSION >= "2.7.0" && RUBY_VERSION < "2.8" + $stderr.puts "Ruby 2.7+ support is untested" unless ENV['SUPPRESS_RUBY_WARNING'] ruby RUBY_VERSION, :engine => 'ruby', :engine_version => RUBY_VERSION else ruby '2.4.0', :engine => 'ruby', :engine_version => '2.4.0' diff --git a/Gemfile.d/app.rb b/Gemfile.d/app.rb index d34ff17c3feed..f6f5e7713aa9b 100644 --- a/Gemfile.d/app.rb +++ b/Gemfile.d/app.rb @@ -15,166 +15,159 @@ # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . +# Note: Indented gems are meant to indicate transient dependencies of parent gems + if CANVAS_RAILS5_2 gem 'rails', '5.2.3' gem 'loofah', '2.3.0' gem 'sprockets', '3.7.2' # 4.0 requires ruby 2.5 else - gem 'rails', '6.0.0' + gem 'rails', '6.0.2.1' end -gem 'rack', '2.0.7' - -gem 'oauth2', '1.4.2', require: false - -gem 'rails-observers', '0.1.5' - -gem 'builder', '3.2.3' -gem 'tzinfo', '1.2.5' - -gem 'encrypted_cookie_store-instructure', '1.2.9', require: 'encrypted_cookie_store' +gem 'academic_benchmarks', '0.0.11', require: false +gem 'active_model-better_errors', '1.6.7', require: 'active_model/better_errors' gem 'active_model_serializers', '0.9.0alpha1', github: 'rails-api/active_model_serializers', ref: '61882e1e4127facfe92e49057aec71edbe981829' -gem 'authlogic', '5.0.4' - gem 'scrypt', '3.0.6' -gem 'active_model-better_errors', '1.6.7', require: 'active_model/better_errors' -gem 'switchman', '1.14.7' - gem 'open4', '1.3.4', require: false -gem 'folio-pagination', '0.0.12', require: 'folio/rails' - # for folio, see the folio README - gem 'will_paginate', '3.1.7', require: false - gem 'addressable', '2.7.0', require: false -gem "after_transaction_commit", '2.0.0' -gem "aws-sdk-dynamodb", "1.36.0" -gem "aws-sdk-kinesis", '1.19.0', require: false -gem "aws-sdk-s3", '1.48.0', require: false -gem "aws-sdk-sns", '1.19.0', require: false -gem "aws-sdk-sqs", '1.22.0', require: false -gem "aws-sdk-core", "3.68.1", require: false - gem "aws-partitions", "1.238.0", require: false # pinning transient dependency -gem "aws-sdk-kms", "1.24.0", require: false -gem "aws-sigv4", "1.1.0", require: false - +gem 'after_transaction_commit', '2.0.0' +gem 'authlogic', '5.0.4' + gem 'scrypt', '3.0.7' +gem 'aws-sdk-core', '3.90.1', require: false + gem 'aws-partitions', '1.279.0', require: false +gem 'aws-sdk-dynamodb', '1.44.0' +gem 'aws-sdk-kinesis', '1.20.0', require: false +gem 'aws-sdk-s3', '1.60.2', require: false +gem 'aws-sdk-sns', '1.21.0', require: false +gem 'aws-sdk-sqs', '1.23.1', require: false +gem 'aws-sdk-kms', '1.29.0', require: false +gem 'aws-sigv4', '1.1.1', require: false gem 'barby', '0.6.8', require: false - gem 'rqrcode', '1.1.1', require: false + gem 'rqrcode', '1.1.2', require: false gem 'chunky_png', '1.3.11', require: false gem 'bcrypt', '3.1.13' gem 'brotli', '0.2.3', require: false +gem 'browser', '4.0.0', require: false +gem 'builder', '3.2.4' gem 'canvas_connect', '0.3.11' gem 'adobe_connect', '1.0.8', require: false gem 'canvas_webex', '0.17' -gem 'inst-jobs', '0.15.14' - gem 'fugit', '1.3.3', require: false - gem 'et-orbi', '1.2.2', require: false -gem 'switchman-inst-jobs', '1.3.6' -gem 'inst-jobs-autoscaling', '1.0.5' - gem 'aws-sdk-autoscaling', '1.28.0', require: false -gem 'ffi', '1.11.1', require: false +gem 'crocodoc-ruby', '0.0.1', require: false +gem 'ddtrace', '0.33.1', require: false +gem 'encrypted_cookie_store-instructure', '1.2.9', require: 'encrypted_cookie_store' +gem 'folio-pagination', '0.0.12', require: 'folio/rails' +gem 'ffi', '1.12.2', require: false +gem 'gepub', '1.0.11' +gem 'graphql', '1.9.17' +gem 'graphql-batch', '0.4.2' gem 'hashery', '2.1.2', require: false -gem 'highline', '2.0.2', require: false -gem 'httparty', '0.17.1' -gem 'i18n', '1.0.0' -gem 'i18nliner', '0.1.1' +gem 'highline', '2.0.3', require: false +gem 'httparty', '0.18.0' +gem 'i18n', '1.8.2' +gem 'i18nliner', '0.1.2' gem 'ruby2ruby', '2.4.4', require: false - gem 'ruby_parser', '3.14.0', require: false -gem 'icalendar', '2.5.3', require: false + gem 'ruby_parser', '3.14.2', require: false +gem 'icalendar', '2.6.1', require: false +gem 'imperium', '0.5.2', require: false gem 'ims-lti', '2.3.0', require: 'ims' -gem 'json_schemer', '0.2.7' -gem 'simple_oauth', '0.3.1', require: false -gem 'json', '2.2.0' +gem 'inst_statsd', '2.1.6' + gem 'statsd-ruby', '1.4.0', require: false + gem 'aroi', '0.0.7', require: false + gem 'dogstatsd-ruby', '4.7.0' +gem 'inst-jobs', '0.15.16' + gem 'fugit', '1.3.3', require: false + gem 'et-orbi', '1.2.2', require: false +gem 'inst-jobs-autoscaling', '1.0.5' + gem 'aws-sdk-autoscaling', '1.32.0', require: false +gem 'inst-jobs-statsd', '1.3.2' +gem 'json', '2.3.0' +gem 'json_schemer', '0.2.10' +gem 'json-jwt', '1.11.0', require: false gem 'link_header', '0.0.8' -gem 'oj', '3.3.9' -gem 'json-jwt', '1.10.2', require: false -gem 'twilio-ruby', '5.27.1', require: false - gem 'mail', '2.7.1', require: false gem 'mini_mime', '1.0.2', require: false gem 'marginalia', '1.8.0', require: false -gem 'mime-types', '3.3.0' -gem 'mini_magick', '4.9.5' -gem 'multi_json', '1.13.1' -gem 'nokogiri', '1.10.4', require: false +gem 'mime-types', '3.3.1' +gem 'mini_magick', '4.10.1' +gem 'multi_json', '1.14.1' +gem 'net-ldap', '0.16.2', require: false +gem 'nokogiri', '1.10.9', require: false gem 'oauth', '0.5.4', require: false -gem 'parallel', '1.18.0', require: false +gem 'oauth2', '1.4.4', require: false +gem 'oj', '3.10.5' +gem 'parallel', '1.19.1', require: false gem 'ruby-progressbar', '1.10.1', require: false # used to show progress of S3Uploader -gem 'retriable', '1.4.1' -gem 'rake', '12.3.1' +gem 'prawn-rails', '1.3.0' +gem 'rack', '2.2.2' +gem 'rack-test', '1.1.0' +gem 'rake', '13.0.1' +gem 'rails-observers', '0.1.5' gem 'ratom-nokogiri', '0.10.8', require: false -gem 'rdiscount', '1.6.8', require: false +gem 'rdiscount', '2.2.0.1', require: false +gem 'redcarpet', '3.5.0', require: false +gem 'retriable', '1.4.1' gem 'ritex', '1.0.1', require: false - gem 'rotp', '5.1.0', require: false -gem 'net-ldap', '0.16.1', require: false gem 'ruby-duration', '3.2.3', require: false -gem 'saml2', '3.0.8' - gem 'nokogiri-xmlsec-instructure', '0.9.6', require: false gem 'rubycas-client', '2.3.9', require: false -gem 'rubyzip', '1.2.2', require: 'zip' +gem 'rubyzip', '2.2.0', require: 'zip' gem 'safe_yaml', '1.0.5', require: false +gem 'saml2', '3.0.8' + gem 'nokogiri-xmlsec-instructure', '0.9.6', require: false gem 'sanitize', '2.1.1', require: false +gem 'sentry-raven', '2.13.0', require: false gem 'shackles', '1.4.2' - -gem 'browser', '2.6.1', require: false - -gem 'crocodoc-ruby', '0.0.1', require: false -gem 'sentry-raven', '2.11.3', require: false -gem 'inst_statsd', '2.1.6' - gem 'statsd-ruby', '1.4.0', require: false - gem 'aroi', '0.0.7', require: false - gem 'dogstatsd-ruby', '4.5.0' -gem 'inst-jobs-statsd', '1.2.3' -gem 'gepub', '1.0.4' -gem 'imperium', '0.5.1', require: false -gem 'academic_benchmarks', '0.0.11', require: false - -gem 'graphql', '1.9.11' -gem 'graphql-batch', '0.4.1' - -gem 'prawn-rails', '1.3.0' - -gem 'redcarpet', '3.5.0', require: false - -gem 'activesupport-suspend_callbacks', path: 'gems/activesupport-suspend_callbacks' -gem 'acts_as_list', path: 'gems/acts_as_list' -gem 'adheres_to_policy', path: 'gems/adheres_to_policy' -gem 'attachment_fu', path: 'gems/attachment_fu' -gem 'autoextend', path: 'gems' -gem 'bookmarked_collection', path: 'gems/bookmarked_collection' -gem 'broadcast_policy', path: "gems/broadcast_policy" -gem 'canvas_breach_mitigation', path: 'gems/canvas_breach_mitigation' -gem 'canvas_color', path: 'gems/canvas_color' -gem 'canvas_crummy', path: 'gems/canvas_crummy' -gem "canvas_dynamodb", path: "gems/canvas_dynamodb" -gem 'canvas_ext', path: 'gems/canvas_ext' -gem 'canvas_http', path: 'gems/canvas_http' -gem 'canvas_kaltura', path: 'gems/canvas_kaltura' -gem 'canvas_panda_pub', path: 'gems/canvas_panda_pub' -gem 'canvas_partman', path: 'gems/canvas_partman' -gem 'event_stream', path: 'gems/event_stream' -gem 'canvas_mimetype_fu', path: 'gems/canvas_mimetype_fu' -gem 'canvas_quiz_statistics', path: 'gems/canvas_quiz_statistics' -gem 'canvas_sanitize', path: 'gems/canvas_sanitize' -gem 'canvas_slug', path: 'gems/canvas_slug' -gem 'canvas_sort', path: 'gems/canvas_sort' -gem 'canvas_stringex', path: 'gems/canvas_stringex' -gem 'canvas_text_helper', path: 'gems/canvas_text_helper' -gem 'canvas_time', path: 'gems/canvas_time' -gem 'canvas_unzip', path: 'gems/canvas_unzip' -gem 'csv_diff', path: 'gems/csv_diff' -gem 'google_drive', path: 'gems/google_drive' -gem 'html_text_helper', path: 'gems/html_text_helper' -gem 'incoming_mail_processor', path: 'gems/incoming_mail_processor' -gem 'json_token', path: 'gems/json_token' -gem 'linked_in', path: 'gems/linked_in' -gem 'live_events', path: 'gems/live_events' -gem 'diigo', path: 'gems/diigo' -gem 'lti-advantage', path: 'gems/lti-advantage' -gem 'lti_outbound', path: 'gems/lti_outbound' -gem 'multipart', path: 'gems/multipart' -gem 'paginated_collection', path: 'gems/paginated_collection' -gem 'stringify_ids', path: 'gems/stringify_ids' -gem 'twitter', path: 'gems/twitter' +gem 'simple_oauth', '0.3.1', require: false +gem 'switchman', '1.14.9' + gem 'open4', '1.3.4', require: false +gem 'switchman-inst-jobs', '1.3.7' +gem 'twilio-ruby', '5.31.1', require: false +gem 'tzinfo', '1.2.5' +gem 'vault', '0.13.0', require: false gem 'vericite_api', '1.5.3' -gem 'utf8_cleaner', path: 'gems/utf8_cleaner' -gem 'workflow', path: 'gems/workflow' +gem 'will_paginate', '3.3.0', require: false # required for folio-pagination + +path 'gems' do + gem 'activesupport-suspend_callbacks' + gem 'acts_as_list' + gem 'adheres_to_policy' + gem 'attachment_fu' + gem 'autoextend' + gem 'bookmarked_collection' + gem 'broadcast_policy' + gem 'canvas_breach_mitigation' + gem 'canvas_color' + gem 'canvas_crummy' + gem 'canvas_dynamodb' + gem 'canvas_ext' + gem 'canvas_http' + gem 'canvas_kaltura' + gem 'canvas_panda_pub' + gem 'canvas_partman' + gem 'canvas_mimetype_fu' + gem 'canvas_quiz_statistics' + gem 'canvas_sanitize' + gem 'canvas_slug' + gem 'canvas_sort' + gem 'canvas_stringex' + gem 'canvas_text_helper' + gem 'canvas_time' + gem 'canvas_unzip' + gem 'csv_diff' + gem 'diigo' + gem 'event_stream' + gem 'google_drive' + gem 'html_text_helper' + gem 'incoming_mail_processor' + gem 'json_token' + gem 'linked_in' + gem 'live_events' + gem 'lti-advantage' + gem 'lti_outbound' + gem 'multipart' + gem 'paginated_collection' + gem 'stringify_ids' + gem 'twitter' + gem 'utf8_cleaner' + gem 'workflow' +end diff --git a/Gemfile.d/assets.rb b/Gemfile.d/assets.rb index 521fb236a34ce..79331f2c422ca 100644 --- a/Gemfile.d/assets.rb +++ b/Gemfile.d/assets.rb @@ -19,9 +19,9 @@ gem 'dress_code', '1.2.0' gem 'colored', '1.2', require: false gem 'colorize', '0.8.1', require: false - gem 'mustache', '1.1.0', require: false + gem 'mustache', '1.1.1', require: false gem 'pygments.rb', '1.2.1', require: false gem 'bluecloth', '2.2.0' # for generating api docs - gem 'yard', '0.9.20' + gem 'yard', '0.9.24' gem 'yard-appendix', '0.1.8' end diff --git a/Gemfile.d/development.rb b/Gemfile.d/development.rb index af762d06c2424..e061544f8f104 100644 --- a/Gemfile.d/development.rb +++ b/Gemfile.d/development.rb @@ -24,6 +24,6 @@ # The ruby debug gems conflict with the IDE-based debugger gem. # Set this option in your dev environment to disable. unless ENV['DISABLE_RUBY_DEBUGGING'] - gem 'byebug', '11.0.1', platform: :mri + gem 'byebug', '11.1.1', platform: :mri end end diff --git a/Gemfile.d/icu.rb b/Gemfile.d/icu.rb index b090cf3274978..94175d761875f 100644 --- a/Gemfile.d/icu.rb +++ b/Gemfile.d/icu.rb @@ -16,5 +16,5 @@ # with this program. If not, see . group :icu do - gem 'ffi-icu', '0.1.10' + gem 'ffi-icu', '0.2.0' end diff --git a/Gemfile.d/postgres.rb b/Gemfile.d/postgres.rb index fcc54957e2a21..39e5334c500ca 100644 --- a/Gemfile.d/postgres.rb +++ b/Gemfile.d/postgres.rb @@ -16,5 +16,5 @@ # with this program. If not, see . group :postgres do - gem 'pg', '1.1.4' + gem 'pg', '1.2.2' end diff --git a/Gemfile.d/sqlite.rb b/Gemfile.d/sqlite.rb index 72e535ffd0dba..ef0eee64ea368 100644 --- a/Gemfile.d/sqlite.rb +++ b/Gemfile.d/sqlite.rb @@ -16,5 +16,5 @@ # with this program. If not, see . group :sqlite do - gem 'sqlite3', '1.4.1' + gem 'sqlite3', '1.4.2' end diff --git a/Gemfile.d/test.rb b/Gemfile.d/test.rb index fd29a6076afad..bf562b2543bf2 100644 --- a/Gemfile.d/test.rb +++ b/Gemfile.d/test.rb @@ -19,23 +19,23 @@ gem 'rails-dom-testing', '2.0.3' gem 'rails-controller-testing', '1.0.4' - gem 'gergich', '1.1.0', require: false + gem 'gergich', '1.2.0', require: false gem 'dotenv', '2.7.5', require: false gem 'testingbot', require: false gem 'brakeman', require: false gem 'simplecov', '0.15.1', require: false gem 'docile', '1.1.5', require: false gem 'simplecov-rcov', '0.2.3', require: false - gem 'puma', '4.2.1' + gem 'puma', '4.3.3' gem 'rspec', '3.9.0' gem 'rspec_around_all', '0.2.0' gem 'rspec-rails', '3.9.0' gem 'rspec-collection_matchers', '1.2.0' - gem 'rspec-support', '3.9.0' + gem 'rspec-support', '3.9.2' gem 'rspec-expectations', '3.9.0' - gem 'rspec-mocks', '3.9.0' - gem 'shoulda-matchers', '4.1.2' + gem 'rspec-mocks', '3.9.1' + gem 'shoulda-matchers', '4.3.0' gem 'rubocop-canvas', require: false, path: 'gems/rubocop-canvas' gem 'rubocop', '0.52.1', require: false @@ -43,27 +43,28 @@ gem 'rubocop-rspec', '1.22.2', require: false gem 'once-ler', '0.1.4' - gem 'sauce_whisk', '0.1.0' + gem 'sauce_whisk', '0.2.2' - # Keep this gem synced with docker-compose/seleniumff/Dockerfile - gem 'selenium-webdriver', '3.142.3' - gem 'childprocess', '1.0.1', require: false - gem 'chromedriver-helper', '2.1.0', require: false + gem 'selenium-webdriver', '3.142.7' + gem 'childprocess', '3.0.0', require: false + gem 'webdrivers', '4.2.0', require: false gem 'selinimum', '0.0.1', require: false, path: 'gems/selinimum' gem 'test-queue', github: 'instructure/test-queue', ref: 'd35166408df3a5396cd809e85dcba175136a69ba', require: false gem 'testrailtagging', '0.3.8.7', require: false - gem 'webmock', '3.7.6', require: false + gem 'webmock', '3.8.2', require: false gem 'crack', '0.4.3', require: false gem 'timecop', '0.9.1' gem 'jira_ref_parser', '1.0.1' gem 'headless', '2.3.1', require: false gem 'escape_code', '0.2', require: false gem 'luminosity_contrast', '0.2.1' - gem 'pact', '1.24.0' + gem 'pact', '1.49.0' + gem 'pact-mock_service', '3.5.0', require: false gem 'pact-messages', '0.2.0' - gem 'pact_broker-client' + gem 'pact_broker-client', '1.25.0' gem 'database_cleaner', '~> 1.5', '>= 1.5.3' - gem 'knapsack', '1.18.0' + gem 'parallel_tests' + gem 'flakey_spec_catcher', require: false end diff --git a/Jenkinsfile b/Jenkinsfile index a22029ec902ac..4a993c180480d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -17,341 +17,377 @@ * You should have received a copy of the GNU Affero General Public License along * with this program. If not, see . */ +def buildParameters = [ + string(name: 'GERRIT_REFSPEC', value: "${env.GERRIT_REFSPEC}"), + string(name: 'GERRIT_EVENT_TYPE', value: "${env.GERRIT_EVENT_TYPE}"), + string(name: 'GERRIT_PROJECT', value: "${env.GERRIT_PROJECT}"), + string(name: 'GERRIT_BRANCH', value: "${env.GERRIT_BRANCH}"), + string(name: 'GERRIT_CHANGE_NUMBER', value: "${env.GERRIT_CHANGE_NUMBER}"), + string(name: 'GERRIT_PATCHSET_NUMBER', value: "${env.GERRIT_PATCHSET_NUMBER}"), + string(name: 'GERRIT_EVENT_ACCOUNT_NAME', value: "${env.GERRIT_EVENT_ACCOUNT_NAME}"), + string(name: 'GERRIT_EVENT_ACCOUNT_EMAIL', value: "${env.GERRIT_EVENT_ACCOUNT_EMAIL}"), + string(name: 'GERRIT_CHANGE_COMMIT_MESSAGE', value: "${env.GERRIT_CHANGE_COMMIT_MESSAGE}"), + string(name: 'GERRIT_HOST', value: "${env.GERRIT_HOST}"), + string(name: 'GERGICH_PUBLISH', value: "${env.GERGICH_PUBLISH}"), + string(name: 'MASTER_BOUNCER_RUN', value: "${env.MASTER_BOUNCER_RUN}") +] -def withGerritCredentials = { Closure command -> - withCredentials([ - sshUserPrivateKey(credentialsId: '44aa91d6-ab24-498a-b2b4-911bcb17cc35', keyFileVariable: 'SSH_KEY_PATH', usernameVariable: 'SSH_USERNAME') - ]) { command() } -} - -def fetchFromGerrit = { String repo, String path, String customRepoDestination = null, String sourcePath = null, String sourceRef = null -> - withGerritCredentials({ -> - println "Fetching ${repo} plugin" - sh """ - mkdir -p ${path}/${customRepoDestination ?: repo} - GIT_SSH_COMMAND='ssh -i \"$SSH_KEY_PATH\" -l \"$SSH_USERNAME\"' \ - git archive --remote=ssh://$GERRIT_URL/${repo} ${sourceRef == null ? 'master' : sourceRef} ${sourcePath == null ? '' : sourcePath} | tar -x -C ${path}/${customRepoDestination ?: repo} - """ - }) +def getImageTagVersion() { + def flags = load('build/new-jenkins/groovy/commit-flags.groovy') + flags.getImageTagVersion() } -def fullSuccessName(name) { - return "_successes/${env.GERRIT_CHANGE_NUMBER}-${env.GERRIT_PATCHSET_NUMBER}-${name}-success" +def runDatadogMetric(name, body) { + def dd = load('build/new-jenkins/groovy/datadog.groovy') + dd.runDataDogForMetric(name,body) } -def hasSuccess(name) { - copyArtifacts(filter: "_successes/*", - optional: true, - projectName: '/${JOB_NAME}', - parameters: "GERRIT_CHANGE_NUMBER=${env.GERRIT_CHANGE_NUMBER},GERRIT_PATCHSET_NUMBER=${GERRIT_PATCHSET_NUMBER}", - selector: lastCompleted()) - if (fileExists("_successes")) { - archiveArtifacts(artifacts: "_successes/*", - projectName: '/${JOB_NAME}') +def skipIfPreviouslySuccessful(name, block) { + runDatadogMetric(name) { + def successes = load('build/new-jenkins/groovy/successes.groovy') + successes.skipIfPreviouslySuccessful(name, true, block) } - return fileExists(fullSuccessName(name)) } -def saveSuccess(name) { - def success_name = fullSuccessName(name) - sh "mkdir -p _successes" - sh "echo 'success' >> ${success_name}" - archiveArtifacts(artifacts: "_successes/*", - projectName: '/${JOB_NAME}') - echo "===> success saved /${env.JOB_NAME}: ${success_name}" +// ignore builds where the current patchset tag doesn't match the +// mainline publishable tag. i.e. ignore ruby-passenger-2.6/pg-12 +// upgrade builds +def isPatchsetPublishable() { + env.PATCHSET_TAG == env.PUBLISHABLE_TAG } -// runs the body if it has not previously succeeded. -// if you don't want the success of the body to mark the -// given name as successful, pass in save = false. -def skipIfPreviouslySuccessful(name, save = true, body) { - if (hasSuccess(name)) { - echo "===> block already successful, skipping: ${fullSuccessName(name)}" - } else { - echo "===> running block: ${fullSuccessName(name)}" - body.call() - if (save) saveSuccess(name) - } +// WARNING! total hack, being removed after covid... +def isCovid() { + env.GERRIT_BRANCH == 'covid' } - -def build_parameters = [ - string(name: 'GERRIT_REFSPEC', value: "${env.GERRIT_REFSPEC}"), - string(name: 'GERRIT_EVENT_TYPE', value: "${env.GERRIT_EVENT_TYPE}"), - string(name: 'GERRIT_BRANCH', value: "${env.GERRIT_BRANCH}"), - string(name: 'GERRIT_CHANGE_NUMBER', value: "${env.GERRIT_CHANGE_NUMBER}"), - string(name: 'GERRIT_PATCHSET_NUMBER', value: "${env.GERRIT_PATCHSET_NUMBER}"), - string(name: 'GERRIT_EVENT_ACCOUNT_NAME', value: "${env.GERRIT_EVENT_ACCOUNT_NAME}"), - string(name: 'GERRIT_EVENT_ACCOUNT_EMAIL', value: "${env.GERRIT_EVENT_ACCOUNT_EMAIL}") -] +// end of hack (covid) pipeline { agent { label 'canvas-docker' } - - options { - ansiColor('xterm') - } + options { ansiColor('xterm') } environment { - // include selenium while smoke is running locally - COMPOSE_FILE = 'docker-compose.new-jenkins.yml:docker-compose.new-jenkins-selenium.yml' GERRIT_PORT = '29418' GERRIT_URL = "$GERRIT_HOST:$GERRIT_PORT" + NAME = getImageTagVersion() + CANVAS_LMS_IMAGE = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms" + + // e.g. postgres-9.5-ruby-passenger-2.4-xenial + TAG_SUFFIX = "postgres-$POSTGRES-ruby-passenger-$RUBY_PASSENGER" - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" - MERGE_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$GERRIT_BRANCH" - CACHE_TAG = "canvas-lms:previous-image" + // e.g. canvas-lms:01.123456.78-postgres-12-ruby-passenger-2.6 + PATCHSET_TAG = "$CANVAS_LMS_IMAGE:$NAME-$TAG_SUFFIX" + + // e.g. canvas-lms:01.123456.78-postgres-9.5-ruby-passenger-2.4-xenial + PUBLISHABLE_TAG = "$CANVAS_LMS_IMAGE:$NAME-postgres-9.5-ruby-passenger-2.4-xenial" + + // e.g. canvas-lms:master when not on another branch + MERGE_TAG = "$CANVAS_LMS_IMAGE:$GERRIT_BRANCH" } stages { - stage('Print Env Variables') { - steps { - timeout(time: 20, unit: 'SECONDS') { - sh 'printenv | sort' - } - } - } - - stage('Plugins and Config Files') { + stage('Setup') { steps { - timeout(time: 3) { + timeout(time: 5) { script { - fetchFromGerrit('gerrit_builder', '.', '', 'canvas-lms/config') - gems = readFile('gerrit_builder/canvas-lms/config/plugins_list').split() - println "Plugin list: ${gems}" - /* fetch plugins */ - gems.each { gem -> - if (env.GERRIT_PROJECT == gem) { - /* this is the commit we're testing */ - fetchFromGerrit(gem, 'gems/plugins', null, null, env.GERRIT_REFSPEC) - } else { - fetchFromGerrit(gem, 'gems/plugins') + runDatadogMetric("Setup"){ + sh 'build/new-jenkins/print-env-excluding-secrets.sh' + sh 'build/new-jenkins/docker-cleanup.sh' + + buildParameters += string(name: 'PATCHSET_TAG', value: "${env.PATCHSET_TAG}") + buildParameters += string(name: 'POSTGRES', value: "${env.POSTGRES}") + buildParameters += string(name: 'RUBY_PASSENGER', value: "${env.RUBY_PASSENGER}") + if (env.CANVAS_LMS_REFSPEC) { + // the plugin builds require the canvas lms refspec to be different. so only + // set this refspec if the main build is requesting it to be set. + // NOTE: this is only being set in main-from-plugin build. so main-canvas wont run this. + buildParameters += string(name: 'CANVAS_LMS_REFSPEC', value: env.CANVAS_LMS_REFSPEC) } - } - fetchFromGerrit('qti_migration_tool', 'vendor', 'QTIMigrationTool') - sh ''' - mv gerrit_builder/canvas-lms/config/* config/ - mv config/knapsack_rspec_report.json ./ - rm config/cache_store.yml - rmdir -p gerrit_builder/canvas-lms/config - cp docker-compose/config/selenium.yml config/ - cp -R docker-compose/config/new-jenkins config/new-jenkins - cp config/delayed_jobs.yml.example config/delayed_jobs.yml - cp config/domain.yml.example config/domain.yml - cp config/external_migration.yml.example config/external_migration.yml - cp config/outgoing_mail.yml.example config/outgoing_mail.yml - ''' + + def credentials = load ('build/new-jenkins/groovy/credentials.groovy') + + // WARNING! total hack, being removed after covid... + // if this build is triggered from a plugin that is from the + // covid branch, we need to checkout the covid branch for canvas-lms + if (isCovid() && env.GERRIT_PROJECT != 'canvas-lms') { + echo 'checking out canvas-lms covid branch' + credentials.withGerritCredentials { + sh ''' + set -ex + git branch -D covid || true + GIT_SSH_COMMAND='ssh -i \"$SSH_KEY_PATH\" -l \"$SSH_USERNAME\"' \ + git fetch origin $GERRIT_BRANCH:origin/$GERRIT_BRANCH + git checkout -b covid origin/covid + ''' + } + } + // end of hack (covid) + + credentials.fetchFromGerrit('gerrit_builder', '.', '', 'canvas-lms/config') + gems = readFile('gerrit_builder/canvas-lms/config/plugins_list').split() + echo "Plugin list: ${gems}" + /* fetch plugins */ + gems.each { gem -> + if (env.GERRIT_PROJECT == gem) { + /* this is the commit we're testing */ + credentials.fetchFromGerrit(gem, 'gems/plugins', null, null, env.GERRIT_REFSPEC) + } else { + // WARNING! total hack, being removed after covid... + // remove if statement when covid is done. only thing in else is needed. + if (isCovid()) { + echo "checkin out ${gem} covid branch" + credentials.fetchFromGerrit(gem, 'gems/plugins', null, null, 'covid') + } + else { + credentials.fetchFromGerrit(gem, 'gems/plugins') + } + // end of hack (covid) + } + } + credentials.fetchFromGerrit('gergich_user_config', '.') + credentials.fetchFromGerrit('qti_migration_tool', 'vendor', 'QTIMigrationTool') + + sh 'mv -v gerrit_builder/canvas-lms/config/* config/' + sh 'rm -v config/cache_store.yml' + sh 'rmdir -p gerrit_builder/canvas-lms/config' + sh 'rm -v config/database.yml' + sh 'rm -v config/security.yml' + sh 'rm -v config/selenium.yml' + sh 'cp -v docker-compose/config/selenium.yml config/' + sh 'cp -vR docker-compose/config/new-jenkins/* config/' + sh 'cp -v config/delayed_jobs.yml.example config/delayed_jobs.yml' + sh 'cp -v config/domain.yml.example config/domain.yml' + sh 'cp -v config/external_migration.yml.example config/external_migration.yml' + sh 'cp -v config/outgoing_mail.yml.example config/outgoing_mail.yml' + sh 'cp -v ./gergich_user_config/gergich_user_config.yml ./gems/dr_diff/config/gergich_user_config.yml' } } } } + } stage('Rebase') { - when { expression { env.GERRIT_EVENT_TYPE == 'patchset-created' } } + when { expression { env.GERRIT_EVENT_TYPE == 'patchset-created' && env.GERRIT_PROJECT == 'canvas-lms' } } steps { timeout(time: 2) { script { - withGerritCredentials({ -> - sh ''' - GIT_SSH_COMMAND='ssh -i \"$SSH_KEY_PATH\" -l \"$SSH_USERNAME\"' \ - git fetch origin $GERRIT_BRANCH - - git config user.name "$GERRIT_EVENT_ACCOUNT_NAME" - git config user.email "$GERRIT_EVENT_ACCOUNT_EMAIL" - - # this helps current build issues where cleanup is needed before proceeding. - # however the later git rebase --abort should be enough once this has - # been on jenkins for long enough to hit all nodes, maybe a couple days? - if [ -d .git/rebase-merge ]; then - echo "A previous build's rebase failed and the build exited without cleaning up. Aborting the previous rebase now..." - git rebase --abort - git checkout $GERRIT_REFSPEC - fi - - # store exit_status inline to ensures the script doesn't exit here on failures - git rebase --preserve-merges origin/$GERRIT_BRANCH; exit_status=$? - if [ $exit_status != 0 ]; then - echo "Warning: Rebase couldn't resolve changes automatically, please resolve these conflicts locally." - git rebase --abort - exit $exit_status - fi - ''' - }) - } - } - } - } + runDatadogMetric("Rebase") { + def credentials = load('build/new-jenkins/groovy/credentials.groovy') + credentials.withGerritCredentials({ -> + sh ''' + GIT_SSH_COMMAND='ssh -i \"$SSH_KEY_PATH\" -l \"$SSH_USERNAME\"' \ + git fetch origin $GERRIT_BRANCH:origin/$GERRIT_BRANCH - stage('Build Image') { - steps { - skipIfPreviouslySuccessful("build-and-push-image", save = false) { - timeout(time: 36) { /* this timeout is `2 * average build time` which currently: 18m * 2 = 36m */ - dockerCacheLoad(image: "$CACHE_TAG") - sh ''' - docker build -t $PATCHSET_TAG . - docker tag $PATCHSET_TAG $CACHE_TAG - ''' + git config user.name "$GERRIT_EVENT_ACCOUNT_NAME" + git config user.email "$GERRIT_EVENT_ACCOUNT_EMAIL" + + # this helps current build issues where cleanup is needed before proceeding. + # however the later git rebase --abort should be enough once this has + # been on jenkins for long enough to hit all nodes, maybe a couple days? + if [ -d .git/rebase-merge ]; then + echo "A previous build's rebase failed and the build exited without cleaning up. Aborting the previous rebase now..." + git rebase --abort + git checkout $GERRIT_REFSPEC + fi + + # store exit_status inline to ensures the script doesn't exit here on failures + git rebase --preserve-merges origin/$GERRIT_BRANCH; exit_status=$? + if [ $exit_status != 0 ]; then + echo "Warning: Rebase couldn't resolve changes automatically, please resolve these conflicts locally." + git rebase --abort + exit $exit_status + fi + ''' + }) + } } } } } - stage('Publish Patchset Image') { + stage ('Build Docker Image') { steps { - skipIfPreviouslySuccessful("build-and-push-image") { - timeout(time: 5) { - // always push the patchset tag otherwise when a later - // patchset is merged this patchset tag is overwritten - sh 'docker push $PATCHSET_TAG' + timeout(time: 36) { /* this timeout is `2 * average build time` which currently: 18m * 2 = 36m */ + skipIfPreviouslySuccessful('docker-build-and-push') { + script { + def flags = load('build/new-jenkins/groovy/commit-flags.groovy') + if (flags.hasFlag('skip-docker-build')) { + sh 'docker pull $MERGE_TAG' + sh 'docker tag $MERGE_TAG $PATCHSET_TAG' + } + else { + if (!flags.hasFlag('skip-cache')) { + sh 'docker pull $MERGE_TAG || true' + } + sh """ + docker build \ + --tag $PATCHSET_TAG \ + --build-arg RUBY_PASSENGER=$RUBY_PASSENGER \ + --build-arg POSTGRES_VERSION=$POSTGRES \ + . + """ + } + } + sh "docker push $PATCHSET_TAG" } } } } stage('Parallel Run Tests') { - parallel { - // TODO: this is temporary until we can get some actual builds passing - stage('Smoke Test') { - steps { - skipIfPreviouslySuccessful("smoke-test") { - timeout(time: 10) { - sh 'build/new-jenkins/docker-compose-pull.sh' - sh 'build/new-jenkins/docker-compose-pull-selenium.sh' - sh 'build/new-jenkins/docker-compose-build-up.sh' - sh 'build/new-jenkins/docker-compose-create-migrate-database.sh' - sh 'build/new-jenkins/smoke-test.sh' + steps { + script { + def stages = [:] + if (env.GERRIT_EVENT_TYPE != 'change-merged' && env.GERRIT_PROJECT == 'canvas-lms' && !isCovid()) { + echo 'adding Linters' + stages['Linters'] = { + skipIfPreviouslySuccessful("linters") { + sh 'build/new-jenkins/linters/run-gergich.sh' + if (env.MASTER_BOUNCER_RUN == '1' && env.GERRIT_EVENT_TYPE == 'patchset-created') { + def credentials = load 'build/new-jenkins/groovy/credentials.groovy' + credentials.withMasterBouncerCredentials { + sh 'build/new-jenkins/linters/run-master-bouncer.sh' + } + } } } } - } - stage('Linters') { - steps { - skipIfPreviouslySuccessful("linters") { - build( - job: 'test-suites/linters', - propagate: false, - parameters: build_parameters - ) + echo 'adding Vendored Gems' + stages['Vendored Gems'] = { + skipIfPreviouslySuccessful("vendored-gems") { + build(job: 'test-suites/vendored-gems', parameters: buildParameters) } } - } - stage('Vendored Gems') { - steps { - skipIfPreviouslySuccessful("vendored-gems") { - build( - job: 'test-suites/vendored-gems', - parameters: build_parameters - ) + echo 'adding Javascript' + stages['Javascript'] = { + skipIfPreviouslySuccessful("javascript") { + build(job: 'test-suites/JS', parameters: buildParameters) + } + } + + echo 'adding Contract Tests' + stages['Contract Tests'] = { + skipIfPreviouslySuccessful("contract-tests") { + build(job: 'test-suites/contract-tests', parameters: buildParameters) + } + } + + if (env.GERRIT_EVENT_TYPE != 'change-merged' && !isCovid()) { + echo 'adding Flakey Spec Catcher' + stages['Flakey Spec Catcher'] = { + skipIfPreviouslySuccessful("flakey-spec-catcher") { + build( + job: 'test-suites/flakey-spec-catcher', + parameters: buildParameters + ) + } } } + + // // keep this around in case there is changes to the subbuilds that need to happen + // // and you have no other way to test it except by running a test build. + // stages['Test Subbuild'] = { + // skipIfPreviouslySuccessful("test-subbuild") { + // build(job: 'test-suites/test-subbuild', parameters: buildParameters) + // } + // } + + // // Don't run these on all patch sets until we have them ready to report results. + // // Uncomment stage to run when developing. + // stages['Xbrowser'] = { + // skipIfPreviouslySuccessful("xbrowser") { + // build(job: 'test-suites/xbrowser', propagate: false, parameters: buildParameters) + // } + // } + + def distribution = load 'build/new-jenkins/groovy/distribution.groovy' + distribution.stashBuildScripts() + + distribution.addRSpecSuites(stages) + distribution.addSeleniumSuites(stages) + + parallel(stages) } -/* - * Don't run these on all patch sets until we have them ready to report results. - * Uncomment stage to run when developing. - * stage('Selenium Chrome') { - * steps { - * skipIfPreviouslySuccessful("selenium-chrome") { - * // propagate set to false until we can get tests passing - * build( - * job: 'test-suites/selenium-chrome', - * propagate: false, - * parameters: build_parameters - * ) - * } - * } - * } - * - * stage('Rspec') { - * steps { - * skipIfPreviouslySuccessful("rspec") { - * // propagate set to false until we can get tests passing - * build( - * job: 'test-suites/rspec', - * propagate: false, - * parameters: build_parameters - * ) - * } - * } - * } - * - * stage('Selenium Performance Chrome') { - * steps { - * skipIfPreviouslySuccessful("selenium-performance-chrome") { - * // propagate set to false until we can get tests passing - * build( - * job: 'test-suites/selenium-performance-chrome', - * propagate: false, - * parameters: build_parameters - * ) - * } - * } - * } - * - * stage('Contract Tests') { - * steps { - * skipIfPreviouslySuccessful("contract-tests") { - * // propagate set to false until we can get tests passing - * build( - * job: 'test-suites/contract-tests', - * propagate: false, - * parameters: build_parameters - * ) - * } - * } - * } - * - * stage('Frontend') { - * steps { - * skipIfPreviouslySuccessful("frontend") { - * // propagate set to false until we can get tests passing - * build( - * job: 'test-suites/frontend', - * propagate: false, - * parameters: build_parameters - * ) - * } - * } - * } - * - * stage('Xbrowser') { - * steps { - * skipIfPreviouslySuccessful("xbrowser") { - * // propagate set to false until we can get tests passing - * build( - * job: 'test-suites/xbrowser', - * propagate: false, - * parameters: build_parameters - * ) - * } - * } - * } - */ } } - stage('Publish Merged Image') { + stage('Publish Image on Merge') { + when { + allOf { + expression { isPatchsetPublishable() } + expression { env.GERRIT_EVENT_TYPE == 'change-merged' } + } + } steps { - timeout(time: 5) { + timeout(time: 10) { script { - if (env.GERRIT_EVENT_TYPE == 'change-merged') { - sh ''' - docker tag $PATCHSET_TAG $MERGE_TAG - docker push $MERGE_TAG - ''' - dockerCacheStore(image: "$CACHE_TAG") + runDatadogMetric("publishImageOnMerge") { + // Retriggers won't have an image to tag/push, pull that + // image if doesn't exist. If image is not found it will + // return NULL + if (!sh (script: 'docker images -q $PATCHSET_TAG')) { + sh 'docker pull $PATCHSET_TAG' + } + + // publish canvas-lms:$GERRIT_BRANCH (i.e. canvas-lms:master) + sh 'docker tag $PUBLISHABLE_TAG $MERGE_TAG' + // push *all* canvas-lms images (i.e. all canvas-lms prefixed tags) + sh 'docker push $MERGE_TAG' } } } } } + + stage('Dependency Check') { + when { expression { env.GERRIT_EVENT_TYPE == 'change-merged' } } + steps { + script { + runDatadogMetric("dependencyCheck") { + def reports = load 'build/new-jenkins/groovy/reports.groovy' + reports.snykCheckDependencies("$PATCHSET_TAG", "/usr/src/app/") + } + } + } + } + + stage('Mark Build as Successful') { + steps { + script { + runDatadogMetric("markBuildAsSuccessful") { + def successes = load 'build/new-jenkins/groovy/successes.groovy' + successes.markBuildAsSuccessful() + } + } + } + } } post { + failure { + script { + if (isPatchsetPublishable() && env.GERRIT_EVENT_TYPE == 'change-merged') { + def branchSegment = env.GERRIT_BRANCH ? "[$env.GERRIT_BRANCH]" : '' + def authorSegment = env.GERRIT_EVENT_ACCOUNT_NAME ? "Patchset by ${env.GERRIT_EVENT_ACCOUNT_NAME}. " : '' + slackSend( + channel: '#canvas_builds', + color: 'danger', + message: "${branchSegment}${env.JOB_NAME} failed on merge. ${authorSegment}(<${env.BUILD_URL}|${env.BUILD_NUMBER}>)" + ) + } + } + } + always { + script { + def rspec = load 'build/new-jenkins/groovy/rspec.groovy' + rspec.uploadSeleniumFailures() + rspec.uploadRSpecFailures() + } + } cleanup { - sh 'build/new-jenkins/docker-cleanup.sh' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' } } } diff --git a/Jenkinsfile.contract-tests b/Jenkinsfile.contract-tests index 255fab82b00c7..4e6ef9c30a198 100644 --- a/Jenkinsfile.contract-tests +++ b/Jenkinsfile.contract-tests @@ -20,23 +20,27 @@ pipeline { agent { label 'canvas-docker' } - options { - ansiColor('xterm') - } + options { ansiColor('xterm') } environment { COMPOSE_FILE = 'docker-compose.new-jenkins.yml' - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" PACT_BROKER = credentials('PACT_BROKER') } stages { + stage ('Pre-Cleanup') { + steps { + timeout(time: 2) { + sh 'build/new-jenkins/docker-cleanup.sh' + } + } + } + stage('Start Docker Images') { steps { timeout(time: 10) { - sh 'docker-compose build && docker-compose up -d' + sh 'build/new-jenkins/docker-compose-pull.sh' + sh 'build/new-jenkins/docker-compose-build-up.sh' sh 'build/new-jenkins/docker-compose-create-migrate-database.sh' } } @@ -73,7 +77,7 @@ pipeline { post { failure { - sh 'mkdir -p spec_results' + sh 'mkdir -vp spec_results' sh 'docker cp $(docker-compose ps -q web):/usr/src/app/log/spec_failures/. ./spec_results/' script { dir('spec_results') { @@ -91,8 +95,8 @@ pipeline { } } cleanup { - sh 'rm -rf spec_results/' - sh 'build/new-jenkins/docker-cleanup.sh' + sh 'rm -vrf spec_results/' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' } } } diff --git a/Jenkinsfile.dive b/Jenkinsfile.dive new file mode 100644 index 0000000000000..3fd5e09108f2b --- /dev/null +++ b/Jenkinsfile.dive @@ -0,0 +1,52 @@ +#!/usr/bin/env groovy + +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This file is part of Canvas. + * + * Canvas is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3 of the License. + * + * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + +pipeline { + agent { label 'canvas-docker' } + options { ansiColor('xterm') } + environment { IMAGE = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:master" } + + stages { + stage('Setup') { + steps { + timeout(time: 2) { + sh 'build/new-jenkins/print-env-excluding-secrets.sh' + sh 'build/new-jenkins/docker-cleanup.sh' + } + } + } + + stage('Dive Analyze') { + steps { + timeout(time: 60) { + sh "docker pull ${env.IMAGE}" + sh 'docker pull wagoodman/dive' + sh 'build/new-jenkins/dive.sh' + } + } + } + } + + post { + cleanup { + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' + } + } +} diff --git a/Jenkinsfile.frontend b/Jenkinsfile.frontend deleted file mode 100644 index 4e1bf7a1def08..0000000000000 --- a/Jenkinsfile.frontend +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env groovy - -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * This file is part of Canvas. - * - * Canvas is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, version 3 of the License. - * - * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License along - * with this program. If not, see . - */ - -def setDockerUp () { - timeout(time: 60) { - echo 'Running containers' - sh 'docker ps' - sh 'printenv | sort' - sh 'build/new-jenkins/docker-compose-pull.sh' - sh 'build/new-jenkins/docker-compose-build-up.sh' - } -} - -def cleanupDocker () { - withEnv(['COMPOSE_FILE=docker-compose.new-jenkins-web.yml:docker-compose.new-jenkins-karma.yml']) { - // Make sure to clean up the karma containers and image - sh 'docker-compose rm -fs karma' - sh 'docker rmi frontend_karma' - // Clean up all the other stuff - sh 'build/new-jenkins/docker-cleanup.sh' - } -} - -def isMerge () { - return env.GERRIT_EVENT_TYPE == 'change-merged' -} - - -pipeline { - agent { label 'canvas-docker' } - options { - ansiColor('xterm') - } - - environment { - COMPOSE_FILE = 'docker-compose.new-jenkins-web.yml' - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" - } - stages { - stage('Setup') { - steps { - setDockerUp() - } - } - stage('Tests Setup') { - environment { - COMPOSE_FILE = 'docker-compose.new-jenkins-web.yml:docker-compose.new-jenkins-karma.yml' - } - steps { - setDockerUp() - } - } - stage('Tests') { - environment { - COMPOSE_FILE = 'docker-compose.new-jenkins-web.yml:docker-compose.new-jenkins-karma.yml' - COVERAGE = isMerge() - } - parallel { - stage('Jest') { - steps { - sh 'build/new-jenkins/frontend/tests-jest.sh' - } - } - stage('Packages') { - steps { - sh 'build/new-jenkins/frontend/tests-packages.sh' - } - } - stage('Karma - Spec Group - coffee') { - environment { - JSPEC_GROUP = 'coffee' - } - steps { - sh 'build/new-jenkins/frontend/tests-karma.sh' - } - } - stage('Karma - Spec Group - jsa - A-F') { - environment { - JSPEC_GROUP = 'jsa' - } - steps { - sh 'build/new-jenkins/frontend/tests-karma.sh' - } - } - stage('Karma - Spec Group - jsg - G') { - environment { - JSPEC_GROUP = 'jsg' - } - steps { - sh 'build/new-jenkins/frontend/tests-karma.sh' - } - } - stage('Karma - Spec Group - jsh - H-Z') { - environment { - JSPEC_GROUP = 'jsh' - } - steps { - sh 'build/new-jenkins/frontend/tests-karma.sh' - } - } - } - } - } - post { - cleanup { - cleanupDocker() - } - } -} diff --git a/Jenkinsfile.js b/Jenkinsfile.js new file mode 100644 index 0000000000000..4adaf1be7f16c --- /dev/null +++ b/Jenkinsfile.js @@ -0,0 +1,186 @@ +#!/usr/bin/env groovy + +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This file is part of Canvas. + * + * Canvas is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3 of the License. + * + * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + +def runCoverage() { + def flags = load 'build/new-jenkins/groovy/commit-flags.groovy' + return env.RUN_COVERAGE == '1' || flags.forceRunCoverage() ? '1' : '' +} + +def isForceFailure() { + def flags = load 'build/new-jenkins/groovy/commit-flags.groovy' + return flags.isForceFailure() ? "1" : '' +} + +def getImageTagVersion() { + def flags = load 'build/new-jenkins/groovy/commit-flags.groovy' + // total hack, we shouldnt do it like this. but until there is a better + // way of passing info across builds, this is path of least resistance.. + // also, it didnt seem to work with multiple return statements, so i'll + // just go ahead and leave this monstrosity here. + return env.RUN_COVERAGE == '1' ? 'master' : flags.getImageTagVersion() +} + +def copyFiles(docker_name, docker_dir, host_dir) { + sh "mkdir -vp ./$host_dir" + sh "docker cp \$(docker ps -qa -f name=$docker_name):/usr/src/app/$docker_dir ./$host_dir" +} + +def withSentry(block) { + def credentials = load 'build/new-jenkins/groovy/credentials.groovy' + credentials.withSentryCredentials(block) +} + +def runInSeriesOrParallel(is_series, stages_map) { + if (is_series) { + echo "running tests in series: ${stages_map.keys}" + stages_map.each { name, block -> + stage(name) { + block() + } + } + } + else { + echo "running tests in parallel: ${stages_map.keys}" + parallel(stages_map) + } +} + +pipeline { + agent { label 'canvas-docker' } + options { ansiColor('xterm') } + + environment { + COMPOSE_FILE = 'docker-compose.new-jenkins-web.yml:docker-compose.new-jenkins-karma.yml' + COVERAGE = runCoverage() + FORCE_FAILURE = isForceFailure() + SENTRY_URL="https://sentry.insops.net" + SENTRY_ORG="instructure" + SENTRY_PROJECT="master-javascript-build" + } + + stages { + stage('Pre-Cleanup') { + steps { + timeout(time: 2) { + sh 'build/new-jenkins/docker-cleanup.sh' + sh 'build/new-jenkins/print-env-excluding-secrets.sh' + sh 'rm -vrf ./tmp/*' + } + } + } + + stage('Tests Setup') { + steps { + timeout(time: 60) { + sh 'build/new-jenkins/docker-compose-pull.sh' + sh 'docker-compose build' + } + } + } + + stage('Test Stage Coordinator') { + steps { + script { + def tests = [:] + + tests['Jest'] = { + withEnv(['CONTAINER_NAME=tests-jest']) { + try { + withSentry { + sh 'build/new-jenkins/js/tests-jest.sh' + } + if (env.COVERAGE == '1') { + copyFiles(env.CONTAINER_NAME, 'coverage-jest', "./tmp/${env.CONTAINER_NAME}-coverage") + } + } + finally { + copyFiles(env.CONTAINER_NAME, 'coverage-js', "./tmp/${env.CONTAINER_NAME}") + } + } + } + + tests['Packages'] = { + withEnv(['CONTAINER_NAME=tests-packages']) { + try { + withSentry { + sh 'build/new-jenkins/js/tests-packages.sh' + } + } + finally { + copyFiles(env.CONTAINER_NAME, 'packages', "./tmp/${env.CONTAINER_NAME}") + } + } + } + + tests['canvas_quizzes'] = { + sh 'build/new-jenkins/js/tests-quizzes.sh' + } + + ['coffee', 'jsa', 'jsg', 'jsh'].each { group -> + tests["Karma - Spec Group - ${group}"] = { + withEnv(["CONTAINER_NAME=tests-karma-${group}", "JSPEC_GROUP=${group}"]) { + try { + withSentry { + sh 'build/new-jenkins/js/tests-karma.sh' + } + if (env.COVERAGE == '1') { + copyFiles(env.CONTAINER_NAME, 'coverage-karma', "./tmp/${env.CONTAINER_NAME}-coverage") + } + } + finally { + copyFiles(env.CONTAINER_NAME, 'coverage-js', "./tmp/${env.CONTAINER_NAME}") + } + } + } + } + + runInSeriesOrParallel(env.COVERAGE == '1', tests) + } + } + } + + + stage('Upload Coverage') { + when { expression { env.COVERAGE == '1' } } + steps { + timeout(time: 10) { + sh 'build/new-jenkins/js/coverage-report.sh' + archiveArtifacts(artifacts: 'coverage-report-js/**/*') + uploadCoverage([ + uploadSource: "/coverage-report-js/report-html", + uploadDest: "canvas-lms-js/coverage" + ]) + } + } + } + } + + post { + always { + script { + junit allowEmptyResults: true, testResults: 'tmp/**/*.xml' + sh 'find ./tmp -path "*.xml"' + } + } + cleanup { + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' + } + } +} diff --git a/Jenkinsfile.linters b/Jenkinsfile.linters deleted file mode 100644 index c412d1d6ac6f2..0000000000000 --- a/Jenkinsfile.linters +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env groovy - -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * This file is part of Canvas. - * - * Canvas is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, version 3 of the License. - * - * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License along - * with this program. If not, see . - */ - -pipeline { - agent { label 'canvas-docker' } - options { - ansiColor('xterm') - } - - environment { - COMPOSE_FILE = 'docker-compose.new-jenkins-web.yml' - - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" - } - stages { - stage('Print Env Variables') { - steps { - timeout(time: 20, unit: 'SECONDS') { - sh 'printenv | sort' - } - } - } - // this is here because someone forgot to add the post cleanup in this build. - // remove this after it runs for a little bit. - stage('temp-cleanup') { - steps { - sh 'build/new-jenkins/docker-cleanup.sh' - } - } - - stage("All Linters") { - parallel { - stage('rlint') { - steps { - sh 'build/new-jenkins/linters/run-rlint.sh' - } - } - stage('brakeman') { - steps { - sh 'build/new-jenkins/linters/run-brakeman.sh' - } - } - stage('master bouncer') { - steps { - sh 'build/new-jenkins/linters/run-master-bouncer.sh' - } - } - stage('commit message') { - steps { - sh 'build/new-jenkins/linters/run-commit-message.sh' - } - } - stage('tatl tael') { - steps { - sh 'build/new-jenkins/linters/run-tatl-tael.sh' - } - } - stage('ESLint - JSX') { - steps { - sh 'build/new-jenkins/linters/run-eslint.sh' - } - } - stage('Stylelint') { - steps { - sh 'build/new-jenkins/linters/run-stylelint.sh' - } - } - stage('XSSLint') { - steps { - sh 'build/new-jenkins/linters/run-xss.sh' - } - } - } - } - } - - post { - cleanup { - sh 'build/new-jenkins/docker-cleanup.sh' - } - } -} diff --git a/Jenkinsfile.main-for-coverage b/Jenkinsfile.main-for-coverage new file mode 100644 index 0000000000000..c407c94a749e3 --- /dev/null +++ b/Jenkinsfile.main-for-coverage @@ -0,0 +1,89 @@ +#!/usr/bin/env groovy + +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This file is part of Canvas. + * + * Canvas is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3 of the License. + * + * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + + +pipeline { + agent { label 'canvas-docker' } + options { ansiColor('xterm') } + + environment { + RUN_COVERAGE = '1' + POSTGRES = '9.5' + RUBY_PASSENGER = '2.4-xenial' + PATCHSET_TAG = "${env.DOCKER_REGISTRY_FQDN}/jenkins/canvas-lms:master" + COVERAGE = '1' + } + + stages { + stage('Print Env Variables') { + steps { + timeout(time: 20, unit: 'SECONDS') { + sh 'build/new-jenkins/print-env-excluding-secrets.sh' + } + } + } + + stage('Parallel Run Tests') { + steps { + script { + def stages = [:] + + echo 'adding Javascript' + stages['Javascript'] = { + build(job: 'test-suites/JS', parameters: [ + string(name: 'CANVAS_LMS_REFSPEC', value: env.CANVAS_LMS_REFSPEC), + string(name: 'RUN_COVERAGE', value: env.RUN_COVERAGE), + string(name: 'POSTGRES', value: env.POSTGRES), + string(name: 'RUBY_PASSENGER', value: env.RUBY_PASSENGER), + string(name: 'PATCHSET_TAG', value: env.PATCHSET_TAG) + ]) + } + + def distribution = load 'build/new-jenkins/groovy/distribution.groovy' + distribution.stashBuildScripts() + distribution.addRSpecSuites(stages) + distribution.addSeleniumSuites(stages) + + parallel(stages) + } + } + } + + stage('Upload Coverage') { + steps { + script { + def rspec = load 'build/new-jenkins/groovy/rspec.groovy' + rspec.uploadSeleniumCoverage() + rspec.uploadRSpecCoverage() + } + } + } + } + + post { + always { + script { + def rspec = load 'build/new-jenkins/groovy/rspec.groovy' + rspec.uploadSeleniumFailures() + rspec.uploadRSpecFailures() + } + } + } +} diff --git a/Jenkinsfile.master-bouncer-check-all b/Jenkinsfile.master-bouncer-check-all new file mode 100644 index 0000000000000..12a64bdb69934 --- /dev/null +++ b/Jenkinsfile.master-bouncer-check-all @@ -0,0 +1,75 @@ +#!/usr/bin/env groovy + +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This file is part of Canvas. + * + * Canvas is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3 of the License. + * + * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + +pipeline { + agent { label 'canvas-docker' } + + options { + ansiColor('xterm') + } + + environment { + // we only use this + PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:master" + GERRIT_BRANCH = "master" + GERGICH_REVIEW_LABEL = "Lint-Review" + } + + stages { + stage('Print Env Variables') { + steps { + timeout(time: 20, unit: 'SECONDS') { + sh 'printenv | sort' + } + } + } + + stage ('Pre-Cleanup') { + steps { + timeout(time: 2) { + sh 'build/new-jenkins/docker-cleanup.sh' + } + } + } + + stage ('Master Bouncer Check All') { + steps { + // should only take about 5-10 min, but just in case + timeout(time: 30) { + script { + def credentials = load 'build/new-jenkins/groovy/credentials.groovy' + credentials.withMasterBouncerCredentials { + credentials.withGerritCredentials { + sh 'build/new-jenkins/linters/run-master-bouncer-all.py' + } + } + } + } + } + } + } + + post { + cleanup { + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' + } + } +} + diff --git a/Jenkinsfile.package-translations b/Jenkinsfile.package-translations index 6ad6238188a24..5218b75b3a89b 100644 --- a/Jenkinsfile.package-translations +++ b/Jenkinsfile.package-translations @@ -20,7 +20,7 @@ def cleanupDocker () { COMPOSE_FILE = 'docker-compose.new-jenkins-package-translations.yml' - sh 'build/new-jenkins/docker-cleanup.sh' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' } @@ -34,6 +34,13 @@ pipeline { COMPOSE_FILE = 'docker-compose.new-jenkins-package-translations.yml' } stages { + stage('Pre-Cleanup') { + steps { + timeout(time: 2) { + cleanupDocker() + } + } + } stage('Sync Translations') { steps { withCredentials([ diff --git a/Jenkinsfile.rspec b/Jenkinsfile.rspec deleted file mode 100644 index 0a5f2e4670950..0000000000000 --- a/Jenkinsfile.rspec +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env groovy - -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * This file is part of Canvas. - * - * Canvas is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, version 3 of the License. - * - * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License along - * with this program. If not, see . - */ - -def ci_node_total = 10; // how many nodes to run on -pipeline { - agent none - options { - ansiColor('xterm') - } - - environment { - COMPOSE_FILE = 'docker-compose.new-jenkins.yml' - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" - KNAPSACK_ENABLED = 1 - KNAPSACK_GENERATE_REPORT = 'false' - KNAPSACK_TEST_FILE_PATTERN = '{spec,gems/plugins/*/spec_canvas}/**/*_spec.rb' - KNAPSACK_EXCLUDE_REGEX = '/selenium/' - KNAPSACK_TEST_DIR = 'spec' - RERUNS_RETRY = 1 - MAX_FAIL = 50 - } - stages { - stage ('Distribute Rspec Tests') { - steps { - script { - def nodes = [:]; - for(int i = 0; i < ci_node_total; i++) { - def index = i; - nodes["rspec set ${(i+1).toString().padLeft(2, '0')}"] = { - withEnv(["CI_NODE_INDEX=$index", "CI_NODE_TOTAL=$ci_node_total"]) { - node('canvas-docker') { - stage("Running RSpec Set ${index}") { - try { - sh 'rm -rf ./tmp/spec_failures' - checkout scm - timeout(time: 60) { - sh 'printenv | sort' - sh 'build/new-jenkins/docker-compose-pull.sh' - sh 'build/new-jenkins/docker-compose-build-up.sh' - sh 'build/new-jenkins/docker-compose-create-migrate-database.sh' - sh 'build/new-jenkins/rspec-with-retries.sh' - } - } - catch (ex) { - // copy spec failures to local - sh 'mkdir -p tmp' - sh 'docker cp $(docker-compose ps -q web):/usr/src/app/log/spec_failures/ ./tmp/spec_failures/' - throw ex - } - finally { - dir ('tmp') { - stash name: "rspec_failures_${index}", includes: 'spec_failures/**/*', allowEmpty: true - } - sh 'rm -rf ./tmp/spec_failures' - sh 'build/new-jenkins/docker-cleanup.sh' - } - } - } - } - } - } - parallel(nodes); - } - } - } - } - post { - failure { - script { - node { - sh 'rm -rf ./compiled_failures' - def htmlFiles; - dir('compiled_failures') { - for(int i = 0; i < ci_node_total; i++) { - def index = i; - dir ("node_${index}") { - unstash "rspec_failures_${index}" - } - } - htmlFiles = findFiles glob: '**/index.html' - } - publishHTML target: [ - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: 'compiled_failures', - reportFiles: htmlFiles.join(','), - reportName: 'Test Failures' - ] - sh 'rm -rf ./compiled_failures' - } - } - } - } -} diff --git a/Jenkinsfile.selenium.chrome b/Jenkinsfile.selenium.chrome deleted file mode 100644 index 93d1aec72fe70..0000000000000 --- a/Jenkinsfile.selenium.chrome +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env groovy - -/* - * Copyright (C) 2019 - present Instructure, Inc. - * - * This file is part of Canvas. - * - * Canvas is free software: you can redistribute it and/or modify it under - * the terms of the GNU Affero General Public License as published by the Free - * Software Foundation, version 3 of the License. - * - * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License along - * with this program. If not, see . - */ - -def ci_node_total = 20; // how many nodes to run on -pipeline { - agent none - options { - ansiColor('xterm') - } - - environment { - COMPOSE_FILE = 'docker-compose.new-jenkins.yml:docker-compose.new-jenkins-selenium.yml' - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" - KNAPSACK_ENABLED = 1 - KNAPSACK_GENERATE_REPORT = 'false' - KNAPSACK_TEST_FILE_PATTERN = '{spec/selenium,gems/plugins/*/spec_canvas/selenium}/**/*_spec.rb' - KNAPSACK_EXCLUDE_REGEX = '/performance/' - KNAPSACK_TEST_DIR = 'spec' - // for now 1 until we stabilize some of the flaky specs - RERUNS_RETRY = 1 - MAX_FAIL = 80 - } - - stages { - stage ('Distribute Selenium Tests') { - steps { - script { - def nodes = [:]; - for(int i = 0; i < ci_node_total; i++) { - def index = i; - nodes["selenium set ${(i+1).toString().padLeft(2, '0')}"] = { - withEnv(["CI_NODE_INDEX=$index", "CI_NODE_TOTAL=$ci_node_total"]) { - node('canvas-docker') { - stage("Running Selenium Set ${index}") { - try { - checkout scm - sh 'rm -rf ./tmp/spec_failures' - timeout(time: 60) { - sh 'printenv | sort' - sh 'build/new-jenkins/docker-compose-pull.sh' - sh 'build/new-jenkins/docker-compose-pull-selenium.sh' - sh 'build/new-jenkins/docker-compose-build-up.sh' - sh 'build/new-jenkins/docker-compose-create-migrate-database.sh' - sh 'build/new-jenkins/rspec-with-retries.sh' - } - } - catch (ex) { - // copy spec failures to local - sh 'mkdir -p tmp' - sh 'docker cp $(docker-compose ps -q web):/usr/src/app/log/spec_failures/ ./tmp/spec_failures/' - throw ex - } - finally { - dir ('tmp') { - stash name: "selenium_failures_${index}", includes: 'spec_failures/**/*', allowEmpty: true - } - sh 'rm -rf ./tmp/spec_failures' - sh 'build/new-jenkins/docker-cleanup.sh' - } - } - } - } - } - } - parallel(nodes); - } - } - } - } - - post { - failure { - script { - node { - sh 'rm -rf ./compiled_failures' - def htmlFiles; - dir('compiled_failures') { - for(int i = 0; i < ci_node_total; i++) { - def index = i; - dir ("node_${index}") { - unstash "selenium_failures_${index}" - } - } - htmlFiles = findFiles glob: '**/index.html' - - def indexHtml = "" - htmlFiles.each { - def spec = (it =~ /.*spec_failures\/(.*)\/index/)[0][1] - indexHtml += "${spec}
" - } - indexHtml += "" - writeFile file: "index.html", text: indexHtml - } - - publishHTML target: [ - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: 'compiled_failures', - reportFiles: "index.html," + htmlFiles.join(','), - reportName: 'Test Failures' - ] - sh 'rm -rf ./compiled_failures' - } - } - } - } -} diff --git a/Jenkinsfile.selenium.flakey_spec_catcher b/Jenkinsfile.selenium.flakey_spec_catcher new file mode 100644 index 0000000000000..30288e8c41e43 --- /dev/null +++ b/Jenkinsfile.selenium.flakey_spec_catcher @@ -0,0 +1,216 @@ +#!/usr/bin/env groovy + +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This file is part of Canvas. + * + * Canvas is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3 of the License. + * + * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + +@groovy.transform.Field +def result_test_count = -1 +@groovy.transform.Field +def result_node_count = -1 +@groovy.transform.Field +def changed_tests = '' +@groovy.transform.Field +def fsc_timeout = false + +def getImageTagVersion() { + def flags = load 'build/new-jenkins/groovy/commit-flags.groovy' + return flags.getImageTagVersion() +} + +def computeTestCount() { + // oops, probably should have added an easier way to _count_ tests... + sh 'rm -vrf tmp' + sh 'mkdir -v tmp' + sh 'chmod -vv 777 tmp' + sh """ + docker run --volume \$WORKSPACE/.git:/usr/src/app/.git \ + --volume \$WORKSPACE/tmp:/usr/src/app/tmp \ + \$PATCHSET_TAG \ + bundle exec flakey_spec_catcher --use-parent --dry-run-quiet > tmp/test_list + """ + changed_tests = readFile('tmp/test_list').trim() + echo "raw result from catcher: \n====\n$changed_tests\n====" + def test_count = 0 + if (changed_tests) { + test_count = changed_tests.split('\n').length + } + echo "expected tests to run: $test_count" + result_test_count = test_count +} + +def computeDistributedCount() { + if (result_test_count < 0) + throw IllegalStateException("call computeTestCount() first") + // this type of distributed thing always needs a hard cutoff + if (env.DISTRIBUTED_CUT_OFF.toInteger() < result_test_count) + throw IllegalStateException("unable to process more than ${env.DISTRIBUTED_CUT_OFF} tests") + if (result_test_count == 0) { + result_node_count = 0 + } + else { + // force a round down + // this will have the following node counts. + // test | nodes + // 1-14 | 1 + // 15-24 | 2 + // 25-34 | 3 + // ... + def distributed_offset = env.DISTRIBUTED_OFFSET.toInteger() + def distributed_factor = env.DISTRIBUTED_FACTOR.toInteger() + result_node_count = (int) ((result_test_count + distributed_offset) / distributed_factor) + } +} + +def executeFlakeySpecCatcher(prefix = 'main') { + sh 'rm -vrf tmp' + try { + timeout(30) { + sh 'build/new-jenkins/docker-compose-pull.sh' + sh 'build/new-jenkins/docker-compose-pull-selenium.sh' + sh 'build/new-jenkins/docker-compose-build-up.sh' + sh 'build/new-jenkins/docker-compose-create-migrate-database.sh' + sh 'build/new-jenkins/rspec-flakey-spec-catcher.sh' + } + } + // Don't fail the build for timeouts + catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException e) { + if (!e.causes[0] instanceof org.jenkinsci.plugins.workflow.steps.TimeoutStepExecution.ExceededTimeout) { + throw e + } else { + echo "Not failing the build due to timeouts" + fsc_timeout = true + } + } + finally { + sh "mkdir -vp tmp/$prefix" + sh( + script: "docker cp \$(docker-compose ps -q web):/usr/src/app/tmp/fsc.out ./tmp/$prefix/fsc.out", + returnStatus: true + ) + archiveArtifacts(artifacts: "tmp/$prefix/fsc.out", allowEmptyArchive: true) + } +} + +def sendSlack(success) { + def color = success ? "good" : "danger" + def jobInfo = " | <$env.BUILD_URL|Jenkins>" + def message = "$jobInfo\n$changed_tests" + if (fsc_timeout) { + message = "Timeout Occurred!\n$message" + } + slackSend channel: '#flakey_spec_catcher_noisy', color: color, message: message +} + +pipeline { + agent { label 'canvas-docker' } + options { ansiColor('xterm') } + + environment { + COMPOSE_FILE = 'docker-compose.new-jenkins.yml:docker-compose.new-jenkins-selenium.yml:docker-compose.new-jenkins-flakey-spec-catcher.yml' + // there will be a node for every 10 tests + DISTRIBUTED_FACTOR = '10' + // this causes distribution to trigger at an offset + DISTRIBUTED_OFFSET = '5' + // if someone is trying to update more than these amount of tests, then just fail. + // that will be at DISTRIBUTED_CUT_OFF / DISTRIBUTED_FACTOR nodes. + DISTRIBUTED_CUT_OFF = '300' + } + + stages { + stage('Checkout and clean') { + steps { + timeout(time: 5) { + sh 'build/new-jenkins/docker-cleanup.sh' + sh 'rm -vrf ./tmp/' + } + } + } + + stage('Print Env Variables') { + steps { + sh 'build/new-jenkins/print-env-excluding-secrets.sh' + } + } + + stage("Compute Build Distribution") { + steps { + script { + computeTestCount() + computeDistributedCount() + echo "expected nodes to run on for $result_test_count tests: $result_node_count" + } + } + } + + stage("Run Flakey Spec Catcher") { + when { expression { result_test_count > 0 } } + steps { + script { + if (result_node_count <= 1) { + echo "running on this node" + executeFlakeySpecCatcher() + } + else { + echo "running on multiple nodes: $result_node_count" + def nodes = [:]; + for(int i = 0; i < result_node_count; i++) { + // make sure to create a new index variable so this value gets + // captured by the lambda for FSC_NODE_INDEX + def index = i + def node_number = (index).toString().padLeft(2, '0') + nodes["flakey set $node_number"] = { + withEnv(["FSC_NODE_TOTAL=$result_node_count", "FSC_NODE_INDEX=$index"]) { + node('canvas-docker') { + stage("Running Flakey Set $node_number") { + try { + sh 'rm -vrf ./tmp' + checkout scm + sh 'build/new-jenkins/docker-cleanup.sh' + executeFlakeySpecCatcher("node$node_number") + } + finally { + sh 'rm -vrf ./tmp' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' + } + } + } + } + } + } + parallel(nodes) + } + } + } + post { + success { + sendSlack(true) + } + failure { + sendSlack(false) + } + } + } + } + + post { + cleanup { + sh 'rm -vrf ./tmp/' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' + } + } +} diff --git a/Jenkinsfile.selenium.performance.chrome b/Jenkinsfile.selenium.performance.chrome index 048aae367b02b..53fb45a7b61be 100644 --- a/Jenkinsfile.selenium.performance.chrome +++ b/Jenkinsfile.selenium.performance.chrome @@ -26,56 +26,130 @@ pipeline { environment { COMPOSE_FILE = 'docker-compose.new-jenkins.yml:docker-compose.new-jenkins-selenium.yml' - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" - // for now 1 until we stabilize some of the flaky specs - RERUNS_RETRY = 1 + GERRIT_PORT = '29418' + GERRIT_URL = "$GERRIT_HOST:$GERRIT_PORT" MAX_FAIL = 5 + PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:master" + RERUNS_RETRY = 0 // no reruns } stages { - stage ('Selenium Performance Tests') { + stage ('Environment Variables') { + steps { + timeout(time: 2) { + sh 'build/new-jenkins/print-env-excluding-secrets.sh' + } + } + } + + stage ('Pre-Cleanup') { + steps { + timeout(time: 2) { + sh 'build/new-jenkins/docker-cleanup.sh' + } + } + } + + // Copied wholesale out of Jenkinsfile, this needs be abstracted if possible + stage('Checkout Plugins') { + steps { + timeout(time: 10) { + script { + def credentials = load 'build/new-jenkins/groovy/credentials.groovy' + credentials.fetchFromGerrit('gerrit_builder', '.', '', 'canvas-lms/config') + gems = readFile('gerrit_builder/canvas-lms/config/plugins_list').split() + println "Plugin list: ${gems}" + + /* fetch plugins */ + gems.each { gem -> + credentials.fetchFromGerrit(gem, 'gems/plugins') + } + credentials.fetchFromGerrit('qti_migration_tool', 'vendor', 'QTIMigrationTool') + + sh ''' + mv -v gerrit_builder/canvas-lms/config/* config/ + rm -v config/cache_store.yml + rmdir -p gerrit_builder/canvas-lms/config + cp -v docker-compose/config/selenium.yml config/ + cp -vR docker-compose/config/new-jenkins config/new-jenkins + cp -v config/delayed_jobs.yml.example config/delayed_jobs.yml + cp -v config/domain.yml.example config/domain.yml + cp -v config/external_migration.yml.example config/external_migration.yml + cp -v config/outgoing_mail.yml.example config/outgoing_mail.yml + ''' + } + } + } + } + + stage ('Setup Containers') { steps { - timeout(time: 30) { - checkout scm - sh 'printenv | sort' + timeout(time: 20) { sh 'build/new-jenkins/docker-compose-pull.sh' sh 'build/new-jenkins/docker-compose-pull-selenium.sh' sh 'build/new-jenkins/docker-compose-build-up.sh' + } + } + } + + stage ('Setup Databases') { + steps { + timeout(time: 5) { sh 'build/new-jenkins/docker-compose-create-migrate-database.sh' - sh 'build/new-jenkins/rspec-with-retries.sh performance' } } + } - post { - unsuccessful { - // copy spec failures to local - sh 'mkdir -p tmp' - sh 'docker cp $(docker-compose ps -q web):/usr/src/app/log/spec_failures/ ./tmp' - script { - def htmlFiles - // find all results files - dir ('tmp') { - htmlFiles = findFiles glob: '**/index.html' - } - // publish html - publishHTML target: [ - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: "tmp", - reportFiles: htmlFiles.join(','), - reportName: 'Test Failures' - ] - } + stage ('Selenium Performance Tests') { + steps { + timeout(time: 60) { + sh 'build/new-jenkins/rspec-with-retries.sh performance' } + } + } + } - cleanup { - sh 'rm -rf ./tmp/spec_failures/' - sh 'build/new-jenkins/docker-cleanup.sh' + post { + success { + slackSend( + channel: env.SLACK_CHANNEL, + color: 'good', + message: "[${env.JOB_NAME}] <${env.BUILD_URL}|${env.BUILD_DISPLAY_NAME}> was successful." + ) + } + + unsuccessful { + // copy spec failures to local + sh 'mkdir -vp tmp' + // [JIRA CCI-168]: need to handle web container never being spun up + sh 'docker cp $(docker-compose ps -q web):/usr/src/app/log/spec_failures/ ./tmp' + script { + def htmlFiles + // find all results files + dir ('tmp') { + htmlFiles = findFiles glob: '**/index.html' } + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: "tmp", + reportFiles: htmlFiles.join(','), + reportName: 'Test Failures' + ] } + + slackSend( + channel: env.SLACK_CHANNEL, + color: 'danger', + message: "[${env.JOB_NAME}] <${env.BUILD_URL}|${env.BUILD_DISPLAY_NAME}> was unsuccessful." + ) + } + + cleanup { + sh 'rm -vrf ./tmp/spec_failures/' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' } } } diff --git a/Jenkinsfile.test-subbuild b/Jenkinsfile.test-subbuild new file mode 100644 index 0000000000000..d829e9b6d4a6a --- /dev/null +++ b/Jenkinsfile.test-subbuild @@ -0,0 +1,39 @@ +#!/usr/bin/env groovy + +/* + * Copyright (C) 2019 - present Instructure, Inc. + * + * This file is part of Canvas. + * + * Canvas is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3 of the License. + * + * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + +def getImageTagVersion() { + def flags = load 'build/new-jenkins/groovy/commit-flags.groovy' + return flags.getImageTagVersion() +} + +pipeline { + agent { label 'canvas-docker' } + options { ansiColor('xterm') } + + stages { + stage('Print Env Variables') { + steps { + timeout(time: 20, unit: 'SECONDS') { + sh 'build/new-jenkins/print-env-excluding-secrets.sh' + } + } + } + } +} diff --git a/Jenkinsfile.vendored-gems b/Jenkinsfile.vendored-gems index 559003d76aa20..fa28e4c9a2f3b 100644 --- a/Jenkinsfile.vendored-gems +++ b/Jenkinsfile.vendored-gems @@ -18,60 +18,68 @@ * with this program. If not, see . */ +def getImageTagVersion() { + def flags = load 'build/new-jenkins/groovy/commit-flags.groovy' + return flags.getImageTagVersion() +} + pipeline { agent { label 'canvas-docker' } - options { - ansiColor('xterm') - } + options { ansiColor('xterm') } environment { COMPOSE_FILE = 'docker-compose.new-jenkins.yml' - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" } stages { - stage ('Run Vendored Gems specs') { + stage ('Setup') { steps { - timeout(time: 60) { - sh 'printenv | sort' + timeout(time: 5) { + sh 'build/new-jenkins/docker-cleanup.sh' + sh 'build/new-jenkins/print-env-excluding-secrets.sh' sh 'build/new-jenkins/docker-compose-pull.sh' sh 'build/new-jenkins/docker-compose-build-up.sh' sh 'build/new-jenkins/docker-compose-create-database.sh' - sh 'build/new-jenkins/test-gems.sh' } } + } - post { - always { - // copy the test results from the container - sh 'docker cp $(docker ps -f "ancestor"="$PATCHSET_TAG" -q):/usr/src/app/tmp/spec_results/. ./spec_results/' - script { - def htmlFiles - // find all results files - dir ('spec_results') { - sh 'mv report.html canvas_i18nliner_results.html' - htmlFiles = findFiles glob: '*results.html' - } - - // publish html - publishHTML target: [ - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: "spec_results", - reportFiles: htmlFiles.join(','), - reportName: 'Test Results' - ] - } + stage ('Run Vendored Gems specs') { + steps { + timeout(time: 10) { + sh 'build/new-jenkins/test-gems.sh' } + } + } + } - cleanup { - sh 'rm -rf ./spec_results/' - sh 'build/new-jenkins/docker-cleanup.sh' + post { + always { + // copy the test results from the container + sh 'docker cp $(docker ps -f "ancestor"="$PATCHSET_TAG" -q):/usr/src/app/tmp/spec_results/. ./spec_results/' + script { + def htmlFiles + // find all results files + dir ('spec_results') { + sh 'mv report.html canvas_i18nliner_results.html' + htmlFiles = findFiles glob: '*results.html' } + + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: "spec_results", + reportFiles: htmlFiles.join(','), + reportName: 'Test Results' + ] } } + + cleanup { + sh 'rm -vrf ./spec_results/' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' + } } } diff --git a/Jenkinsfile.xbrowser b/Jenkinsfile.xbrowser index a49386cce449f..af876c22f52de 100644 --- a/Jenkinsfile.xbrowser +++ b/Jenkinsfile.xbrowser @@ -19,59 +19,62 @@ */ pipeline { - agent none - options { - ansiColor('xterm') - } + agent { label 'canvas-docker' } + options { ansiColor('xterm') } environment { COMPOSE_FILE = 'docker-compose.new-jenkins.yml:docker-compose.new-jenkins-selenium.yml' - // 'refs/changes/63/181863/8' -> '63.181863.8' - NAME = "${env.GERRIT_REFSPEC}".minus('refs/changes/').replaceAll('/','.') - PATCHSET_TAG = "$DOCKER_REGISTRY_FQDN/jenkins/canvas-lms:$NAME" } - // Todo: This will run all xbrowser tests consecutively, still need to get parallel depending on runtime + + // TODO: This will run all xbrowser tests consecutively, still need to get parallel depending on runtime stages { - stage ('Xbrowser Tests') { - agent { label 'canvas-docker' } + stage ('Setup') { steps { - timeout(time: 60) { - sh 'printenv | sort' + timeout(time: 10) { + sh 'build/new-jenkins/docker-cleanup.sh' + sh 'build/new-jenkins/print-env-excluding-secrets.sh' sh 'build/new-jenkins/docker-compose-pull.sh' sh 'build/new-jenkins/docker-compose-pull-selenium.sh' sh 'build/new-jenkins/docker-compose-build-up.sh' sh 'build/new-jenkins/docker-compose-create-migrate-database.sh' - sh 'build/new-jenkins/xbrowser-test.sh' } } + } - post { - unsuccessful { - // copy spec failures to local - sh 'docker cp $(docker-compose ps -q web):/usr/src/app/log/spec_failures/ ./tmp' - script { - def htmlFiles - // find all results files - dir ('tmp') { - htmlFiles = findFiles glob: '**/index.html' - } - // publish html - publishHTML target: [ - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: "tmp", - reportFiles: htmlFiles.join(','), - reportName: 'Test Failures' - ] - } + stage ('Xbrowser Tests') { + steps { + timeout(time: 60) { + sh 'build/new-jenkins/xbrowser-test.sh' } + } + } + } - cleanup { - sh 'rm -rf ./tmp/spec_failures' - sh 'build/new-jenkins/docker-cleanup.sh' + post { + unsuccessful { + // copy spec failures to local + sh 'docker cp $(docker-compose ps -q web):/usr/src/app/log/spec_failures/ ./tmp' + script { + def htmlFiles + // find all results files + dir ('tmp') { + htmlFiles = findFiles glob: '**/index.html' } + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: "tmp", + reportFiles: htmlFiles.join(','), + reportName: 'Test Failures' + ] } } + + cleanup { + sh 'rm -vrf ./tmp/spec_failures' + sh 'build/new-jenkins/docker-cleanup.sh --allow-failure' + } } } diff --git a/Rakefile b/Rakefile index fb7aa50cc12eb..b9bec1ef5099d 100644 --- a/Rakefile +++ b/Rakefile @@ -8,9 +8,3 @@ require 'rake/testtask' Bundler.require(:i18n_tools) CanvasRails::Application.load_tasks - -if ENV['KNAPSACK_ENABLED'] == '1' && defined?(Knapsack) - require 'spec/support/knapsack_extensions' - Knapsack.load_tasks -end - diff --git a/app/coffeescripts/api/enrollmentTermsApi.js b/app/coffeescripts/api/enrollmentTermsApi.js index 84d58894b143b..6b40ea631ff68 100644 --- a/app/coffeescripts/api/enrollmentTermsApi.js +++ b/app/coffeescripts/api/enrollmentTermsApi.js @@ -16,7 +16,8 @@ // with this program. If not, see . import _ from 'underscore' -import Depaginate from 'jsx/shared/CheatDepaginator' + +import NaiveRequestDispatch from 'jsx/shared/network/NaiveRequestDispatch' const listUrl = () => ENV.ENROLLMENT_TERMS_URL @@ -39,11 +40,15 @@ const deserializeTerms = termGroups => ) export default { - list(terms) { + list() { return new Promise((resolve, reject) => { - Depaginate(listUrl()) + const dispatch = new NaiveRequestDispatch() + /* eslint-disable promise/catch-or-return */ + dispatch + .getDepaginated(listUrl()) .then(response => resolve(deserializeTerms(response))) .fail(error => reject(error)) + /* eslint-enable promise/catch-or-return */ }) } } diff --git a/app/coffeescripts/api/gradingPeriodSetsApi.js b/app/coffeescripts/api/gradingPeriodSetsApi.js index e2e86d1421200..b6cbdbb0c2098 100644 --- a/app/coffeescripts/api/gradingPeriodSetsApi.js +++ b/app/coffeescripts/api/gradingPeriodSetsApi.js @@ -17,12 +17,14 @@ import $ from 'jquery' import _ from 'underscore' +import axios from 'axios' + +import 'jquery.instructure_misc_helpers' import I18n from 'i18n!gradingPeriodSetsApi' + import DateHelper from 'jsx/shared/helpers/dateHelper' +import NaiveRequestDispatch from 'jsx/shared/network/NaiveRequestDispatch' import gradingPeriodsApi from './gradingPeriodsApi' -import axios from 'axios' -import Depaginate from 'jsx/shared/CheatDepaginator' -import 'jquery.instructure_misc_helpers' const listUrl = () => ENV.GRADING_PERIOD_SETS_URL @@ -76,11 +78,15 @@ export default { deserializeSet, list() { - return new Promise((resolve, reject) => - Depaginate(listUrl()) + return new Promise((resolve, reject) => { + const dispatch = new NaiveRequestDispatch() + /* eslint-disable promise/catch-or-return */ + dispatch + .getDepaginated(listUrl()) .then(response => resolve(deserializeSets(response))) .fail(error => reject(error)) - ) + /* eslint-enable promise/catch-or-return */ + }) }, create(set) { diff --git a/app/coffeescripts/behaviors/SyllabusBehaviors.js b/app/coffeescripts/behaviors/SyllabusBehaviors.js index fbae755df1fa7..e7a4d5f54c874 100644 --- a/app/coffeescripts/behaviors/SyllabusBehaviors.js +++ b/app/coffeescripts/behaviors/SyllabusBehaviors.js @@ -226,7 +226,7 @@ function bindToMiniCalendar() { } // Binds to edit syllabus dom events -const bindToEditSyllabus = function() { +const bindToEditSyllabus = function(course_summary_enabled) { const $course_syllabus = $('#course_syllabus') $course_syllabus.data('syllabus_body', ENV.SYLLABUS_BODY) const $edit_syllabus_link = $('.edit_syllabus_link') @@ -331,6 +331,9 @@ const bindToEditSyllabus = function() { }, success(data) { + if (data.course.settings.syllabus_course_summary !== course_summary_enabled) { + return window.location.reload() + } /* xsslint safeString.property syllabus_body */ diff --git a/app/coffeescripts/calendar/CalendarEvent.js b/app/coffeescripts/calendar/CalendarEvent.js index 38827f31890f3..f0d7014b71274 100644 --- a/app/coffeescripts/calendar/CalendarEvent.js +++ b/app/coffeescripts/calendar/CalendarEvent.js @@ -21,10 +21,12 @@ import {Spinner} from '@instructure/ui-elements' import $ from 'jquery' import _ from 'underscore' import Backbone from 'Backbone' -import splitAssetString from '../str/splitAssetString' -import Depaginate from 'jsx/shared/CheatDepaginator' + import I18n from 'i18n!calendar.edit' +import NaiveRequestDispatch from 'jsx/shared/network/NaiveRequestDispatch' +import splitAssetString from '../str/splitAssetString' + export default class CalendarEvent extends Backbone.Model { urlRoot = '/api/v1/calendar_events/' @@ -80,7 +82,8 @@ export default class CalendarEvent extends Backbone.Model { } if (this.get('sections_url')) { - sectionsDfd = Depaginate(this.get('sections_url')) + const dispatch = new NaiveRequestDispatch() + sectionsDfd = dispatch.getDepaginated(this.get('sections_url')) } const combinedSuccess = (syncArgs = [], sectionsResp = []) => { diff --git a/app/coffeescripts/calendar/CommonEvent.CalendarEvent.js b/app/coffeescripts/calendar/CommonEvent.CalendarEvent.js index d22507497aef1..6572746ce9c9e 100644 --- a/app/coffeescripts/calendar/CommonEvent.CalendarEvent.js +++ b/app/coffeescripts/calendar/CommonEvent.CalendarEvent.js @@ -185,7 +185,14 @@ Object.assign(CalendarEvent.prototype, { } const names = ((this.calendarEvent && this.calendarEvent.child_events) || []).map( - child_event => child_event.user && child_event.user.sortable_name + child_event => { + if (child_event.user) { + return child_event.user.sortable_name + } else if (child_event.group) { + return child_event.group.name + } + return null + } ) let sorted = names.sort((a, b) => natcompare.strings(a, b)) diff --git a/app/coffeescripts/calendar/EditCalendarEventDetails.js b/app/coffeescripts/calendar/EditCalendarEventDetails.js index 406b26797f662..167ee6ec2b55b 100644 --- a/app/coffeescripts/calendar/EditCalendarEventDetails.js +++ b/app/coffeescripts/calendar/EditCalendarEventDetails.js @@ -43,7 +43,8 @@ export default class EditCalendarEventDetails { contexts: this.event.possibleContexts(), lockedTitle: this.event.lockedTitle, location_name: this.event.location_name, - date: this.event.startDate() + date: this.event.startDate(), + is_child: this.event.object.parent_event_id != null }) ) $(selector).append(this.$form) @@ -196,7 +197,7 @@ export default class EditCalendarEventDetails { // set them up as appropriate variants of datetime_field $date.date_field({ - datepicker: {dateFormat: datePickerFormat(I18n.t('#date.formats.medium_with_weekday'))} + datepicker: {dateFormat: datePickerFormat(I18n.t('#date.formats.default'))} }) $start.time_field() $end.time_field() diff --git a/app/coffeescripts/calendar/EditEventDetailsDialog.js b/app/coffeescripts/calendar/EditEventDetailsDialog.js index 9fe1df32ea84d..bab4214b69e10 100644 --- a/app/coffeescripts/calendar/EditEventDetailsDialog.js +++ b/app/coffeescripts/calendar/EditEventDetailsDialog.js @@ -50,7 +50,7 @@ export default class EditEventDetailsDialog { return this.event.possibleContexts().find(context => context.asset_string === code) } - setupTabs = () => { + setupTabs = async () => { // Set up the tabbed view of the dialog const tabs = dialog.find('#edit_event_tabs') @@ -102,8 +102,28 @@ export default class EditEventDetailsDialog { // to-do pages / discussions cannot be created on the calendar tabs.tabs('remove', 3) - // don't show To Do tab if the planner isn't enabled - if (!ENV.STUDENT_PLANNER_ENABLED) tabs.tabs('remove', 2) + // don't show To Do tab if the planner isn't enabled or a user + // managed calendar isn't selected + const managedContexts = ENV.CALENDAR.MANAGE_CONTEXTS ? ENV.CALENDAR.MANAGE_CONTEXTS : [] + + const selectedContexts = [] + const resp = await $.ajaxJSON('/api/v1/calendar_events/visible_contexts', 'GET') + resp.contexts.forEach(context => { + if (context.selected) selectedContexts.push(context.asset_string) + }) + + let shouldRenderTODO = false + for (let i = 0; i < selectedContexts.length; i++) { + for (let j = 0; j < managedContexts.length; j++) { + shouldRenderTODO = selectedContexts[i] === managedContexts[j] + if (shouldRenderTODO) break + } + if (shouldRenderTODO) break + } + + if (!ENV.STUDENT_PLANNER_ENABLED || !shouldRenderTODO) { + tabs.tabs('remove', 2) + } // don't even show the assignments tab if the user doesn't have // permission to create them @@ -147,7 +167,7 @@ export default class EditEventDetailsDialog { return false } - show = () => { + show = async () => { if (this.event.isAppointmentGroupEvent()) { return new EditApptCalendarEventDialog(this.event).show() } else { @@ -216,7 +236,7 @@ export default class EditEventDetailsDialog { .data('form-widget', this.appointmentGroupDetailsForm) } - this.setupTabs() + await this.setupTabs() // TODO: select the tab that should be active diff --git a/app/coffeescripts/calendar/EditEventView.js b/app/coffeescripts/calendar/EditEventView.js index 486988707c4b8..8a66426ece8b3 100644 --- a/app/coffeescripts/calendar/EditEventView.js +++ b/app/coffeescripts/calendar/EditEventView.js @@ -64,12 +64,12 @@ export default class EditCalendarEventView extends Backbone.View { if (picked_params.start_at) { picked_params.start_date = tz.format( $.fudgeDateForProfileTimezone(picked_params.start_at), - 'date.formats.medium_with_weekday' + 'date.formats.default' ) } else { picked_params.start_date = tz.format( $.fudgeDateForProfileTimezone(picked_params.start_date), - 'date.formats.medium_with_weekday' + 'date.formats.default' ) } @@ -112,7 +112,7 @@ export default class EditCalendarEventView extends Backbone.View { render() { super.render(...arguments) this.$('.date_field').date_field({ - datepicker: {dateFormat: datePickerFormat(I18n.t('#date.formats.medium_with_weekday'))} + datepicker: {dateFormat: datePickerFormat(I18n.t('#date.formats.default'))} }) this.$('.time_field').time_field() this.$('.date_start_end_row').each((_, row) => { diff --git a/app/coffeescripts/calendar/EditPlannerNoteDetails.js b/app/coffeescripts/calendar/EditPlannerNoteDetails.js index 664df31c6dcf0..89f0422dec141 100644 --- a/app/coffeescripts/calendar/EditPlannerNoteDetails.js +++ b/app/coffeescripts/calendar/EditPlannerNoteDetails.js @@ -144,7 +144,7 @@ export default class EditPlannerNoteDetails extends ValidatedFormView { // set them up as appropriate variants of datetime_field $date.datetime_field({ datepicker: { - dateFormat: datePickerFormat(I18n.t('#date.formats.medium_with_weekday')) + dateFormat: datePickerFormat(I18n.t('#date.formats.default')) }, dateOnly: true }) diff --git a/app/coffeescripts/calendar/EditToDoItemDetails.js b/app/coffeescripts/calendar/EditToDoItemDetails.js index da431acbaa1a6..3983e19fe6718 100644 --- a/app/coffeescripts/calendar/EditToDoItemDetails.js +++ b/app/coffeescripts/calendar/EditToDoItemDetails.js @@ -81,7 +81,7 @@ export default class EditToDoItemDetails extends ValidatedFormView { // set them up as appropriate variants of datetime_field $date.datetime_field({ datepicker: { - dateFormat: datePickerFormat(I18n.t('#date.formats.medium_with_weekday')) + dateFormat: datePickerFormat(I18n.t('#date.formats.default')) }, dateOnly: true }) diff --git a/app/coffeescripts/calendar/MessageParticipantsDialog.js b/app/coffeescripts/calendar/MessageParticipantsDialog.js index 86d8c0521d72d..a6add412c80a2 100644 --- a/app/coffeescripts/calendar/MessageParticipantsDialog.js +++ b/app/coffeescripts/calendar/MessageParticipantsDialog.js @@ -132,7 +132,7 @@ export default class MessageParticipantsDialog { if (this.group) { data.tags = this.group.context_codes } else if (this.opts.timeslot) { - data.context_code = this.opts.timeslot.context_code + data.tags = this.opts.timeslot.all_context_codes.split(',') } const deferred = $.ajaxJSON( @@ -147,12 +147,12 @@ export default class MessageParticipantsDialog { }) } - messageSent = data => { + messageSent = () => { this.$form.dialog('close') $.flashMessage(I18n.t('messages_sent', 'Messages Sent')) } - messageFailed = data => { + messageFailed = () => { this.$form .find('.error') .text( diff --git a/app/coffeescripts/calendar/TimeBlockRow.js b/app/coffeescripts/calendar/TimeBlockRow.js index a4e287b6620ca..198c5e0029219 100644 --- a/app/coffeescripts/calendar/TimeBlockRow.js +++ b/app/coffeescripts/calendar/TimeBlockRow.js @@ -45,7 +45,7 @@ export default class TimeBlockRow { this.$end_time = this.$row.find("input[name='end_time']") const $date_field = this.$date.date_field({ - datepicker: {dateFormat: datePickerFormat(I18n.t('#date.formats.medium_with_weekday'))} + datepicker: {dateFormat: datePickerFormat(I18n.t('#date.formats.default'))} }) $date_field.change(this.validate) this.$start_time.time_field().change(this.validate) diff --git a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.js b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.js index 03aec906a98d8..4d4afcc1cc027 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.js +++ b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.js @@ -38,8 +38,8 @@ import SubmissionStateMap from 'jsx/gradebook/SubmissionStateMap' import GradeOverrideEntry from '../../../../jsx/grading/GradeEntry/GradeOverrideEntry' import GradingPeriodsApi from '../../../api/gradingPeriodsApi' import GradingPeriodSetsApi from '../../../api/gradingPeriodSetsApi' -import GradebookSelector from 'jsx/gradezilla/individual-gradebook/components/GradebookSelector' -import {updateFinalGradeOverride} from '../../../../jsx/gradezilla/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi' +import GradebookSelector from 'jsx/gradebook/individual-gradebook/components/GradebookSelector' +import {updateFinalGradeOverride} from '../../../../jsx/gradebook/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi' import 'jquery.instructure_date_and_time' import 'vendor/jquery.ba-tinypubsub' @@ -50,8 +50,6 @@ const {get, set, setProperties} = Ember // http://emberjs.com/api/classes/Ember.ArrayController.html // http://emberjs.com/api/classes/Ember.ObjectController.html -const gradingPeriodIsClosed = gradingPeriod => new Date(gradingPeriod.close_date) < new Date() - function studentsUniqByEnrollments(...args) { let hiddenNameCounter = 1 const options = { @@ -113,8 +111,8 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({ if ( currentProgress && - (currentProgress.progress.workflow_state !== 'completed' && - currentProgress.progress.workflow_state !== 'failed') + currentProgress.progress.workflow_state !== 'completed' && + currentProgress.progress.workflow_state !== 'failed' ) { const attachmentProgress = { progress_id: currentProgress.progress.id, @@ -212,8 +210,6 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({ hideOutcomes: (() => !get(window, 'ENV.GRADEBOOK_OPTIONS.outcome_gradebook_enabled')).property(), - gradezilla: (() => get(window, 'ENV.GRADEBOOK_OPTIONS.gradezilla')).property(), - showDownloadSubmissionsButton: function() { const hasSubmittedSubmissions = this.get('selectedAssignment.has_submitted_submissions') const whitelist = ['online_upload', 'online_text_entry', 'online_url'] @@ -330,9 +326,10 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({ $('#gradebook-export').prop('disabled', true) $('#last-exported-gradebook').hide() - return $.ajaxJSON(ENV.GRADEBOOK_OPTIONS.export_gradebook_csv_url, 'GET').then( - attachment_progress => this.pollGradebookCsvProgress(attachment_progress) - ) + return $.ajaxJSON( + ENV.GRADEBOOK_OPTIONS.export_gradebook_csv_url, + 'GET' + ).then(attachment_progress => this.pollGradebookCsvProgress(attachment_progress)) }, gradeUpdated(submissions) { @@ -462,9 +459,6 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({ }.on('init'), renderGradebookMenu: function() { - if (!this.get('gradezilla')) { - return - } const mountPoint = document.querySelector('[data-component="GradebookSelector"]') if (!mountPoint) { return diff --git a/app/coffeescripts/ember/screenreader_gradebook/main.js b/app/coffeescripts/ember/screenreader_gradebook/main.js index 2b15a100559cd..8bd137b6dbca7 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/main.js +++ b/app/coffeescripts/ember/screenreader_gradebook/main.js @@ -46,9 +46,8 @@ import './templates/content_selection/header' import './templates/content_selection/outcome' import './templates/content_selection/selection_buttons' import './templates/content_selection/student' -import './templates/gradezillaHeader' +import './templates/gradebookHeader' import './templates/grading' -import './templates/header' import './templates/learning_mastery' import './templates/outcome_information' import './templates/screenreader_gradebook' diff --git a/app/coffeescripts/ember/screenreader_gradebook/routes/screenreader_gradebook_route.js b/app/coffeescripts/ember/screenreader_gradebook/routes/screenreader_gradebook_route.js index fd0f4d4e8e6c3..b42ab24f5f60f 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/routes/screenreader_gradebook_route.js +++ b/app/coffeescripts/ember/screenreader_gradebook/routes/screenreader_gradebook_route.js @@ -18,7 +18,7 @@ import {ArrayProxy, ObjectProxy, Route} from 'ember' import _ from 'underscore' import fetchAllPages from '../../shared/xhr/fetch_all_pages' -import {getFinalGradeOverrides} from 'jsx/gradezilla/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi' +import {getFinalGradeOverrides} from 'jsx/gradebook/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi' const ScreenreaderGradebookRoute = Route.extend({ model() { diff --git a/app/coffeescripts/ember/screenreader_gradebook/templates/gradezillaHeader.hbs b/app/coffeescripts/ember/screenreader_gradebook/templates/gradebookHeader.hbs similarity index 100% rename from app/coffeescripts/ember/screenreader_gradebook/templates/gradezillaHeader.hbs rename to app/coffeescripts/ember/screenreader_gradebook/templates/gradebookHeader.hbs diff --git a/app/coffeescripts/ember/screenreader_gradebook/templates/header.hbs b/app/coffeescripts/ember/screenreader_gradebook/templates/header.hbs deleted file mode 100644 index 6f64739bdb1d0..0000000000000 --- a/app/coffeescripts/ember/screenreader_gradebook/templates/header.hbs +++ /dev/null @@ -1,27 +0,0 @@ -{{! -Copyright (C) 2014 - 2017 Instructure, Inc. - -This file is part of Canvas. - -Canvas is free software: you can redistribute it and/or modify it under -the terms of the GNU Affero General Public License as published by the Free -Software Foundation, version 3 of the License. - -Canvas is distributed in the hope that it will be useful, but WITHOUT ANY -WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -A PARTICULAR PURPOSE. See the GNU Affero General Public License for more -details. - -You should have received a copy of the GNU Affero General Public License along -with this program. If not, see . -}} - -
-
-

{{#t 'gradebook_individual_view'}}Gradebook: Individual View{{/t}}

- {{#t 'save_instructions'}}Note: Grades and notes will be saved automatically after moving out of the field.{{/t}} -

- {{#t "switch_to_gradebook"}}Switch to Default Gradebook{{/t}} -

-
-
diff --git a/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs b/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs index 5b4ba7e95ba74..5a81f85bd41eb 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs +++ b/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs @@ -16,18 +16,10 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . }} -{{#if gradezilla}} - {{partial "gradezillaHeader"}} -{{else}} - {{partial "header"}} -{{/if}} +{{partial "gradebookHeader"}} {{#ic-tabs}} - {{#if gradezilla}}