Skip to main content David Edelstein's Blog

Patch a file in GitHub via graphql

Published: 2024-04-22
dave@edelsteinautomotive.com
David Edelstein

I needed to write a recurring job that looked for changes to an external datasource. The two difficulties this addresses is 1) deciding if the data has changed and then 2) getting the update file into GitHub. After the patch is made in GitHub, the automation tooling kicks in and rebuilds the website using the latest json file.

So part 1 is to fetch the data from Google sheets and build a JSON file. This JSON file is written into the local project. Every run of the script generates this JSON file. Important notes about this file:

  • If the format changes, this will retrigger a build. This is desirable.
  • Don’t write timestamps etc otherwise the following comparison will say every change is new!

Next, read this file from disk:

let targetFileContents = await fs.readFile(TARGET_FILE, "utf-8");

Now use the special git trick for coming up with the file oid hash:

var shasum = crypto.createHash("sha1");
shasum.update(`blob ${targetFileContents.length}\0`+targetFileContents)
var newFileOid = shasum.digest("hex");

Now lets query GitHub graphql to request the master commit id and also the target file location oid.

const getMasterCommitId = `
    query {
      repository(owner:"dedels", name:"table4tots") {
        ref(qualifiedName:"refs/heads/master"){
          name
          target {
            oid
          }
        }
        object(expression:"master:static/targetFile.json"){
          oid
        }
      }
    }
    `;


const commitResp = await fetch('https://api.github.com/graphql', {
    method: 'POST',
    body: JSON.stringify({query: getMasterCommitId}),
    headers: {
        'Authorization': `Bearer ${GIT_ACCESS_TOKEN }`,
    },
});
const {data:{repository:{
    ref:{target:{oid:targetParentOid}},
    object:{oid:targetFileOid}
}}} = await commitResp.json();
console.log(`Try to write to commit: ${targetParentOid} and file ${targetFileOid}`);

Key here are the targetParentOid and the targetFileOid. Let us compare the targetFileOid (remote) to the newFileOid (new) to detect if we have a change!

if(targetFileOid==newFileOid){
    console.log("No changes to target file, no commit necessary");
    return;
}
console.log("making a commit...")


const newChangesQuery = `
    mutation m1 {
        createCommitOnBranch(input:{
            branch:{
                repositoryNameWithOwner: "dedels/mytargetrepo",
                branchName:"master"
            },
            message:{headline: "Updated targetFile.json"},
            fileChanges: {
                additions:[
                    {path: "static/targetFile.json", contents:"${Buffer.from(targetfileContents).toString("base64")}"}
                ]
            },
            expectedHeadOid: "${targetParentOid}"
        }){
            commit{
              oid
            }
        }
    }
`;

const execCommit = await fetch('https://api.github.com/graphql', {
    method: 'POST',
    body: JSON.stringify({query: newChangesQuery}),
    headers: {
        'Authorization': `Bearer ${GIT_ACCESS_TOKEN }`,
    },
});

This will also return the latest commit id that can be logged for informational purposes. Remember to fix your repositoryNameWithOwner, branchName, message, and targetFile paths.

This was an interesting project that the GitHub graphql made surprisingly easy. The real key confusing piece was the special single file hash. And the not exactly straightforward way of building paths & branch names.