Skip to content

Commit 52943ef

Browse files
committed
WIP: deploy previews for admin app
1 parent 90037e3 commit 52943ef

4 files changed

Lines changed: 273 additions & 1 deletion

File tree

.github/workflows/previews.yml

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
name: Previews
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, synchronize, closed]
6+
7+
env:
8+
TOKEN: ${{ secrets.DIGITAL_OCEAN_PROXY_TOKEN }}
9+
URL: ${{ vars.DIGITAL_OCEAN_PROXY_URL }}
10+
APP_NAME: pdc-ui-admin-preview-${{ github.event.pull_request.number }}
11+
PROJECT_ID: ${{ secrets.DIGITAL_OCEAN_PROJECT_ID }}
12+
BRANCH: ${{ github.event.pull_request.head.ref }}
13+
14+
jobs:
15+
post-apps:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
pull-requests: write
19+
if: |
20+
github.actor != 'dependabot[bot]' &&
21+
!cancelled() &&
22+
github.event.pull_request.head.repo.full_name == github.repository &&
23+
contains(fromJSON('["opened", "reopened", "synchronize"]'), github.event.action)
24+
needs: delete-app
25+
concurrency:
26+
group: preview-admin-app-${{ github.event.pull_request.number }}
27+
cancel-in-progress: false
28+
steps:
29+
- name: Create or recreate a preview admin app for this PR
30+
id: create-app-get-url
31+
run: |
32+
set -eo pipefail
33+
app_spec=$(cat <<EOF
34+
project_id: ${{ env.PROJECT_ID }}
35+
spec:
36+
alerts:
37+
- rule: DEPLOYMENT_FAILED
38+
- rule: DOMAIN_FAILED
39+
- rule: DEPLOYMENT_LIVE
40+
features:
41+
- buildpack-stack=ubuntu-22
42+
ingress:
43+
rules:
44+
- component:
45+
name: base-fields-admin
46+
match:
47+
path:
48+
prefix: /
49+
name: ${{ env.APP_NAME }}
50+
region: nyc
51+
static_sites:
52+
- catchall_document: index.html
53+
environment_slug: node-js
54+
envs:
55+
- key: VITE_API_URL
56+
scope: BUILD_TIME
57+
value: https://api.sandbox.philanthropydatacommons.org
58+
- key: VITE_KEYCLOAK_AUTHORITY
59+
scope: BUILD_TIME
60+
value: https://auth.philanthropydatacommons.org/
61+
- key: VITE_KEYCLOAK_REALM
62+
scope: BUILD_TIME
63+
value: pdc
64+
- key: VITE_KEYCLOAK_CLIENT_ID
65+
scope: BUILD_TIME
66+
value: pdc-base-fields-admin-previews
67+
- key: NODE_ENV
68+
scope: BUILD_TIME
69+
value: production
70+
github:
71+
branch: ${{ env.BRANCH }}
72+
deploy_on_push: false
73+
repo: PhilanthropyDataCommons/front-end
74+
name: base-fields-admin
75+
output_dir: apps/admin-interface/dist
76+
source_dir: /
77+
EOF
78+
)
79+
# Find existing ID if it exists
80+
app_id=$(curl --fail-with-body -X GET \
81+
-H "Accept: application/json" \
82+
-H "Authorization: Bearer ${TOKEN}" \
83+
--url "${URL}/v2/apps" |
84+
jq -r -c '.apps[] | select (.spec.name == "${{ env.APP_NAME }}") | .id')
85+
http_method=""
86+
http_url=""
87+
if [ "${app_id}" == "" ] || [ "${app_id}" == "null" ]; then
88+
# New app needed, POST a new spec. This is for PR open and reopen events.
89+
echo "New app needed"
90+
http_method=POST
91+
http_url="${URL}/v2/apps"
92+
else
93+
# Existing app, PUT a new spec. This should be a rare case, where the
94+
# DELETE was not correctly called on resynchronize before this.
95+
echo "Existing app ${app_id} found."
96+
http_method=PUT
97+
http_url="${URL}/v2/apps/${app_id}"
98+
# The PUT app spec forbids the `project_id` element so filter it out.
99+
app_spec=$(echo "${app_spec}" | grep -v "project_id:")
100+
fi
101+
102+
if ! app_response=$(curl --fail-with-body -X "${http_method}" \
103+
-H "Accept: application/json" \
104+
-H "Content-Type: application/yaml" \
105+
-H "Authorization: Bearer ${TOKEN}" \
106+
--url "${http_url}" \
107+
-d "${app_spec}")
108+
then
109+
echo "App could not be created. Response:"
110+
echo "${app_response}"
111+
exit 150
112+
fi
113+
114+
echo "App creation response body:"
115+
echo "${app_response}"
116+
117+
app_id=$(echo "${app_response}" | jq -r -c '.app.id')
118+
pending_deployment_id=$(echo "${app_response}" | jq -r -c '.app.pending_deployment.id')
119+
live_url=$(echo "${app_response}" | jq -r -c '.app.live_url')
120+
121+
if [ "${app_id}" == "" ] || [ "${app_id}" == "null" ]; then
122+
echo "App ID could not be found"
123+
exit 151
124+
fi
125+
126+
echo "New app named ${{ env.APP_NAME }} created successfully."
127+
echo "app_id: ${app_id}"
128+
echo "live_url: ${live_url}"
129+
echo "pending_deployment_id: ${pending_deployment_id}"
130+
131+
echo "Getting the deployment momentarily..."
132+
133+
for i in {1..48}; do
134+
sleep 10
135+
echo "Poll attempt ${i}"
136+
if ! deployment_response=$(curl --fail-with-body -X GET \
137+
-H "Accept: application/json" \
138+
-H "Authorization: Bearer ${TOKEN}" \
139+
--url "${URL}/v2/apps/${app_id}/deployments/${pending_deployment_id}")
140+
then
141+
echo "Failed to get a deployment response from DO."
142+
exit 152
143+
fi
144+
phase=$(echo "${deployment_response}" | jq -r -c '.deployment.phase')
145+
echo "Deployment ${phase}"
146+
if [ "${phase}" == "ACTIVE" ]; then
147+
# You might expect to get the live URL from response, no.
148+
break
149+
elif [ "${phase}" == "FAILED" ]; then
150+
exit 153
151+
fi
152+
done
153+
154+
if [ "${phase}" != "ACTIVE" ]; then
155+
echo "Deployment did not succeed in a reasonable amount of time."
156+
echo "Attempting to get DEPLOY logs, if possible. The task fails regardless."
157+
if logs_list_response=$(curl --fail-with-body -X GET \
158+
-H "Accept: application/json" \
159+
-H "Authorization: Bearer ${TOKEN}" \
160+
--url "${URL}/v2/apps/${app_id}/deployments/${pending_deployment_id}/components/service/logs?type=DEPLOY")
161+
then
162+
# Docs are ambiguous, experimentation shows `historic_urls` to be a key.
163+
# Sometimes `url` and `live_url` are here, matching.
164+
# This is best effort, so far with little success.
165+
echo "What URLs are available? Response:"
166+
echo "${logs_list_response}"
167+
if logs_url=$(echo "${logs_list_response}" | jq -r -c '.url')
168+
then
169+
if logs_response=$(curl --fail-with-body -X GET \
170+
--url "${logs_url}")
171+
then
172+
echo "Deploy log file found:"
173+
else
174+
echo "Failed to get a log file. Response:"
175+
fi
176+
echo "${logs_response}"
177+
fi
178+
else
179+
echo "Failed to get the list of DEPLOY logs. Response:"
180+
echo "${logs_list_response}"
181+
fi
182+
echo "Attempting to cancel the deployment. The task fails regardless."
183+
curl --fail-with-body -X POST \
184+
-H "Accept: application/json" \
185+
-H "Authorization: Bearer ${TOKEN}" \
186+
--url "${URL}/v2/apps/${app_id}/deployments/${pending_deployment_id}/cancel"
187+
exit 154
188+
fi
189+
190+
# Now that the app is live, we should be able to get the URL.
191+
live_url=$(curl --fail-with-body -X GET \
192+
-H "Accept: application/json" \
193+
-H "Authorization: Bearer ${TOKEN}" \
194+
--url "${URL}/v2/apps/${app_id}" |
195+
jq -r -c '.app.live_url')
196+
if [ "${live_url}" != "" ] && [ "${live_url}" != "null" ]; then
197+
# Save live_url for the next step.
198+
echo "live_url=${live_url}" >> "${GITHUB_OUTPUT}"
199+
else
200+
echo "The deployment of a deploy preview succeeded but couldn't find the URL."
201+
exit 155
202+
fi
203+
- name: Add deploy preview URL to the PR comments
204+
uses: thollander/actions-comment-pull-request@v3
205+
with:
206+
message: |
207+
🔎 A preview deployment of this pull request is available at ${{ steps.create-app-get-url.outputs.live_url }} assuming this PR is still open. 🔍
208+
comment-tag: deploy-preview-url
209+
210+
delete-app:
211+
runs-on: ubuntu-latest
212+
if: |
213+
github.actor != 'dependabot[bot]' &&
214+
github.event.pull_request.head.repo.full_name == github.repository &&
215+
contains(fromJSON('["closed", "synchronize"]'), github.event.action)
216+
concurrency:
217+
group: preview-admin-app-${{ github.event.pull_request.number }}
218+
cancel-in-progress: false
219+
steps:
220+
- name: Delete a preview app for this PR if it exists
221+
run: |
222+
set -eo pipefail
223+
# Get the list of applications (there is no filter by name call)
224+
if ! app_response=$(curl --fail-with-body -X GET \
225+
-H "Accept: application/json" \
226+
-H "Authorization: Bearer ${TOKEN}" \
227+
--url "${URL}/v2/apps")
228+
then
229+
echo "App list could not be found. Response:"
230+
echo "${app_response}"
231+
# Failure to GET a list of apps should fail the task.
232+
exit 153
233+
fi
234+
app_id=$(echo "${app_response}" |
235+
jq -r -c '.apps[] | select (.spec.name == "${{ env.APP_NAME }}") | .id')
236+
if [ "${app_id}" != "" ] && [ "${app_id}" != "null" ]; then
237+
# Found the app, delete it. Failure on DELETE should fail the task.
238+
curl --fail-with-body -X DELETE \
239+
-H "Accept: application/json" \
240+
-H "Authorization: Bearer ${TOKEN}" \
241+
--url "${URL}/v2/apps/${app_id}"
242+
else
243+
# Failure to find the app in a successfully-gotten list should not
244+
# fail the task. And we cannot print the list because secrets show.
245+
echo "WARNING: ${{ env.APP_NAME }} was not found in the list."
246+
fi

apps/admin-interface/index.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,25 @@
1313
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
1414
<link rel="manifest" href="/manifest.json" />
1515
<title>Philanthropy Data Commons</title>
16+
<script>
17+
// 🔍 Runtime base path detection
18+
const scripts = document.querySelectorAll('script[src]');
19+
let basePath = '/';
20+
for (const script of scripts) {
21+
const src = script.getAttribute('src');
22+
// Vite outputs a main bundle like "assets/index-abc123.js"
23+
if (src && src.includes('index.')) {
24+
const url = new URL(src, window.location.href);
25+
// Extract directory path (e.g., "/pdc-ui-admin-preview-1392-udg7s/")
26+
basePath = url.pathname.substring(
27+
0,
28+
url.pathname.lastIndexOf('/') + 1,
29+
);
30+
break;
31+
}
32+
}
33+
window.__APP_BASE__ = basePath;
34+
</script>
1635
</head>
1736
<body>
1837
<noscript>You need to enable JavaScript to run this app.</noscript>

apps/admin-interface/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export default defineConfig({
1515
port: Number.parseInt(process.env.PORT ?? '3000'),
1616
host: process.env.HOST ?? 'localhost',
1717
},
18+
base: '',
1819
});

packages/utilities/src/router.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { createWebHistory, createRouter } from 'vue-router';
22
import type { Router, RouteRecordRaw } from 'vue-router';
33

4+
declare global {
5+
interface Window {
6+
__APP_BASE__: string | undefined;
7+
}
8+
}
9+
410
declare module 'vue-router' {
511
interface RouteMeta {
612
requiresAuth?: boolean;
@@ -9,7 +15,7 @@ declare module 'vue-router' {
915

1016
const createAppRouter = (routes: RouteRecordRaw[]): Router =>
1117
createRouter({
12-
history: createWebHistory(),
18+
history: createWebHistory(window.__APP_BASE__ ?? '/'),
1319
routes,
1420
});
1521

0 commit comments

Comments
 (0)