ci: add AWS artifact and database workflow #4
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |