Stage 2 · Tools
DevOps Scripts in Practice
CI Helper Scripts
Build scripts, test runners, version bumping, and deployment automation for CI/CD.
CI Script Structure
CI helper scripts run in automated environments — GitHub Actions, GitLab CI, Jenkins. They need to be non-interactive, handle errors cleanly, and provide clear output for debugging.
#!/usr/bin/env bash
set -euo pipefail
# CI scripts should:
# 1. Be non-interactive (no prompts)
# 2. Handle errors with clear messages
# 3. Exit with proper codes
# 4. Log everything for debugging
# Detect CI environment
if [[ -n "{CI:-}" ]]; then
echo "Running in CI: {GITHUB_ACTIONS:-{GITLAB_CI:-{JENKINS_URL:-unknown}}}"
fi
# Retry failed network operations
retry() {
local max_attempts=3
local delay=5
for ((i=1; i<=max_attempts; i++)); do
if "$@"; then
return 0
fi
if (( i < max_attempts )); then
echo "Attempt $i failed, retrying in {delay}s..."
sleep "$delay"
fi
done
return 1
}CI scripts must be non-interactive (no read prompts), handle network failures with retries, and provide clear error messages for debugging in CI logs.
Version Bumping
#!/usr/bin/env bash
set -euo pipefail
BUMP_FILE="VERSION"
get_version() {
cat "$BUMP_FILE" 2>/dev/null || echo "0.0.0"
}
bump_version() {
local type="{1:-patch}"
local current
current=$(get_version)
IFS='.' read -r major minor patch <<< "$current"
case "$type" in
major) echo "$((major + 1)).0.0" ;;
minor) echo "$major.$((minor + 1)).0" ;;
patch) echo "$major.$minor.$((patch + 1))" ;;
*) echo "Unknown bump type: $type" >&2; return 1 ;;
esac
}
# Bump and save
NEW_VERSION=$(bump_version "{1:-patch}")
echo "$NEW_VERSION" > "$BUMP_FILE"
echo "Version bumped: $(get_version) -> $NEW_VERSION"
# Git tag
if [[ -n "{GITHUB_REF:-}" ]]; then
git tag "v$NEW_VERSION"
git push origin "v$NEW_VERSION"
fiSemantic versioning: major (breaking changes), minor (new features), patch (bug fixes). The script reads the current version, bumps it, saves it, and optionally creates a git tag.
Docker Automation
#!/usr/bin/env bash
set -euo pipefail
readonly IMAGE_NAME="myapp"
readonly REGISTRY="ghcr.io/myorg"
readonly VERSION=$(cat VERSION)
# Build image
echo "Building $IMAGE_NAME:$VERSION..."
docker build \
--tag "$REGISTRY/$IMAGE_NAME:$VERSION" \
--tag "$REGISTRY/$IMAGE_NAME:latest" \
--build-arg VERSION="$VERSION" \
--label "version=$VERSION" \
--label "git-sha=$(git rev-parse --short HEAD)" \
.
# Push image
echo "Pushing to $REGISTRY..."
docker push "$REGISTRY/$IMAGE_NAME:$VERSION"
docker push "$REGISTRY/$IMAGE_NAME:latest"
# Scan for vulnerabilities
if command -v trivy &>/dev/null; then
trivy image --exit-code 1 --severity HIGH,CRITICAL "$REGISTRY/$IMAGE_NAME:$VERSION"
fi
echo "Docker build complete: $REGISTRY/$IMAGE_NAME:$VERSION"Build with version and latest tags. Add metadata labels for traceability. Push to registry. Optionally scan for vulnerabilities with trivy. This script runs in CI after tests pass.
Changelog Generation
#!/usr/bin/env bash
set -euo pipefail
generate_changelog() {
local from="{1:-HEAD~10}"
local to="{2:-HEAD}"
echo "# Changelog"
echo ""
echo "## $(date '+%Y-%m-%d')"
echo ""
# Features
local features
features=$(git log "$from..$to" --pretty=format:"- %s" --grep="^feat" 2>/dev/null || true)
if [[ -n "$features" ]]; then
echo "### Features"
echo "$features"
echo ""
fi
# Bug fixes
local fixes
fixes=$(git log "$from..$to" --pretty=format:"- %s" --grep="^fix" 2>/dev/null || true)
if [[ -n "$fixes" ]]; then
echo "### Bug Fixes"
echo "$fixes"
echo ""
fi
# Other changes
local others
others=$(git log "$from..$to" --pretty=format:"- %s" --grep="^chore\|^docs\|^refactor\|^test" 2>/dev/null || true)
if [[ -n "$others" ]]; then
echo "### Other Changes"
echo "$others"
echo ""
fi
}
generate_changelog > CHANGELOG.md
echo "Changelog updated"This script generates a changelog from conventional commits (feat:, fix:, chore:, etc.). It groups changes by type and outputs markdown. Run it before releases.
Test Runners
#!/usr/bin/env bash
set -euo pipefail
TEST_FAILED=0
run_test_suite() {
local name="$1"
local command="$2"
echo "=== Running: $name ==="
if eval "$command"; then
echo "PASS: $name"
else
echo "FAIL: $name"
TEST_FAILED=1
fi
echo ""
}
# Run all test suites
run_test_suite "Unit Tests" "npm test"
run_test_suite "Linting" "npm run lint"
run_test_suite "Type Check" "npm run typecheck"
run_test_suite "Integration Tests" "npm run test:integration"
run_test_suite "Security Audit" "npm audit --audit-level=high"
# Summary
if (( TEST_FAILED )); then
echo "=== Some tests failed ==="
exit 1
else
echo "=== All tests passed ==="
exit 0
fiThis test runner executes multiple test suites and reports pass/fail for each. The TEST_FAILED flag ensures the script exits with failure if any suite fails. CI systems use this exit code to gate deployments.
Pipeline Examples
#!/usr/bin/env bash
set -euo pipefail
# This script runs in GitHub Actions
echo "Branch: $GITHUB_REF_NAME"
echo "Commit: $GITHUB_SHA"
# Run tests
if ! ./scripts/test.sh; then
echo "Tests failed"
exit 1
fi
# Build and push (only on main)
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
echo "Building and pushing..."
./scripts/docker-build.sh
# Deploy to staging
./scripts/deploy.sh staging
# Run smoke tests
if ./scripts/smoke-test.sh staging; then
echo "Staging deploy successful"
else
echo "Smoke tests failed"
exit 1
fi
fi
echo "Pipeline complete"CI pipeline scripts orchestrate the build-test-deploy flow. They check the branch, run tests, build artifacts, deploy to staging, and run smoke tests. Each step checks for failure before proceeding.
1) Use set -euo pipefail. 2) Be non-interactive. 3) Retry network operations. 4) Log everything. 5) Exit with meaningful codes. 6) Keep scripts under 200 lines — split complex logic.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.