diff --git a/.github/workflows/aws-context.sh b/.github/workflows/aws-context.sh new file mode 100755 index 0000000..43da76b --- /dev/null +++ b/.github/workflows/aws-context.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -eo pipefail +if [[ "$GITHUB_REF_TYPE" == 'tag' ]]; then + echo "Found git $GITHUB_REF_TYPE \"$GITHUB_REF_NAME,\" attempting a production deployment." + export GIT_TAG="$(git --no-pager tag --points-at HEAD)" + export PACKAGE_VERSION="v$(cat package.json | jq -r '.version')" + if [[ "$PACKAGE_VERSION" != "$GIT_TAG" || "$GIT_TAG" != "$GITHUB_REF_NAME" ]]; then + echo '::error title=Version String Mismatch:: The package.json version string does not match the git tag!' + echo "PACKAGE_VERSION='$PACKAGE_VERSION'" + echo "GITHUB_REF_NAME='$GITHUB_REF_NAME'" + echo "GIT_TAG='$GIT_TAG'" + cat package.json | jq '.' + exit 10 + fi + echo '::notice title=Deploying to Production::This build will attempt to deploy to production. This is the real deal!' + echo '::set-output name=dry-run::false' + echo "::set-output name=role-arn::$STAKING_PROD_IAM_ARN" +else + echo "Found git $GITHUB_REF_TYPE \"$GITHUB_REF_NAME,\" performing a dry-run." + echo '::notice title=Dry Run::This build is performing a dry run. A dry run attemps to verify everything is good to go without actually changing anything.' + echo '::set-output name=dry-run::true' + echo "::set-output name=role-arn::$STAKING_RO_IAM_ARN" +fi +echo "Done. - ${BASH_SOURCE[0]}" diff --git a/.github/workflows/build.sh b/.github/workflows/build.sh new file mode 100755 index 0000000..deebeb4 --- /dev/null +++ b/.github/workflows/build.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -eo pipefail + +function ee() +{ + echo "$ $*" + eval "$@" +} + +exec 9>&1 # enable tee to write to STDOUT as a file +# print debugging code +ee node --version +ee yarn --version +ee npm --version +# add metadata +echo 'Packing website metadata into distribution.' +cat package.json | jq \ + --arg actor "$GITHUB_ACTOR" \ + --arg branch "$(git branch --show-current)" \ + --arg branchFromTag "$(git branch --contains 'tags/v0.1.0' | tail -n +2 | tail -n 1 | tr -d '[:space:]')" \ + --arg build "$GITHUB_RUN_NUMBER" \ + --arg build_id "$GITHUB_RUN_ID" \ + --arg commit "$(git rev-parse HEAD)" \ + --arg email "$(git log -n 1 --pretty=format:%ae)" \ + --arg node "$(node --version)" \ + --arg ref_type "$GITHUB_REF_TYPE" \ + --arg repo "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \ + --arg tag "$(git --no-pager tag --points-at HEAD)" \ + --arg triggering_actor "$GITHUB_TRIGGERING_ACTOR" \ + '.git += { + $actor, + branch: (if $branch != "" then $branch elif $branchFromTag != "" then $branchFromTag else null end), + build: ($build | tonumber), + build_id: ($build_id | tonumber), + build_url: ($repo + "/actions/runs/" + $build_id), + $commit, + $email, + $node, + $ref_type, + $repo, + tag: ($tag | if . == "" then null else . end), + $triggering_actor + }' > temp.json +mv temp.json package.json +ee 'cat package.json | jq .git' +# generate static site +ee yarn build +# package website +ee 'yarn pack' +PACKAGE_NAME="$(cat package.json | jq -r '.name' | tr -d '@' | tr '/' '-')" +PACKAGE_VERSION="$(cat package.json | jq -r '.version')" +PACKAGE_TAR="$PACKAGE_NAME-v$PACKAGE_VERSION.tgz" +mv "$PACKAGE_TAR" dist.tar.gz +echo "Done. - ${BASH_SOURCE[0]}" diff --git a/.github/workflows/ci.md b/.github/workflows/ci.md new file mode 100644 index 0000000..5bc5a24 --- /dev/null +++ b/.github/workflows/ci.md @@ -0,0 +1,51 @@ +# REX Staking Portal CI +This GitHub Actions workflow lints, tests, builds, and publishes the `rex-staking-portal` Svelte website. + +### Index +1. [Triggers](#triggers) +1. [Inputs](#inputs) +1. [Steps](#steps) +1. [Outputs](#outputs) +1. [See Also](#see-also) + +## Triggers +This GitHub action will run under the following circumstances: +1. When code is pushed to any branch. +1. When any tag is pushed. +1. On any workflow dispatch event, which is triggered manually using the "Workflow Dispatch" button in the Actions tab of the GitHub repository. + +## Inputs +There are no inputs to this workflow beyond the contents of the `package.json` and the GitHub Actions [intrinsic environment variables](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables). + +## Steps +This workflow performs the following steps: +1. Attach Documentation: + 1. Checkout the repo with no submodules or history. + 1. Attach an annotation to the GitHub Actions build summary page containing this CI documentation. +1. Build `rex-staking-portal`: + > This is a build matrix with one or more nodeJS versions. + 1. Checkout this repo without history. + 1. Setup nodeJS at the given major version. + 1. Download project dependencies. + 1. Lint the project. + 1. Generate static site files and pack them in a `*.tar.gz` archive along with project and build metadata. + 1. Attach the `*.tar.gz` file as an artifact. +1. Publish `rex-staking-portal`: + 1. Checkout this repo without history. + 1. Determine whether to perform a deployment or a dry-run, and authenticate to Amazon Web Services (AWS) accordingly. + 1. Download build artifacts from the build matrix above. + 1. Push (or simulate pushing) the build artifacts to the AWS S3 bucket for the REX Staking Portal. + +## Outputs +This workflow produces the following outputs: +1. Archive for upload to AWS S3 (`*.tar.gz`). + +## See Also +- [Pipeline](https://github.com/eosnetworkfoundation/rex-staking-portal/actions/workflows/ci.yml) +- [Project Documentation](../../README.md) + +For assistance with the CI system, please open an issue in this repo or reach out to the ENF Automation team. + +--- +> **_Legal Notice_** +> This repo contains assets created in collaboration with a large language model, machine learning algorithm, or weak artificial intelligence (AI). This notice is required in some countries. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..04fb761 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,87 @@ +name: Staking Portal CI + +on: [push, workflow_dispatch] + +jobs: + documentation: + name: Attach Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'false' + + - name: Attach Documentation + run: cat .github/workflows/ci.md >> $GITHUB_STEP_SUMMARY + + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20] + + name: Build rex-staking-portal - nodeJS v${{ matrix.node-version }} + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup node v${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Init - node v${{ matrix.node-version }} + run: yarn --frozen-lockfile --non-interactive + + - name: Lint - node v${{ matrix.node-version }} + run: yarn check + + - name: Static Build - node v${{ matrix.node-version }} + run: .github/workflows/build.sh + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-node-${{ matrix.node-version }} + path: dist.tar.gz + + aws: + name: Publish rex-staking-portal + runs-on: ubuntu-latest + needs: build + permissions: + id-token: write + contents: read + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Determine AWS Context + id: aws-context + run: .github/workflows/aws-context.sh + env: + STAKING_PROD_IAM_ARN: ${{ secrets.STAKING_PROD_IAM_ARN }} + STAKING_RO_IAM_ARN: ${{ secrets.STAKING_RO_IAM_ARN }} + + - name: Authenticate to AWS + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-region: us-east-1 + role-to-assume: ${{ steps.aws-context.outputs.role-arn }} + role-duration-seconds: 900 + + - name: Download Matrix Artifacts + uses: actions/download-artifact@v4 + + - name: Publish Website + run: .github/workflows/publish.sh + env: + CF_DISTRIBUTION: ${{ secrets.STAKING_PROD_CF_DISTRIBUTION }} + DRY_RUN: ${{ steps.aws-context.outputs.dry-run }} + S3_BUCKET: ${{ secrets.STAKING_PROD_S3_BUCKET }} diff --git a/.github/workflows/publish.sh b/.github/workflows/publish.sh new file mode 100755 index 0000000..bbfb5a2 --- /dev/null +++ b/.github/workflows/publish.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -eo pipefail + +function ee() +{ + echo "$ $*" + eval "$@" +} + +exec 9>&1 # enable tee to write to STDOUT as a file +ee 'aws --version' +ee 'git log -1' +ee "git branch --contains 'tags/$(git --no-pager tag --points-at HEAD)' || :" +echo 'Finding newest matrix artifact.' +ee 'ls -la' +export DIST_DL_FOLDER="$(find . -maxdepth '1' -name 'dist*' -type 'd' | sort -r | head -n '1')" +echo "Identified \"$DIST_DL_FOLDER\" as the matrix build artifact from the most recent nodeJS version, unpacking." +ee "pushd '$DIST_DL_FOLDER'" +ee 'tar -xzf dist.tar.gz' +ee 'pushd dist' +ee 'ls -la' +echo 'Uploading website distribution to Amazon S3.' +export S3_SYNC='aws s3 sync "." "s3://$S3_BUCKET" --delete' +if [[ "$DRY_RUN" != 'false' ]]; then + export S3_SYNC="$S3_SYNC --dryrun" +fi +ee "$S3_SYNC" +echo 'Tagging website objects.' +export TAGS="$(jq -n -c --argjson git "$(cat package.json | jq -c .git)" '{"billing-use": "devrel", "branch": ($git | .branch | tostring), "build-url": ($git | .build_url), "commit": ($git | .commit), "email": ($git | .email), "manual": "false", "tag": ($git | .tag | tostring), "terraform": "false"}')" +ee 'echo "$TAGS" | jq .' +export AWS_TAG_FORMAT="$(echo "$TAGS" | jq -c '{TagSet: (. | to_entries)}' | sed 's/"key"/"Key"/g' | sed 's/"value"/"Value"/g')" +export S3_LIST='aws s3api list-objects-v2 --bucket "$S3_BUCKET" --query "Contents[].{Key:Key}" --output text' +export S3_TAG_OBJECT='aws s3api put-object-tagging --bucket "$S3_BUCKET"' +if [[ "$DRY_RUN" != 'false' ]]; then + echo 'AWS CLI dry run support is inconsistent and this command does not have it, printing object tag command with no dry run.' + for OBJECT in $(eval "$S3_LIST") + do + echo "$ $S3_TAG_OBJECT --key '$OBJECT' --tagging '$AWS_TAG_FORMAT'" + done +else + for OBJECT in $(eval "$S3_LIST") + do + ee "$S3_TAG_OBJECT --key '$OBJECT' --tagging '$AWS_TAG_FORMAT'" + done +fi +echo 'Refreshing AWS Cloudfront (CDN) Edge Nodes' +export AWS_CDN_REFRESH="aws cloudfront create-invalidation --distribution-id \"\$CF_DISTRIBUTION\" --paths '/*'" +if [[ "$DRY_RUN" != 'false' ]]; then + echo 'AWS CLI dry run support is inconsistent and this command does not have it, printing CDN refresh command with no dry run.' + echo "$ $AWS_CDN_REFRESH" +else + echo "$ $AWS_CDN_REFRESH" + export INVALIDATION_ID="$(eval "$AWS_CDN_REFRESH" | tee >(cat - >&9) | jq -r '.Invalidation.Id')" + echo 'Waiting for CDN edge nodes to refresh...' + ee "aws cloudfront wait invalidation-completed --distribution-id \"\$CF_DISTRIBUTION\" --id '$INVALIDATION_ID'" + echo "::notice title=Deployed v$(cat package.json | jq -r .version) to Production Worldwide::Frontend v$(cat package.json | jq -r .version) has been successfully uploaded and the content delivery network has been refreshed worldwide. This is live in production." +fi +echo "Done. - ${BASH_SOURCE[0]}"