diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8ffc0c2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,54 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +/integration_test +/.github +/deployment +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# cloud sql proxy +cloud-sql-proxy + +#npm +.npmrc + +# Sentry Config File +.env.sentry-build-plugin + +# eslint config +eslint.config.mjs +Dockerfile + +allure-results \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..068f27d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,84 @@ +name: Build and deploy CCVA +run-name: Deploy by @${{ github.actor }} + +on: + push: + branches: + - test +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + steps: + - name: Set up SSH and deploy + uses: appleboy/ssh-action@v1.0.3 + env: + COMMIT_SHA: ${{github.sha}} + with: + host: ${{ secrets.BASTION_SERVER_HOST }} + username: githubci + key: ${{ secrets.BASTION_SERVER_SSH_KEY }} + port: ${{ secrets.BASTION_SERVER_SSH_PORT }} + envs: COMMIT_SHA + command_timeout: 40m + + script: | + set -euo pipefail + REPO_URL="git@github.com:${{ github.repository }}" + DEST_DIR="$HOME/Documents/${{ github.repository }}" + + # Create parent directory if it does not exist + mkdir -p "$(dirname "$DEST_DIR")" + + # Check if $DEST_DIR exists + if [ -d "$DEST_DIR" ]; then + cd $DEST_DIR || exit + if [ -d ".git" ]; then + echo "Updating repository..." + git remote set-url origin "$REPO_URL" + git fetch --all + git checkout --force "$COMMIT_SHA" + else + echo "Not a git repository. Re-cloning..." + rm -rf "$DEST_DIR" + git clone "$REPO_URL" "$DEST_DIR" + cd "$DEST_DIR" || exit + git checkout --force "$COMMIT_SHA" + fi + else + echo "Directory does not exist. Cloning repository..." + git clone "$REPO_URL" "$DEST_DIR" + cd "$DEST_DIR" || exit + git checkout --force "$COMMIT_SHA" + fi + + cd $DEST_DIR/deployment + # REGISTRY_BASE=docker.io/ccva + # STACK_NAME=ccva + # DEPLOY_BASE_DOMAIN=ccv.cardanoapi.io + # IMAGE_VERSION_TAG=v1 + # METADATA_API_DATABASE_URL=postgres://drep_id:xxxxxxxxxxx@postgres/drep_metadata + # CARDANO_NETWORK=preprod + # OPERATOR_SIGNING_KEY=xxxxxxxxxxx + # GITHUB_TOKEN=XXXXX + + # Write environment variables to .env file + { + echo 'IMAGE_VERSION_TAG=${{ github.sha }}' + echo 'GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}' + echo 'REGISTRY_BASE=registry.sireto.io/cardanoapi' + echo 'STACK_NAME=ccva' + echo 'BASE_DOMAIN=ccv.cardanoapi.io' + echo 'DATABASE_PASSWORD=postgres' + echo 'CARDANO_NETWORK=preprod' + } > .env + + + + # push images + export DOCKER_HOST=172.31.0.5:2376 + ./scripts/build-images.sh push + + # make deployment. NOTE that this won't fail if the service fails. this might be mis-leading. + export DOCKER_HOST=172.31.0.5:2376 + ./scripts/deploy.sh stack ccva --with-registry-auth \ No newline at end of file diff --git a/.github/workflows/test_integration_playwright.yml b/.github/workflows/test_integration_playwright.yml index d1204ab..f51e58a 100644 --- a/.github/workflows/test_integration_playwright.yml +++ b/.github/workflows/test_integration_playwright.yml @@ -1,9 +1,12 @@ name: Integration Test [Playwright] on: - push: - branches: - - test + workflow_run: + workflows: + - Build and deploy CCVA + branches: test + types: completed + workflow_dispatch: jobs: integration-tests: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8240891 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +ARG NPM_AUTH_TOKEN + +FROM node:20-alpine AS builder +WORKDIR /app +RUN npm install -g prisma +COPY ./package.json ./package-lock.json ./ +COPY ./prisma ./prisma +RUN npm config set //registry.npmjs.org/:_authToken ${NPM_AUTH_TOKEN} --location=global +RUN npm install --legacy-peer-deps +COPY --chown=node:node . . + + +ENV NEXT_PUBLIC_NODE_ENV=production + +RUN npm run build +RUN rm -rf ./.next/cache/* && mkdir moveTarget && mv public ./moveTarget + + +# Runtime image +FROM node:20-alpine +WORKDIR /app +USER node + +ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 +EXPOSE 3000 + + +COPY --from=builder --chown=node:node /app/package.json ./ +COPY --from=builder --chown=node:node /app/node_modules ./node_modules +COPY --from=builder --chown=node:node /app/moveTarget/ ./ +COPY --from=builder --chown=node:node /app/.next ./.next +VOLUME /home/node/.next/cache +CMD ["npm","run","start"] + diff --git a/deployment/.gitignore b/deployment/.gitignore new file mode 100644 index 0000000..c0da577 --- /dev/null +++ b/deployment/.gitignore @@ -0,0 +1,5 @@ +secrets/ +configs/ +.env +/*-rendered.yml + diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 0000000..4f20b98 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,9 @@ +# CCVA Deployment Scripts + +Compose files and scripts to deploy and test environment of ccva. + +## Usage + +```sh +./scripts/build-and-deploy.sh +``` diff --git a/deployment/deployment/scripts/build-images.sh b/deployment/deployment/scripts/build-images.sh new file mode 100644 index 0000000..35e04db --- /dev/null +++ b/deployment/deployment/scripts/build-images.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e + +if [ -z "$IMAGE_VERSION_TAG" ]; then + IMAGE_VERSION_TAG="$(git rev-parse HEAD)" +fi +export IMAGE_VERSION_TAG + +# Define a function to log and execute Docker commands +docker_() { + local cmd="$*" + echo docker "$cmd" + docker $cmd +} + +echo "$1" -eq 'push' + +if [[ "$1" == 'push' ]] +then + set -eo pipefail + . ./scripts/docker-stack-deploy.sh + load_env + docker_ compose -f ./docker-compose-ccva.yml build + + echo "Pushing the images..." + + docker push $REGISTRY_BASE/webapp:${IMAGE_VERSION_TAG} +else + docker_ compose -f ./docker-compose-ccva.yml build +fi diff --git a/deployment/docker-compose-ccva.yml b/deployment/docker-compose-ccva.yml new file mode 100644 index 0000000..cb18277 --- /dev/null +++ b/deployment/docker-compose-ccva.yml @@ -0,0 +1,33 @@ +version: '3.9' + +services: + webapp: + image: ${REGISTRY_BASE}/webapp:${IMAGE_VERSION_TAG} + build: + context: ../ + networks: + - default + - frontend + environment: + - VIRTUAL_HOST=https://${DEPLOY_BASE_DOMAIN} + - DATABASE_URL=postgresql://postgres:postgres@db:5432/ccva + - NEXTAUTH_URL=https://${DEPLOY_BASE_DOMAIN} + + db: + image: postgres:15 + container_name: db + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=ccva + volumes: + - db:/var/lib/postgresql/data + networks: + - default + +networks: + frontend: + name: frontend + external: true +volumes: + db: \ No newline at end of file diff --git a/deployment/scripts/build-and-deploy.sh b/deployment/scripts/build-and-deploy.sh new file mode 100755 index 0000000..29fe279 --- /dev/null +++ b/deployment/scripts/build-and-deploy.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +if [ -z "$IMAGE_VERSION_TAG" ]; then + IMAGE_VERSION_TAG="$(git rev-parse HEAD)" +fi +export IMAGE_VERSION_TAG +set -e + +. ./scripts/docker-stack-deploy.sh +load_env +check_env +USE_REGISTRY="$2" +# Build images +if [[ "$USE_REGISTRY" == "use-registry" ]] +then + echo '> ./scripts/build-images.sh push' + ./scripts/build-images.sh push +else + echo '+ ./scripts/build-images.sh' + + ./scripts/build-images.sh +fi + +function update-service(){ + if [[ "$USE_REGISTRY" == "use-registry" ]] + then + echo '> docker' service update --with-registry-auth --image "$2" "$1" + docker service update --image "$2" "$1" + else + echo '> docker' service update --with-registry-auth --image "$2" "$1" + docker service update --with-registry-auth --image "$2" "$1" + fi +} + +if [[ "$1" == "update-images" ]] +then + update-service ccva_backend "$REGISTRY_BASE"/ccva-backend:${IMAGE_VERSION_TAG} + # test metadata API + +elif [[ $1 == "full" ]] +then + if [[ "$USE_REGISTRY" == "use-registry" ]] + then + ./scripts/deploy.sh stack all --with-registry-auth + else + ./scripts/deploy.sh stack all + fi + +fi diff --git a/deployment/scripts/build-images.sh b/deployment/scripts/build-images.sh new file mode 100755 index 0000000..35e04db --- /dev/null +++ b/deployment/scripts/build-images.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e + +if [ -z "$IMAGE_VERSION_TAG" ]; then + IMAGE_VERSION_TAG="$(git rev-parse HEAD)" +fi +export IMAGE_VERSION_TAG + +# Define a function to log and execute Docker commands +docker_() { + local cmd="$*" + echo docker "$cmd" + docker $cmd +} + +echo "$1" -eq 'push' + +if [[ "$1" == 'push' ]] +then + set -eo pipefail + . ./scripts/docker-stack-deploy.sh + load_env + docker_ compose -f ./docker-compose-ccva.yml build + + echo "Pushing the images..." + + docker push $REGISTRY_BASE/webapp:${IMAGE_VERSION_TAG} +else + docker_ compose -f ./docker-compose-ccva.yml build +fi diff --git a/deployment/scripts/deploy.sh b/deployment/scripts/deploy.sh new file mode 100755 index 0000000..f9a130c --- /dev/null +++ b/deployment/scripts/deploy.sh @@ -0,0 +1,103 @@ +#!/bin/bash +## Load environment variables and deploy to the docker swarm. +## +## Usages: +## ./deploy stack all [--with-registry-auth] +## +set -eo pipefail + +if [ -z "$IMAGE_VERSION_TAG" ]; then + IMAGE_VERSION_TAG="$(git rev-parse HEAD)" +fi +export IMAGE_VERSION_TAG + +. ./scripts/docker-stack-deploy.sh +load_env + +DOCKER_STACKS=("ccva") + +if [ "$1" == "destroy" ] +then + echo "This will remove everything in your stack except volumes, configs and secrets" + echo "Are you Sure? (Y/N)" + read user_input + if ! ( [ "$user_input" = "y" ] || [ "$user_input" = "Y" ]) + then + exit 1 + fi + echo "Proceeding..." # Delete the Docker stack if "destroy" argument is provided + + REVERSE_STACKS=() + for ((i=${#STACKS[@]}-1; i>=0; i--)); do + REVERSE_STACKS+=("${STACKS[i]}") + done + + for CUR_STACK in "${REVERSE_STACKS[@]}"; do + docker stack rm "$CUR_STACK" + sleep 6 # wait 6 seconds for each stack cleanup. + done + + + + # Get the number of nodes in the swarm + NODES=$(docker node ls --format "{{.ID}}" | wc -l) + + # If there is only one node, set the labels + if [ "$NODES" -eq 1 ]; then + NODE_ID=$(docker node ls --format "{{.ID}}") + + docker node update --label-add ccva-test-stack=true \ + "$NODE_ID" + + echo "Labels set on node: $NODE_ID" + else + echo "There are multiple nodes in the docker swarm." + echo "Please set the following labels to correct nodes manually." + echo " - ccva-test-stack " + echo "" + echo " e.g. $ docker node update xxxx --label-add gateway=true" + + exit 1 + fi + +elif [ "$1" == 'stack' ] +then + if !([ "$#" == 2 ] || [ "$#" == 3 ]) + then + echo "Error : $@" + echo "stack requires the stack name". + echo "Usage :" + echo " > $0 stack [stack-name] [--with-registry-auth]". + echo "" + echo " stack-name : One of the following"ß + echo " $DOCKER_STACKS" + else + case "$2" in + all) + + for DEPLOY_STACK in "${DOCKER_STACKS[@]}"; do + docker-stack-deploy "$DEPLOY_STACK" "docker-compose-$DEPLOY_STACK.yml" "$3" + done + + ;; + *) + if [[ ! -f ./"docker-compose-$2.yml" ]] + then + echo "Invalid stack name. $2" + else + docker-stack-deploy $2 "docker-compose-$2.yml" "$3" + fi + ;; + esac + fi +else + echo "Invalid command : $1" + echo + echo " Usage:" + echo " $0 (prepare | destroy | deploy)" + echo '' + echo " Options:" + echo " prepare -> set required labels to docker swarm node." + echo " destroy -> teardown everything except the volumes" + echo " stack [stack_name] -> Deploy the stack." +fi diff --git a/deployment/scripts/docker-stack-deploy.sh b/deployment/scripts/docker-stack-deploy.sh new file mode 100755 index 0000000..675b7a2 --- /dev/null +++ b/deployment/scripts/docker-stack-deploy.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +## Docker swarm doesn't read .env file. +## This script reads env file and variables +## and apply them to compose file and +## then execute `docker stack deploy` + +set -eo pipefail + +function load_env(){ + if [[ -f ./.env ]] + then + set -a + . ./.env + set +a + fi + check_env +} + + +function check_env(){ + + # Path to the .env.example file + EXAMPLE_FILE=".env.example" + + unset_keys=() + + # Read each line of the .env.example file + while IFS= read -r line || [ -n "$line" ]; do + # Skip empty lines + if [ -z "$line" ]; then + continue + fi + + line=$(echo "$line" | sed -e 's/^[[:space:]]*//') + + # Extract the key from each line + key=$(echo "$line" | cut -d'=' -f1) + + if [ -z "${!key}" ]; then + unset_keys+=("$key") + fi + done < "$EXAMPLE_FILE" + + # Print error message for unset keys + if [ ${#unset_keys[@]} -gt 0 ]; then + echo "The following keys are not set in the environment:" + for key in "${unset_keys[@]}"; do + echo "- $key" + done + echo " Exiting due to missing env variables" + exit 2 + fi +} +function docker-stack-deploy(){ + echo "++ docker-stack-deploy" "$@" + ## apply the environment to compose file + ## deploy ccva stack + ## first argument is stack name and 2nd argument is the file name + STACK_NAME=$1 + COMPOSE_FILE=$2 + WITH_REGISTRY_AUTH=$3 + FILENAME=$(basename -- "$COMPOSE_FILE") + EXTENSION="${FILENAME##*.}" + FILENAME_WITHOUT_EXT="${FILENAME%.*}" + RENDERED_FILENAME="${FILENAME_WITHOUT_EXT}-rendered.${EXTENSION}" + ./scripts/envsubst.py < "$COMPOSE_FILE" > "$RENDERED_FILENAME" + if [[ $WITH_REGISTRY_AUTH == "--with-registry-auth" ]] + then + docker stack deploy $WITH_REGISTRY_AUTH -c "$RENDERED_FILENAME" ${STACK_NAME} + else + docker stack deploy -c "$RENDERED_FILENAME" ${STACK_NAME} + fi +} \ No newline at end of file diff --git a/deployment/scripts/envsubst.py b/deployment/scripts/envsubst.py new file mode 100755 index 0000000..908d018 --- /dev/null +++ b/deployment/scripts/envsubst.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +""" +NAME + envsubst.py - substitutes environment variables in bash format strings + +DESCRIPTION + envsubst.py is upgrade of POSIX command `envsubst` + + supported syntax: + normal - ${VARIABLE1} + with default - ${VARIABLE1:-somevalue} +""" + +import os +import re +import sys + + +def envsubst(template_str, env=os.environ): + """Substitute environment variables in the template string, supporting default values.""" + pattern = re.compile(r"\$\{([^}:\s]+)(?::-(.*?))?\}") + + def replace(match): + var = match.group(1) + default_value = match.group(2) if match.group(2) is not None else None + result=env.get(var, default_value) + if result is None: + print(match,"Missing template variable\n"+var,file=sys.stderr) + exit(1) + return result + + return pattern.sub(replace, template_str) + + +def main(): + if len(sys.argv) > 2: + print("Usage: python envsubst.py [template_file]") + sys.exit(1) + + if len(sys.argv) == 2: + template_file = sys.argv[1] + with open(template_file, "r") as file: + template_str = file.read() + else: + template_str = sys.stdin.read() + + result = envsubst(template_str) + + print(result) + + +if __name__ == "__main__": + main() diff --git a/src/lib/connectWallet.tsx b/src/lib/connectWallet.tsx index d125f68..6553b76 100644 --- a/src/lib/connectWallet.tsx +++ b/src/lib/connectWallet.tsx @@ -15,8 +15,9 @@ import toast from 'react-hot-toast'; */ export async function connectWallet(walletName: string): Promise { try { + // @ts-expect-error just use the window.cardano const wallet = await window.cardano[walletName].enable(); // await connectWalletClarity(walletName); - // @ts-expect-error getRewardAddresses is actually a proper function + const stakeAddressHex = (await wallet.getRewardAddresses())[0]; const bytes = Buffer.from(stakeAddressHex, 'hex'); const words = bech32.toWords(bytes);