diff --git a/.github/actions/determine-build-metadata/action.yml b/.github/actions/determine-build-metadata/action.yml new file mode 100644 index 0000000..d8be567 --- /dev/null +++ b/.github/actions/determine-build-metadata/action.yml @@ -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" diff --git a/.github/actions/push-to-origin/action.yml b/.github/actions/push-to-origin/action.yml new file mode 100644 index 0000000..d3e8ef6 --- /dev/null +++ b/.github/actions/push-to-origin/action.yml @@ -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 diff --git a/.github/actions/setup-environment/action.yml b/.github/actions/setup-environment/action.yml new file mode 100644 index 0000000..75508fc --- /dev/null +++ b/.github/actions/setup-environment/action.yml @@ -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" \ No newline at end of file diff --git a/.github/actions/setup-git-ssh/action.yml b/.github/actions/setup-git-ssh/action.yml new file mode 100644 index 0000000..5f89ebd --- /dev/null +++ b/.github/actions/setup-git-ssh/action.yml @@ -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 < ~/.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 \ No newline at end of file diff --git a/.github/actions/validate-build-status/action.yml b/.github/actions/validate-build-status/action.yml new file mode 100644 index 0000000..f499eb3 --- /dev/null +++ b/.github/actions/validate-build-status/action.yml @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2a1451a --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..e1aa20d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 <> $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!" diff --git a/.github/workflows/scheduler-release.yml b/.github/workflows/scheduler-release.yml new file mode 100644 index 0000000..ae9ef61 --- /dev/null +++ b/.github/workflows/scheduler-release.yml @@ -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 diff --git a/.github/workflows/scheduler-stage.yml b/.github/workflows/scheduler-stage.yml new file mode 100644 index 0000000..87681b6 --- /dev/null +++ b/.github/workflows/scheduler-stage.yml @@ -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 diff --git a/.github/workflows/stage.yml b/.github/workflows/stage.yml new file mode 100644 index 0000000..4cb40bc --- /dev/null +++ b/.github/workflows/stage.yml @@ -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." diff --git a/.github/workflows/trigger-build.yml b/.github/workflows/trigger-build.yml new file mode 100644 index 0000000..0233a25 --- /dev/null +++ b/.github/workflows/trigger-build.yml @@ -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