Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

GitHub Actions to automate our GraphQL schema management #2108

Merged
merged 27 commits into from
May 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
338da57
"schema-up" action to automatically update the GraphQL schema
smashwilson Apr 30, 2019
e86e5fc
"schema-automerge" action to automatically merge PRs created by "sche…
smashwilson Apr 30, 2019
dd2d315
Babby's first workflow file
smashwilson Apr 30, 2019
696b3b2
Ah does it not support MON
smashwilson Apr 30, 2019
bbfc7e0
Okay on second thought let's not do the automerge thing
smashwilson Apr 30, 2019
80e7569
Since we're not actually making automerge happen.
smashwilson May 1, 2019
eff8f56
Report `relay-compiler` failures to the opened PR
smashwilson May 1, 2019
7cc72bd
Let's run every ten minutes to work the kinks out.
smashwilson May 1, 2019
3d162bb
Apply suggestions from code review
kuychaco May 2, 2019
324d362
Create schemaUpdateLabel constant
smashwilson May 2, 2019
193432c
Supply missing parameters to createPullRequest mutation
smashwilson May 2, 2019
bddd256
Report success :tada:
smashwilson May 2, 2019
e1d35c3
Maybe specify the COPY paths explicitly?
smashwilson May 2, 2019
3c98bbf
Actually install git
smashwilson May 2, 2019
1eee99d
Configure git username and email (hi @hubot)
smashwilson May 2, 2019
b6740df
`tools.github.graphql` has a different return value than I thought
smashwilson May 2, 2019
ad0b2c8
Looks like it just returns the query shape as an object?
smashwilson May 2, 2019
ba2c5ff
Labelable doesn't have an ID. Just select the null mutation ID
smashwilson May 2, 2019
caa1af9
No quotes around the commit message
smashwilson May 2, 2019
142dcb0
Pass --watchman false to relay-compiler
smashwilson May 2, 2019
6029347
Let's make the already-existing-PR case a neutral result
smashwilson May 2, 2019
22cf03d
git diff doesn't understand the ** splat
smashwilson May 2, 2019
1ba63d7
It's "code" not "exitCode"
smashwilson May 2, 2019
6956b00
Specify the upstream to push to
smashwilson May 2, 2019
bd98a6a
Remove more commit message quotes
smashwilson May 2, 2019
5f0650b
relay-compiler dumps errors to stdout
smashwilson May 2, 2019
f6a9e52
Run it weekly :calendar:
smashwilson May 2, 2019
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
10 changes: 10 additions & 0 deletions .github/main.workflow
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
workflow "GraphQL schema update" {
// Every Monday at 1am.
on = "schedule(0 1 * * 1)"
resolves = "Update schema"
}

action "Update schema" {
uses = "./actions/schema-up"
secrets = ["GITHUB_TOKEN"]
}
20 changes: 20 additions & 0 deletions actions/schema-up/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM node:8-slim

LABEL "com.github.actions.name"="schema-up"
LABEL "com.github.actions.description"="Update GraphQL schema and adjust Relay files"
LABEL "com.github.actions.icon"="arrow-up-right"
LABEL "com.github.actions.color"="blue"

RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*

# Copy the package.json and package-lock.json
COPY package*.json /

# Install dependencies
RUN npm ci

# Copy the rest of your action's code
COPY * /

# Run `node /index.js`
ENTRYPOINT ["node", "/index.js"]
3 changes: 3 additions & 0 deletions actions/schema-up/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# actions/schema-up

Fetch the latest GraphQL schema changes from github.com. Commit and push the schema change directly to the `master` branch if no further changes are made. Otherwise, open a pull request with the ["schema update" label](https://github.com/atom/github/labels/schema%20update) applied, as long as no such pull request already exists.
122 changes: 122 additions & 0 deletions actions/schema-up/fetch-schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');

const {buildClientSchema, printSchema} = require('graphql/utilities');
const SERVER = 'https://api.github.com/graphql';
const introspectionQuery = `
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: false) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: false) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
`;

module.exports = async function() {
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('You must specify a GitHub auth token in GITHUB_TOKEN');
}

const schemaPath = path.resolve(process.env.GITHUB_WORKSPACE, 'graphql', 'schema.graphql');

const res = await fetch(SERVER, {
method: 'POST',
headers: {
'Accept': 'application/vnd.github.antiope-preview+json',
'Content-Type': 'application/json',
'Authorization': 'bearer ' + token,
},
body: JSON.stringify({query: introspectionQuery}),
});
const schemaJSON = await res.json();
const graphQLSchema = buildClientSchema(schemaJSON.data);
await new Promise((resolve, reject) => {
fs.writeFile(schemaPath, printSchema(graphQLSchema), {encoding: 'utf8'}, err => {
if (err) { reject(err); } else { resolve(); }
});
});
};
132 changes: 132 additions & 0 deletions actions/schema-up/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const path = require('path');

