Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: use GitHub status API instead of posting PR comments #199

Open
OliverJAsh opened this issue Apr 22, 2020 · 4 comments
Open

Suggestion: use GitHub status API instead of posting PR comments #199

OliverJAsh opened this issue Apr 22, 2020 · 4 comments

Comments

@OliverJAsh
Copy link
Collaborator

Problem

Sharing some recent work, as discussed on Twitter.

Proposed solution

You can implement onCompare to post a commit status instead of a PR comment. It looks something like this, where the "details" link takes you to the comparison inside of the Build Tracker app.

image

Here's my code. I've just copy and pasted the main parts, hopefully you get the idea and can fill in the gaps. The important posts are postStatus and the way I'm deriving the status description (with stats) inside onCompare.

import { Config } from '@build-tracker/api-client';
import Build from '@build-tracker/build';
import Comparator from '@build-tracker/comparator';
import ArtifactDelta from '@build-tracker/comparator/dist/ArtifactDelta';
import { formatBytes } from '@build-tracker/formatting';
import fetch from 'node-fetch';
import { pipeWith } from 'pipe-ts';
import { ContentType } from 'shared/constants/content-type';
import * as requestHeaders from 'shared/constants/request-headers';
import * as A from 'fp-ts/lib/Array';
import * as O from 'fp-ts/lib/Option';
import { appendPathnameToUrl } from 'url-transformers';
import { APPLICATION_URL, EnvConstants } from './constants';

const createBuildComparisonUrl = (a: string, b: string) =>
  appendPathnameToUrl({ url: APPLICATION_URL })({ pathnameToAppend: `/builds/${a}/${b}` });

const postStatus = (
  {
    sha,
    state,
    targetUrl,
    description,
  }: {
    sha: string;
    state: 'error' | 'failure' | 'pending' | 'success';
    targetUrl: string;
    description: string;
  },
  {
    buildTrackerGithubToken,
  }: {
    buildTrackerGithubToken: string;
  },
) =>
  // https://developer.github.com/v3/repos/statuses/#create-a-status
  fetch(`https://api.github.com/repos/unsplash/unsplash-web/statuses/${sha}`, {
    method: 'POST',
    headers: {
      [requestHeaders.AUTHORIZATION_REQUEST_HEADER]: `token ${buildTrackerGithubToken}`,
      [requestHeaders.CONTENT_TYPE_REQUEST_HEADER]: ContentType.Json,
    },
    body: JSON.stringify({
      state,
      target_url: targetUrl,
      description,
      context: 'Build Tracker',
    }),
  }).then(response =>
    response.ok === false
      ? response.json().then(json => {
          throw new Error(
            `Response was not ok. Status: ${response.status}. Body: ${JSON.stringify(json)}`,
          );
        })
      : Promise.resolve(),
  );

const SIZE_KEY = 'gzip';

// Copied from
// https://github.com/paularmstrong/build-tracker/blob/51b0a32d914c8c0cf11515eb2075923ffb42b399/src/comparator/src/index.ts#L136
const formatDelta = (allArtifactDelta: ArtifactDelta): string =>
  `${formatBytes(allArtifactDelta.sizes[SIZE_KEY])} (${(
    allArtifactDelta.percents[SIZE_KEY] * 100
  ).toFixed(1)}%)`;

const getDescriptionStats = (parentBuild: Build, build: Build) => {
  const parentBuildSum = parentBuild.getSum(parentBuild.artifactNames);
  const buildSum = build.getSum(build.artifactNames);
  const allArtifactDelta = new ArtifactDelta('All', [], buildSum, parentBuildSum, false);
  const formattedDelta = formatDelta(allArtifactDelta);
  const formattedTotalSize = formatBytes(buildSum[SIZE_KEY]);
  const descriptionStats = [`Change: ${formattedDelta}`, `Total: ${formattedTotalSize}`].join(' ');
  return descriptionStats;
};

// Based off of example at
// https://github.com/paularmstrong/build-tracker/blob/8f1df8ca248f5f96619c9b2ff14cd21c7f304522/docs/docs/guides/ci.md
export const onCompare = ({
  buildTrackerGithubToken,
  sha,
}: EnvConstants): Config['onCompare'] => data => {
  const { comparatorData } = data;
  const comparator = Comparator.deserialize(comparatorData);

  if (comparator.builds.length === 1) {
    // We need 2 builds to provide a useful comparison, so if there is only one build, we exit early.
    return Promise.reject(
      new Error(
        `Expected comparator to have exactly 2 builds, but it only has 1. This most likely means the base branch hasn't finished building yet. Retry this build when the base branch has finished building. Note you will need to push a new commit.`,
      ),
    );
  } else {
    const parentBuild = pipeWith(comparator.builds, A.head, O.getOrThrow);
    const build = pipeWith(A.lookup(1, comparator.builds), O.getOrThrow);
    const url = createBuildComparisonUrl(
      build.getMetaValue('parentRevision'),
      build.getMetaValue('revision'),
    );

    const isSuccess = comparator.errors.length === 0;

    const descriptionStats = getDescriptionStats(parentBuild, build);

    const hasWarnings =
      comparator.unexpectedHashChanges.length > 1 || comparator.warnings.length > 1;

    const description = [hasWarnings ? '⚠️ See report.' : undefined, descriptionStats]
      .filter(s => s !== undefined)
      .join(' ');

    return postStatus(
      {
        // Note: we don't use the `build` revision because—in a pull request build—this will
        // represent a merge commit "between the source branch and the upstream branch". This commit
        // doesn't actually exist in the PR.
        // https://docs.travis-ci.com/user/pull-requests/#how-pull-requests-are-built
        sha,
        state: isSuccess ? 'success' : 'failure',
        targetUrl: url,
        description,
      },
      {
        buildTrackerGithubToken,
      },
    );
  }
};
@paularmstrong
Copy link
Owner

This looks like a great evolution! You had a screenshot where it seemed like we might be able to link to a page on the PR checks that could host the markdown table. Do you know if that's still possible? I'd love to be able to keep a simplified view like that without needing to try to digest everything in the web app's UI

@paulirish
Copy link
Contributor

This looks like a great evolution! You had a screenshot where it seemed like we might be able to link to a page on the PR checks that could host the markdown table. Do you know if that's still possible? I'd love to be able to keep a simplified view like that without needing to try to digest everything in the web app's UI

Heh. Oliver's screenshot was of codechecks... and in paularmstrong/build-tracker-action#3 (comment) I just wrote up what you get using "Checks". (indeed: a nice markdown table). I also recommended not using Checks and instead having the user digest everything in the webapp UI ;)

I think it's a product call. The webapp's UI could have all the necessary data …or… the user users the combo of the Checks output and the webapp UI depending on their needs.

@OliverJAsh
Copy link
Collaborator Author

Agreed with @paulirish that a status is probably sufficient, but if you did want to have a dedicated page, you could use the checks API inside of the action.

@paulirish
Copy link
Contributor

paulirish commented Aug 17, 2021

@OliverJAsh btw i worked a bit with your code today and i think i found a small bug..

    const hasWarnings =
      comparator.unexpectedHashChanges.length > 1 || comparator.warnings.length > 1;

    const description = [hasWarnings ? '⚠️ See report.' : undefined, descriptionStats]
      .filter(s => s !== undefined)
      .join(' ');

i think those first two should be a >= instead of >. ;)

that aside, i really appreciate you sharing this! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants