Skip to content

About

Shared GitHub Actions for deploying the FIRST Global api and results apps to PM2

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fgc-actions

Shared GitHub Actions for deploying the FIRST Global apps (first-global-api and first-global-results) to the PM2 host.

Two composite actions:

Action Purpose
ci-gate Answers "have all of this repo's CI workflows gone green at this commit?"
deploy Ships a built tree to the VPS as a new release and reloads PM2.

This repository is public so that first-global-results — itself public — can consume it. GitHub only shares actions from a private repository with other private repositories, so a private version here would need a PAT-checkout workaround in every workflow. Nothing secret lives in this repo: hosts, keys, and paths are all passed in as inputs.


Deployment model

Each app has a deploy root holding immutable releases and the files that exist only on the server:

/home/webserver/deploy/web/
  releases/
    20260817T203344-a1b2c3d4e5f6/     <- unpacked tarball + npm ci --omit=dev
    20260817T191002-9f8e7d6c5b4a/
  shared/
    .env                              <- symlinked into every release

/home/webserver/web -> deploy/web/releases/20260817T203344-a1b2c3d4e5f6

/home/webserver/web is the path PM2 already knows. It becomes a symlink, and deploying means pointing it at a new release and reloading. PM2 stores pm_cwd and pm_exec_path verbatim and re-resolves them when it spawns, so keeping that path string identical is what lets the processes survive without being re-registered.

A deploy is: unpack → symlink shared files → npm ci --omit=dev → atomically swap the symlink → pm2 reload → health check → prune to the newest 5. If the health check fails, the symlink goes back to the previous release and PM2 is reloaded again.

Targets

App PM2 process Path PM2 points at Deploy root
api (prod) FGC-API /home/webserver/api /home/webserver/deploy/api
api (stage) FGC API Stage /home/webserver/api_stage /home/webserver/deploy/api_stage
web (prod) FGC-WEB /home/webserver/web /home/webserver/deploy/web
web (stage) FGC WEB Stage /home/webserver/web_stage /home/webserver/deploy/web_stage

One-time server migration

Run once per app, as webserver, before the first deploy. deploy refuses to run against a path that is still a real directory, so a half-done migration fails loudly rather than leaving PM2 on stale code.

Start with api_stage — it is the cheapest place to discover that PM2 disagrees.

APP=api_stage                 # then web_stage, then api, then web
PM2_NAME='FGC API Stage'      # then 'FGC WEB Stage', 'FGC-API', 'FGC-WEB'

SRC="/home/webserver/$APP"
ROOT="/home/webserver/deploy/$APP"

# 1. Record what PM2 currently believes, so a regression is obvious.
pm2 describe "$PM2_NAME" | tee "$HOME/pm2-$APP.before.txt"

# 2. Stage the new layout and preserve the server-only .env.
mkdir -p "$ROOT/releases" "$ROOT/shared"
cp "$SRC/.env" "$ROOT/shared/.env"

# 3. Move the existing checkout in as the first release, then put a symlink back
#    at the path PM2 knows.
mv "$SRC" "$ROOT/releases/00000000000000-legacy"
ln -s "$ROOT/releases/00000000000000-legacy" "$SRC"

# 4. The legacy release keeps its own .env file; replace it with the shared link
#    so it matches every release that follows.
rm -f "$ROOT/releases/00000000000000-legacy/.env"
ln -s "$ROOT/shared/.env" "$ROOT/releases/00000000000000-legacy/.env"

# 5. Confirm nothing moved out from under PM2.
pm2 reload "$PM2_NAME" --update-env
pm2 describe "$PM2_NAME"

Compare against pm2-$APP.before.txt: exec cwd and script path should read the same as before, and the process should be online. Verify the app actually answers before moving to the next one.

After all four, persist the process list:

pm2 save

SSH access

Generate a key pair for CI (no passphrase — Actions cannot answer a prompt):

ssh-keygen -t ed25519 -C 'github-actions-deploy' -f ~/fgc-deploy -N ''
cat ~/fgc-deploy.pub >> /home/webserver/.ssh/authorized_keys

The private half goes into the DEPLOY_SSH_KEY secret in both repos. Capture the host key for DEPLOY_KNOWN_HOSTS from a trusted machine:

ssh-keyscan -H <vps-host> 2>/dev/null

Host key checking is deliberately left on, so this secret is required. GitHub-hosted runners have dynamic egress addresses, so port 22 has to be broadly reachable — pair this with PasswordAuthentication no and fail2ban.


Usage

ci-gate

jobs:
  gate:
    runs-on: ubuntu-latest
    outputs:
      ok: ${{ steps.gate.outputs.ok }}
    steps:
      - id: gate
        uses: the-orange-alliance/fgc-actions/ci-gate@v1
        with:
          workflows: |
            Build
            Test
          sha: ${{ github.event.workflow_run.head_sha }}
          token: ${{ secrets.GITHUB_TOKEN }}

workflows takes the name: of each workflow, not its filename. A workflow still running yields ok=false; its own completion re-fires the caller, so the last workflow to finish is the one that proceeds. Nothing deploys if any of them failed.

deploy

- uses: the-orange-alliance/fgc-actions/deploy@v1
  with:
    source-dir: release
    host: ${{ secrets.DEPLOY_HOST }}
    ssh-user: ${{ vars.DEPLOY_USER }}
    ssh-key: ${{ secrets.DEPLOY_SSH_KEY }}
    known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
    deploy-root: ${{ vars.DEPLOY_ROOT }}
    app-path: ${{ vars.APP_PATH }}
    pm2-name: ${{ vars.PM2_NAME }}
    health-url: ${{ vars.HEALTH_URL }}

Everything under source-dir is shipped. Shape it in the build job — usually by downloading an artifact into a directory — rather than filtering here.

Input Default Notes
source-dir . Contents become the release.
host — VPS host or IP.
ssh-user webserver Must own the PM2 daemon.
ssh-port 22
ssh-key — Unencrypted private key.
known-hosts — ssh-keyscan -H output. Required.
deploy-root — Holds releases/ and shared/.
app-path — The symlink PM2 points at. Must already be a symlink.
pm2-name — May contain spaces.
release-id timestamp + short SHA Re-deploying the same id replaces that release.
npm-install true Runs npm ci --omit=dev in the release.
shared-files .env Newline-separated; symlinked from shared/.
keep-releases 5 The live release is never pruned.
health-url none Polled after reload; failure triggers rollback.
health-retries 10
health-delay 3 Seconds between attempts.

node, npm, and pm2 must be on PATH in ~/.bashrc for the SSH user, which is what sshd sources when running a remote command. The remote script checks this up front and refuses to start otherwise.

This is deliberately not a login shell. A host can easily carry two node installs — one on ~/.bashrc (where pm2 is, and what the PM2 daemon was started from) and a different one on ~/.profile. Reading the login shell would install dependencies against one node and run the app under another, and may not find pm2 at all.


Rollback

Releases are kept, so rolling back is repointing the symlink:

ROOT=/home/webserver/deploy/web
ls -1t "$ROOT/releases"                       # newest first
ln -sfn "$ROOT/releases/<previous>" "$ROOT/.rollback"
mv -Tf "$ROOT/.rollback" /home/webserver/web
pm2 reload 'FGC-WEB' --update-env

The next successful deploy supersedes it. Note that a rolled-back release still counts toward keep-releases, and the live one is never pruned.

Versioning

Consumers pin @v1. The v1 tag moves forward for backwards-compatible changes; a breaking change to inputs gets a v2.

About

Shared GitHub Actions for deploying the FIRST Global api and results apps to PM2

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages