merge-main-into-remote-branch-guide

A safe, intelligent Git script that merges origin/main into remote feature branches using worktreesβ€”so your current work is never disrupted.

🎯 Why Use This Script?

The Problem It Solves

When working on feature branches, you often need to merge the latest changes from main to:

  • Keep your branch up-to-date
  • Resolve conflicts early
  • Ensure CI/CD pipelines pass with latest dependencies

Traditional approach problems:

git checkout feature-branch
git merge main
# ❌ Your working directory changes
# ❌ Uncommitted work gets in the way
# ❌ Conflicts force you to stop everything
# ❌ Multiple branches = lots of manual switching

How This Script Helps

βœ… Non-Disruptive β€” Uses Git worktrees, never touches your current branch
βœ… Safe β€” Detects conflicts and guides you through resolution
βœ… Intelligent β€” Auto-selects when only one candidate branch exists
βœ… Flexible β€” Supports dry-run mode, branch exclusions, batch operations
βœ… Clean β€” Automatically removes worktrees and temporary branches when done
βœ… User-Friendly β€” Clear prompts, helpful error messages, conflict resolution guides


πŸ“¦ Installation

The ~/.local/bin directory follows the XDG Base Directory specification and is the modern standard for user-local executables. Most Linux distributions automatically include it in PATH.

# 1. Create the directory (if it doesn't exist)
mkdir -p ~/.local/bin

# 2. Add to PATH if needed (most modern systems already include it)
# Check if it's already in your PATH:
echo $PATH | grep -q "$HOME/.local/bin" && echo "Already in PATH" || echo "Need to add to PATH"

# If you need to add it manually:
# For bash:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# For zsh:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# For fish:
fish_add_path ~/.local/bin

# 3. Install the script
curl -o ~/.local/bin/merge-main https://raw.githubusercontent.com/yourusername/yourrepo/main/merge-main-into-remote-branch.sh
chmod +x ~/.local/bin/merge-main

# Or if you already have the file locally:
cp merge-main-into-remote-branch.sh ~/.local/bin/merge-main
chmod +x ~/.local/bin/merge-main

Why ~/.local/bin?

  • βœ… XDG Base Directory standard (widely adopted)
  • βœ… Often already in PATH on modern Linux distributions
  • βœ… Keeps user binaries separate from system binaries
  • βœ… Respects the filesystem hierarchy standard
  • βœ… Works seamlessly with systemd user services

Option 2: ~/bin (Alternative)

Traditional alternative, still widely used.

# Create a personal bin directory
mkdir -p ~/bin

# Add to PATH (usually required)
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Install the script
cp merge-main-into-remote-branch.sh ~/bin/merge-main
chmod +x ~/bin/merge-main

Option 3: System-Wide Installation

Requires sudo but makes it available to all users.

# Make executable and move to system bin
chmod +x merge-main-into-remote-branch.sh
sudo mv merge-main-into-remote-branch.sh /usr/local/bin/merge-main

Option 4: Shell Alias

Keep the script in your project and create an alias.

# Add to ~/.bashrc or ~/.zshrc
echo 'alias merge-main="/full/path/to/merge-main-into-remote-branch.sh"' >> ~/.bashrc
source ~/.bashrc

Verify Installation

# Check if the command is found
which merge-main

# Test with dry-run
DRY_RUN=1 merge-main


πŸš€ Usage

Basic Syntax

merge-main [module] [options]

Parameters:

  • module β€” (optional) Path to the Git repository folder
    • If omitted and current directory is a Git repo, uses it automatically
    • Use . to explicitly use the current directory
    • Otherwise, prompts for module name

Quick Examples

# Run in current directory (if it's a git repo)
merge-main

# Explicitly use current directory
merge-main .

# Specify a repository path
merge-main ~/projects/my-app
merge-main ./backend

# Preview without making changes
DRY_RUN=1 merge-main

# Exclude additional branches
EXCLUDE=develop,staging,release merge-main

# Combine options
DRY_RUN=1 EXCLUDE=hotfix,production merge-main ~/projects/api


πŸŽ›οΈ Environment Variables

DRY_RUN

Preview all Git commands without executing them.

# See what would happen without making changes
DRY_RUN=1 merge-main

# Output example:
# [dry-run] git fetch --prune origin
# [dry-run] git fetch origin main feature/user-auth
# [dry-run] git worktree add -b feature/user-auth ...

When to use:

  • Testing the script for the first time
  • Verifying which branch will be selected
  • Checking excluded branches
  • Understanding the workflow before committing

EXCLUDE

Comma-separated list of branch names to exclude from candidate selection.

# Default exclusions: develop, staging
# Also always excluded: main, HEAD

# Add more exclusions
EXCLUDE=develop,staging,hotfix,production merge-main

# Override defaults (only exclude release)
EXCLUDE=release merge-main

# No extra exclusions (only main and HEAD)
EXCLUDE= merge-main

Common use cases:

  • EXCLUDE=hotfix β€” Avoid merging into emergency fix branches
  • EXCLUDE=production,release β€” Skip protected deployment branches
  • EXCLUDE=archive/* β€” Skip archived branches

πŸ“– Detailed Workflow

Step-by-Step Process

  1. Repository Discovery

    • Uses specified path or auto-detects current directory
    • Validates it’s a Git repository
  2. Fetch Latest State

    • Runs git fetch --prune origin to sync remote branches
    • Ensures you’re working with up-to-date information
  3. Branch Selection

    • Lists all remote branches except main, HEAD, and excluded branches
    • Auto-selects if only one candidate exists
    • Shows numbered menu if multiple candidates exist
  4. Worktree Setup

    • Creates a temporary worktree in a safe location
    • Checks out the target branch in the worktree
    • Your current working directory remains untouched
  5. Merge Execution

    • Merges origin/main into the target branch
    • Creates a timestamped commit message
  6. Outcome Handling

    • Success: Pushes to origin, cleans up worktree and temp branch
    • Conflict: Shows conflicting files, provides resolution guide

🎯 Real-World Scenarios

Scenario 1: Single Feature Branch

You have one active feature branch that needs the latest main.

$ merge-main

==> No module specified β€” using current directory as repo
==> Fetching origin for '.'...
==> Fetching origin/main and origin/feature/user-auth...
==> Auto-selected branch: feature/user-auth
==> Creating worktree at '/tmp/_worktree-feature-user-auth' tracking origin/feature/user-auth
==> Merging origin/main into feature/user-auth...
==> Merge successful. Pushing feature/user-auth to origin...
==> Cleaning up worktree and local branch...

OK: merged main -> feature/user-auth in '.' at 10Apr2026 1430

Scenario 2: Multiple Branches

You have several feature branches and need to choose which one to update.

$ merge-main ~/projects/api

Multiple candidate branches found:
  [1] feature/authentication
  [2] feature/payment-gateway
  [3] bugfix/timeout-issue
Pick one [1-3]: 2

==> Fetching origin/main and origin/feature/payment-gateway...
==> Creating worktree...
==> Merging origin/main into feature/payment-gateway...
==> Merge successful. Pushing feature/payment-gateway to origin...

OK: merged main -> feature/payment-gateway in 'api' at 10Apr2026 1445

Scenario 3: Merge Conflict

The merge encounters conflicts that need manual resolution.

$ merge-main

==> Merging origin/main into feature/new-ui...

!! Merge conflict detected. Conflicting files:
     - src/components/Header.jsx
     - src/styles/theme.css

   Resolve conflicts manually, then push:

   Step 1 β€” Go to the worktree where the conflict lives:
            cd /tmp/_worktree-feature-new-ui

   Step 2 β€” See the full status:
            git status

   Step 3 β€” Open each conflicting file and resolve the markers:

            <<<<<<< HEAD          ← your branch (feature/new-ui)
            your code
            =======
            incoming code
            >>>>>>> origin/main   ← what came from main

   Step 4 β€” Mark each resolved file as done:
            git add <file>

   Step 5 β€” Complete the merge commit:
            git commit

   Step 6 β€” Push the resolved branch:
            git push -u origin feature/new-ui

   Step 7 β€” Clean up the worktree:
            cd -
            git worktree remove /tmp/_worktree-feature-new-ui
            git branch -D feature/new-ui

Resolution Example:

# Navigate to worktree
cd /tmp/_worktree-feature-new-ui

# Edit conflicting files in your editor
vim src/components/Header.jsx

# After resolving conflicts
git add src/components/Header.jsx src/styles/theme.css
git commit
git push -u origin feature/new-ui

# Return to original directory and clean up
cd -
git worktree remove /tmp/_worktree-feature-new-ui
git branch -D feature/new-ui

Scenario 4: Batch Processing

Update all feature branches across multiple repositories.

# Process all git repos in subdirectories
for dir in */; do
    if [[ -d "$dir/.git" ]]; then
        echo "Processing $dir..."
        merge-main "${dir%/}"
    fi
done

Example Output:

Processing api/...
OK: merged main -> feature/v2-endpoints in 'api' at 10Apr2026 1500

Processing frontend/...
OK: merged main -> feature/dashboard in 'frontend' at 10Apr2026 1502

Processing workers/...
!! Merge conflict detected in 'workers'...

Scenario 5: Dry Run Before Production

Test what will happen before making actual changes.

# Preview the entire workflow
DRY_RUN=1 merge-main

[dry-run] git fetch --prune origin
[dry-run] git fetch origin main feature/critical-fix
[dry-run] git worktree add -b feature/critical-fix /tmp/_worktree-feature-critical-fix origin/feature/critical-fix
[dry-run] git merge origin/main -m "merge: main into feature/critical-fix 10Apr2026 1515"
[dry-run] git push -u origin feature/critical-fix
[dry-run] git worktree remove /tmp/_worktree-feature-critical-fix

# If it looks good, run for real
merge-main


πŸ›‘οΈ Safety Features

1. Worktree Isolation

Your current branch and working directory are never modified. All merge operations happen in a temporary worktree.

Your Repo                    Temporary Worktree
---------                    ------------------
main ← you're here          feature/xyz ← merge happens here
feature/xyz (remote)        

2. Stale Worktree Detection

If a previous run was interrupted (conflict, crash, manual abort), the script detects leftover worktrees and handles them intelligently.

With unresolved conflicts:

!! A worktree already exists at: /tmp/_worktree-feature-xyz
   WARNING: These files still have unresolved merge conflicts:
     - src/app.js
     - config/settings.py
   
   Force-removing this worktree will PERMANENTLY DISCARD those changes.
   
   Force remove and start fresh? [y/N]

With uncommitted changes:

!! A worktree already exists at: /tmp/_worktree-feature-xyz
   WARNING: The worktree has uncommitted changes:
     M  src/utils.js
     A  tests/new-test.js
   
   Force-removing this worktree will PERMANENTLY DISCARD those changes.
   
   Force remove and start fresh? [y/N]

Clean worktree (no changes):

==> Stale worktree has no pending changes β€” removing automatically

3. Branch Validation

Automatically excludes:

  • origin/HEAD (symbolic reference)
  • origin/main (the source branch)
  • Branches in EXCLUDE list (default: develop, staging)

4. Graceful Conflict Handling

Instead of leaving you in a broken state, the script:

  • Clearly lists which files have conflicts
  • Provides step-by-step resolution instructions
  • Preserves the worktree so you can fix conflicts manually
  • Exits with status code 1 (won’t silently fail in scripts)

πŸ”§ Advanced Usage

Custom Commit Message Pattern

Edit the script to customize the commit message:

# Find this line in the script (around line 163):
MSG="merge: main into ${BRANCH} ${STAMP}"

# Change to your preferred format:
MSG="chore: sync ${BRANCH} with main (${STAMP})"
MSG="Merge main β†’ ${BRANCH} | ${STAMP}"
MSG="πŸ”€ main β†’ ${BRANCH}"

Integration with CI/CD

Use in automation pipelines:

#!/bin/bash
# .github/workflows/sync-branches.sh

set -e

# Fail fast if merge has conflicts
merge-main || {
    echo "Merge conflict detected - manual intervention required"
    exit 1
}

# Continue with tests, deployments, etc.

Custom Exclusion Lists per Project

Create a wrapper script:

#!/bin/bash
# sync-api-branches.sh

# Project-specific exclusions
export EXCLUDE="hotfix,production,release,v1-stable"

merge-main ~/projects/api

Monitoring in Scripts

Capture output for logging:

LOG_FILE="merge-$(date +%Y%m%d).log"

merge-main 2>&1 | tee -a "$LOG_FILE"

if [[ ${PIPESTATUS[0]} -eq 0 ]]; then
    echo "βœ“ Merge completed successfully" >> "$LOG_FILE"
