diff --git a/.github/workflows/app-build-and-deploy.yml b/.github/workflows/app-build-and-deploy.yml index 43c57c8..fa06f2b 100644 --- a/.github/workflows/app-build-and-deploy.yml +++ b/.github/workflows/app-build-and-deploy.yml @@ -1,5 +1,5 @@ # This workflow will build a docker image, push it to ghcr.io, and deploy it to an Azure WebApp. -# v1.1.0 - This tag coordinates the other reusable parts of this workflow. +# v1.1.1 - This tag coordinates the other reusable parts of this workflow. # * app-build-docker-image.yml # * app-deploy-to-azure.yml # * app-is-deployable.yml @@ -65,35 +65,34 @@ jobs: outputs: version: ${{ env.VERSION }} steps: - - uses: actions/checkout@v4.1.1 + - name: Download package-lock.json + uses: actions/download-artifact@v4 + with: + name: package-lock.json + path: . + - name: Checkout this repo + uses: actions/checkout@v4.1.1 + with: + repository: 'clearlydefined/operations' + ref: 'v1.1.1' + path: 'operations' - name: Get version from package-lock.json id: get_version shell: bash run: | - version='v'$(jq -r '.version' package-lock.json) # e.g. v1.2.0 - if [[ ${{ inputs.deploy-env }} == 'prod' ]]; then - if [[ ${{ needs.determine-trigger.outputs.is-release }} == 'true' ]]; then - # validate the version when triggered by a release - if [[ $version != ${{ github.event.release.tag_name }} ]]; then - echo "Version in package-lock.json ($version) does not match the release tag (${{ github.event.release.tag_name }})" - exit 1 - fi - fi - elif [[ ${{ inputs.deploy-env }} == 'dev' ]]; then - short_sha=$(echo "${{ github.sha }}" | cut -c 1-10) - version=$version'-dev-'$short_sha # e.g. v1.2.0-dev:1234567890 - else - echo "Invalid deploy-env: ${{ inputs.deploy-env }}. Must be 'dev' or 'prod'" - exit 1 - fi - + script_log=$(./operations/scripts/app-workflows/get-version.sh \ + "${{ inputs.deploy-env }}" \ + "${{ needs.determine-trigger.outputs.is-release }}" \ + "${{ github.event.release.tag_name }}" \ + "${{ github.sha }}") || (echo "$script_log" && exit 1) + echo -e "---- script log\n$script_log\n----"; \ + version=$(echo "$script_log" | tail -n 1) echo "VERSION=$version" >> $GITHUB_ENV - echo "BuildAndDeploy: get-version -> outputs -> version: $version" - + build-and-publish-image: name: Build and publish Docker image needs: get-version - uses: clearlydefined/operations/.github/workflows/app-build-docker-image.yml@v1.1.0 + uses: clearlydefined/operations/.github/workflows/app-build-docker-image.yml@v1.1.1 secrets: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} PRODUCTION_DEPLOYERS: ${{ secrets.PRODUCTION_DEPLOYERS }} @@ -105,7 +104,7 @@ jobs: deploy-primary-app-to-azure: name: Deploy to primary Azure app needs: [get-version, build-and-publish-image] - uses: clearlydefined/operations/.github/workflows/app-deploy-to-azure.yml@v1.1.0 + uses: clearlydefined/operations/.github/workflows/app-deploy-to-azure.yml@v1.1.1 secrets: AZURE_WEBAPP_PUBLISH_PROFILE: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} @@ -121,7 +120,7 @@ jobs: name: Deploy to secondary Azure app if: ${{ inputs.secondary-azure-app-name-postfix != '' }} needs: [get-version, build-and-publish-image] - uses: clearlydefined/operations/.github/workflows/app-deploy-to-azure.yml@v1.1.0 + uses: clearlydefined/operations/.github/workflows/app-deploy-to-azure.yml@v1.1.1 secrets: AZURE_WEBAPP_PUBLISH_PROFILE: ${{ secrets.AZURE_SECONDARY_WEBAPP_PUBLISH_PROFILE }} AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} diff --git a/.github/workflows/app-build-docker-image.yml b/.github/workflows/app-build-docker-image.yml index ddc02c0..398db46 100644 --- a/.github/workflows/app-build-docker-image.yml +++ b/.github/workflows/app-build-docker-image.yml @@ -31,7 +31,7 @@ on: jobs: check-deployable: - uses: clearlydefined/operations/.github/workflows/app-is-deployable.yml@v1.1.0 + uses: clearlydefined/operations/.github/workflows/app-is-deployable.yml@v1.1.1 with: deploy-env: ${{ inputs.deploy-env }} secrets: @@ -44,20 +44,22 @@ jobs: outputs: docker-image-name-with-tag: ${{ env.DOCKER_IMAGE_NAME_WITH_TAG }} steps: + - name: Checkout this repo + uses: actions/checkout@v4.1.1 + with: + repository: 'clearlydefined/operations' + ref: 'v1.1.1' + path: 'operations' - name: Determine Docker Image Name id: determine_image_name run: | - image_base_name=ghcr.io/${{ github.repository }} # e.g. ghcr.io/clearlydefined/service - if [[ ${{ inputs.deploy-env }} == 'prod' ]] ; then - image_name_with_tag=$image_base_name':${{ inputs.image-tag }}' - elif [[ ${{ inputs.deploy-env }} == 'dev' ]] ; then - image_name_with_tag=$image_base_name'-dev:${{ inputs.image-tag }}' - else - echo "Invalid deploy-env: ${{ inputs.deploy-env }}. Must be 'dev' or 'prod'" - exit 1 - fi - echo "DOCKER_IMAGE_NAME_WITH_TAG=$image_name_with_tag" >> $GITHUB_ENV - echo "DetermineImageName: determine_image_name -> outputs -> image_name_with_tag: $image_name_with_tag" + script_log=$(./operations/scripts/app-workflows/determine-image-name.sh \ + "${{ github.repository }}" \ + "${{ inputs.deploy-env }}" \ + "${{ inputs.image-tag }}") || (echo "$script_log" && exit 1) + echo -e "---- script log\n$script_log\n----"; \ + image_name=$(echo "$script_log" | tail -n 1) + echo "DOCKER_IMAGE_NAME_WITH_TAG=$image_name" >> $GITHUB_ENV build-docker-image: name: Build Image diff --git a/.github/workflows/app-deploy-to-azure.yml b/.github/workflows/app-deploy-to-azure.yml index d9fe3b2..d6c8ba9 100644 --- a/.github/workflows/app-deploy-to-azure.yml +++ b/.github/workflows/app-deploy-to-azure.yml @@ -37,7 +37,7 @@ on: jobs: check-deployable: - uses: clearlydefined/operations/.github/workflows/app-is-deployable.yml@v1.1.0 + uses: clearlydefined/operations/.github/workflows/app-is-deployable.yml@v1.1.1 with: deploy-env: ${{ inputs.deploy-env }} secrets: diff --git a/.github/workflows/app-is-deployable.yml b/.github/workflows/app-is-deployable.yml index 1cfb645..4fed3c8 100644 --- a/.github/workflows/app-is-deployable.yml +++ b/.github/workflows/app-is-deployable.yml @@ -22,38 +22,45 @@ jobs: outputs: is-dev: ${{ env.IS_DEV }} steps: - # erring on the side of caution by assuming everything is prod unless it is identified as dev and ends in -dev + - name: Checkout this repo + uses: actions/checkout@v4.1.1 + with: + repository: 'clearlydefined/operations' + ref: 'v1.1.1' + path: 'operations' - id: confirm-dev shell: bash run: | - is_dev=false - if [[ "${{ inputs.deploy-env }}" == 'dev' ]]; then - is_dev=true - echo "Deploying to dev environment" - else - echo "Deploying to prod or UNKNOWN environment" - fi + script_log=$(./operations/scripts/app-workflows/confirm-dev.sh \ + "${{ inputs.deploy-env }}") || (echo "$script_log" && exit 1) + echo -e "---- script log\n$script_log\n----"; \ + is_dev=$(echo "$script_log" | tail -n 1) echo "IS_DEV=$is_dev" >> $GITHUB_ENV - echo "Deployable: confirm-dev -> outputs -> is-dev: $is_dev" - + deployable: runs-on: ubuntu-latest needs: confirm-dev # run deployable check for anything that is NOT dev (most conservative approach) if: ${{ needs.confirm-dev.outputs.is-dev != 'true' }} - steps: + steps: + - name: Checkout this repo + uses: actions/checkout@v4.1.1 + with: + repository: 'clearlydefined/operations' + ref: 'v1.1.1' + path: 'operations' + - name: Get organization ID run: | - org_name=${{ github.repository_owner }} - org_info=$(curl \ - -H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/orgs/$org_name) - org_id=$(echo "$org_info" | jq .id) + script_log=$(./operations/scripts/app-workflows/get-org-id.sh \ + "${{ github.repository_owner }}") || (echo "$script_log" && exit 1) + echo -e "---- script log\n$script_log\n----"; \ + org_id=$(echo "$script_log" | tail -n 1) echo "ORG_ID=$org_id" >> $GITHUB_ENV - name: Check team membership run: | + echo "ORG_ID=${{ env.ORG_ID }}" user="${{ github.actor }}" org_id=${{ env.ORG_ID }} org_name=${{ github.repository_owner }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..bbbbca4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install Bats + run: | + git clone https://github.com/bats-core/bats-core.git + cd bats-core + sudo ./install.sh /usr/local + + - name: Run Bats tests + run: bats ./tests/scripts/app-workflows/*.bats diff --git a/scripts/app-workflows/confirm-dev.sh b/scripts/app-workflows/confirm-dev.sh new file mode 100755 index 0000000..28dbeae --- /dev/null +++ b/scripts/app-workflows/confirm-dev.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Inputs +# $1 - deploy_env: environment to deploy (i.e. dev | prod) - used as a label for the Docker image +# +# Outputs +# is-dev: 'true' if deploying to dev environment, 'false' otherwise + +deploy_env="$1" + +# erring on the side of caution by assuming everything is prod unless it is identified as dev and ends in -dev +is_dev='false' +if [[ "$deploy_env" == 'dev' ]]; then + is_dev='true' + echo "Deploying to dev environment" +else + echo "Deploying to prod or UNKNOWN environment" +fi + +echo "confirm-dev -> outputs -> is_dev: $is_dev" +echo $is_dev diff --git a/scripts/app-workflows/determine-image-name.sh b/scripts/app-workflows/determine-image-name.sh new file mode 100755 index 0000000..b22ebe8 --- /dev/null +++ b/scripts/app-workflows/determine-image-name.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Inputs +# $1 - repo: the orgname/reponame where the image will be published (e.g. 'clearlydefined/service') +# $2 - deploy_env: environment to deploy (i.e. dev | prod) - used as a label for the Docker image +# $3 - image-tag: the tag to use for the image (e.g. prod: v1.2.0, dev: v1.2.0+dev:1D3F567890) +# +# Outputs +# image_name_with_tag: the full image name with tag (e.g. ghcr.io/clearlydefined/service:v1.2.0) + +repo="$1" +deploy_env="$2" +image_tag="$3" + +image_base_name="ghcr.io/$repo" # e.g. ghcr.io/clearlydefined/service +if [[ "$deploy_env" == 'prod' ]] ; then + image_name_with_tag="$image_base_name:$image_tag" +elif [[ "$deploy_env" == 'dev' ]] ; then + image_name_with_tag="$image_base_name-dev:$image_tag" +else + echo "ERROR: Invalid deploy environment: $deploy_env. Must be 'dev' or 'prod'" + exit 1 +fi + +echo "determine_image_name -> outputs -> image_name_with_tag: $image_name_with_tag" +echo "$image_name_with_tag" diff --git a/scripts/app-workflows/get-org-id.sh b/scripts/app-workflows/get-org-id.sh new file mode 100755 index 0000000..344be87 --- /dev/null +++ b/scripts/app-workflows/get-org-id.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Inputs +# $1 - org_name: the orgname of the repo (e.g. 'clearlydefined/service' has owner 'clearlydefined') +# +# Outputs +# org_id: the id of the organization that owns the repo + +org_name="$1" + +org_info=$(curl -s -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/$org_name") +org_id=$(echo "$org_info" | jq .id) +if [[ "$org_id" == "null" ]]; then + echo "ERROR: Organization not found: $org_name" + exit 1 +fi + +echo "get-org-id -> outputs -> org_id: $org_id" +echo $org_id diff --git a/scripts/app-workflows/get-version.sh b/scripts/app-workflows/get-version.sh new file mode 100755 index 0000000..145cd3a --- /dev/null +++ b/scripts/app-workflows/get-version.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Inputs +# $1 - deploy_env: environment to deploy (i.e. dev | prod) - used as a label for the Docker image +# $2 - is_release: true if the deployment is triggered by a release, false otherwise +# $3 - release_tag: the tag of the release that triggered the deployment (empty if not a release) +# $4 - sha: the git sha of the commit being deployed +# $5 - lock_file: the path to the package-lock.json file (default: /package-lock.json) +# +# Outputs +# version: the version of the package to deploy (e.g. v1.2.0, v1.2.0-dev-1234567890) + +deploy_env="$1" +is_release="$2" +release_tag="$3" +sha="$4" +lock_file="${5:-package-lock.json}" + +version='v'$(jq -r '.version' $lock_file) # e.g. v1.2.0 +if [[ "$deploy_env" == 'prod' ]]; then + if [[ "$is_release" == 'true' ]]; then + # validate the version when triggered by a release + if [[ "$version" != "$release_tag" ]]; then + echo "ERROR: Version in package-lock.json ($version) does not match the release tag ($release_tag)" + exit 1 + fi + fi +elif [[ "$deploy_env" == 'dev' ]]; then + short_sha=$(echo "$sha" | cut -c 1-10) + version=$version'-dev-'$short_sha # e.g. v1.2.0-dev-1234567890 +else + echo "ERROR: Invalid deploy environment: $deploy_env. Must be 'dev' or 'prod'" + exit 1 +fi + +echo "get-version.sh -> outputs -> version: $version" +echo "$version" diff --git a/tests/scripts/app-workflows/fixtures/package-lock.json b/tests/scripts/app-workflows/fixtures/package-lock.json new file mode 100644 index 0000000..9e940e1 --- /dev/null +++ b/tests/scripts/app-workflows/fixtures/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "test-repo", + "version": "10.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "service", + "version": "1.3.1", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "typescript": "5.0.4" + } + } + } +} diff --git a/tests/scripts/app-workflows/test-confirm-dev.bats b/tests/scripts/app-workflows/test-confirm-dev.bats new file mode 100644 index 0000000..fa0b743 --- /dev/null +++ b/tests/scripts/app-workflows/test-confirm-dev.bats @@ -0,0 +1,27 @@ +#!/usr/bin/env bats + +load 'test_helpers' + +@test "deploy to dev environment" { + run ./scripts/app-workflows/confirm-dev.sh dev + test_value 0 "$status" + test_value "Deploying to dev environment" "${lines[0]}" + test_value "confirm-dev -> outputs -> is_dev: true" "${lines[1]}" + test_value true "${lines[2]}" +} + +@test "deploy to prod environment" { + run ./scripts/app-workflows/confirm-dev.sh prod + test_value 0 "$status" + test_value "Deploying to prod or UNKNOWN environment" "${lines[0]}" + test_value "confirm-dev -> outputs -> is_dev: false" "${lines[1]}" + test_value false "${lines[2]}" +} + +@test "deploy to anything else defaults to prod environment for tighter restrictions" { + run ./scripts/app-workflows/confirm-dev.sh UNKNOWN_ENV + test_value 0 "$status" + test_value "Deploying to prod or UNKNOWN environment" "${lines[0]}" + test_value "confirm-dev -> outputs -> is_dev: false" "${lines[1]}" + test_value false "${lines[2]}" +} diff --git a/tests/scripts/app-workflows/test-determine-image-name.bats b/tests/scripts/app-workflows/test-determine-image-name.bats new file mode 100644 index 0000000..4803e8d --- /dev/null +++ b/tests/scripts/app-workflows/test-determine-image-name.bats @@ -0,0 +1,23 @@ +#!/usr/bin/env bats + +load 'test_helpers' + +@test "deploy to dev environment" { + run ./scripts/app-workflows/determine-image-name.sh test-org/test-repo dev test-tag + test_value 0 "$status" + test_value "determine_image_name -> outputs -> image_name_with_tag: ghcr.io/test-org/test-repo-dev:test-tag" "${lines[0]}" + test_value ghcr.io/test-org/test-repo-dev:test-tag "${lines[1]}" +} + +@test "deploy to prod environment" { + run ./scripts/app-workflows/determine-image-name.sh test-org/test-repo prod test-tag + test_value 0 "$status" + test_value "determine_image_name -> outputs -> image_name_with_tag: ghcr.io/test-org/test-repo:test-tag" "${lines[0]}" + test_value ghcr.io/test-org/test-repo:test-tag "${lines[1]}" +} + +@test "invalid deploy environment" { + run ./scripts/app-workflows/determine-image-name.sh test-org/test-repo BAD_ENV test-tag + test_value 1 "$status" + test_value "ERROR: Invalid deploy environment: BAD_ENV. Must be 'dev' or 'prod'" "${lines[0]}" +} diff --git a/tests/scripts/app-workflows/test-get-org-id.bats b/tests/scripts/app-workflows/test-get-org-id.bats new file mode 100644 index 0000000..7f29f14 --- /dev/null +++ b/tests/scripts/app-workflows/test-get-org-id.bats @@ -0,0 +1,16 @@ +#!/usr/bin/env bats + +load 'test_helpers' + +@test "get github org id" { + run ./scripts/app-workflows/get-org-id.sh "github" + test_value 0 "$status" + test_value "get-org-id -> outputs -> org_id: 9919" "${lines[0]}" + test_value "9919" "${lines[1]}" +} + +@test "missing org name" { + run ./scripts/app-workflows/get-org-id.sh "" + test_value 1 "$status" + test_value "ERROR: Organization not found: " "${lines[0]}" +} diff --git a/tests/scripts/app-workflows/test-get-version.bats b/tests/scripts/app-workflows/test-get-version.bats new file mode 100644 index 0000000..e8c8ee5 --- /dev/null +++ b/tests/scripts/app-workflows/test-get-version.bats @@ -0,0 +1,41 @@ +#!/usr/bin/env bats + +load 'test_helpers' + +package_lock_file="$(dirname "$BATS_TEST_DIRNAME")/app-workflows/fixtures/package-lock.json" + +@test "deploy to dev environment" { + run ./scripts/app-workflows/get-version.sh dev false "" 1234567890ABCDEF "$package_lock_file" + test_value 0 "$status" + test_value "get-version.sh -> outputs -> version: v10.0.1-dev-1234567890" "${lines[0]}" + test_value "v10.0.1-dev-1234567890" "${lines[1]}" +} + +@test "deploy to prod environment triggered by release and version matches" { + # only use version from package-lock.json if it matches the release tag + run ./scripts/app-workflows/get-version.sh prod true v10.0.1 1234567890ABCDEF "$package_lock_file" + test_value 0 "$status" + test_value "get-version.sh -> outputs -> version: v10.0.1" "${lines[0]}" + test_value "v10.0.1" "${lines[1]}" +} + +@test "deploy to prod environment triggered by release and version doesn't matches" { + # fail because version in package-lock.json doesn't match the release tag + run ./scripts/app-workflows/get-version.sh prod true v9.2.0 1234567890ABCDEF "$package_lock_file" + test_value 1 "$status" + test_value "ERROR: Version in package-lock.json (v10.0.1) does not match the release tag (v9.2.0)" "${lines[0]}" +} + +@test "deploy to prod environment triggered by dispatch" { + # always uses version from package-lock.json when triggered by a dispatch + run ./scripts/app-workflows/get-version.sh prod false v9.2.0 1234567890ABCDEF "$package_lock_file" + test_value 0 "$status" + test_value "get-version.sh -> outputs -> version: v10.0.1" "${lines[0]}" + test_value "v10.0.1" "${lines[1]}" +} + +@test "invalid deploy environment" { + run ./scripts/app-workflows/get-version.sh BAD_ENV false v9.2.0 1234567890ABCDEF "$package_lock_file" + test_value 1 "$status" + test_value "ERROR: Invalid deploy environment: BAD_ENV. Must be 'dev' or 'prod'" "${lines[0]}" +} diff --git a/tests/scripts/app-workflows/test_helpers.bash b/tests/scripts/app-workflows/test_helpers.bash new file mode 100644 index 0000000..1791275 --- /dev/null +++ b/tests/scripts/app-workflows/test_helpers.bash @@ -0,0 +1,7 @@ +#!/bin/bash + +test_value() { + local expected="$1" + local actual="$2" + diff <(echo "$actual") <(echo "$expected") || { echo -e "expected: $expected\nactual: '$actual'"; return 1; } +}