From d5c28d4ea6e64f47bc7eaa13a7df3e3763154f69 Mon Sep 17 00:00:00 2001 From: Alex Ivanov Date: Tue, 19 Mar 2024 17:27:40 +0000 Subject: [PATCH] create in-cloud action for running Meticulous tests in the cloud --- .gitignore | 4 +- Dockerfile | 2 +- out/in-cloud.entrypoint.js | 2 + package.json | 30 ++++-- src/actions/in-cloud/get-inputs.ts | 26 +++++ src/actions/in-cloud/in-cloud.ts | 120 ++++++++++++++++++++++ src/{utils => actions/main}/get-inputs.ts | 4 +- src/{action.ts => actions/main/main.ts} | 45 +++----- src/in-cloud.entrypoint.ts | 8 ++ src/{index.ts => main.entrypoint.ts} | 2 +- src/utils/octokit.ts | 19 ++++ yarn.lock | 56 +++++++++- 12 files changed, 267 insertions(+), 51 deletions(-) create mode 100644 out/in-cloud.entrypoint.js create mode 100644 src/actions/in-cloud/get-inputs.ts create mode 100644 src/actions/in-cloud/in-cloud.ts rename src/{utils => actions/main}/get-inputs.ts (95%) rename src/{action.ts => actions/main/main.ts} (80%) create mode 100644 src/in-cloud.entrypoint.ts rename src/{index.ts => main.entrypoint.ts} (76%) create mode 100644 src/utils/octokit.ts diff --git a/.gitignore b/.gitignore index cb7b402d..5f5760ef 100644 --- a/.gitignore +++ b/.gitignore @@ -119,7 +119,6 @@ web_modules/ # Next.js build output .next -out # Nuxt.js build / generate output .nuxt @@ -178,7 +177,8 @@ dist .LSOverride # Icon must end with two \r -Icon +Icon + # Thumbnails ._* diff --git a/Dockerfile b/Dockerfile index 6b3da65a..60432076 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,4 +27,4 @@ COPY src src RUN yarn build -CMD ["/app/dist/index.mjs"] +CMD ["/app/dist/main/main.entrypoint.js"] diff --git a/out/in-cloud.entrypoint.js b/out/in-cloud.entrypoint.js new file mode 100644 index 00000000..65b89060 --- /dev/null +++ b/out/in-cloud.entrypoint.js @@ -0,0 +1,2 @@ +var e=require("@actions/core"),t=require("@actions/github"),o=require("@alwaysmeticulous/common"),r=require("@alwaysmeticulous/remote-replay-launcher"),n=require("@alwaysmeticulous/sentry"),s=require("loglevel"),a=require("retry"),i=require("@alwaysmeticulous/client"),l=require("luxon"),u=require("child_process");function c(e){return e&&e.__esModule?e.default:e}const h=async e=>{const t=a.operation({retries:7,factor:2,minTimeout:1e3}),o=new URL(e);t.attempt((async()=>{await p(o)||t.retry(new Error(`Could not connect to '${e}'. Please check:\n\n1. The server running at '${e}' has fully started by the time the Meticulous action starts. You may need to add a 'sleep 30' after starting the server to ensure that this is the case.\n2. The server running at '${e}' is using tcp instead of tcp6. You can use 'netstat -tulpen' to see what addresses and ports it is bound to.\n\n`))}))},p=async e=>{try{const t=await async function(e){const t=new AbortController,o=setTimeout((()=>t.abort()),5e3),r=await fetch(e,{signal:t.signal});return clearTimeout(o),r}(e);return 502!==t.status}catch(e){return!1}};[" permissions:"," actions: write"," checks: write"," contents: read"," discussions: write"," pull-requests: write"," statuses: write"," deployments: read",""].join("\n");const w=e=>g(e).toLowerCase().includes("resource not accessible by integration")||403===e?.status,g=e=>{const t=e?.message??"";return"string"==typeof t?t:""},d=e=>{const t=c(s).getLogger(o.METICULOUS_LOGGER_NAME);switch((e||"").toLocaleLowerCase()){case"trace":t.setLevel(c(s).levels.TRACE,!1);break;case"debug":t.setLevel(c(s).levels.DEBUG,!1);break;case"info":t.setLevel(c(s).levels.INFO,!1);break;case"warn":t.setLevel(c(s).levels.WARN,!1);break;case"error":t.setLevel(c(s).levels.ERROR,!1);break;case"silent":t.setLevel(c(s).levels.SILENT,!1)}},f=e=>e.slice(0,7),m=l.Duration.fromObject({seconds:10}),b=l.Duration.fromObject({seconds:5}),y=async({context:e,octokit:t})=>{const{owner:o,repo:r}=e.repo,n=e.runId,{data:s}=await t.rest.actions.getWorkflowRun({owner:o,repo:r,run_id:n});return{workflowId:s.workflow_id}},k=async({owner:e,repo:t,workflowId:r,ref:n,commitSha:a,octokit:i})=>{try{await i.rest.actions.createWorkflowDispatch({owner:e,repo:t,workflow_id:r,ref:n})}catch(e){const t=c(s).getLogger(o.METICULOUS_LOGGER_NAME);return(e?.message??"").includes("Workflow does not have 'workflow_dispatch' trigger")?(t.error(`Could not trigger a workflow run on commit ${f(a)} of the base branch (${n}) to compare against, because there was no Meticulous workflow with the 'workflow_dispatch' trigger on the ${n} branch. Visual snapshots of the new flows will be taken, but no comparisons will be made. If you haven't merged the PR to setup Meticulous in Github Actions to the ${n} branch yet then this is expected. Otherwise please check that Meticulous is running on the ${n} branch, that it has a 'workflow_dispatch' trigger, and has the appropiate permissions. See https://app.meticulous.ai/docs/github-actions-v2 for the correct setup.`),void t.debug(e)):w(e)?(t.error(`Missing permission to trigger a workflow run on the base branch (${n}). Visual snapshots of the new flows will be taken, but no comparisons will be made. Please add the 'actions: write' permission to your workflow YAML file: see https://app.meticulous.ai/docs/github-actions-v2 for the correct setup.`),void t.debug(e)):void t.error(`Could not trigger a workflow run on commit ${f(a)} of the base branch (${n}) to compare against. Visual snapshots of the new flows will be taken, but no comparisons will be made. Please check that Meticulous is running on the ${n} branch, that it has a 'workflow_dispatch' trigger, and has the appropiate permissions. See https://app.meticulous.ai/docs/github-actions-v2 for the correct setup.`,e)}await E(m);return await _({owner:e,repo:t,workflowId:r,commitSha:a,octokit:i})},$=async({owner:e,repo:t,workflowRunId:r,octokit:n,timeout:a})=>{const i=c(s).getLogger(o.METICULOUS_LOGGER_NAME);let u=null;const h=l.DateTime.now();for(;(null==u||v(u.status))&&l.DateTime.now().diff(h){const i=c(s).getLogger(o.METICULOUS_LOGGER_NAME),l=await a.rest.actions.listWorkflowRuns({owner:e,repo:t,workflow_id:r,head_sha:n});i.debug(`Workflow runs list: ${JSON.stringify(l.data,null,2)}`);const u=l.data.workflow_runs.find((e=>v(e.status)));if(null!=u)return{...u,workflowRunId:u.id}},v=e=>["in_progress","queued","requested","waiting"].some((t=>t===e)),E=async e=>new Promise((t=>setTimeout(t,e.toMillis()))),T=l.Duration.fromObject({minutes:30}),S=l.Duration.fromObject({minutes:10}),L=async(...t)=>{const r=c(s).getLogger(o.METICULOUS_LOGGER_NAME);try{return await q(...t)}catch(o){r.error(o);const n=`Error while running tests on base ${t[0].base}. No diffs will be reported for this run.`;return r.warn(n),(0,e.warning)(n),{shaToCompareAgainst:null}}},q=async({event:t,apiToken:r,base:n,context:a,octokit:l})=>{const u=c(s).getLogger(o.METICULOUS_LOGGER_NAME),{owner:h,repo:p}=a.repo;if(!n)return{shaToCompareAgainst:null};const w=await(0,i.getLatestTestRunResults)({client:(0,i.createClient)({apiToken:r}),commitSha:n,logicalEnvironmentVersion:5});if(null!=w)return u.info(`Tests already exist for commit ${n} (${w.id})`),{shaToCompareAgainst:n};const{workflowId:g}=await y({context:a,octokit:l}),d=await _({owner:h,repo:p,workflowId:g,commitSha:n,octokit:l});if(null!=d)return u.info(`Waiting on workflow run on base commit (${n}) to compare against: ${d.html_url}`),"pull_request"===t.type?(await U({owner:h,repo:p,workflowRunId:d.workflowRunId,octokit:l,commitSha:n,timeout:T}),{shaToCompareAgainst:n}):await R({owner:h,repo:p,workflowRunId:d.workflowRunId,octokit:l,commitSha:n,timeout:S,logger:u});if("pull_request"!==t.type)return{shaToCompareAgainst:null};const f=t.payload.pull_request.base.ref;u.debug(JSON.stringify({base:n,baseRef:f},null,2));const m=await I({owner:h,repo:p,ref:f,octokit:l});if(u.debug(JSON.stringify({owner:h,repo:p,base:n,baseRef:f,currentBaseSha:m},null,2)),n!==m){const t=`Meticulous tests on base commit ${n} haven't started running so we have nothing to compare against.\n In addition we were not able to trigger a run on ${n} since the '${f}' branch is now pointing to ${m}.\n Therefore no diffs will be reported for this run. Re-running the tests may fix this.`;return u.warn(t),(0,e.warning)(t),{shaToCompareAgainst:null}}const b=await k({owner:h,repo:p,workflowId:g,ref:f,commitSha:n,octokit:l});if(null==b){const t=`Warning: Could not retrieve dispatched workflow run. Will not perform diffs against ${n}.`;return u.warn(t),(0,e.warning)(t),{shaToCompareAgainst:null}}return u.info(`Waiting on workflow run: ${b.html_url}`),await U({owner:h,repo:p,workflowRunId:b.workflowRunId,octokit:l,commitSha:n,timeout:T}),{shaToCompareAgainst:n}},U=async({commitSha:e,...t})=>{const o=await $(t);if(null==o||v(o.status))throw new Error(`Timed out while waiting for workflow run (${t.workflowRunId}) to complete.`);if("completed"!==o.status||"success"!==o.conclusion)throw new Error(`Comparing against visual snapshots taken on ${e}, but the corresponding workflow run [${o.id}] did not complete successfully. See: ${o.html_url}`)},R=async({commitSha:e,logger:t,...o})=>{const r=await $(o);return null==r||v(r.status)?(t.warn(`Timed out while waiting for workflow run (${o.workflowRunId}) to complete. Running without comparisons.`),{shaToCompareAgainst:null}):"completed"!==r.status||"success"!==r.conclusion?(t.warn(`Comparing against visual snapshots taken on ${e}, but the corresponding workflow run [${r.id}] did not complete successfully. See: ${r.html_url}. Running without comparisons.`),{shaToCompareAgainst:null}):{shaToCompareAgainst:e}},I=async({owner:e,repo:t,ref:r,octokit:n})=>{try{const o=await n.rest.repos.getBranch({owner:e,repo:t,branch:r});return o.data.commit.sha}catch(e){if(w(e))throw new Error(`Missing permission to get the head commit of the branch '${r}'. This is required in order to correctly calculate the two commits to compare. Please add the 'contents: read' permission to your workflow YAML file: see https://app.meticulous.ai/docs/github-actions-v2 for the correct setup.`);throw c(s).getLogger(o.METICULOUS_LOGGER_NAME).error(`Unable to get head commit of branch '${r}'. This is required in order to correctly calculate the two commits to compare. Please check www.githubstatus.com, and that you have setup the action correctly, including with the correct permissions: see https://app.meticulous.ai/docs/github-actions-v2 for the correct setup.`),e}},A=({event:e,head:t})=>"push"===e.type?{context:{type:"github",event:"push",beforeSha:e.payload.before,afterSha:e.payload.after,ref:e.payload.ref}}:"pull_request"===e.type?{context:{type:"github",event:"pull-request",title:e.payload.pull_request.title,number:e.payload.pull_request.number,htmlUrl:e.payload.pull_request.html_url,baseSha:e.payload.pull_request.base.sha,headSha:e.payload.pull_request.head.sha}}:{context:{type:"github",event:"workflow-dispatch",ref:e.payload.ref,inputs:e.payload.inputs,headSha:t}},C=async(e,o)=>{if("pull_request"===e.type){const t=e.payload.pull_request.head.sha,r=e.payload.pull_request.base.sha,n=e.payload.pull_request.base.ref;return o.useDeploymentUrl?{base:await M(t,r,n)??r,head:t}:{base:await G(t,r)??r,head:t}}return"push"===e.type?{base:e.payload.before,head:e.payload.after}:"workflow_dispatch"===e.type?{base:null,head:t.context.sha}:O(e)},O=e=>{throw new Error("Unexpected event: "+JSON.stringify(e))},M=(e,t,r)=>{const n=c(s).getLogger(o.METICULOUS_LOGGER_NAME);try{N(),(0,u.execSync)(`git fetch origin ${e}`),(0,u.execSync)(`git fetch origin ${r}`);const o=(0,u.execSync)(`git merge-base ${e} origin/${r}`).toString().trim();return x(o)?o:(n.error(`Failed to get merge base of ${e} and ${r}: value returned by 'git merge-base' was not a valid git SHA ('${o}').Using the base of the pull request instead (${t}).`),null)}catch(o){return n.error(`Failed to get merge base of ${e} and ${r}. Error: ${o}. Using the base of the pull request instead (${t}).`),null}},G=(e,t)=>{const r=process.env.GITHUB_SHA,n=c(s).getLogger(o.METICULOUS_LOGGER_NAME);if(null==r)return n.error(`No GITHUB_SHA environment var set, so can't work out true base of the merge commit. Using the base of the pull request instead (${t}).`),null;try{N();const o=(0,u.execSync)("git rev-list --max-count=1 HEAD").toString().trim();if(o!==r)return n.info(`The head commit SHA (${o}) does not equal GITHUB_SHA environment variable (${r}).\n This is likely because a custom ref has been passed to the 'actions/checkout' action. We're assuming therefore\n that the head commit SHA is not a temporary merge commit, but rather the head of the branch. Therefore we're\n using the base of the pull request (${t}) to compare the visual snapshots against, and not the base\n of GitHub's temporary merge commit.`),null;const s=(0,u.execSync)(`git cat-file -p ${r}`).toString().split("\n").filter((e=>e.startsWith("parent "))).map((e=>e.substring(7).trim()));if(2!==s.length)return n.error(`GITHUB_SHA (${r}) is not a merge commit, so can't work out true base of the merge commit. Using the base of the pull request instead.`),null;const a=s[0];return s[1]!==e?(n.error(`The second parent (${s[1]}) of the GITHUB_SHA merge commit (${r}) is not equal to the head of the PR (${e}),\n so can not confidently determine the base of the merge commit to compare against. Using the base of the pull request instead (${t}).`),null):a}catch(e){return n.error(`Error getting base of merge commit (${r}). Using the base of the pull request instead (${t}).`,e),null}},N=()=>{(0,u.execSync)(`git config --global --add safe.directory "${process.cwd()}"`)},x=e=>/^[a-f0-9]{40}$/.test(e),H=(e,t)=>"push"===e?{type:"push",payload:t}:"pull_request"===e?{type:"pull_request",payload:t}:"workflow_dispatch"===e?{type:"workflow_dispatch",payload:t}:null,D=e=>{if(null==e)throw new Error("github-token is required");try{return(0,t.getOctokit)(e)}catch(e){throw c(s).getLogger(o.METICULOUS_LOGGER_NAME).error(e),new Error("Error connecting to GitHub. Did you specify a valid 'github-token'?")}},W=({name:e,required:t,type:o})=>{const r=e.toUpperCase().replaceAll("-","_"),n=process.env[r];if(("string"===o||"string-array"===o)&&""===n&&!t)return null;const s=B(n,o);if(t&&null==s)throw new Error(`Input ${e} is required`);if(t&&F(s)&&"string-array"!==o)throw new Error(`Input ${e} is required`);if(null!=s&&typeof s!==j(o))throw new Error(`Expected ${o} for input ${e}, but got ${typeof s}`);return s},B=(e,t)=>{if(null==e)return null;if("string"===t)return e;if("string-array"===t)return-1===e.indexOf("\n")?e.split(",").map((e=>e.trim())).filter((e=>""!==e)):e.split("\n").map((e=>e.trim())).filter((e=>""!==e));if("int"===t){const t=Number.parseInt(e);return isNaN(t)?null:t}if("float"===t){const t=Number.parseFloat(e);return isNaN(t)?null:t}if("boolean"===t){if(""===e)return null;if("true"!==e&&"false"!==e)throw new Error("Boolean inputs must be equal to the string 'true' or the string 'false'");return"true"===e}return P(t)},P=e=>{throw new Error(`Only string or number inputs currently supported, but got ${e}`)},F=e=>null==e||"string"==typeof e&&0===e.length,j=e=>"string-array"===e?"object":"string"===e?"string":"int"===e||"float"===e?"number":"boolean"===e?"boolean":P(e);(async()=>{c(s).getLogger(o.METICULOUS_LOGGER_NAME).setDefaultLevel(c(s).levels.INFO);const a=await(0,n.initSentry)("report-diffs-action-in-cloud-v1",1),i=a.startTransaction({name:"report-diffs-action.runMeticulousTestsActionInCloud",description:"Run Meticulous tests action (in cloud)",op:"report-diffs-action.runMeticulousTestsActionInCloud"});+(process.env.RUNNER_DEBUG??"0")&&d("trace");const{apiToken:l,githubToken:u,appUrl:p}={apiToken:W({name:"api-token",required:!0,type:"string"}),githubToken:W({name:"github-token",required:!0,type:"string"}),appUrl:W({name:"app-url",required:!0,type:"string"})},{payload:w}=t.context,g=H(t.context.eventName,w),m=D(u),b=c(s).getLogger(o.METICULOUS_LOGGER_NAME);if(null==g)return void b.warn(`Running report-diffs-action is only supported for 'push', 'pull_request' and 'workflow_dispatch' events, but was triggered on a '${t.context.eventName}' event. Skipping execution.`);const{base:y,head:k}=await C(g,{useDeploymentUrl:!1}),{shaToCompareAgainst:$}=(A({event:g,head:k}),await L({event:g,apiToken:l,base:y,context:t.context,octokit:m}));null!=$&&"pull_request"===g.type?b.info(`Comparing visual snapshots for the commit head of this PR, ${f(k)}, against ${f($)}`):null!=$?b.info(`Comparing visual snapshots for commit ${f(k)} against commit ${f($)}}`):b.info(`Generating visual snapshots for commit ${f(k)}`);try{await h(p);const e=({url:e,basicAuthUser:t,basicAuthPassword:o})=>{b.info(`Secure tunnel to ${p} created: ${e}, user: ${t}, password: ${o}`)};await(0,r.executeRemoteTestRun)({apiToken:l,appUrl:p,commitSha:k,environment:"github-actions",onTunnelCreated:e}),i.setStatus("ok"),i.finish(),await(a.getClient()?.close(5e3)),process.exit(0)}catch(t){const o=t instanceof Error?t.message:`${t}`;(0,e.setFailed)(o),i.setStatus("unknown_error"),i.finish(),await(a.getClient()?.close(5e3)),process.exit(1)}})().catch((t=>{const o=t instanceof Error?t.message:`${t}`;(0,e.setFailed)(o),process.exit(1)})); +//# sourceMappingURL=in-cloud.entrypoint.js.map diff --git a/package.json b/package.json index a3fb1dab..24768d47 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,19 @@ "description": "Run Meticulous tests", "license": "Unliscensed", "private": true, - "type": "module", - "source": "src/index.ts", - "main": "dist/index.mjs", - "types": "dist/index.d.ts", + "targets": { + "main": { + "source": "src/main.entrypoint.ts", + "distDir": "dist" + }, + "in-cloud": { + "source": "src/in-cloud.entrypoint.ts", + "distDir": "out" + } + }, "files": [ - "dist" + "dist", + "out" ], "scripts": { "clean": "rimraf dist tsconfig.tsbuildinfo", @@ -25,11 +32,12 @@ "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^5.1.1", - "@alwaysmeticulous/client": "^2.97.0", - "@alwaysmeticulous/common": "^2.97.0", + "@alwaysmeticulous/client": "^2.118.0", + "@alwaysmeticulous/common": "^2.118.0", + "@alwaysmeticulous/remote-replay-launcher": "^2.118.0", "//": "Upgrading `replay-orchestrator-launcher`? Consider bumping the environment version `LOGICAL_ENVIRONMENT_VERSION` in `constants.ts` if the new version includes visible changes.", "@alwaysmeticulous/replay-orchestrator-launcher": "^2.100.0", - "@alwaysmeticulous/sentry": "^2.72.0", + "@alwaysmeticulous/sentry": "^2.118.0", "@sentry/node": "^7.50.0", "lodash.debounce": "^4.0.8", "loglevel": "^1.8.1", @@ -37,8 +45,8 @@ "retry": "^0.13.1" }, "devDependencies": { - "@alwaysmeticulous/api": "^2.97.0", - "@alwaysmeticulous/sdk-bundles-api": "^2.97.0", + "@alwaysmeticulous/api": "^2.116.0", + "@alwaysmeticulous/sdk-bundles-api": "^2.116.0", "@parcel/packager-ts": "^2.8.3", "@parcel/transformer-typescript-types": "^2.8.3", "@types/jest": "^27.0.3", @@ -60,7 +68,7 @@ "yaml": "^2.4.1" }, "engines": { - "node": ">= 16" + "node": ">= 20" }, "repository": { "type": "git", diff --git a/src/actions/in-cloud/get-inputs.ts b/src/actions/in-cloud/get-inputs.ts new file mode 100644 index 00000000..6c600f5e --- /dev/null +++ b/src/actions/in-cloud/get-inputs.ts @@ -0,0 +1,26 @@ +import { getInputFromEnv } from "../../utils/get-input-from-env"; + +export const getInCloudActionInputs = () => { + // The names, required value, and types should match that in action.yml + const apiToken = getInputFromEnv({ + name: "api-token", + required: true, + type: "string", + }); + const githubToken = getInputFromEnv({ + name: "github-token", + required: true, + type: "string", + }); + const appUrl = getInputFromEnv({ + name: "app-url", + required: true, + type: "string", + }); + + return { + apiToken, + githubToken, + appUrl, + }; +}; diff --git a/src/actions/in-cloud/in-cloud.ts b/src/actions/in-cloud/in-cloud.ts new file mode 100644 index 00000000..7d43c916 --- /dev/null +++ b/src/actions/in-cloud/in-cloud.ts @@ -0,0 +1,120 @@ +import { setFailed } from "@actions/core"; +import { context } from "@actions/github"; +import { METICULOUS_LOGGER_NAME } from "@alwaysmeticulous/common"; +import { executeRemoteTestRun } from "@alwaysmeticulous/remote-replay-launcher"; +import { initSentry } from "@alwaysmeticulous/sentry"; +import log from "loglevel"; +import { throwIfCannotConnectToOrigin } from "../../utils/check-connection"; +import { safeEnsureBaseTestsExists } from "../../utils/ensure-base-exists.utils"; +import { getEnvironment } from "../../utils/environment.utils"; +import { getBaseAndHeadCommitShas } from "../../utils/get-base-and-head-commit-shas"; +import { getCodeChangeEvent } from "../../utils/get-code-change-event"; +import { initLogger, setLogLevel, shortSha } from "../../utils/logger.utils"; +import { getOctokitOrFail } from "../../utils/octokit"; +import { getInCloudActionInputs } from "./get-inputs"; + +export const runMeticulousTestsInCloudAction = async (): Promise => { + initLogger(); + + // Init Sentry without sampling traces on the action run. + // Children processes, (test run executions) will use + // the global sample rate. + const sentryHub = await initSentry("report-diffs-action-in-cloud-v1", 1.0); + + const transaction = sentryHub.startTransaction({ + name: "report-diffs-action.runMeticulousTestsActionInCloud", + description: "Run Meticulous tests action (in cloud)", + op: "report-diffs-action.runMeticulousTestsActionInCloud", + }); + + if (+(process.env["RUNNER_DEBUG"] ?? "0")) { + setLogLevel("trace"); + } + + const { apiToken, githubToken, appUrl } = getInCloudActionInputs(); + const { payload } = context; + const event = getCodeChangeEvent(context.eventName, payload); + const octokit = getOctokitOrFail(githubToken); + const logger = log.getLogger(METICULOUS_LOGGER_NAME); + + if (event == null) { + logger.warn( + `Running report-diffs-action is only supported for 'push', \ + 'pull_request' and 'workflow_dispatch' events, but was triggered \ + on a '${context.eventName}' event. Skipping execution.` + ); + return; + } + + const { base, head } = await getBaseAndHeadCommitShas(event, { + useDeploymentUrl: false, + }); + const environment = getEnvironment({ event, head }); + + const { shaToCompareAgainst } = await safeEnsureBaseTestsExists({ + event, + apiToken, + base, + context, + octokit, + }); + + if (shaToCompareAgainst != null && event.type === "pull_request") { + logger.info( + `Comparing visual snapshots for the commit head of this PR, ${shortSha( + head + )}, against ${shortSha(shaToCompareAgainst)}` + ); + } else if (shaToCompareAgainst != null) { + logger.info( + `Comparing visual snapshots for commit ${shortSha( + head + )} against commit ${shortSha(shaToCompareAgainst)}}` + ); + } else { + logger.info(`Generating visual snapshots for commit ${shortSha(head)}`); + } + + try { + await throwIfCannotConnectToOrigin(appUrl); + + const onTunnelCreated = ({ + url, + basicAuthUser, + basicAuthPassword, + }: { + url: string; + basicAuthUser: string; + basicAuthPassword: string; + }) => { + logger.info( + `Secure tunnel to ${appUrl} created: ${url}, user: ${basicAuthUser}, password: ${basicAuthPassword}` + ); + }; + + await executeRemoteTestRun({ + apiToken, + appUrl, + commitSha: head, + environment: "github-actions", + onTunnelCreated, + }); + + transaction.setStatus("ok"); + transaction.finish(); + + await sentryHub.getClient()?.close(5_000); + + process.exit(0); + } catch (error) { + const message = error instanceof Error ? error.message : `${error}`; + setFailed(message); + + transaction.setStatus("unknown_error"); + transaction.finish(); + + await sentryHub.getClient()?.close(5_000); + + process.exit(1); + } +}; diff --git a/src/utils/get-inputs.ts b/src/actions/main/get-inputs.ts similarity index 95% rename from src/utils/get-inputs.ts rename to src/actions/main/get-inputs.ts index 937e052c..d44079a7 100644 --- a/src/utils/get-inputs.ts +++ b/src/actions/main/get-inputs.ts @@ -1,6 +1,6 @@ -import { getInputFromEnv } from "./get-input-from-env"; +import { getInputFromEnv } from "../../utils/get-input-from-env"; -export const getInputs = () => { +export const getMainActionInputs = () => { // The names, required value, and types should match that in action.yml const apiToken = getInputFromEnv({ name: "api-token", diff --git a/src/action.ts b/src/actions/main/main.ts similarity index 80% rename from src/action.ts rename to src/actions/main/main.ts index d68383fd..95cbe1c3 100644 --- a/src/action.ts +++ b/src/actions/main/main.ts @@ -1,5 +1,5 @@ import { setFailed } from "@actions/core"; -import { context, getOctokit } from "@actions/github"; +import { context } from "@actions/github"; import { DEFAULT_EXECUTION_OPTIONS, METICULOUS_LOGGER_NAME, @@ -10,18 +10,19 @@ import { RunningTestRunExecution } from "@alwaysmeticulous/sdk-bundles-api"; import { initSentry } from "@alwaysmeticulous/sentry"; import debounce from "lodash.debounce"; import log from "loglevel"; -import { addLocalhostAliases } from "./utils/add-localhost-aliases"; -import { throwIfCannotConnectToOrigin } from "./utils/check-connection"; -import { LOGICAL_ENVIRONMENT_VERSION } from "./utils/constants"; -import { safeEnsureBaseTestsExists } from "./utils/ensure-base-exists.utils"; -import { getEnvironment } from "./utils/environment.utils"; -import { getBaseAndHeadCommitShas } from "./utils/get-base-and-head-commit-shas"; -import { getCodeChangeEvent } from "./utils/get-code-change-event"; -import { getInputs } from "./utils/get-inputs"; -import { initLogger, setLogLevel, shortSha } from "./utils/logger.utils"; -import { spinUpProxyIfNeeded } from "./utils/proxy"; -import { ResultsReporter } from "./utils/results-reporter"; -import { waitForDeploymentUrl } from "./utils/wait-for-deployment-url"; +import { addLocalhostAliases } from "../../utils/add-localhost-aliases"; +import { throwIfCannotConnectToOrigin } from "../../utils/check-connection"; +import { LOGICAL_ENVIRONMENT_VERSION } from "../../utils/constants"; +import { safeEnsureBaseTestsExists } from "../../utils/ensure-base-exists.utils"; +import { getEnvironment } from "../../utils/environment.utils"; +import { getBaseAndHeadCommitShas } from "../../utils/get-base-and-head-commit-shas"; +import { getCodeChangeEvent } from "../../utils/get-code-change-event"; +import { initLogger, setLogLevel, shortSha } from "../../utils/logger.utils"; +import { getOctokitOrFail } from "../../utils/octokit"; +import { spinUpProxyIfNeeded } from "../../utils/proxy"; +import { ResultsReporter } from "../../utils/results-reporter"; +import { waitForDeploymentUrl } from "../../utils/wait-for-deployment-url"; +import { getMainActionInputs } from "./get-inputs"; const EXECUTION_OPTIONS = { ...DEFAULT_EXECUTION_OPTIONS, @@ -59,7 +60,7 @@ export const runMeticulousTestsAction = async (): Promise => { useDeploymentUrl, allowedEnvironments, testSuiteId, - } = getInputs(); + } = getMainActionInputs(); const { payload } = context; const event = getCodeChangeEvent(context.eventName, payload); const { owner, repo } = context.repo; @@ -197,19 +198,3 @@ export const runMeticulousTestsAction = async (): Promise => { process.exit(1); } }; - -const getOctokitOrFail = (githubToken: string | null) => { - if (githubToken == null) { - throw new Error("github-token is required"); - } - - try { - return getOctokit(githubToken); - } catch (err) { - const logger = log.getLogger(METICULOUS_LOGGER_NAME); - logger.error(err); - throw new Error( - "Error connecting to GitHub. Did you specify a valid 'github-token'?" - ); - } -}; diff --git a/src/in-cloud.entrypoint.ts b/src/in-cloud.entrypoint.ts new file mode 100644 index 00000000..8776fa0a --- /dev/null +++ b/src/in-cloud.entrypoint.ts @@ -0,0 +1,8 @@ +import { setFailed } from "@actions/core"; +import { runMeticulousTestsInCloudAction } from "./actions/in-cloud/in-cloud"; + +runMeticulousTestsInCloudAction().catch((error) => { + const message = error instanceof Error ? error.message : `${error}`; + setFailed(message); + process.exit(1); +}); diff --git a/src/index.ts b/src/main.entrypoint.ts similarity index 76% rename from src/index.ts rename to src/main.entrypoint.ts index 07ffa128..809b3292 100644 --- a/src/index.ts +++ b/src/main.entrypoint.ts @@ -1,5 +1,5 @@ import { setFailed } from "@actions/core"; -import { runMeticulousTestsAction } from "./action"; +import { runMeticulousTestsAction } from "./actions/main/main"; runMeticulousTestsAction().catch((error) => { const message = error instanceof Error ? error.message : `${error}`; diff --git a/src/utils/octokit.ts b/src/utils/octokit.ts new file mode 100644 index 00000000..db920c6c --- /dev/null +++ b/src/utils/octokit.ts @@ -0,0 +1,19 @@ +import { getOctokit } from "@actions/github"; +import { METICULOUS_LOGGER_NAME } from "@alwaysmeticulous/common"; +import log from "loglevel"; + +export const getOctokitOrFail = (githubToken: string | null) => { + if (githubToken == null) { + throw new Error("github-token is required"); + } + + try { + return getOctokit(githubToken); + } catch (err) { + const logger = log.getLogger(METICULOUS_LOGGER_NAME); + logger.error(err); + throw new Error( + "Error connecting to GitHub. Did you specify a valid 'github-token'?" + ); + } +}; diff --git a/yarn.lock b/yarn.lock index 10745f0e..e0d26340 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,11 +32,27 @@ dependencies: tunnel "^0.0.6" +"@alwaysmeticulous/api@^2.116.0": + version "2.116.0" + resolved "https://registry.yarnpkg.com/@alwaysmeticulous/api/-/api-2.116.0.tgz#e9b4d3d343d990fe103d3f6e2ac886d4a7c65ebd" + integrity sha512-uQoywSRU7ZshunwsFWnNPYfLPtHPtrM0Om2Mzta0hQG5ckAAYFvRZ/k1qkXzHoZvVEbnpfnvDhbQ+VcRv7CdhA== + "@alwaysmeticulous/api@^2.97.0": version "2.97.0" resolved "https://registry.yarnpkg.com/@alwaysmeticulous/api/-/api-2.97.0.tgz#4264db8b423a2147661966f4b0827aa73795f0eb" integrity sha512-WGa6zTfB7JctUUDuYsdkJjoqFu4n62CgFETmy8eIMLjiiFk3IJ1ZBL67Ih4AXg4vr1aAHQtPsTR/Fp7NQZEARA== +"@alwaysmeticulous/client@^2.118.0": + version "2.118.0" + resolved "https://registry.yarnpkg.com/@alwaysmeticulous/client/-/client-2.118.0.tgz#4cc203cae242f20890deb30ea8b2cfb31d5c198d" + integrity sha512-2c2wuiY/EVJyuwm4OE8AMhxQhlBC0YQq0f9V6mT4LTfTdSnFYWqTgl6amWQ4XV5tNxU7RghzG5PYRFzsHi6Jkw== + dependencies: + "@alwaysmeticulous/api" "^2.116.0" + "@alwaysmeticulous/common" "^2.118.0" + axios "^1.2.6" + axios-retry "^3.5.0" + loglevel "^1.8.0" + "@alwaysmeticulous/client@^2.97.0": version "2.97.0" resolved "https://registry.yarnpkg.com/@alwaysmeticulous/client/-/client-2.97.0.tgz#1a9cf13646ab475c62bdf9f6f54684761821199b" @@ -48,6 +64,14 @@ axios-retry "^3.5.0" loglevel "^1.8.0" +"@alwaysmeticulous/common@^2.118.0": + version "2.118.0" + resolved "https://registry.yarnpkg.com/@alwaysmeticulous/common/-/common-2.118.0.tgz#33ee4314d76d774957a5f9ef983d8529019b34e3" + integrity sha512-P/gwfaee3uvU9YyxPRGtMdjSn/tfLMB/U0P4ioMIrW0j2ktxZOiHmuuVY+Dv/PNmr7685N5EM0vO7sHdOsnUNw== + dependencies: + loglevel "^1.8.0" + luxon "^3.2.1" + "@alwaysmeticulous/common@^2.97.0": version "2.97.0" resolved "https://registry.yarnpkg.com/@alwaysmeticulous/common/-/common-2.97.0.tgz#d107d99da2b83343b83917afb118f696c6686928" @@ -72,6 +96,16 @@ p-limit "^3.1.0" proper-lockfile "^4.1.2" +"@alwaysmeticulous/remote-replay-launcher@^2.118.0": + version "2.118.0" + resolved "https://registry.yarnpkg.com/@alwaysmeticulous/remote-replay-launcher/-/remote-replay-launcher-2.118.0.tgz#c974eaad3698b525f796648fbe2f09cee49e6086" + integrity sha512-SpkWK6JL8EHUr6ufaaXNVHqZcelGOBiYxFH7QmhpCQnol4Ftoos1yuZPPQDqR7+rpBf6aVAjgaxWZGVxF7qG9w== + dependencies: + "@alwaysmeticulous/client" "^2.118.0" + "@alwaysmeticulous/common" "^2.118.0" + "@alwaysmeticulous/tunnels-client" "^2.118.0" + loglevel "^1.8.0" + "@alwaysmeticulous/replay-orchestrator-launcher@^2.100.0": version "2.100.0" resolved "https://registry.yarnpkg.com/@alwaysmeticulous/replay-orchestrator-launcher/-/replay-orchestrator-launcher-2.100.0.tgz#540dea870a1ff4f49fff358245904ab9ecfaf69f" @@ -83,20 +117,34 @@ loglevel "^1.8.0" puppeteer "21.9.0" +"@alwaysmeticulous/sdk-bundles-api@^2.116.0": + version "2.116.0" + resolved "https://registry.yarnpkg.com/@alwaysmeticulous/sdk-bundles-api/-/sdk-bundles-api-2.116.0.tgz#ff35f54445e77036644bc7808fe117ac6ef3ef76" + integrity sha512-G4ndttIx5D2vf2GrNYZGwA9sBfeBVPpeRG1fRBNfWMNKys9dXny9eN5ex8b5xAPRjZjWf73+6XLyJFbxZ1imZA== + "@alwaysmeticulous/sdk-bundles-api@^2.97.0": version "2.97.0" resolved "https://registry.yarnpkg.com/@alwaysmeticulous/sdk-bundles-api/-/sdk-bundles-api-2.97.0.tgz#1354284b033a7d2882996c7e04dc9e8381c1d751" integrity sha512-RZzfxMQbs1FSoaDvDI2c0beCTtsd7uv9iYAPyglv3AxH7YfdOxyMCewr+V4sPWUmlFSl3D69+l9hjPgw83KpiQ== -"@alwaysmeticulous/sentry@^2.72.0": - version "2.72.0" - resolved "https://registry.yarnpkg.com/@alwaysmeticulous/sentry/-/sentry-2.72.0.tgz#472391f330dc6b961c2be85da27ad93484cf517c" - integrity sha512-u8BL9/HApUTE2ZFW37c1qb3XPxPQGijJKxuLovkKmYr8+1a8Jgj/UzUOX0GP5AyHIGOI7c8MlgQpCYqEaZhhTw== +"@alwaysmeticulous/sentry@^2.118.0": + version "2.118.0" + resolved "https://registry.yarnpkg.com/@alwaysmeticulous/sentry/-/sentry-2.118.0.tgz#3ebca92e38fefa7ef6d5d9e357f4e8eeaa5a8929" + integrity sha512-jQIR8l5qd//nWhqSSj3CwNNd4OrOV2WzgHFnLBj2cX1N7ybhQ4pL8aPRoG9KtJIqkSTZS42dCd3BHnvS1+sbmw== dependencies: + "@alwaysmeticulous/common" "^2.118.0" "@sentry/node" "^7.36.0" "@sentry/tracing" "^7.36.0" luxon "^3.2.1" +"@alwaysmeticulous/tunnels-client@^2.118.0": + version "2.118.0" + resolved "https://registry.yarnpkg.com/@alwaysmeticulous/tunnels-client/-/tunnels-client-2.118.0.tgz#d9f58d1444ae83f29842c59c3988f990dc527835" + integrity sha512-SvxFJ+ECZlE3Ptd+FfvjwkQs0IY9l37bSOlRy0DCfpgq1AnGEJU5G4QtPT9r9qpxm2/p7wj2PGiiVFyT6MxXwg== + dependencies: + axios "^1.2.6" + loglevel "^1.8.0" + "@ampproject/remapping@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"