diff --git a/.github/workflows/nonregression-dp.yml b/.github/workflows/nonregression-dp.yml new file mode 100644 index 000000000..756bf699b --- /dev/null +++ b/.github/workflows/nonregression-dp.yml @@ -0,0 +1,261 @@ +name: Non Regression Testing (US) (inactive) + +on: + workflow_dispatch: +# schedule: +# - cron: "0 9 * * 1-5" +# push: +# branches: [ main ] + +jobs: + log-context: + runs-on: ubuntu-latest + steps: + # Dump all contexts + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - name: Dump job context + env: + JOB_CONTEXT: ${{ toJson(job) }} + run: echo "$JOB_CONTEXT" + - name: Dump steps context + env: + STEPS_CONTEXT: ${{ toJson(steps) }} + run: echo "$STEPS_CONTEXT" + - name: Dump runner context + env: + RUNNER_CONTEXT: ${{ toJson(runner) }} + run: echo "$RUNNER_CONTEXT" + - name: Dump strategy context + env: + STRATEGY_CONTEXT: ${{ toJson(strategy) }} + run: echo "$STRATEGY_CONTEXT" + - name: Dump matrix context + env: + MATRIX_CONTEXT: ${{ toJson(matrix) }} + run: echo "$MATRIX_CONTEXT" + get-test-definition-files: + name: Get Test Definition Files + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.get-test-definition-files.outputs.result }} + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Get Test Definition Files + id: get-test-definition-files + uses: actions/github-script@v3 + with: + script: | + const fs = require("fs"); + const fsp = fs.promises; + const path = require("path"); + + const { isOHIValidationTimeout } = require("${{ github.workspace }}/.github/workflows/scripts/ohiValidationTimeout"); + + // readdir recursive directory search + const { readdir } = fsp; + async function getFiles(dir) { + const dirents = await readdir(dir, { withFileTypes: true }); + const files = await Promise.all( + dirents.map((dirent) => { + const res = path.join(dir, dirent.name); + return dirent.isDirectory() ? getFiles(res) : res; + }) + ); + return Array.prototype.concat(...files); + } + + const definitionsDir = "test/definitions"; + const testDefinitions = await getFiles(definitionsDir); + + const outputTestFilesMap = testDefinitions + .filter((testDefinitionFile) => !isOHIValidationTimeout(testDefinitionFile)) + .map((testDefinitionFile) => { + return { + testDefinitionFile, + testDisplayName: testDefinitionFile.replace(`${definitionsDir}/`, ""), + }; + }); + const output = { + include: outputTestFilesMap, + }; + console.log(output); + return output; + + test-deploy-recipe: + name: ${{ matrix.testDisplayName }} + needs: [get-test-definition-files] + if: ${{ fromJSON(needs.get-test-definition-files.outputs.matrix).include[0] }} # Avoids empty matrix validation error + runs-on: ubuntu-latest + strategy: + matrix: ${{ fromJSON(needs.get-test-definition-files.outputs.matrix) }} + fail-fast: false + env: + MATRIX: ${{ toJSON(matrix) }} + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Update Test Definition Files URLs + id: get-test-definition-files + env: + TEST_DEFINITION_FILE: ${{ matrix.testDefinitionFile }} + uses: actions/github-script@v3 + with: + script: | + const fs = require('fs'); + const fsp = fs.promises; + const path = require('path'); + + // before returning, we need to edit the deploy config files in-place so they + // use the right URLs from the branch + async function getDeployConfigFile(file, outputDir) { + const data = await fsp.readFile(path.join(outputDir, file)); + return JSON.parse(data); + } + + // Get testDefinitonFile from MATRIX env var + const testDefinitionFile = process.env.TEST_DEFINITION_FILE; + console.log(`Detected Deploy Config: ${JSON.stringify(testDefinitionFile, null, 2)}`) + + // Update URLs to use branch this PR is opened with + const data = await getDeployConfigFile(testDefinitionFile, process.env.GITHUB_WORKSPACE); + + // Update github source URLs with branch name + let jsonContent = JSON.stringify(data, null, 2); + const branchName = process.env.GITHUB_HEAD_REF ? process.env.GITHUB_HEAD_REF : process.env.GITHUB_REF_NAME; + const replacementString = `$1$2-b ${branchName} $3$4`; + const sourceRepositoryRegex = /(.*)(\")(https:\/\/github.com\/newrelic\/open-install-library)(.*)/gi; + jsonContent = jsonContent.replace(sourceRepositoryRegex, replacementString); + console.log(`Detected Deploy Config: ${JSON.stringify(jsonContent, null, 2)}`) + + // Update raw URLs with branch name + const replacementString2 = `$1${branchName}$3`; + const sourceRepositoryRegex2 = /(raw.githubusercontent.com\/newrelic\/open-install-library\/)(main)(\/newrelic\/recipes\/)*/gi; + jsonContent = jsonContent.replace(sourceRepositoryRegex2, replacementString2); + console.log(`Detected Deploy Config: ${JSON.stringify(jsonContent, null, 2)}`) + + // Write file back to workspace + const outputPath = `${process.env.GITHUB_WORKSPACE}/${testDefinitionFile}`; + fs.writeFileSync(outputPath, jsonContent); + + return testDefinitionFile; + + - name: Install npm dependencies for deployer test runner + working-directory: .github/workflows/scripts/deployer-platform + run: npm install + + - name: Execute test + id: runDeployerPlatformTest + working-directory: .github/workflows/scripts/deployer-platform + run: | + node main.js + env: + TEST_DEFINITION_FILE: ${{ matrix.testDefinitionFile }} + AWS_ACCESS_KEY_ID: ${{ secrets.DEPLOYER_PLATFORM_US_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DEPLOYER_PLATFORM_US_AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.DEPLOYER_PLATFORM_US_AWS_REGION }} + SQS_URL: ${{ secrets.DEPLOYER_PLATFORM_US_SQS_URL }} + DYNAMO_TABLE: ${{ secrets.DEPLOYER_PLATFORM_US_DYNAMO_TABLE }} + + - name: Report any error + if: steps.runDeployerPlatformTest.outputs.exit_status != 0 + run: exit 1 + + slack-notify: + runs-on: ubuntu-latest + needs: [test-deploy-recipe] + if: always() + steps: + - name: Build Result Slack Notification + uses: 8398a7/action-slack@v3 + with: + author_name: GitHub Actions + status: custom + fields: commit,repo,ref,author,eventName,message,workflow + custom_payload: | + { + username: "GitHub Actions", + icon_emoji: ":octocat:", + attachments: [{ + color: ${{ + needs.test-deploy-recipe.result == 'success' + }} === true ? '#43cc11' : '#e05d44', + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: `Build for ${process.env.AS_REPO}` + } + }, + { + type: "section", + fields: [ + { + type: "mrkdwn", + text: `*Commit:*\n${process.env.AS_COMMIT}` + }, + { + type: "mrkdwn", + text: `*Author:*\n${process.env.AS_AUTHOR}` + }, + { + type: "mrkdwn", + text: `*Branch:*\n${process.env.AS_REF}` + }, + { + type: "mrkdwn", + text: `*Message:*\n${process.env.AS_MESSAGE}` + }, + { + type: "mrkdwn", + text: `*Type:*\n${process.env.AS_EVENT_NAME}` + }, + { + type: "mrkdwn", + text: "*PR:*\n${{ github.event.pull_request.html_url }}" + }, + { + type: "mrkdwn", + text: `*Workflow:*\n${ process.env.AS_WORKFLOW }` + } + ] + }, + { + type: "section", + text: { + type: "mrkdwn", + text: [ + "*Result:*", + `• ${ ${{ needs.test-deploy-recipe.result == 'success' }} === true ? '✅' : '❌' } Non-regression testing of all recipes: ${{ needs.test-deploy-recipe.result }}` + ].join('\n') + } + }, + { + type: "context", + elements: [ + { + type: "image", + image_url: "https://avatars2.githubusercontent.com/in/15368", + alt_text: "Github Actions" + }, + { + type: "mrkdwn", + text: "This message was created automatically by GitHub Actions." + } + ] + } + ] + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/scripts/deployer-platform/main.js b/.github/workflows/scripts/deployer-platform/main.js new file mode 100644 index 000000000..e668757be --- /dev/null +++ b/.github/workflows/scripts/deployer-platform/main.js @@ -0,0 +1,110 @@ +const fs = require('fs'); +const fsp = fs.promises; +const {SQSClient, SendMessageCommand} = require('@aws-sdk/client-sqs') +const {DynamoDBClient, QueryCommand} = require('@aws-sdk/client-dynamodb') +const {unmarshall} = require('@aws-sdk/util-dynamodb') + +const AWS_REGION = process.env.AWS_REGION +const SQS_URL = process.env.SQS_URL +const DYNAMO_TABLE = process.env.DYNAMO_TABLE +const AWS_CREDS = { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY +}; + +const sqs = new SQSClient({region: AWS_REGION, credentials: AWS_CREDS}) +const dynamodb = new DynamoDBClient({region: AWS_REGION, credentials: AWS_CREDS}) + + +function queryForDeploymentStatus(messageId) { + const query_params = { + TableName: DYNAMO_TABLE, + KeyConditionExpression: 'id = :id', + FilterExpression: 'completed = :completed', + ExpressionAttributeNames: { + '#id': 'id', + '#completed': 'completed', + '#status': 'status', + '#message': 'message', + }, + ExpressionAttributeValues: { + ':id': { + S: messageId, + }, + ':completed': { + BOOL: true, + }, + }, + ProjectionExpression: '#id, #completed, #status, #message', + ScanIndexForward: false, //returns items by descending timestamp + } + return new QueryCommand(query_params) +} + +async function isDeploymentSuccessful(deploymentId, retries, waitSeconds) { + for (let i = 0; i < retries; i++) { + console.log(`Deployment pending, sleeping ${waitSeconds} seconds...`) + await sleep(waitSeconds * 1000) + + try { + const response = await dynamodb.send(queryForDeploymentStatus(deploymentId)) + console.log(`Query succeeded. Items found: ${response.Items.length}`) + + for (let i = 0; i < response.Items.length; i++) { + const item = unmarshall(response.Items[i]) + if (item.completed) { + console.log(`Completed: ${item.id} - ${item.message} - ${item.completed} - ${item.status}`) + if (item.status === 'FAILED') { + console.error(`::error:: Deployment failed: ${item.message}`) + return false + } + + return true + } + } + } catch (err) { + console.log(`Error querying table: ${err}`) + } + } + return false +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +async function getDeployConfigFile(file) { + const data = await fsp.readFile(file); + return JSON.parse(data); +} + +function main() { + getDeployConfigFile(`${process.env.GITHUB_WORKSPACE}/${process.env.TEST_DEFINITION_FILE}`) + .then(async (json) => { + let messageId + try { + const command = new SendMessageCommand({ + QueueUrl: SQS_URL, + MessageBody: JSON.stringify(json), + }) + data = await sqs.send(command) + messageId = data.MessageId + console.log(`Message sent: ${messageId}`) + } catch (err) { + console.error(`Error sending message: ${err}`) + } + + // Execute the query with retries/sleeps + let RETRIES = 200, WAIT_SECONDS = 15 + const success = await isDeploymentSuccessful(messageId, RETRIES, WAIT_SECONDS) + if (!success) { + process.exit(1) + } + }).catch((error) => + console.log(`Error reading deploy config ${process.env.TEST_DEFINITION_FILE}: ${error}`) + ) +} + +if (require.main === module) { + main() +} diff --git a/.github/workflows/scripts/deployer-platform/package.json b/.github/workflows/scripts/deployer-platform/package.json new file mode 100644 index 000000000..68b19825d --- /dev/null +++ b/.github/workflows/scripts/deployer-platform/package.json @@ -0,0 +1,19 @@ +{ + "name": "dp", + "version": "1.0.0", + "description": "Runs OIL tests using deployer-platform to provision target hosts", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@actions/core": "1.2.6", + "@aws-sdk/client-sqs": "3.27.0", + "@aws-sdk/client-dynamodb": "3.27.0", + "@aws-sdk/util-dynamodb": "3.27.0", + "@aws-sdk/credential-providers": "3.451.0" + }, + "keywords": [], + "author": "", + "license": "ISC" +} \ No newline at end of file