diff --git a/README.md b/README.md index 31014ba..8a2587e 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,28 @@ Full documentation including all inputs, outputs, and usage examples is availabl **[docs.devicecloud.dev/ci-cd/github-actions](https://docs.devicecloud.dev/ci-cd/github-actions)** +## Async runs + PR checks (save CI minutes) + +By default the action waits for your run to finish, so the job (and your GitHub +Actions minutes) stays billed for the whole suite. Set `async: true` to submit +the run and exit immediately: + +```yaml +- uses: devicecloud-dev/device-cloud-for-maestro@v2 + with: + api-key: ${{ secrets.DCD_API_KEY }} + app-file: + async: true +``` + +To still gate your PR on the result, install the **DeviceCloud GitHub App** and +connect it to your org in DeviceCloud → Settings → Integrations. DeviceCloud then +posts a `DeviceCloud / Mobile E2E` check on the commit/PR — `in progress` while +the suite runs, then pass/fail when it completes — with a "Re-run failed tests" +button. Make it a required status check in branch protection to block merges on +failures. No `permissions: checks: write` is needed in your workflow; the App +posts the check. + ## Migrating from Maestro Cloud Replace the `uses` line in your workflow: diff --git a/dist/index.js b/dist/index.js index 539ebc0..5d296b8 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2216,7 +2216,7 @@ const Context = __importStar(__nccwpck_require__(8663)); const Utils = __importStar(__nccwpck_require__(1365)); // octokit + plugins const core_1 = __nccwpck_require__(6895); -const plugin_rest_endpoint_methods_1 = __nccwpck_require__(9289); +const plugin_rest_endpoint_methods_1 = __nccwpck_require__(6495); const plugin_paginate_rest_1 = __nccwpck_require__(6212); exports.context = new Context.Context(); const baseUrl = Utils.getApiBaseUrl(); @@ -40645,7 +40645,7 @@ const getLatestDcdVersion = (...args_1) => __awaiter(void 0, [...args_1], void 0 } }); const run = () => __awaiter(void 0, void 0, void 0, function* () { - var _a; + var _a, _b; try { const { androidApiLevel, androidDevice, apiKey, apiUrl, appBinaryId, appFilePath, async, config, deviceLocale, downloadArtifacts, env, excludeFlows, excludeTags, googlePlay, ignoreShaCheck, includeTags, iOSVersion, iosDevice, jsonFile, maestroVersion, name, orientation, report, retry, workspaceFolder, runnerType, debug, moropoV1ApiKey, useBeta, maestroChromeOnboarding, androidNoSnapshot, disableAnimations, githubContext, } = yield (0, params_1.getParameters)(); const REMOVED_MAESTRO_VERSIONS = ['1.39.2', '1.39.7', '2.0.3']; @@ -40718,7 +40718,7 @@ const run = () => __awaiter(void 0, void 0, void 0, function* () { try { process.env.DCD_CI_WRAPPER_VERSION = (__nccwpck_require__(8330)/* .version */ .rE); } - catch (_b) { + catch (_c) { // best-effort — version is optional } // Execute the test command and capture the upload ID @@ -40738,6 +40738,21 @@ const run = () => __awaiter(void 0, void 0, void 0, function* () { if (!uploadId) { throw new Error('Failed to get upload ID from console URL'); } + // Async mode: the CLI has already submitted the run and exited (saving CI + // minutes). Do NOT poll for results — the DeviceCloud GitHub App reports the + // outcome as a "DeviceCloud" check on the commit/PR when the run completes. + // Just surface the upload id/console URL and exit successfully. + if (async) { + const consoleUrl = ((_b = testOutput === null || testOutput === void 0 ? void 0 : testOutput.match(/https:\/\/(?:dev\.)?console\.devicecloud\.dev\/results\?upload=[a-zA-Z0-9-]+/)) === null || _b === void 0 ? void 0 : _b[0]) || ''; + (0, core_1.setOutput)('DEVICE_CLOUD_CONSOLE_URL', consoleUrl); + (0, core_1.setOutput)('DEVICE_CLOUD_UPLOAD_STATUS', 'PENDING'); + (0, core_1.setOutput)('DEVICE_CLOUD_FLOW_RESULTS', '[]'); + console.info('Async run submitted; not waiting for results.' + + (consoleUrl ? ` Track progress: ${consoleUrl}` : '')); + console.info('Install the DeviceCloud GitHub App to get a pass/fail check on this ' + + 'commit/PR when the run completes: https://docs.devicecloud.dev/ci-cd/github-actions'); + return; + } // Get the test status and results const result = yield getTestStatus(uploadId, apiKey, dcdVersionString, apiUrl); if (result) { @@ -40877,13 +40892,16 @@ function getInferredName() { return github.context.sha; } function getGithubContextMetadata() { - var _a, _b, _c; + var _a, _b, _c, _d, _e; const ctx = github.context; const pr = ctx.payload.pull_request; const rawRef = (_b = (_a = pr === null || pr === void 0 ? void 0 : pr.head) === null || _a === void 0 ? void 0 : _a.ref) !== null && _b !== void 0 ? _b : ctx.ref; const branch = (_c = rawRef === null || rawRef === void 0 ? void 0 : rawRef.replace(/^refs\/heads\//, '')) !== null && _c !== void 0 ? _c : ''; + // On pull_request events ctx.sha is a throwaway *merge* commit; a GitHub + // check (and the developer-visible commit) must use the PR's head sha. + const headSha = (_e = (_d = pr === null || pr === void 0 ? void 0 : pr.head) === null || _d === void 0 ? void 0 : _d.sha) !== null && _e !== void 0 ? _e : ctx.sha; const pairs = [ - `gh_sha=${ctx.sha}`, + `gh_sha=${headSha}`, `gh_run_id=${ctx.runId}`, `gh_repo=${ctx.repo.owner}/${ctx.repo.repo}`, ]; @@ -43163,7 +43181,7 @@ paginateRest.VERSION = VERSION; /***/ }), -/***/ 9289: +/***/ 6495: /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => { "use strict"; @@ -43176,12 +43194,12 @@ __nccwpck_require__.d(__webpack_exports__, { restEndpointMethods: () => (/* binding */ restEndpointMethods) }); -;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoint-methods@17.0.0_@octokit+core@7.0.6/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/version.js +;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/version.js const VERSION = "17.0.0"; //# sourceMappingURL=version.js.map -;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoint-methods@17.0.0_@octokit+core@7.0.6/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/generated/endpoints.js +;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/generated/endpoints.js const Endpoints = { actions: { addCustomLabelsToSelfHostedRunnerForOrg: [ @@ -45475,7 +45493,7 @@ var endpoints_default = Endpoints; //# sourceMappingURL=endpoints.js.map -;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoint-methods@17.0.0_@octokit+core@7.0.6/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/endpoints-to-methods.js +;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/endpoints-to-methods.js const endpointMethodsMap = /* @__PURE__ */ new Map(); for (const [scope, endpoints] of Object.entries(endpoints_default)) { @@ -45601,7 +45619,7 @@ function decorate(octokit, scope, methodName, defaults, decorations) { //# sourceMappingURL=endpoints-to-methods.js.map -;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoint-methods@17.0.0_@octokit+core@7.0.6/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/index.js +;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/index.js function restEndpointMethods(octokit) { diff --git a/package.json b/package.json index 834663f..a3b2146 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "dcd-github-action", "description": "run maestro tests on devicecloud.dev", "author": "devicecloud.dev", - "version": "2.2.0", + "version": "2.3.0", "main": "src/index.ts", "license": "MIT", "engines": { diff --git a/src/index.ts b/src/index.ts index 7d4a797..8c9adb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -266,6 +266,29 @@ const run = async (): Promise => { throw new Error('Failed to get upload ID from console URL'); } + // Async mode: the CLI has already submitted the run and exited (saving CI + // minutes). Do NOT poll for results — the DeviceCloud GitHub App reports the + // outcome as a "DeviceCloud" check on the commit/PR when the run completes. + // Just surface the upload id/console URL and exit successfully. + if (async) { + const consoleUrl = + testOutput?.match( + /https:\/\/(?:dev\.)?console\.devicecloud\.dev\/results\?upload=[a-zA-Z0-9-]+/ + )?.[0] || ''; + setOutput('DEVICE_CLOUD_CONSOLE_URL', consoleUrl); + setOutput('DEVICE_CLOUD_UPLOAD_STATUS', 'PENDING'); + setOutput('DEVICE_CLOUD_FLOW_RESULTS', '[]'); + console.info( + 'Async run submitted; not waiting for results.' + + (consoleUrl ? ` Track progress: ${consoleUrl}` : '') + ); + console.info( + 'Install the DeviceCloud GitHub App to get a pass/fail check on this ' + + 'commit/PR when the run completes: https://docs.devicecloud.dev/ci-cd/github-actions' + ); + return; + } + // Get the test status and results const result = await getTestStatus(uploadId, apiKey, dcdVersionString, apiUrl); diff --git a/src/methods/params.test.ts b/src/methods/params.test.ts index a3dde15..91dd592 100644 --- a/src/methods/params.test.ts +++ b/src/methods/params.test.ts @@ -131,6 +131,31 @@ describe('getParameters', () => { expect(withoutCtx.githubContext).toBeUndefined(); }); + it('uses the PR head sha (not the merge sha) on pull_request events', async () => { + // On pull_request events github.context.sha is a throwaway merge commit; the + // metadata (and the GitHub check the backend posts) must use the head sha. + const gh = await import('@actions/github'); + const originalPayload = gh.context.payload; + (gh.context as { payload: unknown }).payload = { + pull_request: { + number: 7, + head: { ref: 'refs/heads/feature-x', sha: 'deadbeefhead' }, + html_url: 'https://github.com/acme/widgets/pull/7', + }, + }; + try { + const params = await getParameters(); + expect(params.githubContext).toContain('gh_sha=deadbeefhead'); + expect(params.githubContext).not.toContain('gh_sha=cafebabe'); + expect(params.githubContext).toContain('gh_pr_number=7'); + expect(params.githubContext).toContain( + 'gh_pr_url=https://github.com/acme/widgets/pull/7' + ); + } finally { + (gh.context as { payload: unknown }).payload = originalPayload; + } + }); + it('honours an explicit api-url and name over the defaults', async () => { inputs['api-url'] = 'https://api.dev.devicecloud.dev'; inputs['name'] = 'My Custom Run'; diff --git a/src/methods/params.ts b/src/methods/params.ts index 2bef27f..2100fb4 100644 --- a/src/methods/params.ts +++ b/src/methods/params.ts @@ -98,8 +98,12 @@ function getGithubContextMetadata(): string[] { const rawRef = pr?.head?.ref ?? ctx.ref; const branch = rawRef?.replace(/^refs\/heads\//, '') ?? ''; + // On pull_request events ctx.sha is a throwaway *merge* commit; a GitHub + // check (and the developer-visible commit) must use the PR's head sha. + const headSha = pr?.head?.sha ?? ctx.sha; + const pairs: string[] = [ - `gh_sha=${ctx.sha}`, + `gh_sha=${headSha}`, `gh_run_id=${ctx.runId}`, `gh_repo=${ctx.repo.owner}/${ctx.repo.repo}`, ];