Skip to content

Commit

Permalink
Get some repo info using the GraphQL API
Browse files Browse the repository at this point in the history
This has a separate rate limit, and getting this information basically doesn't impact it at all. Should save a few thousand REST API calls.
  • Loading branch information
domoscargin committed Jan 1, 2025
1 parent ed53243 commit be7225d
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 107 deletions.
3 changes: 2 additions & 1 deletion build-filtered-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export async function analyseRepo (repo) {
}
repoData.log('analyzing...')

await repoData.fetchAndValidateMetaData()
await repoData.fetchAndValidateRepoInfo()
repoData.log('repo metadata and latest commit details fetched and validated.')
await repoData.fetchAndValidateRepoTree()
repoData.log('tree fetched and validated.')

Expand Down
62 changes: 34 additions & 28 deletions helpers/octokit.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Octokit } from 'octokit'
import { throttling } from '@octokit/plugin-throttling'
import { graphql } from '@octokit/graphql'

const MyOctokit = Octokit.plugin(throttling)
const octokit = new MyOctokit({
Expand Down Expand Up @@ -31,35 +32,40 @@ const octokit = new MyOctokit({
},
})

/**
* Gets repo metadata
*
* @param {string} repoOwner - The owner of the repo
* @param {string} repoName - The name of the repo
* @returns {Promise<import('@octokit/rest').Response<import('@octokit/rest').ReposGetResponse>>}
* @throws {RequestError} - If the request fails
*/
export async function getRepoMetaData (repoOwner, repoName) {
return await octokit.rest.repos.get({
owner: repoOwner,
repo: repoName,
})
}
const graphQLAuth = graphql.defaults({
headers: {
authorization: `token ${process.env.GITHUB_AUTH_TOKEN}`,
}
})

/**
* Gets the latest commit for a repo
* @param {string} repoOwner - The owner of the repo
* @param {string} repoName - The name of the repo
* @returns {Promise<import('@octokit/rest').Response<import('@octokit/rest').ReposListCommitsResponse>>}
* @throws {RequestError} - If the request fails
*/
export async function getLatestCommit (repoOwner, repoName) {
const commits = await octokit.rest.repos.listCommits({
owner: repoOwner,
repo: repoName,
per_page: 1,
})
return commits.data[0]
export async function getRepoInfo (owner, name) {
const query = `
query($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
createdAt
pushedAt
defaultBranchRef {
target {
... on Commit {
oid
}
}
}
}
rateLimit {
cost
remaining
resetAt
}
}
`

const variables = {
owner,
name,
}

return await graphQLAuth(query, variables)
}

/**
Expand Down
45 changes: 17 additions & 28 deletions helpers/repo-data.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
getRepoInfo,
getFileContent,
getLatestCommit,
getRepoMetaData,
getRepoTree,
} from './octokit.mjs'
import * as yarnLock from '@yarnpkg/lockfile'
Expand Down Expand Up @@ -43,6 +42,8 @@ export class RepoData {
this.errorThrown = null
this.repoTree = null
this.frontendVersions = []
this.latestCommitSHA = null
this.graphQLRateLimit = null
}

/**
Expand All @@ -58,24 +59,28 @@ export class RepoData {
}

/**
* Fetches and validates repo metadata
* Fetches metadata and repo tree using GraphQL
*
* @throws {NoMetaDataError} - If metadata could not be fetched
* @throws {NoRepoTreeError} - If the tree could not be fetched
* @throws {RequestError} - If the request fails
*
*/
async fetchAndValidateMetaData () {
const repoMetaData = await getRepoMetaData(this.repoOwner, this.repoName)
if (repoMetaData) {
this.lastUpdated = repoMetaData.data.pushed_at
this.repoCreated = repoMetaData.data.created_at
}
async fetchAndValidateRepoInfo () {
const response = await getRepoInfo(this.repoOwner, this.repoName)

this.repoCreated = response.repository?.createdAt
this.lastUpdated = response.repository?.pushedAt
this.latestCommitSHA = response.repository?.defaultBranchRef?.target?.oid
this.graphQLRateLimit = response.rateLimit

// Some repos won't have a pushed_at
if (!this.repoCreated) {
throw new NoMetaDataError()
}
this.log('metadata fetched and validated.')

if (!this.latestCommitSHA) {
throw new NoCommitsError()
}
}

/**
Expand All @@ -85,32 +90,16 @@ export class RepoData {
* @throws {RequestError} - If the request fails
*/
async fetchAndValidateRepoTree () {
const latestCommitSha = await this.getLatestCommitSha()
this.repoTree = await getRepoTree(
this.repoOwner,
this.repoName,
latestCommitSha
this.latestCommitSHA
)
if (!this.repoTree || !this.repoTree.data || !this.repoTree.data.tree) {
throw new NoRepoTreeError()
}
}

/**
* Gets the SHA of the latest commit
*
* @returns {string} - The SHA of the latest commit
* @throws {NoCommitsError} - If the repo has no commits
* @throws {RequestError} - If the request fails
*/
async getLatestCommitSha () {
const latestCommit = await getLatestCommit(this.repoOwner, this.repoName)
if (latestCommit === undefined) {
throw new NoCommitsError()
}
return latestCommit.sha
}

/**
* Checks if repo is a prototype
*
Expand Down
76 changes: 38 additions & 38 deletions helpers/repo-data.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import { describe, it, expect, vi } from 'vitest'
import { RepoData, NoMetaDataError, NoRepoTreeError, NoCommitsError, UnsupportedLockFileError } from './repo-data.mjs'
import {
getFileContent,
getLatestCommit,
getRepoMetaData,
getRepoTree,
getRepoInfo
} from './octokit.mjs'

// Mock the octokit functions
vi.mock('./octokit.mjs', () => ({
getFileContent: vi.fn(),
getLatestCommit: vi.fn(),
getRepoMetaData: vi.fn(),
getRepoInfo: vi.fn(),
getRepoTree: vi.fn(),
}))

Expand Down Expand Up @@ -57,40 +55,62 @@ describe('RepoData', () => {
)
})

describe('fetchAndValidateMetaData', () => {
describe('fetchAndValidateRepoInfo', () => {
it('should throw a NoMetaDataError if metadata is missing', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
getRepoMetaData.mockResolvedValue({
getRepoInfo.mockResolvedValue({
data: {
pushed_at: null,
created_at: null,
},
repository: {
createdAt: '2022-01-01T00:00:00Z',
pushedAt: '2023-01-01T00:00:00Z'
}
}
})

await expect(repoData.fetchAndValidateMetaData()).rejects.toThrow(
await expect(repoData.fetchAndValidateRepoInfo()).rejects.toThrow(
NoMetaDataError
)
})

it('should fetch and validate metadata', async () => {
it('should throw a NoCommitsError if no latest commit', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
getRepoMetaData.mockResolvedValue({
data: {
pushed_at: '2023-01-01T00:00:00Z',
created_at: '2022-01-01T00:00:00Z',
},
getRepoInfo.mockResolvedValue({
repository: {
createdAt: '2022-01-01T00:00:00Z',
pushedAt: '2023-01-01T00:00:00Z'
}
})

await expect(repoData.fetchAndValidateRepoInfo()).rejects.toThrow(
NoCommitsError
)
})

it('should fetch and validate repo info', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
getRepoInfo.mockResolvedValue({
repository: {
createdAt: '2022-01-01T00:00:00Z',
pushedAt: '2023-01-01T00:00:00Z',
defaultBranchRef: {
target: {
oid: 'test-sha'
}
}
}
})

await repoData.fetchAndValidateMetaData()
await repoData.fetchAndValidateRepoInfo()
expect(repoData.lastUpdated).toBe('2023-01-01T00:00:00Z')
expect(repoData.repoCreated).toBe('2022-01-01T00:00:00Z')
expect(repoData.latestCommitSHA).toBe('test-sha')
})
})

describe('fetchAndValidateRepoTree', () => {
it('should throw a NoRepoTreeError if repo tree is missing', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
vi.spyOn(repoData, 'getLatestCommitSha').mockResolvedValue('test-sha')
repoData.latestCommitSHA = 'test-sha'
getRepoTree.mockResolvedValue({
data: {
tree: null,
Expand All @@ -104,33 +124,13 @@ describe('RepoData', () => {

it('should fetch and validate repo tree', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
getLatestCommit.mockResolvedValue({ sha: 'test-sha' })
getRepoTree.mockResolvedValue({ data: { tree: [] } })

await repoData.fetchAndValidateRepoTree()
expect(repoData.repoTree).toEqual({ data: { tree: [] } })
})
})

describe('getLatestCommitSha', () => {
it('should throw a NoCommitsError if the repo has no commits', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
getLatestCommit.mockResolvedValue(undefined)

await expect(repoData.getLatestCommitSha()).rejects.toThrow(
NoCommitsError
)
})

it('should get the SHA of the latest commit', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
getLatestCommit.mockResolvedValue({ sha: 'test-sha' })

const sha = await repoData.getLatestCommitSha()
expect(sha).toBe('test-sha')
})
})

describe('checkPrototype', () => {
it('should assume prototype if usage_data.js present', async () => {
const repoData = new RepoData(repoOwner, repoName, serviceOwners)
Expand Down
36 changes: 24 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@babel/eslint-parser": "^7.25.9",
"@babel/plugin-syntax-import-assertions": "^7.26.0",
"@eslint/js": "^9.17.0",
"@octokit/graphql": "^8.1.2",
"@octokit/plugin-throttling": "^9.3.0",
"@yarnpkg/lockfile": "^1.1.0",
"eslint": "^9.17.0",
Expand Down
Loading

0 comments on commit be7225d

Please sign in to comment.