feat: add in copy of updated shared deployment workflows for now until they are
Some checks failed
Build/Deploy / trigger-dev-build (push) Has been cancelled
Build/Deploy / trigger-staging-build (push) Has been cancelled
Build/Deploy / trigger-main-build (push) Has been cancelled

published
This commit is contained in:
badblocks 2025-08-14 15:08:23 -07:00
parent bcb7f86b7f
commit e245bcbe96
No known key found for this signature in database
12 changed files with 1113 additions and 0 deletions

View file

@ -0,0 +1,45 @@
name: "Determine Build Metadata"
description: "Extract repository metadata and build configuration from GitHub context"
author: "Portfolio CI/CD"
inputs:
repository:
description: "Repository name in format owner/repo"
required: true
github-ref:
description: "GitHub ref (e.g., refs/heads/main, refs/tags/v1.0.0)"
required: true
outputs:
repo-name:
description: "Repository name only (without owner)"
value: ${{ steps.meta.outputs.repo-name }}
repo-path:
description: "Server deployment path"
value: ${{ steps.meta.outputs.repo-path }}
prod:
description: "Whether this is a production build (true/false)"
value: ${{ steps.meta.outputs.prod }}
runs:
using: "composite"
steps:
- name: Extract metadata
id: meta
shell: bash
run: |
set -eu -o pipefail
REPO="${{ inputs.repository }}"
REPO_NAME_ONLY="$(echo "${REPO}" | cut -d'/' -f2)"
REPO_PROJECT_PATH="/srv/${REPO_NAME_ONLY}"
PROD="${{ startsWith(inputs.github-ref, 'refs/heads/main') }}"
echo "🖊️ Writing determined values:"
echo "repo-name=$REPO_NAME_ONLY" >> $GITHUB_OUTPUT
echo "repo-name -> $REPO_NAME_ONLY"
echo "repo-path=$REPO_PROJECT_PATH" >> $GITHUB_OUTPUT
echo "repo-path -> $REPO_PROJECT_PATH"
echo "prod=$PROD" >> $GITHUB_OUTPUT
echo "prod -> $PROD"

View file

@ -0,0 +1,57 @@
name: "Push to Git Origin"
description: "Push branches and tags to Git origin repository"
author: "Portfolio CI/CD"
inputs:
git-origin-url:
description: "Git origin repository URL (e.g., git@example.com:user/repo.git)"
required: true
branches:
description: 'Space-separated list of branches to push (e.g., "main staging dev" or "staging")'
required: true
push-tags:
description: "Whether to push tags along with branches"
required: false
default: "false"
force:
description: "Whether to force push"
required: false
default: "false"
runs:
using: "composite"
steps:
- name: Setup Git Remotes
shell: bash
run: |
echo "🔧 Setting up git remotes..."
git remote remove git-origin 2>/dev/null || true
git remote add git-origin ${{ inputs.git-origin-url }}
echo "✅ git-origin remote configured"
- name: Push to Git Origin
shell: bash
run: |
echo "📤 Pushing to Git Origin..."
PUSH_ARGS="${{ inputs.branches }}"
if [[ "${{ inputs.push-tags }}" == "true" ]]; then
PUSH_ARGS="$PUSH_ARGS --tags"
echo "🏷️ Including tags in Git origin push"
fi
if [[ "${{ inputs.force }}" == "true" ]]; then
PUSH_ARGS="$PUSH_ARGS --force"
echo "🔥 Forcing push to Git origin"
fi
if git push git-origin $PUSH_ARGS; then
echo "✅ Successfully pushed to Git origin"
else
echo "❌ Failed to push to Git origin"
echo "::warning::Push to Git origin failed, but GitHub push succeeded"
exit 1
fi

View file

