Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <path_to_your_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:
Expand Down
38 changes: 28 additions & 10 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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}`,
];
Expand Down Expand Up @@ -43163,7 +43181,7 @@ paginateRest.VERSION = VERSION;

/***/ }),

/***/ 9289:
/***/ 6495:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => {

"use strict";
Expand All @@ -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: [
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,29 @@ const run = async (): Promise<void> => {
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);

Expand Down
25 changes: 25 additions & 0 deletions src/methods/params.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
6 changes: 5 additions & 1 deletion src/methods/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
];
Expand Down