Skip to content

Merge pull request #52 from dev-dull/ci #133

Merge pull request #52 from dev-dull/ci

Merge pull request #52 from dev-dull/ci #133

Workflow file for this run

name: Build and Run Docker Image
on:
schedule:
# Refresh monthly to pick up base image updates
- cron: '0 0 1 * *'
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
outputs:
DOCKER_IMAGE_NAME: ${{ steps.build-image-name.outputs.DOCKER_IMAGE_NAME }}
VERSION: ${{ steps.build-image-name.outputs.VERSION }}
CODELINE: ${{ steps.build-image-name.outputs.CODELINE }}
INSTANCE_NAME: ${{ steps.build-image-name.outputs.INSTANCE_NAME }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Build Image Name
id: build-image-name
# TODO: going to both ENV and OUTPUTS is probably redundant
run: |
version=$(cat pyterrabacktyl.py | grep __version__ | tr -d "[a-zA-Z ='_]")
# Seems like GITHUB_ENV value can be changed, but value in GITHUB_OUTPUT can NOT
if [ github.ref_name != 'main' ]; then
echo "CODELINE=${{ github.ref_name }}_${version}" | tee -a $GITHUB_ENV | tee -a $GITHUB_OUTPUT
else
echo CODELINE=$(echo ${{ github.ref_name }} | tr '[A-Z]' '[a-z]') | tee -a $GITHUB_ENV | tee -a $GITHUB_OUTPUT
fi
echo DOCKER_IMAGE_NAME=devdull/$(echo ${{ github.event.repository.name }} | tr '[A-Z]' '[a-z]') | tee -a $GITHUB_ENV | tee -a $GITHUB_OUTPUT
echo INSTANCE_NAME=${{ github.ref_name }}_InT | tee -a $GITHUB_ENV | tee -a $GITHUB_OUTPUT
echo "VERSION=${version}" | tee -a $GITHUB_ENV | tee -a $GITHUB_OUTPUT
- name: Build Docker Image
run: |
docker build . --file Dockerfile --tag ${{ env.DOCKER_IMAGE_NAME }}:${{ env.CODELINE }}
echo '## Image Details' >> $GITHUB_STEP_SUMMARY
header=$(docker images | sed -r 's/\s{2,}/|/g' | grep -E '^R' | sed -r 's/^|$/\|/g')
echo "$header" >> $GITHUB_STEP_SUMMARY
echo "$header" | sed -r 's/[^|]/-/g' >> $GITHUB_STEP_SUMMARY
docker images | sed -r 's/\s{2,}/|/g' | grep -E 'pyterrabacktyl' | sed -r 's/^|$/\|/g' >> $GITHUB_STEP_SUMMARY
docker save ${{ env.DOCKER_IMAGE_NAME }}:${{ env.CODELINE }} -o /tmp/${{ github.event.repository.name }}.tar
- name: Upload Docker Image
uses: actions/upload-artifact@v4
with:
name: ${{ github.event.repository.name }}.tar
path: /tmp/${{ github.event.repository.name }}.tar
- name: Create summary header
run: |
echo '## Test Results' | tee -a $GITHUB_STEP_SUMMARY
run_and_test:
runs-on: ubuntu-latest
needs: build
strategy:
matrix:
# TODO: Test OpenTofu
terraform_version:
- '1.10.4' # Latest
- '1.5.6' # Popular
- '1.3.9' # Popular
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Download Docker Image
uses: actions/download-artifact@v4
with:
name: ${{ github.event.repository.name }}.tar
path: /tmp
- name: Load Docker Image
run: |
docker load -i /tmp/${{ github.event.repository.name }}.tar
- name: Run Docker Container
run: |
docker run -d --name ${{ needs.build.outputs.INSTANCE_NAME }} -p 2442:2442 ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }}
- name: Wait for PyTerraBackTYL to start
run: |
for ct in {0..9}
do
# '000' gets set if curl fails
STATUS=$(curl -Ss -o /dev/null -w "%{http_code}" http://localhost:2442 || true)
if [ ${STATUS} -ne 200 ]; then
echo "waiting..."
sleep 1
else
exit 0
fi
done
exit 1
- name: Set up Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ matrix.terraform_version }}
# TODO: nicely formatted summary output doesn't show the failures
# Maybe just write a bunch of shell scripts and exec them here
# Loop over them and report the name ($0), results (✅, ❌)
- name: Set headers for test output
run: |
echo '## Terraform ${{ matrix.terraform_version }}' >> $GITHUB_STEP_SUMMARY
COLS="|Test Name|Status|"
echo "${COLS}" >> /tmp/summary
echo "${COLS}" | sed -r 's/[^|]/-/g' >> /tmp/summary
- name: Run 'happy-path' Test
run: |
cd .github/build_tests
terraform init
terraform plan
terraform apply --auto-approve
docker logs ${{ needs.build.outputs.INSTANCE_NAME }}
echo TFSTATE=$(curl -sS http://localhost:2442/?env=InT) | tee -a $GITHUB_ENV
echo "|Happy Path|✅ Success|" >> /tmp/summary
- name: Validate Terraform State
# Validate that Terraform state saved in PyTerraBackTYL matches the content of the file generated by Terraform
run: |
cd .github/build_tests
TF_CONTENT=$(echo '${{ env.TFSTATE }}' | jq -r '.resources[].instances[].attributes.content | select(. != null)')
FILE_CONTENT=$(cat test.txt)
if [[ "${TF_CONTENT}" != "${FILE_CONTENT}" ]]; then
echo ${TF_CONTENT} != ${FILE_CONTENT} >&2
exit 1
fi
echo -e "|State Saved|✅ Success|" >> /tmp/summary
- name: Validate Terraform State Changed
# The ID of the null resource should change with every apply. Verify PyTerraBackTYL is saving the updated state
run: |
cd .github/build_tests
CURRENT_NULL_RESOURCE_ID=$(echo '${{env.TFSTATE}}' | jq -r '.resources[] | select(.type == "null_resource") | .instances[].attributes.id')
terraform apply --auto-approve
NEW_NULL_RESOURCE_ID=$(curl -sS http://localhost:2442/?env=InT | jq -r '.resources[] | select(.type == "null_resource") | .instances[].attributes.id')
[ -n "${NEW_NULL_RESOURCE_ID}" ]
[ -n "${CURRENT_NULL_RESOURCE_ID}" ]
[ "${CURRENT_NULL_RESOURCE_ID}" -ne "${NEW_NULL_RESOURCE_ID}" ]
echo -e "|State Changed|✅ Success|" >> /tmp/summary
- name: Run Terraform Test, Locked ENV
# Manually lock the environment and capture how terraform exited
id: prelock_InT
continue-on-error: true
run: |
curl -X LOCK -sS http://localhost:2442/lock?env=InT
cd .github/build_tests
terraform apply --auto-approve
- name: Verify Command Failed Successfully
# Verify that terraform failed with a lock error when trying to run against a locked environment
run: |
if [ "${{ steps.prelock_InT.outcome }}" != "success" ]; then
curl -X UNLOCK -sS http://localhost:2442/unlock?env=InT
echo "|Locked Environment is Blocking|✅ Success|" >> /tmp/summary
else
# Picked confusing wording just to be silly
echo "Step unexpectedly failed to fail as expected."
exit 1
fi
- name: Show results, Clean Up
run: |
cat /tmp/summary | tee -a $GITHUB_STEP_SUMMARY
# Hopefully this is being nice to GHA infra and not just wasting CPU cycles
docker rm -f ${{ needs.build.outputs.INSTANCE_NAME }} || true
docker rmi ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }} || true
tag_and_push:
runs-on: ubuntu-latest
needs:
- build
- run_and_test
steps:
- name: Download Docker Image
uses: actions/download-artifact@v4
with:
name: ${{ github.event.repository.name }}.tar
path: /tmp
- name: Load Docker Image
run: |
docker load -i /tmp/${{ github.event.repository.name }}.tar
- name: Docker Login
run: |
echo '${{ secrets.DOCKERHUB_PASSWORD }}' | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
echo '## Push image to DockerHub (v${{ needs.build.outputs.VERSION }})' >> $GITHUB_STEP_SUMMARY
- name: Tag Docker Image
if: github.ref_name != 'main'
run: |
docker push ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }}
echo "${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }}" >> $GITHUB_STEP_SUMMARY
- name: Tag and Push Latest
if: github.ref_name == 'main'
run: |
docker tag ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }} ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }} ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:latest
docker tag ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }} ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }} ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.VERSION }}
docker push ${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:latest
echo "${{ needs.build.outputs.DOCKER_IMAGE_NAME }}:${{ needs.build.outputs.CODELINE }}" >> $GITHUB_STEP_SUMMARY