const {Toolkit} = require('actions-toolkit');
const fetchSchema = require('./fetch-schema');

const schemaUpdateLabel = {
name: 'schema update',
id: 'MDU6TGFiZWwxMzQyMzM1MjQ2',
};

Toolkit.run(async tools => {
await tools.runInWorkspace('git', ['config', '--global', 'user.email', 'hubot@github.com']);
await tools.runInWorkspace('git', ['config', '--global', 'user.name', 'hubot']);

tools.log.info('Fetching the latest GraphQL schema changes.');
await fetchSchema();

const {code: hasSchemaChanges} = await tools.runInWorkspace(
'git', ['diff', '--quiet', '--', 'graphql/schema.graphql'],
{reject: false},
);
if (hasSchemaChanges === 0) {
tools.log.info('No schema changes to fetch.');
tools.exit.neutral('Nothing to do.');
}

tools.log.info('Committing schema changes.');
await tools.runInWorkspace('git', ['commit', '--all', '--message', ':arrow_up: GraphQL schema']);

tools.log.info('Re-running relay compiler.');
const {failed: relayFailed, stdout: relayOutput} = await tools.runInWorkspace(
path.resolve(__dirname, 'node_modules', '.bin', 'relay-compiler'),
['--watchman', 'false', '--src', './lib', '--schema', 'graphql/schema.graphql'],
{reject: false},
);
tools.log.info('Relay output:\n%s', relayOutput);

const {code: hasRelayChanges} = await tools.runInWorkspace(
'git', ['diff', '--quiet'],
{reject: false},
);

if (hasRelayChanges === 0 && !relayFailed) {
tools.log.info('Generated relay files are unchanged.');
const upstream = tools.context.ref.replace(/^refs\/heads\//, '');
await tools.runInWorkspace('git', ['push', 'origin', upstream]);
tools.exit.success('Schema is up to date on master.');
}

tools.log.info('Checking for unmerged schema update pull requests.');
const openPullRequestsQuery = await tools.github.graphql(`
query openPullRequestsQuery($owner: String!, $repo: String!, $labelName: String!) {
repository(owner: $owner, name: $repo) {
id
pullRequests(first: 1, states: [OPEN], labels: [$labelName]) {
totalCount
}
}
}
`, {...tools.context.repo, labelName: schemaUpdateLabel.name});

const repositoryId = openPullRequestsQuery.repository.id;

if (openPullRequestsQuery.repository.pullRequests.totalCount > 0) {
tools.exit.neutral('One or more schema update pull requests are already open. Please resolve those first.');
}

const branchName = `schema-update/${Date.now()}`;
tools.log.info(`Commiting relay-compiler changes to a new branch ${branchName}.`);
await tools.runInWorkspace('git', ['checkout', '-b', branchName]);
if (!relayFailed) {
await tools.runInWorkspace('git', ['commit', '--all', '--message', ':gear: relay-compiler changes']);
}
await tools.runInWorkspace('git', ['push', 'origin', branchName]);

tools.log.info('Creating a pull request.');

let body = `:robot: _This automated pull request brought to you by [a GitHub action](/actions/schema-up)_ :robot:

The GraphQL schema has been automatically updated and \`relay-compiler\` has been re-run on the package source.`;

if (!relayFailed) {
body += ' The modified files have been committed to this branch and pushed. ';
body += 'If all of the tests pass in CI, merge with confidence :zap:';
} else {
body += ' `relay-compiler` failed with the following output:\n\n```\n';
body += relayOutput;
body += '\n```\n\nCheck out this branch to fix things so we don\'t break.';
}

const createPullRequestMutation = await tools.github.graphql(`
mutation createPullRequestMutation($repositoryId: ID!, $headRefName: String!, $body: String!) {
createPullRequest(input: {
repositoryId: $repositoryId
title: "GraphQL schema update"
body: $body
baseRefName: "master"
headRefName: $headRefName
}) {
pullRequest {
id
number
}
}
}
`, {
repositoryId,
headRefName: branchName,
body,
});

const createdPullRequest = createPullRequestMutation.createPullRequest.pullRequest;
tools.log.info(
`Pull request #${createdPullRequest.number} has been opened with the changes from this schema upgrade.`,
);

await tools.github.graphql(`
mutation labelPullRequestMutation($id: ID!, $labelIDs: [ID!]!) {
addLabelsToLabelable(input: {
labelableId: $id,
labelIds: $labelIDs
}) {
clientMutationId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it's a bit annoying that you're forced to ask for return data, even if there's no use for it...

}
}
`, {id: createdPullRequest.id, labelIDs: [schemaUpdateLabel.id]});
tools.exit.success(
`Pull request #${createdPullRequest.number} has been opened and labelled for this schema upgrade.`,
);
}, {
smashwilson marked this conversation as resolved.
Show resolved Hide resolved
secrets: ['GITHUB_TOKEN'],
});
Loading