├── .annotaterb.yml ├── .env.sample ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── fastruby-logo.png │ │ └── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ └── favicon.ico │ └── stylesheets │ │ ├── application.scss │ │ ├── pages │ │ ├── _gemmies-index.scss │ │ └── _lockfiles-new.scss │ │ └── partials │ │ ├── _basics.scss │ │ ├── _compat-cell.scss │ │ ├── _compat-table.scss │ │ └── _email-notification.scss ├── controllers │ ├── api │ │ ├── base_controller.rb │ │ ├── github_notifications_controller.rb │ │ ├── releases_controller.rb │ │ └── results_controller.rb │ ├── application_controller.rb │ ├── email_notifications_controller.rb │ ├── gemmies_controller.rb │ ├── lockfiles_controller.rb │ ├── rails_releases_controller.rb │ ├── sitemaps_controller.rb │ └── static_controller.rb ├── helpers │ ├── application_helper.rb │ ├── lockfiles_helper.rb │ └── rails_releases_helper.rb ├── javascript │ ├── application.js │ └── controllers │ │ └── compat_table_controller.js ├── mailers │ └── application_mailer.rb ├── models │ ├── api_key.rb │ ├── application_record.rb │ ├── compat.rb │ ├── concerns │ │ └── has_timestamps.rb │ ├── current.rb │ ├── email_notification.rb │ ├── gemmy.rb │ ├── github_notification.rb │ ├── inaccessible_gemmy.rb │ ├── inconclusive_compat.rb │ ├── lockfile.rb │ ├── lockfile_dependency.rb │ └── rails_release.rb ├── services │ ├── check_git_branches.rb │ ├── compats │ │ ├── check.rb │ │ ├── check_unchecked.rb │ │ └── checks │ │ │ ├── base.rb │ │ │ ├── bundler_github_check.rb │ │ │ ├── dependency_subsets_check.rb │ │ │ ├── empty_dependencies_check.rb │ │ │ └── rails_gems_check.rb │ ├── email_notifications │ │ └── send_all.rb │ ├── external │ │ └── github.rb │ ├── gemmies │ │ ├── create.rb │ │ ├── process.rb │ │ ├── update_compats.rb │ │ └── update_dependencies_and_versions.rb │ ├── github_notifications │ │ └── process.rb │ ├── lockfiles │ │ └── create.rb │ ├── maintenance │ │ └── hourly.rb │ ├── rails_releases │ │ ├── create.rb │ │ └── process.rb │ └── report_error.rb └── views │ ├── email_notifications │ ├── _form.html.haml │ └── create.turbo_stream.haml │ ├── gemmies │ ├── compat_table.html.haml │ ├── index.html.haml │ ├── new.html.haml │ └── show.html.haml │ ├── layouts │ ├── application.html.haml │ └── mailer.text.erb │ ├── lockfiles │ ├── new.html.haml │ └── show.html.haml │ ├── mailers │ └── email_notification.text.erb │ ├── rails_releases │ └── show.html.haml │ ├── shared │ ├── _compat_cell.html.haml │ ├── _favicons.html.haml │ ├── _flashes.html.haml │ ├── _header.html.haml │ └── _loading.html.haml │ └── static │ └── robots.txt.erb ├── bin ├── build ├── bundle ├── deploy ├── dev ├── importmap ├── prod-console ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml.sample ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── importmap.rb ├── initializers │ ├── assets.rb │ ├── content_security_policy.rb │ ├── filter_parameter_logging.rb │ ├── gems.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── permissions_policy.rb │ ├── redis.rb │ └── sentry.rb ├── locales │ └── en.yml ├── puma.rb ├── redis │ └── shared.yml ├── routes.rb ├── sitemap.rb └── storage.yml ├── db ├── migrate │ ├── 20191222065024_create_gemmies.rb │ ├── 20191222065443_create_rails_releases.rb │ ├── 20191222070102_create_compats.rb │ ├── 20200125221350_create_lockfiles.rb │ ├── 20200125222830_create_lockfile_dependencies.rb │ ├── 20200201222813_create_github_notifications.rb │ ├── 20240911015959_add_missing_sequences.rb │ ├── 20240927154305_create_api_keys.rb │ ├── 20240930143554_add_columns_to_rails_releases_table.rb │ └── 20241125191510_create_inaccessible_gemmies.rb ├── schema.rb └── seeds.rb ├── fastruby-logo.png ├── lib └── tasks │ └── data.rake ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png └── favicon.ico ├── spec ├── cassettes │ ├── GemmiesController │ │ └── create │ │ │ └── when_the_gemmy_params_are_valid │ │ │ ├── creates_a_record_in_the_database.yml │ │ │ └── redirects_to_the_new_gemmy_page.yml │ ├── Gemmies_Process │ │ └── _call │ │ │ ├── calls_UpdateCompats_with_the_gemmy.yml │ │ │ ├── enqueues_Compats_CheckUnchecked_job.yml │ │ │ └── updates_dependencies_for_the_gemmy.yml │ ├── Gemmies_UpdateCompats │ │ └── _call │ │ │ └── creates_a_compat_for_each_rails_release_and_dependency.yml │ └── LockfilesController │ │ ├── GET_show │ │ └── returns_a_success_response.yml │ │ └── POST_create │ │ └── with_valid_params │ │ ├── and_Gemfile_lock_content_has_local_paths │ │ ├── creates_a_new_Lockfile.yml │ │ └── creates_n_inaccessible_gemmies_in_the_database.yml │ │ ├── and_Gemfile_lock_content_was_already_submitted_in_the_past │ │ └── redirects_to_the_existing_lockfile.yml │ │ └── creates_a_new_Lockfile.yml ├── controllers │ ├── api │ │ └── results_controller_spec.rb │ ├── gemmies_controllers_spec.rb │ └── lockfiles_controller_spec.rb ├── factories │ ├── api_key_factory.rb │ ├── compat_factory.rb │ ├── dependency_factory.rb │ ├── gemmy_factory.rb │ ├── lockfile_factory.rb │ └── rails_release_factory.rb ├── fixtures │ ├── Gemfile.local.lock │ └── Gemfile.lock ├── models │ ├── api_key_spec.rb │ └── rails_release_spec.rb ├── rails_helper.rb ├── services │ ├── compats │ │ ├── check_spec.rb │ │ ├── check_unchecked_spec.rb │ │ └── checks │ │ │ └── empty_dependencies_check_spec.rb │ └── gemmies │ │ ├── process_spec.rb │ │ └── update_compats_spec.rb └── spec_helper.rb ├── stuff.rb └── yarn.lock /.annotaterb.yml: -------------------------------------------------------------------------------- 1 | position: after 2 | show_foreign_keys: false 3 | show_indexes: false 4 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | AWS_ACCESS_KEY_ID=123 2 | AWS_SECRET_ACCESS_KEY=456 3 | AWS_REGION=us-east-1 4 | FOG_URL=https://s3.us-east-1.amazonaws.com/railsbump.org 5 | HOST=localhost 6 | REDIS_HOST=localhost:6379 7 | GITHUB_ACCESS_TOKEN=foo123 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | db/schema.rb linguist-generated 2 | vendor/* linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: fastruby 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | concurrency: deploy 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - deploy 9 | 10 | jobs: 11 | 12 | deploy: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: hatchboxio/github-hatchbox-deploy-action@v2 17 | with: 18 | deploy_key: ${{ secrets.HATCHBOX_DEPLOY_KEY }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'releases/*' 8 | pull_request: 9 | branches: 10 | - main 11 | - 'releases/*' 12 | 13 | env: 14 | POSTGRES_USER: postgres 15 | POSTGRES_PASSWORD: postgres 16 | POSTGRES_HOST: localhost 17 | HOST: localhost 18 | REDIS_HOST: localhost:6379 19 | DATABASE_URL: postgres://postgres:postgres@localhost:5432/rails_bump_test 20 | COVERAGE: 'true' 21 | 22 | jobs: 23 | test: 24 | runs-on: ubuntu-latest 25 | services: 26 | postgres: 27 | image: postgres:latest 28 | ports: 29 | - 5432:5432 30 | env: 31 | POSTGRES_USER: postgres 32 | POSTGRES_PASSWORD: postgres 33 | options: >- 34 | --health-cmd="pg_isready -U postgres" 35 | --health-interval=10s 36 | --health-timeout=5s 37 | --health-retries=5 38 | 39 | redis: 40 | image: redis:latest 41 | ports: 42 | - 6379:6379 43 | options: >- 44 | --health-cmd="redis-cli ping" 45 | --health-interval=10s 46 | --health-timeout=5s 47 | --health-retries=5 48 | 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v4 52 | 53 | - name: Install PostgreSQL client 54 | run: | 55 | sudo apt-get -yqq install libpq-dev redis-tools 56 | 57 | - name: Set up Ruby 58 | uses: ruby/setup-ruby@v1 59 | 60 | - name: Install dependencies 61 | run: bundle install 62 | 63 | - name: Wait for PostgreSQL 64 | run: | 65 | echo "Waiting for PostgreSQL to be ready..." 66 | while ! pg_isready -h localhost -p 5432 -U postgres; do 67 | sleep 1 68 | done 69 | 70 | - name: Wait for Redis 71 | run: | 72 | echo "Waiting for Redis to be ready..." 73 | while ! redis-cli ping; do 74 | sleep 1 75 | done 76 | 77 | - name: Set up the database 78 | run: | 79 | cp config/database.yml.sample config/database.yml 80 | bin/rails db:create 81 | bin/rails db:schema:load 82 | bin/rails db:test:prepare 83 | 84 | - name: Run tests 85 | env: 86 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 87 | run: COVERAGE=true bundle exec rspec spec -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .byebug_history 2 | 3 | .DS_Store 4 | 5 | .env 6 | 7 | .rspec 8 | 9 | .vscode 10 | 11 | config/database.yml 12 | 13 | coverage 14 | 15 | node_modules 16 | 17 | log 18 | 19 | storage 20 | 21 | tmp 22 | 23 | public/sitemap.xml.gz 24 | 25 | app/assets/builds 26 | public/assets 27 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.3 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby file: ".ruby-version" 4 | 5 | # gem "activerecord-enhancedsqlite3-adapter", "~> 0.5" 6 | gem "amazing_print", "~> 1.5" 7 | gem "aws-sdk-s3", "~> 1.8", require: false 8 | gem "baseline", github: "fastruby/baseline" 9 | gem "bootsnap", "~> 1.17", require: false 10 | gem "bootstrap", "~> 5.3.3" 11 | gem "dartsass-rails", "~> 0.5" 12 | gem "dotenv", "~> 3.1.2" 13 | gem "fog-aws" 14 | gem "gems", github: "rubygems/gems" # TODO: use released version when > 1.2.0 is released 15 | gem "git", "~> 2.1" 16 | gem "haml", "~> 6.0" 17 | gem "importmap-rails", "~> 2.0" 18 | gem "kredis", "~> 1.2" 19 | gem "net-pop", github: "ruby/net-pop" 20 | gem "octokit", "~> 9.1" 21 | gem "octopoller", "~> 0.3" 22 | gem "propshaft", "~> 0.8" 23 | gem "pry-rails", "~> 0.3" 24 | gem "puma", "~> 6.4" 25 | gem "rails_bootstrap_navbar", "~> 3.0" 26 | gem "rails-i18n", "~> 7.0" 27 | gem "rails", "~> 7.1.3.4" 28 | gem "redis-namespace", "~> 1.11" 29 | gem "redis", "~> 5.0" 30 | gem "sentry-rails", "~> 5.5" 31 | gem "sentry-sidekiq", "~> 5.5" 32 | gem "sidekiq-scheduler", "~> 5.0" 33 | gem "sidekiq", "~> 7.2" 34 | gem "sitemap_generator", "~> 6.3" 35 | gem "stimulus-rails", "~> 1.3" 36 | gem "turbo-rails", "~> 2.0" 37 | 38 | group :development do 39 | gem "annotaterb", "~> 4.4", require: false 40 | gem "better_errors", "~> 2.8" 41 | gem "binding_of_caller", "~> 1.0" 42 | gem "rails-erd", "~> 1.7.2" 43 | end 44 | 45 | # Run against this stable release 46 | group :development, :test do 47 | gem "byebug", "~> 11.1" 48 | gem "codecov", require: false 49 | gem "database_cleaner-active_record" 50 | gem "factory_bot_rails", "~> 6.2" 51 | gem "rspec-rails", "~> 6.1.0" 52 | gem "rails-controller-testing" 53 | gem "simplecov", "~> 0.22", require: false 54 | gem "simplecov-console", require: false 55 | gem "vcr" 56 | gem "webmock" 57 | end 58 | 59 | gem "pg" 60 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/fastruby/baseline.git 3 | revision: 60a5488d068233c1b67a025bfed1deb5b156c25a 4 | specs: 5 | baseline (1.0) 6 | 7 | GIT 8 | remote: https://github.com/ruby/net-pop.git 9 | revision: e8d0afe2773b9eb6a23c39e9e437f6fc0fc7c733 10 | specs: 11 | net-pop (0.1.2) 12 | 13 | GIT 14 | remote: https://github.com/rubygems/gems.git 15 | revision: c20c05888cb2b882bf3b99d3618cc37f69559f6a 16 | specs: 17 | gems (1.2.0) 18 | 19 | GEM 20 | remote: https://rubygems.org/ 21 | specs: 22 | actioncable (7.1.3.4) 23 | actionpack (= 7.1.3.4) 24 | activesupport (= 7.1.3.4) 25 | nio4r (~> 2.0) 26 | websocket-driver (>= 0.6.1) 27 | zeitwerk (~> 2.6) 28 | actionmailbox (7.1.3.4) 29 | actionpack (= 7.1.3.4) 30 | activejob (= 7.1.3.4) 31 | activerecord (= 7.1.3.4) 32 | activestorage (= 7.1.3.4) 33 | activesupport (= 7.1.3.4) 34 | mail (>= 2.7.1) 35 | net-imap 36 | net-pop 37 | net-smtp 38 | actionmailer (7.1.3.4) 39 | actionpack (= 7.1.3.4) 40 | actionview (= 7.1.3.4) 41 | activejob (= 7.1.3.4) 42 | activesupport (= 7.1.3.4) 43 | mail (~> 2.5, >= 2.5.4) 44 | net-imap 45 | net-pop 46 | net-smtp 47 | rails-dom-testing (~> 2.2) 48 | actionpack (7.1.3.4) 49 | actionview (= 7.1.3.4) 50 | activesupport (= 7.1.3.4) 51 | nokogiri (>= 1.8.5) 52 | racc 53 | rack (>= 2.2.4) 54 | rack-session (>= 1.0.1) 55 | rack-test (>= 0.6.3) 56 | rails-dom-testing (~> 2.2) 57 | rails-html-sanitizer (~> 1.6) 58 | actiontext (7.1.3.4) 59 | actionpack (= 7.1.3.4) 60 | activerecord (= 7.1.3.4) 61 | activestorage (= 7.1.3.4) 62 | activesupport (= 7.1.3.4) 63 | globalid (>= 0.6.0) 64 | nokogiri (>= 1.8.5) 65 | actionview (7.1.3.4) 66 | activesupport (= 7.1.3.4) 67 | builder (~> 3.1) 68 | erubi (~> 1.11) 69 | rails-dom-testing (~> 2.2) 70 | rails-html-sanitizer (~> 1.6) 71 | activejob (7.1.3.4) 72 | activesupport (= 7.1.3.4) 73 | globalid (>= 0.3.6) 74 | activemodel (7.1.3.4) 75 | activesupport (= 7.1.3.4) 76 | activerecord (7.1.3.4) 77 | activemodel (= 7.1.3.4) 78 | activesupport (= 7.1.3.4) 79 | timeout (>= 0.4.0) 80 | activestorage (7.1.3.4) 81 | actionpack (= 7.1.3.4) 82 | activejob (= 7.1.3.4) 83 | activerecord (= 7.1.3.4) 84 | activesupport (= 7.1.3.4) 85 | marcel (~> 1.0) 86 | activesupport (7.1.3.4) 87 | base64 88 | bigdecimal 89 | concurrent-ruby (~> 1.0, >= 1.0.2) 90 | connection_pool (>= 2.2.5) 91 | drb 92 | i18n (>= 1.6, < 2) 93 | minitest (>= 5.1) 94 | mutex_m 95 | tzinfo (~> 2.0) 96 | addressable (2.8.7) 97 | public_suffix (>= 2.0.2, < 7.0) 98 | amazing_print (1.6.0) 99 | annotaterb (4.10.2) 100 | ansi (1.5.0) 101 | autoprefixer-rails (10.4.16.0) 102 | execjs (~> 2) 103 | aws-eventstream (1.3.0) 104 | aws-partitions (1.957.0) 105 | aws-sdk-core (3.201.3) 106 | aws-eventstream (~> 1, >= 1.3.0) 107 | aws-partitions (~> 1, >= 1.651.0) 108 | aws-sigv4 (~> 1.8) 109 | jmespath (~> 1, >= 1.6.1) 110 | aws-sdk-kms (1.88.0) 111 | aws-sdk-core (~> 3, >= 3.201.0) 112 | aws-sigv4 (~> 1.5) 113 | aws-sdk-s3 (1.156.0) 114 | aws-sdk-core (~> 3, >= 3.201.0) 115 | aws-sdk-kms (~> 1) 116 | aws-sigv4 (~> 1.5) 117 | aws-sigv4 (1.9.0) 118 | aws-eventstream (~> 1, >= 1.0.2) 119 | base64 (0.2.0) 120 | better_errors (2.10.1) 121 | erubi (>= 1.0.0) 122 | rack (>= 0.9.0) 123 | rouge (>= 1.0.0) 124 | bigdecimal (3.1.8) 125 | binding_of_caller (1.0.1) 126 | debug_inspector (>= 1.2.0) 127 | bootsnap (1.18.3) 128 | msgpack (~> 1.2) 129 | bootstrap (5.3.3) 130 | autoprefixer-rails (>= 9.1.0) 131 | popper_js (>= 2.11.8, < 3) 132 | bootstrap-navbar (3.2.2) 133 | gem_config (~> 0.3) 134 | builder (3.3.0) 135 | byebug (11.1.3) 136 | choice (0.2.0) 137 | codecov (0.2.12) 138 | json 139 | simplecov 140 | coderay (1.1.3) 141 | concurrent-ruby (1.3.3) 142 | connection_pool (2.4.1) 143 | crack (1.0.0) 144 | bigdecimal 145 | rexml 146 | crass (1.0.6) 147 | dartsass-rails (0.5.0) 148 | railties (>= 6.0.0) 149 | sass-embedded (~> 1.63) 150 | database_cleaner-active_record (2.2.0) 151 | activerecord (>= 5.a) 152 | database_cleaner-core (~> 2.0.0) 153 | database_cleaner-core (2.0.1) 154 | date (3.3.4) 155 | debug_inspector (1.2.0) 156 | diff-lcs (1.5.1) 157 | docile (1.4.1) 158 | dotenv (3.1.2) 159 | drb (2.2.1) 160 | erubi (1.13.0) 161 | et-orbi (1.2.11) 162 | tzinfo 163 | excon (1.1.1) 164 | execjs (2.9.1) 165 | factory_bot (6.4.6) 166 | activesupport (>= 5.0.0) 167 | factory_bot_rails (6.4.3) 168 | factory_bot (~> 6.4) 169 | railties (>= 5.0.0) 170 | faraday (2.10.0) 171 | faraday-net_http (>= 2.0, < 3.2) 172 | logger 173 | faraday-net_http (3.1.0) 174 | net-http 175 | fog-aws (3.29.0) 176 | base64 (~> 0.2.0) 177 | fog-core (~> 2.6) 178 | fog-json (~> 1.1) 179 | fog-xml (~> 0.1) 180 | fog-core (2.6.0) 181 | builder 182 | excon (~> 1.0) 183 | formatador (>= 0.2, < 2.0) 184 | mime-types 185 | fog-json (1.2.0) 186 | fog-core 187 | multi_json (~> 1.10) 188 | fog-xml (0.1.4) 189 | fog-core 190 | nokogiri (>= 1.5.11, < 2.0.0) 191 | formatador (1.1.0) 192 | fugit (1.11.0) 193 | et-orbi (~> 1, >= 1.2.11) 194 | raabro (~> 1.4) 195 | gem_config (0.3.2) 196 | git (2.1.1) 197 | activesupport (>= 5.0) 198 | addressable (~> 2.8) 199 | process_executer (~> 1.1) 200 | rchardet (~> 1.8) 201 | globalid (1.2.1) 202 | activesupport (>= 6.1) 203 | google-protobuf (4.27.2-arm64-darwin) 204 | bigdecimal 205 | rake (>= 13) 206 | google-protobuf (4.27.2-x86_64-linux) 207 | bigdecimal 208 | rake (>= 13) 209 | haml (6.3.0) 210 | temple (>= 0.8.2) 211 | thor 212 | tilt 213 | hashdiff (1.1.1) 214 | i18n (1.14.5) 215 | concurrent-ruby (~> 1.0) 216 | importmap-rails (2.0.1) 217 | actionpack (>= 6.0.0) 218 | activesupport (>= 6.0.0) 219 | railties (>= 6.0.0) 220 | io-console (0.7.2) 221 | irb (1.14.0) 222 | rdoc (>= 4.0.0) 223 | reline (>= 0.4.2) 224 | jmespath (1.6.2) 225 | json (2.10.1) 226 | kredis (1.7.0) 227 | activemodel (>= 6.0.0) 228 | activesupport (>= 6.0.0) 229 | redis (>= 4.2, < 6) 230 | logger (1.6.0) 231 | loofah (2.22.0) 232 | crass (~> 1.0.2) 233 | nokogiri (>= 1.12.0) 234 | mail (2.8.1) 235 | mini_mime (>= 0.1.1) 236 | net-imap 237 | net-pop 238 | net-smtp 239 | marcel (1.0.4) 240 | method_source (1.1.0) 241 | mime-types (3.6.0) 242 | logger 243 | mime-types-data (~> 3.2015) 244 | mime-types-data (3.2024.1001) 245 | mini_mime (1.1.5) 246 | minitest (5.24.1) 247 | msgpack (1.7.2) 248 | multi_json (1.15.0) 249 | mutex_m (0.2.0) 250 | net-http (0.4.1) 251 | uri 252 | net-imap (0.4.14) 253 | date 254 | net-protocol 255 | net-protocol (0.2.2) 256 | timeout 257 | net-smtp (0.5.0) 258 | net-protocol 259 | nio4r (2.7.3) 260 | nokogiri (1.16.7-arm64-darwin) 261 | racc (~> 1.4) 262 | nokogiri (1.16.7-x86_64-linux) 263 | racc (~> 1.4) 264 | octokit (9.1.0) 265 | faraday (>= 1, < 3) 266 | sawyer (~> 0.9) 267 | octopoller (0.3.1) 268 | pg (1.5.6) 269 | popper_js (2.11.8) 270 | process_executer (1.1.0) 271 | propshaft (0.9.0) 272 | actionpack (>= 7.0.0) 273 | activesupport (>= 7.0.0) 274 | rack 275 | railties (>= 7.0.0) 276 | pry (0.14.2) 277 | coderay (~> 1.1) 278 | method_source (~> 1.0) 279 | pry-rails (0.3.11) 280 | pry (>= 0.13.0) 281 | psych (5.1.2) 282 | stringio 283 | public_suffix (6.0.1) 284 | puma (6.4.2) 285 | nio4r (~> 2.0) 286 | raabro (1.4.0) 287 | racc (1.8.0) 288 | rack (3.1.7) 289 | rack-session (2.0.0) 290 | rack (>= 3.0.0) 291 | rack-test (2.1.0) 292 | rack (>= 1.3) 293 | rackup (2.1.0) 294 | rack (>= 3) 295 | webrick (~> 1.8) 296 | rails (7.1.3.4) 297 | actioncable (= 7.1.3.4) 298 | actionmailbox (= 7.1.3.4) 299 | actionmailer (= 7.1.3.4) 300 | actionpack (= 7.1.3.4) 301 | actiontext (= 7.1.3.4) 302 | actionview (= 7.1.3.4) 303 | activejob (= 7.1.3.4) 304 | activemodel (= 7.1.3.4) 305 | activerecord (= 7.1.3.4) 306 | activestorage (= 7.1.3.4) 307 | activesupport (= 7.1.3.4) 308 | bundler (>= 1.15.0) 309 | railties (= 7.1.3.4) 310 | rails-controller-testing (1.0.5) 311 | actionpack (>= 5.0.1.rc1) 312 | actionview (>= 5.0.1.rc1) 313 | activesupport (>= 5.0.1.rc1) 314 | rails-dom-testing (2.2.0) 315 | activesupport (>= 5.0.0) 316 | minitest 317 | nokogiri (>= 1.6) 318 | rails-erd (1.7.2) 319 | activerecord (>= 4.2) 320 | activesupport (>= 4.2) 321 | choice (~> 0.2.0) 322 | ruby-graphviz (~> 1.2) 323 | rails-html-sanitizer (1.6.0) 324 | loofah (~> 2.21) 325 | nokogiri (~> 1.14) 326 | rails-i18n (7.0.9) 327 | i18n (>= 0.7, < 2) 328 | railties (>= 6.0.0, < 8) 329 | rails_bootstrap_navbar (3.0.0) 330 | bootstrap-navbar (~> 3.0) 331 | rails (>= 3.0.0) 332 | railties (7.1.3.4) 333 | actionpack (= 7.1.3.4) 334 | activesupport (= 7.1.3.4) 335 | irb 336 | rackup (>= 1.0.0) 337 | rake (>= 12.2) 338 | thor (~> 1.0, >= 1.2.2) 339 | zeitwerk (~> 2.6) 340 | rake (13.2.1) 341 | rchardet (1.8.0) 342 | rdoc (6.7.0) 343 | psych (>= 4.0.0) 344 | redis (5.2.0) 345 | redis-client (>= 0.22.0) 346 | redis-client (0.22.2) 347 | connection_pool 348 | redis-namespace (1.11.0) 349 | redis (>= 4) 350 | reline (0.5.9) 351 | io-console (~> 0.5) 352 | rexml (3.3.7) 353 | rouge (4.3.0) 354 | rspec-core (3.13.0) 355 | rspec-support (~> 3.13.0) 356 | rspec-expectations (3.13.1) 357 | diff-lcs (>= 1.2.0, < 2.0) 358 | rspec-support (~> 3.13.0) 359 | rspec-mocks (3.13.1) 360 | diff-lcs (>= 1.2.0, < 2.0) 361 | rspec-support (~> 3.13.0) 362 | rspec-rails (6.1.4) 363 | actionpack (>= 6.1) 364 | activesupport (>= 6.1) 365 | railties (>= 6.1) 366 | rspec-core (~> 3.13) 367 | rspec-expectations (~> 3.13) 368 | rspec-mocks (~> 3.13) 369 | rspec-support (~> 3.13) 370 | rspec-support (3.13.1) 371 | ruby-graphviz (1.2.5) 372 | rexml 373 | rufus-scheduler (3.9.1) 374 | fugit (~> 1.1, >= 1.1.6) 375 | sass-embedded (1.77.8-arm64-darwin) 376 | google-protobuf (~> 4.26) 377 | sass-embedded (1.77.8-x86_64-linux-gnu) 378 | google-protobuf (~> 4.26) 379 | sawyer (0.9.2) 380 | addressable (>= 2.3.5) 381 | faraday (>= 0.17.3, < 3) 382 | sentry-rails (5.18.2) 383 | railties (>= 5.0) 384 | sentry-ruby (~> 5.18.2) 385 | sentry-ruby (5.18.2) 386 | bigdecimal 387 | concurrent-ruby (~> 1.0, >= 1.0.2) 388 | sentry-sidekiq (5.18.2) 389 | sentry-ruby (~> 5.18.2) 390 | sidekiq (>= 3.0) 391 | sidekiq (7.3.0) 392 | concurrent-ruby (< 2) 393 | connection_pool (>= 2.3.0) 394 | logger 395 | rack (>= 2.2.4) 396 | redis-client (>= 0.22.2) 397 | sidekiq-scheduler (5.0.5) 398 | rufus-scheduler (~> 3.2) 399 | sidekiq (>= 6, < 8) 400 | tilt (>= 1.4.0, < 3) 401 | simplecov (0.22.0) 402 | docile (~> 1.1) 403 | simplecov-html (~> 0.11) 404 | simplecov_json_formatter (~> 0.1) 405 | simplecov-console (0.9.3) 406 | ansi 407 | simplecov 408 | terminal-table 409 | simplecov-html (0.13.1) 410 | simplecov_json_formatter (0.1.4) 411 | sitemap_generator (6.3.0) 412 | builder (~> 3.0) 413 | stimulus-rails (1.3.3) 414 | railties (>= 6.0.0) 415 | stringio (3.1.1) 416 | temple (0.10.3) 417 | terminal-table (4.0.0) 418 | unicode-display_width (>= 1.1.1, < 4) 419 | thor (1.3.1) 420 | tilt (2.4.0) 421 | timeout (0.4.1) 422 | turbo-rails (2.0.6) 423 | actionpack (>= 6.0.0) 424 | activejob (>= 6.0.0) 425 | railties (>= 6.0.0) 426 | tzinfo (2.0.6) 427 | concurrent-ruby (~> 1.0) 428 | unicode-display_width (3.1.4) 429 | unicode-emoji (~> 4.0, >= 4.0.4) 430 | unicode-emoji (4.0.4) 431 | uri (0.13.0) 432 | vcr (6.3.1) 433 | base64 434 | webmock (3.23.1) 435 | addressable (>= 2.8.0) 436 | crack (>= 0.3.2) 437 | hashdiff (>= 0.4.0, < 2.0.0) 438 | webrick (1.8.1) 439 | websocket-driver (0.7.6) 440 | websocket-extensions (>= 0.1.0) 441 | websocket-extensions (0.1.5) 442 | zeitwerk (2.6.17) 443 | 444 | PLATFORMS 445 | arm64-darwin-23 446 | x86_64-linux 447 | 448 | DEPENDENCIES 449 | amazing_print (~> 1.5) 450 | annotaterb (~> 4.4) 451 | aws-sdk-s3 (~> 1.8) 452 | baseline! 453 | better_errors (~> 2.8) 454 | binding_of_caller (~> 1.0) 455 | bootsnap (~> 1.17) 456 | bootstrap (~> 5.3.3) 457 | byebug (~> 11.1) 458 | codecov 459 | dartsass-rails (~> 0.5) 460 | database_cleaner-active_record 461 | dotenv (~> 3.1.2) 462 | factory_bot_rails (~> 6.2) 463 | fog-aws 464 | gems! 465 | git (~> 2.1) 466 | haml (~> 6.0) 467 | importmap-rails (~> 2.0) 468 | kredis (~> 1.2) 469 | net-pop! 470 | octokit (~> 9.1) 471 | octopoller (~> 0.3) 472 | pg 473 | propshaft (~> 0.8) 474 | pry-rails (~> 0.3) 475 | puma (~> 6.4) 476 | rails (~> 7.1.3.4) 477 | rails-controller-testing 478 | rails-erd (~> 1.7.2) 479 | rails-i18n (~> 7.0) 480 | rails_bootstrap_navbar (~> 3.0) 481 | redis (~> 5.0) 482 | redis-namespace (~> 1.11) 483 | rspec-rails (~> 6.1.0) 484 | sentry-rails (~> 5.5) 485 | sentry-sidekiq (~> 5.5) 486 | sidekiq (~> 7.2) 487 | sidekiq-scheduler (~> 5.0) 488 | simplecov (~> 0.22) 489 | simplecov-console 490 | sitemap_generator (~> 6.3) 491 | stimulus-rails (~> 1.3) 492 | turbo-rails (~> 2.0) 493 | vcr 494 | webmock 495 | 496 | RUBY VERSION 497 | ruby 3.3.3p89 498 | 499 | BUNDLED WITH 500 | 2.5.13 501 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2022 Manuel Meurer 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/rails assets:precompile && bundle exec puma -C config/puma.rb 2 | worker: bundle exec sidekiq -c 2 3 | release: bin/rails assets:precompile && bundle exec rake sitemap:create 4 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | css: bin/rails dartsass:watch 3 | worker: bundle exec sidekiq -c 2 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 👊 RailsBump 2 | 3 | [![Test](https://github.com/railsbump/app/actions/workflows/test.yml/badge.svg)](https://github.com/railsbump/app/actions/workflows/test.yml) 4 | [![codecov](https://codecov.io/gh/railsbump/app/graph/badge.svg?token=htyQYxjXHT)](https://codecov.io/gh/railsbump/app) 5 | 6 | Check your Ruby gems for compatibility with all Rails minor versions. 7 | 8 | ## Usage 9 | 10 | The app is live at https://railsbump.org/, where you can check an [individual gem](http://railsbump.org/gems/new) or a [whole Bundler lockfile](http://railsbump.org/lockfiles/new) (Gemfile.lock). 11 | 12 | ## Behind the scenes 13 | 14 | RailsBump uses a few approaches to check whether a gem version is compatible with a specific Rails release: 15 | 16 | - if the gem version has a direct dependency on one of the "Rails gems" (rails, railties, activerecord, activesupport, etc.), it may be clear that it is not compatible with some Rails releases immediately 17 | - if the gem version has a dependency on a specific version of another gem that we already know is not compatible with a Rails release, this gem version is not compatible either, 18 | - if all other approaches don't work, RailsBump simply puts the gem version and a Rails release in a Gemfile and lets Bundler figure it out. If the gem can be successfully installed along with the Rails release, it counts as "compatible". 19 | 20 | Since some gems have a lot of versions and each one needs to be checked against multiple Rails releases, this could get out of hand quickly. To minimize the amount of compatibility checks that need to be done, the gem versions are grouped by their dependencies first. If multiple versions (of the same gem or even of different gems) have the same dependencies, a single check is enough to determine whether all of them are compatible with a specific Rails release or not. 21 | 22 | To actually perform the check, [GitHub Actions](https://github.com/features/actions) are used. For each check, a new branch is created in a [separate repository](https://github.com/railsbump/checker), which triggers a [workflow](https://github.com/railsbump/checker/blob/main/.github/workflows/check.yml) that essentially tries to run `bundle lock` and reports the result back to the RailsBump app via a [webhook](https://docs.github.com/en/developers/webhooks-and-events/about-webhooks). 23 | 24 | ## History 25 | 26 | RailsBump used to be called Ready4Rails until December 2019, when [Manuel Meurer](https://github.com/manuelmeurer) took over from [Florent Guilleux](https://github.com/Florent2) to automate the service that Ready4Rails had been doing more or less manually until then. 27 | 28 | The relaunch took longer than expected, mainly because of the Coronavirus pandemic, and the first usable version of RailsBump was finally launched in August 2020. 29 | 30 | ## Stats 31 | 32 | You can see live stats from Plausible Analytics here: https://plausible.io/railsbump.org 33 | 34 | ## Contributing 35 | 36 | If you notice a bug or have an idea for an improvement, please open an [issue](https://github.com/railsbump/app/issues/new) or submit a [PR](https://github.com/railsbump/app/pulls). 37 | 38 | If you'd like to get involved in the development, get in touch [via email](mailto:hello@railsbump.org)! 39 | 40 | ### Setup 41 | 42 | You will need these services: 43 | 44 | - Postgres 16 or higher 45 | - Redis 46 | 47 | In order to set up the application locally: 48 | 49 | 1. `git clone git@github.com:railsbump/app.git` 50 | 2. `bin/setup` 51 | 3. `rake data:find_or_create_rails_releases` 52 | 4. `foreman start -f Procfile.dev` 53 | 5. Go to http://localhost:3000 54 | 55 | If these steps don't work, please submit a new issue: https://github.com/railsbump/app/issues/new 56 | 57 | We recommend running these scheduled tasks: 58 | 59 | - `bin/rails runner "Compats::CheckUnchecked.call"` once every 5 to 10 minutes 60 | 61 | - `bin/rails runner "Maintenance::Hourly.call"` once an hour 62 | 63 | ## Support 64 | 65 | If you find RailsBump useful and would like to support the ongoing development, please consider 66 | [submitting an issue](https://github.com/railsbump/app/issues) or [become a sponsor](https://github.com/sponsors/fastruby)! 67 | 68 | ## License 69 | 70 | This project is licensed under the MIT License - see the LICENSE.txt file for details. 71 | 72 | ## Sponsorship 73 | 74 | ![FastRuby.io | Technical Debt Remediation Services for Ruby, Rails, and JavaScript](fastruby-logo.png) 75 | 76 | RailsBump is supported, maintained, and funded by [FastRuby.io](https://fastruby.io?utm_source=github&utm_campaign=railsbump-sponsorship). The names and logos for FastRuby.io are trademarks of The Lean Software Boutique LLC. 77 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "sitemap_generator/tasks" 2 | 3 | require_relative "config/application" 4 | 5 | Rails.application.load_tasks 6 | -------------------------------------------------------------------------------- /app/assets/images/fastruby-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/app/assets/images/fastruby-logo.png -------------------------------------------------------------------------------- /app/assets/images/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/app/assets/images/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/assets/images/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/app/assets/images/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/assets/images/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/app/assets/images/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /app/assets/images/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/app/assets/images/icons/favicon-16x16.png -------------------------------------------------------------------------------- /app/assets/images/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/app/assets/images/icons/favicon-32x32.png -------------------------------------------------------------------------------- /app/assets/images/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/app/assets/images/icons/favicon.ico -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.css, which will include all the files 2 | // listed below. 3 | // 4 | // Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 5 | // vendor/assets/stylesheets directory can be referenced here using a relative path. 6 | // 7 | // You're free to add application-wide styles to this file and they'll appear at the bottom of the 8 | // compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 9 | // files in this directory. Styles in this file should be added after the last require_* statement. 10 | // It is generally better to create a new file per style scope. 11 | // 12 | //= require_self 13 | //= require_tree . 14 | 15 | @import "bootstrap"; 16 | 17 | @import "partials/basics"; 18 | @import "partials/compat-table"; 19 | @import "partials/compat-cell"; 20 | @import "partials/email-notification"; 21 | 22 | @import "pages/gemmies-index"; 23 | @import "pages/lockfiles-new"; 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/pages/_gemmies-index.scss: -------------------------------------------------------------------------------- 1 | .gemmies.index { 2 | 3 | #sponsorship { 4 | @include font-size($font-size-sm); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/pages/_lockfiles-new.scss: -------------------------------------------------------------------------------- 1 | .lockfiles.new { 2 | 3 | textarea { 4 | min-height: 100px; 5 | max-height: 500px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_basics.scss: -------------------------------------------------------------------------------- 1 | header { 2 | margin-bottom: 3rem; 3 | } 4 | 5 | main { 6 | padding-bottom: 3rem; 7 | 8 | img { 9 | @extend .img-fluid; 10 | } 11 | 12 | a:not(.btn) { 13 | text-decoration: none; 14 | 15 | &:hover { 16 | text-decoration: underline; 17 | } 18 | } 19 | 20 | section + section { 21 | margin-top: 3rem; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_compat-cell.scss: -------------------------------------------------------------------------------- 1 | body.rails_releases { 2 | h2 { 3 | font-size: 1em; 4 | } 5 | section.checking { 6 | color: $yellow; 7 | } 8 | 9 | section.incompatible { 10 | color: $red; 11 | } 12 | 13 | section.compatible { 14 | color: $green; 15 | } 16 | 17 | section.more { 18 | margin-top: 8px; 19 | } 20 | 21 | section.details { 22 | margin-top: 2em; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_compat-table.scss: -------------------------------------------------------------------------------- 1 | table.compat-table { 2 | 3 | tbody { 4 | 5 | td { 6 | 7 | span { 8 | padding-bottom: 2px; 9 | border-bottom: 1px dotted; 10 | } 11 | } 12 | 13 | .checking { 14 | color: $yellow; 15 | } 16 | 17 | .incompatible { 18 | color: $red; 19 | } 20 | 21 | .compatible { 22 | color: $green; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/assets/stylesheets/partials/_email-notification.scss: -------------------------------------------------------------------------------- 1 | #email_notification_form { 2 | 3 | .success, 4 | .error { 5 | font-weight: bold; 6 | } 7 | 8 | .success { 9 | color: $green; 10 | } 11 | 12 | .error { 13 | color: $red; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | class BaseController < ActionController::API 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/github_notifications_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | class GithubNotificationsController < BaseController 3 | def create 4 | data = request.request_parameters 5 | 6 | github_notification = GithubNotification.create!( 7 | data: data, 8 | action: data["action"], 9 | conclusion: data.dig("check_run", "conclusion"), 10 | branch: data.dig("check_run", "check_suite", "head_branch") 11 | ) 12 | 13 | GithubNotifications::Process.perform_async github_notification.id 14 | 15 | head :ok 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/api/releases_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | class ReleasesController < BaseController 3 | def create 4 | name = params.fetch(:name) 5 | 6 | if name == "rails" 7 | RailsReleases::Create.perform_async params.fetch(:version) 8 | else 9 | Gemmy.find_by_name(name)&.then { 10 | Gemmies::Process.perform_async _1.id 11 | } 12 | end 13 | 14 | head :ok 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/api/results_controller.rb: -------------------------------------------------------------------------------- 1 | module API 2 | class ResultsController < BaseController 3 | before_action :authenticate_api_key! 4 | 5 | def create 6 | @rails_release = RailsRelease.find_by(version: params[:rails_version]) 7 | @compat = @rails_release.compats.find_by_id!(params[:compat_id]) 8 | 9 | if @compat.dependencies == params.require(:dependencies).permit!.to_h 10 | if @compat.process_result(params[:result]) 11 | logger.info "Compat #{@compat.id} processed successfully" 12 | head :ok 13 | else 14 | logger.info "Compat #{@compat.id} process_result failed: #{@compat.errors.full_messages}" 15 | head :unprocessable_entity 16 | end 17 | else 18 | logger.info "Compat #{@compat.id} dependencies do not match" 19 | head :unprocessable_entity 20 | end 21 | end 22 | 23 | private 24 | 25 | def authenticate_api_key! 26 | api_key = request.headers['RAILS-BUMP-API-KEY'] 27 | 28 | return head :unauthorized if invalid_api_key?(api_key) 29 | 30 | logger.info "API Key: #{@api_key.name}" 31 | end 32 | 33 | def invalid_api_key?(api_key) 34 | return true if api_key.nil? 35 | 36 | @api_key = APIKey.find_by(key: api_key) 37 | 38 | return true if @api_key.nil? 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Baseline::ControllerExtensions 3 | 4 | layout -> { false if request.format.js? } 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/email_notifications_controller.rb: -------------------------------------------------------------------------------- 1 | class EmailNotificationsController < ApplicationController 2 | def create 3 | email_notification = EmailNotification.new(email_notification_params) 4 | 5 | unless email_notification.save 6 | render partial: "email_notifications/form", 7 | locals: { email_notification: email_notification }, 8 | status: :unprocessable_entity 9 | end 10 | end 11 | 12 | private 13 | 14 | def email_notification_params 15 | params.require(:email_notification) 16 | .permit(:email, :notifiable_gid) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/gemmies_controller.rb: -------------------------------------------------------------------------------- 1 | class GemmiesController < ApplicationController 2 | def new 3 | @gemmy = Gemmy.new 4 | end 5 | 6 | def create 7 | @gemmy = Gemmies::Create.call(gemmy_params.fetch(:name)) 8 | rescue Gemmies::Create::AlreadyExists => error 9 | redirect_to error.gemmy, 10 | status: :see_other 11 | rescue Gemmies::Create::Error => error 12 | @gemmy = Gemmy.new 13 | flash.now[:alert] = error.message 14 | render :new, 15 | status: :unprocessable_entity 16 | else 17 | redirect_to @gemmy, 18 | status: :see_other 19 | end 20 | 21 | def index 22 | @gemmies = Gemmy.order(created_at: :desc).limit(20) 23 | @inaccessible_gemmies = [] 24 | end 25 | 26 | def show 27 | @gemmy = Gemmy.find_by_name!(params[:id]) 28 | end 29 | 30 | def compat_table 31 | render locals: { 32 | gemmies: Gemmy.find(params[:gemmy_ids].split(",")), 33 | inaccessible_gemmies: InaccessibleGemmy.find(inaccessible_gemmy_ids), 34 | hide_gem_name: params.key?(:hide_gem_name) 35 | } 36 | end 37 | 38 | private 39 | 40 | def gemmy_params 41 | params.require(:gemmy).permit(:name) 42 | end 43 | 44 | def inaccessible_gemmy_ids 45 | ps = params[:inaccessible_gemmy_ids] || "" 46 | 47 | ps.split(",") 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/controllers/lockfiles_controller.rb: -------------------------------------------------------------------------------- 1 | class LockfilesController < ApplicationController 2 | def new 3 | @lockfile = Lockfile.new 4 | end 5 | 6 | def create 7 | @lockfile = Lockfiles::Create.call(lockfile_params.fetch(:content).strip) 8 | 9 | if @lockfile.valid? 10 | redirect_to @lockfile, status: :see_other 11 | else 12 | redirect_to new_lockfile_path, flash: { alert: @lockfile.errors.full_messages.join(". ") } 13 | end 14 | rescue Lockfiles::Create::AlreadyExists => error 15 | redirect_to error.lockfile, 16 | status: :see_other 17 | end 18 | 19 | def show 20 | @lockfile = Lockfile.find_by!(slug: params[:id]) 21 | end 22 | 23 | private 24 | 25 | def lockfile_params 26 | params.require(:lockfile).permit(:content) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/rails_releases_controller.rb: -------------------------------------------------------------------------------- 1 | class RailsReleasesController < ApplicationController 2 | def show 3 | @gemmy = Gemmy.find_by_name!(params[:gemmy_id]) 4 | @rails_release = RailsRelease.find_by!(version: params[:id].gsub("rails-", "").gsub("-", ".")) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/sitemaps_controller.rb: -------------------------------------------------------------------------------- 1 | # app/controllers/sitemaps_controller.rb 2 | class SitemapsController < ApplicationController 3 | require "open-uri" 4 | 5 | def show 6 | sitemap_url = ENV["FOG_URL"] 7 | if sitemap_url.present? 8 | sitemap_content = URI.open("#{sitemap_url}/sitemap.xml").read 9 | send_data sitemap_content, type: "application/xml", disposition: "inline" 10 | else 11 | head :not_found 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /app/controllers/static_controller.rb: -------------------------------------------------------------------------------- 1 | class StaticController < ApplicationController 2 | def robots 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | include Baseline::Helper 3 | 4 | def compats_status(gemmy, compats) 5 | case 6 | when gemmy.inaccessible_gem? then :inconclusive 7 | when compats.compatible.any? then :compatible 8 | when compats.none? || compats.pending.any? then :checking 9 | else :incompatible 10 | end 11 | end 12 | 13 | def compats_label_and_text(compats, gemmy, rails_release) 14 | compatible_compats = compats.compatible 15 | pending_compats = compats.pending 16 | 17 | case 18 | when gemmy.inaccessible_gem? && compats.inconclusive.any? 19 | return ["inconclusive", "We can't determine compatibility of #{gemmy} and #{rails_release} because we don't have access to this gem."] 20 | when compats.none? 21 | return ["checking", "Some versions of #{gemmy} are still being checked for compatibility with #{rails_release}."] 22 | when compatible_compats.none? && pending_compats.none? 23 | return ["none", "No version of #{gemmy} is compatible with #{rails_release}."] 24 | when compatible_compats.none? && pending_compats.any? 25 | return ["checking", "#{pluralize pending_compats.size, "version"} of #{gemmy} #{pending_compats.many? ? "are" : "is"} still being checked for compatibility with #{rails_release}."] 26 | when compatible_compats.ids == compats.ids 27 | return ["all", "All versions of #{gemmy} are compatible with #{rails_release}."] 28 | end 29 | 30 | compatible_versions = gemmy.versions(compatible_compats.map(&:dependencies)) 31 | all_versions = gemmy.versions 32 | incompatible_versions = all_versions - compatible_versions 33 | 34 | compatible_versions_string, all_versions_string = [compatible_versions, all_versions].map { _1.join(":") } 35 | 36 | label, text_prefix = case 37 | when compatible_versions.many? && all_versions_string.start_with?(compatible_versions_string) 38 | [ 39 | "<= #{compatible_versions.last}", 40 | "#{pluralize compatible_versions.size, "Version"} #{compatible_versions.last} and below" 41 | ] 42 | when compatible_versions.many? && all_versions_string.end_with?(compatible_versions_string) 43 | [ 44 | ">= #{compatible_versions.first}", 45 | "#{pluralize compatible_versions.size, "Version"} #{compatible_versions.first} and above" 46 | ] 47 | else 48 | if compatible_versions.many? 49 | major_version_numbers = compatible_versions.map { _1.segments.first }.uniq 50 | major_version_numbers.each do |version_number| 51 | matching_compatible_versions = compatible_versions.select { _1.is_a?(Gem::Version) && _1.segments.first == version_number } 52 | if matching_compatible_versions.size == all_versions.count { _1.segments.first == version_number } 53 | index = compatible_versions.index(matching_compatible_versions.first) 54 | compatible_versions.delete_if { matching_compatible_versions.include?(_1) } 55 | compatible_versions.insert index, "#{version_number}.x" 56 | end 57 | end 58 | end 59 | 60 | label = compatible_versions.size > 3 ? "some" : compatible_versions.join(", ") 61 | versions = compatible_versions.map(&:to_s).to_sentence 62 | 63 | [ 64 | label, 65 | "#{"Version".pluralize(versions.size)} #{versions}" 66 | ] 67 | end 68 | 69 | text = "#{text_prefix} of #{gemmy} #{compatible_versions.many? ? "are" : "is"} compatible with #{rails_release}" 70 | if pending_compats.any? 71 | text << ", but #{pluralize pending_compats.size, "other version"} #{pending_compats.many? ? "are" : "is"} still being checked" 72 | end 73 | 74 | [ 75 | label, 76 | "#{text}." 77 | ] 78 | end 79 | 80 | def head_title 81 | "RailsBump.org: Rails Compatibility Checker Tool" 82 | end 83 | 84 | def display_gemmy_name(gemmy) 85 | if gemmy.accessible_gem? 86 | link_to gemmy.name, gemmy 87 | else 88 | gemmy.name 89 | end 90 | end 91 | 92 | def meta_description 93 | "RailsBump lets you check your Ruby gems for compatibility with all major and minor Rails versions. Check your Gemfile.lock for compatible Rails versions." 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /app/helpers/lockfiles_helper.rb: -------------------------------------------------------------------------------- 1 | module LockfilesHelper 2 | def head_title 3 | if @lockfile 4 | "Compatibility for Gemfile.lock##{@lockfile.slug}" 5 | else 6 | super 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/helpers/rails_releases_helper.rb: -------------------------------------------------------------------------------- 1 | module RailsReleasesHelper 2 | def head_title 3 | if @gemmy 4 | if @rails_release 5 | "#{@gemmy} gem: Compatibility with #{@rails_release}" 6 | else 7 | rails_releases = RailsRelease.order(:version) 8 | first_version = rails_releases.first.version 9 | last_version = rails_releases.last.version 10 | "#{@gemmy} gem: Compatibility with Rails #{first_version} to #{last_version}" 11 | end 12 | else 13 | super 14 | end 15 | end 16 | 17 | def meta_description 18 | if @gemmy 19 | if @rails_release 20 | "RailsBump calculated the compatibility status for the #{@gemmy} gem and #{@rails_release}. Is #{@gemmy} compatible with #{@rails_release}?" 21 | else 22 | rails_releases = RailsRelease.order(:version) 23 | first_version = rails_releases.first.version 24 | last_version = rails_releases.last.version 25 | "RailsBump calculated #{@gemmy}'s compatibility with Rails #{first_version} to #{last_version}. How compatible the #{@gemmy} gem is with different versions of Rails?" 26 | end 27 | else 28 | super 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/javascript/application.js: -------------------------------------------------------------------------------- 1 | import "autosize" 2 | import "@hotwired/turbo-rails" 3 | import "popper" 4 | import "bootstrap" 5 | 6 | import { Application } from "@hotwired/stimulus" 7 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 8 | 9 | const initialize = event => { 10 | autosize(event.target.querySelectorAll(".autosize")) 11 | event.target 12 | .querySelectorAll('[data-bs-toggle="tooltip"]') 13 | .forEach(element => new bootstrap.Tooltip(element)) 14 | } 15 | 16 | document.addEventListener("turbo:load", initialize) 17 | document.addEventListener("turbo:frame-render", initialize) 18 | 19 | const application = Application.start() 20 | 21 | application.debug = false 22 | window.Stimulus = application 23 | 24 | eagerLoadControllersFrom("controllers", application) 25 | -------------------------------------------------------------------------------- /app/javascript/controllers/compat_table_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | static targets = ["earlierRailsVersion"] 5 | 6 | toggleEarlierRailsVersions(event) { 7 | event.preventDefault() 8 | 9 | this.earlierRailsVersionTargets.forEach(element => 10 | element.classList.toggle("d-none") 11 | ) 12 | event.target.innerText = event.target.innerText.replace(/^(\w+)/, match => match === "Show" ? "Hide" : "Show") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default \ 3 | from: "RailsBump <#{ENV["SMTP_FROM"]}>", 4 | template_path: "mailers", 5 | message_stream: :outbound 6 | 7 | layout "mailer" 8 | 9 | def email_notification(email_notification) 10 | @notifiable = email_notification.notifiable 11 | 12 | mail to: email_notification.email, 13 | subject: "[RailsBump] #{@notifiable.is_a?(Gemmy) ? %(Gem "#{@notifiable}") : "Your lockfile"} has been checked successfully." 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/api_key.rb: -------------------------------------------------------------------------------- 1 | class APIKey < ApplicationRecord 2 | validates :name, :key, presence: true, uniqueness: true 3 | validates :name, length: { maximum: 50 } 4 | validates :key, length: { minimum: 64, maximum: 255 } 5 | end 6 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | include Baseline::ModelExtensions 3 | primary_abstract_class 4 | end 5 | -------------------------------------------------------------------------------- /app/models/compat.rb: -------------------------------------------------------------------------------- 1 | class Compat < ApplicationRecord 2 | include HasTimestamps[:checked_at] 3 | 4 | enum status: %i( 5 | pending 6 | compatible 7 | incompatible 8 | inconclusive 9 | ) 10 | 11 | belongs_to :rails_release 12 | 13 | has_many :github_notifications 14 | 15 | validates :status, presence: true, inclusion: { in: %w(pending), if: :unchecked?, message: "must be pending if unchecked" } 16 | 17 | validate :unique_dependencies_for_rails_release, on: :create 18 | 19 | def unique_dependencies_for_rails_release 20 | if Compat.where(rails_release: rails_release) 21 | .where("dependencies::jsonb = ?", dependencies.to_json) 22 | .exists? 23 | errors.add(:dependencies, "must be unique for the given Rails release") 24 | end 25 | end 26 | 27 | validates :status_determined_by, presence: { unless: :pending? }, 28 | absence: { if: :pending? } 29 | 30 | after_initialize do 31 | if new_record? 32 | self.status ||= :pending 33 | end 34 | end 35 | 36 | def to_s 37 | "#{rails_release}, #{dependencies.map { "#{_1} #{_2}" }.join(", ")}" 38 | end 39 | 40 | def gemmies 41 | Gemmy 42 | .from("#{Gemmy.table_name}, json_each(#{Gemmy.table_name}.compat_ids)") 43 | .where(json_each: { value: id.to_s }) 44 | end 45 | 46 | def lockfiles 47 | Lockfile.with_gemmies(gemmies) 48 | end 49 | 50 | # Sort dependencies, JSON does not preserve key order. 51 | def dependencies 52 | deps = super || [] 53 | 54 | deps.sort.to_h 55 | end 56 | 57 | def dependencies=(value) 58 | super 59 | self.dependencies_key = ActiveSupport::Digest.hexdigest(JSON.generate dependencies) 60 | end 61 | 62 | def check_locally 63 | # Rails < 5 requires older Ruby and Bundler versions 64 | # which cannot easily be installed on current Linux systems, 65 | # so we'll only check compats for newer Rails versions locally. 66 | rails_release.version >= Gem::Version.new("5") 67 | end 68 | 69 | # Knows how to process a `result` as reported by the railsbump/checker project and an 70 | # execution of the GitHub Actions workflow. 71 | # 72 | # @param [Hash] Attributes that are the result of the compatibility check 73 | # @return [Boolean] Whether the status was updated or not 74 | def process_result(result) 75 | return true unless pending? 76 | 77 | success = ActiveModel::Type::Boolean.new.cast(result[:success]) 78 | 79 | if success 80 | logger.info "Compat #{id} result is not compatible" 81 | self.update( 82 | checked_at: Time.current, 83 | status: :compatible, 84 | status_determined_by: "#{result[:strategy]}\nOutput: #{result[:output]}" 85 | ) 86 | else 87 | logger.info "Compat #{id} result is not compatible" 88 | self.update( 89 | checked_at: Time.current, 90 | status: :incompatible, 91 | status_determined_by: "#{result[:strategy]}\nOutput: #{result[:output]}" 92 | ) 93 | end 94 | end 95 | end 96 | 97 | # == Schema Information 98 | # 99 | # Table name: compats 100 | # 101 | # id :integer not null, primary key 102 | # checked_at :datetime 103 | # dependencies :json 104 | # dependencies_key :text 105 | # status :integer 106 | # status_determined_by :text 107 | # created_at :datetime not null 108 | # updated_at :datetime not null 109 | # rails_release_id :integer 110 | # 111 | -------------------------------------------------------------------------------- /app/models/concerns/has_timestamps.rb: -------------------------------------------------------------------------------- 1 | HasTimestamps = Baseline::HasTimestamps 2 | -------------------------------------------------------------------------------- /app/models/current.rb: -------------------------------------------------------------------------------- 1 | class Current < ActiveSupport::CurrentAttributes 2 | attribute :missing_value # Set by baseline 3 | end 4 | -------------------------------------------------------------------------------- /app/models/email_notification.rb: -------------------------------------------------------------------------------- 1 | class EmailNotification 2 | include ActiveModel::Model 3 | 4 | NAMESPACE = "email_notifications" 5 | NAMESPACE_REGEX = /\A#{Regexp.escape NAMESPACE}:(.+)/ 6 | 7 | attr_accessor :email, :notifiable 8 | 9 | validates :email, 10 | presence: true, 11 | format: { 12 | with: URI::MailTo::EMAIL_REGEXP, 13 | allow_blank: true 14 | } 15 | validates :notifiable, presence: true 16 | 17 | def self.all 18 | Kredis.redis.keys("#{NAMESPACE}:*").flat_map do |key| 19 | unless notifiable = GlobalID::Locator.locate(key[NAMESPACE_REGEX, 1]) 20 | raise "Could not find notifiable: #{key}" 21 | end 22 | 23 | Kredis.redis.smembers(key).map do |email| 24 | new notifiable: notifiable, email: email 25 | end 26 | end 27 | end 28 | 29 | def notifiable_gid=(value) 30 | self.notifiable = GlobalID::Locator.locate(value) 31 | end 32 | 33 | def save 34 | return false unless valid? 35 | 36 | Kredis.redis.sadd key, email 37 | true 38 | end 39 | 40 | def delete 41 | Kredis.redis.srem key, email 42 | end 43 | 44 | private 45 | 46 | def key 47 | [ 48 | NAMESPACE, 49 | notifiable.to_global_id 50 | ].join(":") 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/models/gemmy.rb: -------------------------------------------------------------------------------- 1 | class Gemmy < ApplicationRecord 2 | FORBIDDEN_NAMES = %w( 3 | new 4 | edit 5 | rails 6 | ) 7 | 8 | validates :name, presence: true, uniqueness: { allow_blank: true }, exclusion: FORBIDDEN_NAMES 9 | 10 | delegate :to_param, :to_s, to: :name 11 | 12 | def accessible_gem? 13 | true 14 | end 15 | 16 | def inaccessible_gem? 17 | !accessible_gem? 18 | end 19 | 20 | def compats_for_rails_release(rails_release) 21 | compats.merge(rails_release.compats) 22 | end 23 | 24 | # Find existing by case-insensitive name 25 | def self.find_by_name(name, raise_error: false) 26 | find_by!("LOWER(name) = ?", name.downcase) 27 | rescue ActiveRecord::RecordNotFound => e 28 | raise e if raise_error 29 | end 30 | 31 | def self.find_by_name!(name) 32 | find_by_name(name, raise_error: true) 33 | end 34 | 35 | # Check all pending compats for compatibility 36 | def check_compatibility 37 | compats.pending.each do |compat| 38 | Compats::Check.new.call(compat) 39 | end 40 | end 41 | 42 | # Check all compats for compatibility 43 | def check_compatibility! 44 | compats.each do |compat| 45 | Compats::Check.new.check!(compat) 46 | end 47 | end 48 | 49 | def compats 50 | Compat.where(id: compat_ids) 51 | end 52 | 53 | def dependencies 54 | dependencies_and_versions 55 | .keys 56 | .map { JSON.parse _1 } 57 | end 58 | 59 | def last_checked_at 60 | compats.maximum(:checked_at) 61 | end 62 | 63 | def versions(dependencies = nil) 64 | version_groups = 65 | dependencies ? 66 | dependencies_and_versions.fetch_values(*dependencies.map { JSON.generate _1 }) : 67 | dependencies_and_versions.values 68 | 69 | version_groups 70 | .flatten 71 | .map(&Gem::Version.method(:new)) 72 | .sort 73 | end 74 | end 75 | 76 | # == Schema Information 77 | # 78 | # Table name: gemmies 79 | # 80 | # id :integer not null, primary key 81 | # compat_ids :json not null 82 | # dependencies_and_versions :json 83 | # name :text 84 | # created_at :datetime not null 85 | # updated_at :datetime not null 86 | # 87 | -------------------------------------------------------------------------------- /app/models/github_notification.rb: -------------------------------------------------------------------------------- 1 | class GithubNotification < ApplicationRecord 2 | include HasTimestamps[:processed_at] 3 | 4 | CONCLUSIONS = %w(success failure skipped cancelled) 5 | 6 | belongs_to :compat, optional: true 7 | 8 | validates :action, presence: true 9 | validates :branch, presence: true 10 | validates :conclusion, inclusion: { in: CONCLUSIONS, if: -> { action == "completed" } } 11 | 12 | def self.actions 13 | pluck(Arel.sql("DISTINCT action")).sort 14 | end 15 | 16 | def method_missing(method, *args, &block) 17 | method_action = method.to_s.sub(/\?\z/, '') 18 | 19 | if method_action.in?(self.class.actions) 20 | action == method_action 21 | else 22 | super 23 | end 24 | end 25 | end 26 | 27 | # == Schema Information 28 | # 29 | # Table name: github_notifications 30 | # 31 | # id :integer not null, primary key 32 | # action :string not null 33 | # branch :string not null 34 | # conclusion :string 35 | # data :json 36 | # processed_at :datetime 37 | # created_at :datetime not null 38 | # updated_at :datetime not null 39 | # compat_id :integer 40 | # 41 | -------------------------------------------------------------------------------- /app/models/inaccessible_gemmy.rb: -------------------------------------------------------------------------------- 1 | class InaccessibleGemmy < ApplicationRecord 2 | belongs_to :lockfile 3 | 4 | FORBIDDEN_NAMES = %w( 5 | new 6 | edit 7 | rails 8 | ) 9 | 10 | validates :name, presence: true, uniqueness: { allow_blank: true } 11 | validates :name, uniqueness: { scope: :lockfile_id } 12 | 13 | delegate :to_param, :to_s, to: :name 14 | 15 | def accessible_gem? 16 | false 17 | end 18 | 19 | def inaccessible_gem? 20 | !accessible_gem? 21 | end 22 | 23 | def compats 24 | RailsRelease.all.map do |rails_release| 25 | InconclusiveCompat.build(rails_release: rails_release, status: 'inconclusive') 26 | end.to_a 27 | end 28 | 29 | def compats_for_rails_release(rails_release) 30 | InconclusiveCompat.build(rails_release: rails_release, status: 'inconclusive') 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/models/inconclusive_compat.rb: -------------------------------------------------------------------------------- 1 | class InconclusiveCompat < Compat 2 | def compatible 3 | [] 4 | end 5 | 6 | def inconclusive 7 | [self] 8 | end 9 | 10 | def pending 11 | [] 12 | end 13 | 14 | def none? 15 | true 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/lockfile.rb: -------------------------------------------------------------------------------- 1 | class Lockfile < ApplicationRecord 2 | has_many :dependencies, class_name: "LockfileDependency", dependent: :destroy 3 | has_many :gemmies, through: :dependencies 4 | has_many :inaccessible_gemmies, dependent: :destroy 5 | 6 | validates :content, presence: true 7 | validates :slug, presence: { if: -> { content.present? } }, uniqueness: { allow_blank: true } 8 | validates :gemmies, presence: { if: -> { content.present? } } 9 | validate :validate_content 10 | validate :validate_gemmies 11 | 12 | before_validation :generate_slug 13 | before_validation :add_gemmies 14 | 15 | delegate :to_param, to: :slug 16 | 17 | scope :with_gemmies, ->(gemmies) { joins(:gemmies).where(gemmies: { id: gemmies }).distinct } 18 | 19 | CONTENT_REGEX = %r( 20 | GEM 21 | .+ 22 | DEPENDENCIES 23 | )xm.freeze 24 | 25 | def compats 26 | Compat.where(id: gemmies.flat_map(&:compat_ids)) 27 | end 28 | 29 | def gem_names 30 | parser = Bundler::LockfileParser.new(content) 31 | parser.dependencies.keys - %w(rails) 32 | end 33 | 34 | def calculated_slug 35 | ActiveSupport::Digest.hexdigest(gem_names.join("#")) 36 | end 37 | 38 | def accessible_and_inaccessible_gemmies 39 | (gemmies + inaccessible_gemmies).sort_by(&:name) 40 | end 41 | 42 | private 43 | 44 | def add_gemmies 45 | return if content.blank? 46 | return if gemmies.any? 47 | 48 | gem_names.each do |gem_name| 49 | begin 50 | gemmy = Gemmy.find_by_name(gem_name) || Gemmies::Create.call(gem_name) 51 | self.gemmies << gemmy 52 | rescue Gemmies::Create::NotFound 53 | self.inaccessible_gemmies.build(name: gem_name) 54 | end 55 | end 56 | end 57 | 58 | def generate_slug 59 | return if self.slug.present? 60 | 61 | self.slug = calculated_slug 62 | end 63 | 64 | def validate_content 65 | unless CONTENT_REGEX.match?(content) 66 | self.errors.add(:content, "does not look like a valid lockfile.") 67 | end 68 | end 69 | 70 | def validate_gemmies 71 | parser = Bundler::LockfileParser.new(content) 72 | 73 | if gem_names.none? 74 | self.errors.add(:content, "No gems found in content.") 75 | end 76 | end 77 | end 78 | 79 | # == Schema Information 80 | # 81 | # Table name: lockfiles 82 | # 83 | # id :integer not null, primary key 84 | # content :text 85 | # slug :text 86 | # created_at :datetime not null 87 | # updated_at :datetime not null 88 | # 89 | -------------------------------------------------------------------------------- /app/models/lockfile_dependency.rb: -------------------------------------------------------------------------------- 1 | class LockfileDependency < ApplicationRecord 2 | belongs_to :lockfile 3 | belongs_to :gemmy 4 | end 5 | 6 | # == Schema Information 7 | # 8 | # Table name: lockfile_dependencies 9 | # 10 | # gemmy_id :integer 11 | # lockfile_id :integer 12 | # 13 | -------------------------------------------------------------------------------- /app/models/rails_release.rb: -------------------------------------------------------------------------------- 1 | class RailsRelease < ApplicationRecord 2 | # Define the repository, workflow file, and branch 3 | GITHUB_REPO = 'railsbump/checker' 4 | GITHUB_WORKFLOW = 'rails_release_sanity_check.yml' 5 | GITHUB_REF = 'main' 6 | 7 | composed_of :version, 8 | class_name: "Gem::Version", 9 | mapping: %w(version to_s), 10 | converter: Gem::Version.method(:new) 11 | 12 | has_many :compats, dependent: :destroy 13 | 14 | validates :version, presence: true, format: { with: /\A\d+\.\d+\z/, allow_blank: true } 15 | 16 | validate do 17 | if version 18 | scope = self.class.where(version: version) 19 | if persisted? 20 | scope.where.not(id: id) 21 | end 22 | if scope.any? 23 | errors.add :version, "is a duplicate" 24 | end 25 | end 26 | end 27 | 28 | scope :latest_major, -> { 29 | pluck(:version) 30 | .group_by { _1[/\A\d+/] } 31 | .sort_by(&:first) 32 | .last(2) 33 | .to_h 34 | .values 35 | .map(&:max) 36 | .then { 37 | where(version: _1).order(:version) 38 | } 39 | } 40 | 41 | def to_s 42 | "Rails #{version}" 43 | end 44 | 45 | def earlier? 46 | !self.class.latest_major.exists?(id: self) 47 | end 48 | 49 | def self.print_all 50 | all.each do |release| 51 | puts "# #{release}" 52 | 53 | puts "Minimum ruby version: #{release.minimum_ruby_version}" 54 | puts "Minimum bundler version: #{release.minimum_bundler_version}" 55 | puts "Minimum rubygems version: #{release.minimum_rubygems_version}" 56 | end 57 | end 58 | 59 | def compatible_ruby_version 60 | if version 61 | Gem::Version.new(version < Gem::Version.new("5") ? "2.7" : RUBY_VERSION) 62 | end 63 | end 64 | 65 | def compatible_bundler_version 66 | if version 67 | Gem::Version.new(version < Gem::Version.new("5") ? "1.17.3" : Bundler::VERSION) 68 | end 69 | end 70 | 71 | def github_actions_sanity_check! 72 | # Initialize the Octokit client with your GitHub token 73 | client = Octokit::Client.new(access_token: ENV['GITHUB_ACCESS_TOKEN']) 74 | 75 | # Trigger the workflow dispatch event 76 | client.workflow_dispatch(GITHUB_REPO, GITHUB_WORKFLOW, GITHUB_REF, inputs: github_action_inputs) 77 | end 78 | 79 | def to_param 80 | "rails-#{version.to_s.gsub(".", "-")}" 81 | end 82 | 83 | private 84 | 85 | # Define the github_action_inputs for the workflow 86 | def github_action_inputs 87 | { 88 | rails_version: version.to_s, 89 | ruby_version: minimum_ruby_version.to_s, 90 | bundler_version: minimum_bundler_version.to_s 91 | } 92 | end 93 | end 94 | 95 | # == Schema Information 96 | # 97 | # Table name: rails_releases 98 | # 99 | # id :integer not null, primary key 100 | # version :text 101 | # created_at :datetime not null 102 | # updated_at :datetime not null 103 | # 104 | -------------------------------------------------------------------------------- /app/services/check_git_branches.rb: -------------------------------------------------------------------------------- 1 | class CheckGitBranches < Baseline::Service 2 | def call 3 | check_uniqueness 4 | 5 | page = 1 6 | 7 | begin 8 | branches = External::Github.list_branches(page) 9 | 10 | break if branches.empty? 11 | 12 | branches 13 | .map { _1.fetch(:name) } 14 | .grep(/\A\d+\z/) 15 | .each do |compat_id| 16 | 17 | unless compat = Compat.find_by(id: compat_id) 18 | External::Github.delete_branch(compat_id) 19 | next 20 | end 21 | 22 | if Date.current > Date.new(2024, 10, 1) 23 | ReportError.call "remove this when all old git branches are gone" 24 | end 25 | if compat.invalid? 26 | compats = Compat.where("dependencies::jsonb = ?", compat.dependencies.to_json) 27 | if compats.size == RailsRelease.count && !compats.include?(compat) 28 | compat.destroy 29 | External::Github.delete_branch(compat.id) 30 | next 31 | end 32 | end 33 | 34 | case 35 | when compat.unchecked? || !compat.pending? 36 | External::Github.delete_branch(compat.id) 37 | when compat.checked_before?(1.week.ago) 38 | compat.unchecked! 39 | External::Github.delete_branch(compat.id) 40 | 41 | if RailsRelease.latest_major.include?(compat.rails_release) 42 | Compats::Check.perform_async compat.id 43 | end 44 | end 45 | end 46 | 47 | page += 1 48 | end while page <= 10 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/services/compats/check.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | 3 | # The purpose of this service is to check a compat, i.e. determine whether its set of dependencies is compatible with its Rails release or not. To do so, several approaches are taken, from least to most complex. 4 | module Compats 5 | class Check < Baseline::Service 6 | CHECK_STRATEGIES = [ 7 | Compats::Checks::EmptyDependenciesCheck, 8 | Compats::Checks::RailsGemsCheck, 9 | Compats::Checks::DependencySubsetsCheck, 10 | Compats::Checks::BundlerGithubCheck 11 | ] 12 | 13 | attr_accessor :compat 14 | 15 | # This method checks a compat by calling all check strategies. It only does checks on pending compats. 16 | # 17 | # If any of them marks the compat as incompatible, the compat is marked as incompatible. 18 | # 19 | # If any of them mark the compat as compatible, the compat is marked as compatible. 20 | # 21 | # @param [Compat] compat The compat to check 22 | def call(compat) 23 | check_uniqueness on_error: :return 24 | 25 | if compat.checked? 26 | raise Error, "Compat is already checked." 27 | end 28 | 29 | CHECK_STRATEGIES.each do |klass| 30 | klass.new(compat).call 31 | end 32 | end 33 | 34 | # This method checks a compat by calling all check strategies. It doesn't care about the compat's current status. 35 | # It will override the current status. 36 | # 37 | # If any of them marks the compat as incompatible, the compat is marked as incompatible. 38 | # 39 | # If any of them mark the compat as compatible, the compat is marked as compatible. 40 | # 41 | # @param [Compat] compat The compat to check 42 | def check!(compat) 43 | CHECK_STRATEGIES.each do |klass| 44 | klass.new(compat).check! 45 | end 46 | end 47 | 48 | private 49 | 50 | # This method checks if any other compats exist, that are marked as compatible and have a superset of the compat's dependencies. If so, the compat must be compatible and is marked as such. 51 | def check_dependency_supersets 52 | # return unless @compat.pending? 53 | # 54 | # TODO: How to convert `.contains` to SQLite? 55 | # if @compat.rails_release.compats.where.contains(dependencies: @compat.dependencies).compatible.any? 56 | # @compat.status = :compatible 57 | # @compat.status_determined_by = "dependency_supersets" 58 | # return 59 | # end 60 | end 61 | 62 | # This method checks a compat by actually attempting to install the compat's dependencies with the compat's Rails version locally. If the installation fails, the compat is marked as incompatible. If it succeeds, it is marked as compatible. If any of the dependencies have native extensions that cannot be built, the compat is marked as inconclusive. 63 | # def check_with_bundler_locally 64 | # return unless @compat.pending? && @compat.check_locally 65 | 66 | # dir = Rails.root.join("tmp", "compats") 67 | # file = dir.join(@compat.id.to_s) 68 | # FileUtils.mkdir_p dir 69 | 70 | # begin 71 | # deps_with_rails = @compat.dependencies.dup.tap { 72 | # _1["rails"] = [ 73 | # _1["rails"], 74 | # "#{@compat.rails_release.version.approximate_recommendation}.0" 75 | # ].compact 76 | # .join(", ") 77 | # } 78 | # gemfile_deps = deps_with_rails.map { 79 | # quoted_versions = _2.split(/\s*,\s*/).map { |d| "'#{d}'" } 80 | # "gem '#{_1}', #{quoted_versions.join(", ")}, require: false" 81 | # } 82 | # File.write file, <<~SCRIPT 83 | # #!/usr/bin/env ruby 84 | 85 | # require "bundler/inline" 86 | 87 | # gemfile true do 88 | # source "https://rubygems.org" 89 | # ruby "#{@compat.rails_release.compatible_ruby_version}" 90 | # #{gemfile_deps.join("\n")} 91 | # end 92 | # SCRIPT 93 | # File.chmod 0755, file 94 | 95 | # stderr, stdout = Bundler.with_unbundled_env do 96 | # Open3.popen3 file.to_s do 97 | # # For some reason, the order matters: readlines must be called on stderr first. 🤷‍♂️ 98 | # [_3, _2].map do |io| 99 | # io.readlines.map(&:strip) 100 | # end 101 | # end 102 | # end 103 | # ensure 104 | # if File.exist?(file) 105 | # FileUtils.rm_rf file 106 | # end 107 | # end 108 | 109 | # stdout.each do |line| 110 | # if match = line.match(/\AInstalling (?\S+) (?\S+)\z/) 111 | # # TODO: uninstall gem again 112 | # end 113 | # end 114 | 115 | # case 116 | # when stderr.empty? 117 | # @compat.status = :compatible 118 | # when stderr.any?(/ERROR: Failed to build gem native extension/) 119 | # @compat.status = :inconclusive 120 | # when stderr.any?(/You have already activated/) 121 | # return 122 | # else 123 | # unless stderr[0].end_with?("Could not find compatible versions (Bundler::SolveFailure)") && 124 | # stderr.exclude?("Your bundle requires a different version of Bundler than the one you're running.") 125 | 126 | # raise Error, "Unexpected stderr: #{stderr.join("\n")}" 127 | # end 128 | 129 | # @compat.status = :incompatible 130 | # end 131 | 132 | # require "byebug"; byebug 133 | # @compat.status_determined_by = "bundler_local" 134 | # end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /app/services/compats/check_unchecked.rb: -------------------------------------------------------------------------------- 1 | module Compats 2 | class CheckUnchecked < Baseline::Service 3 | LIMIT = 10 4 | 5 | def call 6 | do_check 7 | end 8 | 9 | private 10 | 11 | def do_check 12 | count = 0 13 | 14 | RailsRelease 15 | .latest_major 16 | .reverse 17 | .each do |rails_release| 18 | 19 | rails_release 20 | .compats 21 | .unchecked 22 | .find_each do |compat| 23 | 24 | return unless count < LIMIT 25 | 26 | next if check_failed?(compat) 27 | 28 | begin 29 | Compats::Check.call compat 30 | rescue Compats::Check::Error => error 31 | ReportError.call error, 32 | compat_id: compat.id 33 | check_failed!(compat) 34 | else 35 | count += 1 36 | end 37 | end 38 | end 39 | end 40 | 41 | def check_failed_cache_key(compat) 42 | [ 43 | :compat_check_failed, 44 | compat.id 45 | ].join(":") 46 | end 47 | 48 | def check_failed?(compat) 49 | Kredis.redis.exists? \ 50 | check_failed_cache_key(compat) 51 | end 52 | 53 | def check_failed!(compat) 54 | Kredis.redis.setex \ 55 | check_failed_cache_key(compat), 56 | 1.week, 57 | nil 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/services/compats/checks/base.rb: -------------------------------------------------------------------------------- 1 | module Compats::Checks 2 | class Base 3 | def initialize(compat) 4 | @compat = compat 5 | end 6 | 7 | def call 8 | raise NotImplementedError 9 | end 10 | 11 | def check! 12 | raise NotImplementedError 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /app/services/compats/checks/bundler_github_check.rb: -------------------------------------------------------------------------------- 1 | module Compats::Checks 2 | 3 | # This method checks a compat by dispatching the check_bundler workflow. 4 | class BundlerGithubCheck < Base 5 | # Define the repository, workflow file, and branch 6 | GITHUB_REPO = 'railsbump/checker' 7 | GITHUB_WORKFLOW = 'check_bundler.yml' 8 | GITHUB_REF = 'main' 9 | 10 | def call 11 | return unless @compat.pending? && Rails.env.production? 12 | 13 | check! 14 | end 15 | 16 | def check! 17 | # Initialize the Octokit client with your GitHub token 18 | client = Octokit::Client.new(access_token: ENV['GITHUB_ACCESS_TOKEN']) 19 | 20 | # Trigger the workflow dispatch event 21 | client.workflow_dispatch(GITHUB_REPO, GITHUB_WORKFLOW, GITHUB_REF, inputs: inputs) 22 | end 23 | 24 | private 25 | 26 | # Define the inputs for the workflow 27 | def inputs 28 | { 29 | rails_version: @compat.rails_release.version.to_s, 30 | ruby_version: @compat.rails_release.minimum_ruby_version.to_s, 31 | bundler_version: @compat.rails_release.minimum_bundler_version.to_s, 32 | dependencies: JSON::dump(@compat.dependencies), 33 | compat_id: @compat.id.to_s 34 | } 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /app/services/compats/checks/dependency_subsets_check.rb: -------------------------------------------------------------------------------- 1 | module Compats::Checks 2 | 3 | # This method checks if any other compats exist, that are marked as incompatible 4 | # and have a subset of the compat's dependencies. 5 | # 6 | # If so, the compat must be incompatible and is marked as such. 7 | class DependencySubsetsCheck < Base 8 | def call 9 | return unless @compat.pending? && (2..10).cover?(@compat.dependencies.size) 10 | 11 | check! 12 | end 13 | 14 | def check! 15 | subsets = (1..@compat.dependencies.size - 1).flat_map do |count| 16 | @compat.dependencies.keys.combination(count).map { @compat.dependencies.slice *_1 } 17 | end 18 | 19 | subsets.in_groups_of(100, false).each do |group| 20 | if @compat.rails_release.compats.where("dependencies::jsonb = ?", group.to_json).incompatible.any? 21 | @compat.status = :incompatible 22 | @compat.status_determined_by = "dependency_subsets" 23 | @compat.checked! 24 | return 25 | end 26 | end 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /app/services/compats/checks/empty_dependencies_check.rb: -------------------------------------------------------------------------------- 1 | module Compats::Checks 2 | 3 | # This method checks for the simplest case: if the compat has no dependencies, 4 | # it's marked as compatible. 5 | class EmptyDependenciesCheck < Base 6 | def call 7 | return unless @compat.pending? 8 | 9 | check! 10 | end 11 | 12 | def check! 13 | if @compat.dependencies.blank? 14 | @compat.status = :compatible 15 | @compat.status_determined_by = "empty_dependencies" 16 | @compat.checked! 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /app/services/compats/checks/rails_gems_check.rb: -------------------------------------------------------------------------------- 1 | module Compats::Checks 2 | 3 | # This method checks if the dependencies include any Rail gems, and if so, 4 | # if any of them have a different version than the compat's Rails version. 5 | # 6 | # If that's the case, the compat is marked as incompatible. 7 | class RailsGemsCheck < Base 8 | RAILS_GEMS = %w( 9 | actioncable 10 | actionmailbox 11 | actionmailer 12 | actionpack 13 | actiontext 14 | actionview 15 | activejob 16 | activemodel 17 | activerecord 18 | activestorage 19 | activesupport 20 | rails 21 | railties 22 | ) 23 | 24 | def call 25 | return unless @compat.pending? 26 | 27 | check! 28 | end 29 | 30 | def check! 31 | @compat.dependencies.each do |gem_name, requirement| 32 | next unless RAILS_GEMS.include?(gem_name) 33 | requirement_unmet = requirement.split(/\s*,\s*/).any? do |r| 34 | !Gem::Requirement.new(r).satisfied_by?(@compat.rails_release.version) 35 | end 36 | if requirement_unmet 37 | @compat.status = :incompatible 38 | @compat.status_determined_by = "rails_gems" 39 | @compat.checked! 40 | return 41 | end 42 | end 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /app/services/email_notifications/send_all.rb: -------------------------------------------------------------------------------- 1 | module EmailNotifications 2 | class SendAll < Baseline::Service 3 | def call 4 | check_uniqueness 5 | 6 | EmailNotification.all.each do |email_notification| 7 | if email_notification.notifiable.compats.pending.none? 8 | ApplicationMailer.email_notification(email_notification).deliver_now 9 | email_notification.delete 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/services/external/github.rb: -------------------------------------------------------------------------------- 1 | require "octokit" 2 | 3 | module External 4 | class Github < Baseline::ExternalService 5 | REPO = "railsbump/checker" 6 | 7 | def list_branches(page = 1) 8 | client.branches \ 9 | REPO, 10 | per_page: 100, 11 | page: page 12 | end 13 | 14 | def delete_branch(name) 15 | client.delete_branch \ 16 | REPO, 17 | name 18 | rescue Octokit::UnprocessableEntity 19 | false 20 | end 21 | 22 | private 23 | 24 | def client 25 | @client ||= Octokit::Client.new(access_token: ENV.fetch("GITHUB_ACCESS_TOKEN")) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/services/gemmies/create.rb: -------------------------------------------------------------------------------- 1 | require "gems" 2 | 3 | module Gemmies 4 | class Create < Baseline::Service 5 | class AlreadyExists < Error 6 | attr_reader :gemmy 7 | 8 | def initialize(gemmy) 9 | super nil 10 | 11 | @gemmy = gemmy 12 | end 13 | end 14 | 15 | class NotFound < Error 16 | attr_reader :gemmy_name 17 | 18 | def initialize(gemmy_name) 19 | super nil 20 | 21 | @gemmy_name = gemmy_name 22 | end 23 | 24 | def message 25 | %(Gem "#{@gemmy_name}" does not exist.) 26 | end 27 | end 28 | 29 | def call(name) 30 | if name.blank? 31 | raise Error, "Please enter a name." 32 | end 33 | 34 | if existing_gemmy = Gemmy.find_by_name(name) 35 | raise AlreadyExists.new(existing_gemmy) 36 | end 37 | 38 | begin 39 | Gems.info name 40 | rescue Gems::NotFound 41 | raise NotFound.new(name) 42 | end 43 | 44 | gemmy = Gemmy.create!(name: name) 45 | 46 | Process.perform_async gemmy.id 47 | 48 | gemmy 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/services/gemmies/process.rb: -------------------------------------------------------------------------------- 1 | module Gemmies 2 | class Process < Baseline::Service 3 | def call(gemmy_id) 4 | gemmy = Gemmy.find(gemmy_id) 5 | UpdateDependenciesAndVersions.call(gemmy) 6 | UpdateCompats.call(gemmy.id) 7 | 8 | Compats::CheckUnchecked.perform_async 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/services/gemmies/update_compats.rb: -------------------------------------------------------------------------------- 1 | require "gems" 2 | 3 | module Gemmies 4 | class UpdateCompats < Baseline::Service 5 | def call(gemmy_id) 6 | check_uniqueness gemmy_id 7 | 8 | gemmy = Gemmy.find gemmy_id 9 | 10 | RailsRelease.find_each do |rails_release| 11 | gemmy.dependencies.each do |dependencies| 12 | unless rails_release.compats.where("dependencies::jsonb = ?", dependencies.to_json).exists? 13 | rails_release.compats.create! dependencies: dependencies 14 | end 15 | end 16 | end 17 | 18 | compats = gemmy.dependencies.flat_map do |dependencies| 19 | Compat.where("dependencies::jsonb = ?", dependencies.to_json).to_a 20 | end 21 | 22 | gemmy.update! compat_ids: compats.pluck(:id).sort 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/services/gemmies/update_dependencies_and_versions.rb: -------------------------------------------------------------------------------- 1 | require "gems" 2 | 3 | module Gemmies 4 | class UpdateDependenciesAndVersions < Baseline::Service 5 | def call(gemmy) 6 | versions = Octopoller.poll retries: 5 do 7 | Gems 8 | .versions(gemmy.name) 9 | .select { _1.fetch("platform") == "ruby" } 10 | .map { _1.fetch "number" } 11 | rescue Gems::GemError 12 | :re_poll 13 | end 14 | 15 | deps = versions.each_with_object({}) do |version, hash| 16 | cache_key = [ 17 | :deps, 18 | gemmy.name, 19 | version 20 | ].join(":") 21 | key = Rails.cache.fetch(cache_key) do 22 | Gems::V2 23 | .info(gemmy.name, version) 24 | .deep_fetch("dependencies", "runtime") 25 | .map(&:values) 26 | .sort_by(&:first) 27 | .to_h 28 | .then { JSON.generate _1 } 29 | end 30 | hash[key] ||= [] 31 | hash[key] << version 32 | end 33 | 34 | gemmy.update! dependencies_and_versions: deps 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/services/github_notifications/process.rb: -------------------------------------------------------------------------------- 1 | module GithubNotifications 2 | class Process < Baseline::Service 3 | def call(github_notification_id) 4 | github_notification = GithubNotification.find(github_notification_id) 5 | if github_notification.processed? 6 | raise Error, "GitHub Notification #{github_notification.id} has already been processed." 7 | end 8 | 9 | if /\A\d+\z/.match?(github_notification.branch) 10 | @github_notification = github_notification 11 | do_process 12 | end 13 | 14 | github_notification.processed! 15 | end 16 | 17 | private 18 | 19 | def do_process 20 | compat = Compat.find(@github_notification.branch) 21 | 22 | @github_notification.update! compat: compat 23 | 24 | return unless @github_notification.completed? 25 | 26 | case @github_notification.conclusion 27 | when "success" then status = :compatible 28 | when 'failure' then status = :incompatible 29 | when "skipped", "cancelled" 30 | if compat.github_notifications.where(conclusion: @github_notification.conclusion) 31 | .where.not(id: @github_notification) 32 | .none? 33 | compat.unchecked! 34 | return 35 | end 36 | status = :inconclusive 37 | else raise Error, "Unexpected conclusion: #{@github_notification.conclusion}" 38 | end 39 | 40 | compat.update! status: status, status_determined_by: "github_check" 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/services/lockfiles/create.rb: -------------------------------------------------------------------------------- 1 | require "bundler/lockfile_parser" 2 | 3 | module Lockfiles 4 | class Create < Baseline::Service 5 | class AlreadyExists < Error 6 | attr_reader :lockfile 7 | 8 | def initialize(lockfile) 9 | super nil 10 | 11 | @lockfile = lockfile 12 | end 13 | end 14 | 15 | def call(content) 16 | lockfile = build(content) 17 | 18 | lockfile.tap(&:save) 19 | end 20 | 21 | def build(content) 22 | result = Lockfile.new(content: content) 23 | 24 | if existing_lockfile = Lockfile.find_by(slug: result.calculated_slug) 25 | raise AlreadyExists.new(existing_lockfile) 26 | end 27 | 28 | result 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/services/maintenance/hourly.rb: -------------------------------------------------------------------------------- 1 | module Maintenance 2 | class Hourly < Baseline::Service 3 | def call 4 | check_uniqueness 5 | 6 | call_all_private_methods_without_args \ 7 | raise_errors: true 8 | 9 | [ 10 | CheckGitBranches, 11 | EmailNotifications::SendAll 12 | ].each_with_index do |service, index| 13 | service.call_in (index + 1).minutes 14 | end 15 | end 16 | 17 | private 18 | 19 | def delete_old_github_notifications 20 | GithubNotification.created_before(1.month.ago).destroy_all 21 | end 22 | 23 | def check_pending_compats 24 | pending_compats = Compat 25 | .pending 26 | .checked_before(2.hours.ago) 27 | 28 | if pending_compats.any? 29 | ReportError.call "Some compats have been pending for a long time.", 30 | count: pending_compats.size 31 | end 32 | end 33 | 34 | def regenerate_sitemap 35 | SitemapGenerator.verbose = false 36 | SitemapGenerator::Interpreter.run 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/services/rails_releases/create.rb: -------------------------------------------------------------------------------- 1 | module RailsReleases 2 | class Create < Baseline::Service 3 | def call(version) 4 | parsed_version = Gem::Version.new(version) 5 | 6 | return if parsed_version.prerelease? || parsed_version < Gem::Version.new("2.3") 7 | 8 | major, minor = parsed_version.canonical_segments 9 | major_minor_version = "#{major}.#{minor || 0}" 10 | 11 | return if RailsRelease.exists?(version: major_minor_version) 12 | 13 | rails_release = RailsRelease.create!(version: major_minor_version) 14 | 15 | Process.perform_async rails_release.id 16 | 17 | rails_release 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/services/rails_releases/process.rb: -------------------------------------------------------------------------------- 1 | module RailsReleases 2 | class Process < Baseline::Service 3 | def call(rails_release) 4 | Gemmy.find_each do |gemmy| 5 | Gemmies::UpdateCompats.perform_async(gemmy.id) 6 | end 7 | 8 | Compats::CheckUnchecked.perform_async 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/services/report_error.rb: -------------------------------------------------------------------------------- 1 | ReportError = Baseline::ReportError 2 | -------------------------------------------------------------------------------- /app/views/email_notifications/_form.html.haml: -------------------------------------------------------------------------------- 1 | - email_notification = local_assigns.fetch(:email_notification) { EmailNotification.new(notifiable: notifiable) } 2 | 3 | - if email_notification.notifiable.then { _1.compats.none? || _1.compats.pending.any? } 4 | 5 | %section 6 | 7 | %h2 8 | Notify via email 9 | 10 | = turbo_frame_tag :email_notification_form do 11 | = form_with model: email_notification do |form| 12 | 13 | - if email_notification.errors.any? 14 | .error 15 | Oops, an error occurred: 16 | %span.message 17 | = email_notification.errors.full_messages.join(", ") 18 | 19 | .mb-3.col-md-6 20 | = form.label :email, "Enter your email address to receive a notification when all compatibilities are checked.", class: "form-label" 21 | = form.email_field :email, class: "form-control", required: true 22 | %small.form-text 23 | Will only be used for one notification and deleted afterwards. 24 | 25 | = form.hidden_field :notifiable_gid, value: email_notification.notifiable.to_global_id 26 | 27 | = form.submit "Notify me", class: "btn btn-primary", data: { disable_with: t(:please_wait) } 28 | -------------------------------------------------------------------------------- /app/views/email_notifications/create.turbo_stream.haml: -------------------------------------------------------------------------------- 1 | = turbo_stream.update :email_notification_form do 2 | .success 3 | Success! We will notify you when all is done. 4 | -------------------------------------------------------------------------------- /app/views/gemmies/compat_table.html.haml: -------------------------------------------------------------------------------- 1 | - hide_gem_name = local_assigns.fetch(:hide_gem_name) { false } 2 | - stimulus_controller = "compat-table" 3 | - rails_releases = local_assigns.key?(:rails_release) ? RailsRelease.where(id: local_assigns.fetch(:rails_release).id) : RailsRelease.order(:version) 4 | 5 | = async_turbo_frame :compat_table, src: [:compat_table, :gemmies, { inaccessible_gemmy_ids: inaccessible_gemmies.pluck(:id).join(","), gemmy_ids: gemmies.pluck(:id).join(","), hide_gem_name: hide_gem_name }.compact_blank], target: "_top" do 6 | %table.compat-table.table.table-striped{ data: { controller: stimulus_controller } } 7 | 8 | %thead 9 | %tr 10 | - unless hide_gem_name 11 | %th{ rowspan: 2 } 12 | Gem 13 | %th{ colspan: rails_releases.count } 14 | = link_to "Show earlier Rails versions", "#", class: "float-end", data: { action: "#{stimulus_controller}#toggleEarlierRailsVersions" } 15 | Compatible with... 16 | %tr 17 | - rails_releases.each do |rails_release| 18 | - cell_params = rails_release.earlier? ? { data: { "#{stimulus_controller}-target": "earlierRailsVersion" }, class: "d-none" } : {} 19 | 20 | %th.text-nowrap{ **cell_params } 21 | Rails 22 | = rails_release.version 23 | 24 | %tbody 25 | - all_gemmies = (gemmies + inaccessible_gemmies).sort_by(&:name) 26 | - all_gemmies.each do |gemmy| 27 | %tr 28 | - unless hide_gem_name 29 | %td 30 | = display_gemmy_name gemmy 31 | - cache [gemmy, gemmy.compats] do 32 | - rails_releases.each do |rails_release| 33 | - rails_release_compats = gemmy.compats_for_rails_release(rails_release) 34 | - status = compats_status(gemmy, rails_release_compats) 35 | - cell_params = rails_release.earlier? ? { data: { "#{stimulus_controller}-target": "earlierRailsVersion" }, class: "d-none #{status}" } : { class: status } 36 | - label, text = compats_label_and_text(rails_release_compats, gemmy, rails_release) 37 | 38 | %td{ **cell_params } 39 | = render "shared/compat_cell", status: status, label: label, text: text 40 | - if gemmy.accessible_gem? 41 | = "(#{link_to "more", gemmy_rails_release_path(gemmy, rails_release), title: "#{gemmy} gem: Compatibility with Rails #{rails_release.version}"})" 42 | -------------------------------------------------------------------------------- /app/views/gemmies/index.html.haml: -------------------------------------------------------------------------------- 1 | %section 2 | .row 3 | 4 | .col-md-8.col-xl-9 5 | %h1 6 | Welcome to RailsBump! 👋 7 | 8 | %p 9 | RailsBump lets you check your Ruby gems for compatibility with all major Rails versions. This is useful if you're planning to upgrade a Rails app to the next major version. You can either check an individual gem or a "lockfile" generated by 10 | = link_to "Bundler", "https://bundler.io/" 11 | (Gemfile.lock) containing all the gems of your Ruby project. 12 | %br 13 | Read more about how RailsBump works and the history of this project 14 | = link_to "on GitHub", "https://github.com/railsbump/app", target: "_blank" 15 | and please 16 | = link_to "open an issue", "https://github.com/railsbump/app/issues", target: "_blank" 17 | if you encounter any problems. 18 | 19 | #sponsorship.col-md-4.col-xl-3 20 | .card 21 | = image_tag "fastruby-logo.png", class: "card-img-top" 22 | .card-body 23 | %p.card-text 24 | %strong 25 | This project is sponsored by FastRuby.io. 26 | They are a team of Ruby on Rails experts that specialize in 27 | = succeed "." do 28 | = link_to "Rails Upgrade Services", "https://www.fastruby.io?utm_source=railsbump", target: "_blank" 29 | 30 | %section 31 | = link_to "Check a gem", %i(new gemmy), class: "btn btn-primary" 32 | or 33 | = link_to "Check a lockfile", %i(new lockfile), class: "btn btn-primary" 34 | 35 | %section 36 | %h2 37 | Latest checked gems 38 | = render template: "gemmies/compat_table", locals: { gemmies: @gemmies, inaccessible_gemmies: @inaccessible_gemmies } 39 | 40 | %section 41 | %h2 42 | Support 43 | %p 44 | If you find RailsBump useful and would like to support the ongoing development, 45 | = link_to "submit a new issue or pull request", "https://www.github.com/railsbump/app" 46 | or 47 | == #{link_to "become a sponsor", "https://github.com/sponsors/fastruby"}! 🙏 -------------------------------------------------------------------------------- /app/views/gemmies/new.html.haml: -------------------------------------------------------------------------------- 1 | %h1 2 | Check a gem 3 | = render "shared/flashes" 4 | = form_with model: @gemmy do |form| 5 | 6 | .mb-3 7 | = form.text_field :name, class: "form-control", placeholder: "Gem name" 8 | 9 | = form.submit "Check now", class: "btn btn-primary", data: { disable_with: t(:please_wait) } 10 | -------------------------------------------------------------------------------- /app/views/gemmies/show.html.haml: -------------------------------------------------------------------------------- 1 | %section 2 | 3 | %h1 4 | Gem 5 | = @gemmy.name 6 | 7 | %p 8 | Indexed versions: 9 | = @gemmy.versions.size 10 | - if @gemmy.versions.any? 11 | = surround "(", ")" do 12 | = [@gemmy.versions.first, (@gemmy.versions.last if @gemmy.versions.many?)].compact.join(" ... ") 13 | • 14 | = link_to "Show on RubyGems.org", "https://rubygems.org/gems/#{@gemmy.name}", target: "_blank" 15 | 16 | %section 17 | = render template: "gemmies/compat_table", locals: { gemmies: [@gemmy], inaccessible_gemmies: [], hide_gem_name: true } 18 | 19 | = render "email_notifications/form", notifiable: @gemmy 20 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | 3 | %html 4 | %head 5 | %title 6 | = head_title 7 | 8 | = csrf_meta_tags 9 | = csp_meta_tag 10 | 11 | = render "shared/favicons" 12 | 13 | %meta{ charset: "utf-8" } 14 | 15 | %meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge,chrome=1" } 16 | %meta{ "http-equiv": "Content-Type", content: "text/html; charset=utf-8" } 17 | %meta{ name: "description", content: meta_description } 18 | 19 | %meta{ name: "viewport", content: "width=device-width,initial-scale=1" } 20 | 21 | = stylesheet_link_tag :application, data: { turbo_track: "reload" }, media: "all" 22 | 23 | = javascript_include_tag "https://kit.fontawesome.com/0d43dadb44.js", crossorigin: "anonymous" 24 | = javascript_importmap_tags 25 | 26 | - if Rails.env.production? 27 | -# Use the asset host for the API URL, since the Plausible Cloudfront worker doesn't run on railsbump.org but on proxied 28 | = javascript_include_tag "#{ENV['ASSET_HOST']}/js/script.js", defer: true, data: { domain: "railsbump.org", api: asset_url("/api/event", host: ENV['ASSET_HOST']) } 29 | 30 | %body{ class: page_classes } 31 | = render "shared/header" 32 | %main.container-lg 33 | = yield 34 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/lockfiles/new.html.haml: -------------------------------------------------------------------------------- 1 | %h1 2 | Check a lockfile 3 | 4 | = render "shared/flashes" 5 | 6 | = form_with model: @lockfile do |form| 7 | 8 | .mb-3 9 | = form.text_area :content, class: "form-control autosize", placeholder: "Paste the content of your Gemfile.lock here." 10 | 11 | = form.submit "Check now", class: "btn btn-primary", data: { disable_with: t(:please_wait) } 12 | -------------------------------------------------------------------------------- /app/views/lockfiles/show.html.haml: -------------------------------------------------------------------------------- 1 | %section 2 | 3 | %h1 4 | Lockfile check results 5 | 6 | %p 7 | Checked 8 | = l @lockfile.created_at, format: :long 9 | • 10 | = pluralize @lockfile.accessible_and_inaccessible_gemmies.size, "gem" 11 | • 12 | = link_to "Show content", "#", data: { bs_toggle: "modal", bs_target: "#lockfile-content-modal" } 13 | 14 | %section 15 | = render template: "gemmies/compat_table", locals: { gemmies: @lockfile.gemmies, inaccessible_gemmies: @lockfile.inaccessible_gemmies } 16 | 17 | = render "email_notifications/form", notifiable: @lockfile 18 | 19 | #lockfile-content-modal.modal.fade 20 | .modal-dialog.modal-dialog-scrollable.modal-lg 21 | .modal-content 22 | .modal-header 23 | %h1.modal-title.fs-5 24 | Lockfile content 25 | %button.btn-close{ type: "button", data: { bs_dismiss: "modal" } } 26 | .modal-body 27 | %pre 28 | = @lockfile.content 29 | .modal-footer 30 | = link_to "Close", "#", class: "btn btn-primary", data: { bs_dismiss: "modal" } 31 | -------------------------------------------------------------------------------- /app/views/mailers/email_notification.text.erb: -------------------------------------------------------------------------------- 1 | Hi there, 2 | 3 | You left your email to be notified when <%= @notifiable.is_a?(Gemmy) ? %(the gem "#{@notifiable}") : "your lockfile" %> is checked sucessfully. 4 | I am happy to report that this monumental event has just happened! 5 | 6 | Check it out: <%= url_for @notifiable %> 7 | 8 | If you find RailsBump useful and would like to support the ongoing development, you have several options: 9 | 10 | * Submit a new issue or pull request: https://www.github.com/railsbump/app 11 | * Become a sponsor: https://github.com/sponsors/fastruby 12 | 13 | Bye for now, 14 | Your friendly RailsBump bot 15 | -------------------------------------------------------------------------------- /app/views/rails_releases/show.html.haml: -------------------------------------------------------------------------------- 1 | %section 2 | 3 | %h1 4 | Gem 5 | = @gemmy.name 6 | 7 | %p 8 | Indexed versions: 9 | = @gemmy.versions.size 10 | - if @gemmy.versions.any? 11 | = surround "(", ")" do 12 | = [@gemmy.versions.first, (@gemmy.versions.last if @gemmy.versions.many?)].compact.join(" ... ") 13 | • 14 | = link_to "Show on RubyGems.org", "https://rubygems.org/gems/#{@gemmy.name}", target: "_blank" 15 | 16 | %h2 17 | = "Compatibility with Rails: #{@rails_release.version}:" 18 | 19 | - compats = @gemmy.compats 20 | - rails_release_compats = compats.merge(@rails_release.compats) 21 | - status = compats_status(@gemmy, rails_release_compats) 22 | - label, text = compats_label_and_text(rails_release_compats, @gemmy, @rails_release) 23 | 24 | %section{ class: status } 25 | = render "shared/compat_cell", status: status, label: label, text: text 26 | 27 | %section.more 28 | = link_to "Show compatibility with other Rails versions", gemmy_path(@gemmy) 29 | 30 | %section.details 31 | %h3 32 | Gem Details 33 | %p= "This section describes the dependencies associated with each version of the #{@gemmy} gem. ❤️" 34 | %table.compat-table.table.table-striped 35 | %thead 36 | %tr 37 | %th= "#{@gemmy} version" 38 | %th Dependencies 39 | %tbody 40 | - @gemmy.dependencies_and_versions.sort_by { |k, v| v }.each do |dependencies, version| 41 | %tr 42 | %td= version 43 | %td= dependencies 44 | 45 | %section.details 46 | %h3 47 | Compatibility Details 48 | %p 49 | This section is meant to be used for debugging compatibility issues. If you see anything that is unexpected, please share this page with the maintainers. ❤️ 50 | %table.compat-table.table.table-striped 51 | %thead 52 | %tr 53 | %th Status Determined By 54 | %th Status 55 | %th Checked At 56 | %th Dependencies 57 | %tbody 58 | - compats.each do |compat| 59 | %tr 60 | %td= compat.status_determined_by 61 | %td= compat.status 62 | %td= compat.checked_at 63 | %td= compat.dependencies -------------------------------------------------------------------------------- /app/views/shared/_compat_cell.html.haml: -------------------------------------------------------------------------------- 1 | %span{ data: { bs_toggle: "tooltip", bs_placement: "bottom", bs_title: text } } 2 | - case status 3 | - when :checking 4 | %i.fas.fa-spinner.fa-pulse 5 | - when :compatible 6 | %i.far.fa-thumbs-up 7 | - when :inconclusive 8 | %i.far.fa-question-circle 9 | - else 10 | %i.far.fa-thumbs-down 11 | = label -------------------------------------------------------------------------------- /app/views/shared/_favicons.html.haml: -------------------------------------------------------------------------------- 1 | - cache ["favicons", Rails.configuration.revision] do 2 | - Dir[Rails.root.join("app", "assets", "images", "icons", "*.png")].each do |icon| 3 | - next unless size = icon[/-(\d+x\d+)\.png\z/, 1] 4 | - rel = /apple-icon/.match?(icon) ? "apple-touch-icon" : "icon" 5 | %link{ rel: rel, href: image_path("icons/#{File.basename(icon)}"), sizes: size, type: "image/png" } 6 | -------------------------------------------------------------------------------- /app/views/shared/_flashes.html.haml: -------------------------------------------------------------------------------- 1 | - if flash.alert 2 | .errors 3 | .alert.alert-danger{ role: "alert"} 4 | = flash[:alert] -------------------------------------------------------------------------------- /app/views/shared/_header.html.haml: -------------------------------------------------------------------------------- 1 | %header 2 | = navbar container: "lg", expand_at: "md", bg: "dark", brand: "👊 RailsBump" do 3 | = navbar_collapse class: "justify-content-end" do 4 | = navbar_group do 5 | = navbar_item "Check a gem", new_gemmy_path 6 | = navbar_item "Check a lockfile", new_lockfile_path 7 | = navbar_item tag.i(class: %w(fab fa-github)), "https://github.com/railsbump/app", {}, target: "_blank" 8 | -------------------------------------------------------------------------------- /app/views/shared/_loading.html.haml: -------------------------------------------------------------------------------- 1 | .loading 2 | %i.fas.fa-spinner.fa-pulse 3 | Loading, please wait... 4 | -------------------------------------------------------------------------------- /app/views/static/robots.txt.erb: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | Sitemap: <%= ENV["FOG_URL"] %>/sitemap.xml 3 | 4 | User-agent: * 5 | Disallow: /lockfiles/* 6 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bundle install -j $(nproc) 4 | 5 | $(bundle show baseline)/bin/build 6 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # This file was generated by Bundler. 4 | # 5 | # The application 'bundle' is installed as part of a gem, and 6 | # this file is here to facilitate running it. 7 | # 8 | 9 | require "rubygems" 10 | 11 | m = Module.new do 12 | module_function 13 | 14 | def invoked_as_script? 15 | File.expand_path($0) == File.expand_path(__FILE__) 16 | end 17 | 18 | def env_var_version 19 | ENV["BUNDLER_VERSION"] 20 | end 21 | 22 | def cli_arg_version 23 | return unless invoked_as_script? # don't want to hijack other binstubs 24 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 25 | bundler_version = nil 26 | update_index = nil 27 | ARGV.each_with_index do |a, i| 28 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 29 | bundler_version = a 30 | end 31 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 32 | bundler_version = $1 33 | update_index = i 34 | end 35 | bundler_version 36 | end 37 | 38 | def gemfile 39 | gemfile = ENV["BUNDLE_GEMFILE"] 40 | return gemfile if gemfile && !gemfile.empty? 41 | 42 | File.expand_path("../Gemfile", __dir__) 43 | end 44 | 45 | def lockfile 46 | lockfile = 47 | case File.basename(gemfile) 48 | when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") 49 | else "#{gemfile}.lock" 50 | end 51 | File.expand_path(lockfile) 52 | end 53 | 54 | def lockfile_version 55 | return unless File.file?(lockfile) 56 | lockfile_contents = File.read(lockfile) 57 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 58 | Regexp.last_match(1) 59 | end 60 | 61 | def bundler_requirement 62 | @bundler_requirement ||= 63 | env_var_version || 64 | cli_arg_version || 65 | bundler_requirement_for(lockfile_version) 66 | end 67 | 68 | def bundler_requirement_for(version) 69 | return "#{Gem::Requirement.default}.a" unless version 70 | 71 | bundler_gem_version = Gem::Version.new(version) 72 | 73 | bundler_gem_version.approximate_recommendation 74 | end 75 | 76 | def load_bundler! 77 | ENV["BUNDLE_GEMFILE"] ||= gemfile 78 | 79 | activate_bundler 80 | end 81 | 82 | def activate_bundler 83 | gem_error = activation_error_handling do 84 | gem "bundler", bundler_requirement 85 | end 86 | return if gem_error.nil? 87 | require_error = activation_error_handling do 88 | require "bundler/version" 89 | end 90 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 91 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 92 | exit 42 93 | end 94 | 95 | def activation_error_handling 96 | yield 97 | nil 98 | rescue StandardError, LoadError => e 99 | e 100 | end 101 | end 102 | 103 | m.load_bundler! 104 | 105 | if m.invoked_as_script? 106 | load Gem.bin_path("bundler", "bundle") 107 | end 108 | -------------------------------------------------------------------------------- /bin/deploy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $(bundle show baseline)/bin/deploy 4 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if ! gem list foreman -i --silent; then 4 | echo "Installing foreman..." 5 | gem install foreman 6 | fi 7 | 8 | exec foreman start -f Procfile.dev "$@" 9 | -------------------------------------------------------------------------------- /bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /bin/prod-console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $(bundle show baseline)/bin/prod-console many-apps railsbump 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args, exception: true) 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # Install JavaScript dependencies 21 | system!("yarn install") 22 | 23 | puts "\n== Copying sample files ==" 24 | unless File.exist?("config/database.yml") 25 | FileUtils.cp "config/database.yml.sample", "config/database.yml" 26 | end 27 | 28 | unless File.exist?(".env") 29 | FileUtils.cp ".env.sample", ".env" 30 | end 31 | 32 | puts "\n== Preparing database ==" 33 | system! "bin/rails db:prepare" 34 | 35 | puts "\n== Removing old logs and tempfiles ==" 36 | system! "bin/rails log:clear tmp:clear" 37 | 38 | puts "\n== Restarting application server ==" 39 | system! "bin/rails restart" 40 | end 41 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require_relative "config/environment" 2 | 3 | run Rails.application 4 | 5 | Rails.application.load_server 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | # require "action_cable/engine" # Exclude ActionCable 12 | # require "active_storage/engine" # Exclude ActiveStorage 13 | # require "action_mailbox/engine" # Exclude ActionMailbox 14 | 15 | Bundler.require(*Rails.groups) 16 | 17 | module RailsBump 18 | class Application < Rails::Application 19 | config.load_defaults 7.1 20 | config.autoload_lib ignore: %w(assets tasks) 21 | 22 | config.time_zone = "Berlin" 23 | config.revision = ENV.fetch("HEROKU_SLUG_COMMIT") { `git rev-parse HEAD 2> /dev/null`.chomp } 24 | # config.revision = begin 25 | # ENV.fetch("HATCHBOX_REVISION") 26 | # rescue KeyError 27 | # `git rev-parse HEAD 2> /dev/null`.chomp 28 | # end.presence or raise "Could not load revision." 29 | 30 | config.active_record.query_log_tags_enabled = true 31 | config.active_record.sqlite3_production_warning = false 32 | 33 | config.assets.excluded_paths.concat [ 34 | Rails.root.join("app", "assets", "stylesheets") 35 | ] 36 | 37 | config.i18n.raise_on_missing_translations = true 38 | 39 | config.middleware.insert 0, Rack::Deflater 40 | 41 | Rails.application.routes.default_url_options = 42 | config.action_mailer.default_url_options = { 43 | host: ENV.fetch("HOST"), 44 | protocol: "https" 45 | } 46 | 47 | if Rails.version >= "7.2" 48 | raise "this is not needed anymore, yjit should be enabled by default in rails 7.2." 49 | end 50 | # config.after_initialize do 51 | # RubyVM::YJIT.enable 52 | # end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" 4 | require "bootsnap/setup" 5 | -------------------------------------------------------------------------------- /config/database.yml.sample: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: postgresql 9 | encoding: unicode 10 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 11 | timeout: 5000 12 | username: <%= ENV.fetch("POSTGRES_USER") { `echo $USER`} %> 13 | password: <%= ENV.fetch("POSTGRES_PASSWORD") { "" } %> 14 | host: <%= ENV.fetch("POSTGRES_HOST") { "" } %> 15 | 16 | development: 17 | <<: *default 18 | database: <%= ENV.fetch("DB_NAME") { "rails_bump_development" } %> 19 | 20 | # Warning: The database defined as "test" will be erased and 21 | # re-generated from your development database when you run "rake". 22 | # Do not set this db to the same as development or production. 23 | test: 24 | <<: *default 25 | database: rails_bump_test 26 | 27 | production: 28 | <<: *default 29 | database: rails_bump_production -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | require_relative "application" 2 | 3 | Rails.application.initialize! 4 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # In the development environment your application's code is reloaded any time 5 | # it changes. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing 16 | config.server_timing = true 17 | 18 | # Enable/disable caching. By default caching is disabled. 19 | # Run rails dev:cache to toggle caching. 20 | if Rails.root.join("tmp/caching-dev.txt").exist? 21 | config.action_controller.perform_caching = true 22 | config.action_controller.enable_fragment_cache_logging = true 23 | 24 | config.cache_store = :memory_store 25 | config.public_file_server.headers = { 26 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 27 | } 28 | else 29 | config.action_controller.perform_caching = false 30 | 31 | config.cache_store = :null_store 32 | end 33 | 34 | # Store uploaded files on the local file system (see config/storage.yml for options). 35 | # config.active_storage.service = :local 36 | 37 | # Don't care if the mailer can't send. 38 | config.action_mailer.raise_delivery_errors = false 39 | 40 | config.action_mailer.perform_caching = false 41 | 42 | # Print deprecation notices to the Rails logger. 43 | config.active_support.deprecation = :log 44 | 45 | # Raise exceptions for disallowed deprecations. 46 | config.active_support.disallowed_deprecation = :raise 47 | 48 | # Tell Active Support which deprecation messages to disallow. 49 | config.active_support.disallowed_deprecation_warnings = [] 50 | 51 | # Raise an error on page load if there are pending migrations. 52 | config.active_record.migration_error = :page_load 53 | 54 | # Highlight code that triggered database queries in logs. 55 | config.active_record.verbose_query_logs = true 56 | 57 | # Highlight code that enqueued background job in logs. 58 | config.active_job.verbose_enqueue_logs = true 59 | 60 | # Raises error for missing translations. 61 | # config.i18n.raise_on_missing_translations = true 62 | 63 | # Annotate rendered view with file names. 64 | # config.action_view.annotate_rendered_view_with_filenames = true 65 | 66 | # Uncomment if you wish to allow Action Cable access from any origin. 67 | # config.action_cable.disable_request_forgery_protection = true 68 | 69 | # Raise error when a before_action's only/except options reference missing actions 70 | config.action_controller.raise_on_missing_callback_actions = true 71 | 72 | config.hosts << /([a-z]+\.)?#{ENV.fetch "HOST"}/ 73 | end 74 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Code is not reloaded between requests. 5 | config.enable_reloading = false 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment 18 | # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. 22 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 23 | 24 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 25 | # config.asset_host = ENV['ASSET_HOST'] 26 | config.assets.enabled = true 27 | config.assets.compile = false 28 | config.assets.digest = true 29 | config.assets.version = '1.0' 30 | config.assets.css_compressor = :sass 31 | 32 | # Specifies the header that your server uses for sending files. 33 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 34 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 35 | 36 | # Store uploaded files on the local file system (see config/storage.yml for options). 37 | # config.active_storage.service = :cloudflare 38 | 39 | # Mount Action Cable outside main process or domain. 40 | # config.action_cable.mount_path = nil 41 | # config.action_cable.url = "wss://example.com/cable" 42 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 43 | 44 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 45 | # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. 46 | # config.assume_ssl = true 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | config.force_ssl = true 50 | 51 | # Log to STDOUT by default 52 | config.logger = ActiveSupport::Logger.new(STDOUT) 53 | .tap { |logger| logger.formatter = ::Logger::Formatter.new } 54 | .then { |logger| ActiveSupport::TaggedLogging.new(logger) } 55 | 56 | if Rails.version > "7.1.4" 57 | raise "enable multiple loggers when this bug is fixed: https://github.com/rails/rails/issues/49745" 58 | end 59 | # Log to STDOUT and production.log. 60 | # config.logger = [STDOUT, "log/production.log"].map do |destination| 61 | # ActiveSupport::Logger.new(destination, formatter: ::Logger::Formatter.new) 62 | # .then { ActiveSupport::TaggedLogging.new _1 } 63 | # end.then { ActiveSupport::BroadcastLogger.new(*_1) } 64 | 65 | # Prepend all log lines with the following tags. 66 | config.log_tags = [:request_id] 67 | 68 | # Info include generic and useful information about system operation, but avoids logging too much 69 | # information to avoid inadvertent exposure of personally identifiable information (PII). If you 70 | # want to log everything, set the level to "debug". 71 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 72 | 73 | # Use a different cache store in production. 74 | config.cache_store = :redis_cache_store, { 75 | url: ENV["REDIS_URL"], 76 | ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }, 77 | namespace: "cache", 78 | expires_in: 1.month, 79 | reconnect_attempts: 1, 80 | error_handler: -> (method:, returning:, exception:) { 81 | ReportError.call exception, method: method, returning: returning 82 | } 83 | } 84 | 85 | # Use a real queuing backend for Active Job (and separate queues per environment). 86 | # config.active_job.queue_adapter = :resque 87 | # config.active_job.queue_name_prefix = "railsbump_production" 88 | 89 | config.action_mailer.perform_caching = false 90 | 91 | config.action_mailer.smtp_settings = { 92 | user_name: ENV["SMTP_USERNAME"], 93 | password: ENV["SMTP_PASSWORD"], 94 | domain: ENV["SMTP_DOMAIN"], 95 | address: ENV["SMTP_ADDRESS"], 96 | port: ENV["SMTP_PORT"], 97 | authentication: :plain, 98 | enable_starttls_auto: true 99 | } 100 | 101 | # Ignore bad email addresses and do not raise email delivery errors. 102 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 103 | config.action_mailer.raise_delivery_errors = true 104 | 105 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 106 | # the I18n.default_locale when a translation cannot be found). 107 | config.i18n.fallbacks = true 108 | 109 | # Don't log any deprecations. 110 | config.active_support.report_deprecations = false 111 | 112 | # Do not dump schema after migrations. 113 | config.active_record.dump_schema_after_migration = false 114 | 115 | # Enable DNS rebinding protection and other `Host` header attacks. 116 | # config.hosts = [ 117 | # "example.com", # Allow requests from example.com 118 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 119 | # ] 120 | # Skip DNS rebinding protection for the default health check endpoint. 121 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 122 | config.hosts << /([a-z]+\.)?#{ENV.fetch "HOST"}/ 123 | end 124 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # While tests run files are not watched, reloading is not necessary. 5 | config.enable_reloading = false 6 | 7 | # Eager loading loads your entire application. When running a single test locally, 8 | # this is usually not necessary, and can slow down your test suite. However, it's 9 | # recommended that you enable it in continuous integration systems to ensure eager 10 | # loading is working properly before deploying your code. 11 | config.eager_load = ENV["CI"].present? 12 | 13 | # Configure public file server for tests with Cache-Control for performance. 14 | config.public_file_server.enabled = true 15 | config.public_file_server.headers = { 16 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 17 | } 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | config.cache_store = :null_store 23 | 24 | # Render exception templates for rescuable exceptions and raise for other exceptions. 25 | config.action_dispatch.show_exceptions = :rescuable 26 | 27 | # Disable request forgery protection in test environment. 28 | config.action_controller.allow_forgery_protection = false 29 | 30 | # Store uploaded files on the local file system in a temporary directory. 31 | # config.active_storage.service = :test 32 | 33 | config.action_mailer.perform_caching = false 34 | 35 | # Tell Action Mailer not to deliver emails to the real world. 36 | # The :test delivery method accumulates sent emails in the 37 | # ActionMailer::Base.deliveries array. 38 | config.action_mailer.delivery_method = :test 39 | 40 | # Print deprecation notices to the stderr. 41 | config.active_support.deprecation = :stderr 42 | 43 | # Raise exceptions for disallowed deprecations. 44 | config.active_support.disallowed_deprecation = :raise 45 | 46 | # Tell Active Support which deprecation messages to disallow. 47 | config.active_support.disallowed_deprecation_warnings = [] 48 | 49 | # Raises error for missing translations. 50 | # config.i18n.raise_on_missing_translations = true 51 | 52 | # Annotate rendered view with file names. 53 | # config.action_view.annotate_rendered_view_with_filenames = true 54 | 55 | # Raise error when a before_action's only/except options reference missing actions 56 | config.action_controller.raise_on_missing_callback_actions = true 57 | end 58 | -------------------------------------------------------------------------------- /config/importmap.rb: -------------------------------------------------------------------------------- 1 | pin "application" 2 | 3 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" 4 | pin "@hotwired/stimulus", to: "stimulus.min.js" 5 | pin "@hotwired/turbo-rails", to: "turbo.min.js" 6 | pin "autosize", to: "https://cdn.jsdelivr.net/npm/autosize@6.0.1/dist/autosize.min.js" 7 | pin "bootstrap", to: "bootstrap.min.js" 8 | pin "popper", to: "popper.js" 9 | 10 | pin_all_from "app/javascript/controllers", under: "controllers" 11 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Version of your assets, change this if you want to expire all your assets. 2 | Rails.application.config.assets.version = "1.0" 3 | 4 | # Add additional assets to the asset load path. 5 | # Rails.application.config.assets.paths << Emoji.images_path 6 | -------------------------------------------------------------------------------- /config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide content security policy. 2 | # See the Securing Rails Applications Guide for more information: 3 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 4 | 5 | # Rails.application.configure do 6 | # config.content_security_policy do |policy| 7 | # policy.default_src :self, :https 8 | # policy.font_src :self, :https, :data 9 | # policy.img_src :self, :https, :data 10 | # policy.object_src :none 11 | # policy.script_src :self, :https 12 | # policy.style_src :self, :https 13 | # # Specify URI for violation reports 14 | # # policy.report_uri "/csp-violation-report-endpoint" 15 | # end 16 | # 17 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 18 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 19 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 20 | # 21 | # # Report violations without enforcing the policy. 22 | # # config.content_security_policy_report_only = true 23 | # end 24 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 2 | # Use this to limit dissemination of sensitive information. 3 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 4 | Rails.application.config.filter_parameters += [ 5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 6 | ] 7 | -------------------------------------------------------------------------------- /config/initializers/gems.rb: -------------------------------------------------------------------------------- 1 | require "gems" 2 | 3 | Gems.configure do |config| 4 | config.key = ENV["RUBYGEMS_API_KEY"] 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Add new inflection rules using the following format. Inflections 2 | # are locale specific, and you may define rules for as many different 3 | # locales as you wish. All of these examples are active by default: 4 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 5 | # inflect.plural /^(ox)$/i, "\\1en" 6 | # inflect.singular /^(ox)en/i, "\\1" 7 | # inflect.irregular "person", "people" 8 | # inflect.uncountable %w( fish sheep ) 9 | # end 10 | 11 | ActiveSupport::Inflector.inflections :en do |inflect| 12 | inflect.acronym "API" 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | Mime::Type.register_alias "text/plain", :txt -------------------------------------------------------------------------------- /config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Define an application-wide HTTP permissions policy. For further 2 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 3 | 4 | # Rails.application.config.permissions_policy do |policy| 5 | # policy.camera :none 6 | # policy.gyroscope :none 7 | # policy.microphone :none 8 | # policy.usb :none 9 | # policy.fullscreen :self 10 | # policy.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /config/initializers/redis.rb: -------------------------------------------------------------------------------- 1 | Sidekiq.configure_server do |config| 2 | config.redis = { ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }, url: ENV["REDIS_URL"] } 3 | end 4 | 5 | Sidekiq.configure_client do |config| 6 | config.redis = { ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }, url: ENV["REDIS_URL"] } 7 | end 8 | 9 | $redis = Redis.new(url: ENV["REDIS_URL"], ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }) 10 | Baseline.redis = $redis -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | Sentry.init do |config| 2 | config.breadcrumbs_logger = %i(active_support_logger http_logger) 3 | config.include_local_variables = true 4 | config.release = Rails.configuration.revision 5 | config.send_default_pii = true 6 | end 7 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | please_wait: Please wait... 3 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | require "sidekiq" 2 | 3 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 4 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS", max_threads_count) 5 | threads min_threads_count, max_threads_count 6 | 7 | if ENV["RAILS_ENV"] == "production" 8 | require "concurrent-ruby" 9 | worker_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }) 10 | if worker_count > 1 11 | workers worker_count 12 | end 13 | end 14 | 15 | if ENV.fetch("RAILS_ENV", "development") == "development" 16 | worker_timeout 3600 17 | end 18 | 19 | port ENV.fetch("PORT", 3000) 20 | environment ENV.fetch("RAILS_ENV", "development") 21 | pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid") 22 | 23 | plugin :tmp_restart 24 | 25 | preload_app! 26 | 27 | sidekiq = nil 28 | 29 | # on_worker_boot do 30 | # Sidekiq.default_job_options = { 31 | # "retry" => false 32 | # } 33 | # Sidekiq.strict_args!(:warn) 34 | # sidekiq = Sidekiq.configure_embed do |config| 35 | # config.concurrency = 1 36 | # config.redis = { 37 | # url: ENV["REDIS_URL"], 38 | # ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } 39 | # } 40 | # config.merge! \ 41 | # scheduler: { 42 | # schedule: { 43 | # "Compats::CheckUnchecked" => "*/2 * * * *", 44 | # "Maintenance::Hourly" => "0 * * * *" 45 | # }.transform_values { { cron: _1 } } 46 | # } 47 | # end.tap(&:run) 48 | # end 49 | 50 | # on_worker_shutdown do 51 | # sidekiq&.stop 52 | # end 53 | -------------------------------------------------------------------------------- /config/redis/shared.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | url: <%= ENV["REDIS_URL"] %> 3 | ssl_params: 4 | verify_mode: <%= OpenSSL::SSL::VERIFY_NONE %> 5 | timeout: 1 6 | 7 | development: 8 | <<: *default 9 | 10 | test: 11 | <<: *default 12 | 13 | production: 14 | <<: *default 15 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | require "sidekiq/web" 2 | require "sidekiq-scheduler/web" 3 | 4 | if Rails.env.production? 5 | Sidekiq::Web.use Rack::Auth::Basic do |username, password| 6 | { 7 | username => "SIDEKIQ_USERNAME", 8 | password => "SIDEKIQ_PASSWORD" 9 | }.map { ActiveSupport::SecurityUtils.secure_compare _1, ENV.fetch(_2) } 10 | .all? 11 | end 12 | end 13 | 14 | Rails.application.routes.draw do 15 | mount Sidekiq::Web => "sidekiq" 16 | 17 | get '/sitemap.xml', to: 'sitemaps#show' 18 | get "/robots.txt" => "static#robots" 19 | 20 | root "gemmies#index" 21 | 22 | get "up" => "rails/health#show", as: :rails_health_check 23 | 24 | resources :gemmies, path: "gems", only: %i(show new create) do 25 | resources :rails_releases, path: "compatibility", only: %i(show) 26 | 27 | collection do 28 | get :compat_table 29 | end 30 | end 31 | resources :lockfiles, only: %i(new create show) 32 | resources :email_notifications, only: :create 33 | 34 | namespace :api, path: '', constraints: { subdomain: "api" } do 35 | resources :github_notifications, only: :create 36 | resources :releases, only: :create 37 | resources :results, only: :create 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /config/sitemap.rb: -------------------------------------------------------------------------------- 1 | unless Rails.env.local? 2 | require "fog-aws" 3 | 4 | SitemapGenerator::Sitemap.default_host = "https://www.railsbump.org" 5 | SitemapGenerator::Sitemap.public_path = "tmp/sitemaps/" # Temporary storage before uploading to S3 6 | SitemapGenerator::Sitemap.adapter = SitemapGenerator::S3Adapter.new( 7 | fog_provider: "AWS", 8 | fog_directory: "railsbump.org", 9 | fog_region: ENV["AWS_REGION"], 10 | aws_bucket: "railsbump.org", 11 | aws_access_key_id: ENV["AWS_ACCESS_KEY_ID"], 12 | aws_secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"], 13 | aws_region: ENV["AWS_REGION"] 14 | ) 15 | 16 | opts = { 17 | create_index: true, 18 | default_host: "https://www.railsbump.org", 19 | compress: false, 20 | public_path: "/tmp", 21 | sitemaps_host: ENV["FOG_URL"], 22 | sitemaps_path: "" 23 | } 24 | 25 | rails_releases = RailsRelease.order(:version).to_a 26 | 27 | SitemapGenerator::Sitemap.create opts do 28 | # Add static paths 29 | add root_path, changefreq: "daily", priority: 1.0 30 | add new_gemmy_path, changefreq: "monthly" 31 | add new_lockfile_path, changefreq: "monthly" 32 | 33 | # Add dynamic paths for all gemmies 34 | Gemmy.find_each do |gemmy| 35 | add gemmy_path(gemmy), lastmod: gemmy.last_checked_at, changefreq: "weekly", priority: 0.8 36 | 37 | rails_releases.each do |rails_release| 38 | add gemmy_rails_release_path(gemmy, rails_release) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | local: 2 | service: Disk 3 | root: <%= Rails.root.join("storage") %> 4 | 5 | test: 6 | service: Disk 7 | root: <%= Rails.root.join("tmp", "storage") %> 8 | 9 | cloudflare: 10 | service: S3 11 | access_key_id: <%= ENV["CLOUDFLARE_R2_ACCESS_KEY_ID"] %> 12 | secret_access_key: <%= ENV["CLOUDFLARE_R2_SECRET_ACCESS_KEY"] %> 13 | endpoint: https://<%= ENV["CLOUDFLARE_ACCOUNT_ID"] %>.r2.cloudflarestorage.com 14 | bucket: railsbump 15 | region: auto 16 | force_path_style: true 17 | -------------------------------------------------------------------------------- /db/migrate/20191222065024_create_gemmies.rb: -------------------------------------------------------------------------------- 1 | class CreateGemmies < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :gemmies do |t| 4 | t.string :name, index: { unique: true } 5 | t.json :dependencies_and_versions, default: {} 6 | t.json :compat_ids, default: [], null: false 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20191222065443_create_rails_releases.rb: -------------------------------------------------------------------------------- 1 | class CreateRailsReleases < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :rails_releases do |t| 4 | t.string :version, index: { unique: true } 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20191222070102_create_compats.rb: -------------------------------------------------------------------------------- 1 | class CreateCompats < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :compats do |t| 4 | t.jsonb :dependencies 5 | t.string :dependencies_key 6 | t.string :status_determined_by 7 | t.integer :status 8 | t.datetime :checked_at 9 | t.references :rails_release, foreign_key: true 10 | t.timestamps 11 | end 12 | 13 | add_index :compats, [:dependencies_key, :rails_release_id], unique: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20200125221350_create_lockfiles.rb: -------------------------------------------------------------------------------- 1 | class CreateLockfiles < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :lockfiles do |t| 4 | t.text :content 5 | t.string :slug, index: { unique: true } 6 | t.timestamps 7 | end 8 | 9 | enable_extension 'pgcrypto' # Add this line to enable UUID generation 10 | 11 | # change_column :lockfiles, :id, :uuid, default: 'gen_random_uuid()', null: false # Change the primary key column to UUID 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20200125222830_create_lockfile_dependencies.rb: -------------------------------------------------------------------------------- 1 | class CreateLockfileDependencies < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :lockfile_dependencies, id: false do |t| 4 | t.references :lockfile 5 | t.references :gemmy 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20200201222813_create_github_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateGithubNotifications < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :github_notifications do |t| 4 | t.string :conclusion 5 | t.string :action, null: false 6 | t.string :branch, null: false 7 | t.jsonb :data 8 | t.datetime :processed_at 9 | t.references :compat, foreign_key: true 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20240911015959_add_missing_sequences.rb: -------------------------------------------------------------------------------- 1 | class AddMissingSequences < ActiveRecord::Migration[7.1] 2 | def up 3 | up_sequence('gemmies') 4 | up_sequence('compats') 5 | up_sequence('github_notifications') 6 | up_sequence('lockfiles') 7 | up_sequence('rails_releases') 8 | end 9 | 10 | def down 11 | down_sequence('gemmies') 12 | down_sequence('compats') 13 | down_sequence('github_notifications') 14 | down_sequence('lockfiles') 15 | down_sequence('rails_releases') 16 | end 17 | 18 | def down_sequence(_table_name) 19 | # In case of rollback, remove the sequence and reset the column default 20 | execute <<-SQL 21 | ALTER TABLE #{_table_name} ALTER COLUMN id DROP DEFAULT; 22 | SQL 23 | 24 | execute <<-SQL 25 | DROP SEQUENCE IF EXISTS #{_table_name}_id_seq; 26 | SQL 27 | end 28 | 29 | def up_sequence(_table_name) 30 | execute <<-SQL 31 | CREATE SEQUENCE IF NOT EXISTS #{_table_name}_id_seq; 32 | SQL 33 | 34 | execute <<-SQL 35 | ALTER TABLE #{_table_name} ALTER COLUMN id SET DEFAULT nextval('#{_table_name}_id_seq'); 36 | SQL 37 | 38 | # If you have existing data, set the sequence value to the maximum current id to avoid conflicts 39 | execute <<-SQL 40 | SELECT setval('#{_table_name}_id_seq', COALESCE((SELECT MAX(id) FROM #{_table_name}), 1)); 41 | SQL 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /db/migrate/20240927154305_create_api_keys.rb: -------------------------------------------------------------------------------- 1 | class CreateAPIKeys < ActiveRecord::Migration[7.1] 2 | def change 3 | create_table :api_keys do |t| 4 | t.string :name 5 | t.string :key 6 | 7 | t.timestamps 8 | end 9 | 10 | add_index :api_keys, :name, unique: true 11 | add_index :api_keys, :key, unique: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20240930143554_add_columns_to_rails_releases_table.rb: -------------------------------------------------------------------------------- 1 | class AddColumnsToRailsReleasesTable < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :rails_releases, :minimum_ruby_version, :string 4 | add_column :rails_releases, :minimum_bundler_version, :string 5 | add_column :rails_releases, :minimum_rubygems_version, :string 6 | add_column :rails_releases, :maximum_ruby_version, :string 7 | add_column :rails_releases, :maximum_bundler_version, :string 8 | add_column :rails_releases, :maximum_rubygems_version, :string 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20241125191510_create_inaccessible_gemmies.rb: -------------------------------------------------------------------------------- 1 | class CreateInaccessibleGemmies < ActiveRecord::Migration[7.1] 2 | def change 3 | create_table :inaccessible_gemmies do |t| 4 | t.text :name 5 | t.belongs_to :lockfile, null: false, foreign_key: true 6 | t.timestamps 7 | end 8 | 9 | add_index :inaccessible_gemmies, [:lockfile_id, :name], unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.1].define(version: 2024_11_25_191510) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "plpgsql" 16 | 17 | create_table "api_keys", force: :cascade do |t| 18 | t.string "name" 19 | t.string "key" 20 | t.datetime "created_at", null: false 21 | t.datetime "updated_at", null: false 22 | t.index ["key"], name: "index_api_keys_on_key", unique: true 23 | t.index ["name"], name: "index_api_keys_on_name", unique: true 24 | end 25 | 26 | create_table "compats", force: :cascade do |t| 27 | t.bigint "rails_release_id" 28 | t.text "status_determined_by" 29 | t.bigint "status" 30 | t.text "dependencies_key" 31 | t.timestamptz "created_at" 32 | t.timestamptz "updated_at" 33 | t.timestamptz "checked_at" 34 | t.json "dependencies" 35 | end 36 | 37 | create_table "gemmies", force: :cascade do |t| 38 | t.text "name" 39 | t.timestamptz "created_at" 40 | t.timestamptz "updated_at" 41 | t.json "compat_ids", default: [] 42 | t.json "dependencies_and_versions", default: {} 43 | end 44 | 45 | create_table "github_notifications", force: :cascade do |t| 46 | t.text "conclusion" 47 | t.text "action" 48 | t.text "branch" 49 | t.json "data" 50 | t.timestamptz "processed_at" 51 | t.bigint "compat_id" 52 | t.timestamptz "created_at" 53 | t.timestamptz "updated_at" 54 | t.index ["compat_id"], name: "idx_24861_index_github_notifications_on_compat_id" 55 | end 56 | 57 | create_table "inaccessible_gemmies", force: :cascade do |t| 58 | t.text "name" 59 | t.bigint "lockfile_id", null: false 60 | t.datetime "created_at", null: false 61 | t.datetime "updated_at", null: false 62 | t.index ["lockfile_id", "name"], name: "index_inaccessible_gemmies_on_lockfile_id_and_name", unique: true 63 | t.index ["lockfile_id"], name: "index_inaccessible_gemmies_on_lockfile_id" 64 | end 65 | 66 | create_table "lockfile_dependencies", id: false, force: :cascade do |t| 67 | t.bigint "lockfile_id" 68 | t.bigint "gemmy_id" 69 | end 70 | 71 | create_table "lockfiles", force: :cascade do |t| 72 | t.text "content" 73 | t.text "slug" 74 | t.timestamptz "created_at" 75 | t.timestamptz "updated_at" 76 | end 77 | 78 | create_table "rails_releases", force: :cascade do |t| 79 | t.text "version" 80 | t.timestamptz "created_at" 81 | t.timestamptz "updated_at" 82 | t.string "minimum_ruby_version" 83 | t.string "minimum_bundler_version" 84 | t.string "minimum_rubygems_version" 85 | t.string "maximum_ruby_version" 86 | t.string "maximum_bundler_version" 87 | t.string "maximum_rubygems_version" 88 | end 89 | 90 | add_foreign_key "inaccessible_gemmies", "lockfiles" 91 | end 92 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/db/seeds.rb -------------------------------------------------------------------------------- /fastruby-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/fastruby-logo.png -------------------------------------------------------------------------------- /lib/tasks/data.rake: -------------------------------------------------------------------------------- 1 | # lib/tasks/update_rails_releases.rake 2 | namespace :data do 3 | SUPPORTED_RAILS_VERSIONS = %w( 4 | 2.3 5 | 3.0 6 | 3.1 7 | 3.2 8 | 4.0 9 | 4.1 10 | 4.2 11 | 5.0 12 | 5.1 13 | 5.2 14 | 6.0 15 | 6.1 16 | 7.0 17 | 7.1 18 | 7.2 19 | ) 20 | 21 | task find_or_create_rails_releases: :environment do 22 | SUPPORTED_RAILS_VERSIONS.each do |version| 23 | RailsRelease.find_or_create_by(version: version) 24 | end 25 | end 26 | 27 | desc "Update minimum Ruby versions from Rails 2.3 to 7.2" 28 | task update_rails_releases: :find_or_create_rails_releases do 29 | min_versions = { 30 | "2.3" => { 31 | minimum_ruby_version: "1.8.7", 32 | maximum_ruby_version: "1.9.3", 33 | minimum_bundler_version: "1.17.3", 34 | minimum_rubygems_version: "1.3.6" 35 | }, 36 | "3.0" => { 37 | minimum_ruby_version: "1.8.7", 38 | maximum_ruby_version: "1.9.3", 39 | minimum_bundler_version: "1.17.3", 40 | minimum_rubygems_version: "1.3.6" 41 | }, 42 | "3.1" => { 43 | minimum_ruby_version: "1.8.7", 44 | maximum_ruby_version: "2.1.9", 45 | minimum_bundler_version: "1.17.3", 46 | minimum_rubygems_version: "1.3.6" 47 | }, 48 | "3.2" => { 49 | minimum_ruby_version: "1.8.7", 50 | maximum_ruby_version: "2.1.9", 51 | minimum_bundler_version: "1.17.3", 52 | minimum_rubygems_version: "1.3.6" 53 | }, 54 | "4.0" => { 55 | minimum_ruby_version: "1.9.3", 56 | maximum_ruby_version: "2.1.9", 57 | minimum_bundler_version: "1.17.3", 58 | minimum_rubygems_version: "1.3.6" 59 | }, 60 | "4.1" => { 61 | minimum_ruby_version: "1.9.3", 62 | maximum_ruby_version: "2.1.9", 63 | minimum_bundler_version: "1.17.3", 64 | minimum_rubygems_version: "1.3.6" 65 | }, 66 | "4.2" => { 67 | minimum_ruby_version: "1.9.3", 68 | maximum_ruby_version: "2.2.10", 69 | minimum_bundler_version: "1.17.3", 70 | minimum_rubygems_version: "1.3.6" 71 | }, 72 | "5.0" => { 73 | minimum_ruby_version: "2.2.10", 74 | maximum_ruby_version: "2.5.9", 75 | minimum_bundler_version: "1.17.3", 76 | minimum_rubygems_version: "1.3.6" 77 | }, 78 | "5.1" => { 79 | minimum_ruby_version: "2.2.10", 80 | maximum_ruby_version: "2.6.10", 81 | minimum_bundler_version: "1.17.3", 82 | minimum_rubygems_version: "1.3.6" 83 | }, 84 | "5.2" => { 85 | minimum_ruby_version: "2.2.10", 86 | maximum_ruby_version: "2.7.8", 87 | minimum_bundler_version: "1.17.3", 88 | minimum_rubygems_version: "1.3.6" 89 | }, 90 | "6.0" => { 91 | minimum_ruby_version: "2.5.9", 92 | maximum_ruby_version: "3.0.7", 93 | minimum_bundler_version: "2.3.0", 94 | minimum_rubygems_version: "2.5.0" 95 | }, 96 | "6.1" => { 97 | minimum_ruby_version: "2.5.9", 98 | maximum_ruby_version: "3.0.7", 99 | minimum_bundler_version: "2.3.0", 100 | minimum_rubygems_version: "2.5.0" 101 | }, 102 | "7.0" => { 103 | minimum_ruby_version: "2.7.8", 104 | maximum_ruby_version: "3.1.6", 105 | minimum_bundler_version: "2.4.0", 106 | minimum_rubygems_version: "3.0.1" 107 | }, 108 | "7.1" => { 109 | minimum_ruby_version: "2.7.8", 110 | maximum_ruby_version: "3.2.5", 111 | minimum_bundler_version: "2.3.27", 112 | maximum_bundler_version: "2.5.20", 113 | minimum_rubygems_version: "3.0.1" 114 | }, 115 | "7.2" => { 116 | minimum_ruby_version: "3.1.6", 117 | maximum_ruby_version: "3.3.5", 118 | minimum_bundler_version: "2.5.20", 119 | maximum_bundler_version: "2.5.20", 120 | minimum_rubygems_version: "3.2.3" 121 | } 122 | } 123 | 124 | min_versions.each do |version, attrs| 125 | if rails_release = RailsRelease.find_by(version: version) 126 | puts "Updating Rails Release #{rails_release} with #{attrs}" 127 | rails_release.update_columns(attrs) 128 | puts "Updated Rails Release versions" 129 | else 130 | puts "Skipping Rails Release #{version} as it does not exist" 131 | end 132 | end 133 | 134 | puts "Rails Releases updated successfully." 135 | end 136 | end -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rails-bump", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "yarn": "^1.22.10" 6 | } 7 | } -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsBump - Page Not Found 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 34 |
35 | 36 |
37 |
38 |
39 |
40 |

