A non-interactive CI/CD pipeline written entirely in Bash. It fetches source code from Bitbucket, packages or compiles it locally, uploads the artifact to a remote server over SSH, extracts it, and runs a service installer on the remote. Build status notifications are sent to Slack.
- Autobuilder
Entry-point script (e.g. dev-api.example.com-my-service.sh)
│
└── Deployer (scripts/nodejs-pm2-deployer.sh, etc.)
│ Defines: AUTOBUILDER_PACKAGER, AUTOBUILDER_INSTALLER,
│ accepted CLI flags, remote installer flag map
│
└── scripts/engine/engine.sh
│
├── helpers/logger.sh logging, email, Slack
├── helpers/dynamic-flags.sh parse CLI args → variables
├── helpers/server-api.sh SSH connect, remoteCommand, remoteUpload
│ └── servers/{server}.sh server credentials
│
├── helpers/fetch-repository.sh git clone/pull from Bitbucket
│
├── packagers/{packager}.sh compile/zip the payload locally
│ └── helpers/zip-validator.sh
│
├── [remoteUpload] payload.zip + payload-extractor.sh + installer.sh
├── [remoteCommand] payload-extractor.sh unzip on remote
└── [remoteCommand] installers/{installer}.sh start service
└── helpers/post-to-slack.sh success/failure notification
The following must be available on the autobuilder host (the machine running the scripts):
| Tool | Purpose |
|---|---|
bash |
Shell runtime |
git |
Repository fetching and version push-back |
ssh / scp |
Remote server access and file upload |
zip / unzip |
Payload packaging and extraction |
nvm |
Node.js version management (auto-installed if missing) |
go |
Required only for Go binary deployments |
mailx |
Optional — log forwarding via email |
dos2unix |
Normalise line endings on uploaded scripts |
SSH keys must be configured:
~/.ssh/repositories— private key with read access to Bitbucket repositories- Per-server key defined in each
servers/*.shdefinition (e.g.~/.ssh/do-ssh-key)
1. Define a server in servers/my-server.sh:
domain=example.com
serverName=my-server
serverIP=10.0.0.1
user=root
sshKey=~/.ssh/my-key.pem2. Create an entry-point script in the project root:
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
branch=${1:-"main"}
cd "$DIR/scripts"
bash nodejs-pm2-deployer.sh \
-e production \
-r my-bitbucket-workspace \
-p my-api \
-s my-server \
-m 3000 \
-b "$branch"3. Run it:
bash my-api-production.sh
# or with a branch override:
bash my-api-production.sh feature/my-branchEach server is defined as a .sh file inside the servers/ directory. The filename (minus .sh) is the value passed to the -s flag of any deployer.
# servers/my-server.sh
domain=example.com
serverName=my-server
serverIP=203.0.113.10
user=root
sshKey=~/.ssh/my-key.pem| Variable | Description |
|---|---|
domain |
Domain name of the server |
serverName |
Friendly name (matches the filename) |
serverIP |
IP address — use localhost or 127.0.0.1 for local deployments |
user |
SSH login user |
sshKey |
Path to the SSH private key |
Deployer scripts live in scripts/. Each one exports engine configuration variables, defines accepted CLI flags, and sources engine/engine.sh to start the pipeline.
Most deployers accept some or all of these flags:
| Flag | Variable | Required | Default | Description |
|---|---|---|---|---|
-e |
environment |
Yes | — | Target environment (development, production, etc.) |
-r |
repository |
Yes | — | Bitbucket workspace/bucket name |
-p |
projectName |
Yes | — | Repository and project name |
-s |
server |
Yes | — | Server ID (matches a file in servers/) |
-b |
branch |
No | master |
Git branch to deploy |
-w |
sitename |
No | $repository |
Whitelist/site name used in the installation path |
-f |
projectFolder |
No | $projectName |
Override the remote folder name |
Script: scripts/nodejs-pm2-deployer.sh
Packages Node.js backend source (no local build), uploads it, runs npm install and starts/restarts the process under PM2 on the remote server.
Additional flags:
| Flag | Variable | Required | Default | Description |
|---|---|---|---|---|
-m |
port |
Yes | — | Port the service listens on |
-n |
nodeTargetVersion |
No | remote default | Node.js version to use on the remote |
-x |
skipTests |
No | false | Skip npm test on the remote |
Example:
bash nodejs-pm2-deployer.sh \
-e development \
-r my-workspace \
-p my-api \
-s dev-01 \
-m 4000 \
-n 18.20.0 \
-b mainScript: scripts/nextjs-deployer.sh
Zips the full source directory and uploads it. The remote installer runs npm install, npm run build, and starts the app with PM2 (npm start). The build happens on the remote server.
No additional required flags beyond the common set.
Example:
bash nextjs-deployer.sh \
-e production \
-r my-workspace \
-p my-nextjs-app \
-s prod-01Script: scripts/reactjs-deployer.sh
Builds the React app locally (npm run build), zips the build/ output, and deploys it to the web root (/var/www/). Uses Node 18.13.0 by default.
Additional flags:
| Flag | Variable | Required | Default | Description |
|---|---|---|---|---|
-a |
subDomain |
No | $environment |
Subdomain prefix for the web root path |
-l |
basepath |
No | — | URL base path appended to the web root |
-d |
disableSubDomain |
No | false | Deploy to domain/ instead of subdomain.domain/ |
Example:
bash reactjs-deployer.sh \
-e staging \
-r my-workspace \
-p my-react-app \
-s staging-01 \
-a stagingScript: scripts/vite-deployer.sh
Builds a Vite-based frontend locally, zips the dist/ output, and deploys it to the web root. Uses Node 22.18.0 by default. Tests are enabled by default.
Additional flags:
| Flag | Variable | Required | Default | Description |
|---|---|---|---|---|
-a |
subDomain |
No | $environment |
Subdomain prefix |
-l |
basepath |
No | — | URL base path |
-d |
disableSubDomain |
No | false | Deploy to root domain |
-x |
skipTests |
No | false | Skip npm run test |
Example:
bash vite-deployer.sh \
-e production \
-r my-workspace \
-p my-vite-app \
-s prod-01 \
-xScript: scripts/vuejs-webpack-deployer.sh
Builds a Vue.js (webpack) frontend locally, zips the build/ output, and deploys it to the web root. Uses Node 18.13.0. Functionally identical to the React deployer with a full (non-production) npm install.
Flags are the same as the React.js Deployer minus the -x test-skip flag.
Script: scripts/golang-dist-deployer.sh
Compiles a Go binary for linux/amd64 locally, zips it with any YAML config files, uploads it, and installs it as a systemd service on the remote server.
Additional flags:
| Flag | Variable | Required | Default | Description |
|---|---|---|---|---|
-m |
port |
Yes | — | Port the service listens on |
-x |
skipTests |
No | false | Skip go test ./... |
Example:
bash golang-dist-deployer.sh \
-e production \
-r my-workspace \
-p my-go-service \
-s prod-01 \
-m 8080Entry-point scripts in the project root are deployment configurations for specific services. They pin all the flags for a given service so deployments can be triggered with a single command.
Example — dev-api.unlimtid.io-authentication-api.sh:
#!/bin/bash
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
branch=${1:-"development"}
export APP_ROOT_PATH=/opt
cd "$DIR/scripts"
bash nodejs-pm2-deployer.sh \
-e development \
-r firmviewdev \
-w unlimitid \
-p unlimitid-authentication-server \
-f unlimitidio-auth-server \
-n 16.17.1 \
-b "$branch" \
-s crypto-01 \
-m 43003A -test-skip variant of the same script appends -x to skip the test suite for faster deployments.
Convention: name entry-point scripts as {environment}-{domain}-{service}.sh.
Script: scripts/engine/engine.sh
The core orchestration script. Sourced (not called) by deployer scripts. It is not intended to be run directly.
What the engine does, in order:
- Resolves all paths (
ENGINE_DIR,AUTOBUILDER_PATH,TMP_PATH,AUTOBUILDER_BUILD_DIR, etc.) - Sources
logger.shanddynamic-flags.shto parse CLI flags and callverifyArgs() - Validates required variables and the
AUTOBUILDER_PACKAGERvalue - Sources
server-api.shto establish SSH connectivity to the target server - Computes
PAYLOAD_INSTALLATION_TARGET:- Web root deployments (
REMOTE_PATH_WWW_ROOT=true):/var/www/{subDomain.}domain{/basepath} - App deployments:
$APP_ROOT_PATH/$sitename/$environment/$projectFolder
- Web root deployments (
- Sends a Slack notification that the build has started
- Clones or pulls the repository via
fetch-repository.sh - Sources the configured packager to produce
$PAYLOAD_ZIP - Creates the staging directory on the remote, uploads the zip, extractor, and installer scripts
- Executes
payload-extractor.shon the remote to unzip the payload - Executes the installer script on the remote to start the service
- On success, calls
success()which logs, emails, and sends a Slack notification
Key engine variables:
| Variable | Description |
|---|---|
AUTOBUILDER_PACKAGER |
Packager to use (golang-binary, nodejs-source, nodejs-websource, source) |
AUTOBUILDER_INSTALLER |
Remote installer to use (pm2-service, pm2-service-nextjs, sysd-service) |
REMOTE_PATH_WWW_ROOT |
Set true to deploy to /var/www/ instead of $APP_ROOT_PATH |
REMOTE_INSTALL_ENABLE_ROLLBACK |
Set true to generate an uninstall.sh on the remote |
REMOTE_INSTALL_RUN_TESTS |
Set true to run tests on the remote after install |
APP_ROOT_PATH |
Override the default app installation root (default: /home/$user/apps) |
LOG_LOCAL |
Set true to print log output to stdout |
| Script | Purpose |
|---|---|
engine/helpers/dynamic-flags.sh |
Generic getopts parser — maps flags to variables using the arguments array defined by each deployer |
engine/helpers/fetch-repository.sh |
Clones or hard-resets a Bitbucket repository to the specified branch using ~/.ssh/repositories |
engine/helpers/logger.sh |
Structured logging with levels INFO, DEBUG, ERROR, FATAL, SUCCESS; writes to a log file, emails on fatal/success, and notifies Slack |
engine/helpers/server-api.sh |
Provides remoteCommand() and remoteUpload() that transparently work on both SSH-connected and localhost targets |
engine/helpers/payload-extractor.sh |
Remote script — validates and unzips the payload into the installation directory |
engine/helpers/zip-validator.sh |
Validates the local zip is at least 30 KiB before upload |
engine/helpers/post-to-slack.sh |
Fires a Slack incoming webhook with a plain-text message |
engine/helpers/slack_template.json |
Reference Block Kit JSON template for richer Slack messages (not currently wired up) |
Packagers run on the autobuilder host and produce a $PAYLOAD_ZIP file.
| Script | Used by | Behaviour |
|---|---|---|
packagers/source.sh |
Next.js deployer | Zips the entire project directory as-is |
packagers/nodejs-source.sh |
Node.js PM2 deployer | Zips src/, *.js, *.json, and config files — no build step |
packagers/nodejs-websource.sh |
React, Vite, Vue deployers | Installs dependencies, runs optional tests, builds (npm run build), zips only the compiled output directory |
packagers/golang-binary.sh |
Go deployer | Runs optional tests, cross-compiles for linux/amd64 (Makefile or go build fallback), zips the binary and YAML configs |
Installers are uploaded to the remote server and executed via SSH after the payload has been extracted.
Installs a Node.js backend under PM2.
- Installs/switches
nvmandpm2if needed - Auto-detects the entrypoint file (
index.js,main.js,app.js, orsrc/variants) - Runs
npm install - Optionally runs
npm test - Replaces the existing PM2 process and validates
onlinestatus - Runs
pm2 save
Installs a Next.js app under PM2. Identical flow to pm2-service.sh but runs npm run build on the remote and starts the app via pm2 start npm -- start.
Installs a Go binary as a systemd service.
- Generates an
uninstall.shon the remote - Creates a systemd unit file with
Restart=alwaysif one does not already exist - Handles SELinux — temporarily disables enforcement, generates and installs a policy module via
audit2allow, then re-enables enforcement - Runs
systemctl daemon-reload,enable, andrestart
Supports both root-owned (/lib/systemd/system) and user-owned (~/.config/systemd/user) service locations.
Run servers/create-ssh-config.sh on the autobuilder host to regenerate ~/.ssh/config from all server definition files:
bash servers/create-ssh-config.shThis will:
- Delete and recreate
~/.ssh/config - Write a
Hoststanza for every server defined inservers/*.sh - Test connectivity to each server and record
OKorFAILEDas a comment - Restart
sshdviasystemctl
Example output in ~/.ssh/config:
# example.com on my-server
Host 203.0.113.10
StrictHostKeyChecking no
UserKnownHostsFile ~/.ssh/my-key.pem
User root
-
Add a server definition (if the server is new):
# servers/my-new-server.sh domain=example.com serverName=my-new-server serverIP=203.0.113.10 user=deploy sshKey=~/.ssh/deploy-key.pem
-
Create an entry-point script in the project root, choosing the appropriate deployer for your stack.
-
Regenerate SSH config (if the server is new):
bash servers/create-ssh-config.sh
-
Run the deployment:
bash my-new-service-production.sh # or with a branch override: bash my-new-service-production.sh feature/my-branch -
Monitor — logs are written to
$TMP_PATH/logs/{script-name}/{timestamp}.logand build notifications are posted to Slack.