else
    echo "βœ— Merge failed - check log" >> "$LOG_FILE"
    # Send alert, create ticket, etc.
fi


❓ FAQ

Q: What if I accidentally run this on main itself?

The script explicitly excludes main from the candidate list, so it won’t let you merge main into main.

Q: Can I use this with GitHub/GitLab protected branches?

Yes, but you need push permissions. If the target branch is protected, the push step will fail with a permission error. The merge itself happens locally in the worktree.

Q: What happens to my uncommitted changes?

Nothingβ€”they remain untouched. The script works in a separate worktree, not your current directory.

Q: Can I merge into multiple branches at once?

Not directly, but you can script it:

for branch in feature/auth feature/payments feature/notifications; do
    echo "Processing $branch..."
    # Script will auto-select if you filter candidates to match exactly one
    EXCLUDE="$(git branch -r | sed 's|origin/||' | grep -v "$branch" | tr '\n' ',')" merge-main
done

Q: How do I abort a merge in progress?

# Navigate to the worktree
cd /tmp/_worktree-your-branch

# Abort the merge
git merge --abort

# Return and clean up
cd -
git worktree remove /tmp/_worktree-your-branch
git branch -D your-branch

Q: Does this work with submodules?

Yes, but each submodule is a separate Git repository. You’d need to run the script separately for each submodule, or create a wrapper that iterates through them.


πŸ› Troubleshooting

“command not found: merge-main”

Problem: Script is not in PATH or not executable.

Solution:

# Check if it exists
ls -la ~/.local/bin/merge-main

# Make sure it's executable
chmod +x ~/.local/bin/merge-main

# Verify PATH includes ~/.local/bin
echo $PATH | grep "$HOME/.local/bin"

# If not in PATH, add it:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc

# Reload shell config
source ~/.bashrc  # or ~/.zshrc

“not a git repository”

Problem: Running in a directory that isn’t a Git repo.

Solution:

# Check if current dir is a git repo
git status

# Or specify the repo path explicitly
merge-main /path/to/your/repo

“no non-main remote branch found”

Problem: All branches are excluded or only main exists.

Solution:

# Check what branches exist
git branch -r

# Adjust exclusions
EXCLUDE= merge-main  # Remove default exclusions

# Or create a feature branch first
git checkout -b feature/new-feature
git push -u origin feature/new-feature

Worktree conflicts with existing directory

Problem: /tmp/_worktree-branch-name already exists.

Solution:

# The script should handle this automatically
# If it doesn't, manually remove:
git worktree remove --force /tmp/_worktree-branch-name

# Then re-run the script
merge-main


πŸ“ Script Reference

Full Script

#!/usr/bin/env bash
# =============================================================================
# merge-main-into-remote-branch.sh
#
# PURPOSE:
#   Merges origin/main into a non-main remote branch inside a git repo (module).
#   Uses git worktree so your current working branch is never touched.
#
# USAGE:
#   ./merge-main-into-remote-branch.sh [module]
#
#   module  β€” (optional) path to the git repo folder.
#             If omitted, the script checks if the current directory is a git
#             repo and uses it automatically, otherwise it prompts.
#             Pass "." to explicitly use the current directory.
#
# OPTIONS (environment variables):
#   DRY_RUN=1          β€” Preview all git commands without executing them.
#   EXCLUDE=a,b,c      β€” Comma-separated branch names to exclude from candidates
#                        in addition to main and HEAD (default: "develop,staging").
#
# EXAMPLES:
#   ./merge-main-into-remote-branch.sh managebac
#   ./merge-main-into-remote-branch.sh .
#   DRY_RUN=1 ./merge-main-into-remote-branch.sh managebac
#   EXCLUDE=develop,staging,release ./merge-main-into-remote-branch.sh managebac
#
#   # Run across ALL subdirectories that are git repos:
#   for dir in */; do
#       [[ -d "$dir/.git" ]] && ./merge-main-into-remote-branch.sh "${dir%/}"
#   done
# =============================================================================

# NOTE: We intentionally do NOT use `set -e` globally here.
# The merge step is expected to exit non-zero on conflicts, and we need to
# handle that gracefully ourselves rather than letting bash abort the script.
# We keep -u (undefined variable check) and -o pipefail for safety.
set -uo pipefail

# -----------------------------------------------------------------------------
# CONFIG
# -----------------------------------------------------------------------------

# Dry-run mode: set DRY_RUN=1 to preview without making any changes
DRY_RUN="${DRY_RUN:-0}"

# Branches to always exclude from candidate list (on top of main and HEAD)
EXCLUDE="${EXCLUDE:-develop,staging}"

# -----------------------------------------------------------------------------
# HELPERS
# -----------------------------------------------------------------------------

# Print a timestamped info message
info() { echo "==> $*"; }

# Print an error to stderr and exit immediately
die()  { echo "error: $*" >&2; exit 1; }

# Run a command normally, or just print it when DRY_RUN=1
run() {
    if [[ "$DRY_RUN" == "1" ]]; then
        echo "[dry-run] $*"
    else
        "$@"
    fi
}

# Ask a yes/no question β€” returns 0 for yes, 1 for no
# Usage: confirm "Are you sure?" && do_something
confirm() {
    local prompt="${1:-Are you sure?}"
    local reply
    read -rp "$prompt [y/N] " reply
    [[ "${reply,,}" == "y" || "${reply,,}" == "yes" ]]
}

# -----------------------------------------------------------------------------
# RESOLVE MODULE / REPO PATH
# -----------------------------------------------------------------------------

MODULE="${1:-}"

# If no argument given, check if the current directory is already a git repo
if [[ -z "$MODULE" ]]; then
    if git rev-parse --git-dir > /dev/null 2>&1; then
        info "No module specified β€” using current directory as repo"
        MODULE="."
    else
        read -rp "Module name (e.g. managebac) or '.' for current dir: " MODULE
    fi
fi

[[ -z "$MODULE" ]] && die "module name required"
[[ -d "$MODULE" ]]  || die "'$MODULE' is not a directory"

# Move into the module directory
cd "$MODULE"

# Confirm this is actually a git repository
git rev-parse --git-dir > /dev/null 2>&1 || die "'$MODULE' is not a git repository"

# -----------------------------------------------------------------------------
# FETCH LATEST REMOTE STATE
# -----------------------------------------------------------------------------

info "Fetching origin for '$MODULE'..."
run git fetch --prune origin

