Skip to content
Open
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
306 changes: 306 additions & 0 deletions .github/workflows/aws-artifact-db.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
name: AWS Push + Artifact + DB

on:
push:
branches:
- master
- main
- 'ci/**'
pull_request:
branches:
- master
- main
workflow_dispatch:
inputs:
environment:
description: 'GitHub environment to use'
required: true
default: 'production'
type: choice
options:
- production
- staging
deploy_to_s3:
description: 'Upload build artifact to AWS S3'
required: true
default: true
type: boolean
publish_codeartifact:
description: 'Publish npm package to AWS CodeArtifact'
required: true
default: false
type: boolean
run_db_check:
description: 'Run database connectivity check'
required: true
default: true
type: boolean

permissions:
contents: read
id-token: write
actions: read

concurrency:
group: aws-artifact-db-${{ github.ref }}-${{ github.event.inputs.environment || 'production' }}
cancel-in-progress: true

env:
NODE_VERSION: '22'

jobs:
build-test-package:
name: Build, test and package
runs-on: ubuntu-latest
outputs:
artifact_name: sync-db-${{ github.sha }}
package_file: ${{ steps.pack.outputs.package_file }}
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Check format
run: yarn format:check

- name: Lint
run: yarn lint

- name: Build
run: yarn build

- name: Test coverage
run: yarn test:coverage

- name: Create npm package tarball
id: pack
shell: bash
run: |
set -euo pipefail
npm pack --silent > /tmp/npm-pack-output.txt
cat /tmp/npm-pack-output.txt
PACKAGE_FILE="$(grep -E '\.tgz$' /tmp/npm-pack-output.txt | tail -n 1)"
if [ -z "$PACKAGE_FILE" ] || [ ! -f "$PACKAGE_FILE" ]; then
echo "npm pack did not produce a .tgz file" >&2
exit 1
fi
echo "package_file=${PACKAGE_FILE}" >> "$GITHUB_OUTPUT"
mkdir -p dist-artifact
cp "$PACKAGE_FILE" dist-artifact/
cp package.json yarn.lock README.md LICENSE dist-artifact/
if [ -d lib ]; then cp -R lib dist-artifact/lib; fi
if [ -d bin ]; then cp -R bin dist-artifact/bin; fi
if [ -d assets ]; then cp -R assets dist-artifact/assets; fi
tar -czf "sync-db-${GITHUB_SHA}.tar.gz" dist-artifact
sha256sum "sync-db-${GITHUB_SHA}.tar.gz" > "sync-db-${GITHUB_SHA}.tar.gz.sha256"
ls -lah "sync-db-${GITHUB_SHA}.tar.gz" "sync-db-${GITHUB_SHA}.tar.gz.sha256" "$PACKAGE_FILE"

- name: Upload GitHub Actions artifact
uses: actions/upload-artifact@v4
with:
name: sync-db-${{ github.sha }}
path: |
sync-db-${{ github.sha }}.tar.gz
sync-db-${{ github.sha }}.tar.gz.sha256
${{ steps.pack.outputs.package_file }}
retention-days: 30
if-no-files-found: error

db-connectivity:
name: Database connectivity check
runs-on: ubuntu-latest
needs: build-test-package
environment: ${{ github.event.inputs.environment || 'production' }}
if: ${{ github.event_name == 'push' || github.event.inputs.run_db_check == 'true' }}
env:
DB_TYPE: ${{ secrets.DB_TYPE }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_PORT: ${{ secrets.DB_PORT }}
DB_NAME: ${{ secrets.DB_NAME }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_SSL: ${{ secrets.DB_SSL }}
steps:
- name: Setup Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: Install database drivers for connectivity probe
run: npm install --no-save pg mysql2 mssql

- name: Probe database connection
shell: bash
run: |
set -euo pipefail
cat > /tmp/db_probe.js <<'NODE'
const required = ['DB_TYPE', 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];
const missing = required.filter((key) => !process.env[key]);
if (missing.length) {
console.log(`DB check skipped. Missing secrets: ${missing.join(', ')}`);
process.exit(0);
}

const type = process.env.DB_TYPE.toLowerCase();
const sslEnabled = String(process.env.DB_SSL || '').toLowerCase() === 'true';
const port = process.env.DB_PORT ? Number(process.env.DB_PORT) : undefined;

async function checkPostgres() {
const { Client } = require('pg');
const client = new Client({
host: process.env.DB_HOST,
port: port || 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: sslEnabled ? { rejectUnauthorized: false } : false,
connectionTimeoutMillis: 15000,
});
await client.connect();
const result = await client.query('select current_database() as db, current_user as user, now() as server_time');
console.log('PostgreSQL OK:', result.rows[0]);
await client.end();
}

async function checkMysql() {
const mysql = require('mysql2/promise');
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
port: port || 3306,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: sslEnabled ? { rejectUnauthorized: false } : undefined,
connectTimeout: 15000,
});
const [rows] = await connection.execute('select database() as db, current_user() as user, now() as server_time');
console.log('MySQL OK:', rows[0]);
await connection.end();
}

async function checkMssql() {
const sql = require('mssql');
const pool = await sql.connect({
server: process.env.DB_HOST,
port: port || 1433,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
options: {
encrypt: sslEnabled,
trustServerCertificate: true,
enableArithAbort: true,
},
connectionTimeout: 15000,
requestTimeout: 15000,
});
const result = await pool.request().query('select db_name() as db, suser_sname() as [user], getutcdate() as server_time');
console.log('MSSQL OK:', result.recordset[0]);
await pool.close();
}

