diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 9fa6c2a33..13dd10802 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,9 +13,10 @@ "version": "18.15.0" }, "ghcr.io/devcontainers-contrib/features/postgres-asdf:1": {}, - "ghcr.io/devcontainers/features/docker-in-docker:1": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { "version": "20.10.23", "moby": "false", + "azureDnsAutoDetection": "false", "dockerDashComposeVersion": "v2" }, "ghcr.io/devcontainers/features/azure-cli:1": { @@ -24,10 +25,15 @@ }, "ghcr.io/devcontainers/features/github-cli:1": { "version": "latest" + }, + "ghcr.io/devcontainers/features/sshd:1": { + "version": "latest" } }, - // add labels to ports + // add labels to ports that are being used + // this will make it easier to identify what is running on each port + // it's also used by nightly builds to check all services are running as expected "portsAttributes": { "4280": { "label": "Portal App", @@ -48,10 +54,15 @@ "4300": { "label": "Web PubSub API", "onAutoForward": "notify" + }, + "4200": { + "label": "Angular", + "onAutoForward": "notify" } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. + // Note: do not forward 4200 as it's being proxied by 4280 "forwardPorts": [4280, 4242, 3000, 1337], // Use 'postCreateCommand' to run commands after the container is created. @@ -90,7 +101,4 @@ "memory": "8gb", "storage": "32gb" } - - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" } diff --git a/.devcontainer/post-create-command.sh b/.devcontainer/post-create-command.sh index c1163967c..c1d83199a 100755 --- a/.devcontainer/post-create-command.sh +++ b/.devcontainer/post-create-command.sh @@ -11,3 +11,8 @@ npm i -g azure-functions-core-tools@4 --unsafe-perm true # Install monorepo dependencies npm install + +# run npm start if CODESPACE_NAME starts with "CODESPACE_NAME=nightly-build" +if [[ $CODESPACE_NAME == "ci-nightly-build"* ]]; then + npm start > npm _start.log 2>&1 & +fi diff --git a/.github/workflows/codespaces-ci.sh b/.github/workflows/codespaces-ci.sh new file mode 100755 index 000000000..d1f79e66f --- /dev/null +++ b/.github/workflows/codespaces-ci.sh @@ -0,0 +1,166 @@ +#!/bin/bash +set -e + +# IMPORTANT: a valid X_GITHUB_TOKEN is required to run this script +# Token must have the following permissions: 'admin:org', 'codespace', 'repo' + +GITHUB_REPOSITORY="Azure-Samples/contoso-real-estate" +BRANCH="codespaces-ci" +CODESPACE_NAME="ci-nightly-build-$(date +%s)" +CODESPACE_ID="" +RED='\033[0m' # this is not red, it's just to reset the color +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +# login to GitHub CLI +function gh_login() { + echo "Loging in with GitHub CLI as admin..." + echo $X_GITHUB_TOKEN | gh auth login --with-token + + echo "Checking auth status..." + gh auth status +} + +# create a codespace +function gh_create_codespace() { + echo "Creating a codespace $CODESPACE_NAME for $GITHUB_REPOSITORY on branch $BRANCH (w/ ssh)..." + gh codespace create \ + --repo $GITHUB_REPOSITORY \ + --branch $BRANCH \ + --display-name $CODESPACE_NAME \ + --retention-period "15min" \ + --idle-timeout "5min" \ + --machine "largePremiumLinux" \ + --status \ + --default-permissions +} +function api_create_codespace() { + echo "Creating a codespace $CODESPACE_NAME for $GITHUB_REPOSITORY on branch $BRANCH (w/ api)..." + response=$(gh api \ + /repos/$GITHUB_REPOSITORY/codespaces \ + -X POST \ + -H 'Accept: application/vnd.github+json' \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + -f owner="$GITHUB_REPOSITORY" \ + -f repo="$GITHUB_REPOSITORY" \ + -f ref="$BRANCH" \ + -f display_name="$CODESPACE_NAME" \ + -f retentionPeriod='15min' \ + -f idleTimeout='5min' \ + -f machineType=l'argePremiumLinux' \ + -f status='true' \ + -f defaultPermissions='true') + CODESPACE_ID=$(echo "$response" | jq -r '.name') + CODESPACE_URL=$(echo "$response" | jq -r '.web_url') + CODESPACE_API=$(echo "$response" | jq -r '.url') + echo "Codespace created and started:" + echo " - ID: $CODESPACE_ID" + echo " - Web: $CODESPACE_URL" + echo " - API: $CODESPACE_API" +} + +# fetch the codespace ID +function gh_fetch_codespace_id() { + CODESPACE_ID=$(gh codespace list -R $GITHUB_REPOSITORY --jq ".[] | select(.displayName == \"$CODESPACE_NAME\")" --json displayName,name | jq -r '.name') + echo "Codespace created and started: $CODESPACE_ID" +} + +# connect to the codespace and start the services +function gh_codespace_start_services() { + echo "Running all services (over SSH)..." + # (gh codespace ssh -c $CODESPACE_ID "npm start --prefix /workspaces/contoso-real-estate") & + (gh codespace ssh -c $CODESPACE_ID "env") +} + +# check all services are running +function gh_codespace_check_services_status() { + nb_services_down=0 + max_retries=5 + while [ $nb_services_down > 0 ]; do + + echo -ne "Fetching registered services..." + services=$(gh codespace ports -c $CODESPACE_ID --json label,browseUrl | jq -r '.[] | select(.label != "") | .browseUrl') + nb_services=$(echo "$services" | awk 'END { print NR }') + echo " Found $nb_services" + + if [ -z "$services" ]; then + echo "No services found, exiting..." + break + fi + + nb_services_down=0 + echo "---------------------------------------------------------------------------------------------------------" + for service in $services; do + echo -ne "Inspecting: $service ... " + status=$(curl -H "X-Github-Token: $X_GITHUB_TOKEN" -s -o /dev/null -w "%{http_code}" $service) + + if [ $status == 200 ] || [ $status == 404 ]; then + echo -e "${GREEN}$status OK${NC}" + else + echo -e "${RED}$status ERROR${NC}" + ((nb_services_down++)) + fi + done + + if [ $nb_services_down == 0 ]; then + echo "All services are running!" + break + fi + + if [ $max_retries == 0 ]; then + echo "Max retries reached, exiting..." + break + fi + + echo "---------------------------------------------------------------------------------------------------------" + echo "Found $nb_services_down services down..." + echo "Wait 10s before retrying... (retries left: $max_retries)" + sleep 10 + ((max_retries--)) + done +} + +# Wait for all services to start +function wait_for_services() { + echo "Waiting 10 minutes for all dependencies to be installed and starting all services\n" + for i in {1..600}; do + echo -ne "." + sleep 1 + if ! ((i % 60)); then + echo "" + fi + done + echo "" +} + +# stop and delete the codespace +function gh_codespace_stop_and_delete() { + echo "Stopping and deleting codespace $CODESPACE_ID..." + gh codespace stop -c $CODESPACE_ID + gh codespace delete -c $CODESPACE_ID -f +} + +function print_report_and_exit() { + if [ $nb_services == 0 ]; then + echo -e "${RED}ERROR: No services found. Inspect the logs above for more details.${NC}" + exit 1 + elif [ $nb_services_down > 0 ]; then + echo -e "${RED}ERROR: $nb_services_down services are still down. Inspect the logs above for more details.${NC}" + exit 1 + else + echo -e "${GREEN}OK: All services are running, exiting with success.${NC}" + exit 0 + fi +} + +############################################ +gh_login; +# gh_create_codespace; +api_create_codespace; +# gh_fetch_codespace_id; +# wait_for_services; +# gh_codespace_start_services; +wait_for_services; +gh_codespace_check_services_status; +gh_codespace_stop_and_delete; +print_report_and_exit; diff --git a/.github/workflows/codespaces-ci.yml b/.github/workflows/codespaces-ci.yml new file mode 100644 index 000000000..f635f9a44 --- /dev/null +++ b/.github/workflows/codespaces-ci.yml @@ -0,0 +1,22 @@ +name: Nightly Codespace CI + +on: + push: + branches: + - main + - codespaces-ci + schedule: + # Runs every day at 4:00 AM pacific (UTC-7) + - cron: '0 12 * * *' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Deploy to Codespace + env: + X_GITHUB_TOKEN: ${{ secrets.X_GITHUB_TOKEN }} + run: ./.github/workflows/codespaces-ci.sh diff --git a/package-lock.json b/package-lock.json index 8d16879f8..dc099b8db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19583,6 +19583,28 @@ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -20090,6 +20112,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/formidable": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", @@ -27504,6 +27537,24 @@ "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -39238,6 +39289,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -40645,6 +40704,7 @@ "dependencies": { "@fastify/autoload": "^5.8.0", "@fastify/sensible": "^5.5.0", + "data-uri-to-buffer": "^4.0.1", "fastify": "^4.24.3", "fastify-cli": "^5.9.0", "fastify-plugin": "^4.5.1", @@ -40662,6 +40722,14 @@ "typescript": "^5.3.2" } }, + "packages/stripe/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "packages/stripe/node_modules/node-fetch": { "version": "3.3.2", "license": "MIT", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 733786ea6..b2e54e98b 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -7,6 +7,18 @@ import { getListingById, getListings } from "./functions/listings"; import { openApi } from "./functions/openapi"; import { postCheckout } from "./functions/checkout"; +// This route is used by nightly builds as a health check +app.get("get-root", { + route: "", + authLevel: "anonymous", + handler: async () => { + return { + status: 200, + body: "Welcome to Contoso Real Estate API", + }; + } +}); + //#region User Function app.get("get-users", { route: "users", diff --git a/packages/stripe/package.json b/packages/stripe/package.json index b019a02af..039c198c9 100644 --- a/packages/stripe/package.json +++ b/packages/stripe/package.json @@ -35,6 +35,7 @@ "concurrently": "^7.0.0", "tap": "^16.1.0", "ts-node": "^10.4.0", - "typescript": "^5.3.2" + "typescript": "^5.3.2", + "data-uri-to-buffer": "^4.0.1" } }