# -----------------------------------------------------------------------------
# BUILD EXCLUDE PATTERN
# -----------------------------------------------------------------------------

# Convert comma-separated EXCLUDE into a regex alternation e.g. "develop|staging"
EXCLUDE_PATTERN=$(echo "$EXCLUDE" | tr ',' '|')

# -----------------------------------------------------------------------------
# DISCOVER CANDIDATE BRANCHES
# -----------------------------------------------------------------------------

# List all remote branches, strip whitespace, filter out:
#   - origin/HEAD  β€” just a symbolic pointer, not a real branch
#   - origin/main  β€” this is the source, never the target
#   - EXCLUDE list β€” e.g. develop, staging
mapfile -t CANDIDATES < <(
    git branch -r \
        | sed 's/^[[:space:]]*//' \
        | grep -v '^origin/HEAD' \
        | grep -v '^origin/main$' \
        | grep -vE "^origin/(${EXCLUDE_PATTERN})$" \
        | sed 's|^origin/||'
)

# -----------------------------------------------------------------------------
# SELECT TARGET BRANCH
# -----------------------------------------------------------------------------

if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
    die "no non-main remote branch found in '$MODULE' (excluded: main, $EXCLUDE)"

elif [[ ${#CANDIDATES[@]} -eq 1 ]]; then
    # Only one candidate β€” select it automatically, no prompt needed
    BRANCH="${CANDIDATES[0]}"
    info "Auto-selected branch: $BRANCH"

else
    # Multiple candidates β€” present a numbered menu and let the user choose
    echo "Multiple candidate branches found:"
    for i in "${!CANDIDATES[@]}"; do
        printf "  [%d] %s\n" "$((i+1))" "${CANDIDATES[$i]}"
    done
    read -rp "Pick one [1-${#CANDIDATES[@]}]: " PICK

    [[ "$PICK" =~ ^[0-9]+$ ]]                        || die "invalid pick: '$PICK'"
    (( PICK >= 1 && PICK <= ${#CANDIDATES[@]} ))      || die "out of range: $PICK"

    BRANCH="${CANDIDATES[$((PICK-1))]}"
fi

# -----------------------------------------------------------------------------
# PREPARE WORKTREE PATH & COMMIT MESSAGE
# -----------------------------------------------------------------------------

# Sanitize branch name: lowercase, replace slashes and spaces with dashes
SAFE_BRANCH="$(echo "$BRANCH" | tr '[:upper:]/ ' '[:lower:]-')"

if [[ "$MODULE" == "." ]]; then
    # Running inside the repo itself β€” place worktree in /tmp to avoid nesting
    WORKTREE="/tmp/_worktree-${SAFE_BRANCH}"
else
    # Place worktree as a sibling of the module directory
    WORKTREE="../_${MODULE}-${SAFE_BRANCH}"
fi

STAMP="$(date +'%d%b%Y %H%M')"
MSG="merge: main into ${BRANCH} ${STAMP}"

# -----------------------------------------------------------------------------
# FETCH MAIN + TARGET BRANCH (ensure both are up to date)
# -----------------------------------------------------------------------------

info "Fetching origin/main and origin/$BRANCH..."
run git fetch origin main "$BRANCH"

# -----------------------------------------------------------------------------
# HANDLE STALE WORKTREE FROM A PREVIOUS FAILED RUN
# -----------------------------------------------------------------------------

# `git worktree list` prints absolute paths β€” use grep -F (fixed string, no regex)
# to safely match the exact path without worrying about special characters
if git worktree list | grep -qF "$WORKTREE"; then

    echo ""
    echo "!! A worktree already exists at: $WORKTREE"
    echo "   This is likely left over from a previous run that had conflicts."
    echo ""

    # Check for files that are still in an unresolved conflict state (diff-filter=U)
    UNMERGED=$(git -C "$WORKTREE" diff --name-only --diff-filter=U 2>/dev/null || true)

    # Check for any other uncommitted changes (staged, unstaged, untracked)
    UNCOMMITTED=$(git -C "$WORKTREE" status --porcelain 2>/dev/null || true)

    if [[ -n "$UNMERGED" ]]; then
        # Unresolved conflict markers still present β€” highest risk, warn loudly
        echo "   WARNING: These files still have unresolved merge conflicts:"
        echo "$UNMERGED" | sed 's/^/     - /'
        echo ""
        echo "   Force-removing this worktree will PERMANENTLY DISCARD those changes."
        echo ""
        confirm "   Force remove and start fresh?" \
            || die "aborted β€” resolve conflicts manually in $WORKTREE then re-run"

    elif [[ -n "$UNCOMMITTED" ]]; then
        # Changes exist but no conflict markers β€” could be partial manual fixes
        echo "   WARNING: The worktree has uncommitted changes:"
        echo "$UNCOMMITTED" | sed 's/^/     /'
        echo ""
        echo "   Force-removing this worktree will PERMANENTLY DISCARD those changes."
        echo ""
        confirm "   Force remove and start fresh?" \
            || die "aborted β€” inspect $WORKTREE before proceeding"

    else
        # Worktree is clean β€” safe to remove silently without asking
        info "Stale worktree has no pending changes β€” removing automatically"
    fi

    run git worktree remove --force "$WORKTREE"
fi

# Remove any local tracking branch left over from a prior run.
# Try safe delete (-d) first; only force-delete (-D) if git considers it unmerged.
if git branch --list "$BRANCH" | grep -q .; then
    git branch -d "$BRANCH" 2>/dev/null \
        || git branch -D "$BRANCH" 2>/dev/null \
        || true
fi

# -----------------------------------------------------------------------------
# CREATE FRESH WORKTREE TRACKING THE TARGET BRANCH
# -----------------------------------------------------------------------------

info "Creating worktree at '$WORKTREE' tracking origin/$BRANCH"
run git worktree add -b "$BRANCH" "$WORKTREE" "origin/$BRANCH"

# -----------------------------------------------------------------------------
# MERGE origin/main INTO TARGET BRANCH
# -----------------------------------------------------------------------------

info "Merging origin/main into $BRANCH..."

# Temporarily disable pipefail around the merge command.
# git merge exits non-zero on conflicts β€” that is expected and handled below.
# Without this, pipefail would cause the script to abort before we can react.
pushd "$WORKTREE" > /dev/null

set +e
run git merge origin/main -m "$MSG"
MERGE_EXIT=$?
set -e

popd > /dev/null

# -----------------------------------------------------------------------------
# OUTCOME: SUCCESS β€” push and clean up
# -----------------------------------------------------------------------------

if [[ $MERGE_EXIT -eq 0 ]]; then

    info "Merge successful. Pushing $BRANCH to origin..."
    pushd "$WORKTREE" > /dev/null
    run git push -u origin "$BRANCH"
    popd > /dev/null

    info "Cleaning up worktree and local branch..."
    run git worktree remove "$WORKTREE"
    git branch -d "$BRANCH" 2>/dev/null \
        || git branch -D "$BRANCH" 2>/dev/null \
        || true

    echo ""
    echo "OK: merged main -> $BRANCH in '$MODULE' at $STAMP"

# -----------------------------------------------------------------------------
# OUTCOME: CONFLICT β€” show exactly what conflicts exist and guide resolution
# -----------------------------------------------------------------------------

else

    # List the conflicting files immediately so the user knows where to look
    echo ""
    echo "!! Merge conflict detected. Conflicting files:"
    git -C "$WORKTREE" diff --name-only --diff-filter=U \
        | sed 's/^/     - /'

    cat <<EOF

   Resolve conflicts manually, then push:

   Step 1 β€” Go to the worktree where the conflict lives:
            cd $WORKTREE

   Step 2 β€” See the full status:
            git status

   Step 3 β€” Open each conflicting file and resolve the markers:

            <<<<<<< HEAD          ← your branch ($BRANCH)
            your code
            =======
            incoming code
            >>>>>>> origin/main   ← what came from main

   Step 4 β€” Mark each resolved file as done:
            git add <file>

   Step 5 β€” Complete the merge commit:
            git commit

   Step 6 β€” Push the resolved branch:
            git push -u origin $BRANCH

   Step 7 β€” Clean up the worktree (run from the repo root, not inside the worktree):
            cd -
            git worktree remove $WORKTREE
            git branch -D $BRANCH

   TIP: To abandon the merge entirely and start over:
            git merge --abort     ← run this inside $WORKTREE first
            then re-run this script from the repo root.

EOF
    exit 1
fi


🀝 Contributing

Found a bug? Have a feature request? Contributions are welcome!

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This script is provided as-is under the MIT License. Feel free to use, modify, and distribute.


πŸ™ Acknowledgments

Built with love for developers who are tired of:

  • Switching branches constantly
  • Merge conflicts destroying their flow
  • Forgetting which branches need updates
  • Manual, error-prone sync processes

Happy merging! πŸš€

Git Submodules Setup Guide

Git Submodules Setup Guide

Setting up a clean main repo with submodules using temporary bare repositories stored inside .git/ β€” no sibling folder pollution, zero cleanup drama when real remote URLs arrive.

Target Structure

.
β”œβ”€β”€ .git/
β”‚   β”œβ”€β”€ local-submodules/         ← temporary bare repos (hidden, never tracked)
β”‚   β”‚   β”œβ”€β”€ Middlewares.git
β”‚   β”‚   β”œβ”€β”€ PowerAut0mater.git
β”‚   β”‚   └── Shared.git
β”œβ”€β”€ .gitmodules
β”œβ”€β”€ Middlewares/                  ← submodule
β”œβ”€β”€ PowerAut0mater/               ← submodule
└── Shared/                       ← submodule

Why .git/local-submodules/? The .git/ folder is never tracked by Git. This means the bare repos live completely hidden from your working tree, no sibling folders are created, and when you switch to real remote URLs later there is nothing to clean up except one optional rm -rf.

Step 1 β€” Initialize the Main Repository

# Navigate into your project root
cd /projects

# Initialize a new Git repository and name the default branch "main"
# -b main β†’ sets the initial branch name (instead of the old default "master")
git init -b main

Step 2 β€” Create Bare Repos Inside .git/

# Create the hidden folder that will hold all temporary bare repos
# -p β†’ creates parent directories as needed; no error if folder already exists
mkdir -p .git/local-submodules

# Initialize a bare Git repository for each submodule
# --bare β†’ creates a repo with NO working tree (just raw git data)
#          correct format for a repo that will only be pushed to / cloned from
# -b main β†’ sets the default branch name to "main"
git init -b main --bare .git/local-submodules/Middlewares.git
git init -b main --bare .git/local-submodules/PowerAut0mater.git
git init -b main --bare .git/local-submodules/Shared.git

Bare repos need at least one commit before they can be used as submodule sources. Clone each into /tmp, make an empty commit, push back, then clean up:

# Clone the bare repo into /tmp so we have a working tree to commit from
# This creates a temporary normal (non-bare) clone at /tmp/Middlewares
git clone .git/local-submodules/Middlewares.git /tmp/Middlewares

cd /tmp/Middlewares

# Create an empty commit (no files needed) to give the bare repo a valid HEAD
# --allow-empty β†’ normally Git refuses commits with no changes; this flag bypasses that
git commit --allow-empty -m "init"

# Push the empty commit back into the bare repo
# This is what makes the bare repo usable as a submodule source
git push

cd /projects

# Repeat the same process for PowerAut0mater
git clone .git/local-submodules/PowerAut0mater.git /tmp/PowerAut0mater
cd /tmp/PowerAut0mater && git commit --allow-empty -m "init" && git push && cd /projects

# Repeat the same process for Shared
git clone .git/local-submodules/Shared.git /tmp/Shared
cd /tmp/Shared && git commit --allow-empty -m "init" && git push && cd /projects

# Delete all three temporary clones β€” only needed them to seed the bare repos
# -r β†’ recursive (required for directories)
# -f β†’ force (skip confirmation prompts)
rm -rf /tmp/Middlewares /tmp/PowerAut0mater /tmp/Shared

Step 3 β€” Add Submodules to the Main Repo

# Register each bare repo as a submodule of the main repo
# git submodule add <url> <folder>
#   <url>    β†’ path to the submodule's git repo (local path or remote URL)
#   <folder> β†’ where it will appear in the working tree
#
# This command does three things automatically:
#   1. Clones the bare repo into the specified folder
#   2. Creates / updates the .gitmodules file with the path <-> url mapping
#   3. Stages both the .gitmodules change and the new submodule folder pointer
#
# ⚠️  IMPORTANT: the URL must explicitly start with ./
#     Git requires local paths to begin with ./ or ../
#     to distinguish them from remote hostnames
#
#     ❌ git submodule add .git/local-submodules/Middlewares.git Middlewares
#        fatal: repo URL: must be absolute or begin with ./|../
#
#     βœ… git submodule add ./.git/local-submodules/Middlewares.git Middlewares
#        Cloning into '...' done.
git submodule add ./.git/local-submodules/Middlewares.git Middlewares
git submodule add ./.git/local-submodules/PowerAut0mater.git PowerAut0mater
git submodule add ./.git/local-submodules/Shared.git Shared

Step 4 β€” Initial Commit

# Stage everything: .gitmodules + all three submodule folder pointers
git add .

# Commit β€” this snapshot records which exact commit each submodule is pinned to
git commit -m "Initial commit with submodules"

Step 5 β€” Verify

# Print the contents of .gitmodules to confirm all three submodules are registered
# .gitmodules is the config file Git uses to track submodule paths and their source URLs
cat .gitmodules

Expected output:

[submodule "Middlewares"]
    path = Middlewares
    url = ./.git/local-submodules/Middlewares.git

[submodule "PowerAut0mater"]
    path = PowerAut0mater
    url = ./.git/local-submodules/PowerAut0mater.git

[submodule "Shared"]
    path = Shared
    url = ./.git/local-submodules/Shared.git

Later β€” Switching to Real Remote URLs

When you have real remote URLs, run the following from inside the main repo:

Step A β€” Update the URLs

# Update the URL for each submodule inside .gitmodules
# This only edits the config file β€” it does NOT yet affect the live local clone
git submodule set-url Middlewares https://github.com/you/Middlewares.git
git submodule set-url PowerAut0mater https://github.com/you/PowerAut0mater.git
git submodule set-url Shared https://github.com/you/Shared.git

Step B β€” Sync and Update

# Propagate the URL changes from .gitmodules into .git/config
# Without this, Git still uses the OLD URLs internally even though .gitmodules was updated
#
# .gitmodules = "source of truth" (tracked file, shared with the whole team via commits)
# .git/config  = "active runtime config" (local only, NOT tracked by Git)
#
# sync reads .gitmodules and writes the new URLs into .git/config to make them active
git submodule sync

# Stage the updated .gitmodules so the URL change is recorded in history
git add .gitmodules

# Commit the URL change so teammates get the correct URLs when they pull
git commit -m "Update submodule remote URLs"

# Fetch and checkout the correct commit for each submodule from the new remote URLs
# --init      β†’ initializes any submodule not yet set up locally (safe to re-run anytime)
# --recursive β†’ also handles nested submodules (submodules inside submodules)
git submodule update --init --recursive

Step C β€” Optional Cleanup

# Remove the temporary bare repos β€” no longer referenced by anything
# All submodules now point to real remote URLs so this folder is dead weight
rm -rf .git/local-submodules

Your final structure is exactly:

.
β”œβ”€β”€ .git
β”œβ”€β”€ .gitmodules
β”œβ”€β”€ Middlewares      (submodule β†’ real remote)
β”œβ”€β”€ PowerAut0mater   (submodule β†’ real remote)
└── Shared           (submodule β†’ real remote)

Quick Reference

StageCommandWhat it does
Init main repogit init -b mainCreates .git/, sets default branch to main
Create bare reposgit init -b main --bare .git/local-submodules/<n>.gitRepo with no working tree β€” push/clone only
Seed bare repoclone β†’ empty commit β†’ push β†’ rm tempGives bare repo a valid HEAD so it can be used as a source
Add submodulegit submodule add ./<url> <folder>Clones + registers path and url in .gitmodules
Update to real URLgit submodule set-url <n> <url>Edits .gitmodules with the new remote URL
Sync URLsgit submodule syncCopies URLs from .gitmodules into .git/config (makes them active)
Pull from remotesgit submodule update --init --recursiveFetches and checks out correct commit from remote
Cleanup temp reposrm -rf .git/local-submodulesDeletes local bare repos, no longer needed

PFX/PKCS12 Certificate Management Guide

This guide provides comprehensive instructions for managing SSL/TLS certificates in PFX (PKCS12) format, including extraction, verification, and conversion to formats commonly used by web servers like Nginx, Apache, and Traefik.

Prerequisites

  • OpenSSL installed on your system
  • Access to your PFX file
  • Password for the PFX file (if protected)

Understanding PFX Files

What is a PFX file?

PFX (Personal Information Exchange) or PKCS#12 is a binary format that bundles:

  • Private Key – Used to decrypt traffic
  • Certificate – Your domain/server certificate (leaf certificate)
  • Certificate Chain – Intermediate and optionally root CA certificates

Why extract components from PFX?

Many web servers and applications require certificates in PEM format with separate or combined files:

  • Nginx: Requires fullchain.pem and privkey.pem
  • Apache: Requires certificate, private key, and chain as separate files
  • Traefik: Can use either PFX or PEM formats
  • HAProxy: Requires combined certificate + key file
  • Docker containers: Often expect PEM format

Common Use Cases

  1. Setting up SSL/TLS on web servers (Nginx, Apache, Traefik)
  2. Migrating certificates between different platforms
  3. Verifying certificate chain completeness before deployment
  4. Converting Windows-exported certificates to Linux-compatible format
  5. Troubleshooting SSL certificate issues

Working with PFX Files

Setup Environment

First, set your PFX password as an environment variable for convenience:

export PFX_PASSWORD='your_password_here'

Note: For empty passwords, use: export PFX_PASSWORD=''

Check if PFX Contains Full Chain

Before extracting, verify that your PFX file contains the complete certificate chain.

Method 1: Count Certificates

openssl pkcs12 -in star.yourfile.pfx -nodes -nokeys -passin pass:$PFX_PASSWORD | grep -c "BEGIN CERTIFICATE"

Expected Results:

  • 1 = Only your certificate (⚠️ incomplete chain)
  • 2 = Your certificate + 1 intermediate CA (βœ… typical)
  • 3+ = Your certificate + multiple intermediates (βœ… complete chain)

Method 2: Display Certificate Details

openssl pkcs12 -in star.yourfile.pfx -nodes -nokeys -passin pass:$PFX_PASSWORD -info

This command displays:

  • All certificates with their subject and issuer
  • Certificate validity dates
  • Complete certificate chain hierarchy

Extract Components from PFX

Extract Full Certificate Chain (fullchain.pem)

This creates a file containing your certificate and all intermediate certificates.

openssl pkcs12 -in star.yourfile.pfx -out star.yourfile.pem -nodes -nokeys -passin pass:$PFX_PASSWORD

Flags Explained:

  • -in – Input PFX file
  • -out – Output PEM file
  • -nodes – Don’t encrypt the output (removes passphrase)
  • -nokeys – Export only certificates, not the private key
  • -passin pass:$PFX_PASSWORD – Provide password non-interactively

Output: star.yourfile.pem (fullchain.pem)

Extract Private Key (privkey.pem)

This extracts only the private key from the PFX file.

openssl pkcs12 -in star.yourfile.pfx -out star.yourfile.key -nodes -nocerts -passin pass:$PFX_PASSWORD

Flags Explained:

  • -nocerts – Export only the private key, not certificates
  • -nodes – Output key without encryption

Output: star.yourfile.key (privkey.pem)

⚠️ Security Warning: Protect this file! Set proper permissions:

chmod 600 star.yourfile.key

Extract Everything in One File

If you need certificate + chain + private key in a single file:

openssl pkcs12 -in star.yourfile.pfx -out combined.pem -nodes -passin pass:$PFX_PASSWORD

Use Case: HAProxy, some load balancers

Verification Methods

1. View Certificate Subjects and Issuers

openssl crl2pkcs7 -nocrl -certfile star.yourfile.pem | openssl pkcs7 -print_certs -noout

What to look for:

  • Each certificate’s subject should match the next certificate’s issuer
  • Forms a chain from your certificate to the root CA

Example Output:

subject=CN=*.example.com
issuer=CN=Intermediate CA

subject=CN=Intermediate CA
issuer=CN=Root CA

2. Verify Certificate Chain Integrity

openssl verify -CAfile star.yourfile.pem star.yourfile.pem

Expected Output:

star.yourfile.pem: OK

If verification fails, your chain is incomplete or corrupted.

3. Check Certificate Expiration

openssl x509 -in star.yourfile.pem -noout -dates

Output:

notBefore=Jan  1 00:00:00 2024 GMT
notAfter=Dec 31 23:59:59 2025 GMT

4. Display Certificate Details

openssl x509 -in star.yourfile.pem -text -noout

Shows complete certificate information including:

  • Subject and Issuer
  • Validity period
  • Subject Alternative Names (SANs)
  • Key usage
  • Extensions

5. List All Certificates in Chain

openssl storeutl -certs star.yourfile.pem

Displays each certificate in the chain with its details.

Creating Full Chain Certificates

Scenario 1: PFX Missing Intermediate Certificates

If your PFX only contains the leaf certificate, you need to add the chain manually.

Step 1: Extract components

# Extract certificate
openssl pkcs12 -in star.yourfile.pfx -out cert.pem -nodes -nokeys -clcerts -passin pass:$PFX_PASSWORD

# Extract private key
openssl pkcs12 -in star.yourfile.pfx -out privkey.pem -nodes -nocerts -passin pass:$PFX_PASSWORD

Step 2: Obtain intermediate certificates

Download the chain file from your Certificate Authority (CA):

  • Let’s Encrypt: Included automatically
  • DigiCert, Sectigo, etc.: Available in your CA account
  • Or download from: https://www.example-ca.com/chain.pem

Step 3: Combine certificate with chain

cat cert.pem chain.pem > fullchain.pem

Step 4: Create new PFX with complete chain

openssl pkcs12 -export -out newfile.pfx \
  -inkey privkey.pem \
  -in cert.pem \
  -certfile chain.pem \
  -name "*.example.com" \
  -passout pass:$PFX_PASSWORD

Scenario 2: Already Have Separate Files

If you have certificate and chain as separate files:

# Combine certificate and chain
cat star_asb_bh.crt star_asb_bh-chain.pem > fullchain.pem

# Verify the chain
grep -c "BEGIN CERTIFICATE" fullchain.pem
openssl verify -CAfile fullchain.pem fullchain.pem

Optional: Create PFX from separate files:

openssl pkcs12 -export -out newfile.pfx \
  -inkey star_asb_bh.key \
  -in star_asb_bh.crt \
  -certfile star_asb_bh-chain.pem \
  -name "*.asb.bh" \
  -passout pass:$PFX_PASSWORD

Alternative Scenarios

Using with Nginx

Create the required files:

# Full chain certificate
openssl pkcs12 -in star.yourfile.pfx -out /etc/nginx/ssl/fullchain.pem -nodes -nokeys -passin pass:$PFX_PASSWORD

# Private key
openssl pkcs12 -in star.yourfile.pfx -out /etc/nginx/ssl/privkey.pem -nodes -nocerts -passin pass:$PFX_PASSWORD

# Set permissions
chmod 644 /etc/nginx/ssl/fullchain.pem
chmod 600 /etc/nginx/ssl/privkey.pem

Nginx configuration:

server {
    listen 443 ssl;
    server_name example.com;
    
    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
}

Using with Traefik

Option 1: Use PFX directly (if Traefik supports it)

tls:
  stores:
    default:
      defaultCertificate:
        certFile: /path/to/star.yourfile.pfx

Option 2: Convert to PEM format

# Extract both cert and key
openssl pkcs12 -in star.yourfile.pfx -out combined.pem -nodes -passin pass:$PFX_PASSWORD

Traefik configuration:

tls:
  certificates:
    - certFile: /path/to/fullchain.pem
      keyFile: /path/to/privkey.pem

Using with Apache

# Certificate
openssl pkcs12 -in star.yourfile.pfx -out /etc/apache2/ssl/cert.pem -nodes -nokeys -clcerts -passin pass:$PFX_PASSWORD

# Private Key
openssl pkcs12 -in star.yourfile.pfx -out /etc/apache2/ssl/privkey.pem -nodes -nocerts -passin pass:$PFX_PASSWORD

# Chain (intermediate certificates)
openssl pkcs12 -in star.yourfile.pfx -out /etc/apache2/ssl/chain.pem -nodes -nokeys -cacerts -passin pass:$PFX_PASSWORD

Apache configuration:

<VirtualHost *:443>
    SSLEngine on
    SSLCertificateFile /etc/apache2/ssl/cert.pem
    SSLCertificateKeyFile /etc/apache2/ssl/privkey.pem
    SSLCertificateChainFile /etc/apache2/ssl/chain.pem
</VirtualHost>

Troubleshooting

Error: β€œwrong tag” or β€œnested asn1 error”

Cause: Corrupted file, wrong format, or legacy encryption

Solutions:

# Try with legacy provider (OpenSSL 3.x)
openssl pkcs12 -in star.yourfile.pfx -nodes -nokeys -legacy -passin pass:$PFX_PASSWORD

# Try with both providers
openssl pkcs12 -in star.yourfile.pfx -nodes -nokeys -provider legacy -provider default -passin pass:$PFX_PASSWORD

Error: File shows all zeros (0x00)

Check file integrity:

hexdump -C star.yourfile.pfx | head -20

Valid PFX should start with: 30 82 or 30 80 or 30 84

If all zeros: File is corrupted. Re-download or re-export from source.


Error: β€œMAC verification failed”

Cause: Incorrect password

Solution:

  • Verify your password
  • Try empty password: export PFX_PASSWORD=''
  • Re-export the PFX with known password

Certificate Chain Verification Fails

Check the chain order:

openssl storeutl -certs fullchain.pem

Expected order:

  1. Your domain certificate (leaf)
  2. Intermediate CA certificate(s)
  3. Root CA (optional)

Fix incorrect order:

# Manually reorder certificates in a text editor
# Ensure leaf certificate comes first

Missing Intermediate Certificates

Symptoms:

  • Browser shows β€œNET::ERR_CERT_AUTHORITY_INVALID”
  • SSL Labs test shows β€œChain issues”
  • grep -c "BEGIN CERTIFICATE" returns only 1

Solution: Download intermediate certificate from your CA and combine:

cat your-cert.pem intermediate.pem > fullchain.pem

Quick Reference Commands

# Set password
export PFX_PASSWORD='your_password'

# Check certificate count
openssl pkcs12 -in file.pfx -nodes -nokeys -passin pass:$PFX_PASSWORD | grep -c "BEGIN CERTIFICATE"

# Extract full chain
openssl pkcs12 -in file.pfx -out fullchain.pem -nodes -nokeys -passin pass:$PFX_PASSWORD

# Extract private key
openssl pkcs12 -in file.pfx -out privkey.pem -nodes -nocerts -passin pass:$PFX_PASSWORD

# Verify chain
openssl verify -CAfile fullchain.pem fullchain.pem

# View certificate details
openssl x509 -in fullchain.pem -text -noout

# Check expiration
openssl x509 -in fullchain.pem -noout -dates

# Create PFX from separate files
openssl pkcs12 -export -out new.pfx -inkey privkey.pem -in cert.pem -certfile chain.pem

# Combine certificate files
cat cert.pem chain.pem > fullchain.pem

Security Best Practices

  1. Protect Private Keys

    chmod 600 privkey.pem
    chown root:root privkey.pem
    
  2. Never commit certificates to version control

    # Add to .gitignore
    *.pfx
    *.pem
    *.key
    *.crt
    
  3. Use strong passwords for PFX files

    • Minimum 12 characters
    • Mix of letters, numbers, symbols
  4. Regularly rotate certificates

    • Monitor expiration dates
    • Automate renewal where possible
  5. Store backups securely

    • Encrypted storage
    • Access control
    • Regular backup verification

Additional Resources


License

This documentation is provided as-is for educational and reference purposes.


Last Updated: January 2026

How to Use Let’s Encrypt on Windows Server with IIS

This guide explains how to enable a FREE SSL certificate using Let’s Encrypt on a Windows Server running IIS. Specifically, it addresses the challenges of using wildcard certificates for multiple websites with different domains and subdomains.

Scenario:

You have a Windows Server 2019 with IIS 10, a single IP address, and multiple HTTPS websites hosted with different domain names. For subdomains under the same primary domain (like *.example.com), a wildcard certificate works perfectly. However, complications arise when adding websites from different domains, such as mydomain.com.

Initial Setup Example:

IIS 10 hosts the following sites:

  • ABC Server (Website)
    • abc.api.example.com – HTTPS @ 443
  • ABC Client (Website)
    • abc.example.com – HTTPS @ 443
    • bcd.example.com – HTTPS @ 443
    • cde.example.com – HTTPS @ 443
    • admin.example.com – HTTPS @ 443
  • XYZ App (Website)
    • xyz.example.com – HTTPS @ 443
  • SEQ (Website)
    • seq.mydomain.com – HTTPS @ 443

Managing these SSL certificates with multiple domain names can be tricky, but Let’s Encrypt simplifies the process.

Steps to Enable Let’s Encrypt SSL on IIS

  • Enable IIS and Create the .well-known Folder
    • Follow this guide to create the .well-known directory for SSL validation
      • Create a folder on the C drive named well-known. Inside, create another folder called pki-validation. Example: C:\well-known\pki-validation.
      • Place the required validation file in the pki-validation folder.
      • Open IIS Manager and for each site, right-click and select Add Virtual Directory.
      • In the Alias field, enter .well-known. In the Physical Path field, enter the path to the folder you created, e.g., C:\well-known\pki-validation.
      • Confirm with OK. The folder and files should now be accessible via the web.
  • Set Proper Permissions for the C:\well-known\pki-validation Folder
    • Follow this IIS 403 Forbidden solution:
      • Right-click the .well-known folder and select Properties.
      • Navigate to the Security tab.
      • Click Edit and ensure IIS_IUSRS is listed. If not, click Add
      • In the Enter the object names box, type IIS_IUSRS and click OK.
      • Set Read & execute, List folder contents, and Read permissions for IIS_IUSRS.
  • Validate DNS Entries for Each Domain/Subdomain
    • Use a tool like Google Dig to validate DNS entries for the following domains:
      • abc.api.example.com
      • abc.example.com
      • bcd.example.com
      • cde.example.com
      • admim.example.com
      • xyz.example.com
      • seq.mydomain.com
  • Download and Install win-acme
    • Download win-acme from https://www.win-acme.com.
    • After downloading, unblock the files and extract them to C:\win-acme.
  • Run win-acme to Generate SSL Certificates
    • Navigate to C:\win-acme and run win-acme.exe as Administrator.
    • Follow the prompts to select the appropriate site for which you want to generate the SSL certificate.
    • Once complete, your sites will be secured with Let’s Encrypt SSL certificates.

By following these steps, you can manage multiple websites with different domains and subdomains on a single IIS server with Let’s Encrypt SSL certificates, solving the issues typically associated with wildcard certificates for different domains.

Other Resources