1
0
mirror of synced 2025-12-19 18:10:59 -05:00

Convert sync.js to TypeScript (#58009)

This commit is contained in:
Kevin Heis
2025-10-21 07:49:26 -07:00
committed by GitHub
parent 3dcbf8a6cc
commit cd5ca152ad

View File

@@ -15,30 +15,124 @@ const ENABLED_APPS_DIR = 'src/github-apps/data'
const CONFIG_FILE = 'src/github-apps/lib/config.json'
// Actor type mapping from generic names to actual YAML values
export const actorTypeMap = {
export const actorTypeMap: Record<string, string> = {
fine_grained_pat: 'fine_grained_personal_access_token',
server_to_server: 'github_app',
user_to_server: 'user_access_token',
}
// Also need to handle the actual values that come from the source data
// UserProgrammaticAccess maps to fine_grained_pat functionality
const sourceDataActorMap = {
UserProgrammaticAccess: 'fine_grained_pat',
interface AppDataOperation {
slug: string
subcategory: string
verb: string
requestPath: string
}
export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAccessSource) {
interface AppDataOperationWithCategory extends AppDataOperation {
category: string
}
interface PermissionData {
title: string
displayTitle: string
permissions: Array<
AppDataOperationWithCategory & {
access: string
'user-to-server'?: boolean
'server-to-server'?: boolean
'additional-permissions'?: boolean
}
>
}
interface GitHubAppsData {
[pageType: string]: {
[category: string]: AppDataOperation[] | PermissionData
}
}
interface ProgAccessOperationData {
userToServerRest: boolean
serverToServer: boolean
fineGrainedPat: boolean
permissions: Array<Record<string, string>>
allowPermissionlessAccess?: boolean
allowsPublicRead?: boolean
basicAuth?: boolean
disabledForPatV2?: boolean
}
interface ProgAccessData {
[operationId: string]: ProgAccessOperationData
}
interface ProgActorResource {
title?: string
resource_group?: string
visibility?: string
excluded_actors?: string[]
}
interface ProgActorResources {
[key: string]: ProgActorResource
}
interface OpenApiOperation {
operationId: string
summary: string
'x-github': {
category: string
subcategory: string
}
}
interface OpenApiData {
paths: {
[path: string]: {
[verb: string]: OpenApiOperation
}
}
}
interface AppsDataConfig {
pages: {
[pageType: string]: unknown
}
}
interface ProgAccessRawOperation {
operation_ids: string
user_to_server: {
enabled: boolean
}
server_to_server: {
enabled: boolean
}
disabled_for_patv2?: boolean
permission_sets?: Array<Record<string, string>>
allows_permissionless_access?: boolean
allows_public_read?: boolean
basic_auth?: boolean
}
export async function syncGitHubAppsData(
openApiSource: string,
sourceSchemas: string[],
progAccessSource: string,
): Promise<void> {
console.log(
`Generating GitHub Apps data from ${openApiSource}, ${sourceSchemas} and ${progAccessSource}`,
)
const { progAccessData, progActorResources } = await getProgAccessData(progAccessSource)
for (const schemaName of sourceSchemas) {
const data = JSON.parse(await readFile(path.join(openApiSource, schemaName), 'utf8'))
const appsDataConfig = JSON.parse(await readFile(CONFIG_FILE, 'utf8'))
const data = JSON.parse(
await readFile(path.join(openApiSource, schemaName), 'utf8'),
) as OpenApiData
const appsDataConfig = JSON.parse(await readFile(CONFIG_FILE, 'utf8')) as AppsDataConfig
// Initialize the data structure with keys for each page type
const githubAppsData = {}
const githubAppsData: GitHubAppsData = {}
for (const pageType of Object.keys(appsDataConfig.pages)) {
githubAppsData[pageType] = {}
}
@@ -54,13 +148,16 @@ export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAcces
const isFineGrainedPat =
isUserAccessToken && !progAccessData[operation.operationId].disabledForPatV2
const { category, subcategory } = operation['x-github']
const appDataOperation = {
const appDataOperation: AppDataOperation = {
slug: slug(operation.summary),
subcategory,
verb,
requestPath,
}
const appDataOperationWithCategory = Object.assign({ category }, appDataOperation)
const appDataOperationWithCategory: AppDataOperationWithCategory = Object.assign(
{ category },
appDataOperation,
)
// server-to-server
if (isInstallationAccessToken) {
addAppData(githubAppsData['server-to-server-rest'], category, appDataOperation)
@@ -85,11 +182,6 @@ export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAcces
),
)
// Debug logging for checks-related operations
const hasChecksPermission = progAccessData[operation.operationId].permissions.some(
(permissionSet) => permissionSet.checks,
)
if (!allPermissionSetsExcluded) {
addAppData(githubAppsData['fine-grained-pat'], category, appDataOperation)
}
@@ -99,9 +191,9 @@ export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAcces
for (const permissionSet of progAccessData[operation.operationId].permissions) {
for (const [permissionName, readOrWrite] of Object.entries(permissionSet)) {
const { title, displayTitle } = getDisplayTitle(permissionName, progActorResources)
if (progActorResources[permissionName]['visibility'] === 'private') continue
if (progActorResources[permissionName]?.['visibility'] === 'private') continue
const excludedActors = progActorResources[permissionName]['excluded_actors']
const excludedActors = progActorResources[permissionName]?.['excluded_actors']
const additionalPermissions = calculateAdditionalPermissions(
progAccessData[operation.operationId].permissions,
@@ -143,7 +235,9 @@ export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAcces
),
'additional-permissions': additionalPermissions,
}
serverToServerPermissions[permissionName].permissions.push(
const permissionsArray = (serverToServerPermissions[permissionName] as PermissionData)
.permissions
permissionsArray.push(
Object.assign(
{},
appDataOperationWithCategory,
@@ -174,7 +268,9 @@ export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAcces
}
}
findGrainedPatPermissions[permissionName].permissions.push(
const permissionsArray = (findGrainedPatPermissions[permissionName] as PermissionData)
.permissions
permissionsArray.push(
Object.assign({}, appDataOperationWithCategory, {
'additional-permissions': additionalPermissions,
access: readOrWrite,
@@ -214,7 +310,10 @@ export async function syncGitHubAppsData(openApiSource, sourceSchemas, progAcces
}
}
export async function getProgAccessData(progAccessSource, isRest = false) {
export async function getProgAccessData(
progAccessSource: string,
isRest = false,
): Promise<{ progAccessData: ProgAccessData; progActorResources: ProgActorResources }> {
const useRemoteGitHubFiles = progAccessSource === 'rest-api-description'
// check for required PAT
if (useRemoteGitHubFiles && !process.env.GITHUB_TOKEN) {
@@ -223,8 +322,8 @@ export async function getProgAccessData(progAccessSource, isRest = false) {
)
}
let progAccessDataRaw
let progActorResources
let progAccessDataRaw: ProgAccessRawOperation[]
let progActorResources: ProgActorResources
const progAccessFilepath = 'config/access_control/programmatic_access.yaml'
const progActorDirectory =
'config/access_control/fine_grained_permissions/programmatic_actor_fine_grained_resources'
@@ -232,14 +331,14 @@ export async function getProgAccessData(progAccessSource, isRest = false) {
if (!useRemoteGitHubFiles) {
progAccessDataRaw = yaml.load(
await readFile(path.join(progAccessSource, progAccessFilepath), 'utf8'),
)
) as ProgAccessRawOperation[]
progActorResources = await getProgActorResourceContent({
gitHubSourceDirectory: path.join(progAccessSource, progActorDirectory),
})
} else {
progAccessDataRaw = yaml.load(
await getContents('github', 'github', 'master', progAccessFilepath),
)
) as ProgAccessRawOperation[]
progActorResources = await getProgActorResourceContent({
owner: 'github',
repo: 'github',
@@ -248,9 +347,9 @@ export async function getProgAccessData(progAccessSource, isRest = false) {
})
}
const progAccessData = {}
const progAccessData: ProgAccessData = {}
for (const operation of progAccessDataRaw) {
const operationData = {
const operationData: ProgAccessOperationData = {
userToServerRest: operation.user_to_server.enabled,
serverToServer: operation.server_to_server.enabled,
fineGrainedPat: operation.user_to_server.enabled && !operation.disabled_for_patv2,
@@ -260,6 +359,7 @@ export async function getProgAccessData(progAccessSource, isRest = false) {
allowPermissionlessAccess: operation.allows_permissionless_access,
allowsPublicRead: operation.allows_public_read,
basicAuth: operation.basic_auth,
disabledForPatV2: operation.disabled_for_patv2,
}
// Handle comma-separated operation IDs
@@ -272,9 +372,12 @@ export async function getProgAccessData(progAccessSource, isRest = false) {
return { progAccessData, progActorResources }
}
function getDisplayPermissions(permissionSets, progActorResources) {
function getDisplayPermissions(
permissionSets: Array<Record<string, string>>,
progActorResources: ProgActorResources,
): Array<Record<string, string>> {
const displayPermissions = permissionSets.map((permissionSet) => {
const displayPermissionSet = {}
const displayPermissionSet: Record<string, string> = {}
Object.entries(permissionSet).forEach(([key, value]) => {
const { displayTitle } = getDisplayTitle(key, progActorResources, true)
displayPermissionSet[displayTitle] = value
@@ -286,33 +389,45 @@ function getDisplayPermissions(permissionSets, progActorResources) {
return displayPermissions
}
function sortObjectByKeys(obj) {
function sortObjectByKeys<T>(obj: Record<string, T>): Record<string, T> {
return Object.keys(obj)
.sort()
.reduce((acc, key) => {
.reduce(
(acc, key) => {
acc[key] = obj[key]
return acc
}, {})
},
{} as Record<string, T>,
)
}
function sortObjectByTitle(obj) {
function sortObjectByTitle(obj: Record<string, PermissionData | unknown>): Record<string, unknown> {
return Object.keys(obj)
.sort((a, b) => {
if (obj[a].displayTitle > obj[b].displayTitle) {
const aData = obj[a] as PermissionData
const bData = obj[b] as PermissionData
if (aData.displayTitle > bData.displayTitle) {
return 1
}
if (obj[a].displayTitle < obj[b].displayTitle) {
if (aData.displayTitle < bData.displayTitle) {
return -1
}
return 0
})
.reduce((acc, key) => {
.reduce(
(acc, key) => {
acc[key] = obj[key]
return acc
}, {})
},
{} as Record<string, unknown>,
)
}
function getDisplayTitle(permissionName, progActorResources, isRest = false) {
function getDisplayTitle(
permissionName: string,
progActorResources: ProgActorResources,
isRest = false,
): { title: string; displayTitle: string } {
const tempTitle = permissionName.replace(/_/g, ' ')
const permissionNameExists = progActorResources[permissionName]
if (!permissionNameExists) {
@@ -328,7 +443,7 @@ function getDisplayTitle(permissionName, progActorResources, isRest = false) {
if (!title) {
console.warn(`No title found for title ${title} resource group ${resourceGroup}`)
return ''
return { title: '', displayTitle: '' }
}
const displayTitle = isRest
@@ -342,14 +457,16 @@ function getDisplayTitle(permissionName, progActorResources, isRest = false) {
return { title, displayTitle }
}
function sentenceCase(str) {
function sentenceCase(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* Calculates whether an operation has additional permissions beyond a single permission.
*/
export function calculateAdditionalPermissions(permissionSets) {
export function calculateAdditionalPermissions(
permissionSets: Array<Record<string, string>>,
): boolean {
return (
permissionSets.length > 1 ||
permissionSets.some((permissionSet) => Object.keys(permissionSet).length > 1)
@@ -360,7 +477,10 @@ export function calculateAdditionalPermissions(permissionSets) {
* Determines whether a metadata permission should be filtered out when it has additional permissions.
* Prevents misleading documentation where mutating operations appear to only need metadata access.
*/
export function shouldFilterMetadataPermission(permissionName, permissionSets) {
export function shouldFilterMetadataPermission(
permissionName: string,
permissionSets: Array<Record<string, string>>,
): boolean {
if (permissionName !== 'metadata') {
return false
}
@@ -368,7 +488,11 @@ export function shouldFilterMetadataPermission(permissionName, permissionSets) {
return calculateAdditionalPermissions(permissionSets)
}
export function isActorExcluded(excludedActors, actorType, actorTypeMap = {}) {
export function isActorExcluded(
excludedActors: string[] | undefined | null | unknown,
actorType: string,
actorTypeMap: Record<string, string> = {},
): boolean {
if (!excludedActors || !Array.isArray(excludedActors)) {
return false
}
@@ -394,14 +518,21 @@ export function isActorExcluded(excludedActors, actorType, actorTypeMap = {}) {
return false
}
function addAppData(storage, category, data) {
function addAppData(
storage: Record<string, AppDataOperation[] | PermissionData>,
category: string,
data: AppDataOperation,
): void {
if (!storage[category]) {
storage[category] = []
}
storage[category].push(data)
;(storage[category] as AppDataOperation[]).push(data)
}
async function validateAppData(data, pageType) {
async function validateAppData(
data: Record<string, AppDataOperation[] | PermissionData>,
pageType: string,
): Promise<void> {
if (pageType.includes('permissions')) {
for (const value of Object.values(data)) {
const { isValid, errors } = validateJson(permissionSchema, value)
@@ -412,7 +543,7 @@ async function validateAppData(data, pageType) {
}
} else {
for (const arrayItems of Object.values(data)) {
for (const item of arrayItems) {
for (const item of arrayItems as AppDataOperation[]) {
const { isValid, errors } = validateJson(enabledSchema, item)
if (!isValid) {
console.error(JSON.stringify(errors, null, 2))
@@ -423,6 +554,14 @@ async function validateAppData(data, pageType) {
}
}
interface ProgActorResourceContentOptions {
owner?: string
repo?: string
branch?: string
path?: string
gitHubSourceDirectory?: string | null
}
// When getting files from the GitHub repo locally (or in a Codespace)
// you can pass the full or relative path to the `github` repository
// directory on disk.
@@ -434,21 +573,21 @@ async function getProgActorResourceContent({
branch,
path,
gitHubSourceDirectory = null,
}) {
}: ProgActorResourceContentOptions): Promise<ProgActorResources> {
// Get files either locally from disk or from the GitHub remote repo
let files
let files: string[]
if (gitHubSourceDirectory) {
files = await getProgActorContentFromDisk(gitHubSourceDirectory)
} else {
files = await getDirectoryContents(owner, repo, branch, path)
files = await getDirectoryContents(owner!, repo!, branch!, path!)
}
// We need to format the file content into a single object. Each file
// contains a single key and a single value that needs to be added
// to the object.
const progActorResources = {}
const progActorResources: ProgActorResources = {}
for (const file of files) {
const fileContent = yaml.load(file)
const fileContent = yaml.load(file) as Record<string, ProgActorResource>
// Each file should only contain a single key and value.
if (Object.keys(fileContent).length !== 1) {
throw new Error(`Error: The file ${JSON.stringify(fileContent)} must only have one key.`)
@@ -460,7 +599,7 @@ async function getProgActorResourceContent({
return progActorResources
}
async function getProgActorContentFromDisk(directory) {
async function getProgActorContentFromDisk(directory: string): Promise<string[]> {
const files = walk(directory, {
includeBasePath: true,
directories: false,