404 - Page Not Found

41 |

Sorry, the page you are looking for does not exist.

42 |

Submit a new issue on GitHub

43 |

Return Home

44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsBump - Unprocessable Entity 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 34 |
35 | 36 |
37 |
38 |
39 |
40 |

422 - Unprocessable Entity

41 |

Sorry, we couldn't process your request. Please check your input and try again.

42 |

Submit a new issue on GitHub

43 |

Return Home

44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsBump - Internal Server Error 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 34 |
35 | 36 |
37 |
38 |
39 |
40 |

500 - Internal Server Error

41 |

Sorry, something went wrong on our end. Please try again later.

42 |

Submit a new issue on GitHub

43 |

Return Home

44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railsbump/app/55dd9e529fbdf49a077324f9f558d53e45c96e4f/public/favicon.ico -------------------------------------------------------------------------------- /spec/cassettes/GemmiesController/create/when_the_gemmy_params_are_valid/creates_a_record_in_the_database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://rubygems.org/api/v1/gems/next_rails.json 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Gems 1.2.0 16 | - Ruby 17 | Connection: 18 | - keep-alive 19 | Keep-Alive: 20 | - '30' 21 | Content-Type: 22 | - application/x-www-form-urlencoded 23 | response: 24 | status: 25 | code: 200 26 | message: OK 27 | headers: 28 | Connection: 29 | - keep-alive 30 | Content-Length: 31 | - '1323' 32 | Content-Type: 33 | - application/json; charset=utf-8 34 | X-Frame-Options: 35 | - SAMEORIGIN 36 | X-Xss-Protection: 37 | - '0' 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Permitted-Cross-Domain-Policies: 41 | - none 42 | Referrer-Policy: 43 | - strict-origin-when-cross-origin 44 | Cross-Origin-Opener-Policy: 45 | - same-origin 46 | Access-Control-Allow-Origin: 47 | - "*" 48 | Access-Control-Allow-Methods: 49 | - GET 50 | Access-Control-Max-Age: 51 | - '1728000' 52 | Cache-Control: 53 | - max-age=60, public 54 | Content-Security-Policy: 55 | - 'default-src ''self''; font-src ''self'' https://fonts.gstatic.com; img-src 56 | ''self'' data: https://secure.gaug.es https://gravatar.com https://www.gravatar.com 57 | https://secure.gravatar.com https://*.fastly-insights.com https://avatars.githubusercontent.com; 58 | object-src ''none''; script-src ''self'' ''sha256-xlpvqdZ5/hVDKsyanT3N+RasZlYKC+bYQ/4furku1bA='' 59 | https://secure.gaug.es https://www.fastly-insights.com ''nonce-57cc98e3ed62ec9f01b2f7447700a6a1''; 60 | style-src ''self'' ''unsafe-inline'' https://fonts.googleapis.com; connect-src 61 | ''self'' https://s3-us-west-2.amazonaws.com/rubygems-dumps/ https://*.fastly-insights.com 62 | https://fastly-insights.com https://api.github.com http://localhost:*; form-action 63 | ''self'' https://github.com/login/oauth/authorize; frame-ancestors ''self''; 64 | base-uri ''self''; report-uri https://csp-report.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pub852fa3e2312391fafa5640b60784e660&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=service%3Arubygems.org%2Cversion%3Ae5b0e6e2a54de1f86ad532a2e86d4c2524d09794%2Cenv%3Aproduction%2Ctrace_id%3A4021853868012513981' 65 | X-Request-Id: 66 | - c949d428-6ebc-43b5-9442-da22f1e21ff6 67 | X-Runtime: 68 | - '0.032452' 69 | Strict-Transport-Security: 70 | - max-age=31536000 71 | X-Backend: 72 | - F_Rails 54.68.67.63:443 73 | Accept-Ranges: 74 | - bytes 75 | Date: 76 | - Mon, 25 Nov 2024 19:39:13 GMT 77 | Via: 78 | - 1.1 varnish 79 | Age: 80 | - '0' 81 | X-Served-By: 82 | - cache-ewr-kewr1740025-EWR 83 | X-Cache: 84 | - HIT 85 | X-Cache-Hits: 86 | - '1' 87 | X-Timer: 88 | - S1732563554.931653,VS0,VE2 89 | Vary: 90 | - Accept-Encoding 91 | Server: 92 | - RubyGems.org 93 | body: 94 | encoding: ASCII-8BIT 95 | string: '{"name":"next_rails","downloads":6087277,"version":"1.4.2","version_created_at":"2024-10-25T17:41:04.118Z","version_downloads":688667,"platform":"ruby","authors":"Ernesto 96 | Tagwerker, Luis Sagastume","info":"A set of handy tools to upgrade your Rails 97 | application and keep it up to date","licenses":["MIT"],"metadata":{},"yanked":false,"sha":"ae72cb83439de812bd44801e33941eda00abe9602bf222404303737de80abec4","spec_sha":"2b8cbbd5d1a947e0812894d78dd5fa17b09a1b8231169d17f04fa96100c9ba81","project_uri":"https://rubygems.org/gems/next_rails","gem_uri":"https://rubygems.org/gems/next_rails-1.4.2.gem","homepage_uri":"https://github.com/fastruby/next_rails","wiki_uri":null,"documentation_uri":"https://www.rubydoc.info/gems/next_rails/1.4.2","mailing_list_uri":null,"source_code_uri":null,"bug_tracker_uri":null,"changelog_uri":null,"funding_uri":null,"dependencies":{"development":[{"name":"bundler","requirements":"\u003e= 98 | 1.16, \u003c 3.0"},{"name":"byebug","requirements":"\u003e= 0"},{"name":"rake","requirements":"\u003e= 99 | 0"},{"name":"rexml","requirements":"= 3.3.8"},{"name":"rspec","requirements":"~\u003e 100 | 3.0"},{"name":"simplecov","requirements":"~\u003e 0.17.1"},{"name":"timecop","requirements":"~\u003e 101 | 0.9.1"},{"name":"webmock","requirements":"= 3.16.2"}],"runtime":[{"name":"rainbow","requirements":"\u003e= 102 | 3"}]}}' 103 | recorded_at: Mon, 25 Nov 2024 19:39:13 GMT 104 | recorded_with: VCR 6.3.1 105 | -------------------------------------------------------------------------------- /spec/cassettes/GemmiesController/create/when_the_gemmy_params_are_valid/redirects_to_the_new_gemmy_page.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://rubygems.org/api/v1/gems/next_rails.json 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Gems 1.2.0 16 | - Ruby 17 | Connection: 18 | - keep-alive 19 | Keep-Alive: 20 | - '30' 21 | Content-Type: 22 | - application/x-www-form-urlencoded 23 | response: 24 | status: 25 | code: 200 26 | message: OK 27 | headers: 28 | Connection: 29 | - keep-alive 30 | Content-Length: 31 | - '1323' 32 | Content-Type: 33 | - application/json; charset=utf-8 34 | X-Frame-Options: 35 | - SAMEORIGIN 36 | X-Xss-Protection: 37 | - '0' 38 | X-Content-Type-Options: 39 | - nosniff 40 | X-Permitted-Cross-Domain-Policies: 41 | - none 42 | Referrer-Policy: 43 | - strict-origin-when-cross-origin 44 | Cross-Origin-Opener-Policy: 45 | - same-origin 46 | Access-Control-Allow-Origin: 47 | - "*" 48 | Access-Control-Allow-Methods: 49 | - GET 50 | Access-Control-Max-Age: 51 | - '1728000' 52 | Cache-Control: 53 | - max-age=60, public 54 | Content-Security-Policy: 55 | - 'default-src ''self''; font-src ''self'' https://fonts.gstatic.com; img-src 56 | ''self'' data: https://secure.gaug.es https://gravatar.com https://www.gravatar.com 57 | https://secure.gravatar.com https://*.fastly-insights.com https://avatars.githubusercontent.com; 58 | object-src ''none''; script-src ''self'' ''sha256-xlpvqdZ5/hVDKsyanT3N+RasZlYKC+bYQ/4furku1bA='' 59 | https://secure.gaug.es https://www.fastly-insights.com ''nonce-57cc98e3ed62ec9f01b2f7447700a6a1''; 60 | style-src ''self'' ''unsafe-inline'' https://fonts.googleapis.com; connect-src 61 | ''self'' https://s3-us-west-2.amazonaws.com/rubygems-dumps/ https://*.fastly-insights.com 62 | https://fastly-insights.com https://api.github.com http://localhost:*; form-action 63 | ''self'' https://github.com/login/oauth/authorize; frame-ancestors ''self''; 64 | base-uri ''self''; report-uri https://csp-report.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pub852fa3e2312391fafa5640b60784e660&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=service%3Arubygems.org%2Cversion%3Ae5b0e6e2a54de1f86ad532a2e86d4c2524d09794%2Cenv%3Aproduction%2Ctrace_id%3A4021853868012513981' 65 | X-Request-Id: 66 | - c949d428-6ebc-43b5-9442-da22f1e21ff6 67 | X-Runtime: 68 | - '0.032452' 69 | Strict-Transport-Security: 70 | - max-age=31536000 71 | X-Backend: 72 | - F_Rails 54.68.67.63:443 73 | Accept-Ranges: 74 | - bytes 75 | Age: 76 | - '0' 77 | Date: 78 | - Mon, 25 Nov 2024 19:39:13 GMT 79 | Via: 80 | - 1.1 varnish 81 | X-Served-By: 82 | - cache-ewr-kewr1740046-EWR 83 | X-Cache: 84 | - MISS 85 | X-Cache-Hits: 86 | - '0' 87 | X-Timer: 88 | - S1732563554.697367,VS0,VE121 89 | Vary: 90 | - Accept-Encoding 91 | Server: 92 | - RubyGems.org 93 | body: 94 | encoding: ASCII-8BIT 95 | string: '{"name":"next_rails","downloads":6087277,"version":"1.4.2","version_created_at":"2024-10-25T17:41:04.118Z","version_downloads":688667,"platform":"ruby","authors":"Ernesto 96 | Tagwerker, Luis Sagastume","info":"A set of handy tools to upgrade your Rails 97 | application and keep it up to date","licenses":["MIT"],"metadata":{},"yanked":false,"sha":"ae72cb83439de812bd44801e33941eda00abe9602bf222404303737de80abec4","spec_sha":"2b8cbbd5d1a947e0812894d78dd5fa17b09a1b8231169d17f04fa96100c9ba81","project_uri":"https://rubygems.org/gems/next_rails","gem_uri":"https://rubygems.org/gems/next_rails-1.4.2.gem","homepage_uri":"https://github.com/fastruby/next_rails","wiki_uri":null,"documentation_uri":"https://www.rubydoc.info/gems/next_rails/1.4.2","mailing_list_uri":null,"source_code_uri":null,"bug_tracker_uri":null,"changelog_uri":null,"funding_uri":null,"dependencies":{"development":[{"name":"bundler","requirements":"\u003e= 98 | 1.16, \u003c 3.0"},{"name":"byebug","requirements":"\u003e= 0"},{"name":"rake","requirements":"\u003e= 99 | 0"},{"name":"rexml","requirements":"= 3.3.8"},{"name":"rspec","requirements":"~\u003e 100 | 3.0"},{"name":"simplecov","requirements":"~\u003e 0.17.1"},{"name":"timecop","requirements":"~\u003e 101 | 0.9.1"},{"name":"webmock","requirements":"= 3.16.2"}],"runtime":[{"name":"rainbow","requirements":"\u003e= 102 | 3"}]}}' 103 | recorded_at: Mon, 25 Nov 2024 19:39:13 GMT 104 | recorded_with: VCR 6.3.1 105 | -------------------------------------------------------------------------------- /spec/controllers/api/results_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe API::ResultsController, type: :controller do 4 | let(:api_key) { FactoryBot.create(:api_key) } 5 | let(:rails_release) { FactoryBot.create(:rails_release) } 6 | let(:compat) { FactoryBot.create(:compat, rails_release: rails_release, status: :pending) } 7 | let(:headers) { { 'RAILS-BUMP-API-KEY' => api_key.key } } 8 | 9 | describe "POST #create" do 10 | context "with valid dependencies and result is successful" do 11 | it "updates the compat and returns ok" do 12 | request.headers.merge!(headers) 13 | 14 | post :create, params: { 15 | rails_version: rails_release.version, 16 | compat_id: compat.id, 17 | dependencies: compat.dependencies, 18 | result: { 19 | success: true, 20 | strategy: 'some_strategy' 21 | } 22 | } 23 | 24 | expect(compat.reload.status).to eq('compatible') 25 | expect(response).to have_http_status(:ok) 26 | end 27 | end 28 | 29 | context "with valid dependencies and result is unsuccessful" do 30 | it "updates the compat and returns ok" do 31 | request.headers.merge!(headers) 32 | 33 | post :create, params: { 34 | rails_version: rails_release.version, 35 | compat_id: compat.id, 36 | dependencies: compat.dependencies, 37 | result: { 38 | success: false, 39 | strategy: 'some_strategy' 40 | } 41 | } 42 | 43 | expect(compat.reload.status).to eq('incompatible') 44 | expect(response).to have_http_status(:ok) 45 | end 46 | end 47 | 48 | context "with invalid dependencies" do 49 | it "returns unprocessable entity" do 50 | request.headers.merge!(headers) 51 | post :create, params: { 52 | rails_version: rails_release.version, 53 | compat_id: compat.id, 54 | dependencies: { invalid: 'dependency' }, 55 | result: 'some_result' 56 | } 57 | 58 | expect(response).to have_http_status(:unprocessable_entity) 59 | end 60 | end 61 | 62 | context "with invalid API key" do 63 | it "returns unauthorized" do 64 | request.headers.merge!({ 'RAILS-BUMP-API-KEY' => '' }) 65 | post :create, params: { 66 | rails_version: rails_release.version, 67 | compat_id: compat.id, 68 | dependencies: compat.dependencies, 69 | result: 'some_result' 70 | } 71 | 72 | expect(response).to have_http_status(:unauthorized) 73 | end 74 | end 75 | 76 | context "if result processing fails" do 77 | it "returns unprocessable entity" do 78 | allow_any_instance_of(Compat).to receive(:process_result).and_return(false) 79 | 80 | request.headers.merge!(headers) 81 | 82 | post :create, params: { 83 | rails_version: rails_release.version, 84 | compat_id: compat.id, 85 | dependencies: compat.dependencies, 86 | result: 'some_result' 87 | } 88 | 89 | expect(response).to have_http_status(:unprocessable_entity) 90 | end 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /spec/controllers/gemmies_controllers_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe GemmiesController, type: :controller do 4 | describe "create", vcr: { record: :once } do 5 | context "when the gemmy params are valid" do 6 | it "redirects to the new gemmy page" do 7 | post :create, params: { gemmy: { name: "next_rails" } } 8 | 9 | expect(response).to redirect_to(gemmy_path(Gemmy.find_by_name("next_rails"))) 10 | end 11 | 12 | it "creates a record in the database" do 13 | expect do 14 | post :create, params: { gemmy: { name: "next_rails" } } 15 | end.to change(Gemmy, :count).by(1) 16 | end 17 | 18 | context "when the gemmy params are invalid" do 19 | it "renders the new gemmy page" do 20 | post :create, params: { gemmy: { name: "" } } 21 | 22 | expect(response).to render_template(:new) 23 | end 24 | 25 | it "does not create a record in the database" do 26 | expect do 27 | post :create, params: { gemmy: { name: "" } } 28 | end.not_to change(Gemmy, :count) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/controllers/lockfiles_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe LockfilesController, type: :controller, vcr: { record: :once } do 4 | let(:lockfile) { FactoryBot.build(:lockfile) } 5 | 6 | describe "GET #new" do 7 | it "returns a success response" do 8 | get :new 9 | expect(response).to be_successful 10 | end 11 | end 12 | 13 | describe "POST #create" do 14 | context "with valid params" do 15 | it "creates a new Lockfile" do 16 | expect do 17 | post :create, params: { lockfile: { content: lockfile.content } } 18 | end.to change(Lockfile, :count).by(1) 19 | 20 | expect(response).to redirect_to(Lockfile.last) 21 | end 22 | 23 | context "and Gemfile.lock content was already submitted in the past" do 24 | it "redirects to the existing lockfile" do 25 | expect do 26 | post :create, params: { lockfile: { content: lockfile.content } } 27 | end.to change(Lockfile, :count).by(1) 28 | 29 | expect(response).to redirect_to(Lockfile.last) 30 | 31 | expect do 32 | post :create, params: { lockfile: { content: lockfile.content } } 33 | end.not_to change(Lockfile, :count) 34 | 35 | expect(response).to redirect_to(Lockfile.last) 36 | end 37 | end 38 | 39 | context "and Gemfile.lock content has local paths" do 40 | it "creates a new Lockfile" do 41 | content = File.read("spec/fixtures/Gemfile.local.lock") 42 | 43 | expect do 44 | post :create, params: { lockfile: { content: content } } 45 | end.to change(Lockfile, :count).by(1) 46 | 47 | expect(response).to redirect_to(Lockfile.last) 48 | end 49 | 50 | it "creates n inaccessible gemmies in the database" do 51 | content = File.read("spec/fixtures/Gemfile.local.lock") 52 | 53 | expect do 54 | post :create, params: { lockfile: { content: content } } 55 | end.to change(InaccessibleGemmy, :count).by(2) 56 | 57 | lockfile = Lockfile.last 58 | 59 | expect(lockfile.inaccessible_gemmies.map(&:name)).to match_array(["gitlab-specific-attr-ancrypted", "openbao_client"]) 60 | end 61 | end 62 | end 63 | 64 | context "when lockfile already exists" do 65 | before do 66 | allow(Lockfiles::Create).to receive(:call).and_raise(Lockfiles::Create::AlreadyExists.new(lockfile)) 67 | end 68 | 69 | it "redirects to the existing lockfile" do 70 | post :create, params: { lockfile: { content: 'existing lockfile content' } } 71 | expect(response).to redirect_to(lockfile) 72 | end 73 | end 74 | 75 | context "when a general error occurs" do 76 | it "renders the new template with an error message" do 77 | post :create, params: { lockfile: { content: 'invalid lockfile content' } } 78 | 79 | expect(response).to redirect_to(new_lockfile_path) 80 | expect(flash[:alert]).to eq("Gemmies can't be blank. Content does not look like a valid lockfile.. Content No gems found in content.") 81 | end 82 | end 83 | end 84 | 85 | describe "GET #show" do 86 | before { lockfile.save } 87 | 88 | it "returns a success response" do 89 | get :show, params: { id: lockfile.to_param } 90 | expect(response).to be_successful 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /spec/factories/api_key_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :api_key do 3 | name { "rails-bump-checker-action" } 4 | key { "rails-bump-checker-action-123123123123123123123123123-123123123123123123123123123" } 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/compat_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :compat do 3 | rails_release { RailsRelease.last || FactoryBot.create(:rails_release) } 4 | 5 | dependencies { {"mail"=>"~> 2.2", "rspec"=>"~> 2.0"} } 6 | end 7 | end -------------------------------------------------------------------------------- /spec/factories/dependency_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :dependency do 3 | compat 4 | end 5 | end -------------------------------------------------------------------------------- /spec/factories/gemmy_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :gemmy do 3 | name { "madmin"} 4 | end 5 | end -------------------------------------------------------------------------------- /spec/factories/lockfile_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :lockfile do 3 | slug { ActiveSupport::Digest.hexdigest("rails#rspec") } 4 | content { File.read("spec/fixtures/Gemfile.lock") } 5 | end 6 | end -------------------------------------------------------------------------------- /spec/factories/rails_release_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :rails_release do 3 | version { "7.1" } 4 | end 5 | end -------------------------------------------------------------------------------- /spec/fixtures/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/fastruby/baseline.git 3 | revision: 60a5488d068233c1b67a025bfed1deb5b156c25a 4 | specs: 5 | baseline (1.0) 6 | 7 | GIT 8 | remote: https://github.com/ruby/net-pop.git 9 | revision: e8d0afe2773b9eb6a23c39e9e437f6fc0fc7c733 10 | specs: 11 | net-pop (0.1.2) 12 | 13 | GIT 14 | remote: https://github.com/rubygems/gems.git 15 | revision: c20c05888cb2b882bf3b99d3618cc37f69559f6a 16 | specs: 17 | gems (1.2.0) 18 | 19 | GEM 20 | remote: https://rubygems.org/ 21 | specs: 22 | actioncable (7.1.3.4) 23 | actionpack (= 7.1.3.4) 24 | activesupport (= 7.1.3.4) 25 | nio4r (~> 2.0) 26 | websocket-driver (>= 0.6.1) 27 | zeitwerk (~> 2.6) 28 | actionmailbox (7.1.3.4) 29 | actionpack (= 7.1.3.4) 30 | activejob (= 7.1.3.4) 31 | activerecord (= 7.1.3.4) 32 | activestorage (= 7.1.3.4) 33 | activesupport (= 7.1.3.4) 34 | mail (>= 2.7.1) 35 | net-imap 36 | net-pop 37 | net-smtp 38 | actionmailer (7.1.3.4) 39 | actionpack (= 7.1.3.4) 40 | actionview (= 7.1.3.4) 41 | activejob (= 7.1.3.4) 42 | activesupport (= 7.1.3.4) 43 | mail (~> 2.5, >= 2.5.4) 44 | net-imap 45 | net-pop 46 | net-smtp 47 | rails-dom-testing (~> 2.2) 48 | actionpack (7.1.3.4) 49 | actionview (= 7.1.3.4) 50 | activesupport (= 7.1.3.4) 51 | nokogiri (>= 1.8.5) 52 | racc 53 | rack (>= 2.2.4) 54 | rack-session (>= 1.0.1) 55 | rack-test (>= 0.6.3) 56 | rails-dom-testing (~> 2.2) 57 | rails-html-sanitizer (~> 1.6) 58 | actiontext (7.1.3.4) 59 | actionpack (= 7.1.3.4) 60 | activerecord (= 7.1.3.4) 61 | activestorage (= 7.1.3.4) 62 | activesupport (= 7.1.3.4) 63 | globalid (>= 0.6.0) 64 | nokogiri (>= 1.8.5) 65 | actionview (7.1.3.4) 66 | activesupport (= 7.1.3.4) 67 | builder (~> 3.1) 68 | erubi (~> 1.11) 69 | rails-dom-testing (~> 2.2) 70 | rails-html-sanitizer (~> 1.6) 71 | activejob (7.1.3.4) 72 | activesupport (= 7.1.3.4) 73 | globalid (>= 0.3.6) 74 | activemodel (7.1.3.4) 75 | activesupport (= 7.1.3.4) 76 | activerecord (7.1.3.4) 77 | activemodel (= 7.1.3.4) 78 | activesupport (= 7.1.3.4) 79 | timeout (>= 0.4.0) 80 | activestorage (7.1.3.4) 81 | actionpack (= 7.1.3.4) 82 | activejob (= 7.1.3.4) 83 | activerecord (= 7.1.3.4) 84 | activesupport (= 7.1.3.4) 85 | marcel (~> 1.0) 86 | activesupport (7.1.3.4) 87 | base64 88 | bigdecimal 89 | concurrent-ruby (~> 1.0, >= 1.0.2) 90 | connection_pool (>= 2.2.5) 91 | drb 92 | i18n (>= 1.6, < 2) 93 | minitest (>= 5.1) 94 | mutex_m 95 | tzinfo (~> 2.0) 96 | addressable (2.8.7) 97 | public_suffix (>= 2.0.2, < 7.0) 98 | amazing_print (1.6.0) 99 | annotaterb (4.10.2) 100 | autoprefixer-rails (10.4.16.0) 101 | execjs (~> 2) 102 | aws-eventstream (1.3.0) 103 | aws-partitions (1.957.0) 104 | aws-sdk-core (3.201.3) 105 | aws-eventstream (~> 1, >= 1.3.0) 106 | aws-partitions (~> 1, >= 1.651.0) 107 | aws-sigv4 (~> 1.8) 108 | jmespath (~> 1, >= 1.6.1) 109 | aws-sdk-kms (1.88.0) 110 | aws-sdk-core (~> 3, >= 3.201.0) 111 | aws-sigv4 (~> 1.5) 112 | aws-sdk-s3 (1.156.0) 113 | aws-sdk-core (~> 3, >= 3.201.0) 114 | aws-sdk-kms (~> 1) 115 | aws-sigv4 (~> 1.5) 116 | aws-sigv4 (1.9.0) 117 | aws-eventstream (~> 1, >= 1.0.2) 118 | base64 (0.2.0) 119 | better_errors (2.10.1) 120 | erubi (>= 1.0.0) 121 | rack (>= 0.9.0) 122 | rouge (>= 1.0.0) 123 | bigdecimal (3.1.8) 124 | binding_of_caller (1.0.1) 125 | debug_inspector (>= 1.2.0) 126 | bootsnap (1.18.3) 127 | msgpack (~> 1.2) 128 | bootstrap (5.3.3) 129 | autoprefixer-rails (>= 9.1.0) 130 | popper_js (>= 2.11.8, < 3) 131 | bootstrap-navbar (3.2.2) 132 | gem_config (~> 0.3) 133 | builder (3.3.0) 134 | byebug (11.1.3) 135 | coderay (1.1.3) 136 | concurrent-ruby (1.3.3) 137 | connection_pool (2.4.1) 138 | crack (1.0.0) 139 | bigdecimal 140 | rexml 141 | crass (1.0.6) 142 | dartsass-rails (0.5.0) 143 | railties (>= 6.0.0) 144 | sass-embedded (~> 1.63) 145 | database_cleaner-active_record (2.2.0) 146 | activerecord (>= 5.a) 147 | database_cleaner-core (~> 2.0.0) 148 | database_cleaner-core (2.0.1) 149 | date (3.3.4) 150 | debug_inspector (1.2.0) 151 | diff-lcs (1.5.1) 152 | docile (1.4.1) 153 | dotenv (3.1.2) 154 | drb (2.2.1) 155 | erubi (1.13.0) 156 | et-orbi (1.2.11) 157 | tzinfo 158 | execjs (2.9.1) 159 | factory_bot (6.4.6) 160 | activesupport (>= 5.0.0) 161 | factory_bot_rails (6.4.3) 162 | factory_bot (~> 6.4) 163 | railties (>= 5.0.0) 164 | faraday (2.10.0) 165 | faraday-net_http (>= 2.0, < 3.2) 166 | logger 167 | faraday-net_http (3.1.0) 168 | net-http 169 | fugit (1.11.0) 170 | et-orbi (~> 1, >= 1.2.11) 171 | raabro (~> 1.4) 172 | gem_config (0.3.2) 173 | git (2.1.1) 174 | activesupport (>= 5.0) 175 | addressable (~> 2.8) 176 | process_executer (~> 1.1) 177 | rchardet (~> 1.8) 178 | globalid (1.2.1) 179 | activesupport (>= 6.1) 180 | google-protobuf (4.27.2-arm64-darwin) 181 | bigdecimal 182 | rake (>= 13) 183 | google-protobuf (4.27.2-x86_64-linux) 184 | bigdecimal 185 | rake (>= 13) 186 | haml (6.3.0) 187 | temple (>= 0.8.2) 188 | thor 189 | tilt 190 | hashdiff (1.1.1) 191 | i18n (1.14.5) 192 | concurrent-ruby (~> 1.0) 193 | importmap-rails (2.0.1) 194 | actionpack (>= 6.0.0) 195 | activesupport (>= 6.0.0) 196 | railties (>= 6.0.0) 197 | io-console (0.7.2) 198 | irb (1.14.0) 199 | rdoc (>= 4.0.0) 200 | reline (>= 0.4.2) 201 | jmespath (1.6.2) 202 | kredis (1.7.0) 203 | activemodel (>= 6.0.0) 204 | activesupport (>= 6.0.0) 205 | redis (>= 4.2, < 6) 206 | logger (1.6.0) 207 | loofah (2.22.0) 208 | crass (~> 1.0.2) 209 | nokogiri (>= 1.12.0) 210 | mail (2.8.1) 211 | mini_mime (>= 0.1.1) 212 | net-imap 213 | net-pop 214 | net-smtp 215 | marcel (1.0.4) 216 | method_source (1.1.0) 217 | mini_mime (1.1.5) 218 | minitest (5.24.1) 219 | msgpack (1.7.2) 220 | mutex_m (0.2.0) 221 | net-http (0.4.1) 222 | uri 223 | net-imap (0.4.14) 224 | date 225 | net-protocol 226 | net-protocol (0.2.2) 227 | timeout 228 | net-smtp (0.5.0) 229 | net-protocol 230 | nio4r (2.7.3) 231 | nokogiri (1.16.7-arm64-darwin) 232 | racc (~> 1.4) 233 | nokogiri (1.16.7-x86_64-linux) 234 | racc (~> 1.4) 235 | octokit (9.1.0) 236 | faraday (>= 1, < 3) 237 | sawyer (~> 0.9) 238 | octopoller (0.3.1) 239 | pg (1.5.6) 240 | popper_js (2.11.8) 241 | process_executer (1.1.0) 242 | propshaft (0.9.0) 243 | actionpack (>= 7.0.0) 244 | activesupport (>= 7.0.0) 245 | rack 246 | railties (>= 7.0.0) 247 | pry (0.14.2) 248 | coderay (~> 1.1) 249 | method_source (~> 1.0) 250 | pry-rails (0.3.11) 251 | pry (>= 0.13.0) 252 | psych (5.1.2) 253 | stringio 254 | public_suffix (6.0.1) 255 | puma (6.4.2) 256 | nio4r (~> 2.0) 257 | raabro (1.4.0) 258 | racc (1.8.0) 259 | rack (3.1.7) 260 | rack-session (2.0.0) 261 | rack (>= 3.0.0) 262 | rack-test (2.1.0) 263 | rack (>= 1.3) 264 | rackup (2.1.0) 265 | rack (>= 3) 266 | webrick (~> 1.8) 267 | rails (7.1.3.4) 268 | actioncable (= 7.1.3.4) 269 | actionmailbox (= 7.1.3.4) 270 | actionmailer (= 7.1.3.4) 271 | actionpack (= 7.1.3.4) 272 | actiontext (= 7.1.3.4) 273 | actionview (= 7.1.3.4) 274 | activejob (= 7.1.3.4) 275 | activemodel (= 7.1.3.4) 276 | activerecord (= 7.1.3.4) 277 | activestorage (= 7.1.3.4) 278 | activesupport (= 7.1.3.4) 279 | bundler (>= 1.15.0) 280 | railties (= 7.1.3.4) 281 | rails-controller-testing (1.0.5) 282 | actionpack (>= 5.0.1.rc1) 283 | actionview (>= 5.0.1.rc1) 284 | activesupport (>= 5.0.1.rc1) 285 | rails-dom-testing (2.2.0) 286 | activesupport (>= 5.0.0) 287 | minitest 288 | nokogiri (>= 1.6) 289 | rails-html-sanitizer (1.6.0) 290 | loofah (~> 2.21) 291 | nokogiri (~> 1.14) 292 | rails-i18n (7.0.9) 293 | i18n (>= 0.7, < 2) 294 | railties (>= 6.0.0, < 8) 295 | rails_bootstrap_navbar (3.0.0) 296 | bootstrap-navbar (~> 3.0) 297 | rails (>= 3.0.0) 298 | railties (7.1.3.4) 299 | actionpack (= 7.1.3.4) 300 | activesupport (= 7.1.3.4) 301 | irb 302 | rackup (>= 1.0.0) 303 | rake (>= 12.2) 304 | thor (~> 1.0, >= 1.2.2) 305 | zeitwerk (~> 2.6) 306 | rake (13.2.1) 307 | rchardet (1.8.0) 308 | rdoc (6.7.0) 309 | psych (>= 4.0.0) 310 | redis (5.2.0) 311 | redis-client (>= 0.22.0) 312 | redis-client (0.22.2) 313 | connection_pool 314 | redis-namespace (1.11.0) 315 | redis (>= 4) 316 | reline (0.5.9) 317 | io-console (~> 0.5) 318 | rexml (3.3.7) 319 | rouge (4.3.0) 320 | rspec-core (3.13.0) 321 | rspec-support (~> 3.13.0) 322 | rspec-expectations (3.13.1) 323 | diff-lcs (>= 1.2.0, < 2.0) 324 | rspec-support (~> 3.13.0) 325 | rspec-mocks (3.13.1) 326 | diff-lcs (>= 1.2.0, < 2.0) 327 | rspec-support (~> 3.13.0) 328 | rspec-rails (6.1.4) 329 | actionpack (>= 6.1) 330 | activesupport (>= 6.1) 331 | railties (>= 6.1) 332 | rspec-core (~> 3.13) 333 | rspec-expectations (~> 3.13) 334 | rspec-mocks (~> 3.13) 335 | rspec-support (~> 3.13) 336 | rspec-support (3.13.1) 337 | rufus-scheduler (3.9.1) 338 | fugit (~> 1.1, >= 1.1.6) 339 | sass-embedded (1.77.8-arm64-darwin) 340 | google-protobuf (~> 4.26) 341 | sass-embedded (1.77.8-x86_64-linux-gnu) 342 | google-protobuf (~> 4.26) 343 | sawyer (0.9.2) 344 | addressable (>= 2.3.5) 345 | faraday (>= 0.17.3, < 3) 346 | sentry-rails (5.18.2) 347 | railties (>= 5.0) 348 | sentry-ruby (~> 5.18.2) 349 | sentry-ruby (5.18.2) 350 | bigdecimal 351 | concurrent-ruby (~> 1.0, >= 1.0.2) 352 | sentry-sidekiq (5.18.2) 353 | sentry-ruby (~> 5.18.2) 354 | sidekiq (>= 3.0) 355 | sidekiq (7.3.0) 356 | concurrent-ruby (< 2) 357 | connection_pool (>= 2.3.0) 358 | logger 359 | rack (>= 2.2.4) 360 | redis-client (>= 0.22.2) 361 | sidekiq-scheduler (5.0.5) 362 | rufus-scheduler (~> 3.2) 363 | sidekiq (>= 6, < 8) 364 | tilt (>= 1.4.0, < 3) 365 | simplecov (0.22.0) 366 | docile (~> 1.1) 367 | simplecov-html (~> 0.11) 368 | simplecov_json_formatter (~> 0.1) 369 | simplecov-html (0.13.1) 370 | simplecov_json_formatter (0.1.4) 371 | sitemap_generator (6.3.0) 372 | builder (~> 3.0) 373 | stimulus-rails (1.3.3) 374 | railties (>= 6.0.0) 375 | stringio (3.1.1) 376 | temple (0.10.3) 377 | thor (1.3.1) 378 | tilt (2.4.0) 379 | timeout (0.4.1) 380 | turbo-rails (2.0.6) 381 | actionpack (>= 6.0.0) 382 | activejob (>= 6.0.0) 383 | railties (>= 6.0.0) 384 | tzinfo (2.0.6) 385 | concurrent-ruby (~> 1.0) 386 | uri (0.13.0) 387 | vcr (6.3.1) 388 | base64 389 | webmock (3.23.1) 390 | addressable (>= 2.8.0) 391 | crack (>= 0.3.2) 392 | hashdiff (>= 0.4.0, < 2.0.0) 393 | webrick (1.8.1) 394 | websocket-driver (0.7.6) 395 | websocket-extensions (>= 0.1.0) 396 | websocket-extensions (0.1.5) 397 | zeitwerk (2.6.17) 398 | 399 | PLATFORMS 400 | arm64-darwin-23 401 | x86_64-linux 402 | 403 | DEPENDENCIES 404 | amazing_print (~> 1.5) 405 | annotaterb (~> 4.4) 406 | aws-sdk-s3 (~> 1.8) 407 | baseline! 408 | better_errors (~> 2.8) 409 | binding_of_caller (~> 1.0) 410 | bootsnap (~> 1.17) 411 | bootstrap (~> 5.3.3) 412 | byebug (~> 11.1) 413 | dartsass-rails (~> 0.5) 414 | database_cleaner-active_record 415 | dotenv (~> 3.1.2) 416 | factory_bot_rails (~> 6.2) 417 | gems! 418 | git (~> 2.1) 419 | haml (~> 6.0) 420 | importmap-rails (~> 2.0) 421 | kredis (~> 1.2) 422 | net-pop! 423 | octokit (~> 9.1) 424 | octopoller (~> 0.3) 425 | pg 426 | propshaft (~> 0.8) 427 | pry-rails (~> 0.3) 428 | puma (~> 6.4) 429 | rails (~> 7.1.3.4) 430 | rails-controller-testing 431 | rails-i18n (~> 7.0) 432 | rails_bootstrap_navbar (~> 3.0) 433 | redis (~> 5.0) 434 | redis-namespace (~> 1.11) 435 | rspec-rails (~> 6.1.0) 436 | sentry-rails (~> 5.5) 437 | sentry-sidekiq (~> 5.5) 438 | sidekiq (~> 7.2) 439 | sidekiq-scheduler (~> 5.0) 440 | simplecov (~> 0.22) 441 | sitemap_generator (~> 6.3) 442 | stimulus-rails (~> 1.3) 443 | turbo-rails (~> 2.0) 444 | vcr 445 | webmock 446 | 447 | RUBY VERSION 448 | ruby 3.3.3p89 449 | 450 | BUNDLED WITH 451 | 2.5.13 452 | -------------------------------------------------------------------------------- /spec/models/api_key_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe APIKey, type: :model do 4 | describe "#valid?" do 5 | let(:valid_attributes) { { name: "Valid API Key", key: "a" * 64 } } 6 | 7 | it "is valid with valid attributes" do 8 | api_key = APIKey.new(valid_attributes) 9 | expect(api_key).to be_valid 10 | end 11 | 12 | it "is invalid without a name" do 13 | api_key = APIKey.new(valid_attributes.except(:name)) 14 | expect(api_key).not_to be_valid 15 | expect(api_key.errors[:name]).to include("can't be blank") 16 | end 17 | 18 | it "is invalid without a key" do 19 | api_key = APIKey.new(valid_attributes.except(:key)) 20 | expect(api_key).not_to be_valid 21 | expect(api_key.errors[:key]).to include("can't be blank") 22 | end 23 | 24 | it "is invalid with a duplicate name" do 25 | APIKey.create!(valid_attributes) 26 | api_key = APIKey.new(valid_attributes) 27 | expect(api_key).not_to be_valid 28 | expect(api_key.errors[:name]).to include("has already been taken") 29 | end 30 | 31 | it "is invalid with a duplicate key" do 32 | APIKey.create!(valid_attributes) 33 | api_key = APIKey.new(valid_attributes) 34 | expect(api_key).not_to be_valid 35 | expect(api_key.errors[:key]).to include("has already been taken") 36 | end 37 | 38 | it "is invalid with a name longer than 50 characters" do 39 | api_key = APIKey.new(valid_attributes.merge(name: "a" * 51)) 40 | expect(api_key).not_to be_valid 41 | expect(api_key.errors[:name]).to include("is too long (maximum is 50 characters)") 42 | end 43 | 44 | it "is invalid with a key shorter than 64 characters" do 45 | api_key = APIKey.new(valid_attributes.merge(key: "a" * 63)) 46 | expect(api_key).not_to be_valid 47 | expect(api_key.errors[:key]).to include("is too short (minimum is 64 characters)") 48 | end 49 | 50 | it "is invalid with a key longer than 255 characters" do 51 | api_key = APIKey.new(valid_attributes.merge(key: "a" * 256)) 52 | expect(api_key).not_to be_valid 53 | expect(api_key.errors[:key]).to include("is too long (maximum is 255 characters)") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/models/rails_release_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe RailsRelease, type: :model do 4 | subject { described_class.new(version: "6.0") } 5 | 6 | describe "valid?" do 7 | it "returns true" do 8 | expect(subject).to be_valid 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require_relative '../config/environment' 5 | # Prevent database truncation if the environment is production 6 | abort("The Rails environment is running in production mode!") if Rails.env.production? 7 | require 'rspec/rails' 8 | require 'database_cleaner/active_record' 9 | # Add additional requires below this line. Rails is not loaded until this point! 10 | 11 | # Requires supporting ruby files with custom matchers and macros, etc, in 12 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 13 | # run as spec files by default. This means that files in spec/support that end 14 | # in _spec.rb will both be required and run as specs, causing the specs to be 15 | # run twice. It is recommended that you do not name files matching this glob to 16 | # end with _spec.rb. You can configure this pattern with the --pattern 17 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 18 | # 19 | # The following line is provided for convenience purposes. It has the downside 20 | # of increasing the boot-up time by auto-requiring all files in the support 21 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 22 | # require only the support files necessary. 23 | # 24 | # Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } 25 | 26 | # Checks for pending migrations and applies them before tests are run. 27 | # If you are not using ActiveRecord, you can remove these lines. 28 | begin 29 | ActiveRecord::Migration.maintain_test_schema! 30 | rescue ActiveRecord::PendingMigrationError => e 31 | abort e.to_s.strip 32 | end 33 | 34 | VCR.configure do |config| 35 | config.cassette_library_dir = "#{::Rails.root}/spec/cassettes" 36 | config.hook_into :webmock 37 | config.ignore_localhost = true 38 | config.configure_rspec_metadata! 39 | end 40 | 41 | RSpec.configure do |config| 42 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 43 | config.fixture_paths = [ 44 | Rails.root.join('spec/fixtures') 45 | ] 46 | 47 | config.around(:each) do |example| 48 | DatabaseCleaner.cleaning do 49 | example.run 50 | end 51 | end 52 | 53 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 54 | # examples within a transaction, remove the following line or assign false 55 | # instead of true. 56 | config.use_transactional_fixtures = true 57 | 58 | # You can uncomment this line to turn off ActiveRecord support entirely. 59 | # config.use_active_record = false 60 | 61 | # RSpec Rails can automatically mix in different behaviours to your tests 62 | # based on their file location, for example enabling you to call `get` and 63 | # `post` in specs under `spec/controllers`. 64 | # 65 | # You can disable this behaviour by removing the line below, and instead 66 | # explicitly tag your specs with their type, e.g.: 67 | # 68 | # RSpec.describe UsersController, type: :controller do 69 | # # ... 70 | # end 71 | # 72 | # The different available types are documented in the features, such as in 73 | # https://rspec.info/features/6-0/rspec-rails 74 | config.infer_spec_type_from_file_location! 75 | 76 | # Filter lines from Rails gems in backtraces. 77 | config.filter_rails_from_backtrace! 78 | # arbitrary gems may also be filtered via: 79 | # config.filter_gems_from_backtrace("gem name") 80 | end 81 | -------------------------------------------------------------------------------- /spec/services/compats/check_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Compats::Check, type: :service do 4 | let(:compat) { FactoryBot.create(:compat, rails_release: rails_release, status: "pending", dependencies: []) } 5 | let(:rails_release) { FactoryBot.create(:rails_release) } 6 | let(:service) { described_class.new } 7 | 8 | describe "#call" do 9 | context "when compat is already checked" do 10 | before { allow(compat).to receive(:checked?).and_return(true) } 11 | 12 | it "raises an error" do 13 | expect { service.call(compat) }.to raise_error(Compats::Check::Error, "Compat is already checked.") 14 | end 15 | end 16 | 17 | context "when compat is not checked" do 18 | before do 19 | allow(compat).to receive(:checked!) 20 | end 21 | 22 | it "calls all private methods and marks compat as checked" do 23 | service.call(compat) 24 | 25 | expect(compat).to have_received(:checked!) 26 | end 27 | end 28 | end 29 | 30 | xdescribe '#check_with_bundler_locally', vcr: { record: :all } do 31 | before do 32 | FactoryBot.create(:rails_release, version: "7.1") 33 | FactoryBot.create(:rails_release, version: "7.2") 34 | 35 | Gemmies::UpdateDependenciesAndVersions.call(gemmy) 36 | Gemmies::UpdateCompats.call(gemmy.id) 37 | gemmy.reload 38 | end 39 | 40 | context 'when using the skunk gem' do 41 | let(:gemmy) { FactoryBot.create(:gemmy, name: 'skunk') } 42 | 43 | it 'marks the compat as compatible' do 44 | gemmy.compat_ids.each do |compat_id| 45 | compat = Compat.find(compat_id) 46 | service = described_class.new 47 | service.compat = compat 48 | service.send(:check_with_bundler_locally) 49 | 50 | expect(compat.status).to eq('compatible') 51 | expect(compat.status_determined_by).to eq('bundler_local') 52 | end 53 | end 54 | end 55 | 56 | context 'when using the win32-api gem' do 57 | let(:gemmy) { FactoryBot.create(:gemmy, name: 'win32-api') } 58 | 59 | it 'marks the compat as inconclusive' do 60 | gemmy.compat_ids.each do |compat_id| 61 | compat = Compat.find(compat_id) 62 | service = described_class.new 63 | service.compat = compat 64 | service.send(:check_with_bundler_locally) 65 | 66 | expect(compat.status).to eq('inconclusive') 67 | expect(compat.status_determined_by).to eq('bundler_local') 68 | end 69 | end 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /spec/services/compats/check_unchecked_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Compats::CheckUnchecked, type: :service do 4 | describe "do_call" do 5 | it "calls Compats::Check#call" do 6 | expect(Compats::Check).to receive(:call) 7 | 8 | release = FactoryBot.create :rails_release, version: "5.0" 9 | compat = FactoryBot.create :compat, rails_release: release 10 | 11 | Compats::CheckUnchecked.call 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /spec/services/compats/checks/empty_dependencies_check_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Compats::Checks::EmptyDependenciesCheck, type: :service do 4 | let(:rails_release) { FactoryBot.create(:rails_release) } 5 | let(:compat) { Compat.new(status: :pending, rails_release: rails_release, dependencies: {}) } 6 | let(:check) { described_class.new(compat) } 7 | 8 | describe "#call" do 9 | context "when compat is pending and has no dependencies" do 10 | it "marks compat as compatible" do 11 | check.call 12 | 13 | expect(compat.status).to eq("compatible") 14 | expect(compat.status_determined_by).to eq("empty_dependencies") 15 | expect(compat).to be_checked 16 | end 17 | end 18 | 19 | context "when compat is not pending" do 20 | it "does not change the status" do 21 | compat.status = :incompatible 22 | 23 | expect do 24 | check.call 25 | end.not_to change { compat.status } 26 | end 27 | end 28 | 29 | context "when compat has dependencies" do 30 | it "does not mark compat as compatible" do 31 | compat.dependencies = {"colorize"=>"~> 0.8, >= 0.8.1", "require_all"=>"~> 1.1, >= 1.1.6"} 32 | 33 | check.call 34 | 35 | expect(compat.status).not_to eq(:compatible) 36 | end 37 | end 38 | 39 | context "when compat is already checked" do 40 | it "does not change the status" do 41 | compat.status = :incompatible 42 | compat.status_determined_by = "rails_gems" 43 | compat.checked! 44 | 45 | check.call 46 | 47 | expect(compat.status).to eq("incompatible") 48 | end 49 | end 50 | 51 | context "when compat has nil dependencies" do 52 | it "marks compat as compatible" do 53 | compat.dependencies = nil 54 | 55 | check.call 56 | 57 | expect(compat.status).to eq("compatible") 58 | expect(compat.status_determined_by).to eq("empty_dependencies") 59 | expect(compat).to be_checked 60 | end 61 | end 62 | end 63 | 64 | describe "#check!" do 65 | context "when compat is pending and has no dependencies" do 66 | it "marks compat as compatible and saves it" do 67 | expect(compat).to receive(:save!) 68 | 69 | check.check! 70 | 71 | expect(compat.status).to eq("compatible") 72 | expect(compat.status_determined_by).to eq("empty_dependencies") 73 | expect(compat).to be_checked 74 | end 75 | end 76 | 77 | context "when compat is not pending" do 78 | it "does not change the status and does not save it" do 79 | compat.status = :incompatible 80 | expect(compat).to receive(:save!) 81 | 82 | check.check! 83 | 84 | expect(compat.status).to eq("compatible") 85 | end 86 | end 87 | end 88 | end -------------------------------------------------------------------------------- /spec/services/gemmies/process_spec.rb: -------------------------------------------------------------------------------- 1 | # spec/services/gemmies/process_spec.rb 2 | require 'rails_helper' 3 | 4 | RSpec.describe Gemmies::Process, type: :service, vcr: { record: :once } do 5 | describe '#call' do 6 | let(:gemmy) { FactoryBot.create(:gemmy, name: 'ombu_labs-auth') } 7 | let(:service) { described_class.new } 8 | 9 | before do 10 | FactoryBot.create(:rails_release, version: "7.1") 11 | FactoryBot.create(:rails_release, version: "7.2") 12 | 13 | allow(Compats::CheckUnchecked).to receive(:perform_async) 14 | 15 | service.call(gemmy.id) 16 | gemmy.reload 17 | end 18 | 19 | it 'updates dependencies for the gemmy' do 20 | deps = [{"devise"=>"~> 4.8.1", "omniauth"=>"~> 2.1.0", "omniauth-github"=>"~> 2.0.0", "omniauth-rails_csrf_protection"=>">= 0", "rails"=>">= 6.0"}, {"devise"=>"~> 4.8.1", "omniauth"=>"~> 2.1.0", "omniauth-github"=>"~> 2.0.0", "rails"=>">= 6.0"}] 21 | 22 | expect(gemmy.dependencies).to eq(deps) 23 | end 24 | 25 | it 'calls UpdateCompats with the gemmy' do 26 | expect(gemmy.compat_ids.size).to eq(4) 27 | end 28 | 29 | it 'enqueues Compats::CheckUnchecked job' do 30 | expect(Compats::CheckUnchecked).to have_received(:perform_async) 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /spec/services/gemmies/update_compats_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe Gemmies::UpdateCompats do 4 | describe "#call", vcr: { record: :once } do 5 | let(:gemmy) { FactoryBot.create :gemmy } 6 | 7 | before do 8 | @rails_release = FactoryBot.create :rails_release 9 | Gemmies::UpdateDependenciesAndVersions.call(gemmy) 10 | end 11 | 12 | it "creates a compat for each rails release and dependency" do 13 | described_class.call(gemmy.id) 14 | 15 | expect(@rails_release.compats.count).to eq 5 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV["COVERAGE"] 2 | require "simplecov" 3 | require "simplecov-console" 4 | require "codecov" 5 | 6 | SimpleCov.start "rails" 7 | 8 | formatters = [ 9 | SimpleCov::Formatter::HTMLFormatter, 10 | SimpleCov::Formatter::Console 11 | ] 12 | 13 | formatters << SimpleCov::Formatter::Codecov if ENV['CODECOV_TOKEN'] 14 | 15 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(formatters) 16 | end 17 | 18 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 19 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 20 | # The generated `.rspec` file contains `--require spec_helper` which will cause 21 | # this file to always be loaded, without a need to explicitly require it in any 22 | # files. 23 | # 24 | # Given that it is always loaded, you are encouraged to keep this file as 25 | # light-weight as possible. Requiring heavyweight dependencies from this file 26 | # will add to the boot time of your test suite on EVERY test run, even for an 27 | # individual file that may not need all of that loaded. Instead, consider making 28 | # a separate helper file that requires the additional dependencies and performs 29 | # the additional setup, and require it from the spec files that actually need 30 | # it. 31 | # 32 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 33 | RSpec.configure do |config| 34 | # rspec-expectations config goes here. You can use an alternate 35 | # assertion/expectation library such as wrong or the stdlib/minitest 36 | # assertions if you prefer. 37 | config.expect_with :rspec do |expectations| 38 | # This option will default to `true` in RSpec 4. It makes the `description` 39 | # and `failure_message` of custom matchers include text for helper methods 40 | # defined using `chain`, e.g.: 41 | # be_bigger_than(2).and_smaller_than(4).description 42 | # # => "be bigger than 2 and smaller than 4" 43 | # ...rather than: 44 | # # => "be bigger than 2" 45 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 46 | end 47 | 48 | # rspec-mocks config goes here. You can use an alternate test double 49 | # library (such as bogus or mocha) by changing the `mock_with` option here. 50 | config.mock_with :rspec do |mocks| 51 | # Prevents you from mocking or stubbing a method that does not exist on 52 | # a real object. This is generally recommended, and will default to 53 | # `true` in RSpec 4. 54 | mocks.verify_partial_doubles = true 55 | end 56 | 57 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 58 | # have no way to turn it off -- the option exists only for backwards 59 | # compatibility in RSpec 3). It causes shared context metadata to be 60 | # inherited by the metadata hash of host groups and examples, rather than 61 | # triggering implicit auto-inclusion in groups with matching metadata. 62 | config.shared_context_metadata_behavior = :apply_to_host_groups 63 | 64 | # The settings below are suggested to provide a good initial experience 65 | # with RSpec, but feel free to customize to your heart's content. 66 | =begin 67 | # This allows you to limit a spec run to individual examples or groups 68 | # you care about by tagging them with `:focus` metadata. When nothing 69 | # is tagged with `:focus`, all examples get run. RSpec also provides 70 | # aliases for `it`, `describe`, and `context` that include `:focus` 71 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 72 | config.filter_run_when_matching :focus 73 | 74 | # Allows RSpec to persist some state between runs in order to support 75 | # the `--only-failures` and `--next-failure` CLI options. We recommend 76 | # you configure your source control system to ignore this file. 77 | config.example_status_persistence_file_path = "spec/examples.txt" 78 | 79 | # Limits the available syntax to the non-monkey patched syntax that is 80 | # recommended. For more details, see: 81 | # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ 82 | config.disable_monkey_patching! 83 | 84 | # Many RSpec users commonly either run the entire suite or an individual 85 | # file, and it's useful to allow more verbose output when running an 86 | # individual spec file. 87 | if config.files_to_run.one? 88 | # Use the documentation formatter for detailed output, 89 | # unless a formatter has already been configured 90 | # (e.g. via a command-line flag). 91 | config.default_formatter = "doc" 92 | end 93 | 94 | # Print the 10 slowest examples and example groups at the 95 | # end of the spec run, to help surface which specs are running 96 | # particularly slow. 97 | config.profile_examples = 10 98 | 99 | # Run specs in random order to surface order dependencies. If you find an 100 | # order dependency and want to debug it, you can fix the order by providing 101 | # the seed, which is printed after each run. 102 | # --seed 1234 103 | config.order = :random 104 | 105 | # Seed global randomization in this process using the `--seed` CLI option. 106 | # Setting this allows you to use `--seed` to deterministically reproduce 107 | # test failures related to randomization by passing the same `--seed` value 108 | # as the one that triggered the failure. 109 | Kernel.srand config.seed 110 | =end 111 | end 112 | -------------------------------------------------------------------------------- /stuff.rb: -------------------------------------------------------------------------------- 1 | # remove columns 2 | 3 | ActiveRecord::Migration.remove_column :gemmies, :versions 4 | ActiveRecord::Migration.rename_column :gemmies, :versions_and_dependencies, :dependencies_and_versions 5 | 6 | # rename gemfiles to lockfiles 7 | 8 | ActiveRecord::Migration.rename_table :gemfiles, :lockfiles 9 | ActiveRecord::Migration.rename_table :gemfile_dependencies, :lockfile_dependencies 10 | ActiveRecord::Migration.rename_column :lockfile_dependencies, :gemfile_id, :lockfile_id 11 | 12 | # add index to compats 13 | 14 | begin 15 | ActiveRecord::Migration.add_index :compats, [:dependencies, :rails_release_id], unique: true 16 | rescue ActiveRecord::RecordNotUnique => e 17 | puts e.message 18 | if e.message =~ /Key \(dependencies, rails_release_id\)=\((.+), (\d)\) is duplicated/ 19 | records=Compat.where(dependencies:JSON.load($1),rails_release_id:$2) 20 | if records.many? 21 | records.limit(records.size - 1).each(&:destroy) 22 | retry 23 | else 24 | raise "wat? #{e.message}" 25 | end 26 | else 27 | raise e 28 | end 29 | end 30 | 31 | # add compatible_reason to compats 32 | 33 | ActiveRecord::Migration.add_column :compats, :compatible_reason, :string 34 | GitHubNotification.count 35 | 36 | # add conclusion to github notifications 37 | 38 | ActiveRecord::Migration.add_column :github_notifications, :conclusion, :string 39 | ActiveRecord::Migration.add_column :github_notifications, :action, :string 40 | ActiveRecord::Migration.add_column :github_notifications, :branch, :string 41 | 42 | # restart console 43 | 44 | GithubNotification.where(action:nil).find_each do |gn| 45 | gn.action = gn.data["action"] 46 | gn.conclusion = gn.data.dig("check_run", "conclusion") 47 | gn.branch = gn.data.dig("check_run", "check_suite", "head_branch") 48 | gn.save validate: false 49 | end 50 | ActiveRecord::Migration.change_column_null :github_notifications, :action, false 51 | ActiveRecord::Migration.change_column_null :github_notifications, :branch, false 52 | 53 | GithubNotification.where(id: [142472,142115,142470,141380,141378,141381,141379,141371,141376,141377,140928,141003,140997,140974,140813,140775,140786,140823,140716,140720,140724,140685,140715,140677,140686,140676,140662,140669,140670,140661,140522,140532,140515,140510,140513,140502,140492,140491,140479,140441,140444,140473,140448,140420,140419,140440,140410,140403,140402,140409,140145,140166,140120,140115,140110,140111,140077,140099,140107,140082,140045,140061,140066,140037,140042,140040,140017,140010,140020,140012]).each do |gn| 54 | gn.unprocessed! 55 | gn.compat.update! compatible: nil, compatible_reason: nil 56 | GithubNotifications::Process.call gn 57 | end 58 | 59 | # refactor compats 60 | 61 | ActiveRecord::Migration.add_column :compats, :status, :integer 62 | 63 | Compat.where(compatible: nil).update_all status: Compat.statuses.fetch(:pending) 64 | Compat.where(compatible: true).update_all status: Compat.statuses.fetch(:compatible) 65 | Compat.where(compatible: false).update_all status: Compat.statuses.fetch(:incompatible) 66 | 67 | ActiveRecord::Migration.remove_column :compats, :compatible 68 | 69 | ActiveRecord::Migration.rename_column :compats, :compatible_reason, :status_determined_by 70 | 71 | # recheck all compats after fixes 72 | 73 | [Compat.inconclusive,Compat.incompatible.checked_after(Date.new(2020,12,4).beginning_of_day)].each do |scope| 74 | scope.update_all status: 0, status_determined_by:nil,checked_at:nil 75 | end 76 | 77 | # bundler 2 requires ruby >= 2.3.0 78 | 79 | # rails 2.3 - ruby 2.7, bundler 2.1.4 (oder der konsistenz wegen auch 1.17.3?) 80 | # rails 3.0 - ruby 2.7, bundler 1.17.3 (3.0.0, 3.0.1 und 3.0.2 brauchen bundler 1.0.x, alle darüber 1.x) 81 | # rails 3.1 - ruby 2.7, bundler 1.17.3 82 | # rails 3.2 - ruby 2.7, bundler 1.17.3 83 | # rails 4.0 - ruby 2.7, bundler 1.17.3 84 | # rails 4.1 - ruby 2.7, bundler 1.17.3 85 | # rails 4.2 - ruby 2.7, bundler 1.17.3 86 | # rails 5.0 - ruby 2.7, bundler 2.1.4 87 | # rails 5.1 - ruby 2.7, bundler 2.1.4 88 | # rails 5.2 - ruby 2.7, bundler 2.1.4 89 | 90 | rails_version = "3.0" 91 | ruby_version = "2.7" 92 | bundler_version = "1.17.3" 93 | 94 | action_file = File.join(git.dir.path, ".github", "workflows", "ci.yml") 95 | File.write action_file, <<~TEXT.chomp 96 | name: CI 97 | on: 98 | push: 99 | branches-ignore: 100 | - main 101 | 102 | jobs: 103 | 104 | verify: 105 | name: Verify 106 | runs-on: ubuntu-latest 107 | 108 | steps: 109 | 110 | - name: Check out code 111 | uses: actions/checkout@v2 112 | 113 | - name: Set up Ruby 114 | uses: ruby/setup-ruby@v1 115 | with: 116 | ruby-version: #{ruby_version} 117 | bundler: none 118 | 119 | - name: Install bundler 120 | run: gem install bundler -v #{bundler_version} 121 | 122 | - name: Try to create lockfile 123 | run: | 124 | gem list 125 | bundle --version 126 | bundle _#{bundler_version}_ install 127 | TEXT 128 | 129 | gemfile_content = "source "https://rubygems.org"\ngem "rails", "~> #{rails_version}.0"" 130 | File.write File.join(git.dir.path, "Gemfile"), gemfile_content 131 | 132 | git.add [action_file, "Gemfile"] 133 | git.commit "Rails #{rails_version}, Ruby #{ruby_version}, Bundler #{bundler_version}" 134 | git.push "origin", "test" 135 | 136 | # add compat_ids to gemmies 137 | 138 | ActiveRecord::Migration.add_column :gemmies, :compat_ids, :text, array: true, default: [], null: false 139 | 140 | Compat.find_each do |c| 141 | c.gemmies.update_all(["compat_ids = array_append(compat_ids, ?)", c.id.to_s]) 142 | end 143 | 144 | Gemmy.find_each do |g| 145 | compats = RailsRelease.all.map do 146 | _1.compats.merge(g.compats) 147 | end 148 | 149 | next if compats.map(&:size).uniq.one? 150 | 151 | dependencies = compats.flatten.map(&:dependencies).uniq 152 | 153 | dependencies.each do |d| 154 | RailsRelease.find_each do |rr| 155 | compat = rr.compats.where(dependencies: d).first_or_create! 156 | unless g.compat_ids.include?(compat.id.to_s) 157 | Gemmy.where(id: g).update_all(["compat_ids = array_append(compat_ids, ?)", compat.id.to_s]) 158 | g.touch 159 | end 160 | end 161 | end 162 | end 163 | 164 | # fix gemmy dependency encoding 165 | 166 | Gemmy.find_each do |g| 167 | g.dependencies_and_versions.transform_keys! { JSON.generate JSON.load(_1) } 168 | if g.changed? 169 | g.save! 170 | end 171 | end 172 | 173 | co=[] 174 | Compat.find_each do |c| 175 | puts c.id 176 | co<, #, #, #, #, #, #, #, #, #, #, #, #] 205 | 206 | # convert db from postgres to sqlite 207 | 208 | # /Users/manuel/Library/Python/3.9/bin/db-to-sqlite "postgresql://railsbump:MUDv4XUUKrpsFGVytPHoduzNZ7ojqY@railsbump.cbqgwmohh80g.eu-central-1.rds.amazonaws.com/railsbump_production" storage/development.sqlite3 --all --progress --skip github_notifications 209 | 210 | ActiveRecord::Migration.create_table :github_notifications do |t| 211 | t.string :conclusion 212 | t.string :action, :branch, null: false 213 | t.json :data 214 | t.datetime :processed_at 215 | t.references :compat 216 | t.timestamps 217 | end 218 | 219 | # fix db columns 220 | 221 | Rails.application.eager_load! 222 | ApplicationRecord.descendants.each do |klass| 223 | puts klass 224 | %w(created_at updated_at).each do |column| 225 | puts column 226 | next unless klass.columns_hash[column]&.type == :text 227 | ActiveRecord::Migration.add_column klass.table_name, "#{column}_new", :datetime 228 | klass.where("#{column}_new": nil).update_all "#{column}_new = DATETIME(#{column})" 229 | ActiveRecord::Migration.remove_column klass.table_name, column 230 | ActiveRecord::Migration.rename_column klass.table_name, "#{column}_new", column 231 | ActiveRecord::Migration.change_column_null klass.table_name, column, false 232 | end 233 | end 234 | 235 | ActiveRecord::Migration.add_column :compats, :checked_at_new, :datetime 236 | Compat.where(checked_at_new: nil).update_all "checked_at_new = DATETIME(checked_at)" 237 | ActiveRecord::Migration.remove_column :compats, :checked_at 238 | ActiveRecord::Migration.rename_column :compats, :checked_at_new, :checked_at 239 | 240 | ActiveRecord::Migration.add_column :compats, :dependencies_new, :json 241 | Compat.where(dependencies_new: nil).update_all "dependencies_new = JSON(dependencies)" 242 | ActiveRecord::Migration.remove_column :compats, :dependencies 243 | ActiveRecord::Migration.rename_column :compats, :dependencies_new, :dependencies 244 | 245 | ActiveRecord::Migration.add_column :gemmies, :compat_ids_new, :json 246 | Gemmy.where(compat_ids_new: nil).update_all "compat_ids_new = JSON(compat_ids)" 247 | ActiveRecord::Migration.remove_column :gemmies, :compat_ids 248 | ActiveRecord::Migration.rename_column :gemmies, :compat_ids_new, :compat_ids 249 | ActiveRecord::Migration.change_table :gemmies do |t| 250 | t.check_constraint "JSON_TYPE(compat_ids) = 'array'", name: "gemmy_compat_ids_is_array" 251 | end 252 | ActiveRecord::Migration.change_column_null :gemmies, :compat_ids, false 253 | ActiveRecord::Migration.change_column_default :gemmies, :compat_ids, [] 254 | 255 | ActiveRecord::Migration.add_column :gemmies, :dependencies_and_versions_new, :json 256 | Gemmy.where(dependencies_and_versions_new: nil).update_all "dependencies_and_versions_new = JSON(dependencies_and_versions)" 257 | ActiveRecord::Migration.remove_column :gemmies, :dependencies_and_versions 258 | ActiveRecord::Migration.rename_column :gemmies, :dependencies_and_versions_new, :dependencies_and_versions 259 | ActiveRecord::Migration.change_column_default :gemmies, :dependencies_and_versions, {} 260 | 261 | # update compat dependencies keys 262 | 263 | Compat.find_each do |c| 264 | c.update! dependencies_key: ActiveSupport::Digest.hexdigest(JSON.generate c.dependencies) 265 | end 266 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | yarn@^1.22.10: 6 | version "1.22.22" 7 | resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.22.tgz#ac34549e6aa8e7ead463a7407e1c7390f61a6610" 8 | integrity sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg== 9 | --------------------------------------------------------------------------------