Using GitHub as an Encrypted Forgejo Backup
I don't like the idea of putting all of my eggs in one basket. Imagine putting your life's work onto some SSD you got off of Amazon without a second thought. Maybe you've got a fancy RAID setup so hardware failure doesn't apply to you? That means nothing if your hardware gets destroyed by a fire or stolen by a rogue gaggle of gnomes. That got me thinking: how can I back up my repos elsewhere if I don't trust other people with my code?
The Idea
I love self-hosting as much as I can; honestly, after GitHub announced they would charge for the use of self-hosted runners, I think that was the final straw. I did my research and landed on Forgejo, but how can I add some redundancy? What if I could still use GitHub to host my code, but now allow Microsoft to get their grubby LLM training hands on it? What if I could encrypt my code and commit it onto GitHub as a mirror of my private repositories???
Is This Against TOS?
As of writing, the GitHub terms of service (TOS) doesn't explicitly forbid uploading encrypted blobs... so we're good!!
How Should We Encrypt It?
A password should be used for the encryption. The reason we use a password over anything else is that you can easily remember it. If you all of a sudden lose all of your hardware that you host your git on, you can still get it all back as long as you've remembered the password. The password doesn't even have to be secure (it could just be "password"), since the whole reason I wanted to encrypt stuff in the first place was to prevent LLMs from being trained off of my code.
As for the method of encryption, I decided to use GPG!
The Implementation
I created a new repository on Forgejo that acted as a reusable composite action. The repository just consists of a README.md and an action.yml. If you haven’t used composite actions before, here’s the simplest way to think about them:
A composite action is basically “a little script packaged like an action”.
It’s a list of shell steps that you can re-use in lots of repos without copy/pasting.
Below is the action, but instead of dumping it and running away, we’ll walk through it step-by-step.
Action Configuration
Before we get into the steps, here's what you should configure.
Inputs
These are values you pass in your workflow YAML:
gh_owner(required): the GitHub username or org that owns the backup repo.target_repo(optional): the name of the backup repo. If blank, it uses the same name as the source repo.target_branch(optional): the branch to push to on GitHub (defaults to main).include_forgejo_dir(optional): whether to include the .forgejo/ too.create_repo(optional): if true, the action can create the GitHub repo for you.
Environment Variables and Secrets
This action expects two secrets to exist as environment variables:
MIRROR_PASSWORD: a passphrase used to encrypt files.GH_PAT: a GitHub Personal Access Token used to push (and optionally create repos).
It also needs a token to clone the source repo inside the runner:
FORGEJO_TOKEN(preferred), orGITHUB_TOKEN(fallback)
Why both? Because different runners / setups expose tokens differently. This action tries to “just work” in both Forgejo and GitHub-style environments.
Step 1: Install Tools
This step installs everything the runner needs: git, gpg, curl, jq, etc.
- name: Install tools
shell: sh
run: |
set -euo pipefail
apk add --no-cache \
git gnupg coreutils findutils \
curl jq ca-certificates
update-ca-certificates- We’re on an Alpine-based runner, so we use
apk. gpgis the encryption tool.curl+jqare used to talk to GitHub’s API and parse JSON.set -euo pipefailmakes the shell scripts "fail loudly”:-e: exit on error-u: error if you use an unset variable (prevents sneaky bugs)pipefail: if something fails in a pipeline, treat it as failure
Step 2: Checkout the Repo (manually)
Normally you’d use actions/checkout. But this is Forgejo + act friendly, so it clones manually.
- name: Checkout (manual, Forgejo/act-safe)
shell: sh
env:
FORGEJO_TOKEN: $
GITHUB_TOKEN: $
run: |
set -euo pipefail
TOKEN="${FORGEJO_TOKEN:-${GITHUB_TOKEN:-}}"
[ -n "${TOKEN:-}" ] || { echo "ERROR: No FORGEJO_TOKEN/GITHUB_TOKEN available for cloning."; exit 1; }
SERVER="${GITHUB_SERVER_URL:-${CI_SERVER_URL:-}}"
REPO="${GITHUB_REPOSITORY:-}"
[ -n "${SERVER:-}" ] || { echo "ERROR: No GITHUB_SERVER_URL/CI_SERVER_URL set by runner."; exit 1; }
[ -n "${REPO:-}" ] || { echo "ERROR: No GITHUB_REPOSITORY set by runner."; exit 1; }
URL="${SERVER}/${REPO}.git"
echo "Cloning: ${URL}"
rm -rf ./* ./.??* 2>/dev/null || true
case "$URL" in
https://*) AUTH_URL="https://x-access-token:${TOKEN}@${URL#https://}" ;;
http://*) AUTH_URL="http://x-access-token:${TOKEN}@${URL#http://}" ;;
*) echo "ERROR: Unexpected URL scheme: $URL"; exit 1 ;;
esac
git clone --depth 1 "$AUTH_URL" .
echo "Checked out commit: $(git rev-parse HEAD)"This picks a token, preferring FORGEJO_TOKEN, but falling back to GITHUB_TOKEN. It then figures out where the repo lives, builds a clone URL, wipes the workspace, and shallow clones the repository.
Step 3: Validate Configuration and Write a State File
This step checks you provided everything, figures out names, normalizes booleans, then writes the key values into mirror.env (which is also encrypted and packaged with the repo).
- name: Validate / Configure
shell: sh
env:
MIRROR_PASSWORD: $
GH_PAT: $
INPUT_GH_OWNER: $
INPUT_TARGET_REPO: $
INPUT_TARGET_BRANCH: $
INPUT_INCLUDE_FORGEJO_DIR: $
INPUT_CREATE_REPO: $
run: |
set -euo pipefail
[ -n "${MIRROR_PASSWORD:-}" ] || { echo "Missing env.MIRROR_PASSWORD"; exit 1; }
[ -n "${GH_PAT:-}" ] || { echo "Missing env.GH_PAT (GitHub PAT)"; exit 1; }
[ -n "${INPUT_GH_OWNER:-}" ] || { echo "Missing input gh_owner"; exit 1; }
echo "::add-mask::${MIRROR_PASSWORD}"
echo "::add-mask::${GH_PAT}"
origin="$(git remote get-url origin 2>/dev/null || true)"
[ -n "$origin" ] || { echo "ERROR: No git origin found after checkout."; exit 1; }
SRC_REPO_NAME="$(printf '%s' "$origin" | sed -E 's#\.git$##; s#^.*[:/]+([^/]+)/([^/]+)$#\2#')"
[ -n "$SRC_REPO_NAME" ] || { echo "ERROR: Failed to derive source repo name from origin: $origin"; exit 1; }
GH_OWNER="${INPUT_GH_OWNER}"
TARGET_REPO="${INPUT_TARGET_REPO:-}"
if [ -z "$TARGET_REPO" ]; then TARGET_REPO="$SRC_REPO_NAME"; fi
TARGET_BRANCH="${INPUT_TARGET_BRANCH:-main}"
INCLUDE_FORGEJO_DIR="${INPUT_INCLUDE_FORGEJO_DIR:-true}"
CREATE_REPO="${INPUT_CREATE_REPO:-false}"
case "$INCLUDE_FORGEJO_DIR" in true|false) ;; "true"|"false") INCLUDE_FORGEJO_DIR="${INCLUDE_FORGEJO_DIR%\"}"; INCLUDE_FORGEJO_DIR="${INCLUDE_FORGEJO_DIR#\"}";; 1) INCLUDE_FORGEJO_DIR="true";; 0) INCLUDE_FORGEJO_DIR="false";; *) INCLUDE_FORGEJO_DIR="true";; esac
case "$CREATE_REPO" in true|false) ;; "true"|"false") CREATE_REPO="${CREATE_REPO%\"}"; CREATE_REPO="${CREATE_REPO#\"}";; 1) CREATE_REPO="true";; 0) CREATE_REPO="false";; *) CREATE_REPO="false";; esac
GH_REPO="${GH_OWNER}/${TARGET_REPO}"
printf 'SRC_REPO_NAME=%s\n' "$SRC_REPO_NAME" > mirror.env
printf 'GH_OWNER=%s\n' "$GH_OWNER" >> mirror.env
printf 'TARGET_REPO=%s\n' "$TARGET_REPO" >> mirror.env
printf 'TARGET_BRANCH=%s\n' "$TARGET_BRANCH" >> mirror.env
printf 'INCLUDE_FORGEJO_DIR=%s\n' "$INCLUDE_FORGEJO_DIR" >> mirror.env
printf 'CREATE_REPO=%s\n' "$CREATE_REPO" >> mirror.env
printf 'GH_REPO=%s\n' "$GH_REPO" >> mirror.env
echo "Configured:"
echo " Source repo: $SRC_REPO_NAME"
echo " GitHub target: $GH_REPO"
echo " Target branch: $TARGET_BRANCH"
echo " Include .forgejo: $INCLUDE_FORGEJO_DIR"
echo " Create repo: $CREATE_REPO"This step fails early if secrets / required inputs are missing and performs some other useful steps:
- ::add-mask:: tells the Actions log: “if you see this value, hide it”.
- It figures out the source repo name from git remote get-url origin.
- It computes the GitHub destination repo name:
- If you didn’t specify target_repo, it uses the same as the source.
- It writes everything to mirror.env.
Why Write mirror.env?
Because each step in a composite action runs in a fresh shell. This is the easiest way to pass “state” from one step to the next without re-calculating everything.
Step 4: Encrypt the Entire Repository
This is the core: take the repo, output a second repo that contains only encrypted blobs.
- name: Encrypt repo (file-by-file)
shell: sh
env:
MIRROR_PASSWORD: $
run: |
set -euo pipefail
umask 077
. ./mirror.env
SRC="$(pwd)"
OUT="$(mktemp -d)"
echo "Using OUT=${OUT}"
if [ "${INCLUDE_FORGEJO_DIR}" = "true" ]; then
FIND_CMD='find . -type f ! -path "./.git/*" -print0'
else
FIND_CMD='find . -type f ! -path "./.git/*" ! -path "./.forgejo/*" ! -path "./.forgejo" -print0'
fi
eval "$FIND_CMD" | while IFS= read -r -d '' f; do
rel="${f#./}"
mkdir -p "$OUT/$(dirname "$rel")"
gpg --batch --yes --pinentry-mode loopback \
--passphrase "$MIRROR_PASSWORD" \
--symmetric --cipher-algo AES256 \
-o "$OUT/$rel.gpg" "$f"
done
cat > "$OUT/README.txt" <<EOF
This repository is an encrypted mirror of \`${SRC_REPO_NAME}\`.
Each original file is stored as <path>.gpg and was encrypted symmetrically with a passphrase.
EOF
count="$(find "$OUT" -type f -name '*.gpg' | wc -l | tr -d ' ')"
echo "Encrypted file count: $count"
[ "$count" != "0" ] || { echo "ERROR: No files were encrypted."; exit 1; }
bad="$(find "$OUT" -type f ! -name '*.gpg' ! -name 'README.txt' | head -n 20 || true)"
if [ -n "$bad" ]; then
echo "Refusing: found unexpected plaintext files in mirror output:"
echo "$bad"
exit 1
fi
printf 'OUT_DIR=%s\n' "$OUT" >> "$SRC/mirror.env"Step 5 (Optional): Create the Remote Repository
This step calls the GitHub API to check if the destination repo exists. If it doesn’t:
- fail (default), or
- create it (if
create_repo=true)
- name: Ensure GitHub repo exists (optional create)
shell: sh
env:
GH_PAT: $
run: |
set -euo pipefail
. ./mirror.env
api="https://api.github.com"
auth_hdr="Authorization: token ${GH_PAT}"
accept="Accept: application/vnd.github+json"
version="X-GitHub-Api-Version: 2022-11-28"
status="$(curl -sS -o /tmp/gh_repo.json -w '%{http_code}' \
-H "$auth_hdr" -H "$accept" -H "$version" \
"${api}/repos/${GH_REPO}" || true)"
echo "Repo check status: $status"
if [ "$status" = "200" ]; then
echo "GitHub repo exists: ${GH_REPO}"
exit 0
fi
if [ "$status" != "404" ]; then
cat /tmp/gh_repo.json || true
echo "ERROR: Unexpected status checking repo."
exit 1
fi
if [ "${CREATE_REPO}" != "true" ]; then
echo "ERROR: GitHub repo missing: ${GH_REPO}"
echo "Create it manually (private), or set create_repo=true (requires PAT perms)."
exit 1
fi
payload="$(jq -n --arg name "$TARGET_REPO" '{
name: $name,
private: true,
has_issues: false,
has_projects: false,
has_wiki: false,
auto_init: false
}')"
create_status="$(curl -sS -o /tmp/gh_create.json -w '%{http_code}' \
-X POST -H "$auth_hdr" -H "$accept" -H "$version" \
-d "$payload" \
"${api}/user/repos" || true)"
echo "Create status: $create_status"
if [ "$create_status" != "201" ]; then
cat /tmp/gh_create.json || true
echo "ERROR: Failed to create repo. Use a classic PAT with 'repo' scope, or pre-create the repo."
exit 1
fi
echo "Created GitHub repo: ${GH_REPO}"Step 6: Convert the Encrypted Blobs into a Repository and Commit
Now we take the $OUT_DIR folder (full of .gpg files), initialize a brand new git repo in it, commit everything, and force push to GitHub.
- name: Push encrypted mirror to GitHub
shell: sh
env:
GH_PAT: $
run: |
set -euo pipefail
. ./mirror.env
: "${OUT_DIR:?OUT_DIR not set}"
: "${GH_REPO:?GH_REPO not set}"
: "${TARGET_BRANCH:?TARGET_BRANCH not set}"
cd "$OUT_DIR"
git init
git config user.name "forgejo-actions"
git config user.email "[email protected]"
git config --global init.defaultBranch main
git add -A
git commit -m "Encrypted mirror @ ${GITHUB_SHA:-unknown}" || true
git branch -M "$TARGET_BRANCH"
git remote add origin "https://x-access-token:${GH_PAT}@github.com/${GH_REPO}.git"
git push --force --set-upstream origin "$TARGET_BRANCH"How Do I Use It?
You just need to include a small amount of configuration in your workflow. Here's an example from one of my projects:
name: Mirror to GitHub
on:
push:
branches: ["main"]
workflow_dispatch: {}
jobs:
mirror:
runs-on: docker
container:
image: node:20-alpine
steps:
- uses: http://git.home/alex.thomson/github-mirror-action@main
with:
gh_owner: alexjthomson
target_branch: main
include_forgejo_dir: true
create_repo: false
env:
GH_PAT: $
MIRROR_PASSWORD: $
FORGEJO_TOKEN: $How Do I Decrypt It?
On your machine (not in CI), you can decrypt a file like:
gpg --output package.json --decrypt package.json.gpgYou’ll be prompted for the passphrase.
If you want to restore the whole repo, you can write a small script that finds all *.gpg files, decrypts them, and strips the .gpg extension.
The Final Action
Who am I to deny you the entire action? Here you go!
name: Encrypted mirror to GitHub
description: Encrypt each file in the repo and push to a private GitHub repo.
inputs:
gh_owner:
description: "GitHub owner/org (required)"
required: true
target_repo:
description: "Target GitHub repo name (blank = same as source repo name)"
required: false
default: ""
target_branch:
description: "Target branch on GitHub"
required: false
default: "main"
include_forgejo_dir:
description: "Encrypt .forgejo directory too (true/false)"
required: false
default: "true"
create_repo:
description: "Create GitHub repo if missing (true/false)"
required: false
default: "false"
runs:
using: "composite"
steps:
- name: Install tools
shell: sh
run: |
set -euo pipefail
apk add --no-cache \
git gnupg coreutils findutils \
curl jq ca-certificates
update-ca-certificates
- name: Checkout (manual, Forgejo/act-safe)
shell: sh
env:
FORGEJO_TOKEN: $
GITHUB_TOKEN: $
run: |
set -euo pipefail
TOKEN="${FORGEJO_TOKEN:-${GITHUB_TOKEN:-}}"
[ -n "${TOKEN:-}" ] || { echo "ERROR: No FORGEJO_TOKEN/GITHUB_TOKEN available for cloning."; exit 1; }
SERVER="${GITHUB_SERVER_URL:-${CI_SERVER_URL:-}}"
REPO="${GITHUB_REPOSITORY:-}"
[ -n "${SERVER:-}" ] || { echo "ERROR: No GITHUB_SERVER_URL/CI_SERVER_URL set by runner."; exit 1; }
[ -n "${REPO:-}" ] || { echo "ERROR: No GITHUB_REPOSITORY set by runner."; exit 1; }
URL="${SERVER}/${REPO}.git"
echo "Cloning: ${URL}"
rm -rf ./* ./.??* 2>/dev/null || true
case "$URL" in
https://*) AUTH_URL="https://x-access-token:${TOKEN}@${URL#https://}" ;;
http://*) AUTH_URL="http://x-access-token:${TOKEN}@${URL#http://}" ;;
*) echo "ERROR: Unexpected URL scheme: $URL"; exit 1 ;;
esac
git clone --depth 1 "$AUTH_URL" .
echo "Checked out commit: $(git rev-parse HEAD)"
- name: Validate / Configure
shell: sh
env:
MIRROR_PASSWORD: $
GH_PAT: $
INPUT_GH_OWNER: $
INPUT_TARGET_REPO: $
INPUT_TARGET_BRANCH: $
INPUT_INCLUDE_FORGEJO_DIR: $
INPUT_CREATE_REPO: $
run: |
set -euo pipefail
[ -n "${MIRROR_PASSWORD:-}" ] || { echo "Missing env.MIRROR_PASSWORD"; exit 1; }
[ -n "${GH_PAT:-}" ] || { echo "Missing env.GH_PAT (GitHub PAT)"; exit 1; }
[ -n "${INPUT_GH_OWNER:-}" ] || { echo "Missing input gh_owner"; exit 1; }
echo "::add-mask::${MIRROR_PASSWORD}"
echo "::add-mask::${GH_PAT}"
origin="$(git remote get-url origin 2>/dev/null || true)"
[ -n "$origin" ] || { echo "ERROR: No git origin found after checkout."; exit 1; }
SRC_REPO_NAME="$(printf '%s' "$origin" | sed -E 's#\.git$##; s#^.*[:/]+([^/]+)/([^/]+)$#\2#')"
[ -n "$SRC_REPO_NAME" ] || { echo "ERROR: Failed to derive source repo name from origin: $origin"; exit 1; }
GH_OWNER="${INPUT_GH_OWNER}"
TARGET_REPO="${INPUT_TARGET_REPO:-}"
if [ -z "$TARGET_REPO" ]; then TARGET_REPO="$SRC_REPO_NAME"; fi
TARGET_BRANCH="${INPUT_TARGET_BRANCH:-main}"
INCLUDE_FORGEJO_DIR="${INPUT_INCLUDE_FORGEJO_DIR:-true}"
CREATE_REPO="${INPUT_CREATE_REPO:-false}"
# Normalise booleans (accept true/false, "true"/"false", 1/0)
case "$INCLUDE_FORGEJO_DIR" in true|false) ;; "true"|"false") INCLUDE_FORGEJO_DIR="${INCLUDE_FORGEJO_DIR%\"}"; INCLUDE_FORGEJO_DIR="${INCLUDE_FORGEJO_DIR#\"}";; 1) INCLUDE_FORGEJO_DIR="true";; 0) INCLUDE_FORGEJO_DIR="false";; *) INCLUDE_FORGEJO_DIR="true";; esac
case "$CREATE_REPO" in true|false) ;; "true"|"false") CREATE_REPO="${CREATE_REPO%\"}"; CREATE_REPO="${CREATE_REPO#\"}";; 1) CREATE_REPO="true";; 0) CREATE_REPO="false";; *) CREATE_REPO="false";; esac
GH_REPO="${GH_OWNER}/${TARGET_REPO}"
printf 'SRC_REPO_NAME=%s\n' "$SRC_REPO_NAME" > mirror.env
printf 'GH_OWNER=%s\n' "$GH_OWNER" >> mirror.env
printf 'TARGET_REPO=%s\n' "$TARGET_REPO" >> mirror.env
printf 'TARGET_BRANCH=%s\n' "$TARGET_BRANCH" >> mirror.env
printf 'INCLUDE_FORGEJO_DIR=%s\n' "$INCLUDE_FORGEJO_DIR" >> mirror.env
printf 'CREATE_REPO=%s\n' "$CREATE_REPO" >> mirror.env
printf 'GH_REPO=%s\n' "$GH_REPO" >> mirror.env
echo "Configured:"
echo " Source repo: $SRC_REPO_NAME"
echo " GitHub target: $GH_REPO"
echo " Target branch: $TARGET_BRANCH"
echo " Include .forgejo: $INCLUDE_FORGEJO_DIR"
echo " Create repo: $CREATE_REPO"
- name: Encrypt repo (file-by-file)
shell: sh
env:
MIRROR_PASSWORD: $
run: |
set -euo pipefail
umask 077
. ./mirror.env
SRC="$(pwd)"
OUT="$(mktemp -d)"
echo "Using OUT=${OUT}"
if [ "${INCLUDE_FORGEJO_DIR}" = "true" ]; then
FIND_CMD='find . -type f ! -path "./.git/*" -print0'
else
FIND_CMD='find . -type f ! -path "./.git/*" ! -path "./.forgejo/*" ! -path "./.forgejo" -print0'
fi
eval "$FIND_CMD" | while IFS= read -r -d '' f; do
rel="${f#./}"
mkdir -p "$OUT/$(dirname "$rel")"
gpg --batch --yes --pinentry-mode loopback \
--passphrase "$MIRROR_PASSWORD" \
--symmetric --cipher-algo AES256 \
-o "$OUT/$rel.gpg" "$f"
done
cat > "$OUT/README.txt" <<EOF
This repository is an encrypted mirror of \`${SRC_REPO_NAME}\`.
Each original file is stored as <path>.gpg and was encrypted symmetrically with a passphrase.
EOF
count="$(find "$OUT" -type f -name '*.gpg' | wc -l | tr -d ' ')"
echo "Encrypted file count: $count"
[ "$count" != "0" ] || { echo "ERROR: No files were encrypted."; exit 1; }
bad="$(find "$OUT" -type f ! -name '*.gpg' ! -name 'README.txt' | head -n 20 || true)"
if [ -n "$bad" ]; then
echo "Refusing: found unexpected plaintext files in mirror output:"
echo "$bad"
exit 1
fi
printf 'OUT_DIR=%s\n' "$OUT" >> "$SRC/mirror.env"
- name: Ensure GitHub repo exists (optional create)
shell: sh
env:
GH_PAT: $
run: |
set -euo pipefail
. ./mirror.env
api="https://api.github.com"
auth_hdr="Authorization: token ${GH_PAT}"
accept="Accept: application/vnd.github+json"
version="X-GitHub-Api-Version: 2022-11-28"
status="$(curl -sS -o /tmp/gh_repo.json -w '%{http_code}' \
-H "$auth_hdr" -H "$accept" -H "$version" \
"${api}/repos/${GH_REPO}" || true)"
echo "Repo check status: $status"
if [ "$status" = "200" ]; then
echo "GitHub repo exists: ${GH_REPO}"
exit 0
fi
if [ "$status" != "404" ]; then
cat /tmp/gh_repo.json || true
echo "ERROR: Unexpected status checking repo."
exit 1
fi
if [ "${CREATE_REPO}" != "true" ]; then
echo "ERROR: GitHub repo missing: ${GH_REPO}"
echo "Create it manually (private), or set create_repo=true (requires PAT perms)."
exit 1
fi
# Create in user namespace (works for user; for org, token must permit and you should adapt endpoint)
payload="$(jq -n --arg name "$TARGET_REPO" '{
name: $name,
private: true,
has_issues: false,
has_projects: false,
has_wiki: false,
auto_init: false
}')"
create_status="$(curl -sS -o /tmp/gh_create.json -w '%{http_code}' \
-X POST -H "$auth_hdr" -H "$accept" -H "$version" \
-d "$payload" \
"${api}/user/repos" || true)"
echo "Create status: $create_status"
if [ "$create_status" != "201" ]; then
cat /tmp/gh_create.json || true
echo "ERROR: Failed to create repo. Use a classic PAT with 'repo' scope, or pre-create the repo."
exit 1
fi
echo "Created GitHub repo: ${GH_REPO}"
- name: Push encrypted mirror to GitHub
shell: sh
env:
GH_PAT: $
run: |
set -euo pipefail
. ./mirror.env
: "${OUT_DIR:?OUT_DIR not set}"
: "${GH_REPO:?GH_REPO not set}"
: "${TARGET_BRANCH:?TARGET_BRANCH not set}"
cd "$OUT_DIR"
git init
git config user.name "forgejo-actions"
git config user.email "[email protected]"
git config --global init.defaultBranch main
git add -A
git commit -m "Encrypted mirror @ ${GITHUB_SHA:-unknown}" || true
git branch -M "$TARGET_BRANCH"
git remote add origin "https://x-access-token:${GH_PAT}@github.com/${GH_REPO}.git"
git push --force --set-upstream origin "$TARGET_BRANCH"