(async () => {
if (['postgres', 'postgresql', 'pg'].includes(type)) return checkPostgres();
if (['mysql', 'mariadb'].includes(type)) return checkMysql();
if (['mssql', 'sqlserver', 'sql-server'].includes(type)) return checkMssql();
throw new Error(`Unsupported DB_TYPE: ${process.env.DB_TYPE}. Use postgres, mysql or mssql.`);
})().catch((error) => {
console.error('DB connectivity failed:', error.message);
process.exit(1);
});
NODE
node /tmp/db_probe.js

aws-push-artifact:
name: Push artifact to AWS
runs-on: ubuntu-latest
needs:
- build-test-package
- db-connectivity
if: ${{ always() && needs.build-test-package.result == 'success' && (needs.db-connectivity.result == 'success' || needs.db-connectivity.result == 'skipped') && (github.event_name == 'push' || github.event.inputs.deploy_to_s3 == 'true' || github.event.inputs.publish_codeartifact == 'true') }}
environment: ${{ github.event.inputs.environment || 'production' }}
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
AWS_ROLE_TO_ASSUME: ${{ secrets.AWS_ROLE_TO_ASSUME }}
AWS_ACCESS_KEY_ID_VALUE: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY_VALUE: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_S3_ARTIFACT_BUCKET: ${{ secrets.AWS_S3_ARTIFACT_BUCKET }}
AWS_CODEARTIFACT_DOMAIN: ${{ secrets.AWS_CODEARTIFACT_DOMAIN }}
AWS_CODEARTIFACT_DOMAIN_OWNER: ${{ secrets.AWS_CODEARTIFACT_DOMAIN_OWNER }}
AWS_CODEARTIFACT_REPOSITORY: ${{ secrets.AWS_CODEARTIFACT_REPOSITORY }}
steps:
- name: Download GitHub artifact
uses: actions/download-artifact@v4
with:
name: sync-db-${{ github.sha }}
path: artifact

- name: Configure AWS credentials using OIDC role
if: ${{ env.AWS_ROLE_TO_ASSUME != '' }}
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ env.AWS_REGION || 'us-east-1' }}

- name: Configure AWS credentials using access keys fallback
if: ${{ env.AWS_ROLE_TO_ASSUME == '' && env.AWS_ACCESS_KEY_ID_VALUE != '' && env.AWS_SECRET_ACCESS_KEY_VALUE != '' }}
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID_VALUE }}
aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY_VALUE }}
aws-region: ${{ env.AWS_REGION || 'us-east-1' }}

- name: Verify AWS credentials configured
shell: bash
run: |
set -euo pipefail
if [ -z "${AWS_ROLE_TO_ASSUME:-}" ] && { [ -z "${AWS_ACCESS_KEY_ID_VALUE:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY_VALUE:-}" ]; }; then
echo "AWS push skipped. Missing AWS_ROLE_TO_ASSUME or AWS access-key secrets."
exit 0
fi
aws sts get-caller-identity

- name: Push artifact to S3
if: ${{ env.AWS_S3_ARTIFACT_BUCKET != '' && (github.event_name == 'push' || github.event.inputs.deploy_to_s3 == 'true') }}
shell: bash
run: |
set -euo pipefail
PREFIX="sync-db/${GITHUB_REF_NAME}/${GITHUB_SHA}"
aws s3 cp artifact/ "s3://${AWS_S3_ARTIFACT_BUCKET}/${PREFIX}/" --recursive
aws s3 cp artifact/ "s3://${AWS_S3_ARTIFACT_BUCKET}/sync-db/latest/" --recursive
echo "Artifacts pushed to s3://${AWS_S3_ARTIFACT_BUCKET}/${PREFIX}/"

- name: Publish package to AWS CodeArtifact
if: ${{ env.AWS_CODEARTIFACT_DOMAIN != '' && env.AWS_CODEARTIFACT_REPOSITORY != '' && github.event.inputs.publish_codeartifact == 'true' }}
shell: bash
run: |
set -euo pipefail
npm_tarball="$(ls artifact/*.tgz | head -n 1)"
args=(--tool npm --domain "${AWS_CODEARTIFACT_DOMAIN}" --repository "${AWS_CODEARTIFACT_REPOSITORY}" --region "${AWS_REGION:-us-east-1}")
if [ -n "${AWS_CODEARTIFACT_DOMAIN_OWNER:-}" ]; then
args+=(--domain-owner "${AWS_CODEARTIFACT_DOMAIN_OWNER}")
fi
aws codeartifact login "${args[@]}"
npm publish "$npm_tarball" --access restricted

- name: Deployment summary
shell: bash
run: |
{
echo "## AWS artifact workflow"
echo "- Commit: $GITHUB_SHA"
echo "- Branch: $GITHUB_REF_NAME"
echo "- Environment: ${{ github.event.inputs.environment || 'production' }}"
echo "- GitHub artifact: sync-db-$GITHUB_SHA"
echo "- S3 bucket: ${AWS_S3_ARTIFACT_BUCKET:-not configured}"
echo "- CodeArtifact domain: ${AWS_CODEARTIFACT_DOMAIN:-not configured}"
echo "- CodeArtifact repository: ${AWS_CODEARTIFACT_REPOSITORY:-not configured}"
} >> "$GITHUB_STEP_SUMMARY"
Loading