@ -0,0 +1,78 @@
name: 'Setup Environment'
description: 'Create environment files and variables for deployment'
author: 'Portfolio CI/CD'
inputs:
release-type:
description: 'Release type (staging or prod)'
required: true
domain:
description: 'Domain name'
required: true
android-sms-gateway-url:
description: 'Android SMS Gateway URL'
required: true
android-sms-gateway-login:
description: 'Android SMS Gateway login'
required: true
android-sms-gateway-password:
description: 'Android SMS Gateway password'
required: true
my-phone-number:
description: 'Phone number for SMS delivery'
required: true
super-secret-salt:
description: 'Secret salt for TOTP generation'
required: true
wireguard-allowed-ips:
description: 'WireGuard allowed IPs'
required: true
wireguard-private-key:
description: 'WireGuard private key'
required: true
wireguard-addresses:
description: 'WireGuard addresses'
required: true
wireguard-public-key:
description: 'WireGuard public key'
required: true
wireguard-endpoint-host:
description: 'WireGuard endpoint host'
required: true
wireguard-endpoint-port:
description: 'WireGuard endpoint port'
required: true
prod:
description: 'Whether this is a production deployment (true/false)'
required: false
default: 'false'
outputs:
env-file:
description: 'Path to the created .env file'
value: '.env'
runs:
using: 'composite'
steps:
- name: Create .env from secrets and environment variables
shell: bash
run: |
echo "🔧 Creating environment configuration..."
echo "DOMAIN=\"${{ inputs.domain }}\"" > .env
echo "NUXT_ANDROID_SMS_GATEWAY_URL=\"${{ inputs.android-sms-gateway-url }}\"" >> .env
echo "NUXT_ANDROID_SMS_GATEWAY_LOGIN=\"${{ inputs.android-sms-gateway-login }}\"" >> .env
echo "NUXT_ANDROID_SMS_GATEWAY_PASSWORD=\"${{ inputs.android-sms-gateway-password }}\"" >> .env
echo "NUXT_MY_PHONE_NUMBER=\"${{ inputs.my-phone-number }}\"" >> .env
echo "NUXT_SUPER_SECRET_SALT=\"${{ inputs.super-secret-salt }}\"" >> .env
echo "WIREGUARD_ALLOWED_IPS=\"${{ inputs.wireguard-allowed-ips }}\"" >> .env
echo "WIREGUARD_PRIVATE_KEY=\"${{ inputs.wireguard-private-key }}\"" >> .env
echo "WIREGUARD_ADDRESSES=\"${{ inputs.wireguard-addresses }}\"" >> .env
echo "WIREGUARD_PUBLIC_KEY=\"${{ inputs.wireguard-public-key }}\"" >> .env
echo "WIREGUARD_ENDPOINT_HOST=\"${{ inputs.wireguard-endpoint-host }}\"" >> .env
echo "WIREGUARD_ENDPOINT_PORT=\"${{ inputs.wireguard-endpoint-port }}\"" >> .env
echo "RELEASE_TYPE=\"${{ inputs.release-type }}\"" >> .env
echo "PROD=\"${{ inputs.prod }}\"" >> .env
echo "✅ Environment file created with $(wc -l < .env) variables"

View file

@ -0,0 +1,52 @@
name: "Setup Git SSH"
description: "Configure SSH key and Git signing for commits and pushes"
author: "Portfolio CI/CD"
inputs:
commit-ssh-private-key:
description: "SSH private key for commit signing"
required: false
push-ssh-private-key:
description: "SSH private key for git-origin operations"
required: false
actor:
description: "GitHub actor name for git user configuration"
default: ""
required: false
actor-id:
description: "GitHub actor ID for git email configuration"
default: ""
required: false
runs:
using: "composite"
steps:
- name: Setup git user.name and user.email
shell: bash
run: |
git config user.name "${{ inputs.actor != '' && inputs.actor || github.actor }}"
git config user.email "${{ inputs.actor-id != '' && inputs.actor-id || github.actor_id }}+${{ inputs.actor != '' && inputs.actor || github.actor }}@users.noreply.github.com"
- name: Setup SSH for key-based push
shell: bash
if: ${{ inputs.push-ssh-private-key != '' }}
run: |
mkdir -p ~/.ssh
echo "${{ inputs.push-ssh-private-key }}" > ~/.ssh/id-push
chmod 600 ~/.ssh/id-push
cat > ~/.ssh/config <<EOF
Host *
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
IdentityFile $HOME/.ssh/id-push
LogLevel QUIET
EOF
- name: Setup git for commit signing
shell: bash
if: ${{ inputs.commit-ssh-private-key != '' }}
run: |
mkdir -p ~/.ssh
echo "${{ inputs.commit-ssh-private-key }}" > ~/.ssh/id-commit
chmod 600 ~/.ssh/id-commit
git config gpg.format ssh
git config user.signingkey ~/.ssh/id-commit
git config commit.gpgsign true

View file

