12 Commits

Author SHA1 Message Date
3a498c00ee 1.6.0
Some checks failed
Default (tags) / security (push) Successful in 54s
Default (tags) / test (push) Successful in 2m9s
Default (tags) / release (push) Failing after 1m29s
Default (tags) / metadata (push) Successful in 1m54s
2024-12-14 00:54:38 +01:00
bb248ed408 feat(core): Add changelog fetching and parsing functionality 2024-12-14 00:54:38 +01:00
e843197211 1.5.3
Some checks failed
Default (tags) / security (push) Successful in 47s
Default (tags) / test (push) Successful in 2m17s
Default (tags) / release (push) Failing after 1m31s
Default (tags) / metadata (push) Successful in 1m54s
2024-12-14 00:47:24 +01:00
3502a661ea fix(core): Fix filtering logic for returning only tagged commits 2024-12-14 00:47:24 +01:00
d103778a75 1.5.2
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Successful in 2m19s
Default (tags) / release (push) Failing after 1m32s
Default (tags) / metadata (push) Successful in 1m58s
2024-12-14 00:33:58 +01:00
9b1b91eb31 fix(core): Ensure stability of core functionalities. 2024-12-14 00:33:58 +01:00
25b2519324 1.5.1
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Successful in 2m22s
Default (tags) / release (push) Failing after 1m42s
Default (tags) / metadata (push) Successful in 2m6s
2024-12-14 00:32:34 +01:00
166b289eb2 fix(core): Refine logging format in CodeFeed class 2024-12-14 00:32:34 +01:00
6ca6b37b1d 1.5.0
Some checks failed
Default (tags) / security (push) Successful in 53s
Default (tags) / test (push) Successful in 2m13s
Default (tags) / release (push) Failing after 1m50s
Default (tags) / metadata (push) Successful in 2m7s
2024-12-14 00:30:35 +01:00
5d0d125e43 feat(core): Refactor TypeScript interfaces and improve module exports 2024-12-14 00:30:35 +01:00
470f4fe730 1.4.1
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Successful in 2m18s
Default (tags) / release (push) Failing after 1m37s
Default (tags) / metadata (push) Successful in 1m59s
2024-12-13 22:16:48 +01:00
daeb38c91c fix(core): Corrected log formatting for commit information output in CodeFeed 2024-12-13 22:16:47 +01:00
7 changed files with 195 additions and 98 deletions

View File

@ -1,5 +1,38 @@
# Changelog
## 2024-12-14 - 1.6.0 - feat(core)
Add changelog fetching and parsing functionality
- Implemented loadChangelogFromRepo to directly load the changelog from a Gitea repository.
- Introduced parsing functionality to extract specific version details from the loaded changelog.
- Updated CodeFeed class to utilize the changelog for version verification and commit processing.
## 2024-12-14 - 1.5.3 - fix(core)
Fix filtering logic for returning only tagged commits
- Ensure `allCommits` is filtered to only include commits with 'tagged' status before returning.
## 2024-12-14 - 1.5.2 - fix(core)
Ensure stability of core functionalities.
## 2024-12-14 - 1.5.1 - fix(core)
Refine logging format in CodeFeed class
- Modified console log format in fetchAllCommitsFromInstance method for better readability.
## 2024-12-14 - 1.5.0 - feat(core)
Refactor TypeScript interfaces and improve module exports
- Moved TypeScript interfaces to a dedicated file (ts/interfaces/index.ts).
- Updated import/export structure to improve code readability and maintainability.
- Enhanced the package.json to utilize a module exports field for better resolution.
## 2024-12-13 - 1.4.1 - fix(core)
Corrected log formatting for commit information output in CodeFeed
- Fixed formatting issue in commit log output within the CodeFeed class to ensure proper display of timestamps.
## 2024-12-13 - 1.4.0 - feat(CodeFeed)
Enhance commit results with human-readable time

View File

@ -1,10 +1,12 @@
{
"name": "@foss.global/codefeed",
"version": "1.4.0",
"version": "1.6.0",
"private": false,
"description": "a module for creating feeds for code development",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"exports": {
".": "./dist_ts/index.js",
"./interfaces": "./dist_ts/interfaces/index.js"
},
"type": "module",
"author": "Task Venture Capital GmbH",
"license": "MIT",

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@foss.global/codefeed',
version: '1.4.0',
version: '1.6.0',
description: 'a module for creating feeds for code development'
}

View File

@ -1,3 +1,10 @@
// module
import * as interfaces from './interfaces/index.js';
export {
interfaces,
}
// @push.rocks
import * as qenv from '@push.rocks/qenv';
import * as smartnpm from '@push.rocks/smartnpm';

View File

@ -1,56 +1,12 @@
import * as plugins from './codefeed.plugins.js';
interface RepositoryOwner {
login: string;
}
interface Repository {
owner: RepositoryOwner;
name: string;
}
interface CommitAuthor {
date: string;
}
interface CommitDetail {
message: string;
author: CommitAuthor;
}
interface Commit {
sha: string;
commit: CommitDetail;
}
interface Tag {
commit?: {
sha?: string;
};
}
interface RepoSearchResponse {
data: Repository[];
}
export interface CommitResult {
baseUrl: string;
org: string;
repo: string;
timestamp: string;
hash: string;
commitMessage: string;
tagged: boolean;
publishedOnNpm: boolean;
prettyAgoTime: string;
}
export class CodeFeed {
private baseUrl: string;
private token?: string;
private npmRegistry = new plugins.smartnpm.NpmRegistry();
private smartxmlInstance = new plugins.smartxml.SmartXml();
private lastRunTimestamp: string;
private changelogContent: string;
constructor(baseUrl: string, token?: string, lastRunTimestamp?: string) {
this.baseUrl = baseUrl;
@ -60,8 +16,66 @@ export class CodeFeed {
}
/**
* Fetch all organizations from the Gitea instance.
* Load the changelog directly from the Gitea repository.
*/
private async loadChangelogFromRepo(owner: string, repo: string): Promise<void> {
const url = `${this.baseUrl}/api/v1/repos/${owner}/${repo}/contents/changelog.md`;
const headers: Record<string, string> = {};
if (this.token) {
headers['Authorization'] = `token ${this.token}`;
}
const response = await fetch(url, { headers });
if (!response.ok) {
console.error(`Could not fetch CHANGELOG.md from ${owner}/${repo}: ${response.status} ${response.statusText}`);
this.changelogContent = '';
return;
}
const data = await response.json();
if (!data.content) {
console.warn(`No content field found in response for ${owner}/${repo}/changelog.md`);
this.changelogContent = '';
return;
}
const decodedContent = Buffer.from(data.content, 'base64').toString('utf8');
this.changelogContent = decodedContent;
}
/**
* Parse the changelog to find the entry for a given version.
* The changelog format is assumed as:
*
* # Changelog
*
* ## <date> - <version> - <description>
* <changes...>
*/
private getChangelogForVersion(version: string): string | undefined {
if (!this.changelogContent) {
return undefined;
}
const lines = this.changelogContent.split('\n');
const versionHeaderIndex = lines.findIndex((line) => line.includes(`- ${version} -`));
if (versionHeaderIndex === -1) {
return undefined;
}
const changelogLines: string[] = [];
for (let i = versionHeaderIndex + 1; i < lines.length; i++) {
const line = lines[i];
// The next version header starts with `## `
if (line.startsWith('## ')) {
break;
}
changelogLines.push(line);
}
return changelogLines.join('\n').trim();
}
private async fetchAllOrganizations(): Promise<string[]> {
const url = `${this.baseUrl}/api/v1/orgs`;
const response = await fetch(url, {
@ -76,18 +90,17 @@ export class CodeFeed {
return data.map((org) => org.username);
}
/**
* Fetch organization-level activity RSS feed.
*/
private async fetchOrgRssFeed(optionsArg: {
orgName: string,
repoName?: string,
}): Promise<any[]> {
let rssUrl: string
let rssUrl: string;
if (optionsArg.orgName && !optionsArg.repoName) {
rssUrl = `${this.baseUrl}/${optionsArg.orgName}.atom`;
} else if (optionsArg.orgName && optionsArg.repoName) {
rssUrl = `${this.baseUrl}/${optionsArg.orgName}/${optionsArg.repoName}.atom`;
} else {
throw new Error('Invalid arguments provided to fetchOrgRssFeed.');
}
const response = await fetch(rssUrl);
@ -96,36 +109,25 @@ export class CodeFeed {
}
const rssText = await response.text();
// Parse the Atom feed using fast-xml-parser
const rssData = this.smartxmlInstance.parseXmlToObject(rssText);
// Return the <entry> elements from the feed
return rssData.feed.entry || [];
}
/**
* Check if the organization's RSS feed has any new activities since the last run.
*/
private async hasNewActivity(optionsArg: {
orgName: string,
repoName?: string,
}): Promise<boolean> {
const entries = await this.fetchOrgRssFeed(optionsArg);
// Filter entries to find new activities since the last run
return entries.some((entry: any) => {
const updated = new Date(entry.updated);
return updated > new Date(this.lastRunTimestamp);
});
}
/**
* Fetch all repositories accessible to the token/user.
*/
private async fetchAllRepositories(): Promise<Repository[]> {
private async fetchAllRepositories(): Promise<plugins.interfaces.IRepository[]> {
let page = 1;
const allRepos: Repository[] = [];
const allRepos: plugins.interfaces.IRepository[] = [];
while (true) {
const url = new URL(`${this.baseUrl}/api/v1/repos/search`);
@ -140,7 +142,7 @@ export class CodeFeed {
throw new Error(`Failed to fetch repositories: ${resp.statusText}`);
}
const data: RepoSearchResponse = await resp.json();
const data: plugins.interfaces.IRepoSearchResponse = await resp.json();
allRepos.push(...data.data);
if (data.data.length < 50) {
@ -152,12 +154,9 @@ export class CodeFeed {
return allRepos;
}
/**
* Fetch all tags for a given repository.
*/
private async fetchTags(owner: string, repo: string): Promise<Set<string>> {
let page = 1;
const tags: Tag[] = [];
const tags: plugins.interfaces.ITag[] = [];
while (true) {
const url = new URL(`${this.baseUrl}/api/v1/repos/${owner}/${repo}/tags`);
@ -173,7 +172,7 @@ export class CodeFeed {
throw new Error(`Failed to fetch tags for ${owner}/${repo}: ${resp.statusText}`);
}
const data: Tag[] = await resp.json();
const data: plugins.interfaces.ITag[] = await resp.json();
tags.push(...data);
if (data.length < 50) {
@ -192,13 +191,10 @@ export class CodeFeed {
return taggedCommitShas;
}
/**
* Fetch commits from the last 24 hours for a repository.
*/
private async fetchRecentCommitsForRepo(owner: string, repo: string): Promise<Commit[]> {
private async fetchRecentCommitsForRepo(owner: string, repo: string): Promise<plugins.interfaces.ICommit[]> {
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
let page = 1;
const recentCommits: Commit[] = [];
const recentCommits: plugins.interfaces.ICommit[] = [];
while (true) {
const url = new URL(`${this.baseUrl}/api/v1/repos/${owner}/${repo}/commits`);
@ -214,7 +210,7 @@ export class CodeFeed {
throw new Error(`Failed to fetch commits for ${owner}/${repo}: ${resp.statusText}`);
}
const data: Commit[] = await resp.json();
const data: plugins.interfaces.ICommit[] = await resp.json();
if (data.length === 0) {
break;
}
@ -234,13 +230,10 @@ export class CodeFeed {
return recentCommits;
}
/**
* Fetch all commits by querying all organizations.
*/
public async fetchAllCommitsFromInstance(): Promise<CommitResult[]> {
public async fetchAllCommitsFromInstance(): Promise<plugins.interfaces.ICommitResult[]> {
const orgs = await this.fetchAllOrganizations();
console.log(`Found ${orgs.length} organizations`);
let allCommits: CommitResult[] = [];
let allCommits: plugins.interfaces.ICommitResult[] = [];
for (const orgName of orgs) {
console.log(`Checking activity for organization: ${orgName}`);
@ -253,7 +246,7 @@ export class CodeFeed {
console.log(`No new activity for organization: ${orgName}`);
continue;
}
} catch (error) {
} catch (error: any) {
console.error(`Error fetching activity for organization ${orgName}:`, error.message);
continue;
}
@ -271,10 +264,11 @@ export class CodeFeed {
console.log(`No new activity for repository: ${orgName}/${r.name}`);
continue;
}
} catch (error) {
} catch (error: any) {
console.error(`Error fetching activity for repository ${orgName}/${r.name}:`, error.message);
continue;
}
const org = r.owner.login;
const repo = r.name;
console.log(`Processing repository ${org}/${repo}`);
@ -283,8 +277,11 @@ export class CodeFeed {
const taggedCommitShas = await this.fetchTags(org, repo);
const commits = await this.fetchRecentCommitsForRepo(org, repo);
// Load the changelog from this repo.
await this.loadChangelogFromRepo(org, repo);
const commitResults = commits.map((c) => {
const commit: CommitResult = {
const commit: plugins.interfaces.ICommitResult = {
baseUrl: this.baseUrl,
org,
repo,
@ -294,43 +291,55 @@ export class CodeFeed {
commitMessage: c.commit.message,
tagged: taggedCommitShas.has(c.sha),
publishedOnNpm: false,
}
changelog: undefined
};
return commit;
});
if (commitResults.length > 0) {
try {
const packageInfo = await this.npmRegistry.getPackageInfo(`@${org}/${repo}`);
for (const commit of commitResults.filter((c) => c.tagged)) {
for (const commitResult of commitResults.filter((c) => c.tagged)) {
const versionCandidate = commitResult.commitMessage.replace('\n', '').trim();
const correspondingVersion = packageInfo.allVersions.find((versionArg) => {
return versionArg.version === commit.commitMessage.replace('\n', '');
return versionArg.version === versionCandidate;
});
if (correspondingVersion) {
commit.publishedOnNpm = true;
commitResult.publishedOnNpm = true;
const changelogEntry = this.getChangelogForVersion(versionCandidate);
if (changelogEntry) {
commitResult.changelog = changelogEntry;
}
}
} catch (error) {
}
} catch (error: any) {
console.error(`Failed to fetch package info for ${org}/${repo}:`, error.message);
}
}
allCommits.push(...commitResults);
} catch (error) {
} catch (error: any) {
console.error(`Skipping repository ${org}/${repo} due to error:`, error.message);
}
}
}
console.log(`Processed ${allCommits.length} commits in total.`);
allCommits = allCommits.filter(commitArg => commitArg.tagged);
console.log(`Filtered to ${allCommits.length} commits with tagged statuses.`);
for (const c of allCommits) {
console.log(`______________________________________________________
${c.prettyAgoTime}
${c.timestamp}
Commit ${c.hash} by ${c.org}/${c.repo} at ${c.timestamp}
console.log(` ==========================================================================
${c.prettyAgoTime} ago:
${c.org}/${c.repo}
${c.commitMessage}
Published on npm: ${c.publishedOnNpm}
${c.changelog ? `Changelog:\n${c.changelog}\n` : ''}
`);
}
return allCommits;
}
}

45
ts/interfaces/index.ts Normal file
View File

@ -0,0 +1,45 @@
export interface IRepositoryOwner {
login: string;
}
export interface IRepository {
owner: IRepositoryOwner;
name: string;
}
export interface ICommitAuthor {
date: string;
}
export interface ICommitDetail {
message: string;
author: ICommitAuthor;
}
export interface ICommit {
sha: string;
commit: ICommitDetail;
}
export interface ITag {
commit?: {
sha?: string;
};
}
export interface IRepoSearchResponse {
data: IRepository[];
}
export interface ICommitResult {
baseUrl: string;
org: string;
repo: string;
timestamp: string;
hash: string;
commitMessage: string;
tagged: boolean;
publishedOnNpm: boolean;
prettyAgoTime: string;
changelog: string | undefined;
}

View File

@ -8,7 +8,8 @@
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
"paths": {
}
},
"exclude": [
"dist_*/**/*.d.ts"