# Deploying Flask Apps to Heroku with GitHub Actions
#### Throughout this document, I will be using an example Heroku app, `myherokuappname`. Anywhere you see this, please substitute in your Heroku app name. Later in the instructions I will cover how to make an app on Heroku.
## Requirements
- Update your python dependencies
- Run `pipenv lock -r > requirements.txt` (note the `>` after the `-r`) in the directory where your `Pipfile` and `requirements.txt` files are located (they should be in the same directory)
- The output of `pipenv lock -r` is a special text string containing all of your python dependencies designed to be written to file
- The `>` operator in bash/zsh takes the incoming buffer, in this case the text output, and runs an overwrite command on the following argument, in this case `requirements.txt`, which will completely replace the contents of `requirements.txt` with the output of `pipenv lock -r`
- Don't worry, this command won't *remove* any dependencies, so long as they're still in your `Pipfile`
- Double check your project file structure. The root folder that you push to GitHub (i.e. the files that you see immediately when navigating to the repo without clicking anything) needs to contain your `Dockerfile`, `.dockerignore`, and `.gitignore`, and your `Dockerfile` needs to be correctly configured relative to the folders around it
- If your file structure looks like the standard Python starter

then you're good to go
- If it looks more like the Express starter

then you need to ensure that your `Dockerfile`, `.dockerignore`, and `.gitignore` files all exist outisde your frontend and backend folders, and that the contents of your `Dockerfile` are appropriately adjusted to build an app with that file structure:
```Docker=
FROM node:12 AS build-stage
WORKDIR /react-app
COPY react-app/. .
# You have to set this because it should be set during build time.
ENV REACT_APP_BASE_URL=https://myherokuappname.herokuapp.com/
# Build our React App
RUN npm install
RUN npm run build
FROM python:THE.EXACT.PYTHON.VERSION.IN.YOUR.PIPFILE
# Setup Flask environment
ENV FLASK_APP=app
ENV FLASK_ENV=production
ENV SQLALCHEMY_ECHO=True
EXPOSE 8000
WORKDIR /var/www
COPY backend/. .
COPY . .
COPY --from=build-stage /react-app/build/* app/static/
# Install Python Dependencies
RUN pip install -r requirements.txt
RUN pip install psycopg2
# Run flask environment
CMD gunicorn app:app
```
where calls to `react-app` and `backend` are appropriately substituted with whatever you have opted to name your respective folders.
#### Note: any console commands below beginning with `heroku` are directory and terminal-state agnostic; they may be run from any folder, inside or outside of your pipenv shell.
1. Create an app on Heroku. This is as simple as signing in to [the heroku dashboard](https://dashboard.heroku.com/apps) and either
- Clicking "new" in the top right corner, and selecting "new app"
- If you've never created an app on Heroku before, selecting the big purple "create a new app" button you should be greeted with in the middle of the page.
2. Once you've named your app, you should be taken to that app's dashboard page. Select the `Resources` tab, and search for "Heroku Postgres" - you're looking for the result with what looks like an elephant with two differently colored ears, on a purple square. Select it, and then choose "Hobby dev - free".
3. Go to the `Settings` tab, and click `Reveal Config Vars` - you should see a `DATABASE_URL` environment variable already created for you. In the empty box beneath, enter a key of `SECRET_KEY`, and give it a value of any collection of legal US keyboard characters. This key is what `Flask-WTF` will use to generate a CSRF token for your app.
- This key does not have to match the key you use in your local `.env` file - in fact, matching it won't do anything, since the CSRF token stored for `http://localhost:xxxx` is inaccessible to any other site, so generating two keys with the same cipher is pointless.
- If you skip this step, there is a 100% chance your app will crash on launch with an internal server error saying something to the effect of "a secret key is required for CSRF"
4. In your `Dockerfile`, there is a line with the environment variable `REACT_APP_BASE_URL`. Remove everything after the `=`, including whatever <> or {} characters might be there, and put in plain text the link to your Heroku site. This link will be of the format `https://myherokuappname.herokuapp.com`. If you're unsure, at the top of your heroku dashboard for the app you can click `Open App`, which will open a new tab or window with the exact link to your site, which you can copy and paste. Make sure to eliminate any extra whitespace; `REACT_APP_BASE_URL=https://myherokuappname.herokuapp.com`.
5. Open your terminal, and run `heroku apps`. If you are successfully logged into the Heroku CLI, which you should be if you've ever done it before (it persists), you should see your email address, followed by all of the apps on your Heroku account. If you are not logged in, it will give you an invalid credentials warning, and then prompt you to press any key to trigger browser-authenticated login.
- if you've never installed or configured the Heroku CLI before, the easiest way is to install the NPM package for it, globally (as distinct from within this specific project - your current terminal location does not matter)
- simply run `npm install -g heroku` - on Macs this may additionally require `sudo`
6. Once you've confirmed that you're logged in and that the new app you've created is accessible through the CLI, run
`heroku authorizations:create`
noting the lack of spaces on either side of the `:`. What this will do is generate a long-lived CLI access token, which can be used in place of a username&password, as it uniquely identifies a user authorized to interact with your Heroku account on your behalf, using your email address. There will be a line that begins with `token` - copy everything after that word, to the end of the line. It should be a series of letters, numbers, and hyphens.
7. Open the GitHub repo for your app, and click the `Settings` tab. Scroll down and select `Secrets`, then `new repository secret`. You may name this secret whatever you like; I suggest `HEROKU_API_KEY`. Then paste the token you copied from your terminal into the larger box below.
8. Click the `Actions` tab.

Select `set up a new workflow yourself`, which should open a new page with a text editor and a dummy file. Completely delete the contents of that dummy file, and replace it with this, exactly:
```bash=
name: Python application
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
steps:
- uses: actions/checkout@v2
- name: Login to Heroku Container registry
run: heroku container:login
- name: Build and push
run: heroku container:push -a myherokuappname web
- name: Release
run: heroku container:release -a myherokuappname web
```
In this section:
```bash=
on:
push:
branches:
- main
```
if your primary, production-facing branch is called something other than `main` (`production`, `master`, `Deploy`, etc) be sure to change the word `main` to match your primary branch name exactly, capitalization and all.
In this section:
```bash=
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }},
```
if you've named your environment variable something other than HEROKU_API_KEY, be sure to postfix `secrets.` with exactly that.
In this section:
```bash=
- name: Build and push
run: heroku container:push -a myherokuappname web
- name: Release
run: heroku container:release -a myherokuappname web
```
between `-a` and `web`, you must insert your heroku app name, formatted exactly as it comes before `.herokuapp.com` in the url.
9. Click `start commit` and then feel free to add a personalized commit message if you want; if you don't, GitHub will add one for you automatically, so feel free to simply click the green button.
What you've just done is create a series of commands that instructs GitHub, every time there is an explicit `push` to your main branch, OR a merged pull request (which is treated as a `push` in GitHub's internal system), GitHub will, on your behalf, build your app on its very expensive, very fast servers, and then send your app over to Heroku using your authorization token.
You've also created changes on your GitHub that don't exist on your local machine. If we don't pull these changes down before the next time we try to merge or push, we'll likely introduce merge conflicts. Quickly run `git pull` from your terminal to sync the `.github` folder.
10. This github workflow will only build the static portions of your app; your database must now be configured directly. Back in your terminal, we have to run the commands to actually build your database. On your local machine, typically we would run `flask db upgrade` and `flask seed all` to do this. These same commands must now get run on your Heroku app; to do this, prefix them both with `heroku run -a myherokuappname`. The result should look something like
```bash=
user:~/$ heroku run -a myherokuappname flask db upgrade
user:~/$ heroku run -a myherokuappname flask seed all
```
Generally speaking, any commands which you can run to interface with your Python app on your local machine, you can also run to interface with your Heroku app; they just need to be prefixed with `heroku run -a myherokuappname` to cast them through to Heroku.
One exception to this is `flask db migrate -m "commit message"` - this is a command that creates migration files for Alembic to run. Because file *creation* is something that is resolved by simply pushing to GitHub and the consequent automatic build, you should only ever have to run this command locally, as the files will already exist by the time you're able to run any commands on Heroku.
At this point, any errors that occur while attempting to upgrade or seed your database are likely due to errors within your migrations or seed files, and not errors specifically to do with Heroku. If you ever run into a situation where your local migrations are totally out of whack, and your database models seem fine but you just can't seem to upgrade, the following general workflow should solve most problems:
1. Drop and re-create your database, either through Postbird or the `psql` CLI.
`DROP DATABASE database_name;`
`CREATE DATABSE database_name WITH OWNER flask_app_name;`
3. In `app/migrations/versions`, delete all of the contents of that `versions` folder, but DO NOT delete the folder itself.
Before:

