Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(github action): preview service acceptance #2891

Merged
merged 27 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
70fc343
fix(github action): preview acceptance test
iainsproat Sep 5, 2024
1f1b8aa
Remove pnpm
iainsproat Sep 5, 2024
0fa58d0
Merge branch 'main' into iain/fix-preview-service-acceptance-action
iainsproat Sep 5, 2024
613a798
REVERTME: force change in package.json to determine if github action run
iainsproat Sep 5, 2024
f3d97e9
fix dependency installation command
iainsproat Sep 5, 2024
e92e96e
fix glob pattern
iainsproat Sep 5, 2024
86d95c5
Allow postgres connection string to be configured for acceptance test
iainsproat Sep 5, 2024
50f3eb8
Different postgres connection string if running inside preview container
iainsproat Sep 5, 2024
de3eca2
Use host network in github action
iainsproat Sep 5, 2024
b4db33b
Connect to host.docker.internal
iainsproat Sep 5, 2024
13be529
Run the preview-service image as a github action service
iainsproat Sep 5, 2024
2ff5447
do not run within a container
iainsproat Sep 5, 2024
e660439
fix image name
iainsproat Sep 5, 2024
a21f42e
Add correct permission to job
iainsproat Sep 5, 2024
aa2e3f9
Add logging to the test, to understand progress
iainsproat Sep 6, 2024
549f2e9
Allow database name to be passed in to acceptance test
iainsproat Sep 6, 2024
9c07649
I can't knex properly
iainsproat Sep 6, 2024
6975fa3
Only delete the database if the test helper owns (created) it
iainsproat Sep 6, 2024
2ff40e1
Upload image to s3 bucket
iainsproat Sep 6, 2024
2f2dba4
fix S3 endpoint variable
iainsproat Sep 6, 2024
940cf44
fix path, to include bucket in path
iainsproat Sep 6, 2024
3e91d65
Merge branch 'main' into iain/fix-preview-service-acceptance-action
iainsproat Sep 6, 2024
6f52a5b
anchor link instead of image until I can figure out github comments
iainsproat Sep 6, 2024
c0c56eb
fix linting issue
iainsproat Sep 6, 2024
9763e1c
Merge branch 'main' into iain/fix-preview-service-acceptance-action
iainsproat Sep 7, 2024
965073e
Merge branch 'main' into iain/fix-preview-service-acceptance-action
iainsproat Sep 7, 2024
d5dc5e6
Merge branch 'main' into iain/fix-preview-service-acceptance-action
iainsproat Sep 16, 2024
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
106 changes: 69 additions & 37 deletions .github/workflows/preview-service-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,65 @@ on:
- .yarnrc.yml .
- .yarn
- package.json
- packages/frontend-2/type-augmentations/stubs/**
- packages/preview-service/**
- packages/viewer/**
- packages/objectloader/**
- packages/shared/**
- '.github/workflows/preview-service-acceptance.yml'
- 'packages/frontend-2/type-augmentations/stubs/**/*'
- 'packages/preview-service/**/*'
- 'packages/viewer/**/*'
- 'packages/objectloader/**/*'
- 'packages/shared/**/*'

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/speckle-preview-service
OUTPUT_FILE_PATH: 'preview-service-output/${{ github.sha }}.png'

jobs:
build-preview-service:
name: Build Preview Service
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
packages: write # publishing container to GitHub registry

steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/[email protected]
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/[email protected]
with:
tags: type=sha,format=long
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and load preview-service Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./packages/preview-service/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
outputs:
tags: ${{ steps.meta.outputs.tags }}

preview-service-acceptance:
name: Preview Service Acceptance test
runs-on: ubuntu-latest
needs: build-preview-service

permissions:
contents: write # to update the screenshot saved in the branch. This is a HACK as GitHub API does not yet support uploading attachments to a comment.
pull-requests: write # to write a comment on the PR
packages: read # to download the preview-service image

services:
postgres:
# Docker Hub image
Expand All @@ -33,61 +82,44 @@ jobs:
--health-retries 5
ports:
- 5432:5432

permissions:
contents: write # to update the screenshot saved in the branch. This is a HACK as GitHub API does not yet support uploading attachments to a comment.
pull-requests: write # to write a comment on the PR
preview-service:
image: ${{ needs.build-preview-service.outputs.tags }}
env:
# note that the host is the postgres service name
PG_CONNECTION_STRING: postgres://preview_service_test:preview_service_test@postgres:5432/preview_service_test

steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
- name: Install dependencies
working-directory: utils/preview-service-acceptance
working-directory: packages/preview-service
run: yarn install

