Skip to content

Commit

Permalink
feat(deployment): support Heroku (#38)
Browse files Browse the repository at this point in the history
* add PORT to start script

* upgrade cypress to allow deploy

* move universal-cookie to devDependencies

* write cacheDirectories in package.json for heroku

* add inquirer, prompt for host

* add to ci

* deploy from GH actions

* fix graphql handler

* add heroku specific items to CI

* updates to support heroku

* heroku : dont create apps if user says no

* put back commented out things

* interpolate prisma env
  • Loading branch information
cball authored Aug 25, 2020
1 parent 23cb253 commit 02e4a93
Show file tree
Hide file tree
Showing 22 changed files with 963 additions and 200 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*ejs*
113 changes: 22 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,115 +73,46 @@ npx create-bison-app MyApp

## Setup the database

1. Create a new database locally (Postgres is the only type fully supported right now)
1. Make sure your database user has permission to create schemas and databases. We recommend using a superuser account locally to keep things easy.
1. Search for DATABASE_URL in the project, and make sure the values are correct for your system.
1. Setup your local database with `yarn db:setup`. You'll be prompted to create it if it doesn't already exist:

![Prisma DB Create Prompt](https://user-images.githubusercontent.com/14339/88480536-7e1fb180-cf24-11ea-85c9-9bed43c9dfe4.png)

If you'd like to change the database name or schema, change the DATABASE_URL in `prisma/.env`.

# Run the app locally
## Run the app locally

From the root, run `yarn dev`. This:

- runs `next dev` to run the frontend and serverless functions locally
- starts a watcher to generate the Prisma client on schema changes
- starts a watcher to generate TypeScript types for GraphQL files

# Recommended Dev Workflow

You're not required to follow this exact workflow, but we've found it gives a good developer experience.

## API

1. Generate a new GraphQL module using `yarn g:graphql`.
1. Write a type, query, input, or mutation using [Nexus](https://nexusjs.org/guides/schema)
1. Create a new request test using `yarn g:test:request`
1. Run `yarn test` to start the test watcher
1. Add tests cases and update schema code accordingly
1. The GraphQL playground (localhost:3000/api/graphql) can be helpful to form the proper queries to use in tests.
1. `types.ts` and `api.graphql` should update automatically as you change files. Sometimes it's helpful to open these as a sanity check before moving on to the frontend code.

## Frontend

1. Generate a new page using `yarn g:page`
1. Generate a new component using `yarn g:component`
1. If you need to fetch data in your component, use a cell. Generate one using `yarn g:cell` (TODO)
1. To generate a typed GraphQL query, simply add it to the component or page:

```ts
export const SIGNUP_MUTATION = gql`
mutation signup($data: SignupInput!) {
signup(data: $data) {
token
user {
id
}
}
}
`;
```

5. Use the newly generated types from codegen instead of the typical `useQuery` or `useMutation` hook. For the example above, that would be `useSignupMutation`. You'll now have a fully typed response to work with!

```tsx
import { User, useMeQuery } from './types';

// adding this will auto-generate a custom hook in ./types
export const ME_QUERY = gql`
query me {
me {
id
email
}
}
`;

// an example of taking a user as an argument with optional attributes
function noIdea(user: Partial<User>) {
console.log(user.email);
}

function fakeComponent() {
// use the generated hook
const { data, loading, error } = useMeQuery();

if (loading) return <Loading />;

// data.user will be fully typed
return <Success user={data.user}>
}
```

# Set up CI

This project uses GitHub Actions for CI and should work out of the box. Note, as you add ENV vars to your app, you'll want to also add them in GitHub Secrets.
## Next Steps

Easiest CI configuration ever, right?
After the app is running locally, you'll want to [set up deployment](./docs/deployment) and [CI](./docs/ci)

# Setup Preview / Production Deployments
# Docs

To ensure your project can be deployed using GitHub Actions, you need to add a few ENV vars to GitHub Secrets:
- [Recommended Dev Workflow](./docs/devWorkflow.md)
- [Deployment](./docs/deployment.md)
- [CI Setup](./docs/ci.md)
- [FAQ](./docs/faq.md)

![ENV Vars](https://user-images.githubusercontent.com/14339/89292945-228fab00-d62b-11ea-90c2-4198dfcf30f1.png)
Have an idea to improve Bison? [Let us know!](https://github.com/echobind/bisonapp/issues/new)

The Vercel project and org id, can be copied from `.vercel/project.json`. You can generate a token from https://vercel.com/account/tokens.
<hr style="margin-top: 120px" />

After tests pass, the app will deploy to Vercel. By default, every push creates a preview deployment. Merging to the main branch will deploy to staging, and pushing to the production branch will deploy to production.
### About Echobind

If you'd like to change these configurations, update the section below:
Echobind is a full-service digital agency that creates web and mobile apps for clients across a variety of industries.

```
## For a typical JAMstack flow, this should be your default branch.
## For a traditional flow that auto-deploys staging and deploys prod is as needed, keep as is
if: github.ref != 'refs/heads/production' # every branch EXCEPT production
```
We're experts in React, React Native, Node, GraphQL, and Rails.

# FAQ
If you're building a new app, your team is tackling a hard problem, or you just need help getting over the finish line, we'd love to work with you. [Say hello](https://echobind.com/contact) and tell us what you're working on.

## Where are the generated types?

TypeScript Types for GraphQL types, queries, and mutations are generated automatically and placed in `./types.ts`.

## VSCode can't find new types, even though they are in ./types.ts

Try reopening VSCode.
<p align="center" style="margin-top:40px">
<a href="https://echobind.com" target="_blank">
<img src="https://user-images.githubusercontent.com/14339/80931246-808bc880-8d86-11ea-9de5-39203d3ed5f5.png" alt="Echobind Logo">
</a>
</p>
95 changes: 93 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,78 @@
#!/usr/bin/env node
const createBisonApp = require(".");
const inquirer = require("inquirer");
const Logo = require("./logo");
const execa = require("execa");

/**
* Generates questions for Inquirer based on an app name.
* @param {string} appName The name of the app
*/
function generateQuestions(appName) {
return [
{
name: "githubRepo",
type: "input",
message: "Create a new GitHub repo and paste the url here:",
},
{
name: "host.name",
type: "list",
message: "Where will you deploy the app?",
choices: [
{ name: "Vercel (recommended)", value: "vercel" },
{ name: "Heroku", value: "heroku" },
],
default: "vercel",
},
{
name: "host.createAppsAndPipelines",
type: "confirm",
message: "Do you want to automatically create apps and pipelines?",
when: (answers) => answers.host.name === "heroku",
default: true,
},
{
name: "host.staging.name",
type: "input",
message: "Enter the name for the staging app (must be unique)",
when: ({ host }) => host.name === "heroku" && host.createAppsAndPipelines,
default: `${appName}-staging`,
},
{
name: "host.staging.db",
type: "list",
message: "What database tier do you want on staging?",
choices: ["heroku-postgresql:hobby-dev", "heroku-postgresql:standard-0"],
when: ({ host }) => host.name === "heroku" && host.createAppsAndPipelines,
default: "heroku-postgresql:hobby-dev",
},
{
name: "host.production.name",
type: "input",
message: "Enter the name for the production app (must be unique)",
when: ({ host }) => host.name === "heroku" && host.createAppsAndPipelines,
default: `${appName}`,
},
{
name: "host.production.db",
type: "list",
message: "What database tier do you want on production?",
choices: ["heroku-postgresql:hobby-dev", "heroku-postgresql:standard-0"],
when: ({ host }) => host.name === "heroku" && host.createAppsAndPipelines,
default: "heroku-postgresql:standard-0",
},
];
}

/**
* Verifies a user is logged into the Heroku CLI.
* If they aren't
*/
async function verifyHerokuLogin() {
const { stdout } = await execa("heroku", ["whoami"]);
console.log(`Logged into Heroku as ${stdout}`);
}

require("yargs").usage(
"$0 <name>",
Expand All @@ -10,7 +83,25 @@ require("yargs").usage(
type: "string",
});
},
function (yargs) {
createBisonApp(yargs.name);
async function (yargs) {
// Show the logo!
console.log(Logo);

const { name } = yargs;
const questions = generateQuestions(name);
const answers = await inquirer.prompt(questions);
const hostName = answers.host.name;

if (hostName !== "heroku") createBisonApp({ name, ...answers });

// If heroku, make sure they are logged in before continuing.
try {
await verifyHerokuLogin();
createBisonApp({ name, ...answers });
} catch {
console.error(
`\n\nIt looks like you're not logged in to Heroku CLI. Use \`heroku login\` and try again.`
);
}
}
).argv;
41 changes: 41 additions & 0 deletions docs/ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Set up CI

This project uses GitHub Actions for CI and should work out of the box. After you've successfully deployed your app, you need to do is set a few ENV vars. Like most CI environments, as you add ENV vars to your app you'll want to also add them in GitHub Secrets.

## Configure Vercel Deployments

To deploy to Vercel, you need to set `VERCEL_ORG_ID`, `VERCEL_PROJECT_ID` and `VERCEL_TOKEN` ENV vars.

![ENV Vars](https://user-images.githubusercontent.com/14339/89292945-228fab00-d62b-11ea-90c2-4198dfcf30f1.png)

The Vercel project and org id, can be copied from `.vercel/project.json`. You can generate a token from https://vercel.com/account/tokens.

### CI Workflow

After tests pass, the app will deploy. Every push will create a preview deployment. Merging to the main branch will deploy to staging, and pushing to the production branch will deploy to production.

If you'd like to change these configurations to a more typical JAMstack flow (where merging to the main branch deploys to production), update the section below in `..github/workflows/main.js.yml`:

```
## For a typical JAMstack flow, this should be your default branch.
## For a traditional flow that auto-deploys staging and deploys prod is as needed, keep as is
if: github.ref != 'refs/heads/production' # every branch EXCEPT production
```

## Configure Heroku Deployments

Heroku needs to login as a user in order to create apps and deploy code. While you can do this using your personal Heroku account, we highly recommend creating a "machine user" for better security (for example, ci@echobind.com). The machine user only has access to the required repositories instead of every repository your user account has access to.

Heroku requires an email and an API Token in order to log in. To get an API token visit your [account settings](https://dashboard.heroku.com/account).

![Heroku Account Settings](https://user-images.githubusercontent.com/14339/90963163-9af7c800-e483-11ea-9a15-0f86bd63cf7e.png)

Next, add `HEROKU_API_TOKEN` and `HEROKU_EMAIL` to GitHub Secrets:

![Heroku Secrets](https://user-images.githubusercontent.com/14339/90963197-df836380-e483-11ea-8449-0076da74c689.png)

After tests pass, the app will deploy. Every push will create a preview deployment. Merging to the main branch will deploy to staging, and pushing to the production branch will deploy to production.

## You're done!

Easiest CI configuration ever, right?
22 changes: 22 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Preview / Staging and Production Deployment Setup

## Deploying to Vercel

- Create a new Vercel app by running `vercel`.
- Create a staging and production database on Heroku, Digital Ocean, or AWS. For production, you likely want to enable some form of connection pooling so that you don't exhaust database connections.
- Add the appropriate values for the `APP_SECRET` and `DATABASE_URL` ENV vars to the app settings page (https://vercel.com/<org>/<app>/settings/environment-variables). Use the staging URL for preview and the production URL for production.

Verify things work by running `vercel` again.

## Deploying to Heroku

Heroku is not typically used to host Jamstack apps. If possible, you should leverage Vercel or Netlify (coming soon!) as they have some advantages in doing so. That said, we wanted to ensure Bison apps could still be deployed to Heroku if required. Especially since many choose to use Heroku to host their database.

- Create a new staging app: `heroku apps:create myapp-staging --remote staging`
- Add a value for APP_SECRET. `heroku config:add APP_SECRET=mysecret --remote staging`
- Add a database: `heroku addons:create heroku-postgresql:hobby-dev --remote staging`
- Create a new production app: `heroku apps:create myapp --remote production`
- Add a value for APP_SECRET. `heroku config:add APP_SECRET=mysecret --remote production`
- Add a database: `heroku addons:create heroku-postgresql:standard-0 --remote production`. If you're just testing things out, hobby-dev is sufficient. For production apps, you'll want to use the Standard Tier.

Verify things work by running `git push staging` and `git push production`.
65 changes: 65 additions & 0 deletions docs/devWorkflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Recommended Dev Workflow

You're not required to follow this exact workflow, but we've found it gives a good developer experience.

We start from the API and then create the frontend. The reason for this is that Bison will generate types for your GraphQL operations which you will leverage in your components on the frontend.

## API

1. Generate a new GraphQL module using `yarn g:graphql`.
1. Write a type, query, input, or mutation using [Nexus](https://nexusjs.org/guides/schema)
1. Create a new request test using `yarn g:test:request`
1. Run `yarn test` to start the test watcher
1. Add tests cases and update schema code accordingly. The GraphQL playground (localhost:3000/api/graphql) can be helpful to form the proper queries to use in tests.
1. `types.ts` and `api.graphql` should update automatically as you change files. Sometimes it's helpful to open these as a sanity check before moving on to the frontend code.

## Frontend

1. Generate a new page using `yarn g:page`
1. Generate a new component using `yarn g:component`
1. If you need to fetch data in your component, use a cell. Generate one using `yarn g:cell`
1. To generate a typed GraphQL query, simply add it to the component or page:

```ts
export const SIGNUP_MUTATION = gql`
mutation signup($data: SignupInput!) {
signup(data: $data) {
token
user {
id
}
}
}
`;
```

5. Use the newly generated hooks from Codegen instead of the typical `useQuery` or `useMutation` hook. For the example above, that would be `useSignupMutation`. You'll now have a fully typed response to work with!

```tsx
import { User, useMeQuery } from './types';

// adding this will auto-generate a custom hook in ./types.
export const ME_QUERY = gql`
query me {
me {
id
email
}
}
`;

// an example of taking a user as an argument with optional attributes
function noIdea(user: Partial<User>) {
console.log(user.email);
}

function fakeCell() {
// use the generated hook
const { data, loading, error } = useMeQuery();

if (loading) return <Loading />;

// data.user will be fully typed
return <Success user={data.user}>
}
```
9 changes: 9 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# FAQ

## Where are the generated types?

TypeScript Types for GraphQL types, queries, and mutations are generated automatically and placed in `./types.ts`.

## VSCode can't find new types, even though they are in ./types.ts

Try reopening VSCode.
Loading

0 comments on commit 02e4a93

Please sign in to comment.