CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
setup-github.ts446 linesDownload Raw Back to services
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { promises as fsp } from 'node:fs';8import * as path from 'node:path';9import { ProxyAgent } from 'undici';10 11import {12  getGitHubRepoInfoAsync,13  getGitRepoRootAsync,14  getLatestGitHubRelease,15  isGitHubRepositoryAsync,16} from '../utils/gitUtils.js';17import { createDebugLogger } from '@qwen-code/qwen-code-core';18import { writeStderrLine } from '../utils/stdioHelpers.js';19 20const debugLogger = createDebugLogger('SETUP_GITHUB');21 22export const GITHUB_WORKFLOW_PATHS = [23  'qwen-dispatch/qwen-dispatch.yml',24  'qwen-assistant/qwen-invoke.yml',25  'issue-triage/qwen-triage.yml',26  'issue-triage/qwen-scheduled-triage.yml',27  'pr-review/qwen-review.yml',28];29 30const GITIGNORE_ENTRIES = ['.qwen/', 'gha-creds-*.json'];31export const MAX_WORKFLOW_DOWNLOAD_BYTES = 5 * 1024 * 1024;32 33export type GithubSetupGitignoreStatus =34  | 'created'35  | 'updated'36  | 'unchanged'37  | 'failed'38  | 'skipped';39 40export interface GithubSetupWriteMetadata {41  sizeBytes: number;42}43 44export interface SetupGithubFileOps {45  assertCanWrite?(): void;46  ensureWorkflowDirectory(gitRepoRoot: string): Promise<void>;47  writeTextFile(48    gitRepoRoot: string,49    relativePath: string,50    content: string,51  ): Promise<GithubSetupWriteMetadata>;52  readTextFile(53    gitRepoRoot: string,54    relativePath: string,55  ): Promise<string | undefined>;56}57 58export interface GithubSetupWorkflowResult {59  sourcePath: string;60  path: string;61  status: 'written' | 'failed';62  sizeBytes?: number;63  error?: string;64}65 66export interface GithubSetupGitignoreResult {67  path: '.gitignore';68  status: GithubSetupGitignoreStatus;69  added?: string[];70  error?: string;71}72 73export interface SetupGithubResult {74  kind: 'github_setup';75  workspaceCwd: string;76  gitRepoRoot: string;77  releaseTag: string;78  readmeUrl: string;79  secretsUrl?: string;80  workflows: GithubSetupWorkflowResult[];81  gitignore: GithubSetupGitignoreResult;82  warnings: string[];83  partial?: boolean;84}85 86export interface SetupGithubOptions {87  cwd?: string;88  workspaceRoot?: string;89  proxy?: string;90  abortSignal?: AbortSignal;91  fetchImpl?: typeof fetch;92  fileOps?: SetupGithubFileOps;93}94 95export class SetupGithubError extends Error {96  readonly code: string;97  readonly status: number;98  readonly partial: boolean;99  readonly partialResult?: SetupGithubResult;100 101  constructor(102    code: string,103    message: string,104    status: number,105    partialResult?: SetupGithubResult,106  ) {107    super(message);108    this.name = 'SetupGithubError';109    this.code = code;110    this.status = status;111    this.partial = partialResult !== undefined;112    this.partialResult = partialResult;113  }114}115 116const nodeFileOps: SetupGithubFileOps = {117  assertCanWrite(): void {},118 119  async ensureWorkflowDirectory(gitRepoRoot: string): Promise<void> {120    await fsp.mkdir(path.join(gitRepoRoot, '.github', 'workflows'), {121      recursive: true,122    });123  },124 125  async writeTextFile(126    gitRepoRoot: string,127    relativePath: string,128    content: string,129  ): Promise<GithubSetupWriteMetadata> {130    const target = path.join(gitRepoRoot, relativePath);131    await fsp.writeFile(target, content, { mode: 0o644 });132    return { sizeBytes: Buffer.byteLength(content, 'utf8') };133  },134 135  async readTextFile(136    gitRepoRoot: string,137    relativePath: string,138  ): Promise<string | undefined> {139    try {140      return await fsp.readFile(path.join(gitRepoRoot, relativePath), 'utf8');141    } catch (error) {142      if ((error as NodeJS.ErrnoException).code === 'ENOENT') {143        return undefined;144      }145      throw error;146    }147  },148};149 150export async function setupGithub(151  options: SetupGithubOptions = {},152): Promise<SetupGithubResult> {153  const cwd = options.cwd ?? process.cwd();154  const fileOps = options.fileOps ?? nodeFileOps;155 156  if (!(await isGitHubRepositoryAsync({ cwd }))) {157    throw new SetupGithubError(158      'github_repository_not_found',159      'Unable to determine the GitHub repository. /setup-github must be run from a git repository.',160      400,161    );162  }163 164  let gitRepoRoot: string;165  try {166    gitRepoRoot = await getGitRepoRootAsync({ cwd });167  } catch (error) {168    debugLogger.debug('Failed to get git repo root:', error);169    throw new SetupGithubError(170      'github_repository_not_found',171      'Unable to determine the GitHub repository. /setup-github must be run from a git repository.',172      400,173    );174  }175 176  if (options.workspaceRoot) {177    const [gitRootReal, workspaceReal] = await Promise.all([178      realpathOrResolve(gitRepoRoot),179      realpathOrResolve(options.workspaceRoot),180    ]);181    if (gitRootReal !== workspaceReal) {182      throw new SetupGithubError(183        'github_git_root_mismatch',184        'The Git repository root must match the daemon workspace root.',185        400,186      );187    }188  }189 190  fileOps.assertCanWrite?.();191 192  let releaseTag: string;193  try {194    releaseTag = await getLatestGitHubRelease(options.proxy);195  } catch (error) {196    writeStderrLine(197      `qwen setup-github: failed to determine latest qwen-code-action release: ${198        error instanceof Error ? error.message : String(error)199      }`,200    );201    debugLogger.debug(202      'Failed to determine latest qwen-code-action release:',203      error,204    );205    throw new SetupGithubError(206      'github_release_lookup_failed',207      'Unable to determine the latest qwen-code-action release on GitHub.',208      502,209    );210  }211 212  const readmeUrl = `https://github.com/QwenLM/qwen-code-action/blob/${releaseTag}/README.md#quick-start`;213  const secretsUrl = await resolveSecretsUrl(cwd);214  const downloads = await downloadWorkflows({215    releaseTag,216    proxy: options.proxy,217    abortSignal: options.abortSignal,218    fetchImpl: options.fetchImpl ?? fetch,219  });220 221  const result: SetupGithubResult = {222    kind: 'github_setup',223    workspaceCwd: options.workspaceRoot ?? gitRepoRoot,224    gitRepoRoot,225    releaseTag,226    readmeUrl,227    ...(secretsUrl ? { secretsUrl } : {}),228    workflows: [],229    gitignore: { path: '.gitignore', status: 'skipped' },230    warnings: [],231  };232 233  result.gitignore = await updateGitignore(gitRepoRoot, fileOps);234  if (result.gitignore.status === 'failed') {235    result.warnings.push('Failed to update .gitignore.');236  }237 238  try {239    await fileOps.ensureWorkflowDirectory(gitRepoRoot);240    for (const workflow of downloads) {241      const relativePath = path.posix.join(242        '.github',243        'workflows',244        path.posix.basename(workflow.sourcePath),245      );246      try {247        const write = await fileOps.writeTextFile(248          gitRepoRoot,249          relativePath,250          workflow.content,251        );252        result.workflows.push({253          sourcePath: workflow.sourcePath,254          path: relativePath,255          status: 'written',256          sizeBytes: write.sizeBytes,257        });258      } catch (error) {259        result.partial = true;260        result.workflows.push({261          sourcePath: workflow.sourcePath,262          path: relativePath,263          status: 'failed',264          error: error instanceof Error ? error.message : String(error),265        });266        throw new SetupGithubError(267          'github_workflow_write_failed',268          `Unable to write ${relativePath}.`,269          500,270          result,271        );272      }273    }274  } catch (error) {275    if (error instanceof SetupGithubError) throw error;276    throw new SetupGithubError(277      'github_workflow_write_failed',278      'Unable to create .github/workflows.',279      500,280      result,281    );282  }283 284  return result;285}286 287export async function updateGitignore(288  gitRepoRoot: string,289  fileOps: SetupGithubFileOps = nodeFileOps,290): Promise<GithubSetupGitignoreResult> {291  try {292    const existingContent = await fileOps.readTextFile(293      gitRepoRoot,294      '.gitignore',295    );296    if (existingContent === undefined) {297      const content = GITIGNORE_ENTRIES.join('\n') + '\n';298      await fileOps.writeTextFile(gitRepoRoot, '.gitignore', content);299      return {300        path: '.gitignore',301        status: 'created',302        added: [...GITIGNORE_ENTRIES],303      };304    }305 306    const missingEntries = GITIGNORE_ENTRIES.filter(307      (entry) =>308        !existingContent309          .split(/\r?\n/)310          .some((line) => line.split('#')[0].trim() === entry),311    );312    if (missingEntries.length === 0) {313      return { path: '.gitignore', status: 'unchanged' };314    }315 316    const nextContent =317      existingContent + '\n' + missingEntries.join('\n') + '\n';318    await fileOps.writeTextFile(gitRepoRoot, '.gitignore', nextContent);319    return {320      path: '.gitignore',321      status: 'updated',322      added: missingEntries,323    };324  } catch (error) {325    debugLogger.debug('Failed to update .gitignore:', error);326    return {327      path: '.gitignore',328      status: 'failed',329      error: error instanceof Error ? error.message : String(error),330    };331  }332}333 334async function downloadWorkflows(options: {335  releaseTag: string;336  proxy?: string;337  abortSignal?: AbortSignal;338  fetchImpl: typeof fetch;339}): Promise<Array<{ sourcePath: string; content: string }>> {340  const internalAbort = new AbortController();341  try {342    const dispatcher = options.proxy343      ? new ProxyAgent(options.proxy)344      : undefined;345    return await Promise.all(346      GITHUB_WORKFLOW_PATHS.map(async (workflow) => {347        const endpoint = `https://raw.githubusercontent.com/QwenLM/qwen-code-action/refs/tags/${options.releaseTag}/examples/workflows/${workflow}`;348        const response = await options.fetchImpl(endpoint, {349          method: 'GET',350          dispatcher,351          signal: AbortSignal.any([352            AbortSignal.timeout(30_000),353            internalAbort.signal,354            ...(options.abortSignal ? [options.abortSignal] : []),355          ]),356        } as RequestInit);357 358        if (!response.ok) {359          throw new Error(360            `Invalid response code downloading ${endpoint}: ${response.status} - ${response.statusText}`,361          );362        }363        return {364          sourcePath: workflow,365          content: await readResponseTextWithLimit(response, workflow),366        };367      }),368    );369  } catch (error) {370    internalAbort.abort();371    const message = error instanceof Error ? error.message : String(error);372    debugLogger.debug('Failed to download qwen-code-action workflows:', error);373    throw new SetupGithubError(374      'github_workflow_download_failed',375      `Unable to download qwen-code-action workflows from GitHub. ${message}`,376      502,377    );378  }379}380 381async function readResponseTextWithLimit(382  response: Response,383  sourcePath: string,384): Promise<string> {385  const contentLength = response.headers.get('content-length');386  if (contentLength !== null) {387    const parsedLength = Number(contentLength);388    if (389      Number.isFinite(parsedLength) &&390      parsedLength > MAX_WORKFLOW_DOWNLOAD_BYTES391    ) {392      throw new Error(393        `${sourcePath} exceeds download limit of ${MAX_WORKFLOW_DOWNLOAD_BYTES} bytes`,394      );395    }396  }397 398  if (!response.body) return '';399 400  const reader = response.body.getReader();401  const chunks: Uint8Array[] = [];402  let totalBytes = 0;403  try {404    while (true) {405      const { done, value } = await reader.read();406      if (done) break;407      if (!value) continue;408      totalBytes += value.byteLength;409      if (totalBytes > MAX_WORKFLOW_DOWNLOAD_BYTES) {410        await reader.cancel().catch(() => {});411        throw new Error(412          `${sourcePath} exceeds download limit of ${MAX_WORKFLOW_DOWNLOAD_BYTES} bytes`,413        );414      }415      chunks.push(value);416    }417  } finally {418    reader.releaseLock();419  }420 421  const body = new Uint8Array(totalBytes);422  let offset = 0;423  for (const chunk of chunks) {424    body.set(chunk, offset);425    offset += chunk.byteLength;426  }427  return new TextDecoder().decode(body);428}429 430async function resolveSecretsUrl(cwd: string): Promise<string | undefined> {431  try {432    const repoInfo = await getGitHubRepoInfoAsync({ cwd });433    return `https://github.com/${repoInfo.owner}/${repoInfo.repo}/settings/secrets/actions`;434  } catch {435    return undefined;436  }437}438 439async function realpathOrResolve(input: string): Promise<string> {440  try {441    return await fsp.realpath(input);442  } catch {443    return path.resolve(input);444  }445}446 
basant307/AI_Governance_Project · CoolFace