Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions .github/workflows/previews.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
name: Previews

on:
pull_request:
types: [opened, reopened, synchronize, closed]

env:
TOKEN: ${{ secrets.DIGITAL_OCEAN_PROXY_TOKEN }}
URL: ${{ vars.DIGITAL_OCEAN_PROXY_URL }}
APP_NAME: pdc-ui-admin-preview-${{ github.event.pull_request.number }}
PROJECT_ID: ${{ secrets.DIGITAL_OCEAN_PROJECT_ID }}
BRANCH: ${{ github.event.pull_request.head.ref }}

jobs:
post-apps:
runs-on: ubuntu-latest
permissions:
pull-requests: write
if: |
github.actor != 'dependabot[bot]' &&
!cancelled() &&
github.event.pull_request.head.repo.full_name == github.repository &&
contains(fromJSON('["opened", "reopened", "synchronize"]'), github.event.action)
needs: delete-app
concurrency:
group: preview-admin-app-${{ github.event.pull_request.number }}
cancel-in-progress: false
steps:
- name: Create or recreate a preview admin app for this PR
id: create-app-get-url
run: |
set -eo pipefail
app_spec=$(cat <<EOF
project_id: ${{ env.PROJECT_ID }}
spec:
alerts:
- rule: DEPLOYMENT_FAILED
- rule: DOMAIN_FAILED
- rule: DEPLOYMENT_LIVE
features:
- buildpack-stack=ubuntu-22
ingress:
rules:
- component:
name: base-fields-admin
match:
path:
prefix: /
name: ${{ env.APP_NAME }}
region: nyc
static_sites:
- catchall_document: index.html
environment_slug: node-js
envs:
- key: VITE_API_URL
scope: BUILD_TIME
value: https://api.sandbox.philanthropydatacommons.org
- key: VITE_KEYCLOAK_AUTHORITY
scope: BUILD_TIME
value: https://auth.philanthropydatacommons.org/
- key: VITE_KEYCLOAK_REALM
scope: BUILD_TIME
value: pdc
- key: VITE_KEYCLOAK_CLIENT_ID
scope: BUILD_TIME
value: pdc-base-fields-admin-previews
- key: NODE_ENV
scope: BUILD_TIME
value: production
github:
branch: ${{ env.BRANCH }}
deploy_on_push: false
repo: PhilanthropyDataCommons/front-end
name: base-fields-admin
output_dir: apps/admin-interface/dist
source_dir: /
EOF
)
# Find existing ID if it exists
app_id=$(curl --fail-with-body -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${URL}/v2/apps" |
jq -r -c '.apps[] | select (.spec.name == "${{ env.APP_NAME }}") | .id')
http_method=""
http_url=""
if [ "${app_id}" == "" ] || [ "${app_id}" == "null" ]; then
# New app needed, POST a new spec. This is for PR open and reopen events.
echo "New app needed"
http_method=POST
http_url="${URL}/v2/apps"
else
# Existing app, PUT a new spec. This should be a rare case, where the
# DELETE was not correctly called on resynchronize before this.
echo "Existing app ${app_id} found."
http_method=PUT
http_url="${URL}/v2/apps/${app_id}"
# The PUT app spec forbids the `project_id` element so filter it out.
app_spec=$(echo "${app_spec}" | grep -v "project_id:")
fi

if ! app_response=$(curl --fail-with-body -X "${http_method}" \
-H "Accept: application/json" \
-H "Content-Type: application/yaml" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${http_url}" \
-d "${app_spec}")
then
echo "App could not be created. Response:"
echo "${app_response}"
exit 150
fi

echo "App creation response body:"
echo "${app_response}"

app_id=$(echo "${app_response}" | jq -r -c '.app.id')
pending_deployment_id=$(echo "${app_response}" | jq -r -c '.app.pending_deployment.id')
live_url=$(echo "${app_response}" | jq -r -c '.app.live_url')

if [ "${app_id}" == "" ] || [ "${app_id}" == "null" ]; then
echo "App ID could not be found"
exit 151
fi

echo "New app named ${{ env.APP_NAME }} created successfully."
echo "app_id: ${app_id}"
echo "live_url: ${live_url}"
echo "pending_deployment_id: ${pending_deployment_id}"

echo "Getting the deployment momentarily..."

for i in {1..48}; do
sleep 10
echo "Poll attempt ${i}"
if ! deployment_response=$(curl --fail-with-body -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${URL}/v2/apps/${app_id}/deployments/${pending_deployment_id}")
then
echo "Failed to get a deployment response from DO."
exit 152
fi
phase=$(echo "${deployment_response}" | jq -r -c '.deployment.phase')
echo "Deployment ${phase}"
if [ "${phase}" == "ACTIVE" ]; then
# You might expect to get the live URL from response, no.
break
elif [ "${phase}" == "FAILED" ]; then
exit 153
fi
done

if [ "${phase}" != "ACTIVE" ]; then
echo "Deployment did not succeed in a reasonable amount of time."
echo "Attempting to get DEPLOY logs, if possible. The task fails regardless."
if logs_list_response=$(curl --fail-with-body -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${URL}/v2/apps/${app_id}/deployments/${pending_deployment_id}/components/service/logs?type=DEPLOY")
then
# Docs are ambiguous, experimentation shows `historic_urls` to be a key.
# Sometimes `url` and `live_url` are here, matching.
# This is best effort, so far with little success.
echo "What URLs are available? Response:"
echo "${logs_list_response}"
if logs_url=$(echo "${logs_list_response}" | jq -r -c '.url')
then
if logs_response=$(curl --fail-with-body -X GET \
--url "${logs_url}")
then
echo "Deploy log file found:"
else
echo "Failed to get a log file. Response:"
fi
echo "${logs_response}"
fi
else
echo "Failed to get the list of DEPLOY logs. Response:"
echo "${logs_list_response}"
fi
echo "Attempting to cancel the deployment. The task fails regardless."
curl --fail-with-body -X POST \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${URL}/v2/apps/${app_id}/deployments/${pending_deployment_id}/cancel"
exit 154
fi

# Now that the app is live, we should be able to get the URL.
live_url=$(curl --fail-with-body -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${URL}/v2/apps/${app_id}" |
jq -r -c '.app.live_url')
if [ "${live_url}" != "" ] && [ "${live_url}" != "null" ]; then
# Save live_url and subdomain for the next step.
echo "live_url=${live_url}" >> "${GITHUB_OUTPUT}"
subdomain=$(echo "${live_url}" |
sed 's/^https:\/\/\([0-9a-z-]\+\)\.ondigitalocean.app/\1/')
echo "subdomain=${subdomain}" >> "${GITHUB_OUTPUT}"
else
echo "The deployment of a deploy preview succeeded but couldn't find the URL."
exit 155
fi
- name: Add deploy preview URL to the PR comments
uses: thollander/actions-comment-pull-request@v3
with:
message: |
🔎 A preview deployment of this pull request is available at https://utilities.philanthropydatacommons.org/${{ steps.create-app-get-url.outputs.subdomain }}/ (and upstream ${{ steps.create-app-get-url.outputs.live_url }}) assuming this PR is still open. 🔍
comment-tag: deploy-preview-url

delete-app:
runs-on: ubuntu-latest
if: |
github.actor != 'dependabot[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository &&
contains(fromJSON('["closed", "synchronize"]'), github.event.action)
concurrency:
group: preview-admin-app-${{ github.event.pull_request.number }}
cancel-in-progress: false
steps:
- name: Delete a preview app for this PR if it exists
run: |
set -eo pipefail
# Get the list of applications (there is no filter by name call)
if ! app_response=$(curl --fail-with-body -X GET \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${URL}/v2/apps")
then
echo "App list could not be found. Response:"
echo "${app_response}"
# Failure to GET a list of apps should fail the task.
exit 153
fi
app_id=$(echo "${app_response}" |
jq -r -c '.apps[] | select (.spec.name == "${{ env.APP_NAME }}") | .id')
if [ "${app_id}" != "" ] && [ "${app_id}" != "null" ]; then
# Found the app, delete it. Failure on DELETE should fail the task.
curl --fail-with-body -X DELETE \
-H "Accept: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
--url "${URL}/v2/apps/${app_id}"
else
# Failure to find the app in a successfully-gotten list should not
# fail the task. And we cannot print the list because secrets show.
echo "WARNING: ${{ env.APP_NAME }} was not found in the list."
fi
4 changes: 4 additions & 0 deletions apps/admin-interface/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<title>Philanthropy Data Commons</title>
<script>
// 🔍 Runtime base path detection
window.__APP_BASE__ = window.location.pathname;
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
1 change: 1 addition & 0 deletions apps/admin-interface/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export default defineConfig({
port: Number.parseInt(process.env.PORT ?? '3000'),
host: process.env.HOST ?? 'localhost',
},
base: '',
});
8 changes: 7 additions & 1 deletion packages/utilities/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { createWebHistory, createRouter } from 'vue-router';
import type { Router, RouteRecordRaw } from 'vue-router';

declare global {
interface Window {
__APP_BASE__: string | undefined;
}
}

declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean;
Expand All @@ -9,7 +15,7 @@ declare module 'vue-router' {

const createAppRouter = (routes: RouteRecordRaw[]): Router =>
createRouter({
history: createWebHistory(),
history: createWebHistory(window.__APP_BASE__ ?? '/'),
routes,
});

Expand Down
Loading