Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/scripts/deploy-services.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT"

export PATH="/usr/local/go/bin:${PATH:-}"

BRANCH="${1:-main}"
FORCE_APPS="${DEPLOY_FORCE_APPS:-0}"

OLD_SHA="$(git rev-parse HEAD)"
log() {
echo "$*" >&2
}

log "Fetching and updating ${BRANCH} (was ${OLD_SHA:0:7})..."
git fetch origin "$BRANCH"
git checkout "$BRANCH"
git pull origin "$BRANCH"
NEW_SHA="$(git rev-parse HEAD)"
log "Now at ${NEW_SHA:0:7}"

DETERMINE_ARGS=("$OLD_SHA" "$NEW_SHA")
if [[ "$FORCE_APPS" == "1" ]]; then
DETERMINE_ARGS=(--force-apps "${DETERMINE_ARGS[@]}")
fi

eval "$(bash ./.github/scripts/determine-deploy-targets.sh "${DETERMINE_ARGS[@]}")"

log "Building tools..."
make tools

for app in $DEPLOY_APPS; do
log "Building ${app}..."
make "$app"
done

for app in $DEPLOY_APPS; do
if systemctl list-unit-files "${app}.service" --no-legend 2>/dev/null | grep -q "${app}.service"; then
log "Restarting ${app}..."
sudo systemctl restart "${app}"
else
log "Skipping ${app} restart (no systemd unit)"
fi
done

log "Deploy complete (${NEW_SHA:0:7})"
128 changes: 128 additions & 0 deletions .github/scripts/determine-deploy-targets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# Decide which Services binaries need rebuilding between two git refs.
#
# Tools are always rebuilt. Apps (hermes, atlas, zeus) are rebuilt only when
# changed files touch their source tree or Go dependency closure.
#
# Usage:
# determine-deploy-targets.sh [--force-apps] [BASE_REF] [HEAD_REF]
#
# Prints shell assignments to stdout:
# DEPLOY_TOOLS=1
# DEPLOY_APPS="hermes atlas"
#
# Logs analysis details to stderr.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT"

FORCE_APPS=0
POSITIONAL=()