After:

Note that `alembic.ini` and the files beneath it are not actually within the `versions` folder, but are siblings of it within the `migrations` folder, as they're at the same horizontal level.
5. Run `flask db migrate` (the `-m "commit message"` is optional) once, which tells Flask-Migrate and Alembic to diff the current state of your models against the current state of your database and previous migrations, and creates a migration that takes the minimum number of steps from the most recently run migration, to get your database to match your models. This is why, if you create migrations without running them, or if you try to downgrade before running `flask db migrate`, you get an error that says `target database is not up to date` - all of the migrations which already exist must be run before a new migration can be created.
Since at this point, we will have 0 migrations previously created, and an empty database, it will create one migration to match the total absolute state of our models, with all of the steps necessary to compeltely construct the database from scratch. Then, we can run `flask db upgrade` once to run that migration, and `flask seed all` to run our seeders.
To recap:
1. drop database
2. create database
3. delete contents of `versions` folder
4. `flask db migrate`
5. `flask db upgrade`
6. `flask seed all`
If any errors occur during steps 4-6, there are errors either in your models, or in your seeders. If you're seeing `no such command seed` or similar, there is likely a syntax error somewhere in your models or in `app/__init__.py` that is preventing the "seed" command from getting created (which happens in `seeds/__init__.py`, if you were curious). Try simply starting your app with `flask run`, which will usually throw the error properly for you to find it.
4. Lastly, if we've had to drop and recreate our database locally, we'll have to do the same thing on Heroku. Heroku doesn't let you explicitly *drop* your database, since that requires escalated privileges that you simply cannot be granted. However, you *are* allowed to destroy all of the tables and the extant database structure, which is pretty much the same thing. The command to do this is
`heroku pg:reset -a myherokuappname --confirm myherokuappname`
Notice you must enter your app name twice - this is beacuse for large production apps, destroying the entire database is unusual, and could be catastrophic, so Heroku wants you to be extra sure you know what you're doing. Optionally, you may simply run `heroku pg:reset -a myherokuappname`, which will open a dialogue asking you to re-enter your app name to confirm.
After resetting your Heroku database, you must push the updates to your models/migrations/seeders to GitHub, wait for your app to successfully build and push, and then you can run the Heroku CLI commands for upgrading and seeding your database.
To recap:
1. `heroku pg:reset -a myherokuappname --confirm myherokuappname`
2. `git add / commit / push`
3. Watch the build in the Actions tab on your repo and wait for it to finish
4. `heroku run -a myappname flask db upgrade`
5. `heroku run -a myappname flask seed all`