name: CI on: push: branches: - master pull_request: env: IMAGE_NAME: ${{ vars.IMAGE_NAME }} REGISTRY_LOCATION: ${{ vars.REGISTRY_LOCATION }} REGISTRY_ORGANIZATION: ${{ vars.REGISTRY_ORGANIZATION }} PORT: ${{ vars.PORT }} TEST_PORT: ${{ vars.TEST_PORT }} REGISTRY_USER: ${{ secrets.REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} PROD_SERVER_HOST: ${{ secrets.PROD_SERVER_HOST }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} BUILD_NUMBER: build-${{ gitea.run_number }} jobs: lint: name: โœ… Lint & Validate runs-on: ubuntu-latest steps: - name: ๐Ÿ“ฅ Checkout repository uses: actions/checkout@v4 - name: ๐Ÿ”ง Validate HTML run: | echo "Basic HTML validation" test -f index.html build: name: Build & Push Docker Image runs-on: ubuntu-latest needs: lint steps: - name: ๐Ÿ“ฅ Checkout repository uses: actions/checkout@v4 - name: ๐Ÿ‹ Build Docker image run: | docker build \ -t $IMAGE_NAME:$BUILD_NUMBER \ -t $IMAGE_NAME:latest \ . - name: โ„น๏ธ Image info run: docker images | grep $IMAGE_NAME - name: ๐Ÿ”‘ Login to registry run: | echo "$REGISTRY_TOKEN" | docker login $REGISTRY_LOCATION \ -u "$REGISTRY_USER" --password-stdin - name: ๐Ÿท๏ธ Build images run: | docker tag $IMAGE_NAME:latest $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:latest docker tag $IMAGE_NAME:$BUILD_NUMBER $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:$BUILD_NUMBER - name: ๐Ÿš€ Push images run: | docker push $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:latest docker push $REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:$BUILD_NUMBER deploy: name: ๐Ÿš€ Deploy image to prod runs-on: ubuntu-latest needs: build steps: - name: ๐Ÿ”‘ Setup SSH run: | mkdir -p ~/.ssh echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H $PROD_SERVER_HOST >> ~/.ssh/known_hosts - name: ๐Ÿ—๏ธ Login to registry run: | echo "$REGISTRY_TOKEN" | docker login $REGISTRY_LOCATION \ -u "$REGISTRY_USER" --password-stdin - name: ๐Ÿ”Œ SSH into the prod server run: | ssh $DEPLOY_USER@$PROD_SERVER_HOST << 'EOF' sleep 5 - name: ๐Ÿš€ Deploy container to prod run: | set -e NEW_IMAGE_NAME="$REGISTRY_LOCATION/$REGISTRY_ORGANIZATION/$IMAGE_NAME:$BUILD_NUMBER" TEST_CONTAINER_NAME=test-$IMAGE_NAME OLD_IMAGE_NAME=$(docker inspect $IMAGE_NAME --format='{{.Config.Image}}') NEW_CONTAINER_IMAGE="$IMAGE_NAME:$BUILD_NUMBER" echo "๐Ÿ—๏ธ Building functions" # checks the health of the given container name $1 # returns "healthy" or "unhealthy" is_container_healthy() { for i in {1..15}; do RETURN=$(docker inspect --format='{{.State.Health.Status}}' $1) [ "$RETURN" = "healthy" ] && break [ "$RETURN" = "unhealthy" ] && break sleep 2 done echo "$RETURN" } echo "๐ŸŽฃ Pulling image $NEW_IMAGE_NAME" docker pull "$NEW_IMAGE_NAME" echo "๐Ÿงช Test the new container health" # if old test container is running then stop and remove it if docker ps -a --format '{{.Names}}' | grep "^$TEST_CONTAINER_NAME$"; then echo "Container ${TEST_CONTAINER_NAME} is running. Stopping and removing..." docker stop $TEST_CONTAINER_NAME docker rm $TEST_CONTAINER_NAME fi echo "Starting new test container" docker run -d \ --name $TEST_CONTAINER_NAME \ --health-cmd="curl -f http://$PROD_SERVER_HOST:$TEST_PORT/health || exit 1" \ --health-interval=5s \ --health-retries=5 \ --health-timeout=2s \ --health-start-period=5s \ -p $TEST_PORT:80 \ "$NEW_CONTAINER_IMAGE" TEST_CONTAINER_HEALTHCHECK=$(is_container_healthy $TEST_CONTAINER_NAME) docker stop $TEST_CONTAINER_NAME docker rm $TEST_CONTAINER_NAME echo "๐Ÿšš Deploy the new image if healthy, otherwise rollback" if [ "$TEST_CONTAINER_HEALTHCHECK" = "healthy" ]; then echo "โœ… Test Container is healthy - starting deployment" # Stop previous prod running container if found if [ -n "$OLD_IMAGE_NAME" ]; then echo "Stopping previous prod running container" docker stop $IMAGE_NAME docker rm $IMAGE_NAME fi docker run -d \ --name $IMAGE_NAME \ --health-cmd="curl -f http://$PROD_SERVER_HOST:$PORT/health || exit 1" \ --health-interval=5s \ --health-retries=5 \ --health-timeout=2s \ --health-start-period=5s \ -p $PORT:80 \ "$NEW_CONTAINER_IMAGE" PROD_CONTAINER_HEALTHCHECK=$(is_container_healthy $IMAGE_NAME) if [ "$PROD_CONTAINER_HEALTHCHECK" != "healthy" ]; then echo "โŒ Deployment failed โ€” rolling back" docker logs $IMAGE_NAME docker stop $IMAGE_NAME docker rm -f $IMAGE_NAME docker image rm $NEW_CONTAINER_IMAGE if [ -n "$OLD_IMAGE_NAME" ]; then docker run -d \ --name $IMAGE_NAME \ --health-cmd="curl -f http://$PROD_SERVER_HOST:$PORT/health || exit 1" \ --health-interval=5s \ --health-retries=5 \ --health-timeout=2s \ --health-start-period=5s \ -p $PORT:80 \ "$OLD_IMAGE_NAME" if [["$(is_container_healthy $IMAGE_NAME)" == "healthy" ]]; then echo "โŒ Deployment failed โ€” โœ… rollback successfull" else echo "โŒ Deployment failed โ€” โŒ rollback failed" fi exit 1 fi echo "โŒ Deployment failed โ€” โŒ rollback not possible (no previous version was running)" exit 1 fi if [ -n "$OLD_IMAGE_NAME" ]; then docker image rm $OLD_IMAGE_NAME fi echo "โœ… Deployment successfull ($IMAGE_NAME)" exit 0 else docker image rm $NEW_CONTAINER_IMAGE echo "โŒ Deployment failed โ€” โค๏ธ healthcheck failed on the test container" exit 1 fi