Skip to content

Commit 63453cf

Browse files
authored
feat: async mode + PR head sha for backend-posted checks (#38)
* feat: async mode + PR head sha for backend-posted checks - Respect `async`: when set, submit the run and exit 0 without polling (saves CI minutes). The DeviceCloud GitHub App reports the result as a check on the commit/PR when the run completes. - Fix gh_sha to use the PR head sha (pr.head.sha) instead of the throwaway merge commit so the check lands on the developer-visible commit. - Rebuild dist, document async + the GitHub App in the README, add a params test for the PR head-sha behaviour. * Bump version from 2.2.0 to 2.3.0
1 parent 009e399 commit 63453cf

6 files changed

Lines changed: 104 additions & 12 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,28 @@ Full documentation including all inputs, outputs, and usage examples is availabl
1717
1818
**[docs.devicecloud.dev/ci-cd/github-actions](https://docs.devicecloud.dev/ci-cd/github-actions)**
1919
20+
## Async runs + PR checks (save CI minutes)
21+
22+
By default the action waits for your run to finish, so the job (and your GitHub
23+
Actions minutes) stays billed for the whole suite. Set `async: true` to submit
24+
the run and exit immediately:
25+
26+
```yaml
27+
- uses: devicecloud-dev/device-cloud-for-maestro@v2
28+
with:
29+
api-key: ${{ secrets.DCD_API_KEY }}
30+
app-file: <path_to_your_app_file>
31+
async: true
32+
```
33+
34+
To still gate your PR on the result, install the **DeviceCloud GitHub App** and
35+
connect it to your org in DeviceCloud → Settings → Integrations. DeviceCloud then
36+
posts a `DeviceCloud / Mobile E2E` check on the commit/PR — `in progress` while
37+
the suite runs, then pass/fail when it completes — with a "Re-run failed tests"
38+
button. Make it a required status check in branch protection to block merges on
39+
failures. No `permissions: checks: write` is needed in your workflow; the App
40+
posts the check.
41+
2042
## Migrating from Maestro Cloud
2143

2244
Replace the `uses` line in your workflow:

dist/index.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2216,7 +2216,7 @@ const Context = __importStar(__nccwpck_require__(8663));
22162216
const Utils = __importStar(__nccwpck_require__(1365));
22172217
// octokit + plugins
22182218
const core_1 = __nccwpck_require__(6895);
2219-
const plugin_rest_endpoint_methods_1 = __nccwpck_require__(9289);
2219+
const plugin_rest_endpoint_methods_1 = __nccwpck_require__(6495);
22202220
const plugin_paginate_rest_1 = __nccwpck_require__(6212);
22212221
exports.context = new Context.Context();
22222222
const baseUrl = Utils.getApiBaseUrl();
@@ -41984,7 +41984,7 @@ const getLatestDcdVersion = (...args_1) => __awaiter(void 0, [...args_1], void 0
4198441984
}
4198541985
});
4198641986
const run = () => __awaiter(void 0, void 0, void 0, function* () {
41987-
var _a;
41987+
var _a, _b;
4198841988
try {
4198941989
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)();
4199041990
const REMOVED_MAESTRO_VERSIONS = ['1.39.2', '1.39.7', '2.0.3'];
@@ -42057,7 +42057,7 @@ const run = () => __awaiter(void 0, void 0, void 0, function* () {
4205742057
try {
4205842058
process.env.DCD_CI_WRAPPER_VERSION = (__nccwpck_require__(8330)/* .version */ .rE);
4205942059
}
42060-
catch (_b) {
42060+
catch (_c) {
4206142061
// best-effort — version is optional
4206242062
}
4206342063
// Execute the test command and capture the upload ID
@@ -42077,6 +42077,21 @@ const run = () => __awaiter(void 0, void 0, void 0, function* () {
4207742077
if (!uploadId) {
4207842078
throw new Error('Failed to get upload ID from console URL');
4207942079
}
42080+
// Async mode: the CLI has already submitted the run and exited (saving CI
42081+
// minutes). Do NOT poll for results — the DeviceCloud GitHub App reports the
42082+
// outcome as a "DeviceCloud" check on the commit/PR when the run completes.
42083+
// Just surface the upload id/console URL and exit successfully.
42084+
if (async) {
42085+
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]) || '';
42086+
(0, core_1.setOutput)('DEVICE_CLOUD_CONSOLE_URL', consoleUrl);
42087+
(0, core_1.setOutput)('DEVICE_CLOUD_UPLOAD_STATUS', 'PENDING');
42088+
(0, core_1.setOutput)('DEVICE_CLOUD_FLOW_RESULTS', '[]');
42089+
console.info('Async run submitted; not waiting for results.' +
42090+
(consoleUrl ? ` Track progress: ${consoleUrl}` : ''));
42091+
console.info('Install the DeviceCloud GitHub App to get a pass/fail check on this ' +
42092+
'commit/PR when the run completes: https://docs.devicecloud.dev/ci-cd/github-actions');
42093+
return;
42094+
}
4208042095
// Get the test status and results
4208142096
const result = yield getTestStatus(uploadId, apiKey, dcdVersionString, apiUrl);
4208242097
if (result) {
@@ -42216,13 +42231,16 @@ function getInferredName() {
4221642231
return github.context.sha;
4221742232
}
4221842233
function getGithubContextMetadata() {
42219-
var _a, _b, _c;
42234+
var _a, _b, _c, _d, _e;
4222042235
const ctx = github.context;
4222142236
const pr = ctx.payload.pull_request;
4222242237
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;
4222342238
const branch = (_c = rawRef === null || rawRef === void 0 ? void 0 : rawRef.replace(/^refs\/heads\//, '')) !== null && _c !== void 0 ? _c : '';
42239+
// On pull_request events ctx.sha is a throwaway *merge* commit; a GitHub
42240+
// check (and the developer-visible commit) must use the PR's head sha.
42241+
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;
4222442242
const pairs = [
42225-
`gh_sha=${ctx.sha}`,
42243+
`gh_sha=${headSha}`,
4222642244
`gh_run_id=${ctx.runId}`,
4222742245
`gh_repo=${ctx.repo.owner}/${ctx.repo.repo}`,
4222842246
];
@@ -44502,7 +44520,7 @@ paginateRest.VERSION = VERSION;
4450244520

4450344521
/***/ }),
4450444522

44505-
/***/ 9289:
44523+
/***/ 6495:
4450644524
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __nccwpck_require__) => {
4450744525

4450844526
"use strict";
@@ -44515,12 +44533,12 @@ __nccwpck_require__.d(__webpack_exports__, {
4451544533
restEndpointMethods: () => (/* binding */ restEndpointMethods)
4451644534
});
4451744535

44518-
;// 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
44536+
;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/version.js
4451944537
const VERSION = "17.0.0";
4452044538

4452144539
//# sourceMappingURL=version.js.map
4452244540

44523-
;// 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
44541+
;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/generated/endpoints.js
4452444542
const Endpoints = {
4452544543
actions: {
4452644544
addCustomLabelsToSelfHostedRunnerForOrg: [
@@ -46814,7 +46832,7 @@ var endpoints_default = Endpoints;
4681446832

4681546833
//# sourceMappingURL=endpoints.js.map
4681646834

46817-
;// 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
46835+
;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/endpoints-to-methods.js
4681846836

4681946837
const endpointMethodsMap = /* @__PURE__ */ new Map();
4682046838
for (const [scope, endpoints] of Object.entries(endpoints_default)) {
@@ -46940,7 +46958,7 @@ function decorate(octokit, scope, methodName, defaults, decorations) {
4694046958

4694146959
//# sourceMappingURL=endpoints-to-methods.js.map
4694246960

46943-
;// 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
46961+
;// CONCATENATED MODULE: ./node_modules/.pnpm/@octokit+plugin-rest-endpoi_88f1cfdccbcd12f9bd89a662a3d08bce/node_modules/@octokit/plugin-rest-endpoint-methods/dist-src/index.js
4694446962

4694546963

4694646964
function restEndpointMethods(octokit) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "dcd-github-action",
33
"description": "run maestro tests on devicecloud.dev",
44
"author": "devicecloud.dev",
5-
"version": "2.2.0",
5+
"version": "2.3.0",
66
"main": "src/index.ts",
77
"license": "MIT",
88
"engines": {

src/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,29 @@ const run = async (): Promise<void> => {
266266
throw new Error('Failed to get upload ID from console URL');
267267
}
268268

269+
// Async mode: the CLI has already submitted the run and exited (saving CI
270+
// minutes). Do NOT poll for results — the DeviceCloud GitHub App reports the
271+
// outcome as a "DeviceCloud" check on the commit/PR when the run completes.
272+
// Just surface the upload id/console URL and exit successfully.
273+
if (async) {
274+
const consoleUrl =
275+
testOutput?.match(
276+
/https:\/\/(?:dev\.)?console\.devicecloud\.dev\/results\?upload=[a-zA-Z0-9-]+/
277+
)?.[0] || '';
278+
setOutput('DEVICE_CLOUD_CONSOLE_URL', consoleUrl);
279+
setOutput('DEVICE_CLOUD_UPLOAD_STATUS', 'PENDING');
280+
setOutput('DEVICE_CLOUD_FLOW_RESULTS', '[]');
281+
console.info(
282+
'Async run submitted; not waiting for results.' +
283+
(consoleUrl ? ` Track progress: ${consoleUrl}` : '')
284+
);
285+
console.info(
286+
'Install the DeviceCloud GitHub App to get a pass/fail check on this ' +
287+
'commit/PR when the run completes: https://docs.devicecloud.dev/ci-cd/github-actions'
288+
);
289+
return;
290+
}
291+
269292
// Get the test status and results
270293
const result = await getTestStatus(uploadId, apiKey, dcdVersionString, apiUrl);
271294

src/methods/params.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,31 @@ describe('getParameters', () => {
131131
expect(withoutCtx.githubContext).toBeUndefined();
132132
});
133133

134+
it('uses the PR head sha (not the merge sha) on pull_request events', async () => {
135+
// On pull_request events github.context.sha is a throwaway merge commit; the
136+
// metadata (and the GitHub check the backend posts) must use the head sha.
137+
const gh = await import('@actions/github');
138+
const originalPayload = gh.context.payload;
139+
(gh.context as { payload: unknown }).payload = {
140+
pull_request: {
141+
number: 7,
142+
head: { ref: 'refs/heads/feature-x', sha: 'deadbeefhead' },
143+
html_url: 'https://github.com/acme/widgets/pull/7',
144+
},
145+
};
146+
try {
147+
const params = await getParameters();
148+
expect(params.githubContext).toContain('gh_sha=deadbeefhead');
149+
expect(params.githubContext).not.toContain('gh_sha=cafebabe');
150+
expect(params.githubContext).toContain('gh_pr_number=7');
151+
expect(params.githubContext).toContain(
152+
'gh_pr_url=https://github.com/acme/widgets/pull/7'
153+
);
154+
} finally {
155+
(gh.context as { payload: unknown }).payload = originalPayload;
156+
}
157+
});
158+
134159
it('honours an explicit api-url and name over the defaults', async () => {
135160
inputs['api-url'] = 'https://api.dev.devicecloud.dev';
136161
inputs['name'] = 'My Custom Run';

src/methods/params.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,12 @@ function getGithubContextMetadata(): string[] {
9898
const rawRef = pr?.head?.ref ?? ctx.ref;
9999
const branch = rawRef?.replace(/^refs\/heads\//, '') ?? '';
100100

101+
// On pull_request events ctx.sha is a throwaway *merge* commit; a GitHub
102+
// check (and the developer-visible commit) must use the PR's head sha.
103+
const headSha = pr?.head?.sha ?? ctx.sha;
104+
101105
const pairs: string[] = [
102-
`gh_sha=${ctx.sha}`,
106+
`gh_sha=${headSha}`,
103107
`gh_run_id=${ctx.runId}`,
104108
`gh_repo=${ctx.repo.owner}/${ctx.repo.repo}`,
105109
];

0 commit comments

Comments
 (0)