#TODO load the docker image from a previous job
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and load preview-service Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./packages/preview-service/Dockerfile
load: true
push: false
tags: speckle/preview-service:local
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Run the acceptance test
working-directory: packages/preview-service
run: yarn test:acceptance
env:
PREVIEW_SERVICE_IMAGE: speckle/preview-service:local
OUTPUT_FILE_PATH: /tmp/preview-service-output.png
NODE_ENV: test
TEST_DB: preview_service_test
# note that the host is localhost, but the port is the port mapped to the postgres service
PG_CONNECTION_STRING: postgres://preview_service_test:preview_service_test@localhost:5432/preview_service_test
OUTPUT_FILE_PATH: ${{ env.OUTPUT_FILE_PATH }}
S3_BUCKET: ${{ vars.S3_BUCKET }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_ENDPOINT: ${{ vars.S3_ENDPOINT }}
S3_REGION: ${{ vars.S3_REGION }}

- uses: actions/upload-artifact@v4
name: Upload the output from the preview-service
id: upload-preview-service-output
with:
name: preview-service-output
path: /tmp/preview-service-output.png
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '📸 Preview service has generated the following image:<br/><img src="${{ steps.upload-preview-service-output.outputs.artifact-url }}"/><br/>'
body: '📸 Preview service has generated <a href="${{ vars.S3_ENDPOINT }}/${{ vars.S3_BUCKET }}/${{ env.OUTPUT_FILE_PATH}}">an image.</a>'
})
1 change: 1 addition & 0 deletions packages/preview-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.645.0",
"@babel/core": "^7.17.5",
"@types/express": "^4.17.13",
"@types/lodash-es": "^4.17.6",
Expand Down
111 changes: 71 additions & 40 deletions packages/preview-service/tests/acceptance/acceptance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,36 @@ import { acceptanceTest } from '#/helpers/testExtensions.js'
import { ObjectPreview, type ObjectPreviewRow } from '@/repositories/objectPreview.js'
import { Previews } from '@/repositories/previews.js'
import cryptoRandomString from 'crypto-random-string'
import { afterEach, beforeEach, describe, expect, inject } from 'vitest'
import { afterEach, beforeEach, describe, expect } from 'vitest'
import { promises as fs } from 'fs'
import { spawn } from 'child_process'
import { OBJECTS_TABLE_NAME } from '#/migrations/migrations.js'
import type { Angle } from '@/domain/domain.js'
import { testLogger as logger } from '@/observability/logging.js'

import { PutObjectCommand, PutObjectCommandInput, S3Client } from '@aws-sdk/client-s3'

const getS3Config = () => {
return {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY || '',
secretAccessKey: process.env.S3_SECRET_KEY || ''
},
endpoint: process.env.S3_ENDPOINT || '',
forcePathStyle: true,
// s3ForcePathStyle: true,
// signatureVersion: 'v4',
region: process.env.S3_REGION || 'us-east-1'
}
}

