feat: add in copy of updated shared deployment workflows for now until they are
published
This commit is contained in:
parent
bcb7f86b7f
commit
e245bcbe96
12 changed files with 1113 additions and 0 deletions
45
.github/actions/determine-build-metadata/action.yml
vendored
Normal file
45
.github/actions/determine-build-metadata/action.yml
vendored
Normal 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"
|
||||||
57
.github/actions/push-to-origin/action.yml
vendored
Normal file
57
.github/actions/push-to-origin/action.yml
vendored
Normal 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
|
||||||
78
.github/actions/setup-environment/action.yml
vendored
Normal file
78
.github/actions/setup-environment/action.yml
vendored
Normal 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"
|
||||||
52
.github/actions/setup-git-ssh/action.yml
vendored
Normal file
52
.github/actions/setup-git-ssh/action.yml
vendored
Normal 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
|
||||||
140
.github/actions/validate-build-status/action.yml
vendored
Normal file
140
.github/actions/validate-build-status/action.yml
vendored
Normal 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
240
.github/workflows/build.yml
vendored
Normal 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
159
.github/workflows/deploy.yml
vendored
Normal 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
162
.github/workflows/release.yml
vendored
Normal 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
37
.github/workflows/scheduler-release.yml
vendored
Normal 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
33
.github/workflows/scheduler-stage.yml
vendored
Normal 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
80
.github/workflows/stage.yml
vendored
Normal 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
30
.github/workflows/trigger-build.yml
vendored
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue