{"id":13879104,"url":"https://github.com/Shopify/maintenance_tasks","last_synced_at":"2025-07-16T15:31:34.093Z","repository":{"id":37073064,"uuid":"297674098","full_name":"Shopify/maintenance_tasks","owner":"Shopify","description":"A Rails engine for queueing and managing data migrations.","archived":false,"fork":false,"pushed_at":"2025-07-15T20:17:17.000Z","size":3049,"stargazers_count":1162,"open_issues_count":8,"forks_count":93,"subscribers_count":251,"default_branch":"main","last_synced_at":"2025-07-16T06:10:28.116Z","etag":null,"topics":["backfill","data","migration","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Shopify.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-09-22T14:25:41.000Z","updated_at":"2025-07-15T16:00:31.000Z","dependencies_parsed_at":"2023-10-16T22:03:14.577Z","dependency_job_id":"6d182878-254d-4808-8665-1e7bf17def37","html_url":"https://github.com/Shopify/maintenance_tasks","commit_stats":{"total_commits":1075,"total_committers":60,"mean_commits":"17.916666666666668","dds":0.6865116279069767,"last_synced_commit":"20682ab0805be6a7cb7910be0377dcdc2c93c157"},"previous_names":[],"tags_count":43,"template":false,"template_full_name":null,"purl":"pkg:github/Shopify/maintenance_tasks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Shopify%2Fmaintenance_tasks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Shopify%2Fmaintenance_tasks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Shopify%2Fmaintenance_tasks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Shopify%2Fmaintenance_tasks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Shopify","download_url":"https://codeload.github.com/Shopify/maintenance_tasks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Shopify%2Fmaintenance_tasks/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265521426,"owners_count":23781500,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backfill","data","migration","rails","ruby"],"created_at":"2024-08-06T08:02:09.997Z","updated_at":"2025-07-16T15:31:34.066Z","avatar_url":"https://github.com/Shopify.png","language":"Ruby","readme":"# Maintenance Tasks\n\nA Rails engine for queuing and managing maintenance tasks.\n\nBy ”maintenance task”, this project means a data migration, i.e. code that\nchanges data in the database, often to support schema migrations. For example,\nin order to introduce a new `NOT NULL` column, it has to be added as nullable\nfirst, backfilled with values, before finally being changed to `NOT NULL`. This\nengine helps with the second part of this process, backfilling.\n\nMaintenance tasks are collection-based tasks, usually using Active Record, that\nupdate the data in your database. They can be paused or interrupted. Maintenance\ntasks can operate [in batches](#processing-batch-collections) and use\n[throttling](#throttling) to control the load on your database.\n\nMaintenance tasks aren't meant to happen on a regular basis. They're used as\nneeded, or as one-offs. Normally maintenance tasks are ephemeral, so they are\nused briefly and then deleted.\n\nThe Rails engine has a web-based UI for listing maintenance tasks, seeing their\nstatus, and starting, pausing and restarting them.\n\n[![Link to demo video](static/demo.png)](https://www.youtube.com/watch?v=BTuvTQxlFzs)\n\n## Should I Use Maintenance Tasks?\n\nMaintenance tasks have a limited, specific job UI. While the engine can be used\nto provide a user interface for other data changes, such as data changes for\nsupport requests, we recommend you use regular application code for those use\ncases instead. These inevitably require more flexibility than this engine will\nbe able to provide.\n\nIf your task shouldn't run as an Active Job, it probably isn't a good match for\nthis gem. If your task doesn't need to run in the background, consider a runner\nscript instead. If your task doesn't need to be interruptible, consider a normal\nActive Job.\n\nMaintenance tasks can be interrupted between iterations. If your task [isn't\ncollection-based](#tasks-that-dont-need-a-collection) (no CSV file or database\ntable) or has very large batches, it will get limited benefit from throttling\n(pausing between iterations) or interrupting. This might be fine, or the added\ncomplexity of maintenance Tasks over normal Active Jobs may not be worthwhile.\n\nIf your task updates your database schema instead of data, use a migration\ninstead of a maintenance task.\n\nIf your task happens regularly, consider Active Jobs with a scheduler or cron,\n[job-iteration jobs][job-iteration] and/or [custom rails_admin\nUIs][rails-admin-engines] instead of the Maintenance Tasks gem. Maintenance\ntasks should be ephemeral, to suit their intentionally limited UI. They should\nnot repeat.\n\n[job-iteration]: https://github.com/shopify/job-iteration\n\nTo create seed data for a new application, use the provided Rails `db/seeds.rb`\nfile instead.\n\nIf your application can't handle a half-completed migration, maintenance tasks\nare probably the wrong tool. Remember that maintenance tasks are intentionally\npausable and can be cancelled halfway.\n\n[rails-admin-engines]: https://www.ruby-toolbox.com/categories/rails_admin_interfaces\n\n## Installation\n\nTo install the gem and run the install generator, execute:\n\n```sh-session\nbundle add maintenance_tasks\nbin/rails generate maintenance_tasks:install\n```\n\nThe generator creates and runs a migration to add the necessary table to your\ndatabase. It also mounts Maintenance Tasks in your `config/routes.rb`. By\ndefault the web UI can be accessed in the new `/maintenance_tasks` path.\n\nThis gem uses the [Rails Error Reporter][rails-error-reporting] to report errors.\nIf you are using a bug tracking service you may want to subscribe to the\nreporter. See [Reporting Errors](#reporting-errors) for more information.\n\n[rails-error-reporting]: https://guides.rubyonrails.org/error_reporting.html\n\n### Active Job Dependency\n\nThe Maintenance Tasks framework relies on Active Job behind the scenes to run\nTasks. The default queuing backend for Active Job is\n[asynchronous][async-adapter]. It is **strongly recommended** to change this to\na persistent backend so that Task progress is not lost during code or\ninfrastructure changes. For more information on configuring a queuing backend,\ntake a look at the [Active Job documentation][active-job-docs].\n\n[async-adapter]: https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html\n[active-job-docs]: https://guides.rubyonrails.org/active_job_basics.html#setting-the-backend\n\n### Action Controller \u0026 Action View Dependency\n\nThe Maintenance Tasks framework relies on Action Controller and Action View to\nrender the UI. If you're using Rails in API-only mode, see [Using Maintenance\nTasks in API-only\napplications](#using-maintenance-tasks-in-api-only-applications).\n\n### Autoloading\n\nThe Maintenance Tasks framework does not support autoloading in `:classic` mode.\nPlease ensure your application is using [Zeitwerk][] to load your code. For more\ninformation, please consult the [Rails guides on autoloading and reloading\nconstants][autoloading].\n\n[Zeitwerk]: https://github.com/fxn/zeitwerk\n[autoloading]: https://guides.rubyonrails.org/autoloading_and_reloading_constants.html\n\n## Usage\n\nThe typical Maintenance Tasks workflow is as follows:\n\n1. [Generate a class describing the Task](#creating-a-task) and the work to be\n   done.\n2. Run the Task\n   - either by [using the included web UI](#running-a-task-from-the-web-ui),\n   - or by [using the command line](#running-a-task-from-the-command-line),\n   - or by [using Ruby](#running-a-task-from-ruby).\n3. [Monitor the Task](#monitoring-your-tasks-status)\n   - either by using the included web UI,\n   - or by manually checking your task’s run’s status in your database.\n4. Optionally, delete the Task code if you no longer need it.\n\n### Creating a Task\n\nA generator is provided to create tasks. Generate a new task by running:\n\n```sh-session\nbin/rails generate maintenance_tasks:task update_posts\n```\n\nThis creates the task file `app/tasks/maintenance/update_posts_task.rb`.\n\nThe generated task is a subclass of `MaintenanceTasks::Task` that implements:\n\n* `collection`: return an Active Record Relation or an Array to be iterated\n  over.\n* `process`: do the work of your maintenance task on a single record\n\nOptionally, tasks can also implement a custom `#count` method, defining the\nnumber of elements that will be iterated over. Your task’s `tick_total` will be\ncalculated automatically based on the collection size, but this value may be\noverridden if desired using the `#count` method (this might be done, for\nexample, to avoid the query that would be produced to determine the size of your\ncollection).\n\nExample:\n\n```ruby\n# app/tasks/maintenance/update_posts_task.rb\n\nmodule Maintenance\n  class UpdatePostsTask \u003c MaintenanceTasks::Task\n    def collection\n      Post.all\n    end\n\n    def process(post)\n      post.update!(content: \"New content!\")\n    end\n  end\nend\n```\n\n#### Customizing the Batch Size\n\nWhen processing records from an Active Record Relation, records are fetched in\nbatches internally, and then each record is passed to the `#process` method.\nMaintenance Tasks will query the database to fetch records in batches of 100 by\ndefault, but the batch size can be modified using the `collection_batch_size`\nmacro:\n\n```ruby\n# app/tasks/maintenance/update_posts_task.rb\n\nmodule Maintenance\n  class UpdatePostsTask \u003c MaintenanceTasks::Task\n    # Fetch records in batches of 1000\n    collection_batch_size(1000)\n\n    def collection\n      Post.all\n    end\n\n    def process(post)\n      post.update!(content: \"New content!\")\n    end\n  end\nend\n```\n\n### Creating a CSV Task\n\nYou can also write a Task that iterates on a CSV file. Note that writing CSV\nTasks **requires Active Storage to be configured**. Ensure that the dependency\nis specified in your application’s Gemfile, and that you’ve followed the [setup\ninstructions][storage-setup]. See also [Customizing which Active Storage service\nto use][storage-customizing].\n\n[storage-setup]: https://edgeguides.rubyonrails.org/active_storage_overview.html#setup\n[storage-customizing]: #customizing-which-active-storage-service-to-use\n\nGenerate a CSV Task by running:\n\n```sh-session\nbin/rails generate maintenance_tasks:task import_posts --csv\n```\n\nThe generated task is a subclass of `MaintenanceTasks::Task` that implements:\n\n* `process`: do the work of your maintenance task on a `CSV::Row`\n\n```ruby\n# app/tasks/maintenance/import_posts_task.rb\n\nmodule Maintenance\n  class ImportPostsTask \u003c MaintenanceTasks::Task\n    csv_collection\n\n    def process(row)\n      Post.create!(title: row[\"title\"], content: row[\"content\"])\n    end\n  end\nend\n```\n\n`posts.csv`:\n```csv\ntitle,content\nMy Title,Hello World!\n```\n\nThe files uploaded to your Active Storage service provider will be renamed to\ninclude an ISO 8601 timestamp and the Task name in snake case format.\n\nThe implicit `#count` method loads and parses the entire file to determine the\naccurate number of rows. With files with millions of rows, it takes several\nseconds to process. Consider skipping the count (defining a `count` that returns\n`nil`) or use an approximation, eg: count the number of new lines:\n\n```ruby\ndef count(task)\n  task.csv_content.count(\"\\n\") - 1\nend\n```\n\n#### CSV options\n\nTasks can pass [options for Ruby's CSV parser][csv-parse-options] by adding\nkeyword arguments to `csv_collection`:\n\n[csv-parse-options]: https://ruby-doc.org/3.3.0/stdlibs/csv/CSV.html#class-CSV-label-Options+for+Parsing\n\n```ruby\n# app/tasks/maintenance/import_posts_task.rb\n\nmodule Maintenance\n  class ImportPosts\n    csv_collection(skip_lines: /^#/, converters: -\u003e(field) { field.strip })\n\n    def process(row)\n      Post.create!(title: row[\"title\"], content: row[\"content\"])\n    end\n  end\nend\n```\n\nThese options instruct Ruby's CSV parser to skip lines that start with a `#`,\nand removes the leading and trailing spaces from any field, so that the\nfollowing file will be processed identically as the previous example:\n\n`posts.csv`:\n```csv\n# A comment\ntitle,content\n My Title ,Hello World!\n```\n\n#### Batch CSV Tasks\n\nTasks can process CSVs in batches. Add the `in_batches` option to your task’s\n`csv_collection` macro:\n\n```ruby\n# app/tasks/maintenance/batch_import_posts_task.rb\n\nmodule Maintenance\n  class BatchImportPostsTask \u003c MaintenanceTasks::Task\n    csv_collection(in_batches: 50)\n\n    def process(batch_of_rows)\n      Post.insert_all(post_rows.map(\u0026:to_h))\n    end\n  end\nend\n```\n\nAs with a regular CSV task, ensure you’ve implemented the following method:\n\n* `process`: do the work of your Task on a batch (array of `CSV::Row` objects).\n\nNote that `#count` is calculated automatically based on the number of batches in\nyour collection, and your Task’s progress will be displayed in terms of batches\n(not the total number of rows in your CSV).\n\nNon-batched CSV tasks will have an effective batch size of 1, which can reduce\nthe efficiency of your database operations.\n\n### Processing Batch Collections\n\nThe Maintenance Tasks gem supports processing Active Records in batches. This\ncan reduce the number of calls your Task makes to the database. Use\n`ActiveRecord::Batches#in_batches` on the relation returned by your collection\nto specify that your Task should process batches instead of records. Active\nRecord defaults to 1000 records by batch, but a custom size can be specified.\n\n```ruby\n# app/tasks/maintenance/update_posts_in_batches_task.rb\n\nmodule Maintenance\n  class UpdatePostsInBatchesTask \u003c MaintenanceTasks::Task\n    def collection\n      Post.in_batches\n    end\n\n    def process(batch_of_posts)\n      batch_of_posts.update_all(content: \"New content added on #{Time.now.utc}\")\n    end\n  end\nend\n```\n\nEnsure that you’ve implemented the following methods:\n\n* `collection`: return an `ActiveRecord::Batches::BatchEnumerator`.\n* `process`: do the work of your Task on a batch (`ActiveRecord::Relation`).\n\nNote that `#count` is calculated automatically based on the number of batches in\nyour collection, and your Task’s progress will be displayed in terms of batches\n(not the number of records in the relation).\n\n**Important!** Batches should only be used if `#process` is performing a batch\noperation such as `#update_all` or `#delete_all`. If you need to iterate over\nindividual records, you should define a collection that [returns an\n`ActiveRecord::Relation`](#creating-a-task). This uses batching internally, but\nloads the records with one SQL query. Conversely, batch collections load the\nprimary keys of the records of the batch first, and then perform an additional\nquery to load the records when calling `each` (or any `Enumerable` method)\ninside `#process`.\n\n### Tasks that don’t need a Collection\n\nSometimes, you might want to run a Task that performs a single operation, such\nas enqueuing another background job or querying an external API. The gem\nsupports collection-less tasks.\n\nGenerate a collection-less Task by running:\n\n```sh-session\nbin/rails generate maintenance_tasks:task no_collection_task --no-collection\n```\n\nThe generated task is a subclass of `MaintenanceTasks::Task` that implements:\n\n* `process`: do the work of your maintenance task\n\n```ruby\n# app/tasks/maintenance/no_collection_task.rb\n\nmodule Maintenance\n  class NoCollectionTask \u003c MaintenanceTasks::Task\n    no_collection\n\n    def process\n      SomeAsyncJob.perform_later\n    end\n  end\nend\n```\n\n### Tasks with Custom Enumerators\n\nIf you have a special use case requiring iteration over an unsupported\ncollection type, such as external resources fetched from some API, you can\nimplement the `enumerator_builder(cursor:)` method in your task.\n\nThis method should return an `Enumerator`, yielding pairs of `[item, cursor]`.\nMaintenance Tasks takes care of persisting the current cursor position and will\nprovide it as the `cursor` argument if your task is interrupted or resumed. The\n`cursor` is stored as a `String`, so your custom enumerator should handle\nserializing/deserializing the value if required.\n\n```ruby\n# app/tasks/maintenance/custom_enumerator_task.rb\n\nmodule Maintenance\n  class CustomEnumeratorTask \u003c MaintenanceTasks::Task\n    def enumerator_builder(cursor:)\n      after_id = cursor\u0026.to_i\n      PostAPI.index(after_id: after_id).map { |post| [post, post.id] }.to_enum\n    end\n\n    def process(post)\n      Post.create!(post)\n    end\n  end\nend\n```\n\n### Throttling\n\nMaintenance tasks often modify a lot of data and can be taxing on your database.\nThe gem provides a throttling mechanism that can be used to throttle a Task when\na given condition is met. If a Task is throttled (the throttle block returns\ntrue), it will be interrupted and retried after a backoff period has passed. The\ndefault backoff is 30 seconds.\n\nSpecify the throttle condition as a block:\n\n```ruby\n# app/tasks/maintenance/update_posts_throttled_task.rb\n\nmodule Maintenance\n  class UpdatePostsThrottledTask \u003c MaintenanceTasks::Task\n    throttle_on(backoff: 1.minute) do\n      DatabaseStatus.unhealthy?\n    end\n\n    def collection\n      Post.all\n    end\n\n    def process(post)\n      post.update!(content: \"New content added on #{Time.now.utc}\")\n    end\n  end\nend\n```\n\nNote that it’s up to you to define a throttling condition that makes sense for\nyour app. Shopify implements `DatabaseStatus.healthy?` to check various MySQL\nmetrics such as replication lag, DB threads, whether DB writes are available,\netc.\n\nTasks can define multiple throttle conditions. Throttle conditions are inherited\nby descendants, and new conditions will be appended without impacting existing\nconditions.\n\nThe backoff can also be specified as a Proc that receives no arguments:\n\n```ruby\n# app/tasks/maintenance/update_posts_throttled_task.rb\n\nmodule Maintenance\n  class UpdatePostsThrottledTask \u003c MaintenanceTasks::Task\n    throttle_on(backoff: -\u003e { RandomBackoffGenerator.generate_duration } ) do\n      DatabaseStatus.unhealthy?\n    end\n    # ...\n  end\nend\n```\n\n### Custom Task Parameters\n\nTasks may need additional information, supplied via parameters, to run.\nParameters can be defined as Active Model Attributes in a Task, and then become\naccessible to any of Task’s methods: `#collection`, `#count`, or `#process`.\n\n```ruby\n# app/tasks/maintenance/update_posts_via_params_task.rb\n\nmodule Maintenance\n  class UpdatePostsViaParamsTask \u003c MaintenanceTasks::Task\n    attribute :updated_content, :string\n    validates :updated_content, presence: true\n\n    def collection\n      Post.all\n    end\n\n    def process(post)\n      post.update!(content: updated_content)\n    end\n  end\nend\n```\n\nTasks can leverage Active Model Validations when defining parameters. Arguments\nsupplied to a Task accepting parameters will be validated before the Task starts\nto run. Since arguments are specified in the user interface via text area\ninputs, it’s important to check that they conform to the format your Task\nexpects, and to sanitize any inputs if necessary.\n\n#### Validating Task Parameters\n\nTask attributes can be validated using Active Model Validations. Attributes are\nvalidated before a Task is enqueued.\n\nIf an attribute uses an inclusion validator with a supported `in:` option, the\nset of values will be used to populate a dropdown in the user interface. The\nfollowing types are supported:\n\n* Arrays\n* Procs and lambdas that optionally accept the Task instance, and return an\n  Array.\n* Callable objects that receive one argument, the Task instance, and return an\n  Array.\n* Methods that return an Array, called on the Task instance.\n\nFor enumerables that don't match the supported types, a text field will be\nrendered instead.\n\n### Masking Task Parameters\n\nTask attributes can be masked in the UI by adding `mask_attribute` class method\nin the task class. This will replace the value in the arguments list with\n`[FILTERED]` in the UI.\n\n```ruby\n# app/tasks/maintenance/sensitive_params_task.rb\n\nmodule Maintenance\n  class SensitiveParamsTask \u003c MaintenanceTasks::Task\n    attribute :sensitive_content, :string\n\n    mask_attribute :sensitive_content\n  end\nend\n```\n\nIf you have any filtered parameters in the global [Rails parameter\nfilter][rails-parameter-filter], they will be automatically taken into account\nwhen masking the parameters, which means that you can mask parameters across all\ntasks by adding them to the global rails parameters filter.\n\n[rails-parameter-filter]:https://guides.rubyonrails.org/configuring.html#config-filter-parameters\n\n```ruby\nRails.application.config.filter_parameters += %i[token]\n```\n\n### Custom cursor columns to improve performance\n\nThe [job-iteration gem][job-iteration], on which this gem depends, adds an\n`order by` clause to the relation returned by the `collection` method, in order\nto iterate through records. It defaults to order on the `id` column.\n\nThe [job-iteration gem][job-iteration] supports configuring which columns are\nused to order the cursor, as documented in\n[`build_active_record_enumerator_on_records`][ji-ar-enumerator-doc].\n\n[ji-ar-enumerator-doc]: https://www.rubydoc.info/gems/job-iteration/JobIteration/EnumeratorBuilder#build_active_record_enumerator_on_records-instance_method\n\nThe `maintenance-tasks` gem exposes the ability that `job-iteration` provides to\ncontrol the cursor columns, through the `cursor_columns` method in the\n`MaintenanceTasks::Task` class. If the `cursor_columns` method returns `nil`,\nthe query is ordered by the primary key. If cursor columns values change during\nan iteration, records may be skipped or yielded multiple times.\n\n```ruby\nmodule Maintenance\n  class UpdatePostsTask \u003c MaintenanceTasks::Task\n    def cursor_columns\n      [:created_at, :id]\n    end\n\n    def collection\n      Post.where(created_at: 2.days.ago...1.hour.ago)\n    end\n\n    def process(post)\n      post.update!(content: \"updated content\")\n    end\n  end\nend\n```\n\n### Subscribing to instrumentation events\n\nIf you are interested in actioning a specific task event, please refer to the\n[Using Task Callbacks](#using-task-callbacks) section below. However, if you\nwant to subscribe to all events, irrespective of the task, you can use the\nfollowing Active Support notifications:\n\n```ruby\nenqueued.maintenance_tasks    # This event is published when a task has been enqueued by the user.\nsucceeded.maintenance_tasks   # This event is published when a task has finished without any errors.\ncancelled.maintenance_tasks   # This event is published when the user explicitly halts the execution of a task.\npaused.maintenance_tasks      # This event is published when a task is paused by the user in the middle of its run.\nerrored.maintenance_tasks     # This event is published when the task's code produces an unhandled exception.\n```\n\nThese notifications offer a way to monitor the lifecycle of maintenance tasks in\nyour application.\n\nUsage example:\n\n```ruby\nActiveSupport::Notifications.subscribe(\"succeeded.maintenance_tasks\") do |*, payload|\n  task_name = payload[:task_name]\n  arguments = payload[:arguments]\n  metadata = payload[:metadata]\n  job_id = payload[:job_id]\n  run_id = payload[:run_id]\n  time_running = payload[:time_running]\n  started_at = payload[:started_at]\n  ended_at = payload[:ended_at]\nrescue =\u003e e\n  Rails.logger.error(e)\nend\n\nActiveSupport::Notifications.subscribe(\"errored.maintenance_tasks\") do |*, payload|\n  task_name = payload[:task_name]\n  error = payload[:error]\n  error_message = error[:message]\n  error_class = error[:class]\n  error_backtrace = error[:backtrace]\nrescue =\u003e e\n  Rails.logger.error(e)\nend\n\n# or\n\nclass MaintenanceTasksInstrumenter \u003c ActiveSupport::Subscriber\n  attach_to :maintenance_tasks\n\n  def enqueued(event)\n    task_name = event.payload[:task_name]\n    arguments = event.payload[:arguments]\n    metadata = event.payload[:metadata]\n\n    SlackNotifier.broadcast(SLACK_CHANNEL,\n      \"Job #{task_name} was started by #{metadata[:user_email]}} with arguments #{arguments.to_s.truncate(255)}\")\n  rescue =\u003e e\n    Rails.logger.error(e)\n  end\nend\n```\n\n### Using Task Callbacks\n\nThe Task provides callbacks that hook into its life cycle.\n\nAvailable callbacks are:\n\n* `after_start`\n* `after_pause`\n* `after_interrupt`\n* `after_cancel`\n* `after_complete`\n* `after_error`\n\n```ruby\nmodule Maintenance\n  class UpdatePostsTask \u003c MaintenanceTasks::Task\n    after_start :notify\n\n    def notify\n      NotifyJob.perform_later(self.class.name)\n    end\n\n    # ...\n  end\nend\n```\n\nNote: The `after_error` callback is guaranteed to complete, so any exceptions\nraised in your callback code are ignored. If your `after_error` callback code\ncan raise an exception, you’ll need to rescue it and handle it appropriately\nwithin the callback.\n\n```ruby\nmodule Maintenance\n  class UpdatePostsTask \u003c MaintenanceTasks::Task\n    after_error :dangerous_notify\n\n    def dangerous_notify\n      # This error is rescued and ignored in favour of the original error causing the error flow.\n      raise NotDeliveredError\n    end\n\n    # ...\n  end\nend\n```\n\nIf any of the other callbacks cause an exception, it will be handled by the\nerror handler, and will cause the task to stop running.\n\n### Considerations when writing Tasks\n\nMaintenance Tasks relies on the queue adapter configured for your application to\nrun the job which is processing your Task. The guidelines for writing Task may\ndepend on the queue adapter but in general, you should follow these rules:\n\n* Duration of `Task#process`: processing a single element of the collection\n  should take less than 25 seconds, or the duration set as a timeout for Sidekiq\n  or the queue adapter configured in your application. Short batches allow the\n  Task to be safely interrupted and resumed.\n* Idempotency of `Task#process`: it should be safe to run `process` multiple\n  times for the same element of the collection. Read more in [this Sidekiq best\n  practice][sidekiq-idempotent]. It’s important if the Task errors and you run\n  it again, because the same element that caused the Task to give an error may\n  well be processed again. It especially matters in the situation described\n  above, when the iteration duration exceeds the timeout: if the job is\n  re-enqueued, multiple elements may be processed again.\n\n[sidekiq-idempotent]: https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional\n\n#### Task object life cycle and memoization\n\nWhen the Task runs or resumes, the Runner enqueues a job, which processes the\nTask. That job will instantiate a Task object which will live for the duration\nof the job. The first time the job runs, it will call `count`. Every time a job\nruns, it will call `collection` on the Task object, and then `process` for each\nitem in the collection, until the job stops. The job stops when either the\ncollection is finished processing or after the maximum job runtime has expired.\n\nThis means memoization can be misleading within `process`, since the memoized\nvalues will be available for subsequent calls to `process` within the same job.\nStill, memoization can be used for throttling or reporting, and you can use\n[Task callbacks](#using-task-callbacks) to persist or log a report for example.\n\n### Writing tests for a Task\n\nThe task generator will also create a test file for your task in the folder\n`test/tasks/maintenance/`. At a minimum, it’s recommended that the `#process`\nmethod in your task be tested. You may also want to test the `#collection` and\n`#count` methods for your task if they are sufficiently complex.\n\nExample:\n\n```ruby\n# test/tasks/maintenance/update_posts_task_test.rb\n\nrequire \"test_helper\"\n\nmodule Maintenance\n  class UpdatePostsTaskTest \u003c ActiveSupport::TestCase\n    test \"#process performs a task iteration\" do\n      post = Post.new\n\n      Maintenance::UpdatePostsTask.process(post)\n\n      assert_equal \"New content!\", post.content\n    end\n  end\nend\n```\n\n### Writing tests for a CSV Task\n\nYou should write tests for your `#process` method in a CSV Task as well. It\ntakes a `CSV::Row` as an argument. You can pass a row, or a hash with string\nkeys to `#process` from your test.\n\n```ruby\n# test/tasks/maintenance/import_posts_task_test.rb\n\nrequire \"test_helper\"\n\nmodule Maintenance\n  class ImportPostsTaskTest \u003c ActiveSupport::TestCase\n    test \"#process performs a task iteration\" do\n      assert_difference -\u003e { Post.count } do\n        Maintenance::UpdatePostsTask.process({\n          \"title\" =\u003e \"My Title\",\n          \"content\" =\u003e \"Hello World!\",\n        })\n      end\n\n      post = Post.last\n      assert_equal \"My Title\", post.title\n      assert_equal \"Hello World!\", post.content\n    end\n  end\nend\n```\n\n### Writing tests for a Task with parameters\n\nTests for tasks with parameters need to instantiate the task class in order to\nassign attributes. Once the task instance is setup, you may test `#process`\nnormally.\n\n```ruby\n# test/tasks/maintenance/update_posts_via_params_task_test.rb\n\nrequire \"test_helper\"\n\nmodule Maintenance\n  class UpdatePostsViaParamsTaskTest \u003c ActiveSupport::TestCase\n    setup do\n      @task = UpdatePostsViaParamsTask.new\n      @task.updated_content = \"Testing\"\n    end\n\n    test \"#process performs a task iteration\" do\n      assert_difference -\u003e { Post.first.content } do\n        @task.process(Post.first)\n      end\n    end\n  end\nend\n```\n\n### Writing tests for a Task that uses a custom enumerator\n\nTests for tasks that use custom enumerators need to instantiate the task class\nin order to call `#enumerator_builder`. Once the task instance is set up,\nvalidate that `#enumerator_builder` returns an enumerator yielding pairs of\n`[item, cursor]` as expected.\n\n```ruby\n# test/tasks/maintenance/custom_enumerating_task.rb\n\nrequire \"test_helper\"\n\nmodule Maintenance\n  class CustomEnumeratingTaskTest \u003c ActiveSupport::TestCase\n    setup do\n      @task = CustomEnumeratingTask.new\n    end\n\n    test \"#enumerator_builder returns enumerator yielding pairs of [item, cursor]\" do\n      enum = @task.enumerator_builder(cursor: 0)\n      expected_items = [:b, :c]\n\n      assert_equal 2, enum.size\n\n      enum.each_with_index do |item, cursor|\n        assert_equal expected_items[cursor], item\n      end\n    end\n\n    test \"#process performs a task iteration\" do\n      # ...\n    end\n  end\nend\n```\n\n### Running a Task\n\n#### Running a Task from the Web UI\n\nYou can run your new Task by accessing the Web UI and clicking on \"Run\".\n\n#### Running a Task from the command line\n\nAlternatively, you can run your Task in the command line:\n\n```sh-session\nbundle exec maintenance_tasks perform Maintenance::UpdatePostsTask\n```\n\nTo run a Task that processes CSVs from the command line, use the `--csv` option:\n\n```sh-session\nbundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv \"path/to/my_csv.csv\"\n```\n\nThe `--csv` option also works with CSV content coming from the standard input:\n\n```sh-session\ncurl \"some/remote/csv\" |\n  bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv\n```\n\nTo run a Task that takes arguments from the command line, use the `--arguments`\noption, passing arguments as a set of \\\u003ckey\u003e:\\\u003cvalue\u003e pairs:\n\n```sh-session\nbundle exec maintenance_tasks perform Maintenance::ParamsTask \\\n  --arguments post_ids:1,2,3 content:\"Hello, World!\"\n```\n\n#### Running a Task from Ruby\n\nYou can also run a Task in Ruby by sending `run` with a Task name to Runner:\n\n```ruby\nMaintenanceTasks::Runner.run(name: \"Maintenance::UpdatePostsTask\")\n```\n\nTo run a Task that processes CSVs using the Runner, provide a Hash containing an\nopen IO object and a filename to `run`:\n\n```ruby\nMaintenanceTasks::Runner.run(\n  name: \"Maintenance::ImportPostsTask\",\n  csv_file: { io: File.open(\"path/to/my_csv.csv\"), filename: \"my_csv.csv\" }\n)\n```\n\nTo run a Task that takes arguments using the Runner, provide a Hash containing\nthe set of arguments (`{ parameter_name: argument_value }`) to `run`:\n\n```ruby\nMaintenanceTasks::Runner.run(\n  name: \"Maintenance::ParamsTask\",\n  arguments: { post_ids: \"1,2,3\" }\n)\n```\n\n### Monitoring your Task’s status\n\nThe web UI will provide updates on the status of your Task. Here are the states\na Task can be in:\n\n* **new**: A Task that has not yet been run.\n* **enqueued**: A Task that is waiting to be performed after a user has\n  instructed it to run.\n* **running**: A Task that is currently being performed by a job worker.\n* **pausing**: A Task that was paused by a user, but needs to finish work before\n  stopping.\n* **paused**: A Task that was paused by a user and is not performing. It can be\n  resumed.\n* **interrupted**: A Task that has been momentarily interrupted by the job\n  infrastructure.\n* **cancelling**: A Task that was cancelled by a user, but needs to finish work\n  before stopping.\n* **cancelled**: A Task that was cancelled by a user and is not performing. It\n  cannot be resumed.\n* **succeeded**: A Task that finished successfully.\n* **errored**: A Task that encountered an unhandled exception while performing.\n\n### Using Maintenance Tasks in API-only applications\n\nThe Maintenance Tasks engine uses Rails sessions for flash messages and storing\nthe CSRF token. For the engine to work in an API-only Rails application, you\nneed to add a [session middleware][] and the `ActionDispatch::Flash` middleware.\nThe engine also defines a strict [Content Security Policy][], make sure to\ninclude `ActionDispatch::ContentSecurityPolicy::Middleware` in your app's\nmiddleware stack to ensure the CSP is delivered to the user's browser.\n\n[session middleware]: https://guides.rubyonrails.org/api_app.html#using-session-middlewares\n[Content Security Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP\n\nConfiguring Rails applications is beyond the scope of this documentation, but\none way to do this is to add these lines to your application configuration:\n\n```ruby\n# config/application.rb\nmodule YourApplication\n  class Application \u003c Rails::Application\n    # ...\n    config.api_only = true\n\n    config.middleware.insert_before ::Rack::Head, ::ActionDispatch::Flash\n    config.middleware.insert_before ::Rack::Head, ::ActionDispatch::ContentSecurityPolicy::Middleware\n    config.session_store :cookie_store, key: \"_#{railtie_name.chomp(\"_application\")}_session\", secure: true\n    config.middleware.insert_before ::ActionDispatch::Flash, config.session_store, config.session_options\n    config.middleware.insert_before config.session_store, ActionDispatch::Cookies\n  end\nend\n```\n\nYou can read more in the [Using Rails for API-only Applications][rails api]\nRails guide.\n\n[rails api]: https://guides.rubyonrails.org/api_app.html\n\n### How Maintenance Tasks runs a Task\n\nMaintenance tasks can be running for a long time, and the purpose of the gem is\nto make it easy to continue running tasks through deploys, [Kubernetes Pod\nscheduling][k8s-scheduling], [Heroku dyno restarts][heroku-cycles] or other\ninfrastructure or code changes.\n\n[k8s-scheduling]: https://kubernetes.io/docs/concepts/scheduling-eviction/\n[heroku-cycles]: https://www.heroku.com/dynos/lifecycle\n\nThis means a Task can safely be interrupted, re-enqueued and resumed without any\nintervention at the end of an iteration, after the `process` method returns.\n\nBy default, a running Task will be interrupted after running for more than 5\nminutes. This is [configured in the `job-iteration` gem][max-job-runtime] and\ncan be tweaked in an initializer if necessary.\n\n[max-job-runtime]: https://github.com/Shopify/job-iteration/blob/-/guides/best-practices.md#max-job-runtime\n\nRunning tasks will also be interrupted and re-enqueued when needed. For example\n[when Sidekiq workers shut down for a deploy][sidekiq-deploy]:\n\n[sidekiq-deploy]: https://github.com/mperham/sidekiq/wiki/Deployment\n\n* When Sidekiq receives a TSTP or TERM signal, it will consider itself to be\n  stopping.\n* When Sidekiq is stopping, JobIteration stops iterating over the enumerator.\n  The position in the iteration is saved, a new job is enqueued to resume work,\n  and the Task is marked as interrupted.\n\nWhen Sidekiq is stopping, it will give workers 25 seconds to finish before\nforcefully terminating them (this is the default but can be configured with the\n`--timeout` option). Before the worker threads are terminated, Sidekiq will try\nto re-enqueue the job so your Task will be resumed. However, the position in the\ncollection won’t be persisted so at least one iteration may run again.\n\nJob queues other than Sidekiq may handle this in different ways.\n\n#### Help! My Task is stuck\n\nIf the queue adapter configured for your application doesn’t have this property,\nor if Sidekiq crashes, is forcefully terminated, or is unable to re-enqueue the\njobs that were in progress, the Task may be in a seemingly stuck situation where\nit appears to be running but is not. In that situation, pausing or cancelling it\nwill not result in the Task being paused or cancelled, as the Task will get\nstuck in a state of `pausing` or `cancelling`. As a work-around, if a Task is\n`cancelling` for more than 5 minutes, you can cancel it again. It will then be\nmarked as fully cancelled, allowing you to run it again.\n\nIf you are stuck in `pausing` and wish to preserve your tasks's position\n(instead of cancelling and rerunning), you may click \"Force pause\".\n\n### Configuring the gem\n\nThere are a few configurable options for the gem. Custom configurations should\nbe placed in a `maintenance_tasks.rb` initializer.\n\n#### Reporting errors\n\nExceptions raised while a Task is performing are rescued and information about\nthe error is persisted and visible in the UI.\n\nErrors are also sent to the `Rails.error.reporter`, which can be configured by\nyour application. See the [Error Reporting in Rails\nApplications][rails-error-reporting] guide for more details.\n\nReports to the error reporter will contain the following data:\n\n* `error`: The exception that was raised.\n* `context`: A hash with additional information about the Task and the error:\n   * `task_name`: The name of the Task that errored\n   * `started_at`: The time the Task started\n   * `ended_at`: The time the Task errored\n   * `run_id`: The id of the errored Task run\n   * `tick_count`: The tick count at the time of the error\n   * `errored_element`: The element, if any, that was being processed when the\n* `source`: This will be `maintenance-tasks`\n\nNote that `context` may be empty if the Task produced an error before any\ncontext could be gathered (for example, if deserializing the job to process your\nTask failed).\n\nHere's an example custom subscriber to the Rails error reporter for integrating\nwith an exception monitoring service (Bugsnag):\n\n```ruby\n# config/initializers/maintenance_tasks.rb\n\nclass MaintenanceTasksErrorSubscriber\n  def report(error, handled:, severity:, context:, source: nil)\n    return unless source == \"maintenance-tasks\"\n\n    Bugsnag.notify(error) do |notification|\n      notification.add_metadata(:task, context)\n    end\n  end\nend\n\nRails.error.subscribe(MaintenanceTasksErrorSubscriber.new)\n```\n\n#### Reporting errors during iteration\n\nBy default, errors raised during task iteration will be raised to the\napplication and iteration will stop. However, you may want to handle some errors\nand continue iteration. `MaintenanceTasks::Task.report_on` can be used to rescue\ncertain exceptions and report them to the Rails error reporter. Any keyword\narguments are passed to\n[ActiveSupport::ErrorReporter#report][as-error-reporter-report]:\n\n[as-error-reporter-report]: https://api.rubyonrails.org/classes/ActiveSupport/ErrorReporter.html#method-i-report\n\n```ruby\nclass MyTask \u003c MaintenanceTasks::Task\n  report_on(MyException, OtherException, severity: :info, context: {task_name: \"my_task\"})\nend\n```\n\n`MaintenanceTasks::Task` also includes `ActiveSupport::Rescuable` which you can\nuse to implement custom error handling.\n\n```ruby\nclass MyTask \u003c MaintenanceTasks::Task\n  rescue_from(MyException) do |exception|\n    handle(exception)\n  end\nend\n```\n\n#### Customizing the maintenance tasks module\n\n`MaintenanceTasks.tasks_module` can be configured to define the module in which\ntasks will be placed.\n\n```ruby\n# config/initializers/maintenance_tasks.rb\n\nMaintenanceTasks.tasks_module = \"TaskModule\"\n```\n\nIf no value is specified, it will default to `Maintenance`.\n\n#### Organizing tasks using namespaces\n\nTasks may be nested arbitrarily deeply under `app/tasks/maintenance`, for\nexample given a task file\n`app/tasks/maintenance/team_name/service_name/update_posts_task.rb` we can\ndefine the task as:\n\n```ruby\nmodule Maintenance\n  module TeamName\n    module ServiceName\n      class UpdatePostsTask \u003c MaintenanceTasks::Task\n        def process(rows)\n          # ...\n        end\n      end\n    end\n  end\nend\n```\n\n#### Customizing the underlying job class\n\n`MaintenanceTasks.job` can be configured to define a Job class for your tasks to\nuse. This is a global configuration, so this Job class will be used across all\nmaintenance tasks in your application.\n\n```ruby\n# config/initializers/maintenance_tasks.rb\n\nMaintenanceTasks.job = \"CustomTaskJob\"\n\n# app/jobs/custom_task_job.rb\n\nclass CustomTaskJob \u003c MaintenanceTasks::TaskJob\n  queue_as :low_priority\nend\n```\n\nThe Job class **must inherit** from `MaintenanceTasks::TaskJob`.\n\nNote that `retry_on` is not supported for custom Job classes, so failed jobs\ncannot be retried.\n\n#### Customizing the rate at which task progress gets updated\n\n`MaintenanceTasks.ticker_delay` can be configured to customize how frequently\ntask progress gets persisted to the database. It can be a `Numeric` value or an\n`ActiveSupport::Duration` value.\n\n```ruby\n# config/initializers/maintenance_tasks.rb\n\nMaintenanceTasks.ticker_delay = 2.seconds\n```\n\nIf no value is specified, it will default to 1 second.\n\n#### Customizing which Active Storage service to use\n\nThe Active Storage framework in Rails 6.1 and up supports multiple storage\nservices. To specify which service to use,\n`MaintenanceTasks.active_storage_service` can be configured with the service’s\nkey, as specified in your application’s `config/storage.yml`:\n\n```yaml\n# config/storage.yml\n\nuser_data:\n  service: GCS\n  credentials: \u003c%= Rails.root.join(\"path/to/user/data/keyfile.json\") %\u003e\n  project: \"my-project\"\n  bucket: \"user-data-bucket\"\n\ninternal:\n  service: GCS\n  credentials: \u003c%= Rails.root.join(\"path/to/internal/keyfile.json\") %\u003e\n  project: \"my-project\"\n  bucket: \"internal-bucket\"\n```\n\n```ruby\n# config/initializers/maintenance_tasks.rb\n\nMaintenanceTasks.active_storage_service = :internal\n```\n\nThere is no need to configure this option if your application uses only one\nstorage service. `Rails.configuration.active_storage.service` is used by\ndefault.\n\n#### Customizing the backtrace cleaner\n\n`MaintenanceTasks.backtrace_cleaner` can be configured to specify a backtrace\ncleaner to use when a Task errors and the backtrace is cleaned and persisted. An\n`ActiveSupport::BacktraceCleaner` should be used.\n\n```ruby\n# config/initializers/maintenance_tasks.rb\n\ncleaner = ActiveSupport::BacktraceCleaner.new\ncleaner.add_silencer { |line| line =~ /ignore_this_dir/ }\n\nMaintenanceTasks.backtrace_cleaner = cleaner\n```\n\nIf none is specified, the default `Rails.backtrace_cleaner` will be used to\nclean backtraces.\n\n#### Customizing the parent controller for the web UI\n\n`MaintenanceTasks.parent_controller` can be configured to specify a controller\nclass for all of the web UI engine's controllers to inherit from.\n\nThis allows applications with common logic in their `ApplicationController` (or\nany other controller) to optionally configure the web UI to inherit that logic\nwith a simple assignment in the initializer.\n\n```ruby\n# config/initializers/maintenance_tasks.rb\n\nMaintenanceTasks.parent_controller = \"Services::CustomController\"\n\n# app/controllers/services/custom_controller.rb\n\nclass Services::CustomController \u003c ActionController::Base\n  include CustomSecurityThings\n  include CustomLoggingThings\n  # ...\nend\n```\n\nThe parent controller value **must** be a string corresponding to an existing\ncontroller class which **must inherit** from `ActionController::Base`.\n\nIf no value is specified, it will default to `\"ActionController::Base\"`.\n\n#### Configure time after which the task will be considered stuck\n\nTo specify a time duration after which a task is considered stuck if it has not\nbeen updated, you can configure `MaintenanceTasks.stuck_task_duration`. This\nduration should account for job infrastructure events that may prevent the\nmaintenance tasks job from being executed and cancelling the task.\n\nThe value for `MaintenanceTasks.stuck_task_duration` must be an\n`ActiveSupport::Duration`. If no value is specified, it will default to 5\nminutes.\n\n#### Configure status reload frequency\n\n`MaintenanceTasks.status_reload_frequency` can be configured to specify how often\nthe run status should be reloaded during iteration. By default, the status is\nreloaded every second, but this can be increased to improve performance. Note that increasing the reload interval impacts how quickly\nyour task will stop if it is paused or interrupted.\n\n```ruby\n# config/initializers/maintenance_tasks.rb\nMaintenanceTasks.status_reload_frequency = 10.seconds  # Reload status every 10 seconds\n```\n\nIndividual tasks can also override this setting using the `reload_status_every` method:\n\n```ruby\n# app/tasks/maintenance/update_posts_task.rb\n\nmodule Maintenance\n  class UpdatePostsTask \u003c MaintenanceTasks::Task\n    # Reload status every 5 seconds instead of the global default\n    reload_status_every(5.seconds)\n\n    def collection\n      Post.all\n    end\n\n    def process(post)\n      post.update!(content: \"New content!\")\n    end\n  end\nend\n```\n\nThis optimization can significantly reduce database queries, especially for short iterations.\nThis is especially useful if the task doesn't need to check for cancellation/pausing very often.\n\n#### Metadata\n\n`MaintenanceTasks.metadata` can be configured to specify a proc from which to\nget extra information about the run. Since this proc will be ran in the context\nof the `MaintenanceTasks.parent_controller`, it can be used to keep the id or\nemail of the user who performed the maintenance task.\n\n```ruby\n# config/initializers/maintenance_tasks.rb\nMaintenanceTasks.metadata = -\u003e() do\n  { user_email: current_user.email }\nend\n```\n\n## Upgrading\n\nUse bundler to check for and upgrade to newer versions. After installing a new\nversion, re-run the install command:\n\n```sh-session\nbin/rails generate maintenance_tasks:install\n```\n\nThis ensures that new migrations are installed and run as well.\n\n### What if I’ve deleted my previous Maintenance Task migrations?\n\nThe install command will attempt to reinstall these old migrations and migrating\nthe database will cause problems. Use `bin/rails\nmaintenance_tasks:install:migrations` to copy the gem’s migrations to your\n`db/migrate` folder. Check the release notes to see if any new migrations were\nadded since your last gem upgrade. Ensure that these are kept, but remove any\nmigrations that already ran.\n\nRun the migrations using `bin/rails db:migrate`.\n\n## Contributing\n\nWould you like to report an issue or contribute with code? We accept issues and\npull requests. You can find the contribution guidelines on\n[CONTRIBUTING.md][contributing].\n\n[contributing]: https://github.com/Shopify/maintenance_tasks/blob/main/.github/CONTRIBUTING.md\n\n## Releasing new versions\n\nUpdates should be added to the latest draft release on GitHub as Pull Requests\nare merged.\n\nOnce a release is ready, follow these steps:\n\n* Update `spec.version` in `maintenance_tasks.gemspec`.\n* Run `bundle install` to bump the `Gemfile.lock` version of the gem.\n* Open a PR and merge on approval.\n* Deploy via [Shipit][shipit] and see the new version on\n  \u003chttps://rubygems.org/gems/maintenance_tasks\u003e.\n* Ensure the release has documented all changes and publish it.\n* Create a new [draft release on GitHub][release] with the title “Upcoming\n  Release”. The tag version can be left blank. This will be the starting point\n  for documenting changes related to the next release.\n\n[release]: https://help.github.com/articles/creating-releases/\n[shipit]: https://shipit.shopify.io/shopify/maintenance_tasks/rubygems\n","funding_links":[],"categories":["Ruby","Gems"],"sub_categories":["Data Management"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FShopify%2Fmaintenance_tasks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FShopify%2Fmaintenance_tasks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FShopify%2Fmaintenance_tasks/lists"}