From acad2cc0caae653769eb1d5a750b1a078173e16a Mon Sep 17 00:00:00 2001 From: Timothy Pidashev Date: Thu, 9 Jan 2025 17:20:42 -0800 Subject: [PATCH] begin work on deployment process --- .docker/Dockerfile.dev | 2 +- .docker/Dockerfile.release | 29 ++++-- .github/scripts/deploy_release.sh | 83 ++++++++++++++++ Makefile | 152 +++++++++++++++++++++++------- compose.release.yml | 24 +++++ test | 0 6 files changed, 243 insertions(+), 47 deletions(-) create mode 100755 .github/scripts/deploy_release.sh delete mode 100644 test diff --git a/.docker/Dockerfile.dev b/.docker/Dockerfile.dev index cb697e5..fa64ac9 100644 --- a/.docker/Dockerfile.dev +++ b/.docker/Dockerfile.dev @@ -24,4 +24,4 @@ RUN echo "PUBLIC_VERSION=${CONTAINER_WEB_VERSION}" > /app/.env && \ EXPOSE 3000 -CMD pnpm install && pnpm run dev +CMD ["pnpm", "install", "&&", "pnpm", "run", "dev"] diff --git a/.docker/Dockerfile.release b/.docker/Dockerfile.release index aeeb6dd..47125b8 100644 --- a/.docker/Dockerfile.release +++ b/.docker/Dockerfile.release @@ -1,32 +1,41 @@ -from node:22-alpine +# Stage 1: Build and install dependencies +FROM node:22-alpine AS builder WORKDIR /app +# Install necessary dependencies, including pnpm RUN set -eux \ - & apk add \ - --no-cache \ - nodejs \ - curl + && apk add --no-cache nodejs curl \ + && npm install -g pnpm +# Copy package files COPY package.json pnpm-lock.yaml ./ +# Set build arguments ARG CONTAINER_WEB_VERSION ARG ENVIRONMENT ARG BUILD_DATE ARG GIT_COMMIT -RUN echo "PUBLIC_VERSION=${CONTAINER_FHCC_VERSION}" > /app/.env && \ +# Create .env file with build-time environment variables +RUN echo "PUBLIC_VERSION=${CONTAINER_WEB_VERSION}" > /app/.env && \ echo "PUBLIC_ENVIRONMENT=${ENVIRONMENT}" >> /app/.env && \ echo "PUBLIC_BUILD_DATE=${BUILD_DATE}" >> /app/.env && \ echo "PUBLIC_GIT_COMMIT=${GIT_COMMIT}" >> /app/.env -RUN pnpm install --frozen-lockfile --production -RUN pnpm run build +# Install dependencies (including development dependencies) and build the project +RUN pnpm install --frozen-lockfile && pnpm run build +# Stage 2: Set up the production environment FROM node:22-alpine + WORKDIR /app +# Expose the port for the application EXPOSE 3000 -CMD node ./dist/server/entry.mjs -COPY --from=builder /app/.dist ./ +# Copy the build artifacts from the builder stage +COPY --from=builder /app/dist ./dist + +# Set the command to run the application +CMD ["node", "./dist/server/entry.mjs"] diff --git a/.github/scripts/deploy_release.sh b/.github/scripts/deploy_release.sh new file mode 100755 index 0000000..746b7e0 --- /dev/null +++ b/.github/scripts/deploy_release.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +# Set variables +BRANCH_NAME="$1" +COMMIT_HASH="$2" +GHCR_USERNAME="$3" +GHCR_TOKEN="$4" +DEPLOY_TYPE="$5" +REPO_OWNER="$6" +COMPOSE_FILE="$7" +CADDYFILE="$8" +MAKEFILE="$9" + +# Echo out variable names and their content on single lines +echo "BRANCH_NAME: $BRANCH_NAME" +echo "COMMIT_HASH: $COMMIT_HASH" +echo "GHCR_USERNAME: $GHCR_USERNAME" +echo "DEPLOY_TYPE: $DEPLOY_TYPE" +echo "REPO_OWNER: $REPO_OWNER" +echo "COMPOSE_FILE: $COMPOSE_FILE" +echo "CADDYFILE: $CADDYFILE" +echo "MAKEFILE: $MAKEFILE" + +# Set the staging directory +STAGING_DIR="/root/deployments/.staging-${COMMIT_HASH}" + +# Set the tmux session name for release +TMUX_SESSION="deployment-release" + +# Function to cleanup existing release deployment +cleanup_release_deployment() { + echo "Cleaning up existing release deployment..." + # Stop and remove all release containers + docker-compose -f "/root/deployments/release/docker-compose.yml" down -v 2>/dev/null + # Remove release images + docker rmi $(docker images "ghcr.io/$REPO_OWNER/*:release" -q) 2>/dev/null + # Kill release tmux session if it exists + tmux kill-session -t "$TMUX_SESSION" 2>/dev/null + # Remove release deployment directory + rm -rf /root/deployments/release +} + +# Function to create deployment directory +create_deployment_directory() { + echo "Creating deployment directory..." + mkdir -p /root/deployments/release +} + +# Function to pull Docker images +pull_docker_images() { + echo "Pulling Docker images..." + docker pull ghcr.io/$REPO_OWNER/web:release +} + +# Function to copy and prepare files +copy_and_prepare_files() { + echo "Copying and preparing files..." + # Copy files preserving names and locations + install -D "$STAGING_DIR/$COMPOSE_FILE" "/root/deployments/release/$COMPOSE_FILE" + install -D "$STAGING_DIR/$CADDYFILE" "/root/deployments/release/$CADDYFILE" + install -D "$STAGING_DIR/$MAKEFILE" "/root/deployments/release/$MAKEFILE" + # Replace {$COMMIT_HASH} with $COMMIT_HASH in $CADDYFILE + sed -i "s/{\$COMMIT_HASH}/$COMMIT_HASH/g" "/root/deployments/release/$CADDYFILE" + # Replace {commit_hash} with $COMMIT_HASH in $COMPOSE_FILE + sed -i "s/{commit_hash}/$COMMIT_HASH/g" "/root/deployments/release/$COMPOSE_FILE" +} + +# Function to start the deployment +start_deployment() { + echo "Starting deployment..." + # Create new tmux session with specific name + tmux new-session -d -s "$TMUX_SESSION" + tmux send-keys -t "$TMUX_SESSION" "cd /root/deployments/release && make run release" Enter +} + +# Main execution +cleanup_release_deployment +create_deployment_directory +copy_and_prepare_files +cd "/root/deployments/release" +pull_docker_images +start_deployment +echo "Release build $COMMIT_HASH deployed successfully!" diff --git a/Makefile b/Makefile index 4abccdb..67c3099 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PROJECT_NAME := "web" +PROJECT_NAME := "timmypidashev.dev" PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) " PROJECT_VERSION := "v1.0.1" PROJECT_LICENSE := "MIT" @@ -7,51 +7,36 @@ PROJECT_REGISTRY := "ghcr.io/timmypidashev/web" PROJECT_ORGANIZATION := "org.opencontainers" CONTAINER_WEB_NAME := "web" -CONTAINER_WEB_VERSION := "v1.0.0" +CONTAINER_WEB_VERSION := "v1.0.1" CONTAINER_WEB_LOCATION := "src/" CONTAINER_WEB_DESCRIPTION := "My portfolio website!" .DEFAULT_GOAL := help -.PHONY: run build push prune bump -.SILENT: run build push prune bump +.PHONY: watch run build push prune bump exec +.SILENT: watch run build push prune bump exec help: @echo "Available targets:" - @echo " run - Runs the docker compose file with the specified environment (dev or release)" + @echo " run - Runs the docker compose file with the specified environment (local, dev, preview, or release)" @echo " build - Builds the specified docker image with the appropriate environment" @echo " push - Pushes the built docker image to the registry" + @echo " pull - Pulls the latest specified docker image from the registry" @echo " prune - Removes all built and cached docker images and containers" @echo " bump - Bumps the project and container versions" - -run: - # Arguments: - # [environment]: 'dev' or 'release' - # - # Explanation: - # * Runs the docker compose file with the specified environment(compose.dev.yml, or compose.release.yml) - # * Passes all generated arguments to the compose file. - - # Make sure we have been given proper arguments. - @if [ "$(word 2,$(MAKECMDGOALS))" = "dev" ]; then \ - echo "Running in development environment"; \ - elif [ "$(word 2,$(MAKECMDGOALS))" = "release" ]; then \ - echo "Running in release environment"; \ - else \ - echo "Invalid usage. Please use 'make run dev' or 'make run release'"; \ - exit 1; \ - fi - - docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --remove-orphans - + @echo " exec - Spawns a shell to execute commands from within a running container" build: # Arguments # [container]: Build context(which container to build ['all' to build every container defined]) - # [environment]: 'dev' or 'release' + # [environment]: 'local', 'dev', 'preview', or 'release' # # Explanation: # * Builds the specified docker image with the appropriate environment. # * Passes all generated arguments to docker build-kit. + # * Installs pre-commit hooks if in a git repository. + + # Install pre-commit hooks if in a git repository. + $(call install_precommit) # Extract container and environment inputted. $(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS))) @@ -64,6 +49,33 @@ build: $(foreach container,$(containers),$(call container_build,$(container) $(INPUT_ENVIRONMENT))), \ $(call container_build,$(INPUT_CONTAINER) $(INPUT_ENVIRONMENT))) +run: + # Arguments: + # [environment]: 'local', 'dev', 'preview', or 'release' + # + # Explanation: + # * Runs the docker compose file with the specified environment(compose.local.yml, compose.dev.yml, compose.preview.yml, or compose.release.yml) + # * Passes all generated arguments to the compose file. + # * Installs pre-commit hooks if in a git repository. + + # Install pre-commit hooks if in a git repository. + $(call install_precommit) + + # Make sure we have been given proper arguments. + @if [ "$(word 2,$(MAKECMDGOALS))" = "local" ]; then \ + echo "Running in local environment"; \ + docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --watch --remove-orphans; \ + elif [ "$(word 2,$(MAKECMDGOALS))" = "preview" ]; then \ + echo "Running in preview environment"; \ + docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --remove-orphans; \ + elif [ "$(word 2,$(MAKECMDGOALS))" = "release" ]; then \ + echo "Running in release environment"; \ + docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --remove-orphans; \ + else \ + echo "Invalid usage. Please use 'make run <'local', 'dev', 'preview', or 'release'>"; \ + exit 1; \ + fi + push: # Arguments # [container]: Push context(which container to push to the registry) @@ -81,7 +93,45 @@ push: # NOTE: docker will complain if the container tag is invalid, no need to validate here. @docker push $(PROJECT_REGISTRY)/$(INPUT_CONTAINER):$(INPUT_VERSION) +pull: + # TODO: FIX COMMAND PULL + # Arguments + # [container]: Pull context (which container to pull from the registry) + # [environment]: 'local', 'dev', 'preview', or 'release' + # + # Explanation: + # * Pulls the specified container version from the registry defined in the user configuration. + # * Uses sed and basename to extract the repository name. + + # Extract container and environment inputted. + $(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS))) + $(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET)))) + $(eval INPUT_ENVIRONMENT := $(lastword $(subst :, ,$(INPUT_TARGET)))) + + # Validate environment + @if [ "$(strip $(INPUT_ENVIRONMENT))" != "local" ] [ "$(strip $(INPUT_ENVIRONMENT))" != "dev" ] && [ "$(strip $(INPUT_ENVIRONMENT))" != "preview" ] && [ "$(strip $(INPUT_ENVIRONMENT))" != "release" ]; then \ + echo "Invalid environment. Please specify 'local', 'dev', 'preview', or 'release'"; \ + exit 1; \ + fi + + # Extract repository name from PROJECT_SOURCES using sed and basename + $(eval REPO_NAME := $(shell echo "$(PROJECT_SOURCES)" | sed 's|https://github.com/[^/]*/||' | sed 's/\.git$$//' | xargs basename)) + + # Determine the correct tag based on the environment and container + $(eval TAG := $(if $(filter $(INPUT_ENVIRONMENT),local),\ + $(INPUT_CONTAINER):$(INPUT_ENVIRONMENT),\ + $(if $(filter $(INPUT_CONTAINER),$(REPO_NAME)),\ + $(PROJECT_REGISTRY):$(if $(filter $(INPUT_ENVIRONMENT),prev),prev,$(call container_version,$(INPUT_CONTAINER))),\ + $(PROJECT_REGISTRY)/$(INPUT_CONTAINER):$(if $(filter $(INPUT_ENVIRONMENT),prev),prev,$(call container_version,$(INPUT_CONTAINER)))))) + + # Pull the specified container from the registry + @echo "Pulling container: $(INPUT_CONTAINER)" + @echo "Environment: $(INPUT_ENVIRONMENT)" + @echo "Tag: $(TAG)" + @docker pull $(TAG) + prune: + # TODO: IMPLEMENT COMMAND PRUNE # Removes all built and cached docker images and containers. bump: @@ -114,7 +164,22 @@ bump: # Bump the container version and global version in the Makefile perl -pi -e 's/^PROJECT_VERSION\s*:=\s*\K.*/"v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))"/ if /^PROJECT_VERSION\s*:=/' Makefile; perl -pi -e 's/^CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION\s*:=\s*\K.*/"v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_CONTAINER_VERSION))"/ if /^CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION\s*:=/' Makefile; + + # Commit and push to git origin + git add . + git commit -a -S -m "Bump $(INPUT_CONTAINER) to v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))" + git push + git push origin tag v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION)) + +exec: + # Extract container and environment inputted. + $(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS))) + $(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET)))) + $(eval INPUT_ENVIRONMENT := $(lastword $(subst :, ,$(INPUT_TARGET)))) + $(eval COMPOSE_FILE := compose.$(INPUT_ENVIRONMENT).yml) + docker compose -f $(COMPOSE_FILE) run --service-ports $(INPUT_CONTAINER) sh + # This function generates Docker build arguments based on variables defined in the Makefile. # It extracts variable assignments, removes whitespace, and formats them as build arguments. # Additionally, it appends any custom shell generated arguments defined below. @@ -129,6 +194,7 @@ define args gsub(":", "", $$1); \ printf "--build-arg %s=%s ", $$1, $$2 \ }') \ + --build-arg ENVIRONMENT='"$(shell echo $(INPUT_ENVIRONMENT))"' \ --build-arg BUILD_DATE='"$(shell date)"' \ --build-arg GIT_COMMIT='"$(shell git rev-parse HEAD)"' endef @@ -155,22 +221,19 @@ define container_build $(eval ENVIRONMENT := $(word 2,$1)) $(eval ARGS := $(shell echo $(args))) $(eval VERSION := $(strip $(call container_version,$(CONTAINER)))) - $(eval TAG := $(CONTAINER):$(ENVIRONMENT)) + $(eval PROJECT := $(strip $(subst ",,$(PROJECT_NAME)))) + $(eval TAG := $(PROJECT).$(CONTAINER):$(ENVIRONMENT)) @echo "Building container: $(CONTAINER)" @echo "Environment: $(ENVIRONMENT)" @echo "Version: $(VERSION)" - @if [ "$(strip $(ENVIRONMENT))" != "dev" ] && [ "$(strip $(ENVIRONMENT))" != "release" ]; then \ - echo "Invalid environment. Please specify 'dev' or 'release'"; \ + @if [ "$(strip $(ENVIRONMENT))" != "local" ] && [ "$(strip $(ENVIRONMENT))" != "dev" ] && [ "$(strip $(ENVIRONMENT))" != "preview" ] && [ "$(strip $(ENVIRONMENT))" != "release" ]; then \ + echo "Invalid environment. Please specify 'local', 'dev', 'preview', or 'release'"; \ exit 1; \ fi - - $(if $(filter $(strip $(ENVIRONMENT)),release), \ - $(eval TAG := $(PROJECT_REGISTRY)/$(CONTAINER):$(VERSION)), \ - ) - - docker buildx build --load -t $(TAG) -f .docker/Dockerfile.$(ENVIRONMENT) ./$(strip $(subst $(SPACE),,$(call container_location,$(CONTAINER))))/. $(ARGS) $(call labels,$(shell echo $(CONTAINER_NAME) | tr '[:lower:]' '[:upper:]')) --no-cache + + docker buildx build --no-cache --load -t $(TAG) -f .docker/Dockerfile.$(ENVIRONMENT) ./$(strip $(subst $(SPACE),,$(call container_location,$(CONTAINER))))/. $(ARGS) $(call labels,$(shell echo $(CONTAINER_NAME) | tr '[:lower:]' '[:upper:]')) --debug endef define container_location @@ -178,9 +241,26 @@ define container_location $(CONTAINER_$(CONTAINER_NAME)_LOCATION) endef +define container_name + $(strip $(shell echo '$(1)' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')) +endef + define container_version $(strip $(eval CONTAINER_NAME := $(shell echo $(1) | tr '[:lower:]' '[:upper:]'))) \ $(if $(CONTAINER_$(CONTAINER_NAME)_VERSION), \ $(shell echo $(strip $(strip $(CONTAINER_$(CONTAINER_NAME)_VERSION))) | tr -d '[:space:]'), \ $(error Version data for container $(1) not found)) endef + +define install_precommit + $(strip \ + $(shell \ + if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then \ + pre-commit install > /dev/null 2>&1; \ + fi \ + ) \ + ) +endef + +%: + @: diff --git a/compose.release.yml b/compose.release.yml index e69de29..064d7bc 100644 --- a/compose.release.yml +++ b/compose.release.yml @@ -0,0 +1,24 @@ +services: + caddy: + container_name: proxy + image: caddy:latest + ports: + - 80:80 + - 443:443 + volumes: + - ./.caddy/Caddyfile.dev:/etc/caddy/Caddyfile:rw + networks: + - proxy_network + depends_on: + - release.timmypidashev.dev + + release.timmypidashev.dev: + container_name: timmypidashev + image: ghcr.io/timmypidashev/timmypidashev.dev:release + networks: + - proxy_network + +networks: + proxy_network: + name: proxy_network + external: true diff --git a/test b/test deleted file mode 100644 index e69de29..0000000