while [[ $# -gt 0 ]]; do
case "$1" in
--force-apps)
FORCE_APPS=1
shift
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
POSITIONAL+=("$1")
shift
;;
esac
done

BASE_REF="${POSITIONAL[0]:-HEAD~1}"
HEAD_REF="${POSITIONAL[1]:-HEAD}"

ALL_APPS=(hermes atlas zeus)
DEPLOY_APPS=()

log() {
echo "$*" >&2
}

app_dep_dirs() {
local app="$1"
go list -deps -f '{{.Dir}}' "./apps/${app}/" | grep "^${REPO_ROOT}/" || true
}

file_affects_app() {
local file="$1"
local app="$2"
local abs="${REPO_ROOT}/${file}"

[[ "$file" == apps/${app}/* ]] && return 0

local dir
while IFS= read -r dir; do
[[ -z "$dir" ]] && continue
if [[ "$abs" == "${dir}/"* ]] || [[ "$abs" == "$dir" ]]; then
return 0
fi
done < <(app_dep_dirs "$app")

return 1
}

rebuild_all_apps() {
local reason="$1"
log "Rebuilding all apps: ${reason}"
DEPLOY_APPS=("${ALL_APPS[@]}")
}

if [[ "$FORCE_APPS" -eq 1 ]]; then
rebuild_all_apps "forced"
elif [[ "$BASE_REF" == "0000000000000000000000000000000000000000" ]]; then
rebuild_all_apps "no previous commit (force-push or first deploy)"
else
CHANGED_FILES=()
while IFS= read -r file; do
[[ -n "$file" ]] && CHANGED_FILES+=("$file")
done < <(git diff --name-only "$BASE_REF" "$HEAD_REF")

if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
log "No changed files between ${BASE_REF} and ${HEAD_REF}"
else
log "Changed files (${#CHANGED_FILES[@]}):"
for file in "${CHANGED_FILES[@]}"; do
log " - ${file}"
done

for file in "${CHANGED_FILES[@]}"; do
case "$file" in
go.mod | go.sum | Makefile)
rebuild_all_apps "${file} changed"
break
;;
esac
done

if [[ ${#DEPLOY_APPS[@]} -eq 0 ]]; then
for app in "${ALL_APPS[@]}"; do
for file in "${CHANGED_FILES[@]}"; do
if file_affects_app "$file" "$app"; then
log "${app}: affected by ${file}"
DEPLOY_APPS+=("$app")
break
fi
done
done
fi
fi
fi

log "Deploy plan: tools=always apps=${DEPLOY_APPS[*]:-(none)}"

echo "DEPLOY_TOOLS=1"
if [[ ${#DEPLOY_APPS[@]} -eq 0 ]]; then
echo 'DEPLOY_APPS=""'
else
echo "DEPLOY_APPS=\"${DEPLOY_APPS[*]}\""
fi
5 changes: 5 additions & 0 deletions .github/scripts/install-cloudflared.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
curl -L https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-archive-keyring.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update
sudo apt-get install cloudflared
cloudflared --version
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ jobs:
exit 1
fi

- name: Test deploy target detection
run: |
git fetch origin main
bash .github/scripts/determine-deploy-targets.sh origin/main HEAD
bash .github/scripts/determine-deploy-targets.sh --force-apps origin/main HEAD

- name: Build apps and tools
run: |
go build -o /dev/null ./apps/...
Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/deploy-manual.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Manually Deploy Services to Prod

on:
workflow_dispatch:

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check if user is a repo admin
run: |
if [[ $(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission" | jq -r '.permission') != "admin" ]]; then
echo "Only repo admins can run this action."
exit 1
fi

- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: Download dependencies
run: go mod download

- name: Verify build
run: |
go build -o /dev/null ./apps/atlas/
go build -o /dev/null ./apps/hermes/
go build -o /dev/null ./apps/zeus/

- name: Determine deploy targets
run: bash ./.github/scripts/determine-deploy-targets.sh --force-apps

- name: Install cloudflared
run: ./.github/scripts/install-cloudflared.sh

- name: Load SSH Key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.VPS_SSH_PRIVATE_KEY }}
known_hosts: ${{ secrets.VPS_SSH_KNOWN_HOSTS }}

- name: Deploy Services
env:
TUNNEL_SERVICE_TOKEN_ID: ${{ secrets.CF_GHA_CLIENT_ID }}
TUNNEL_SERVICE_TOKEN_SECRET: ${{ secrets.CF_GHA_CLIENT_SECRET }}
run: |
branch_ref="${{ github.ref }}"
branch_name="${branch_ref#refs/heads/}"
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" root@ssh.raidhub.io \
"cd /RaidHub/Services && DEPLOY_FORCE_APPS=1 bash ./.github/scripts/deploy-services.sh \"$branch_name\""
Comment on lines +51 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The branch_name variable, derived from github.ref, is used unsafely in a run block, creating a command injection vulnerability if the branch name contains shell metacharacters.
Severity: HIGH

Suggested Fix

Pass the branch name to the script via an environment variable instead of direct string interpolation. Set branch_name in the env context of the step, and then reference it as $BRANCH_NAME within the script. This prevents the shell from interpreting metacharacters in the branch name during expansion. For example: env: { BRANCH_NAME: ${{ github.ref_name }} } and then use $BRANCH_NAME in the run script. Using github.ref_name is also safer as it only contains the branch/tag name.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: .github/workflows/deploy-manual.yml#L51-L54

Potential issue: The `branch_name` variable is derived from `${{ github.ref }}` and is
directly substituted into a bash `run` block. If a branch name contains shell
metacharacters like `$()` or backticks, it can lead to arbitrary command execution on
the GitHub Actions runner. The `run` block on line 54 executes an SSH command where
`$branch_name` is expanded by the local shell. A malicious branch name like
`feature/fix-$(evil_command)` would cause `evil_command` to run on the runner. While an
admin check mitigates direct exploitation, an admin could unknowingly trigger the
workflow on a malicious branch created by another user with push access.

Did we get this right? 👍 / 👎 to inform future reviews.

49 changes: 49 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Deploy to Prod

on:
push:
branches:
- main

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: Download dependencies
run: go mod download

- name: Verify build
run: |
go build -o /dev/null ./apps/atlas/
go build -o /dev/null ./apps/hermes/
go build -o /dev/null ./apps/zeus/

Comment on lines +25 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The deploy workflow will fail because actions/checkout creates a shallow clone, but a later step requires more git history to run git diff.
Severity: CRITICAL

Suggested Fix

In the deploy.yml workflow, configure the actions/checkout@v4 step with fetch-depth: 0 to ensure the full git history is available. This will allow the git diff command to access the previous commit and execute successfully.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: .github/workflows/deploy.yml#L25-L28

Potential issue: The `deploy.yml` workflow uses `actions/checkout@v4` without specifying
`fetch-depth`, which defaults to a shallow clone of depth 1. A subsequent step in the
workflow attempts to run `git diff` using the previous commit SHA
(`github.event.before`). Because this previous commit is not available in the shallow
clone, the `git diff` command fails with a `fatal: bad object` error. This failure halts
the entire workflow, preventing any deployments from running on pushes to the `main`
branch.

- name: Determine deploy targets
run: |
bash ./.github/scripts/determine-deploy-targets.sh \
"${{ github.event.before }}" "${{ github.sha }}"

- name: Install cloudflared
run: ./.github/scripts/install-cloudflared.sh

- name: Load SSH Key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.VPS_SSH_PRIVATE_KEY }}
known_hosts: ${{ secrets.VPS_SSH_KNOWN_HOSTS }}

- name: Deploy Services
env:
TUNNEL_SERVICE_TOKEN_ID: ${{ secrets.CF_GHA_CLIENT_ID }}
TUNNEL_SERVICE_TOKEN_SECRET: ${{ secrets.CF_GHA_CLIENT_SECRET }}
run: |
ssh -o ProxyCommand="cloudflared access ssh --hostname %h" root@ssh.raidhub.io \
"cd /RaidHub/Services && bash ./.github/scripts/deploy-services.sh main"
Loading