name: Build & Deploy on: workflow_dispatch: push: branches: [main] pull_request: branches: [main] jobs: build-deploy: runs-on: ubuntu-latest steps: - name: Checkout the repo uses: actions/checkout@v4 - name: ๐Ÿ— Set up yq uses: frenck/action-setup-yq@v1 - name: Get full and partial repository name id: meta run: | echo "GITHUB_REPOSITORY: ${{ github.repository }}" repo_temp="" if [[ "${{ github.repository }}" == git@* && "${{ github.repository }}" == *:* && "${{ github.repository }}" == *.git ]]; then echo "Detected SSH style git remote (e.g. git@host:owner/repo.git)" # Extracts 'owner/repo' from 'git@host:owner/repo.git' repo_temp=$(echo "${{ github.repository }}" | sed 's/\.git$//' | awk -F: '{print $2}') elif [[ "${{ github.repository }}" == "https://"* && "${{ github.repository }}" == *.git ]]; then echo "Detected HTTPS URL ending in .git (e.g. https://host/owner/repo.git)" # Extracts 'owner/repo' from 'https://host/owner/repo.git' repo_temp=$(echo "${{ github.repository }}" | sed 's/\.git$//' | awk -F/ '{print $(NF-1)"/"$(NF)}') elif [[ "${{ github.repository }}" == *.git ]]; then # Catches other cases ending in .git, like owner/repo.git or other path based .git repos echo "Detected path-like git repo ending in .git (e.g. owner/repo.git)" # Extracts 'owner/repo' from 'owner/repo.git' repo_temp=$(echo "${{ github.repository }}" | sed 's/\.git$//') else echo "Assuming owner/repo format (e.g. owner/repo)" repo_temp="${{ github.repository }}" fi # Sanitize and lowercase for Docker image naming conventions REPO=$(echo "$repo_temp" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9\/-]/-/g' -e 's/--\+/-/g' -e 's/^-//g' -e 's/-$//g') echo "REPO (image base name): $REPO" echo "REPO=$REPO" >> $GITHUB_OUTPUT REPO_NAME_ONLY=$(echo "$REPO" | cut -d'/' -f2) echo "REPO_NAME_ONLY (project name): $REPO_NAME_ONLY" echo "REPO_NAME_ONLY=$REPO_NAME_ONLY" >> $GITHUB_OUTPUT REPO_PROJECT_PATH=/srv/$(echo "$REPO_NAME_ONLY") echo "REPO_PROJECT_PATH: $REPO_PROJECT_PATH" echo "REPO_PROJECT_PATH=$REPO_PROJECT_PATH" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Set PROD environment variable run: | echo "โœ… Exit script on any error" set -eu -o pipefail prod_value="" echo "๐Ÿ” Check if PROD is set via vars; if not, determine from github.ref" if [ -z "${{ vars.PROD }}" ]; then prod_value="${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-prerelease') }}" echo "๐Ÿ“ฆ PROD mode unset, determined from github.ref (starts with v and does not end with -prerelease?): ${prod_value}" else prod_value="${{ vars.PROD }}" echo "๐Ÿ“ฆ PROD mode already set to: ${prod_value}" fi echo "๐Ÿ–Š๏ธ Writing determined values to GITHUB_ENV:" echo "PROD=${prod_value}" >> $GITHUB_ENV echo "PROD=${prod_value} -> GITHUB_ENV" - name: Generate tags id: generated_docker_tags run: | echo "โœ… Exit script on any error" set -eu -o pipefail # echo current shell echo "Current shell: $SHELL" IMAGE_BASE_NAME="${{ steps.meta.outputs.REPO }}" GIT_SHA="${{ github.sha }}" GIT_REF="${{ github.ref }}" echo "Inputs for tagging:" echo "IMAGE_BASE_NAME: $IMAGE_BASE_NAME" echo "GIT_SHA: $GIT_SHA" echo "GIT_REF: $GIT_REF" echo "PROD status for tagging: ${PROD}" TAG_LIST=() VERSION_TAG_LIST=() if [[ -n "$GIT_SHA" ]]; then SHORT_SHA=$(echo "$GIT_SHA" | cut -c1-7) TAG_LIST+=("${IMAGE_BASE_NAME}:sha-${SHORT_SHA}") TAG_LIST+=("${IMAGE_BASE_NAME}:sha-${GIT_SHA}") else echo "๐Ÿ”ด No Git SHA found, cannot generate tags. Aborting." exit 1 fi GIT_TAG_VERSION="" # Extract version only if GIT_REF is a tag like refs/tags/vX.Y.Z or refs/tags/vX.Y.Z-prerelease if [[ "$GIT_REF" == refs/tags/v* ]]; then GIT_TAG_VERSION=$(echo "$GIT_REF" | sed 's%refs/tags/v%%' | sed 's%-prerelease$%%') if [[ "$GIT_TAG_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then echo "Detected Git tag: v$GIT_TAG_VERSION" MAJOR=${BASH_REMATCH[1]} MINOR=${BASH_REMATCH[2]} PATCH=${BASH_REMATCH[3]} echo "Parsed version: Major=$MAJOR, Minor=$MINOR, Patch=$PATCH from v$GIT_TAG_VERSION" if [ "$MAJOR" -gt 0 ]; then VERSION_TAG_LIST+=("${IMAGE_BASE_NAME}:v${MAJOR}") else echo "โ„น๏ธ Major version is 0 (v$GIT_TAG_VERSION). Skipping MAJOR-only tag v0. Please reference by MAJOR.MINOR or MAJOR.MINOR.PATCH." fi VERSION_TAG_LIST+=("${IMAGE_BASE_NAME}:v${MAJOR}.${MINOR}") VERSION_TAG_LIST+=("${IMAGE_BASE_NAME}:v${MAJOR}.${MINOR}.${PATCH}") else echo "โš ๏ธ Git tag 'v$GIT_TAG_VERSION' is not a valid semantic version (x.y.z) but should be. Aborting." exit 1 fi fi if [ "$PROD" = "true" ]; then echo "๐Ÿ“ฆ Generating PROD tags." TAG_LIST+=("${IMAGE_BASE_NAME}:stable") TAG_LIST+=("${IMAGE_BASE_NAME}:latest") if [ ${#VERSION_TAG_LIST[@]} -gt 0 ]; then TAG_LIST+=("${VERSION_TAG_LIST[@]}") else echo "๐Ÿ”ด PROD mode is true, but Git ref ($GIT_REF) is not a valid version tag. This is unexpected, aborting." exit 1 fi else # Non-PROD echo "๐Ÿ”จ Generating STAGING tags." TAG_LIST+=("${IMAGE_BASE_NAME}:staging") TAG_LIST+=("${IMAGE_BASE_NAME}:latest-staging") if [ ${#VERSION_TAG_LIST[@]} -gt 0 ]; then echo "๐Ÿ”จ This is also a prerelease version, generating version tags with '-prerelease' suffix." VERSION_TAG_LIST=("${VERSION_TAG_LIST[@]/%/-prerelease}") TAG_LIST+=("${VERSION_TAG_LIST[@]}") else echo "โ„น๏ธ Git ref ($GIT_REF) is not a valid version tag. Skipping versioned -prerelease tag generation." fi fi echo "Final list of generated tags:" printf "%s\n" "${TAG_LIST[@]}" if [[ -z "$GIT_SHA" || ${#TAG_LIST[@]} -lt 4 ]]; then echo "โš ๏ธ No tags (or too few) were generated based on the logic. Need at least 4 tags: Git commit short and full length SHA tags, a latest/latest-staging tag, and a stable/staging tag. This is unexpected, aborting." exit 1 fi # Output the tags for the docker build action (output name is 'tag') { echo "tag<> "$GITHUB_OUTPUT" - name: Run prebuild tasks run: | echo "๐Ÿ”„ Chdir to src/pkmntrade_club/theme/static_src" cd src/pkmntrade_club/theme/static_src echo "๐Ÿ“ฆ Install npm dependencies" npm install . echo "๐Ÿ”จ Build the tailwind theme css" npm run build # - name: Cache Docker layers # uses: actions/cache@v4 # with: # path: ${{ runner.temp }}/.cache # key: ${{ runner.os }}-${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}-${{ github.sha }} # restore-keys: | # ${{ runner.os }}-${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}- # - name: Inject buildx-cache # uses: reproducible-containers/buildkit-cache-dance@4b2444fec0c0fb9dbf175a96c094720a692ef810 # v2.1.4 # with: # cache-source: ${{ runner.temp }}/.cache/buildx-cache - name: Build container id: build_container uses: docker/build-push-action@v6 with: outputs: type=docker,dest=${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar tags: ${{ steps.generated_docker_tags.outputs.tag }} build-args: CACHE_DIR=${{ runner.temp }}/.cache/dockerfile-cache context: . #cache-from: type=local,src=${{ runner.temp }}/.cache/buildx-cache #cache-to: type=local,src=${{ runner.temp }}/.cache/buildx-cache-new,mode=max # - name: Rotate cache # along with cache-from & cache-to: prevents cache from growing indefinitely # run: | # rm -rf ${{ runner.temp }}/.cache/buildx-cache # mv ${{ runner.temp }}/.cache/buildx-cache-new ${{ runner.temp }}/.cache/buildx-cache # - name: Upload container as artifact # uses: actions/upload-artifact@v4 # with: # name: ${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar # path: ${{ runner.temp }}/${{ steps.meta.outputs.REPO_NAME_ONLY }}-${{ github.ref_name }}_${{ github.sha }}.tar # if-no-files-found: error # compression-level: 0 - name: Update docker compose and "pin" just built image by adding image digest to tag env: IMAGE_DIGEST: ${{ steps.build_container.outputs.DIGEST }} IMAGE_BASE_NAME: ${{ steps.meta.outputs.REPO }} run: | set -eu -o pipefail echo "PROD_STATUS: ${PROD}" echo "IMAGE_DIGEST: ${IMAGE_DIGEST}" echo "IMAGE_BASE_NAME (for compose update): ${IMAGE_BASE_NAME}" TARGET_COMPOSE_FILE="" SERVICE_YQ_PATH="" IMAGE_TAG="" echo "Checking yq version..." yq --version if [ "${PROD}" = "true" ]; then TARGET_COMPOSE_FILE="./server/docker-compose_web.yml" SERVICE1_YQ_PATH=".services.web" SERVICE2_YQ_PATH=".services.celery" IMAGE_TAG="stable" echo "Updating PROD configuration in $TARGET_COMPOSE_FILE for service $SERVICE_YQ_PATH" yq -i "${SERVICE1_YQ_PATH}.image = \"${IMAGE_BASE_NAME}:${IMAGE_TAG}@${IMAGE_DIGEST}\"" "$TARGET_COMPOSE_FILE" yq -i "${SERVICE2_YQ_PATH}.image = \"${IMAGE_BASE_NAME}:${IMAGE_TAG}@${IMAGE_DIGEST}\"" "$TARGET_COMPOSE_FILE" else TARGET_COMPOSE_FILE="./server/docker-compose_staging.yml" SERVICE1_YQ_PATH=".services.\"web-staging\"" SERVICE2_YQ_PATH=".services.\"celery-staging\"" IMAGE_TAG="staging" echo "Updating STAGING configuration in $TARGET_COMPOSE_FILE for service $SERVICE_YQ_PATH" yq -i "${SERVICE1_YQ_PATH}.image = \"${IMAGE_BASE_NAME}:${IMAGE_TAG}@${IMAGE_DIGEST}\"" "$TARGET_COMPOSE_FILE" yq -i "${SERVICE2_YQ_PATH}.image = \"${IMAGE_BASE_NAME}:${IMAGE_TAG}@${IMAGE_DIGEST}\"" "$TARGET_COMPOSE_FILE" fi echo "Successfully updated $TARGET_COMPOSE_FILE. Image pinned to:" yq "${SERVICE_YQ_PATH}.image" "$TARGET_COMPOSE_FILE" - name: Get Deploy Secrets uses: bitwarden/sm-action@v2 with: access_token: ${{ secrets.BW_ACCESS_TOKEN }} secrets: | 27a8da8d-5fb4-4f58-baaf-b2dd0032eca8 > DEPLOY_HOST 4cf9ab8d-1772-4ab0-9219-b2dd003315d4 > DEPLOY_KEY 9aefe34e-c2cf-442e-973c-b2dd0032b6cf > ENV_FILE_BASE64 d3bb47f8-bfc0-4a61-9cee-b2df0147a02a > CF_PEM_CERT 5f658ddf-aadd-4464-b501-b2df0147c338 > CF_PEM_CA - name: Set up SSH run: | mkdir -p $HOME/.ssh echo -e "${DEPLOY_KEY}" > $HOME/.ssh/deploy.key chmod 700 $HOME/.ssh chmod 600 $HOME/.ssh/deploy.key cat >>$HOME/.ssh/config < ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/.env && chmod 600 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/.env" echo "๐Ÿ”‘ Set up certificates" ssh deploy "mkdir -p ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs && chmod 550 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs && chown 99:root ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs" printf "%s" "$CF_PEM_CERT" | ssh deploy "cat > ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/crt.pem && chmod 440 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/crt.pem && chown 99:root ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/crt.pem" printf "%s" "$CF_PEM_CA" | ssh deploy "cat > ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/ca.pem && chmod 440 ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/ca.pem && chown 99:root ${{ steps.meta.outputs.REPO_PROJECT_PATH}}/certs/ca.pem" echo "๐Ÿš€ Start the new containers" if [ "${PROD}" = true ]; then ssh deploy "cd ${{ steps.meta.outputs.REPO_PROJECT_PATH }} && docker compose -f docker-compose_core.yml -f docker-compose_web.yml up -d --no-build" else ssh deploy "cd ${{ steps.meta.outputs.REPO_PROJECT_PATH }} && docker compose -f docker-compose_core.yml -f docker-compose_staging.yml up -d --no-build" fi # echo "๐Ÿš€ Start the new containers, zero-downtime" # if [ "${PROD}" = true ]; then # ssh deploy <<