Skip to content

Commit e1d95b3

Browse files
ARC-736 improving .env file structure (#1372)
* ARC-1508 - GHE UI: Update manual form page (#1363) * - Updated the order of inputs in the manual form - Text changes * - Reverted * using .env for docker-compose variables with authtoken * only create .env file in non-prod envs * updating postmerge to move global values back to .env * adding example dev local * latest work on creating default .env file, updating contributing docs * adding better documentation Co-authored-by: kAy <[email protected]>
1 parent 16cb20e commit e1d95b3

11 files changed

+143
-91
lines changed

Diff for: .dockerignore

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# Environment Files
22
.env
3-
.envrc
3+
.env.local
4+
.env.local.*
5+
.env.*.local
6+
.env.*.local.*
47

58
# DB Secret File
69
*.pem
10+
**/*.pem
711

812
# Transpiled files
913
build/

Diff for: .env.example renamed to .env.development

+3-22
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
# APP VARIABLES - NEEDED FOR PRODUCTION
22
# The ID of your GitHub App
33
NODE_ENV=development
4-
APP_ID=
5-
APP_URL=
6-
WEBHOOK_SECRET=development
7-
GITHUB_CLIENT_ID=
8-
GITHUB_CLIENT_SECRET=
94
SQS_BACKFILL_QUEUE_URL=http://127.0.0.1:4566/000000000000/backfill
105
SQS_BACKFILL_QUEUE_REGION=us-west-1
116
SQS_PUSH_QUEUE_URL=http://127.0.0.1:4566/000000000000/push
@@ -15,32 +10,18 @@ SQS_DEPLOYMENT_QUEUE_REGION=us-west-1
1510
SQS_BRANCH_QUEUE_URL=http://127.0.0.1:4566/000000000000/branch
1611
SQS_BRANCH_QUEUE_REGION=us-west-1
1712
MICROS_AWS_REGION=us-west-1
18-
# Optional but needed for feature flags
19-
LAUNCHDARKLY_KEY=
20-
21-
# Unique name for the jira app, used in the Atlassian Connect manifest to
22-
# differentiate this instance from other deployments (staging, dev instances, etc).
23-
INSTANCE_NAME=<jira-app-name>
13+
GLOBAL_HASH_SECRET=testsecret
2414

2515
# The Postgres URL used to connect to the database and secret for encrypting data
2616
DATABASE_URL=postgres://postgres:[email protected]:5432/jira-dev
27-
# Generate using `openssl rand -hex 32`
28-
STORAGE_SECRET=
17+
STORAGE_SECRET=1a5ce262945c691b1da27ed704a6c694f6eb6d8e15bc1612e2460d1aa76ce8fd
2918

3019
# DEVELOPMENT VARIABLES
3120
ATLASSIAN_SECRET=development-secret
3221
AWS_ACCESS_KEY_ID=development
3322
AWS_SECRET_ACCESS_KEY=development
34-
TUNNEL_PORT=8080
35-
WORKER_PORT=8081
36-
TUNNEL_SUBDOMAIN=<subdomain>
37-
PRIVATE_KEY_PATH=<your-private-key>.pem
38-
ATLASSIAN_URL=https://<your-instance>.atlassian.net
39-
NGROK_AUTHTOKEN=<your-ngrok-authtoken>
40-
41-
# Use `trace` to get verbose logging or `info` to show less
23+
MAINTENANCE_MODE=false
4224
LOG_LEVEL=debug
43-
WEBHOOK_PROXY_URL=
4425

4526
# Cryptor
4627
CRYPTOR_URL=http://cryptor-sidecar:26272

Diff for: .env.development.local-example

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
##### DUPLICATE THIS FILE AND REMOVE THE `-example` AT THE END #####
2+
3+
# APP VARIABLES - NEEDED FOR PRODUCTION
4+
# Github App Information
5+
APP_ID=
6+
WEBHOOK_SECRET=
7+
GITHUB_CLIENT_ID=
8+
GITHUB_CLIENT_SECRET=
9+
# Path to github private key file (relative to root of this project). If at the root, just specify filename
10+
PRIVATE_KEY_PATH=
11+
12+
# Unique name for the jira app, used in the Atlassian Connect manifest to
13+
# differentiate this instance from other deployments (staging, dev instances, etc).
14+
INSTANCE_NAME=
15+
# The Jira Instance URL you created for this app (ex. https://example.atlassian.net)
16+
ATLASSIAN_URL=

Diff for: .gitignore

+6-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
db/config.json
33

44
# Environment Files
5-
.env*
6-
!.env.example
7-
!.env.test
5+
.env
6+
.env.local
7+
.env.local.*
8+
.env.*.local
9+
.env.*.local.*
810

911
# DB Secret File
1012
*.pem
13+
**/*.pem
1114

1215
# Transpiled files
1316
build/

Diff for: .husky/create-env.sh

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
3+
DIR=$(dirname "$0")
4+
FILE="${DIR}/../.env"
5+
6+
if [ ! -f "$FILE" ]; then
7+
echo ".env file not found, creating it..."
8+
touch "$FILE"
9+
echo "APP_URL=http://localhost" >> "$FILE"
10+
echo "WEBHOOK_PROXY_URL=http://localhost/github/events" >> "$FILE"
11+
echo "NGROK_AUTHTOKEN=insert ngrok token here" >> "$FILE"
12+
echo ".env file created with defaults"
13+
fi

Diff for: .husky/post-merge

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/sh
2+
. "$(dirname "$0")/_/husky.sh"
3+
4+
DIR=$(dirname "$0")
5+
OLDFILE="${DIR}/../.env"
6+
NEWFILE="${DIR}/../.env.development.local"
7+
REGEX="^(APP_URL|WEBHOOK_PROXY_URL|NGROK_AUTHTOKEN)=.*"
8+
if [ -f "$OLDFILE" ] && [ ! -f "$NEWFILE" ]; then
9+
echo "Detected old .env file, moving to .env.development.local"
10+
mv "$OLDFILE" "$NEWFILE"
11+
echo "Moving global values back to .env file"
12+
cat "$NEWFILE" | grep -oP "${REGEX}" > "$OLDFILE"
13+
echo "Removing moved from .env.development.local file"
14+
sed -i -E "s/${REGEX}//" "$NEWFILE"
15+
fi
16+
17+
. "$(dirname "$0")/create-env.sh"

Diff for: CONTRIBUTING.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,20 @@ Your new GitHub app will need the following repository permissions & events:
6565
+ Repository
6666
+ Workflow run
6767

68-
### Setting up your `.env` file
68+
### Setting up your environment file
6969

70-
Once you've set up your GitHub app and cloned this repo, copy the content from `.env.example` and paste it to a new file called `.env`, with the following configuration:
70+
The environment files work in a fairly standardized way of having a "global" `.env` that holds information needed across all environments but is not committed. Then we have `NODE_ENV` specific environment files like `.env.development`, `.env.test`, etc, as they are non-sensitive default variables needed for those environments to work. Since they are committed, please be careful not to add sensitive information to these files - if you need to add sensitive information or you want to overwrite the environment defaults, you can create a `.local` version of that file and that will never be committed.
71+
72+
Once you've set up your GitHub app and cloned this repo, copy the file `.env.development.local-example` to a new file called `.env.development.local`. Fill in the blank fields in the file:
7173

7274
+ `APP_ID` and `GITHUB_CLIENT_ID`: Copy these values over from your new GitHub app page.
73-
+ `APP_URL`: `https://DOMAIN`
7475
+ `GITHUB_CLIENT_SECRET`: You'll need to generate a new one on your GitHub app page by hitting the `Generate a new client secret` button. Copy and paste the generated secret.
75-
+ `TUNNEL_SUBDOMAIN`: the subdomain you want to use to allow access from the internet to your local machine
7676
+ `PRIVATE_KEY_PATH`: You'll also need to generate a new private key on your GitHub app page, download it, move it to the source root of this repo, and set `PRIVATE_KEY_PATH=<your-private-key-name>.pem`
77-
+ `ATLASSIAN_URL`: The URL for the Jira instance you're testing it. If you don't have one now, please set the value of this variable after going through the step 1 of "Configuring the Jira instance" section of this document.
78-
+ `STORAGE_SECRET`: It needs to be set to a 32 char secret (anything else fails). You can generate one by running `openssl rand -hex 32` in your terminal and paste directly to your .env file.
79-
+ `INSTANCE_NAME`: Your Jira app name - will show as "GitHub (instance-name)"
80-
+ `WEBHOOK_PROXY_URL`: `https://DOMAIN/github/events`
77+
+ `ATLASSIAN_URL`: The URL for the Jira instance you're testing on. If you don't have one now, [please set the value of this variable from the steps mentioned here](#create-your-jira-instance).
78+
+ `INSTANCE_NAME`: Your Jira app name - will show as "GitHub for Jira (instance-name)"
79+
80+
Lastly, you need to replace the value of the follow variables in the global `.env` file:
81+
8182
+ `NGROK_AUTHTOKEN`: Your ngrok authtoken. If you want to use ngrok as a tunnel to test it on your Jira instance, you need an authtoken. Simply [login/signup to ngrok](https://dashboard.ngrok.com/get-started/setup), copy & paste the authtoken into this var.
8283

8384
### Running the app
@@ -87,7 +88,6 @@ The first time you run the app, simply run:
8788
```
8889
yarn install # installs node modules
8990
docker-compose up # Spin up docker containers
90-
yarn run init # Creates DBs and initializes tables, creates sqs queues
9191
```
9292

9393
That's it. Everything is ran in docker-compose, including redis, postgres, ngrok and the app (main and worker thread).

Diff for: package.json

+11-10
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,30 @@
1212
"prestart": "run-p setup clean db:dev",
1313
"init": "run-p db",
1414
"start": "run-p start:main start:worker",
15-
"start:main": "tsnd -r tsconfig-paths/register --watch=.env,db/config.json,**/*.hbs --inspect=0.0.0.0:9229 --respawn --transpile-only -- src/main.ts",
16-
"start:worker": "tsnd -r tsconfig-paths/register --watch=.env,db/config.json,**/*.hbs --inspect=0.0.0.0:9230 --respawn --transpile-only -- src/worker.ts",
15+
"start:main": "tsnd -r tsconfig-paths/register --watch=.env*,db/config.json,**/*.hbs --inspect=0.0.0.0:9229 --respawn --transpile-only -- src/main.ts",
16+
"start:worker": "tsnd -r tsconfig-paths/register --watch=.env*,db/config.json,**/*.hbs --inspect=0.0.0.0:9230 --respawn --transpile-only -- src/worker.ts",
1717
"start:production": "run-p start:main:production start:worker:production",
1818
"start:main:production": "run-s db:migrate:prod && node -r tsconfig-paths/register src/main",
1919
"start:worker:production": "run-s db:migrate:prod && node -r tsconfig-paths/register src/worker",
2020
"clean": "rimraf coverage build tmp src/**/*.js src/**/*.js.map test/**/*.js test/**/*.js.map",
21-
"setup": "ts-node -r tsconfig-paths/register prestart.ts",
22-
"setup:test": "NODE_ENV=test ts-node -r tsconfig-paths/register prestart.ts",
21+
"setup": "dotenv -- ts-node -r tsconfig-paths/register prestart.ts",
2322
"build": "tsc -p ./tsconfig.json",
2423
"build:release": "tsc -p ./tsconfig.release.json",
2524
"build:watch": "tsc -w -p ./tsconfig.json",
2625
"lint": "run-p lint:*",
2726
"lint:ts": "eslint . --ext .ts,.tsx",
2827
"lint:yaml": "yamllint github-for-jira.sd.yml",
29-
"pretest": "run-p clean setup:test db:test",
28+
"pretest": "NODE_ENV=test run-p clean setup db:test",
3029
"test": "jest --runInBand --forceExit --coverage",
3130
"test:watch": "jest --runInBand --watchAll",
3231
"db": "run-s db:create db:migrate",
3332
"db:create": "run-p db:create:dev db:create:test",
34-
"db:create:dev": "dotenv -- sequelize db:create || exit 0",
35-
"db:create:test": "dotenv -e .env.test -- sequelize db:create --env test || exit 0",
33+
"db:create:dev": "dotenv -c development -- sequelize db:create || exit 0",
34+
"db:create:test": "dotenv -c test -- sequelize db:create --env test || exit 0",
3635
"db:create:prod": "sequelize db:create --env production-migrate || exit 0",
3736
"db:migrate": "run-p db:migrate:dev db:migrate:test",
38-
"db:migrate:dev": "dotenv -- sequelize db:migrate",
39-
"db:migrate:test": "dotenv -e .env.test -- sequelize db:migrate --env test",
37+
"db:migrate:dev": "dotenv -c development -- sequelize db:migrate",
38+
"db:migrate:test": "dotenv -c test -- sequelize db:migrate --env test",
4039
"db:migrate:prod": "sequelize db:migrate --env production-migrate",
4140
"db:dev": "run-s db:create:dev db:migrate:dev",
4241
"db:test": "run-s db:create:test db:migrate:test",
@@ -61,6 +60,8 @@
6160
"cookie-session": "^2.0.0",
6261
"csurf": "^1.11.0",
6362
"date-fns": "^1.29.0",
63+
"dotenv": "^16.0.1",
64+
"dotenv-expand": "^8.0.3",
6465
"express": "^4.17.3",
6566
"express-rate-limit": "^5.1.3",
6667
"express-sslify": "^1.2.0",
@@ -109,7 +110,7 @@
109110
"@types/uuid": "^8.3.4",
110111
"@typescript-eslint/eslint-plugin": "~5.12.0",
111112
"@typescript-eslint/parser": "~5.12.0",
112-
"dotenv-cli": "^4.0.0",
113+
"dotenv-cli": "^6.0.0",
113114
"eslint": "~8.9.0",
114115
"eslint-config-prettier": "~8.3.0",
115116
"eslint-import-resolver-typescript": "^2.5.0",

Diff for: prestart.ts

+29-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { envVars } from "./src/config/env";
21
import axios, { AxiosResponse } from "axios";
32
import * as fs from "fs";
43
import * as path from "path";
5-
import { isNodeDev } from "./src/util/is-node-env";
4+
import { exec } from "child_process";
5+
import { isNodeProd } from "./src/util/is-node-env";
66

7-
const envFilePath = path.resolve(__dirname, ".env");
7+
const envFileName = ".env";
8+
const envFilePath = path.resolve(__dirname, envFileName);
89

910
const callTunnel = async () => {
1011
const results = await Promise.all([
@@ -22,12 +23,6 @@ const wait = async (time = 0) => {
2223
};
2324

2425
const waitForTunnel = async () => {
25-
// Does .env exist?
26-
if (isNodeDev() && !fs.existsSync(envFilePath)) {
27-
console.error(`.env file doesn't exist. Please create it following the steps in the CONTRIBUTING.md file.`);
28-
process.exit(1);
29-
}
30-
3126
// Call the service 3 times until ready
3227
const response = await callTunnel()
3328
.catch(callTunnel)
@@ -42,22 +37,22 @@ const waitForTunnel = async () => {
4237
envContents = envContents.replace(/APP_URL=.*/, `APP_URL=${ngrokDomain}`);
4338
envContents = envContents.replace(/WEBHOOK_PROXY_URL=.*/, `WEBHOOK_PROXY_URL=${ngrokDomain}/github/events`);
4439
fs.writeFileSync(envFilePath, envContents);
45-
console.info(`Updated .env file to use ngrok domain ${ngrokDomain}.`);
40+
console.info(`Updated ${envFileName} file to use ngrok domain '${ngrokDomain}'.`);
4641
} catch (e) {
4742
console.info(`'${envFilePath}' not found, skipping...`);
4843
}
4944
} else {
50-
console.info("Ngrok not running, skipping updating .env file.");
45+
console.info(`Ngrok not running, skipping updating ${envFileName} file.`);
5146
}
5247
};
5348

5449
const callQueues = async () => {
55-
const URLs = Object.keys(envVars)
56-
.filter(key => /^SQS_.*_QUEUE_URL$/.test(key))
57-
.map(key => envVars[key]);
50+
const queueUrls = Object.entries(process.env)
51+
.filter(([key, value]) => /^SQS_.*_QUEUE_URL$/.test(key) && value)
52+
.map(([_key, value]) => value as string);
5853
console.info(`Checking for localstack initialization...`);
59-
if (URLs.length) {
60-
const url = new URL(URLs[0]);
54+
if (queueUrls.length) {
55+
const url = new URL(queueUrls[0]);
6156
const response = await axios.get(`${url.protocol}//${url.host}/health`, { responseType: "json" });
6257
if (response.data?.services?.sqs !== "running") {
6358
console.info("localstack not initialized.");
@@ -66,9 +61,9 @@ const callQueues = async () => {
6661
console.info(`localstack initialized.`);
6762
}
6863

69-
console.info(`Calling queues: ${URLs.join(", ")}`);
64+
console.info(`Calling queues: ${queueUrls.join(", ")}`);
7065
return Promise.all(
71-
URLs.map(async (value) => {
66+
queueUrls.map(async (value) => {
7267
try {
7368
await axios.get(value);
7469
console.info(`Queue ${value} is ready...`);
@@ -92,10 +87,25 @@ const waitForQueues = async () => {
9287
console.info("All queues ready.");
9388
};
9489

90+
const createEnvFile = async () => {
91+
if (!isNodeProd() && !fs.existsSync(envFilePath)) {
92+
return new Promise<void>((resolve, reject) => {
93+
exec("./.husky/create-env.sh", (error, stdout) => {
94+
if (error) {
95+
reject(error);
96+
return;
97+
}
98+
console.info(stdout);
99+
resolve();
100+
});
101+
})
102+
}
103+
};
104+
95105
// Check to see if ngrok is up and running
96106
(async function main() {
97-
98107
try {
108+
await createEnvFile();
99109
await Promise.all([
100110
waitForTunnel(),
101111
waitForQueues()

Diff for: src/config/env.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import dotenv from "dotenv";
1+
import { config } from "dotenv";
2+
import { expand } from "dotenv-expand";
23
import path from "path";
34
import { LogLevelString } from "bunyan";
4-
import { getNodeEnv, isNodeTest } from "utils/is-node-env";
5+
import { getNodeEnv } from "utils/is-node-env";
56
import { EnvironmentEnum } from "interfaces/common";
67

78
const nodeEnv: EnvironmentEnum = EnvironmentEnum[getNodeEnv()];
@@ -26,15 +27,16 @@ const requiredEnvVars = [
2627
"CRYPTOR_SIDECAR_CLIENT_IDENTIFICATION_CHALLENGE"
2728
];
2829

29-
const filename = isNodeTest() ? ".env.test" : ".env";
30-
const env = dotenv.config({
31-
path: path.resolve(process.cwd(), filename)
32-
});
33-
34-
// TODO: add checks for environment variables here and error out if missing any
35-
if (env.error && nodeEnv !== EnvironmentEnum.production) {
36-
throw env.error;
37-
}
30+
// Load environment files
31+
const envFile = ".env";
32+
[
33+
`${envFile}.${nodeEnv}.local`,
34+
`${envFile}.local`,
35+
`${envFile}.${nodeEnv}`,
36+
envFile
37+
].map((env) => expand(config({
38+
path: path.resolve(__dirname, "../..", env)
39+
})));
3840

3941
const getProxyFromEnvironment = (): string | undefined => {
4042
const proxyHost = process.env.EXTERNAL_ONLY_PROXY_HOST;
@@ -65,7 +67,7 @@ export interface EnvVars {
6567
NODE_ENV: EnvironmentEnum,
6668
MICROS_ENV: EnvironmentEnum;
6769
MICROS_SERVICE_VERSION?: string;
68-
MICROS_GROUP: string;
70+
MICROS_GROUP: string;
6971
SQS_BACKFILL_QUEUE_URL: string;
7072
SQS_BACKFILL_QUEUE_REGION: string;
7173
SQS_PUSH_QUEUE_URL: string;

0 commit comments

Comments
 (0)