Skip to content
Merged
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,34 @@ 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.

### One check per job

If a PR runs more than once — iOS and Android as separate jobs, say — give each
run a `check-name` so they don't both post a check with the same name:

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
with:
api-key: ${{ secrets.DCD_API_KEY }}
app-file: build/app.ipa
async: true
check-name: iOS # -> a check called "DeviceCloud / iOS"

- uses: devicecloud-dev/device-cloud-for-maestro@v2
with:
api-key: ${{ secrets.DCD_API_KEY }}
app-file: build/app.apk
async: true
check-name: Android # -> a check called "DeviceCloud / Android"
```

Each one can then be required separately in branch protection. Without it both
runs post the same check name, GitHub treats them as one required check, and the
gate follows whichever run finished last — a passing Android run can clear a gate
the failing iOS run should have held. Keep the value fixed for a given job:
GitHub matches required checks by name, so a name that changes per commit can
never be required.

## Migrating from Maestro Cloud

Replace the `uses` line in your workflow:
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ inputs:
description: 'Automatically attach GitHub/PR context (branch, SHA, PR number, PR URL, run ID, repo) to the test run as metadata. Set to "false" to opt out.'
required: false
default: 'true'
check-name:
description: 'Name this run''s GitHub check, e.g. "iOS" gives a check called "DeviceCloud / iOS". Use it when a PR runs more than once (iOS and Android, say) so each run gets its own check that can be required separately in branch protection. Keep it the same on every run of a given job.'
required: false

outputs:
DEVICE_CLOUD_CONSOLE_URL:
Expand Down
36 changes: 28 additions & 8 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 @@ -43947,7 +43947,16 @@ function getInferredName() {
}
return github.context.sha;
}
function getGithubContextMetadata() {
/**
* `checkName` names the GitHub check this run posts. It is appended to the
* backend's base name ("DeviceCloud / iOS"), which is what lets a PR that runs
* iOS and Android as two submissions carry two checks that branch protection
* can require separately — GitHub matches required checks by name, so two runs
* sharing one name collapse into a single gate that follows whichever finished
* last. Keep it constant for a given job; a value that varies per commit can
* never be a required check.
*/
function getGithubContextMetadata(checkName) {
var _a, _b, _c, _d, _e;
const ctx = github.context;
const pr = ctx.payload.pull_request;
Expand All @@ -43963,6 +43972,8 @@ function getGithubContextMetadata() {
];
if (branch)
pairs.push(`gh_branch=${branch}`);
if (checkName)
pairs.push(`gh_check_name=${checkName}`);
if (pr) {
pairs.push(`gh_pr_number=${pr.number}`);
if (pr.html_url)
Expand Down Expand Up @@ -44027,8 +44038,17 @@ function getParameters() {
required: false,
});
const useBeta = core.getInput('use-beta', { required: false }) === 'true';
const checkName = core.getInput('check-name', { required: false }).trim();
const includeGithubContext = core.getInput('include-github-context', { required: false }) !== 'false';
const githubContext = includeGithubContext ? getGithubContextMetadata() : undefined;
const githubContext = includeGithubContext
? getGithubContextMetadata(checkName)
: undefined;
if (checkName && !includeGithubContext) {
// Without the context there is no sha to post against, so no check at all —
// say so rather than letting the input look like it did something.
core.warning('check-name is ignored because include-github-context is false: with no ' +
'commit context attached, DeviceCloud posts no check on this run.');
}
const maestroChromeOnboarding = core.getInput('maestro-chrome-onboarding', { required: false }) === 'true';
const androidNoSnapshot = core.getInput('android-no-snapshot', { required: false }) === 'true';
const disableAnimations = core.getInput('disable-animations', { required: false }) === 'true';
Expand Down Expand Up @@ -46245,7 +46265,7 @@ paginateRest.VERSION = VERSION;

/***/ }),

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

"use strict";
Expand All @@ -46258,12 +46278,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 @@ -48557,7 +48577,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 @@ -48683,7 +48703,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
29 changes: 28 additions & 1 deletion src/methods/params.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';

// Drive getParameters() through mocked GitHub Actions SDKs. `inputs` is the
// per-test action-input map; getInput/getMultilineInput read from it.
const { inputs } = vi.hoisted(() => ({ inputs: {} as Record<string, string> }));
const { inputs, warnings } = vi.hoisted(() => ({
inputs: {} as Record<string, string>,
warnings: [] as string[],
}));

vi.mock('@actions/core', () => ({
getInput: (name: string) => inputs[name] ?? '',
getMultilineInput: (name: string) =>
inputs[name] ? inputs[name].split('\n').filter((l) => l.trim() !== '') : [],
warning: (message: string) => warnings.push(message),
}));

vi.mock('@actions/github', () => ({
Expand All @@ -25,6 +29,7 @@ import { getParameters } from './params';

beforeEach(() => {
for (const k of Object.keys(inputs)) delete inputs[k];
warnings.length = 0;
// A valid baseline: api key + exactly one app source.
inputs['api-key'] = 'k';
inputs['app-file'] = 'app.apk';
Expand Down Expand Up @@ -131,6 +136,28 @@ describe('getParameters', () => {
expect(withoutCtx.githubContext).toBeUndefined();
});

it('sends check-name so a PR can carry one check per job', async () => {
// Two runs on one commit otherwise post two identically named checks, which
// branch protection can only gate as a single entry.
const withoutName = await getParameters();
expect(
withoutName.githubContext?.some((p) => p.startsWith('gh_check_name='))
).toBe(false);

inputs['check-name'] = ' iOS smoke ';
const withName = await getParameters();
expect(withName.githubContext).toContain('gh_check_name=iOS smoke');
});

it('warns rather than silently dropping check-name when context is off', async () => {
inputs['check-name'] = 'iOS';
inputs['include-github-context'] = 'false';

await getParameters();

expect(warnings.join('\n')).toContain('check-name is ignored');
});

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.
Expand Down
25 changes: 23 additions & 2 deletions src/methods/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,16 @@ function getInferredName(): string {
return github.context.sha;
}

function getGithubContextMetadata(): string[] {
/**
* `checkName` names the GitHub check this run posts. It is appended to the
* backend's base name ("DeviceCloud / iOS"), which is what lets a PR that runs
* iOS and Android as two submissions carry two checks that branch protection
* can require separately — GitHub matches required checks by name, so two runs
* sharing one name collapse into a single gate that follows whichever finished
* last. Keep it constant for a given job; a value that varies per commit can
* never be a required check.
*/
function getGithubContextMetadata(checkName?: string): string[] {
const ctx = github.context;
const pr = ctx.payload.pull_request;

Expand All @@ -109,6 +118,7 @@ function getGithubContextMetadata(): string[] {
];

if (branch) pairs.push(`gh_branch=${branch}`);
if (checkName) pairs.push(`gh_check_name=${checkName}`);
if (pr) {
pairs.push(`gh_pr_number=${pr.number}`);
if (pr.html_url) pairs.push(`gh_pr_url=${pr.html_url}`);
Expand Down Expand Up @@ -206,9 +216,20 @@ export async function getParameters(): Promise<Params> {
});
const useBeta = core.getInput('use-beta', { required: false }) === 'true';

const checkName = core.getInput('check-name', { required: false }).trim();
const includeGithubContext =
core.getInput('include-github-context', { required: false }) !== 'false';
const githubContext = includeGithubContext ? getGithubContextMetadata() : undefined;
const githubContext = includeGithubContext
? getGithubContextMetadata(checkName)
: undefined;
if (checkName && !includeGithubContext) {
// Without the context there is no sha to post against, so no check at all —
// say so rather than letting the input look like it did something.
core.warning(
'check-name is ignored because include-github-context is false: with no ' +
'commit context attached, DeviceCloud posts no check on this run.'
);
}

const maestroChromeOnboarding = core.getInput('maestro-chrome-onboarding', { required: false }) === 'true';
const androidNoSnapshot = core.getInput('android-no-snapshot', { required: false }) === 'true';
Expand Down