Skip to content

Commit 0a7b41c

Browse files
authored
Adding automatic app install (#2064)
* fixing APP_KEY throughout * fixing test * fixing test for auto install app * fixing e2e failure * fixing auto install * fixing a few things * small fix for staging/production * updating snapshots
1 parent ffb0392 commit 0a7b41c

27 files changed

+149
-87
lines changed

.env.example

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,18 @@ NGROK_AUTHTOKEN=some-token
44
# Unique name for the jira app, used in the Atlassian Connect manifest to
55
# differentiate this instance from other deployments (staging, dev instances, etc).
66
# Recommended to use your github username here
7-
INSTANCE_NAME=your-app-instance-name
7+
APP_KEY=com.github.integration.your-unique-instance-name
8+
9+
# Your Jira URL
10+
ATLASSIAN_URL=https://your-unique-jira-subdomain.atlassian.net
11+
12+
# Add your Jira email/API token for automatic app installation
13+
# Create a new API token in Jira [https://id.atlassian.com/manage-profile/security/api-tokens]
14+
JIRA_ADMIN_EMAIL=
15+
JIRA_ADMIN_API_TOKEN=
816

917
# These will be automatically filled when the docker container starts
1018
APP_URL=http://localhost
1119
WEBHOOK_PROXY_URL=http://localhost
20+
21+

.env.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ DEBUG=nock.*
2323
MICROS_AWS_REGION=us-west-1
2424

2525
# Do not change these as this will break unit tests
26-
INSTANCE_NAME=test-atlassian-instance
26+
APP_KEY=com.github.integration.test-atlassian-instance
2727
APP_URL=https://test-github-app-instance.com
2828

2929
# The Postgres URL used to connect to the database and secret for encrypting data

.github/workflows/on-push.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
echo "${{ secrets.E2E_GITHUB_PRIVATE_KEY }}" > jira-test.pem
3838
echo "APP_URL=http://localhost" > .env
3939
echo "WEBHOOK_PROXY_URL=http://localhost" >> .env
40-
echo "INSTANCE_NAME=fusion-test" >> .env
40+
echo "APP_KEY=com.github.integration.fusion-test" >> .env
4141
echo "NODE_ENV=test" >> .env
4242
echo "APP_NAME=jira" >> .env
4343
echo "APP_ID=${{ secrets.E2E_GITHUB_APP_ID }}" >> .env
@@ -89,7 +89,7 @@ jobs:
8989
echo "APP_URL=http://localhost" > .env
9090
echo "WEBHOOK_PROXY_URL=http://localhost" >> .env
9191
echo "NGROK_AUTHTOKEN=${{ secrets.E2E_NGROK_AUTHTOKEN }}" >> .env
92-
echo "INSTANCE_NAME=fusion-arc-e2e" >> .env
92+
echo "APP_KEY=com.github.integration.fusion-arc-e2e" >> .env
9393
echo "NODE_ENV=e2e" >> .env
9494
echo "APP_NAME=jira-e2e" >> .env
9595
echo "APP_ID=${{ secrets.E2E_GITHUB_APP_ID }}" >> .env

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Environment Files
22
.env
3-
.env.local
3+
.env.bak.local
44
.env.local.*
55
.env.*.local
66
.env.*.local.*
@@ -55,4 +55,4 @@ test/**/*.js.map
5555
test/e2e/test-results/
5656

5757
*.drawio.bkp
58-
*.drawio.dtmp
58+
*.drawio.dtmp

.husky/post-merge

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,4 @@
11
#!/bin/sh
22
. "$(dirname "$0")/_/husky.sh"
33

4-
DIR=$(dirname "$0")
5-
OLDFILE="${DIR}/../.env"
6-
NEWFILE="${DIR}/../.env.development.local"
7-
REGEX="^(APP_URL|WEBHOOK_PROXY_URL|NGROK_AUTHTOKEN|INSTANCE_NAME)=.*"
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 -oE "${REGEX}" > "$OLDFILE"
13-
echo "Removing moved from .env.development.local file"
14-
sed -i '' -E "s/${REGEX}//" "$NEWFILE"
15-
fi
16-
174
. "$(dirname "$0")/create-env.sh"

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ Once you've set up your GitHub app and cloned this repo, copy the file `.env.dev
7575
+ `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.
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`
7777
+ `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)"
78+
+ `APP_KEY`: Your Jira app key - need to be unique for your development app
7979

8080
Lastly, you need to replace the value of the follow variables in the global `.env` file:
8181

Dockerfile

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ FROM node:14.21-alpine3.16 as build
44
RUN apk add g++ make python3
55

66
# adding to solve vuln
7-
RUN apk add --update --upgrade busybox
8-
RUN apk add --update --upgrade libretls
9-
RUN apk add --update --upgrade openssl
10-
RUN apk add --update --upgrade zlib
7+
RUN apk add --update --upgrade busybox libretls openssl zlib
118

129
COPY . /app
1310
WORKDIR /app

docker-compose.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,15 @@ services:
7070
SQS_BRANCH_QUEUE_URL: http://localstack:4566/000000000000/branch
7171
SQS_TEST_QUEUE_URL: http://localstack:4566/000000000000/test-sqs-client
7272
REDISX_CACHE_HOST: redis
73-
NODE_ENV: $NODE_ENV
73+
NODE_ENV: ${NODE_ENV:-development} # defaults to development if NODE_ENV not set
74+
75+
installation:
76+
depends_on:
77+
- app
78+
volumes:
79+
- ./etc/app-install:/app
80+
build:
81+
context: .
82+
dockerfile: etc/app-install/Dockerfile
83+
env_file:
84+
- .env

etc/app-install/Dockerfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM alpine:latest as build
2+
3+
RUN apk --no-cache add curl jq
4+
5+
COPY ./etc/app-install/app-install.sh ./app/app-install.sh
6+
WORKDIR /app
7+
8+
CMD ["sh", "app-install.sh"]

etc/app-install/app-install.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Checks to see if required env vars are set
2+
if [[ -z "$JIRA_ADMIN_EMAIL" ]] || [[ -z "$JIRA_ADMIN_API_TOKEN" ]] || [[ -z "$ATLASSIAN_URL" ]] || [[ -z "$APP_KEY" ]]
3+
then
4+
echo "Missing environment variables from .env - Please fill in 'JIRA_ADMIN_EMAIL', 'JIRA_ADMIN_API_TOKEN', 'ATLASSIAN_URL' and 'APP_KEY' to be able to have the app install automatically."
5+
exit 1
6+
fi
7+
8+
curl --head -X GET -f --retry 30 --retry-all-errors --retry-delay 5 http://app:8080/healthcheck
9+
10+
# Fetching the new ngrok URL, not fetching the one from the .env because its not updated
11+
BASE_URL=$(curl -s http://ngrok:4040/api/tunnels | jq -r '.tunnels[] | select(.proto == "https") | .public_url')
12+
ID="${APP_KEY##*.}"
13+
# Uninstalling the app first
14+
curl -s -X DELETE -u "$JIRA_ADMIN_EMAIL:$JIRA_ADMIN_API_TOKEN" -H "Content-Type: application/vnd.atl.plugins.install.uri+json" "${ATLASSIAN_URL}/rest/plugins/1.0/${APP_KEY}-key"
15+
echo "Uninstalling old version of the app"
16+
17+
# Getting the UPM token first, which will be used for app installation
18+
UPM_TOKEN=$(curl -s -u "$JIRA_ADMIN_EMAIL:$JIRA_ADMIN_API_TOKEN" --head "${ATLASSIAN_URL}/rest/plugins/1.0/" | fgrep upm-token | cut -c 12- | tr -d '\r\n')
19+
20+
# Installing the app
21+
curl -s -o /dev/null -u "$JIRA_ADMIN_EMAIL:$JIRA_ADMIN_API_TOKEN" -H "Content-Type: application/vnd.atl.plugins.install.uri+json" -X POST "${ATLASSIAN_URL}/rest/plugins/1.0/?token=${UPM_TOKEN}" -d "{\"pluginUri\":\"${BASE_URL}/jira/atlassian-connect.json\", \"pluginName\": \"Github for Jira (${ID})\"}"
22+
echo ""
23+
echo "The app has been successfully installed."
24+
echo "
25+
*********************************************************************************************************************
26+
IF YOU ARE USING A FREE NGROK ACCOUNT, PLEASE DO THIS STEP FIRST!!!
27+
Before going to your app, please open this URL first: ${BASE_URL}.
28+
This will open up the ngrok page, don't worry just click on the Visit button.
29+
That's it, you're all ready!
30+
*********************************************************************************************************************
31+
*********************************************************************************************************************
32+
Now open your app in this URL: ${ATLASSIAN_URL}/plugins/servlet/ac/${APP_KEY}/gh-addon-admin
33+
*********************************************************************************************************************
34+
"

etc/cryptor-mock/cryptor-mock.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
/**
2-
* Mimicks APIs from https://developer.atlassian.com/platform/cryptor/integration/integrating-sidecar/#rest-api
2+
* Mimics APIs from https://developer.atlassian.com/platform/cryptor/integration/integrating-sidecar/#rest-api
33
*/
44

55
const express = require("express");
66
const bodyParser = require("body-parser");
77

88
const app = express();
9+
910
app.use(bodyParser.json());
10-
app.get("/healthcheck", (_, res)=>{
11-
res.send({ok: true});
11+
12+
app.get("/healthcheck", (_, res) => {
13+
res.send({ ok: true });
1214
});
15+
1316
app.post("/cryptor/encrypt/*", (req, res) => {
1417
if (req.headers["x-cryptor-client"] !== process.env.CRYPTOR_SIDECAR_CLIENT_IDENTIFICATION_CHALLENGE) {
1518
res.status(403).send("Wrong challenge");
@@ -24,9 +27,10 @@ app.post("/cryptor/encrypt/*", (req, res) => {
2427
cipherText: `encrypted:${plainText}`,
2528
originPlainText: plainText
2629
};
27-
console.log('-- cyrptor mock encrypt', {ret});
30+
console.log("-- cyrptor mock encrypt", { ret });
2831
res.status(200).json(ret);
2932
});
33+
3034
app.post("/cryptor/decrypt", (req, res) => {
3135
if (req.headers["x-cryptor-client"] !== process.env.CRYPTOR_SIDECAR_CLIENT_IDENTIFICATION_CHALLENGE) {
3236
res.status(403).send("Wrong challenge");
@@ -45,10 +49,27 @@ app.post("/cryptor/decrypt", (req, res) => {
4549
plainText: cipherText.substring("encrypted:".length),
4650
originCipherText: cipherText
4751
};
48-
console.log('-- cyrptor mock decrypt', {ret});
52+
console.log("-- cyrptor mock decrypt", { ret });
4953
res.status(200).json(ret);
5054
});
5155

52-
app.listen(26272, () => {
56+
const server = app.listen(26272, () => {
5357
console.log(`Cryptor mock app running on 26272`);
5458
});
59+
60+
let connections = [];
61+
server.on('connection', connection => {
62+
connections.push(connection);
63+
connection.on('close', () => connections = connections.filter(curr => curr !== connection));
64+
});
65+
66+
const exit = () => {
67+
server.close(() => process.exit(0));
68+
connections.forEach(connection => {
69+
connection.end();
70+
connection.destroy();
71+
});
72+
}
73+
74+
process.on('SIGTERM', exit);
75+
process.on('SIGINT', exit);

github-for-jira.sd.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ environmentOverrides:
327327
environmentVariables:
328328
APP_URL: https://github-for-jira.dev.services.atlassian.com
329329
WEBHOOK_PROXY_URL: https://github-for-jira.dev.services.atlassian.com
330-
INSTANCE_NAME: development
330+
APP_KEY: com.github.integration.development
331331
NODE_OPTIONS: "--no-deprecation"
332332
LOG_LEVEL: debug
333333
SENTRY_ENVIRONMENT: ddev
@@ -399,7 +399,7 @@ environmentOverrides:
399399
environmentVariables:
400400
APP_URL: https://github.stg.atlassian.com
401401
WEBHOOK_PROXY_URL: https://github.stg.atlassian.com
402-
INSTANCE_NAME: staging
402+
APP_KEY: com.github.integration.staging
403403
LOG_LEVEL: debug
404404
SENTRY_ENVIRONMENT: stg-west
405405
APP_ID: '12645'
@@ -500,7 +500,7 @@ environmentOverrides:
500500
environmentVariables:
501501
APP_URL: https://github.atlassian.com
502502
WEBHOOK_PROXY_URL: https://github.atlassian.com
503-
INSTANCE_NAME: production
503+
APP_KEY: com.github.integration.production
504504
SENTRY_ENVIRONMENT: prod-west
505505
APP_ID: '14320'
506506
GITHUB_CLIENT_ID: Iv1.45aafbb099e1c1d7

src/config/env.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const envVars: EnvVars = new Proxy<object>({}, {
4646
envCheck(
4747
"APP_ID",
4848
"APP_URL",
49-
"INSTANCE_NAME",
49+
"APP_KEY",
5050
"WEBHOOK_SECRET",
5151
"GITHUB_CLIENT_ID",
5252
"GITHUB_CLIENT_SECRET",
@@ -82,10 +82,10 @@ export interface EnvVars {
8282

8383
APP_ID: string;
8484
APP_URL: string;
85+
APP_KEY: string;
8586
WEBHOOK_SECRET: string;
8687
GITHUB_CLIENT_ID: string;
8788
GITHUB_CLIENT_SECRET: string;
88-
INSTANCE_NAME: string;
8989
DATABASE_URL: string;
9090
STORAGE_SECRET: string;
9191
PRIVATE_KEY_PATH: string;

src/models/jira-client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { getAxiosInstance, JiraClientError } from "../jira/client/axios";
22
import { AxiosInstance } from "axios";
33
import { Installation } from "./installation";
44
import Logger from "bunyan";
5-
import { getAppKey } from "utils/app-properties-utils";
5+
import { envVars } from "config/env";
66

77
// TODO: why are there 2 jira clients?
88
// Probably because this one has types :mindpop:
@@ -40,17 +40,17 @@ export class JiraClient {
4040
}
4141

4242
async appPropertiesCreate(isConfiguredState: boolean) {
43-
return await this.axios.put(`/rest/atlassian-connect/latest/addons/${getAppKey()}/properties/is-configured`, {
43+
return await this.axios.put(`/rest/atlassian-connect/latest/addons/${envVars.APP_KEY}/properties/is-configured`, {
4444
"isConfigured": isConfiguredState
4545
});
4646
}
4747

4848
async appPropertiesGet() {
49-
return await this.axios.get(`/rest/atlassian-connect/latest/addons/${getAppKey()}/properties/is-configured`);
49+
return await this.axios.get(`/rest/atlassian-connect/latest/addons/${envVars.APP_KEY}/properties/is-configured`);
5050
}
5151

5252
async appPropertiesDelete() {
53-
return await this.axios.delete(`/rest/atlassian-connect/latest/addons/${getAppKey()}/properties/is-configured`);
53+
return await this.axios.delete(`/rest/atlassian-connect/latest/addons/${envVars.APP_KEY}/properties/is-configured`);
5454
}
5555

5656
async checkAdminPermissions(accountId: string) {

src/routes/github/create-branch/github-create-branch-get.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AnalyticsEventTypes, AnalyticsScreenEventsEnum } from "interfaces/commo
66
import { Repository, Subscription } from "models/subscription";
77
import Logger from "bunyan";
88
import { getLogger } from "config/logger";
9+
import { envVars } from "config/env";
910
const MAX_REPOS_RETURNED = 20;
1011

1112
export const GithubCreateBranchGet = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
@@ -37,10 +38,9 @@ export const GithubCreateBranchGet = async (req: Request, res: Response, next: N
3738
// TODO move to middleware or shared for create-branch-options-get
3839
// Redirecting when the users are not configured (have no subscriptions)
3940
if (!subscriptions) {
40-
const instance = process.env.INSTANCE_NAME;
4141
res.render("no-configuration.hbs", {
4242
nonce: res.locals.nonce,
43-
configurationUrl: `${jiraHost}/plugins/servlet/ac/com.github.integration.${instance}/github-select-product-page`
43+
configurationUrl: `${jiraHost}/plugins/servlet/ac/${envVars.APP_KEY}/github-select-product-page`
4444
});
4545

4646
sendAnalytics(AnalyticsEventTypes.ScreenEvent, {

src/routes/github/create-branch/github-create-branch-options-get.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { GitHubServerApp } from "~/src/models/github-server-app";
55
import { sendAnalytics } from "utils/analytics-client";
66
import { AnalyticsEventTypes, AnalyticsScreenEventsEnum } from "interfaces/common";
77
import { getLogger } from "config/logger";
8+
import { envVars } from "config/env";
89

910
// TODO - this entire route could be abstracted out into a generic get instance route on github/instance
1011
export const GithubCreateBranchOptionsGet = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
@@ -33,10 +34,9 @@ export const GithubCreateBranchOptionsGet = async (req: Request, res: Response,
3334
const servers = await getGitHubServers(jiraHost);
3435

3536
if (!servers.hasCloudServer && !servers.gheServerInfos.length) {
36-
const instance = process.env.INSTANCE_NAME;
3737
res.render("no-configuration.hbs", {
3838
nonce: res.locals.nonce,
39-
configurationUrl: `${jiraHost}/plugins/servlet/ac/com.github.integration.${instance}/github-select-product-page`
39+
configurationUrl: `${jiraHost}/plugins/servlet/ac/${envVars.APP_KEY}/github-select-product-page`
4040
});
4141

4242
sendAnalytics(AnalyticsEventTypes.ScreenEvent, {

src/routes/github/setup/github-setup-router.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import singleInstallation from "fixtures/jira-configuration/single-installation.
1010

1111
describe("Github Setup", () => {
1212
let frontendApp: Application;
13+
let jiraDomain: string;
1314

1415
beforeEach(async () => {
16+
jiraDomain = jiraHost.replace(/https?:\/\//, "").replace(/\.atlassian\.(net|com)/, "");
1517
frontendApp = getFrontendApp();
1618
});
1719

@@ -110,7 +112,7 @@ describe("Github Setup", () => {
110112
})
111113
)
112114
.send({
113-
jiraDomain: envVars.INSTANCE_NAME
115+
jiraDomain
114116
})
115117
.expect(res => {
116118
expect(res.status).toBe(200);
@@ -138,11 +140,11 @@ describe("Github Setup", () => {
138140
})
139141
)
140142
.send({
141-
jiraDomain: envVars.INSTANCE_NAME
143+
jiraDomain
142144
})
143145
.expect(res => {
144146
expect(res.status).toBe(200);
145-
expect(res.body.redirect).toBe(`${jiraHost}/plugins/servlet/ac/com.github.integration.${envVars.INSTANCE_NAME}/github-post-install-page`);
147+
expect(res.body.redirect).toBe(`${jiraHost}/plugins/servlet/ac/${envVars.APP_KEY}/github-post-install-page`);
146148
});
147149
});
148150

0 commit comments

Comments
 (0)