describe.sequential('Acceptance', () => {
describe.sequential('Run the preview-service image in docker', () => {
beforeEach(() => {
const dbName = inject('dbName')
//purposefully running in the background without waiting
void runProcess('docker', [
'run',
'--env',
`PG_CONNECTION_STRING=postgres://preview_service_test:[email protected]:5432/${dbName}`,
'--rm',
'--name',
'preview-service',
'speckle/preview-service:local'
])
// const dbName = inject('dbName')
logger.info('🤜 running acceptance test before-each')
})
afterEach(async () => {
await runProcess('docker', ['stop', 'preview-service'])
afterEach(() => {
logger.info('🤛 running acceptance test after-each')
})

// we use integration test and not e2e test because we don't need the server
Expand All @@ -36,11 +42,6 @@ describe.sequential('Acceptance', () => {
},
async ({ context }) => {
const { db } = context
const dbName = inject('dbName')
logger.info(
{ databaseName: dbName },
'Running test in database: {databaseName}'
)
// load data
const streamId = cryptoRandomString({ length: 10 })
const objectId = cryptoRandomString({ length: 10 })
Expand Down Expand Up @@ -76,6 +77,10 @@ describe.sequential('Acceptance', () => {
.where('streamId', streamId)
.andWhere('objectId', objectId)

logger.info(
{ result: objectPreviewResult, streamId, objectId },
'🔍 Polled object preview for a result for {streamId} and {objectId}'
)
// wait a second before polling again
await new Promise((resolve) => setTimeout(resolve, 1000))
}
Expand All @@ -84,34 +89,60 @@ describe.sequential('Acceptance', () => {
.select(['data'])
.where('id', objectPreviewResult[0].preview['all' as Angle])
.first()
logger.info({ previewData }, '🔍 Retrieved preview data')

if (!previewData) {
expect(previewData).toBeDefined()
expect(previewData).not.toBeNull()
return //HACK to appease typescript
}

//TODO use environment variable
const outputFilePath =
process.env.OUTPUT_FILE_PATH || '/tmp/preview-service-output.png'
await fs.writeFile(outputFilePath, previewData.data)
if (!process.env.OUTPUT_FILE_PATH)
throw new Error('OUTPUT_FILE_PATH environment variable not set')

const outputFilePath = process.env.OUTPUT_FILE_PATH

const s3Config = getS3Config()

if (s3Config.credentials.accessKeyId && s3Config.credentials.secretAccessKey) {
logger.info(
{ outputFilePath },
'S3 credentials provided, saving to S3 at {outputFilePath}'
)
const s3Client = new S3Client(s3Config)

const params: PutObjectCommandInput = {
Bucket: 'github-action-speckle-preview-service-acceptance-test',
Key: outputFilePath,
Body: previewData.data,
ACL: 'public-read',
Metadata: {
// Defines metadata tags.
// 'x-amz-meta-my-key': 'your-value'
}
}

const uploadObject = async () => {
try {
const data = await s3Client.send(new PutObjectCommand(params))
logger.info(
'Successfully uploaded object: ' + params.Bucket + '/' + params.Key
)
return data
} catch (err) {
logger.error(err, 'Failed to upload object')
}
}

await uploadObject()
} else {
logger.info(
{ outputFilePath },
'No S3 credentials provided, saving to local file system at {outputFilePath}'
)
await fs.writeFile(outputFilePath, previewData.data)
}
}
)
})
})

function runProcess(cmd: string, cmdArgs: string[], extraEnv?: Record<string, string>) {
return new Promise((resolve, reject) => {
const childProc = spawn(cmd, cmdArgs, { env: { ...process.env, ...extraEnv } })
childProc.stdout.pipe(process.stdout)
childProc.stderr.pipe(process.stderr)

childProc.on('close', (code) => {
if (code === 0) {
resolve('success')
} else {
reject(`Parser exited with code ${code}`)
}
})
})
}
29 changes: 20 additions & 9 deletions packages/preview-service/tests/hooks/globalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ declare module 'vitest' {
}
}

const dbName = `preview_service_${cryptoRandomString({
length: 10,
type: 'alphanumeric'
})}`.toLocaleLowerCase() //postgres will automatically lower case new db names
const dbName =
process.env.TEST_DB || // in the acceptance tests we need to use a database name that is known prior to the test running
`preview_service_${cryptoRandomString({
length: 10,
type: 'alphanumeric'
})}`.toLocaleLowerCase() //postgres will automatically lower case new db names
let isDatabaseCreatedExternally = true

/**
* Global setup hook
Expand All @@ -28,12 +31,18 @@ const dbName = `preview_service_${cryptoRandomString({
export async function setup({ provide }: GlobalSetupContext) {
logger.info('🏃🏻‍♀️‍➡️ Running vitest setup global hook')
const superUserDbClient = getTestDb()
await superUserDbClient.raw(`CREATE DATABASE ${dbName}
const dbAlreadyExists = await superUserDbClient('pg_database')
.select('datname')
.where('datname', dbName)
if (!dbAlreadyExists.length) {
isDatabaseCreatedExternally = false
await superUserDbClient.raw(`CREATE DATABASE ${dbName}
WITH
OWNER = preview_service_test
ENCODING = 'UTF8'
TABLESPACE = pg_default
CONNECTION LIMIT = -1;`)
}
await superUserDbClient.destroy() // need to explicitly close the connection in clients to prevent hanging tests

// this provides the dbName to all tests, and can be accessed via inject('dbName'). NB: The test extensions already implement this, so use a test extension.
Expand All @@ -56,9 +65,11 @@ export async function teardown() {
await down(db) //we need the migration to occur in our named database, so cannot use knex's built in migration functionality.
await db.destroy() // need to explicitly close the connection in clients to prevent hanging tests

//use connection without database to drop the db
const superUserDbClient = getTestDb()
await superUserDbClient.raw(`DROP DATABASE ${dbName};`)
await superUserDbClient.destroy() // need to explicitly close the connection in clients to prevent hanging tests
if (!isDatabaseCreatedExternally) {
//use connection without database to drop the db
const superUserDbClient = getTestDb()
await superUserDbClient.raw(`DROP DATABASE ${dbName};`)
await superUserDbClient.destroy() // need to explicitly close the connection in clients to prevent hanging tests
}
logger.info('✅ Completed the vitest teardown global hook')
}
Loading
Loading