@ -0,0 +1,140 @@
name: "Validate Build Status"
description: "Validate that a build workflow has completed successfully for a specific branch and commit"
author: "Portfolio CI/CD"
inputs:
from-branch:
description: "Name of the branch to check (e.g., dev, staging)"
required: true
to-branch:
description: "Base branch to compare against (e.g., staging, main)"
required: true
job-name:
description: "Name of the build job to check"
required: true
workflow-name:
description: "Name of the workflow to check"
required: false
default: "Build/Deploy"
force:
description: "Force validation even if other conditions fail"
required: false
default: "false"
outputs:
should-proceed:
description: "Whether the validation passed and should proceed"
value: ${{ steps.validation.outputs.should_proceed }}
commits-ahead:
description: "Number of commits ahead of comparison branch"
value: ${{ steps.validation.outputs.commits_ahead }}
runs:
using: "composite"
steps:
- name: Run validation checks
id: validation
shell: bash
run: |
echo "🔍 Running build validation checks for ${{ inputs.from-branch }}..."
# Check if there are new commits in branch since last comparison
git checkout ${{ inputs.from-branch }}
# git fetch --tags
# git pull --tags
COMMITS_AHEAD=$(git rev-list --count origin/${{ inputs.to-branch }}..${{ inputs.from-branch }})
echo "📊 Commits ahead of ${{ inputs.to-branch }}: $COMMITS_AHEAD"
echo "commits_ahead=$COMMITS_AHEAD" >> $GITHUB_OUTPUT
if [[ "${{ inputs.force }}" == "true" ]]; then
echo "⚡ Force flag enabled - skipping all validation checks"
echo "should_proceed=true" >> $GITHUB_OUTPUT
exit 0
fi
if [ "$COMMITS_AHEAD" -eq 0 ]; then
echo " No new commits to process"
echo "should_proceed=false" >> $GITHUB_OUTPUT
exit 0
fi
# Check for any [skip ci] in recent commit messages
SKIP_CI=$(git log --oneline origin/${{ inputs.to-branch }}..${{ inputs.from-branch }} | grep -i "\[skip ci\]" || true)
if [ -n "$SKIP_CI" ]; then
echo "⏭️ Skipping processing due to [skip ci] flag in commits"
echo "should_proceed=false" >> $GITHUB_OUTPUT
exit 0
fi
# Get the actual branch HEAD commit
COMMIT_SHA=$(git rev-parse ${{ inputs.from-branch }})
echo "🔍 Checking build job status for ${{ inputs.from-branch }} commit: $COMMIT_SHA"
# Get workflow runs for the specific commit
echo "🔍 Querying GitHub API for workflow runs..."
RUNS_RESPONSE=$(curl -s -H "Authorization: Bearer ${{ github.token }}" \
-H "Accept: application/vnd.github.v3+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${{ github.repository }}/actions/runs?head_sha=${COMMIT_SHA}")
# Check if API call was successful
if [ $? -ne 0 ]; then
echo "❌ Failed to call GitHub API"
echo "::error::GitHub API call failed"
exit 1
fi
# Check if response contains total_count
WORKFLOW_RUNS_COUNT=$(echo "$RUNS_RESPONSE" | jq -r '.total_count' 2>/dev/null || echo "error")
if [ "$WORKFLOW_RUNS_COUNT" = "error" ] || [ "$WORKFLOW_RUNS_COUNT" = "null" ]; then
echo "❌ Invalid API response format"
echo "API Response: $RUNS_RESPONSE"
echo "::error::Invalid response from GitHub API"
exit 1
fi
echo "📊 Found $WORKFLOW_RUNS_COUNT workflow run(s) for commit $COMMIT_SHA"
# Extract run ID for build_deploy.yml workflow (safely handle empty results)
BUILD_RUN_ID=$(echo "$RUNS_RESPONSE" | jq -r '.workflow_runs[] | select(.name == "${{ inputs.workflow-name }}") | .id' 2>/dev/null | head -1)
# Remove any whitespace/newlines
BUILD_RUN_ID=$(echo "$BUILD_RUN_ID" | tr -d '\n\r ')
if [ -z "$BUILD_RUN_ID" ] || [ "$BUILD_RUN_ID" = "null" ]; then
echo "❌ No build workflow run found for commit ${COMMIT_SHA}"
echo "::error::Expected build workflow run not found - this indicates a problem with the CI/CD pipeline"
exit 1
fi
echo "✅ Found build workflow run: $BUILD_RUN_ID"
# Get jobs for the workflow run
JOBS_RESPONSE=$(curl -s -H "Authorization: Bearer ${{ github.token }}" \
-H "Accept: application/vnd.github.v3+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${BUILD_RUN_ID}/jobs")
# Find the build job status
BUILD_JOB_STATUS=$(echo "$JOBS_RESPONSE" | jq -r '.jobs[] | select(.name == "${{ inputs.job-name }}") | .status')
BUILD_JOB_CONCLUSION=$(echo "$JOBS_RESPONSE" | jq -r '.jobs[] | select(.name == "${{ inputs.job-name }}") | .conclusion')
echo "📊 Build job status: $BUILD_JOB_STATUS, conclusion: $BUILD_JOB_CONCLUSION"
if [ "$BUILD_JOB_STATUS" = "completed" ] && ([ "$BUILD_JOB_CONCLUSION" = "success" ] || [ "$BUILD_JOB_CONCLUSION" = "skipped" ]); then
if [ "$BUILD_JOB_CONCLUSION" = "skipped" ]; then
echo "✅ Build job skipped (artifact already exists)!"
else
echo "✅ Build job completed successfully!"
fi
else
echo "❌ Build job not successful (status: $BUILD_JOB_STATUS, conclusion: $BUILD_JOB_CONCLUSION)"
echo "🔗 View job details: https://github.com/${{ github.repository }}/actions/runs/${BUILD_RUN_ID}"
echo "If job is in_progress, wait for it to complete, then re-run this workflow"
echo "should_proceed=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "✅ No blockers, ready to proceed"
echo "should_proceed=true" >> $GITHUB_OUTPUT

240
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,240 @@
# yaml-language-server: $schema=https://json-schema.org/draft-07/schema#
name: _build
concurrency:
group: build
cancel-in-progress: false
on:
workflow_call:
jobs:
check:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
should-build: ${{ steps.check.outputs.should-build }}
artifact-run-id: ${{ steps.check.outputs.artifact-run-id }}
artifact-id: ${{ steps.check.outputs.artifact-id }}
build-sha: ${{ steps.check.outputs.build-sha }}
repo-name: ${{ steps.meta.outputs.repo-name }}
repo-path: ${{ steps.meta.outputs.repo-path }}
image-tar: ${{ steps.check.outputs.image-tar }}
prod: ${{ steps.meta.outputs.prod }}
tag: ${{ steps.check.outputs.tag }}
should-deploy: ${{ steps.check.outputs.should-deploy }}
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine build metadata
id: meta
uses: ./.github/actions/determine-build-metadata
with:
repository: ${{ github.repository }}
github-ref: ${{ github.ref }}
- name: Check if build/deploy needed
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eu -o pipefail
SHOULD_BUILD=false
SHOULD_DEPLOY=false
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
IMAGE_TAR="${{ steps.meta.outputs.repo-name }}-${{github.sha}}.tar"
VERSION="$(git tag --points-at ${{ github.sha }})"
IS_VERSION_TAGGED="$([[ "$VERSION" =~ ^v ]] && echo "true" || echo "false")"
BRANCH_NAME="$([[ "${{ github.ref }}" =~ ^refs/tags/v ]] && echo "main" || echo "${{ github.ref }}" | sed 's/refs\/heads\///')"
COMMIT_MESSAGE="$(git log --format=%s -n 1 ${{ github.sha }})"
if [[ "${{ steps.meta.outputs.prod }}" == "true" ]]; then
TAG="${{ steps.meta.outputs.repo-name }}:sha-${SHORT_SHA},${{ steps.meta.outputs.repo-name }}:latest"
else
TAG="${{ steps.meta.outputs.repo-name }}-dev:sha-${SHORT_SHA}"
fi
if [[ "$BRANCH_NAME" == "staging" ]]; then
echo "🔨 Build is on staging - will also deploy to staging"
SHOULD_DEPLOY="true"
elif [[ "$BRANCH_NAME" == "main" ]]; then
if [[ ! "$COMMIT_MESSAGE" =~ ^chore:\ Release ]] || [[ "$IS_VERSION_TAGGED" == "false" ]]; then
echo "❌ Skipping build for main: $COMMIT_MESSAGE. Not a version release. Missing tag or proper commit message for release."
exit 1
fi
echo "🏷️ Version tagged build on main: $VERSION - will also deploy to production"
SHOULD_DEPLOY="true"
fi
echo "🔍 Checking for in progress builds with git SHA ${{github.sha}}"
IN_PROGRESS_BUILDS=""
if ! IN_PROGRESS_BUILDS=$(gh api repos/${{github.repository}}/actions/runs --jq '.workflow_runs[] | select(.head_sha == "${{github.sha}}" and .status == "in_progress" and .name == "Build/Deploy" and .id != ${{ github.run_id }}) | .id' 2>/dev/null); then
echo "❌ Failed to check for in-progress builds via GitHub API"
echo "::error::GitHub API call failed while checking for in-progress builds"
exit 1
fi
IN_PROGRESS_BUILDS=$(echo "$IN_PROGRESS_BUILDS" | head -1)
if [[ -n "$IN_PROGRESS_BUILDS" ]]; then
echo "❌ Found in-progress build for SHA ${{github.sha}}: $IN_PROGRESS_BUILDS - wait for existing build to complete before triggering a new one or rerunning this workflow"
exit 1
fi
echo "🔍 Checking for existing build artifacts with git SHA ${{github.sha}}"
# Check for existing artifacts with this SHA across all workflow runs
ARTIFACT_INFO=""
if ! ARTIFACT_INFO=$(gh api repos/${{github.repository}}/actions/artifacts --jq '.artifacts[] | select(.name | endswith("-${{github.sha}}.tar")) | {id, name, workflow_run} | @json' 2>/dev/null); then
echo "❌ Failed to check for existing artifacts via GitHub API"
echo "::error::GitHub API call failed while checking for existing artifacts"
exit 1
fi
ARTIFACT_INFO=$(echo "$ARTIFACT_INFO" | head -1)
if [[ -n "$ARTIFACT_INFO" ]]; then
ARTIFACT_ID=$(echo "$ARTIFACT_INFO" | jq -r '.id')
ARTIFACT_NAME=$(echo "$ARTIFACT_INFO" | jq -r '.name')
RUN_ID=$(echo "$ARTIFACT_INFO" | jq -r '.workflow_run.id')
echo "✅ Found existing artifact $ARTIFACT_NAME from run: $RUN_ID"
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
echo "artifact-id -> $ARTIFACT_ID"
else
RUN_ID="${{ github.run_id }}"
SHOULD_BUILD="true"
echo "🔨 No existing build artifact found; will build..."
fi
echo "should-build=$SHOULD_BUILD" >> $GITHUB_OUTPUT
echo "should-build -> $SHOULD_BUILD"
echo "should-deploy=$SHOULD_DEPLOY" >> $GITHUB_OUTPUT
echo "should-deploy -> $SHOULD_DEPLOY"
echo "artifact-run-id=$RUN_ID" >> $GITHUB_OUTPUT
echo "artifact-run-id -> $RUN_ID"
echo "build-sha=${{github.sha}}" >> $GITHUB_OUTPUT
echo "build-sha -> ${{github.sha}}"
echo "image-tar=$IMAGE_TAR" >> $GITHUB_OUTPUT
echo "image-tar -> $IMAGE_TAR"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "tag -> $TAG"
build:
needs: check
if: ${{ needs.check.outputs.should-build == 'true' }}
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }}
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure scripts are executable
run: chmod +x deploy/scripts/*.sh
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate cache keys
id: cache-keys
run: |
# Generate cache key based on package-lock.json for dependencies
DEPS_HASH=$(sha256sum package-lock.json | cut -d' ' -f1 | cut -c1-8)
echo "deps-hash=${DEPS_HASH}" >> $GITHUB_OUTPUT
echo "deps-hash -> $DEPS_HASH"
# Generate cache key based on source files and paths
SOURCE_PATHS="app/ server/ public/ nuxt.config.ts tsconfig.json tailwind.config.js package.json"
SRC_HASH=$(find $SOURCE_PATHS -type f 2>/dev/null -exec sha256sum {} + | sha256sum | cut -d' ' -f1 | cut -c1-8 || echo "fallback-$(date +%s)")
echo "src-hash=${SRC_HASH}" >> $GITHUB_OUTPUT
echo "src-hash -> $SRC_HASH"
- name: Cache Docker layers
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-deps-${{ steps.cache-keys.outputs.deps-hash }}-src-${{ steps.cache-keys.outputs.src-hash }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-deps-${{ steps.cache-keys.outputs.deps-hash }}-src-${{ steps.cache-keys.outputs.src-hash }}-
${{ runner.os }}-buildx-deps-${{ steps.cache-keys.outputs.deps-hash }}-
${{ runner.os }}-buildx-
- name: Extract version for Docker build
id: extract_version
run: |
if [[ "${{ github.ref }}" =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)(-.*)?$ ]]; then
VERSION="${BASH_REMATCH[1]}"
if [[ -n "${BASH_REMATCH[2]}" ]]; then
VERSION="${VERSION}${BASH_REMATCH[2]}"
fi
echo "🏷️ Using git tag version: ${VERSION}"
else
VERSION=$(node -p "require('./package.json').version || '0.0.0'")
SHA="${{ github.sha }}"
GIT_SHA_SHORT="${SHA:0:7}"
VERSION="${VERSION}+${GIT_SHA_SHORT}"
echo "📦 Using package.json + SHA version: ${VERSION}"
fi
echo "VERSION=${VERSION}" >> $GITHUB_ENV
- name: Build container
uses: docker/build-push-action@v6
with:
outputs: type=docker,dest=${{ runner.temp }}/${{ needs.check.outputs.image-tar }}
tags: ${{ needs.check.outputs.tag }}
build-args: |
VERSION=${{ env.VERSION }}
context: .
target: runtime
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
platforms: linux/amd64
- name: Rotate cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
- name: Upload container as artifact
id: upload-artifact
uses: actions/upload-artifact@v4
with:
name: ${{ needs.check.outputs.image-tar }}
path: ${{ runner.temp }}/${{ needs.check.outputs.image-tar }}
if-no-files-found: error
retention-days: 30
compression-level: 0
- name: Notify successful build
run: |
echo "🎉 Build completed successfully!"
echo "📋 Summary:"
echo " - Source: ${{ github.ref }}"
echo " - Status: ✅ Build Successful"
echo " - Next: Stage this build manually or wait for the next scheduled staging."
deploy:
needs: [check, build]
if: ${{ always() && needs.check.outputs.should-deploy == 'true' }}
permissions:
actions: read
contents: read
uses: ./.github/workflows/deploy.yml
with:
artifact-run-id: ${{ needs.check.outputs.artifact-run-id }}
artifact-id: ${{ needs.check.outputs.should-build == 'true' && needs.build.outputs.artifact-id || needs.check.outputs.artifact-id }}
build-sha: ${{ needs.check.outputs.build-sha }}
repo-name: ${{ needs.check.outputs.repo-name }}
repo-path: ${{ needs.check.outputs.repo-path }}
image-tar: ${{ needs.check.outputs.image-tar }}
prod: ${{ needs.check.outputs.prod == 'true' }}
tag: ${{ needs.check.outputs.tag }}
secrets: inherit

159
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,159 @@
# yaml-language-server: $schema=https://json-schema.org/draft-07/schema#
name: _deploy
concurrency:
group: deploy
cancel-in-progress: false
on:
workflow_call:
inputs:
tag:
description: "Tag to deploy"
required: true
type: string
prod:
description: "Whether to deploy to production"
required: true
type: boolean
image-tar:
description: "Name of image tarball"
required: true
type: string
artifact-run-id:
description: "ID of workflow run where artifact was created"
required: true
type: string
artifact-id:
description: "ID of artifact"
required: true
type: string
build-sha:
description: "SHA of build"
required: true
type: string
repo-name:
description: "Name of repository"
required: true
type: string
repo-path:
description: "Path to repository on server"
required: true
type: string
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.prod && 'production' || 'staging' }}
env:
RELEASE_TYPE: ${{ inputs.prod && 'prod' || 'staging' }}
permissions:
actions: read
contents: read
steps:
- name: Checkout the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Ensure scripts are executable
run: chmod +x deploy/scripts/*.sh
- name: Setup environment configuration
uses: ./.github/actions/setup-environment
with:
release-type: ${{ inputs.prod && 'prod' || 'staging' }}
domain: ${{ secrets.DOMAIN }}
android-sms-gateway-url: ${{ secrets.NUXT_ANDROID_SMS_GATEWAY_URL }}
android-sms-gateway-login: ${{ secrets.NUXT_ANDROID_SMS_GATEWAY_LOGIN }}
android-sms-gateway-password: ${{ secrets.NUXT_ANDROID_SMS_GATEWAY_PASSWORD }}
my-phone-number: ${{ secrets.NUXT_MY_PHONE_NUMBER }}
super-secret-salt: ${{ secrets.NUXT_SUPER_SECRET_SALT }}
wireguard-allowed-ips: ${{ secrets.WIREGUARD_ALLOWED_IPS }}
wireguard-private-key: ${{ secrets.WIREGUARD_PRIVATE_KEY }}
wireguard-addresses: ${{ secrets.WIREGUARD_ADDRESSES }}
wireguard-public-key: ${{ secrets.WIREGUARD_PUBLIC_KEY }}
wireguard-endpoint-host: ${{ secrets.WIREGUARD_ENDPOINT_HOST }}
wireguard-endpoint-port: ${{ secrets.WIREGUARD_ENDPOINT_PORT }}
prod: ${{ inputs.prod }}
- name: Set up SSH
run: |
mkdir -p $HOME/.ssh
echo -e "${{ secrets.DEPLOY_KEY }}" > $HOME/.ssh/deploy.key
chmod 700 $HOME/.ssh
chmod 600 $HOME/.ssh/deploy.key
cat >>$HOME/.ssh/config <<END
Host deploy
HostName ${{ secrets.DEPLOY_HOST }}
Port ${{ secrets.DEPLOY_PORT }}
User ${{ secrets.DEPLOY_USER }}
IdentityFile $HOME/.ssh/deploy.key
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
ControlMaster auto
ControlPath $HOME/.ssh/control-%C
ControlPersist yes
LogLevel QUIET
ConnectionAttempts 3
ConnectTimeout 10
ServerAliveInterval 10
END
- name: Download container image artifact
id: download-artifact
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}
github-token: ${{ github.token }}
artifact-ids: ${{ inputs.artifact-id }}
run-id: ${{ inputs.artifact-run-id }}
- name: Configure HAProxy
env:
DOCKER_HOST: ssh://deploy
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DOMAIN: ${{ secrets.DOMAIN }}
BACKEND_NAME: ${{ inputs.repo-name }}-${{ env.RELEASE_TYPE }}
CF_PEM_CERT: ${{ secrets.CF_PEM_CERT }}
CF_PEM_CA: ${{ secrets.CF_PEM_CA }}
run: |
echo "✅ Exit on any error"
set -eu -o pipefail
echo "🔄 Load environment variables"
set -a
source .env
set +a
echo "🎯 Running HAProxy configuration script"
./deploy/scripts/configure-haproxy.sh add "$BACKEND_NAME"
- name: Deploy to Server
env:
IMAGE_TAR: ${{ steps.download-artifact.outputs.download-path }}/${{ inputs.image-tar }}/${{ inputs.image-tar }}
REPO_PROJECT_PATH: ${{ inputs.repo-path }}
IMAGE_TAG: ${{ inputs.tag }}
GIT_SHA: ${{ inputs.build-sha }}
DOCKER_HOST: ssh://deploy
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
run: |
echo "✅ Exit on any error"
set -eu -o pipefail
echo "🔄 Load environment variables"
set -a
source .env
set +a
echo "🎯 Running deployment script"
./deploy/scripts/deploy-blue-green.sh
- name: Notify successful deployment
run: |
echo "🎉 Deployment completed successfully!"
echo "📋 Summary:"
echo " - Source: ${{ inputs.prod && 'main (production)' || 'staging' }} branch"
echo " - Status: ✅ Deployed Successfully"
echo " - Next: Verify deployment status!"

162
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,162 @@
name: _release
#TODO test without these
permissions:
actions: read
contents: write
issues: write
pull-requests: write
concurrency:
group: release
cancel-in-progress: false
on:
workflow_call:
inputs:
force_release:
description: "Force release even if validation fails"
required: false
default: false
type: boolean
jobs:
validate-staging-build:
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.validation.outputs.should-proceed }}
steps:
- name: Checkout staging
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: staging
- name: Validate staging build status
id: validation
uses: ./.github/actions/validate-build-status
with:
job-name: trigger-staging-build / build
from-branch: staging
to-branch: main
force: ${{ inputs.force_release }}
generate-release-and-push-all:
needs: validate-staging-build
runs-on: ubuntu-latest
if: ${{ inputs.force_release == true || needs.validate-staging-build.outputs.should_release == 'true' }}
outputs:
skip-release: ${{steps.semantic-release.outputs.skip-release }}
steps:
- name: Checkout staging
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: staging
- name: Setup Git SSH for commit signing and push operations
uses: ./.github/actions/setup-git-ssh
with:
commit-ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
push-ssh-private-key: ${{ secrets.PUSH_SSH_PRIVATE_KEY }}
actor: ${{ github.actor }}
actor-id: ${{ github.actor_id }}
- name: Set git origin to GIT_ORIGIN_REPO_URL
run: |
GIT_ORIGIN_URL="${{ secrets.GIT_ORIGIN_REPO_URL }}"
if [ -n "$GIT_ORIGIN_URL" ]; then
git remote remove origin
if git remote add -f --tags origin "$GIT_ORIGIN_URL"; then
echo "✅ Git origin configured correctly"
else
echo "❌ Failed to configure Git origin"
echo "::error::Failed to configure Git origin"
exit 1
fi
else
echo "❌ No Git origin URL configured - cannot proceed without origin URL"
echo "::error::GIT_ORIGIN_REPO_URL secret is required but not configured"
exit 1
fi
- name: Merge staging commits to main
run: |
echo "🔄 Merging staging commits to main..."
git checkout main
STAGING_COMMITS=$(git log --oneline main..staging --pretty=format:"* %s")
if [ -z "$STAGING_COMMITS" ]; then
if [[ "${{inputs.force_release}}" == "true" ]]; then
echo "⚠️ No new commits in staging to merge, but force release is enabled"
echo "::warning::No commits to merge from staging to main"
exit 0
else
echo "❌ No new commits in staging to merge"
echo "::error::No commits to merge from staging to main"
exit 1
fi
else
echo "📋 Commits to be merged:"
echo "$STAGING_COMMITS"
if git merge staging -X theirs -m "chore: merge staging changes for next release"; then
echo "✅ Staging commits merged to main successfully"
else
echo "⚠️ Merge conflicts detected - resolving with staging preference..."
git checkout staging -- .
if [ -f CHANGELOG.md ]; then
git checkout HEAD -- CHANGELOG.md
echo "📋 Kept main's CHANGELOG.md (managed by semantic-release)"
fi
git add -A
git commit -m "chore: merge staging changes for next release"
echo "✅ Conflicts resolved - staging files preferred, main's CHANGELOG.md kept"
fi
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Install semantic-release
run: npm install semantic-release
- name: Run semantic-release on main, generate release, and push
id: semantic-release
env:
GITHUB_TOKEN: "${{ github.token }}" # used for release generation
GIT_AUTHOR_NAME: "${{ github.actor }}"
GIT_AUTHOR_EMAIL: "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
GIT_COMMITTER_NAME: "${{ github.actor }}"
GIT_COMMITTER_EMAIL: "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com"
run: |
echo "🚀 Running semantic-release on main branch..."
npx semantic-release
if git log main -1 --format="%s" | grep -q "^chore: Release"; then
echo "✅ Semantic-release completed and pushed to origin/main"
echo "skip-release=false" >> $GITHUB_OUTPUT
else
echo "❌ Semantic-release did not generate a release, exiting."
echo "skip-release=true" >> $GITHUB_OUTPUT
[[ "${{ inputs.force_release }}" == "true" ]] && exit 1
fi
- name: Notify successful release
if: ${{ steps.semantic-release.outputs.skip-release != 'true' }}
run: |
echo "🎉 Production release completed successfully!"
echo "📋 Summary:"
echo " - Source: staging branch commits (regular merged)"
echo " - Target: main branch (semantic-release)"
echo " - Semantic-release: ✅ Complete on main"
echo " - staging branch: Maintains independent history"
echo " - dev branch: Maintains independent history"
echo " - Tags: ✅ Created and pushed"
echo " - GitHub & Git Origin: ✅ Main branch synchronized"
echo " - Status: 🚀 Ready to build and deploy!"

37
.github/workflows/scheduler-release.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: Release
permissions:
actions: read
contents: write
issues: write
pull-requests: write
on:
schedule:
# Run weekly on Mondays 12:00 UTC/04:00 PST (UTC-08)
- cron: "0 12 * * 1"
workflow_dispatch:
inputs:
force_release:
description: "Force release even if validation fails"
required: false
default: false
type: boolean
note_for_dispatch:
description: "Note: This workflow will always call release.yml@staging, but it MUST BE RUN FROM THE MAIN BRANCH to ensure environment consistency."
required: false
default: false
type: boolean
jobs:
call-release-workflow:
permissions:
actions: read
contents: write
issues: write
pull-requests: write
uses: badbl0cks/portfolio/.github/workflows/release.yml@staging
if: github.ref == 'refs/heads/main'
with:
force_release: ${{ inputs.force_release || false }}
secrets: inherit

33
.github/workflows/scheduler-stage.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Stage
permissions:
actions: read
contents: write
on:
schedule:
# Run daily Monday-Friday at 13:00 UTC/05:00 PST (UTC-08)
- cron: "0 13 * * 1-5"
workflow_dispatch:
inputs:
force_staging:
description: "Force staging even if validation fails"
required: false
default: false
type: boolean
note_for_dispatch:
description: "Note: This workflow will always call stage.yml@dev, but it MUST BE RUN FROM THE MAIN BRANCH to ensure environment consistency."
required: false
default: false
type: boolean
jobs:
call-staging-workflow:
permissions:
actions: read
contents: write
uses: badbl0cks/portfolio/.github/workflows/stage.yml@dev
if: github.ref == 'refs/heads/main'
with:
force_staging: ${{ inputs.force_staging || false }}
secrets: inherit

80
.github/workflows/stage.yml vendored Normal file
View file

@ -0,0 +1,80 @@
name: _stage
permissions:
actions: read
contents: write
concurrency:
group: stage
cancel-in-progress: false
on:
workflow_call:
inputs:
force_staging:
description: "Force staging even if validation fails"
required: false
default: false
type: boolean
jobs:
validate-dev-build:
runs-on: ubuntu-latest
outputs:
should_stage: ${{ steps.validation.outputs.should-proceed }}
steps:
- name: Checkout dev
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: dev
- name: Validate dev build status
id: validation
uses: ./.github/actions/validate-build-status
with:
job-name: trigger-dev-build / build
from-branch: dev
to-branch: staging
force: ${{ inputs.force_staging }}
push-to-staging:
needs: validate-dev-build
runs-on: ubuntu-latest
if: ${{ inputs.force_staging == true || needs.validate-dev-build.outputs.should_stage == 'true' }}
steps:
- name: Checkout dev
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: dev
- name: Setup Git SSH for push operations
uses: ./.github/actions/setup-git-ssh
with:
push-ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
actor: ${{ github.actor }}
actor-id: ${{ github.actor_id }}
- name: Reset staging branch to refs/heads/dev
run: |
echo "🚀 Resetting staging branch..."
git checkout staging
git reset --hard dev
echo "✅ Staging branch reset to refs/heads/dev"
- name: Push to git origin
uses: ./.github/actions/push-to-origin
with:
git-origin-url: ${{ secrets.GIT_ORIGIN_REPO_URL }}
branches: staging
force: true
- name: Notify successful staging
run: |
echo "🎉 Staging completed successfully!"
echo "📋 Summary:"
echo " - Source: dev branch"
echo " - Status: ✅ Pushed to staging"
echo " - Next: Perform staging and integration tests. If satisfactory, manually dispatch the staging-to-main workflow to generate a production release, which will then trigger a production build and deployment."

30
.github/workflows/trigger-build.yml vendored Normal file
View file

@ -0,0 +1,30 @@
# yaml-language-server: $schema=https://json-schema.org/draft-07/schema#
name: Build/Deploy
on:
workflow_dispatch:
push:
branches: [dev, staging, main]
jobs:
trigger-dev-build:
permissions:
actions: read
contents: read
if: ${{ github.ref == 'refs/heads/dev' }}
uses: badbl0cks/portfolio/.github/workflows/build.yml@dev
secrets: inherit
trigger-staging-build:
permissions:
actions: read
contents: read
if: ${{ github.ref == 'refs/heads/staging' }}
uses: badbl0cks/portfolio/.github/workflows/build.yml@staging
secrets: inherit
trigger-main-build:
permissions:
actions: read
contents: read
if: ${{ github.ref == 'refs/heads/main' }}
uses: badbl0cks/portfolio/.github/workflows/build.yml@main
secrets: inherit