Release Phase
Last updated October 11, 2024
Table of Contents
The release phase enables certain tasks to be run before a new release of an app is deployed. The release phase can be useful for tasks such as:
- Sending CSS, JS, and other assets from the app’s slug to a CDN or S3 bucket
- Priming or invalidating cache stores
- Running database schema migrations
If a release phase task fails, the new release is not deployed, leaving the current release unaffected.
When using the release phase, there are a number of design considerations to take into account, especially if performing database migrations. The release phase has a 1-hour timeout, and this limit cannot be extended.
Specifying release phase tasks
To specify the tasks to run during the release phase, define a release
process type in the app’s Procfile. For deploying Docker images to Heroku, learn more about using the release phase with Container Registry.
In this Procfile example, release
executes a Django database migration:
release: python manage.py migrate
web: gunicorn myproject.wsgi
Whereas in this example, release
runs a script that includes several different commands:
release: ./release-tasks.sh
web: gunicorn myproject.wsgi
When does the release
command run?
The release
command runs in a one-off dyno whenever a new release is created, unless the release is caused by changes to an add-on’s config vars. All of the following events create a new release:
- A successful app build
- A change to the value of a config var (unless the config var is associated with an add-on)
- A pipeline promotion
- A rollback
- A release via the platform API
- Provisioning a new add-on
App dynos do not boot for a new release until the release phase finishes successfully:
Use the heroku ps
command to see your release
command running.
The dyno type can be set using `heroku ps:type release={type}, but only after the release phase is run for the first time.
Release command failure
If the release
command exits with a non-zero exit status, or if it’s shut down by the dyno manager, the release fails. In this case, the release is not deployed to the app’s dyno formation. An email notification is generated in the event of a release phase failure.
If a release is triggered by a change to the value of a config var, the config var value remains changed even if the release
command fails.
It is possible for a build to succeed and its associated release to fail. This does not clear the build cache.
A failed release
command usually requires a fix to an app’s code. After making the necessary changes, push the new code to trigger a new release.
In some cases, a release failure is unrelated to the app’s code. For example, an external service might be unavailable during the release phase. In such occurrences, the releases-retry CLI plugin can be used to retry a failed release without needing to trigger a new build on the app.
Checking release status & logs
To check on the status of a release, including failed
releases and pending
releases due to a long-running release
command, run heroku releases
.
$ heroku releases
=== limitless-savannah-19617 Releases - Current: v52
v53 Deploy ad7c527 release command failed jbyrum@heroku.com
v52 Deploy b41eb7c jbyrum@heroku.com
v51 Deploy 38352d3 jbyrum@heroku.com
...
To see the output of a particular release
command, use the heroku releases:output
command:
$ heroku releases:output RELEASE_NUMBER
--- Migrating db ---
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
Release logs are also available from the Heroku dashboard:
Heroku automatically updates any pending
releases to failed
after 24 hours.
Checking release status programmatically
To check the status of a release programmatically, query the Platform API for a specific release or a list of all releases. See the Platform API documentation for more details. In the following example using curl
, the status
key has a value of failed
:
$ curl -n https://api.heroku.com/apps/<app_id_or_name>/releases/ \
-H "Accept: application/vnd.heroku+json; version=3"
{
"app":{
"id":"a933b6af-b2e3-4a03-91a9-1c758110a553",
"name":"limitless-savannah-19617"
},
"created_at":"2016-07-29T22:43:20Z",
"description":"Deploy ad7c527",
"status":"failed",
"id":"735a9e8c-ef49-4047-8079-984d40a84051",
"slug":{
"id":"a49ed40f-801a-45ff-8b90-758c5a33637f"
},
"updated_at":"2016-07-29T22:43:20Z",
"user":{
"email":"jbyrum@heroku.com",
"id":"cbcd34ce-c556-4289-8bc7-2dc160649fb7"
},
"version":53,
"current":true,
"output_stream_url":"https://release-output.heroku.com/streams/a9/..."
}
The output of the release is available under the output_stream_url
attribute and can be retrieved programmatically by making a GET
request on the URL.
The output is sent via chunked encoding, and the connection is closed when the command completes.
If a client connects after data is sent for a given URL, the data is buffered from the start of the command. Output can be streamed while the command is in progress, and at any time after it has completed (in the latter case, all output will be sent immediately).
Canceling a release command
To cancel a release
command, identify the one-off dyno executing the command:
$ heroku ps
=== release (Eco): bundle exec rake db:migrate
release.5129: up 2016/02/01 12:17:45 (~ 2s ago)
Then, provide the name of the one-off dyno to the heroku ps:stop
command:
$ heroku ps:stop release.5129
Review apps and the postdeploy script
Review apps run the code in any GitHub pull request in a complete, disposable Heroku app. Review apps support a postdeploy script, which is used to run one-time setup tasks.
The following timeline illustrates the order of operations and a recommended division of tasks when using both the release phase and a postdeploy script for review apps.
With each new pull request, review app creation begins.
- After a successful build, the
release
command runs. This is recommended for:- Database schema setup and migrations
- CDN uploads
- Cache invalidation and warmup
- If release phase fails, review app creation also fails
After the release phase succeeds:
- The postdeploy script runs. This is recommended for one-off tasks, such as:
- Setting up OAuth clients and DNS
- Loading seed or test data into the review app’s test database
On any subsequent changes to the pull request:
- The
release
command runs again. - The postdeploy script does NOT run again. The postdeploy runs only once on creation of the review app.
The recommendations in this section also apply to Deploy to Heroku button apps that use a postdeploy script.
Design considerations
Minimize the amount of time a failed release is most recent
In the case where a build has succeeded, but the release phase failed, the built slug will be used by any later releases triggered by add-on config var changes. As those releases do not run the release phase, this can mean application code deploys without release phase tasks it depends on (database migrations, for example) being run. We recommend any release phase failures are resolved as soon as possible to avoid this (for example, by temporarily rolling back to a previous release).
Do not use the release phase for tasks requiring file system persistence
The release phase is not suitable for tasks that require filesystem persistence, as filesystem changes during the release phase will not be deployed to your app’s dyno formation (the dyno filesystem is ephemeral). For example, asset compilation should happen during builds. The release phase then can be used to upload the compiled assets to a CDN.
The following considerations are primarily relevant when creating manual database migration scripts. If using an ORM, such as ActiveRecord, these considerations may not apply. Learn more about database best practices.
Use transactions for database migrations
When performing a database migration, always use transactions. A transaction ensures that all migration operations are successful before committing changes to the database, minimizing the possibility of a failed partial migration during the release phase. If a database migration fails during the release phase (i.e., the migration command exits with a non-zero status), the new release will not be deployed. If transactions were not used, this could leave the database in a partially migrated state. We suggest using heroku run
, rather than the release phase, to make schema/data corrections.
Check whether a database has already been migrated before executing a migration
Many actions create a new release, such as setting a config var or adding a new addon to the app. The database migration script should check whether a database has already been migrated before executing a new migration (e.g., does table/column exist, if not add it). Doing so will prevent a new release – such as one created from a new config var – from rerunning database migrations.
Before running a migration, grab an advisory lock on the database
Heroku releases can run concurrently, which can be a problem if the release phase is executing a database migration. Many popular relational databases, such as Postgres and MySQL, offer advisory lock functionality, which can be used to prevent concurrent migrations. Advisory locks are application enforced database locks; when acquired, the tables are not locked for writing, so the application will continue to behave normally.
Postgres makes it possible to acquire an advisory lock before running migrations. In the example below, we try to get an advisory lock with the key migration
:
SELECT pg_try_advisory_lock(migration);
If the lock is successful, Postgres will return t
. With a lock in place, it is now safe to run migrations. If unsuccessful, f
is returned, instead, and migrations are safe to fail.
If the advisory lock is acquired within a transaction, it will be automatically released when the transaction is committed. Locks can be released manually using:
SELECT pg_advisory_unlock_all();
or
SELECT pg_advisory_unlock(key);
Ensure wrapped release phase tasks have their exit status properly rolled up
In the case of a release phase where multiple tasks are wrapped into a single file (e.g., release-tasks.sh
), ensure each of those tasks has its exit status captured. If the captured status is not 0
(zero), the wrapper is responsible for adequately conveying it. Not capturing exit statuses for any wrapped commands will see their statuses swallowed, and the release phase will report as successful merely because of the successful invocation of release.sh
.
A simple example of this behavior would look like:
# release-tasks.sh
## Step to execute
bundle exec rails db:migrate
# check for a good exit
if [ $? -ne 0 ]
then
# something went wrong; convey that and exit
exit 1
fi
# other code, potentially
Known issues
- It is possible for a release command to fail due to an add-on not being fully provisioned when the command is run. For example, database migrations are triggered, but the database add-on isn’t finished with provisioning. This issue does NOT impact Heroku Postgres or Heroku Key-Value Store.