mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-14 05:15:19 +00:00
further wip
This commit is contained in:
parent
963f6fd69e
commit
775b87a06c
11 changed files with 420 additions and 156 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env SKIP_ENV_VALIDATION=1 yarn workspaces foreach -A run build",
|
"build": "cross-env SKIP_ENV_VALIDATION=1 yarn workspaces foreach -A run build",
|
||||||
"test": "yarn workspaces foreach -A run test",
|
"test": "yarn workspaces foreach -A run test",
|
||||||
"dev": "yarn dev:prisma:migrate:dev && npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web watch:mcp watch:schemas",
|
"dev": "concurrently --kill-others --names \"zoekt,worker,web,mcp,schemas\" 'yarn dev:zoekt' 'yarn dev:backend' 'yarn dev:web' 'yarn watch:mcp' 'yarn watch:schemas'",
|
||||||
"with-env": "cross-env PATH=\"$PWD/bin:$PATH\" dotenv -e .env.development -c --",
|
"with-env": "cross-env PATH=\"$PWD/bin:$PATH\" dotenv -e .env.development -c --",
|
||||||
"dev:zoekt": "yarn with-env zoekt-webserver -index .sourcebot/index -rpc",
|
"dev:zoekt": "yarn with-env zoekt-webserver -index .sourcebot/index -rpc",
|
||||||
"dev:backend": "yarn with-env yarn workspace @sourcebot/backend dev:watch",
|
"dev:backend": "yarn with-env yarn workspace @sourcebot/backend dev:watch",
|
||||||
|
|
@ -21,9 +21,9 @@
|
||||||
"build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db,@sourcebot/shared}' run build"
|
"build:deps": "yarn workspaces foreach -R --from '{@sourcebot/schemas,@sourcebot/error,@sourcebot/crypto,@sourcebot/db,@sourcebot/shared}' run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0"
|
||||||
"npm-run-all": "^4.1.5"
|
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.7.0",
|
"packageManager": "yarn@4.7.0",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
||||||
|
|
@ -364,12 +364,12 @@ export class ConnectionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public async dispose() {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
this.worker.close();
|
await this.worker.close();
|
||||||
this.queue.close();
|
await this.queue.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,12 +101,12 @@ export class RepoPermissionSyncer {
|
||||||
}, 1000 * 5);
|
}, 1000 * 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public async dispose() {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
this.worker.close();
|
await this.worker.close();
|
||||||
this.queue.close();
|
await this.queue.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async schedulePermissionSync(repos: Repo[]) {
|
private async schedulePermissionSync(repos: Repo[]) {
|
||||||
|
|
|
||||||
|
|
@ -101,12 +101,12 @@ export class UserPermissionSyncer {
|
||||||
}, 1000 * 5);
|
}, 1000 * 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public async dispose() {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
this.worker.close();
|
await this.worker.close();
|
||||||
this.queue.close();
|
await this.queue.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async schedulePermissionSync(users: User[]) {
|
private async schedulePermissionSync(users: User[]) {
|
||||||
|
|
|
||||||
|
|
@ -90,13 +90,28 @@ else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement(
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanup = async (signal: string) => {
|
const cleanup = async (signal: string) => {
|
||||||
logger.info(`Recieved ${signal}, cleaning up...`);
|
logger.info(`Received ${signal}, cleaning up...`);
|
||||||
|
|
||||||
connectionManager.dispose();
|
const shutdownTimeout = 30000; // 30 seconds
|
||||||
repoManager.dispose();
|
|
||||||
repoPermissionSyncer.dispose();
|
try {
|
||||||
userPermissionSyncer.dispose();
|
await Promise.race([
|
||||||
indexSyncer.dispose();
|
Promise.all([
|
||||||
|
indexSyncer.dispose(),
|
||||||
|
repoManager.dispose(),
|
||||||
|
connectionManager.dispose(),
|
||||||
|
repoPermissionSyncer.dispose(),
|
||||||
|
userPermissionSyncer.dispose(),
|
||||||
|
promClient.dispose(),
|
||||||
|
]),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Shutdown timeout')), shutdownTimeout)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
logger.info('All workers shut down gracefully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Shutdown timeout or error, forcing exit:', error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
await redis.quit();
|
await redis.quit();
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,46 @@
|
||||||
import { createBullBoard } from '@bull-board/api';
|
import { createBullBoard } from '@bull-board/api';
|
||||||
import { ExpressAdapter } from '@bull-board/express';
|
import { ExpressAdapter } from '@bull-board/express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { PrismaClient, Repo, RepoIndexingJobStatus } from "@sourcebot/db";
|
import { PrismaClient, Repo, RepoJobStatus, RepoJobType } from "@sourcebot/db";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger, Logger } from "@sourcebot/logger";
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { BullBoardGroupMQAdapter, Job, Queue, ReservedJob, Worker } from "groupmq";
|
import { BullBoardGroupMQAdapter, Job, Queue, ReservedJob, Worker } from "groupmq";
|
||||||
import { Redis } from 'ioredis';
|
import { Redis } from 'ioredis';
|
||||||
import { AppContext, repoMetadataSchema, RepoWithConnections, Settings } from "./types.js";
|
import { AppContext, repoMetadataSchema, RepoWithConnections, Settings } from "./types.js";
|
||||||
import { getAuthCredentialsForRepo, getRepoPath, measure } from './utils.js';
|
import { getAuthCredentialsForRepo, getRepoPath, getShardPrefix, measure } from './utils.js';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { cloneRepository, fetchRepository, unsetGitConfig, upsertGitConfig } from './git.js';
|
import { cloneRepository, fetchRepository, isPathAValidGitRepoRoot, unsetGitConfig, upsertGitConfig } from './git.js';
|
||||||
import { indexGitRepository } from './zoekt.js';
|
import { indexGitRepository } from './zoekt.js';
|
||||||
|
import { rm, readdir } from 'fs/promises';
|
||||||
|
|
||||||
const logger = createLogger('index-syncer');
|
const LOG_TAG = 'index-syncer';
|
||||||
|
const logger = createLogger(LOG_TAG);
|
||||||
|
const createJobLogger = (jobId: string) => createLogger(`${LOG_TAG}:job:${jobId}`);
|
||||||
|
|
||||||
type IndexSyncJob = {
|
type JobPayload = {
|
||||||
|
type: 'INDEX' | 'CLEANUP';
|
||||||
jobId: string;
|
jobId: string;
|
||||||
}
|
repoId: number;
|
||||||
|
repoName: string;
|
||||||
|
};
|
||||||
|
|
||||||
const JOB_TIMEOUT_MS = 1000 * 60 * 60 * 6; // 6 hour indexing timeout
|
const JOB_TIMEOUT_MS = 1000 * 60 * 60 * 6; // 6 hour indexing timeout
|
||||||
|
|
||||||
|
|
||||||
|
const groupmqLifecycleExceptionWrapper = async (name: string, fn: () => Promise<void>) => {
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
logger.error(`Exception thrown while executing lifecycle function \`${name}\`.`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export class IndexSyncer {
|
export class IndexSyncer {
|
||||||
private interval?: NodeJS.Timeout;
|
private interval?: NodeJS.Timeout;
|
||||||
private queue: Queue<IndexSyncJob>;
|
private queue: Queue<JobPayload>;
|
||||||
private worker: Worker<IndexSyncJob>;
|
private worker: Worker<JobPayload>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private db: PrismaClient,
|
private db: PrismaClient,
|
||||||
|
|
@ -31,28 +48,26 @@ export class IndexSyncer {
|
||||||
redis: Redis,
|
redis: Redis,
|
||||||
private ctx: AppContext,
|
private ctx: AppContext,
|
||||||
) {
|
) {
|
||||||
this.queue = new Queue<IndexSyncJob>({
|
this.queue = new Queue<JobPayload>({
|
||||||
redis,
|
redis,
|
||||||
namespace: 'index-sync-queue',
|
namespace: 'index-sync-queue',
|
||||||
jobTimeoutMs: JOB_TIMEOUT_MS,
|
jobTimeoutMs: JOB_TIMEOUT_MS,
|
||||||
// logger: true,
|
logger,
|
||||||
|
maxAttempts: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.worker = new Worker<IndexSyncJob>({
|
this.worker = new Worker<JobPayload>({
|
||||||
queue: this.queue,
|
queue: this.queue,
|
||||||
maxStalledCount: 1,
|
maxStalledCount: 1,
|
||||||
stalledInterval: 1000,
|
|
||||||
handler: this.runJob.bind(this),
|
handler: this.runJob.bind(this),
|
||||||
concurrency: this.settings.maxRepoIndexingJobConcurrency,
|
concurrency: this.settings.maxRepoIndexingJobConcurrency,
|
||||||
|
logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.worker.on('completed', this.onJobCompleted.bind(this));
|
this.worker.on('completed', this.onJobCompleted.bind(this));
|
||||||
this.worker.on('failed', this.onJobFailed.bind(this));
|
this.worker.on('failed', this.onJobFailed.bind(this));
|
||||||
this.worker.on('stalled', this.onJobStalled.bind(this));
|
this.worker.on('stalled', this.onJobStalled.bind(this));
|
||||||
this.worker.on('error', async (error) => {
|
this.worker.on('error', this.onWorkerError.bind(this));
|
||||||
Sentry.captureException(error);
|
|
||||||
logger.error(`Index syncer worker error.`, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// @nocheckin
|
// @nocheckin
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
@ -69,75 +84,133 @@ export class IndexSyncer {
|
||||||
|
|
||||||
public async startScheduler() {
|
public async startScheduler() {
|
||||||
this.interval = setInterval(async () => {
|
this.interval = setInterval(async () => {
|
||||||
const thresholdDate = new Date(Date.now() - this.settings.reindexIntervalMs);
|
await this.scheduleIndexJobs();
|
||||||
|
await this.scheduleCleanupJobs();
|
||||||
const repos = await this.db.repo.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{ indexedAt: null },
|
|
||||||
{ indexedAt: { lt: thresholdDate } },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NOT: {
|
|
||||||
indexingJobs: {
|
|
||||||
some: {
|
|
||||||
OR: [
|
|
||||||
// Don't schedule if there are active jobs that were created within the threshold date.
|
|
||||||
// This handles the case where a job is stuck in a pending state and will never be scheduled.
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
status: {
|
|
||||||
in: [
|
|
||||||
RepoIndexingJobStatus.PENDING,
|
|
||||||
RepoIndexingJobStatus.IN_PROGRESS,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
createdAt: {
|
|
||||||
gt: thresholdDate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// Don't schedule if there are recent failed jobs (within the threshold date).
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{ status: RepoIndexingJobStatus.FAILED },
|
|
||||||
{ completedAt: { gt: thresholdDate } },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (repos.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.scheduleIndexSync(repos);
|
|
||||||
}, 1000 * 5);
|
}, 1000 * 5);
|
||||||
|
|
||||||
this.worker.run();
|
this.worker.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async scheduleIndexSync(repos: Repo[]) {
|
private async scheduleIndexJobs() {
|
||||||
|
const thresholdDate = new Date(Date.now() - this.settings.reindexIntervalMs);
|
||||||
|
const reposToIndex = await this.db.repo.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{ indexedAt: null },
|
||||||
|
{ indexedAt: { lt: thresholdDate } },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NOT: {
|
||||||
|
jobs: {
|
||||||
|
some: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
type: RepoJobType.INDEX,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
// Don't schedule if there are active jobs that were created within the threshold date.
|
||||||
|
// This handles the case where a job is stuck in a pending state and will never be scheduled.
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
status: {
|
||||||
|
in: [
|
||||||
|
RepoJobStatus.PENDING,
|
||||||
|
RepoJobStatus.IN_PROGRESS,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
gt: thresholdDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Don't schedule if there are recent failed jobs (within the threshold date).
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ status: RepoJobStatus.FAILED },
|
||||||
|
{ completedAt: { gt: thresholdDate } },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reposToIndex.length > 0) {
|
||||||
|
await this.createJobs(reposToIndex, RepoJobType.INDEX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async scheduleCleanupJobs() {
|
||||||
|
const thresholdDate = new Date(Date.now() - this.settings.repoGarbageCollectionGracePeriodMs);
|
||||||
|
|
||||||
|
const reposToCleanup = await this.db.repo.findMany({
|
||||||
|
where: {
|
||||||
|
connections: {
|
||||||
|
none: {}
|
||||||
|
},
|
||||||
|
OR: [
|
||||||
|
{ indexedAt: null },
|
||||||
|
{ indexedAt: { lt: thresholdDate } },
|
||||||
|
],
|
||||||
|
// Don't schedule if there are active jobs that were created within the threshold date.
|
||||||
|
NOT: {
|
||||||
|
jobs: {
|
||||||
|
some: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
type: RepoJobType.CLEANUP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: {
|
||||||
|
in: [
|
||||||
|
RepoJobStatus.PENDING,
|
||||||
|
RepoJobStatus.IN_PROGRESS,
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
gt: thresholdDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reposToCleanup.length > 0) {
|
||||||
|
await this.createJobs(reposToCleanup, RepoJobType.CLEANUP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createJobs(repos: Repo[], type: RepoJobType) {
|
||||||
// @note: we don't perform this in a transaction because
|
// @note: we don't perform this in a transaction because
|
||||||
// we want to avoid the situation where a job is created and run
|
// we want to avoid the situation where a job is created and run
|
||||||
// prior to the transaction being committed.
|
// prior to the transaction being committed.
|
||||||
const jobs = await this.db.repoIndexingJob.createManyAndReturn({
|
const jobs = await this.db.repoJob.createManyAndReturn({
|
||||||
data: repos.map(repo => ({
|
data: repos.map(repo => ({
|
||||||
|
type,
|
||||||
repoId: repo.id,
|
repoId: repo.id,
|
||||||
}))
|
})),
|
||||||
|
include: {
|
||||||
|
repo: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
|
|
@ -145,22 +218,29 @@ export class IndexSyncer {
|
||||||
groupId: `repo:${job.repoId}`,
|
groupId: `repo:${job.repoId}`,
|
||||||
data: {
|
data: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
|
type,
|
||||||
|
repoName: job.repo.name,
|
||||||
|
repoId: job.repo.id,
|
||||||
},
|
},
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runJob(job: ReservedJob<IndexSyncJob>) {
|
private async runJob(job: ReservedJob<JobPayload>) {
|
||||||
const id = job.data.jobId;
|
const id = job.data.jobId;
|
||||||
const { repo } = await this.db.repoIndexingJob.update({
|
const logger = createJobLogger(id);
|
||||||
|
logger.info(`Running job ${id} for repo ${job.data.repoName}`);
|
||||||
|
|
||||||
|
const { repo, type: jobType } = await this.db.repoJob.update({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
status: RepoIndexingJobStatus.IN_PROGRESS,
|
status: RepoJobStatus.IN_PROGRESS,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
type: true,
|
||||||
repo: {
|
repo: {
|
||||||
include: {
|
include: {
|
||||||
connections: {
|
connections: {
|
||||||
|
|
@ -173,10 +253,14 @@ export class IndexSyncer {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.syncGitRepository(repo);
|
if (jobType === RepoJobType.INDEX) {
|
||||||
|
await this.indexRepository(repo, logger);
|
||||||
|
} else if (jobType === RepoJobType.CLEANUP) {
|
||||||
|
await this.cleanupRepository(repo, logger);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncGitRepository(repo: RepoWithConnections) {
|
private async indexRepository(repo: RepoWithConnections, logger: Logger) {
|
||||||
const { path: repoPath, isReadOnly } = getRepoPath(repo, this.ctx);
|
const { path: repoPath, isReadOnly } = getRepoPath(repo, this.ctx);
|
||||||
|
|
||||||
const metadata = repoMetadataSchema.parse(repo.metadata);
|
const metadata = repoMetadataSchema.parse(repo.metadata);
|
||||||
|
|
@ -185,6 +269,14 @@ export class IndexSyncer {
|
||||||
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
|
const cloneUrlMaybeWithToken = credentials?.cloneUrlWithToken ?? repo.cloneUrl;
|
||||||
const authHeader = credentials?.authHeader ?? undefined;
|
const authHeader = credentials?.authHeader ?? undefined;
|
||||||
|
|
||||||
|
// If the repo path exists but it is not a valid git repository root, this indicates
|
||||||
|
// that the repository is in a bad state. To fix, we remove the directory and perform
|
||||||
|
// a fresh clone.
|
||||||
|
if (existsSync(repoPath) && !(await isPathAValidGitRepoRoot(repoPath)) && !isReadOnly) {
|
||||||
|
logger.warn(`${repoPath} is not a valid git repository root. Deleting directory and performing fresh clone.`);
|
||||||
|
await rm(repoPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (existsSync(repoPath) && !isReadOnly) {
|
if (existsSync(repoPath) && !isReadOnly) {
|
||||||
// @NOTE: in #483, we changed the cloning method s.t., we _no longer_
|
// @NOTE: in #483, we changed the cloning method s.t., we _no longer_
|
||||||
// write the clone URL (which could contain a auth token) to the
|
// write the clone URL (which could contain a auth token) to the
|
||||||
|
|
@ -238,57 +330,94 @@ export class IndexSyncer {
|
||||||
logger.info(`Indexed ${repo.displayName} in ${indexDuration_s}s`);
|
logger.info(`Indexed ${repo.displayName} in ${indexDuration_s}s`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onJobCompleted(job: Job<IndexSyncJob>) {
|
private async cleanupRepository(repo: Repo, logger: Logger) {
|
||||||
const { repo } = await this.db.repoIndexingJob.update({
|
const { path: repoPath, isReadOnly } = getRepoPath(repo, this.ctx);
|
||||||
where: { id: job.data.jobId },
|
if (existsSync(repoPath) && !isReadOnly) {
|
||||||
data: {
|
logger.info(`Deleting repo directory ${repoPath}`);
|
||||||
status: RepoIndexingJobStatus.COMPLETED,
|
await rm(repoPath, { recursive: true, force: true });
|
||||||
repo: {
|
}
|
||||||
update: {
|
|
||||||
|
const shardPrefix = getShardPrefix(repo.orgId, repo.id);
|
||||||
|
const files = (await readdir(this.ctx.indexPath)).filter(file => file.startsWith(shardPrefix));
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = `${this.ctx.indexPath}/${file}`;
|
||||||
|
logger.info(`Deleting shard file ${filePath}`);
|
||||||
|
await rm(filePath, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onJobCompleted = async (job: Job<JobPayload>) =>
|
||||||
|
groupmqLifecycleExceptionWrapper('onJobCompleted', async () => {
|
||||||
|
const logger = createJobLogger(job.data.jobId);
|
||||||
|
const jobData = await this.db.repoJob.update({
|
||||||
|
where: { id: job.data.jobId },
|
||||||
|
data: {
|
||||||
|
status: RepoJobStatus.COMPLETED,
|
||||||
|
completedAt: new Date(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (jobData.type === RepoJobType.INDEX) {
|
||||||
|
const repo = await this.db.repo.update({
|
||||||
|
where: { id: jobData.repoId },
|
||||||
|
data: {
|
||||||
indexedAt: new Date(),
|
indexedAt: new Date(),
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Completed index job ${job.data.jobId} for repo ${repo.name}`);
|
||||||
|
}
|
||||||
|
else if (jobData.type === RepoJobType.CLEANUP) {
|
||||||
|
const repo = await this.db.repo.delete({
|
||||||
|
where: { id: jobData.repoId },
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Completed cleanup job ${job.data.jobId} for repo ${repo.name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private onJobFailed = async (job: Job<JobPayload>) =>
|
||||||
|
groupmqLifecycleExceptionWrapper('onJobFailed', async () => {
|
||||||
|
const logger = createJobLogger(job.data.jobId);
|
||||||
|
|
||||||
|
const { repo } = await this.db.repoJob.update({
|
||||||
|
where: { id: job.data.jobId },
|
||||||
|
data: {
|
||||||
|
completedAt: new Date(),
|
||||||
|
errorMessage: job.failedReason,
|
||||||
},
|
},
|
||||||
completedAt: new Date(),
|
select: { repo: true }
|
||||||
},
|
});
|
||||||
select: { repo: true }
|
|
||||||
|
logger.error(`Failed job ${job.data.jobId} for repo ${repo.name}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`Completed index job ${job.data.jobId} for repo ${repo.name}`);
|
private onJobStalled = async (jobId: string) =>
|
||||||
}
|
groupmqLifecycleExceptionWrapper('onJobStalled', async () => {
|
||||||
|
const logger = createJobLogger(jobId);
|
||||||
|
const { repo } = await this.db.repoJob.update({
|
||||||
|
where: { id: jobId },
|
||||||
|
data: {
|
||||||
|
status: RepoJobStatus.FAILED,
|
||||||
|
completedAt: new Date(),
|
||||||
|
errorMessage: 'Job stalled',
|
||||||
|
},
|
||||||
|
select: { repo: true }
|
||||||
|
});
|
||||||
|
|
||||||
private async onJobFailed(job: Job<IndexSyncJob>) {
|
logger.error(`Job ${jobId} stalled for repo ${repo.name}`);
|
||||||
const { repo } = await this.db.repoIndexingJob.update({
|
|
||||||
where: { id: job.data.jobId },
|
|
||||||
data: {
|
|
||||||
status: RepoIndexingJobStatus.FAILED,
|
|
||||||
completedAt: new Date(),
|
|
||||||
errorMessage: job.failedReason,
|
|
||||||
},
|
|
||||||
select: { repo: true}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.error(`Failed index job ${job.data.jobId} for repo ${repo.name}`);
|
private async onWorkerError(error: Error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
logger.error(`Index syncer worker error.`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onJobStalled(jobId: string) {
|
public async dispose() {
|
||||||
const { repo } = await this.db.repoIndexingJob.update({
|
|
||||||
where: { id: jobId },
|
|
||||||
data: {
|
|
||||||
status: RepoIndexingJobStatus.FAILED,
|
|
||||||
completedAt: new Date(),
|
|
||||||
errorMessage: 'Job stalled',
|
|
||||||
},
|
|
||||||
select: { repo: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.error(`Job ${jobId} stalled for repo ${repo.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose() {
|
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
this.worker.close();
|
await this.worker.close();
|
||||||
this.queue.close();
|
await this.queue.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import express, { Request, Response } from 'express';
|
import express, { Request, Response } from 'express';
|
||||||
|
import { Server } from 'http';
|
||||||
import client, { Registry, Counter, Gauge } from 'prom-client';
|
import client, { Registry, Counter, Gauge } from 'prom-client';
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
|
|
||||||
|
|
@ -7,6 +8,8 @@ const logger = createLogger('prometheus-client');
|
||||||
export class PromClient {
|
export class PromClient {
|
||||||
private registry: Registry;
|
private registry: Registry;
|
||||||
private app: express.Application;
|
private app: express.Application;
|
||||||
|
private server: Server;
|
||||||
|
|
||||||
public activeRepoIndexingJobs: Gauge<string>;
|
public activeRepoIndexingJobs: Gauge<string>;
|
||||||
public pendingRepoIndexingJobs: Gauge<string>;
|
public pendingRepoIndexingJobs: Gauge<string>;
|
||||||
public repoIndexingReattemptsTotal: Counter<string>;
|
public repoIndexingReattemptsTotal: Counter<string>;
|
||||||
|
|
@ -98,12 +101,12 @@ export class PromClient {
|
||||||
res.end(metrics);
|
res.end(metrics);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.listen(this.PORT, () => {
|
this.server = this.app.listen(this.PORT, () => {
|
||||||
logger.info(`Prometheus metrics server is running on port ${this.PORT}`);
|
logger.info(`Prometheus metrics server is running on port ${this.PORT}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegistry(): Registry {
|
dispose() {
|
||||||
return this.registry;
|
this.server.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -558,9 +558,9 @@ export class RepoManager {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
this.indexWorker.close();
|
await this.indexWorker.close();
|
||||||
this.indexQueue.close();
|
await this.indexQueue.close();
|
||||||
this.gcQueue.close();
|
await this.gcQueue.close();
|
||||||
this.gcWorker.close();
|
await this.gcWorker.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,13 +54,15 @@ model Repo {
|
||||||
webUrl String?
|
webUrl String?
|
||||||
connections RepoToConnection[]
|
connections RepoToConnection[]
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
|
|
||||||
|
/// @deprecated status tracking is now done via the `jobs` table.
|
||||||
repoIndexingStatus RepoIndexingStatus @default(NEW)
|
repoIndexingStatus RepoIndexingStatus @default(NEW)
|
||||||
|
|
||||||
permittedUsers UserToRepoPermission[]
|
permittedUsers UserToRepoPermission[]
|
||||||
permissionSyncJobs RepoPermissionSyncJob[]
|
permissionSyncJobs RepoPermissionSyncJob[]
|
||||||
permissionSyncedAt DateTime? /// When the permissions were last synced successfully.
|
permissionSyncedAt DateTime? /// When the permissions were last synced successfully.
|
||||||
|
|
||||||
indexingJobs RepoIndexingJob[]
|
jobs RepoJob[]
|
||||||
indexedAt DateTime? /// When the repo was last indexed successfully.
|
indexedAt DateTime? /// When the repo was last indexed successfully.
|
||||||
|
|
||||||
external_id String /// The id of the repo in the external service
|
external_id String /// The id of the repo in the external service
|
||||||
|
|
@ -76,16 +78,22 @@ model Repo {
|
||||||
@@index([orgId])
|
@@index([orgId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RepoIndexingJobStatus {
|
enum RepoJobStatus {
|
||||||
PENDING
|
PENDING
|
||||||
IN_PROGRESS
|
IN_PROGRESS
|
||||||
COMPLETED
|
COMPLETED
|
||||||
FAILED
|
FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
model RepoIndexingJob {
|
enum RepoJobType {
|
||||||
|
INDEX
|
||||||
|
CLEANUP
|
||||||
|
}
|
||||||
|
|
||||||
|
model RepoJob {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
status RepoIndexingJobStatus @default(PENDING)
|
type RepoJobType
|
||||||
|
status RepoJobStatus @default(PENDING)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
completedAt DateTime?
|
completedAt DateTime?
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import winston, { format } from 'winston';
|
import winston, { format, Logger } from 'winston';
|
||||||
import { Logtail } from '@logtail/node';
|
import { Logtail } from '@logtail/node';
|
||||||
import { LogtailTransport } from '@logtail/winston';
|
import { LogtailTransport } from '@logtail/winston';
|
||||||
import { MESSAGE } from 'triple-beam';
|
import { MESSAGE } from 'triple-beam';
|
||||||
|
|
@ -48,7 +48,7 @@ const createLogger = (label: string) => {
|
||||||
format: combine(
|
format: combine(
|
||||||
errors({ stack: true }),
|
errors({ stack: true }),
|
||||||
timestamp(),
|
timestamp(),
|
||||||
labelFn({ label: label })
|
labelFn({ label: label }),
|
||||||
),
|
),
|
||||||
transports: [
|
transports: [
|
||||||
new winston.transports.Console({
|
new winston.transports.Console({
|
||||||
|
|
@ -85,3 +85,7 @@ const createLogger = (label: string) => {
|
||||||
export {
|
export {
|
||||||
createLogger
|
createLogger
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Logger,
|
||||||
|
}
|
||||||
113
yarn.lock
113
yarn.lock
|
|
@ -10061,6 +10061,17 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"cliui@npm:^8.0.1":
|
||||||
|
version: 8.0.1
|
||||||
|
resolution: "cliui@npm:8.0.1"
|
||||||
|
dependencies:
|
||||||
|
string-width: "npm:^4.2.0"
|
||||||
|
strip-ansi: "npm:^6.0.1"
|
||||||
|
wrap-ansi: "npm:^7.0.0"
|
||||||
|
checksum: 10c0/4bda0f09c340cbb6dfdc1ed508b3ca080f12992c18d68c6be4d9cf51756033d5266e61ec57529e610dacbf4da1c634423b0c1b11037709cc6b09045cbd815df5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"clone@npm:^1.0.2":
|
"clone@npm:^1.0.2":
|
||||||
version: 1.0.4
|
version: 1.0.4
|
||||||
resolution: "clone@npm:1.0.4"
|
resolution: "clone@npm:1.0.4"
|
||||||
|
|
@ -10459,6 +10470,23 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"concurrently@npm:^9.2.1":
|
||||||
|
version: 9.2.1
|
||||||
|
resolution: "concurrently@npm:9.2.1"
|
||||||
|
dependencies:
|
||||||
|
chalk: "npm:4.1.2"
|
||||||
|
rxjs: "npm:7.8.2"
|
||||||
|
shell-quote: "npm:1.8.3"
|
||||||
|
supports-color: "npm:8.1.1"
|
||||||
|
tree-kill: "npm:1.2.2"
|
||||||
|
yargs: "npm:17.7.2"
|
||||||
|
bin:
|
||||||
|
conc: dist/bin/concurrently.js
|
||||||
|
concurrently: dist/bin/concurrently.js
|
||||||
|
checksum: 10c0/da37f239f82eb7ac24f5ddb56259861e5f1d6da2ade7602b6ea7ad3101b13b5ccec02a77b7001402d1028ff2fdc38eed55644b32853ad5abf30e057002a963aa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"content-disposition@npm:0.5.4":
|
"content-disposition@npm:0.5.4":
|
||||||
version: 0.5.4
|
version: 0.5.4
|
||||||
resolution: "content-disposition@npm:0.5.4"
|
resolution: "content-disposition@npm:0.5.4"
|
||||||
|
|
@ -11810,7 +11838,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"escalade@npm:^3.2.0":
|
"escalade@npm:^3.1.1, escalade@npm:^3.2.0":
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
resolution: "escalade@npm:3.2.0"
|
resolution: "escalade@npm:3.2.0"
|
||||||
checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65
|
checksum: 10c0/ced4dd3a78e15897ed3be74e635110bbf3b08877b0a41be50dcb325ee0e0b5f65fc2d50e9845194d7c4633f327e2e1c6cce00a71b617c5673df0374201d67f65
|
||||||
|
|
@ -12781,6 +12809,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"get-caller-file@npm:^2.0.5":
|
||||||
|
version: 2.0.5
|
||||||
|
resolution: "get-caller-file@npm:2.0.5"
|
||||||
|
checksum: 10c0/c6c7b60271931fa752aeb92f2b47e355eac1af3a2673f47c9589e8f8a41adc74d45551c1bc57b5e66a80609f10ffb72b6f575e4370d61cc3f7f3aaff01757cde
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0":
|
"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "get-intrinsic@npm:1.3.0"
|
resolution: "get-intrinsic@npm:1.3.0"
|
||||||
|
|
@ -17617,6 +17652,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"require-directory@npm:^2.1.1":
|
||||||
|
version: 2.1.1
|
||||||
|
resolution: "require-directory@npm:2.1.1"
|
||||||
|
checksum: 10c0/83aa76a7bc1531f68d92c75a2ca2f54f1b01463cb566cf3fbc787d0de8be30c9dbc211d1d46be3497dac5785fe296f2dd11d531945ac29730643357978966e99
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"require-from-string@npm:^2.0.2":
|
"require-from-string@npm:^2.0.2":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "require-from-string@npm:2.0.2"
|
resolution: "require-from-string@npm:2.0.2"
|
||||||
|
|
@ -17931,9 +17973,9 @@ __metadata:
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "root-workspace-0b6124@workspace:."
|
resolution: "root-workspace-0b6124@workspace:."
|
||||||
dependencies:
|
dependencies:
|
||||||
|
concurrently: "npm:^9.2.1"
|
||||||
cross-env: "npm:^7.0.3"
|
cross-env: "npm:^7.0.3"
|
||||||
dotenv-cli: "npm:^8.0.0"
|
dotenv-cli: "npm:^8.0.0"
|
||||||
npm-run-all: "npm:^4.1.5"
|
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|
@ -18015,6 +18057,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"rxjs@npm:7.8.2":
|
||||||
|
version: 7.8.2
|
||||||
|
resolution: "rxjs@npm:7.8.2"
|
||||||
|
dependencies:
|
||||||
|
tslib: "npm:^2.1.0"
|
||||||
|
checksum: 10c0/1fcd33d2066ada98ba8f21fcbbcaee9f0b271de1d38dc7f4e256bfbc6ffcdde68c8bfb69093de7eeb46f24b1fb820620bf0223706cff26b4ab99a7ff7b2e2c45
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"safe-array-concat@npm:^1.1.3":
|
"safe-array-concat@npm:^1.1.3":
|
||||||
version: 1.1.3
|
version: 1.1.3
|
||||||
resolution: "safe-array-concat@npm:1.1.3"
|
resolution: "safe-array-concat@npm:1.1.3"
|
||||||
|
|
@ -18443,6 +18494,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"shell-quote@npm:1.8.3":
|
||||||
|
version: 1.8.3
|
||||||
|
resolution: "shell-quote@npm:1.8.3"
|
||||||
|
checksum: 10c0/bee87c34e1e986cfb4c30846b8e6327d18874f10b535699866f368ade11ea4ee45433d97bf5eada22c4320c27df79c3a6a7eb1bf3ecfc47f2c997d9e5e2672fd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"shell-quote@npm:^1.6.1":
|
"shell-quote@npm:^1.6.1":
|
||||||
version: 1.8.2
|
version: 1.8.2
|
||||||
resolution: "shell-quote@npm:1.8.2"
|
resolution: "shell-quote@npm:1.8.2"
|
||||||
|
|
@ -18864,7 +18922,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0":
|
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3":
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
resolution: "string-width@npm:4.2.3"
|
resolution: "string-width@npm:4.2.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -19128,6 +19186,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"supports-color@npm:8.1.1":
|
||||||
|
version: 8.1.1
|
||||||
|
resolution: "supports-color@npm:8.1.1"
|
||||||
|
dependencies:
|
||||||
|
has-flag: "npm:^4.0.0"
|
||||||
|
checksum: 10c0/ea1d3c275dd604c974670f63943ed9bd83623edc102430c05adb8efc56ba492746b6e95386e7831b872ec3807fd89dd8eb43f735195f37b5ec343e4234cc7e89
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0":
|
"supports-color@npm:^5.3.0, supports-color@npm:^5.5.0":
|
||||||
version: 5.5.0
|
version: 5.5.0
|
||||||
resolution: "supports-color@npm:5.5.0"
|
resolution: "supports-color@npm:5.5.0"
|
||||||
|
|
@ -19438,6 +19505,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tree-kill@npm:1.2.2":
|
||||||
|
version: 1.2.2
|
||||||
|
resolution: "tree-kill@npm:1.2.2"
|
||||||
|
bin:
|
||||||
|
tree-kill: cli.js
|
||||||
|
checksum: 10c0/7b1b7c7f17608a8f8d20a162e7957ac1ef6cd1636db1aba92f4e072dc31818c2ff0efac1e3d91064ede67ed5dc57c565420531a8134090a12ac10cf792ab14d2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"trim-lines@npm:^3.0.0":
|
"trim-lines@npm:^3.0.0":
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
resolution: "trim-lines@npm:3.0.1"
|
resolution: "trim-lines@npm:3.0.1"
|
||||||
|
|
@ -20487,7 +20563,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0":
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
resolution: "wrap-ansi@npm:7.0.0"
|
resolution: "wrap-ansi@npm:7.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -20574,6 +20650,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"y18n@npm:^5.0.5":
|
||||||
|
version: 5.0.8
|
||||||
|
resolution: "y18n@npm:5.0.8"
|
||||||
|
checksum: 10c0/4df2842c36e468590c3691c894bc9cdbac41f520566e76e24f59401ba7d8b4811eb1e34524d57e54bc6d864bcb66baab7ffd9ca42bf1eda596618f9162b91249
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"yallist@npm:^3.0.2":
|
"yallist@npm:^3.0.2":
|
||||||
version: 3.1.1
|
version: 3.1.1
|
||||||
resolution: "yallist@npm:3.1.1"
|
resolution: "yallist@npm:3.1.1"
|
||||||
|
|
@ -20604,6 +20687,28 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"yargs-parser@npm:^21.1.1":
|
||||||
|
version: 21.1.1
|
||||||
|
resolution: "yargs-parser@npm:21.1.1"
|
||||||
|
checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"yargs@npm:17.7.2":
|
||||||
|
version: 17.7.2
|
||||||
|
resolution: "yargs@npm:17.7.2"
|
||||||
|
dependencies:
|
||||||
|
cliui: "npm:^8.0.1"
|
||||||
|
escalade: "npm:^3.1.1"
|
||||||
|
get-caller-file: "npm:^2.0.5"
|
||||||
|
require-directory: "npm:^2.1.1"
|
||||||
|
string-width: "npm:^4.2.3"
|
||||||
|
y18n: "npm:^5.0.5"
|
||||||
|
yargs-parser: "npm:^21.1.1"
|
||||||
|
checksum: 10c0/ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"yocto-queue@npm:^0.1.0":
|
"yocto-queue@npm:^0.1.0":
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
resolution: "yocto-queue@npm:0.1.0"
|
resolution: "yocto-queue@npm:0.1.0"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue