feat: add organization allowlist and denylist filters, enhance changelog loading, and improve fetch functions
- Introduced orgAllowlist and orgDenylist properties to filter organizations during fetching. - Enhanced loadChangelogFromRepo to check multiple potential changelog file names. - Updated fetchTags to return a map of tag names associated with their SHAs. - Improved pagination logic in fetchAllOrganizations and fetchRepositoriesForOrg to handle larger datasets. - Added retry logic in fetchFunction to handle rate limiting and server errors more gracefully. - Modified ITag interface to include an optional name property for better tag handling.
This commit is contained in:
		
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							@@ -16,17 +16,17 @@
 | 
				
			|||||||
    "buildDocs": "(tsdoc)"
 | 
					    "buildDocs": "(tsdoc)"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@git.zone/tsbuild": "^2.3.2",
 | 
					    "@git.zone/tsbuild": "^2.6.8",
 | 
				
			||||||
    "@git.zone/tsbundle": "^2.2.5",
 | 
					    "@git.zone/tsbundle": "^2.5.1",
 | 
				
			||||||
    "@git.zone/tsrun": "^1.2.46",
 | 
					    "@git.zone/tsrun": "^1.2.46",
 | 
				
			||||||
    "@git.zone/tstest": "^1.0.96",
 | 
					    "@git.zone/tstest": "^2.3.8",
 | 
				
			||||||
    "@push.rocks/tapbundle": "^5.6.3",
 | 
					    "@push.rocks/tapbundle": "^6.0.3",
 | 
				
			||||||
    "@types/node": "^22.15.2"
 | 
					    "@types/node": "^22.15.2"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@push.rocks/lik": "^6.2.2",
 | 
					    "@push.rocks/lik": "^6.2.2",
 | 
				
			||||||
    "@push.rocks/qenv": "^6.1.0",
 | 
					    "@push.rocks/qenv": "^6.1.3",
 | 
				
			||||||
    "@push.rocks/smartnpm": "^2.0.4",
 | 
					    "@push.rocks/smartnpm": "^2.0.6",
 | 
				
			||||||
    "@push.rocks/smarttime": "^4.1.1",
 | 
					    "@push.rocks/smarttime": "^4.1.1",
 | 
				
			||||||
    "@push.rocks/smartxml": "^1.1.1"
 | 
					    "@push.rocks/smartxml": "^1.1.1"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5808
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5808
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										185
									
								
								ts/index.ts
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								ts/index.ts
									
									
									
									
									
								
							@@ -4,6 +4,7 @@ export class CodeFeed {
 | 
				
			|||||||
  private baseUrl: string;
 | 
					  private baseUrl: string;
 | 
				
			||||||
  private token?: string;
 | 
					  private token?: string;
 | 
				
			||||||
  private lastRunTimestamp: string;
 | 
					  private lastRunTimestamp: string;
 | 
				
			||||||
 | 
					  private pageLimit = 50;
 | 
				
			||||||
  // Raw changelog content for the current repository
 | 
					  // Raw changelog content for the current repository
 | 
				
			||||||
  private changelogContent: string = '';
 | 
					  private changelogContent: string = '';
 | 
				
			||||||
  // npm registry helper for published-on-npm checks
 | 
					  // npm registry helper for published-on-npm checks
 | 
				
			||||||
@@ -16,6 +17,9 @@ export class CodeFeed {
 | 
				
			|||||||
  private enableNpmCheck: boolean = true;
 | 
					  private enableNpmCheck: boolean = true;
 | 
				
			||||||
  // return only tagged commits (false by default)
 | 
					  // return only tagged commits (false by default)
 | 
				
			||||||
  private enableTaggedOnly: boolean = false;
 | 
					  private enableTaggedOnly: boolean = false;
 | 
				
			||||||
 | 
					  // allow/deny filters
 | 
				
			||||||
 | 
					  private orgAllowlist?: string[];
 | 
				
			||||||
 | 
					  private orgDenylist?: string[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    baseUrl: string,
 | 
					    baseUrl: string,
 | 
				
			||||||
@@ -26,6 +30,8 @@ export class CodeFeed {
 | 
				
			|||||||
      cacheWindowMs?: number;
 | 
					      cacheWindowMs?: number;
 | 
				
			||||||
      enableNpmCheck?: boolean;
 | 
					      enableNpmCheck?: boolean;
 | 
				
			||||||
      taggedOnly?: boolean;
 | 
					      taggedOnly?: boolean;
 | 
				
			||||||
 | 
					      orgAllowlist?: string[];
 | 
				
			||||||
 | 
					      orgDenylist?: string[];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this.baseUrl = baseUrl;
 | 
					    this.baseUrl = baseUrl;
 | 
				
			||||||
@@ -37,6 +43,8 @@ export class CodeFeed {
 | 
				
			|||||||
    this.cacheWindowMs = options?.cacheWindowMs;
 | 
					    this.cacheWindowMs = options?.cacheWindowMs;
 | 
				
			||||||
    this.enableNpmCheck = options?.enableNpmCheck ?? true;
 | 
					    this.enableNpmCheck = options?.enableNpmCheck ?? true;
 | 
				
			||||||
    this.enableTaggedOnly = options?.taggedOnly ?? false;
 | 
					    this.enableTaggedOnly = options?.taggedOnly ?? false;
 | 
				
			||||||
 | 
					    this.orgAllowlist = options?.orgAllowlist;
 | 
				
			||||||
 | 
					    this.orgDenylist = options?.orgDenylist;
 | 
				
			||||||
    this.cache = [];
 | 
					    this.cache = [];
 | 
				
			||||||
    // npm registry instance for version lookups
 | 
					    // npm registry instance for version lookups
 | 
				
			||||||
    this.npmRegistry = new plugins.smartnpm.NpmRegistry();
 | 
					    this.npmRegistry = new plugins.smartnpm.NpmRegistry();
 | 
				
			||||||
@@ -61,7 +69,14 @@ export class CodeFeed {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 1) get all organizations
 | 
					    // 1) get all organizations
 | 
				
			||||||
    const orgs = await this.fetchAllOrganizations();
 | 
					    let orgs = await this.fetchAllOrganizations();
 | 
				
			||||||
 | 
					    // apply allow/deny filters
 | 
				
			||||||
 | 
					    if (this.orgAllowlist && this.orgAllowlist.length > 0) {
 | 
				
			||||||
 | 
					      orgs = orgs.filter((o) => this.orgAllowlist!.includes(o));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (this.orgDenylist && this.orgDenylist.length > 0) {
 | 
				
			||||||
 | 
					      orgs = orgs.filter((o) => !this.orgDenylist!.includes(o));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 2) fetch repos per org in parallel
 | 
					    // 2) fetch repos per org in parallel
 | 
				
			||||||
    const repoLists = await Promise.all(
 | 
					    const repoLists = await Promise.all(
 | 
				
			||||||
@@ -122,11 +137,15 @@ export class CodeFeed {
 | 
				
			|||||||
      await this.loadChangelogFromRepo(owner, name);
 | 
					      await this.loadChangelogFromRepo(owner, name);
 | 
				
			||||||
      // fetch tags for this repo
 | 
					      // fetch tags for this repo
 | 
				
			||||||
      let taggedShas: Set<string>;
 | 
					      let taggedShas: Set<string>;
 | 
				
			||||||
 | 
					      let tagNameBySha: Map<string, string>;
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        taggedShas = await this.fetchTags(owner, name);
 | 
					        const tagInfo = await this.fetchTags(owner, name);
 | 
				
			||||||
 | 
					        taggedShas = tagInfo.shas;
 | 
				
			||||||
 | 
					        tagNameBySha = tagInfo.map;
 | 
				
			||||||
      } catch (e: any) {
 | 
					      } catch (e: any) {
 | 
				
			||||||
        console.error(`Failed to fetch tags for ${owner}/${name}:`, e.message);
 | 
					        console.error(`Failed to fetch tags for ${owner}/${name}:`, e.message);
 | 
				
			||||||
        taggedShas = new Set<string>();
 | 
					        taggedShas = new Set<string>();
 | 
				
			||||||
 | 
					        tagNameBySha = new Map<string, string>();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      // fetch npm package info only if any new commits correspond to a tag
 | 
					      // fetch npm package info only if any new commits correspond to a tag
 | 
				
			||||||
      const hasTaggedCommit = commits.some((c) => taggedShas.has(c.sha));
 | 
					      const hasTaggedCommit = commits.some((c) => taggedShas.has(c.sha));
 | 
				
			||||||
@@ -141,14 +160,23 @@ export class CodeFeed {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      // build commit entries
 | 
					      // build commit entries
 | 
				
			||||||
      for (const c of commits) {
 | 
					      for (const c of commits) {
 | 
				
			||||||
        const versionCandidate = c.commit.message.replace(/\n/g, '').trim();
 | 
					 | 
				
			||||||
        const isTagged = taggedShas.has(c.sha);
 | 
					        const isTagged = taggedShas.has(c.sha);
 | 
				
			||||||
        const publishedOnNpm = isTagged && pkgInfo
 | 
					        // derive version from tag name if present (strip leading 'v')
 | 
				
			||||||
          ? pkgInfo.allVersions.some((v) => v.version === versionCandidate)
 | 
					        let versionFromTag: string | undefined;
 | 
				
			||||||
 | 
					        if (isTagged) {
 | 
				
			||||||
 | 
					          const tagName = tagNameBySha.get(c.sha);
 | 
				
			||||||
 | 
					          if (tagName) {
 | 
				
			||||||
 | 
					            versionFromTag = tagName.startsWith('v') ? tagName.substring(1) : tagName;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const publishedOnNpm = isTagged && pkgInfo && versionFromTag
 | 
				
			||||||
 | 
					          ? pkgInfo.allVersions.some((v) => v.version === versionFromTag)
 | 
				
			||||||
          : false;
 | 
					          : false;
 | 
				
			||||||
        let changelogEntry: string | undefined;
 | 
					        let changelogEntry: string | undefined;
 | 
				
			||||||
        if (this.changelogContent) {
 | 
					        if (this.changelogContent) {
 | 
				
			||||||
          changelogEntry = this.getChangelogForVersion(versionCandidate);
 | 
					          if (versionFromTag) {
 | 
				
			||||||
 | 
					            changelogEntry = this.getChangelogForVersion(versionFromTag);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        newResults.push({
 | 
					        newResults.push({
 | 
				
			||||||
          baseUrl: this.baseUrl,
 | 
					          baseUrl: this.baseUrl,
 | 
				
			||||||
@@ -187,40 +215,49 @@ export class CodeFeed {
 | 
				
			|||||||
      return this.cache;
 | 
					      return this.cache;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // no caching: apply tagged-only filter if requested
 | 
					    // no caching: apply tagged-only filter if requested
 | 
				
			||||||
 | 
					    // sort and dedupe
 | 
				
			||||||
 | 
					    const seen = new Set<string>();
 | 
				
			||||||
 | 
					    const unique = newResults.filter((c) => {
 | 
				
			||||||
 | 
					      if (seen.has(c.hash)) return false;
 | 
				
			||||||
 | 
					      seen.add(c.hash);
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    unique.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
 | 
				
			||||||
    if (this.enableTaggedOnly) {
 | 
					    if (this.enableTaggedOnly) {
 | 
				
			||||||
      return newResults.filter((c) => c.tagged === true);
 | 
					      return unique.filter((c) => c.tagged === true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return newResults;
 | 
					    return unique;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Load the changelog directly from the Gitea repository.
 | 
					   * Load the changelog directly from the Gitea repository.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private async loadChangelogFromRepo(owner: string, repo: string): Promise<void> {
 | 
					  private async loadChangelogFromRepo(owner: string, repo: string): Promise<void> {
 | 
				
			||||||
    const url = `/api/v1/repos/${owner}/${repo}/contents/changelog.md`;
 | 
					 | 
				
			||||||
    const headers: Record<string, string> = {};
 | 
					    const headers: Record<string, string> = {};
 | 
				
			||||||
    if (this.token) {
 | 
					    if (this.token) headers['Authorization'] = `token ${this.token}`;
 | 
				
			||||||
      headers['Authorization'] = `token ${this.token}`;
 | 
					    const candidates = [
 | 
				
			||||||
    }
 | 
					      'CHANGELOG.md',
 | 
				
			||||||
 | 
					      'changelog.md',
 | 
				
			||||||
 | 
					      'Changelog.md',
 | 
				
			||||||
 | 
					      'docs/CHANGELOG.md',
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    for (const path of candidates) {
 | 
				
			||||||
 | 
					      const url = `/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`;
 | 
				
			||||||
      const response = await this.fetchFunction(url, { headers });
 | 
					      const response = await this.fetchFunction(url, { headers });
 | 
				
			||||||
      if (!response.ok) {
 | 
					      if (!response.ok) {
 | 
				
			||||||
      console.error(
 | 
					        continue;
 | 
				
			||||||
        `Could not fetch CHANGELOG.md from ${owner}/${repo}: ${response.status} ${response.statusText}`
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      this.changelogContent = '';
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
        const data = await response.json();
 | 
					        const data = await response.json();
 | 
				
			||||||
    if (!data.content) {
 | 
					        if (data && data.content) {
 | 
				
			||||||
      console.warn(`No content field found in response for ${owner}/${repo}/changelog.md`);
 | 
					          this.changelogContent = Buffer.from(data.content, 'base64').toString('utf8');
 | 
				
			||||||
      this.changelogContent = '';
 | 
					 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
    // decode base64 content
 | 
					        // continue trying others
 | 
				
			||||||
    this.changelogContent = Buffer.from(data.content, 'base64').toString('utf8');
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.changelogContent = '';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -257,49 +294,68 @@ export class CodeFeed {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Fetch all tags for a given repo and return the set of tagged commit SHAs
 | 
					   * Fetch all tags for a given repo and return the set of tagged commit SHAs
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  private async fetchTags(owner: string, repo: string): Promise<Set<string>> {
 | 
					  private async fetchTags(owner: string, repo: string): Promise<{ shas: Set<string>; map: Map<string, string> }> {
 | 
				
			||||||
    const taggedShas = new Set<string>();
 | 
					    const taggedShas = new Set<string>();
 | 
				
			||||||
 | 
					    const tagNameBySha = new Map<string, string>();
 | 
				
			||||||
    let page = 1;
 | 
					    let page = 1;
 | 
				
			||||||
    while (true) {
 | 
					    while (true) {
 | 
				
			||||||
      const url = `/api/v1/repos/${owner}/${repo}/tags?limit=50&page=${page}`;
 | 
					      const url = `/api/v1/repos/${owner}/${repo}/tags?limit=${this.pageLimit}&page=${page}`;
 | 
				
			||||||
      const resp = await this.fetchFunction(url, {
 | 
					      const resp = await this.fetchFunction(url, {
 | 
				
			||||||
        headers: this.token ? { Authorization: `token ${this.token}` } : {},
 | 
					        headers: this.token ? { Authorization: `token ${this.token}` } : {},
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      if (!resp.ok) {
 | 
					      if (!resp.ok) {
 | 
				
			||||||
        console.error(`Failed to fetch tags for ${owner}/${repo}: ${resp.status} ${resp.statusText}`);
 | 
					        console.error(`Failed to fetch tags for ${owner}/${repo}: ${resp.status} ${resp.statusText}`);
 | 
				
			||||||
        return taggedShas;
 | 
					        return { shas: taggedShas, map: tagNameBySha };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const data: plugins.interfaces.ITag[] = await resp.json();
 | 
					      const data: plugins.interfaces.ITag[] = await resp.json();
 | 
				
			||||||
      if (data.length === 0) break;
 | 
					      if (data.length === 0) break;
 | 
				
			||||||
      for (const t of data) {
 | 
					      for (const t of data) {
 | 
				
			||||||
        if (t.commit?.sha) taggedShas.add(t.commit.sha);
 | 
					        const sha = t.commit?.sha;
 | 
				
			||||||
 | 
					        if (sha) {
 | 
				
			||||||
 | 
					          taggedShas.add(sha);
 | 
				
			||||||
 | 
					          if (t.name) tagNameBySha.set(sha, t.name);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      if (data.length < 50) break;
 | 
					      }
 | 
				
			||||||
 | 
					      if (data.length < this.pageLimit) break;
 | 
				
			||||||
      page++;
 | 
					      page++;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return taggedShas;
 | 
					    return { shas: taggedShas, map: tagNameBySha };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async fetchAllOrganizations(): Promise<string[]> {
 | 
					  private async fetchAllOrganizations(): Promise<string[]> {
 | 
				
			||||||
    const resp = await this.fetchFunction('/api/v1/orgs', {
 | 
					    const headers = this.token ? { Authorization: `token ${this.token}` } : {};
 | 
				
			||||||
      headers: this.token ? { Authorization: `token ${this.token}` } : {},
 | 
					    let page = 1;
 | 
				
			||||||
    });
 | 
					    const orgs: string[] = [];
 | 
				
			||||||
 | 
					    while (true) {
 | 
				
			||||||
 | 
					      const resp = await this.fetchFunction(`/api/v1/orgs?limit=${this.pageLimit}&page=${page}`, { headers });
 | 
				
			||||||
      if (!resp.ok) {
 | 
					      if (!resp.ok) {
 | 
				
			||||||
      throw new Error(`Failed to fetch organizations: ${resp.statusText}`);
 | 
					        throw new Error(`Failed to fetch organizations: ${resp.status} ${resp.statusText}`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const data: { username: string }[] = await resp.json();
 | 
					      const data: { username: string }[] = await resp.json();
 | 
				
			||||||
    return data.map((o) => o.username);
 | 
					      if (data.length === 0) break;
 | 
				
			||||||
 | 
					      orgs.push(...data.map((o) => o.username));
 | 
				
			||||||
 | 
					      if (data.length < this.pageLimit) break;
 | 
				
			||||||
 | 
					      page++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return orgs;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async fetchRepositoriesForOrg(org: string): Promise<plugins.interfaces.IRepository[]> {
 | 
					  private async fetchRepositoriesForOrg(org: string): Promise<plugins.interfaces.IRepository[]> {
 | 
				
			||||||
    const resp = await this.fetchFunction(`/api/v1/orgs/${org}/repos?limit=50`, {
 | 
					    const headers = this.token ? { Authorization: `token ${this.token}` } : {};
 | 
				
			||||||
      headers: this.token ? { Authorization: `token ${this.token}` } : {},
 | 
					    let page = 1;
 | 
				
			||||||
    });
 | 
					    const repos: plugins.interfaces.IRepository[] = [];
 | 
				
			||||||
 | 
					    while (true) {
 | 
				
			||||||
 | 
					      const resp = await this.fetchFunction(`/api/v1/orgs/${org}/repos?limit=${this.pageLimit}&page=${page}`, { headers });
 | 
				
			||||||
      if (!resp.ok) {
 | 
					      if (!resp.ok) {
 | 
				
			||||||
      throw new Error(`Failed to fetch repositories for ${org}: ${resp.statusText}`);
 | 
					        throw new Error(`Failed to fetch repositories for ${org}: ${resp.status} ${resp.statusText}`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const data: plugins.interfaces.IRepository[] = await resp.json();
 | 
					      const data: plugins.interfaces.IRepository[] = await resp.json();
 | 
				
			||||||
    return data;
 | 
					      if (data.length === 0) break;
 | 
				
			||||||
 | 
					      repos.push(...data);
 | 
				
			||||||
 | 
					      if (data.length < this.pageLimit) break;
 | 
				
			||||||
 | 
					      page++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return repos;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async fetchRecentCommitsForRepo(
 | 
					  private async fetchRecentCommitsForRepo(
 | 
				
			||||||
@@ -308,23 +364,52 @@ export class CodeFeed {
 | 
				
			|||||||
    sinceTimestamp?: string
 | 
					    sinceTimestamp?: string
 | 
				
			||||||
  ): Promise<plugins.interfaces.ICommit[]> {
 | 
					  ): Promise<plugins.interfaces.ICommit[]> {
 | 
				
			||||||
    const since = sinceTimestamp ?? this.lastRunTimestamp;
 | 
					    const since = sinceTimestamp ?? this.lastRunTimestamp;
 | 
				
			||||||
    const resp = await this.fetchFunction(
 | 
					    const headers = this.token ? { Authorization: `token ${this.token}` } : {};
 | 
				
			||||||
      `/api/v1/repos/${owner}/${repo}/commits?since=${encodeURIComponent(
 | 
					    let page = 1;
 | 
				
			||||||
        since
 | 
					    const commits: plugins.interfaces.ICommit[] = [];
 | 
				
			||||||
      )}&limit=50`,
 | 
					    while (true) {
 | 
				
			||||||
      { headers: this.token ? { Authorization: `token ${this.token}` } : {} }
 | 
					      const url = `/api/v1/repos/${owner}/${repo}/commits?since=${encodeURIComponent(since)}&limit=${this.pageLimit}&page=${page}`;
 | 
				
			||||||
    );
 | 
					      const resp = await this.fetchFunction(url, { headers });
 | 
				
			||||||
      if (!resp.ok) {
 | 
					      if (!resp.ok) {
 | 
				
			||||||
      throw new Error(`Failed to fetch commits for ${owner}/${repo}: ${resp.statusText}`);
 | 
					        throw new Error(`Failed to fetch commits for ${owner}/${repo}: ${resp.status} ${resp.statusText}`);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const data: plugins.interfaces.ICommit[] = await resp.json();
 | 
					      const data: plugins.interfaces.ICommit[] = await resp.json();
 | 
				
			||||||
    return data;
 | 
					      if (data.length === 0) break;
 | 
				
			||||||
 | 
					      commits.push(...data);
 | 
				
			||||||
 | 
					      if (data.length < this.pageLimit) break;
 | 
				
			||||||
 | 
					      page++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return commits;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async fetchFunction(
 | 
					  public async fetchFunction(
 | 
				
			||||||
    urlArg: string,
 | 
					    urlArg: string,
 | 
				
			||||||
    optionsArg: RequestInit = {}
 | 
					    optionsArg: RequestInit = {}
 | 
				
			||||||
  ): Promise<Response> {
 | 
					  ): Promise<Response> {
 | 
				
			||||||
    return fetch(`${this.baseUrl}${urlArg}`, optionsArg);
 | 
					    const maxAttempts = 4;
 | 
				
			||||||
 | 
					    let attempt = 0;
 | 
				
			||||||
 | 
					    let lastError: any;
 | 
				
			||||||
 | 
					    while (attempt < maxAttempts) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const resp = await fetch(`${this.baseUrl}${urlArg}`, optionsArg);
 | 
				
			||||||
 | 
					        // retry on 429 and 5xx
 | 
				
			||||||
 | 
					        if (resp.status === 429 || resp.status >= 500) {
 | 
				
			||||||
 | 
					          const retryAfter = Number(resp.headers.get('retry-after'));
 | 
				
			||||||
 | 
					          const backoffMs = retryAfter
 | 
				
			||||||
 | 
					            ? retryAfter * 1000
 | 
				
			||||||
 | 
					            : Math.min(32000, 1000 * Math.pow(2, attempt)) + Math.floor(Math.random() * 250);
 | 
				
			||||||
 | 
					          await new Promise((r) => setTimeout(r, backoffMs));
 | 
				
			||||||
 | 
					          attempt++;
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return resp;
 | 
				
			||||||
 | 
					      } catch (e: any) {
 | 
				
			||||||
 | 
					        lastError = e;
 | 
				
			||||||
 | 
					        const backoffMs = Math.min(32000, 1000 * Math.pow(2, attempt)) + Math.floor(Math.random() * 250);
 | 
				
			||||||
 | 
					        await new Promise((r) => setTimeout(r, backoffMs));
 | 
				
			||||||
 | 
					        attempt++;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    throw new Error(`fetchFunction failed after retries for ${urlArg}: ${lastError?.message ?? 'unknown error'}`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -22,6 +22,7 @@ export interface ICommit {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ITag {
 | 
					export interface ITag {
 | 
				
			||||||
 | 
					  name?: string;
 | 
				
			||||||
  commit?: {
 | 
					  commit?: {
 | 
				
			||||||
    sha?: string;
 | 
					    sha?: string;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user