3 Commits

19 changed files with 277 additions and 368 deletions

View File

@@ -1,5 +1,13 @@
# Changelog
## 2026-03-28 - 2.13.0 - feat(cache,build,docs)
switch cache storage to SmartMongo and align build configuration with updated dependencies
- replaces LocalTsmDb usage with SmartMongo in the cache layer and related tests
- moves tsbundle and tswatch configuration from npmextra.json to .smartconfig.json
- updates Deno, frontend, and provider dependencies and adds @std/assert imports for tests
- refreshes README to document managed secrets, sync, action log, and the expanded handler and view set
## 2026-03-02 - 2.12.0 - feat(pipelines)
add pipelines view modes, time-range filtering, group aggregation, sorting, and job log polling

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/gitops",
"version": "2.12.0",
"version": "2.13.0",
"exports": "./mod.ts",
"nodeModulesDir": "auto",
"tasks": {
@@ -12,15 +12,16 @@
"@std/fs": "jsr:@std/fs@^1.0.19",
"@std/encoding": "jsr:@std/encoding@^1.0.10",
"@api.global/typedrequest-interfaces": "npm:@api.global/typedrequest-interfaces@^3.0.19",
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.3.0",
"@api.global/typedserver": "npm:@api.global/typedserver@^8.4.6",
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
"@apiclient.xyz/gitea": "npm:@apiclient.xyz/gitea@^1.2.0",
"@apiclient.xyz/gitlab": "npm:@apiclient.xyz/gitlab@^2.2.0",
"@push.rocks/smartmongo": "npm:@push.rocks/smartmongo@^5.1.0",
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.0.15",
"@push.rocks/smartsecret": "npm:@push.rocks/smartsecret@^1.0.1"
"@apiclient.xyz/gitea": "npm:@apiclient.xyz/gitea@^1.5.0",
"@apiclient.xyz/gitlab": "npm:@apiclient.xyz/gitlab@^2.6.0",
"@push.rocks/smartmongo": "npm:@push.rocks/smartmongo@^7.0.0",
"@push.rocks/smartdata": "npm:@push.rocks/smartdata@^7.1.3",
"@push.rocks/smartsecret": "npm:@push.rocks/smartsecret@^1.0.2",
"@std/assert": "jsr:@std/assert@^1.0.0"
},
"compilerOptions": {
"lib": [

21
license Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/gitops",
"version": "2.12.0",
"version": "2.13.0",
"description": "GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs",
"main": "mod.ts",
"type": "module",
@@ -14,15 +14,20 @@
"license": "MIT",
"dependencies": {
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "8.4.0",
"@api.global/typedsocket": "^4.1.0",
"@apiclient.xyz/gitea": "1.4.0",
"@apiclient.xyz/gitlab": "2.5.0",
"@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6"
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/gitea": "^1.5.0",
"@apiclient.xyz/gitlab": "^2.6.0",
"@design.estate/dees-catalog": "^3.49.0",
"@design.estate/dees-element": "^2.2.4"
},
"devDependencies": {
"@git.zone/tsbundle": "^2.9.0",
"@git.zone/tswatch": "^3.2.0"
"@git.zone/tsbundle": "^2.10.0",
"@git.zone/tswatch": "^3.3.2"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}

BIN
pipelines-current-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,18 +1,21 @@
# @serve.zone/gitops
A unified dashboard for managing Gitea and GitLab instances — browse projects, manage secrets, monitor CI/CD pipelines, stream build logs, and receive webhook notifications, all from a single app.
A unified dashboard for managing Gitea and GitLab instances — browse projects, manage secrets, monitor CI/CD pipelines, stream build logs, sync configurations, and receive webhook notifications, all from a single app. 🚀
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## 🚀 Features
## Features
- **Multi-Provider** — Connect to Gitea and GitLab simultaneously via a unified provider abstraction
- **Secrets Management** — View, create, update, and delete CI/CD secrets across projects and groups
- **Pipeline Monitoring** — Browse pipelines, view jobs, retry failed builds, cancel running ones
- **Build Log Streaming** — Fetch and display raw job logs with monospace rendering
- **Webhook Integration** — Receive push/PR/pipeline events via `POST /webhook/:connectionId` and broadcast to all connected clients in real-time via WebSocket
- **Managed Secrets** — Define secrets once and push them to multiple providers/scopes automatically
- **Pipeline Monitoring** — Browse pipelines with time-range filtering, view modes, group aggregation, and sorting; view jobs, retry failed builds, cancel running ones
- **Build Log Streaming** — Fetch and display raw job logs with monospace rendering and live polling
- **Sync Configurations** — Define repo sync rules across providers with status tracking
- **Action Log** — Global audit trail of all operations across the system
- **Webhook Integration** — Receive push/PR/pipeline events via `POST /webhook/:connectionId` and broadcast to all connected WebSocket clients in real-time
- **Secrets Cache & Scanning** — Background scan service fetches and caches all secrets every 24h with upsert-based deduplication
- **Secure Token Storage** — Connection tokens stored in OS keychain via `@push.rocks/smartsecret` (encrypted file fallback), never in plaintext on disk
- **Auto-Refresh** — Frontend polls for updates every 30s, with manual refresh available on every view
@@ -24,7 +27,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
- [Deno](https://deno.land/) v2+
- [pnpm](https://pnpm.io/) (for frontend deps and bundling)
- MongoDB-compatible database (auto-provisioned via `@push.rocks/smartmongo` / `LocalTsmDb`)
- MongoDB-compatible database (auto-provisioned via `@push.rocks/smartmongo`)
### Setup
@@ -74,7 +77,7 @@ Data is stored at `~/.serve.zone/gitops/`:
│ (HTTP/WS)│ (Providers) │ (24h background scan) │
├──────────┤ ├───────────────────────────┤
│ Handlers │ GiteaProvider│ CacheDb │
(9 total)│ GitLabProvider│ (LocalTsmDb + SmartdataDb)│
(12 total)│ GitLabProvider│ (SmartMongo + SmartdataDb)│
├──────────┴───────────────┴───────────────────────────┤
│ StorageManager │
│ (filesystem key-value store) │
@@ -87,7 +90,7 @@ Data is stored at `~/.serve.zone/gitops/`:
│ Frontend SPA │
│ Lit + dees-catalog + smartstate │
├──────────────────────────────────────────────────────┤
Dashboard │ 8 Views │ WebSocket Client │ Auto-Refresh│
│ Dashboard │ 11 Views │ WebSocket Client │ Auto-Refresh│
└──────────────────────────────────────────────────────┘
```
@@ -96,30 +99,33 @@ Data is stored at `~/.serve.zone/gitops/`:
- **`GitopsApp`** — Main orchestrator. Owns all subsystems, handles startup/shutdown lifecycle.
- **`ConnectionManager`** — CRUD for provider connections. Tokens secured in OS keychain. Background health checks on startup.
- **`BaseProvider`** → **`GiteaProvider`** / **`GitLabProvider`** — Unified interface over both APIs (projects, groups, secrets, pipelines, jobs, logs).
- **`OpsServer`** — TypedServer-based HTTP/WebSocket server with 9 handler modules:
- **`OpsServer`** — TypedServer-based HTTP/WebSocket server with 12 handler modules:
- `AdminHandler` — JWT-based auth (login/logout/verify)
- `ConnectionsHandler` — Connection CRUD + test
- `ProjectsHandler` / `GroupsHandler` — Browse repos and orgs
- `SecretsHandler` — Cache-first secret CRUD
- `PipelinesHandler` — Pipeline list/jobs/retry/cancel
- `ManagedSecretsHandler` — Managed secret definitions and push operations
- `PipelinesHandler` — Pipeline list/jobs/retry/cancel with filtering and aggregation
- `LogsHandler` — Job log fetch
- `WebhookHandler` — Custom HTTP route for incoming webhooks
- `ActionsHandler` — Force scan / scan status
- `ActionLogHandler` — Global audit trail queries
- `SyncHandler` — Repo sync configuration and status
- **`SecretsScanService`** — Background scanner with upsert-based deduplication. Runs on startup and every 24h.
- **`CacheDb`** — Embedded MongoDB via `LocalTsmDb` + `SmartdataDb`. TTL-based expiration with periodic cleanup.
- **`CacheDb`** — Embedded MongoDB via `SmartMongo` + `SmartdataDb`. TTL-based expiration with periodic cleanup.
- **`StorageManager`** — Filesystem-backed key-value store with atomic writes.
### Frontend (`ts_web/`)
- Built with [Lit](https://lit.dev/) web components and [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog) UI library
- Reactive state management via `smartstate` (4 state parts: login, connections, data, UI)
- 8 tabbed views: Overview, Connections, Projects, Groups, Secrets, Pipelines, Build Log, Actions
- Built with [Lit](https://lit.dev/) web components using TC39 standard decorators and [@design.estate/dees-catalog](https://code.foss.global/design.estate/dees-catalog) UI library
- Reactive state management via `smartstate` (login, connections, data, UI state parts)
- 11 tabbed views: Overview, Connections, Projects, Groups, Secrets, Managed Secrets, Pipelines, Build Log, Actions, Action Log, Sync
- WebSocket client for real-time webhook push notifications
- Bundled to `ts_bundled/bundle.ts` via `@git.zone/tsbundle` (base64-encoded, committed to git)
### Shared Types (`ts_interfaces/`)
- `data/` — Data models (`IProject`, `ISecret`, `IPipeline`, `IIdentity`, etc.)
- `data/` — Data models (`IProject`, `ISecret`, `IPipeline`, `IIdentity`, `IConnection`, `ISyncConfig`, `IManagedSecret`, `IActionLogEntry`, etc.)
- `requests/` — TypedRequest interfaces for all RPC endpoints
## 🔌 API
@@ -156,17 +162,33 @@ All endpoints use [TypedRequest](https://code.foss.global/api.global/typedreques
| `getAllSecrets` | Get all secrets for a connection+scope (cache-first) |
| `getSecrets` | Get secrets for a specific entity (cache-first) |
| `createSecret` / `updateSecret` / `deleteSecret` | Secret CRUD |
| `getPipelines` | List pipelines for a project |
| `getPipelines` | List pipelines for a project (with time-range filtering) |
| `getPipelineJobs` | List jobs for a pipeline |
| `retryPipeline` / `cancelPipeline` | Pipeline actions |
| `getJobLog` | Fetch raw build log for a job |
### Managed Secrets
| Method | Description |
|---|---|
| `getManagedSecrets` | List managed secret definitions |
| `createManagedSecret` / `updateManagedSecret` / `deleteManagedSecret` | Managed secret CRUD |
### Sync
| Method | Description |
|---|---|
| `getSyncConfigs` | List sync configurations |
| `createSyncConfig` / `updateSyncConfig` / `deleteSyncConfig` | Sync config CRUD |
| `getRepoSyncStatus` | Get sync status for repos |
### Actions
| Method | Description |
|---|---|
| `forceScanSecrets` | Trigger immediate full secrets scan |
| `getScanStatus` | Get scan status, last result, timestamp |
| `getActionLog` | Query global audit trail |
### Webhooks
@@ -200,16 +222,16 @@ gitops/
├── mod.ts # Entry point
├── deno.json # Deno config + import map
├── package.json # npm metadata + scripts
├── npmextra.json # tsbundle + tswatch config
├── .smartconfig.json # tsbundle + tswatch config
├── html/index.html # HTML shell
├── ts/ # Backend
│ ├── classes/ # GitopsApp, ConnectionManager
│ ├── classes/ # GitopsApp, ConnectionManager, SyncManager, ActionLog
│ ├── providers/ # BaseProvider, GiteaProvider, GitLabProvider
│ ├── storage/ # StorageManager
│ ├── cache/ # CacheDb, CacheCleaner, SecretsScanService
│ │ └── documents/ # CachedProject, CachedSecret
│ └── opsserver/ # OpsServer + 9 handlers
│ ├── handlers/ # AdminHandler, SecretsHandler, etc.
│ └── opsserver/ # OpsServer + 12 handlers
│ ├── handlers/ # AdminHandler, SecretsHandler, SyncHandler, etc.
│ └── helpers/ # Guards (JWT verification)
├── ts_interfaces/ # Shared TypeScript types
│ ├── data/ # IProject, ISecret, IPipeline, etc.
@@ -217,14 +239,14 @@ gitops/
├── ts_web/ # Frontend SPA
│ ├── appstate.ts # Smartstate store + actions
│ └── elements/ # Lit web components
│ └── views/ # 8 view components
│ └── views/ # 11 view components
├── ts_bundled/bundle.ts # Embedded frontend (base64, committed)
└── test/ # Deno tests
```
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -1,4 +1,4 @@
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
import { assertEquals, assertExists } from '@std/assert';
import { BaseProvider, GiteaProvider, GitLabProvider } from '../ts/providers/index.ts';
import { ConnectionManager } from '../ts/classes/connectionmanager.ts';
import { GitopsApp } from '../ts/classes/gitopsapp.ts';

View File

@@ -1,4 +1,4 @@
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
import { assertEquals, assertExists } from '@std/assert';
import { StorageManager } from '../ts/storage/index.ts';
import * as smartsecret from '@push.rocks/smartsecret';

View File

@@ -1,22 +1,20 @@
import { assertEquals, assertExists } from 'https://deno.land/std@0.208.0/assert/mod.ts';
import { LocalTsmDb } from '@push.rocks/smartmongo';
import { assertEquals, assertExists } from '@std/assert';
import { SmartMongo } from '@push.rocks/smartmongo';
import { SmartdataDb, SmartDataDbDoc, Collection, svDb, unI } from '@push.rocks/smartdata';
Deno.test({
name: 'TsmDb spike: LocalTsmDb + SmartdataDb roundtrip',
name: 'TsmDb spike: SmartMongo + SmartdataDb roundtrip',
sanitizeOps: false,
sanitizeResources: false,
fn: async () => {
const tmpDir = await Deno.makeTempDir();
// 1. Start local MongoDB-compatible server
const localDb = new LocalTsmDb({ folderPath: tmpDir });
const { connectionUri } = await localDb.start();
assertExists(connectionUri);
const mongo = await SmartMongo.createAndStart();
const mongoDescriptor = await mongo.getMongoDescriptor();
assertExists(mongoDescriptor.mongoDbUrl);
// 2. Connect smartdata
const smartDb = new SmartdataDb({
mongoDbUrl: connectionUri,
mongoDbUrl: mongoDescriptor.mongoDbUrl,
mongoDbName: 'gitops_spike_test',
});
await smartDb.init();
@@ -52,8 +50,8 @@ Deno.test({
assertEquals(found.label, 'spike');
assertEquals(found.value, 42);
// 6. Cleanup — smartDb closes; localDb.stop() hangs under Deno, so fire-and-forget
// 6. Cleanup — smartDb closes; mongo.stop() may hang under Deno, so fire-and-forget
await smartDb.close();
localDb.stop().catch(() => {});
mongo.stop().catch(() => {});
},
});

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/gitops',
version: '2.12.0',
version: '2.13.0',
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
}

View File

@@ -14,7 +14,7 @@ export interface ICacheDbOptions {
export class CacheDb {
private static instance: CacheDb | null = null;
private localTsmDb: InstanceType<typeof plugins.smartmongo.LocalTsmDb> | null = null;
private smartMongo: InstanceType<typeof plugins.smartmongo.SmartMongo> | null = null;
private smartdataDb: InstanceType<typeof plugins.smartdata.SmartdataDb> | null = null;
private options: Required<ICacheDbOptions>;
@@ -39,13 +39,11 @@ export class CacheDb {
async start(): Promise<void> {
logger.info('Starting CacheDb...');
this.localTsmDb = new plugins.smartmongo.LocalTsmDb({
folderPath: this.options.storagePath,
});
const { connectionUri } = await this.localTsmDb.start();
this.smartMongo = await plugins.smartmongo.SmartMongo.createAndStart();
const mongoDescriptor = await this.smartMongo.getMongoDescriptor();
this.smartdataDb = new plugins.smartdata.SmartdataDb({
mongoDbUrl: connectionUri,
mongoDbUrl: mongoDescriptor.mongoDbUrl,
mongoDbName: this.options.dbName,
});
await this.smartdataDb.init();
@@ -58,9 +56,9 @@ export class CacheDb {
await this.smartdataDb.close();
this.smartdataDb = null;
}
if (this.localTsmDb) {
// localDb.stop() may hang under Deno — fire-and-forget with timeout
const stopPromise = this.localTsmDb.stop().catch(() => {});
if (this.smartMongo) {
// smartMongo.stop() may hang under Deno — fire-and-forget with timeout
const stopPromise = this.smartMongo.stop().catch(() => {});
await Promise.race([
stopPromise,
new Promise<void>((resolve) => {
@@ -68,7 +66,7 @@ export class CacheDb {
Deno.unrefTimer(id);
}),
]);
this.localTsmDb = null;
this.smartMongo = null;
}
logger.success('CacheDb stopped');
}

View File

@@ -230,7 +230,7 @@ export class ConnectionManager {
try {
if (conn.providerType === 'gitlab') {
const gitlabClient = new plugins.gitlabClient.GitLabClient(conn.baseUrl, conn.token);
const group = await gitlabClient.getGroupByPath(conn.groupFilter);
const group = await gitlabClient.getGroup(conn.groupFilter);
conn.groupFilterId = String(group.id);
logger.info(`Resolved group filter "${conn.groupFilter}" to ID ${conn.groupFilterId}`);
} else {

View File

@@ -522,7 +522,7 @@ export class SyncManager {
for (const segment of groupSegments) {
currentPath = currentPath ? `${currentPath}/${segment}` : segment;
try {
const group = await client.getGroupByPath(currentPath);
const group = await client.getGroup(currentPath);
parentId = group.id;
} catch {
// Group doesn't exist — create it
@@ -533,7 +533,7 @@ export class SyncManager {
} catch (createErr: any) {
// 409 = already exists (race condition), try fetching again
if (String(createErr).includes('409') || String(createErr).includes('already')) {
const group = await client.getGroupByPath(currentPath);
const group = await client.getGroup(currentPath);
parentId = group.id;
} else {
throw createErr;
@@ -558,7 +558,7 @@ export class SyncManager {
try {
// Check if project exists by path
await client.getGroupByPath(projectPath);
await client.getGroup(projectPath);
// If this succeeds, it's actually a group, not a project... unlikely but handle
} catch {
// Project doesn't exist as a group path; try creating it
@@ -1107,7 +1107,7 @@ export class SyncManager {
if (!targetProject) return;
const client = new plugins.gitlabClient.GitLabClient(targetConn.baseUrl, targetConn.token);
const protectedBranches = await client.getProtectedBranches(targetProject.id);
const protectedBranches = await client.requestGetProtectedBranches(targetProject.id);
if (protectedBranches.length === 0) return;
// Get list of branches in the local mirror (= source branches)
@@ -1119,7 +1119,7 @@ export class SyncManager {
for (const pb of protectedBranches) {
if (!localBranches.has(pb.name)) {
logger.syncLog('info', `Unprotecting stale branch "${pb.name}" on ${targetFullPath}`, 'api');
await client.unprotectBranch(targetProject.id, pb.name);
await client.requestUnprotectBranch(targetProject.id, pb.name);
}
}
} catch (err) {
@@ -1666,14 +1666,14 @@ export class SyncManager {
// Walk the basePath to find the parent group, then create "obsolete" subgroup
let parentId: number | undefined;
if (basePath) {
const parentGroup = await client.getGroupByPath(basePath);
const parentGroup = await client.getGroup(basePath);
parentId = parentGroup.id;
}
// Try to get existing obsolete group
const obsoletePath = basePath ? `${basePath}/obsolete` : 'obsolete';
try {
const group = await client.getGroupByPath(obsoletePath);
const group = await client.getGroup(obsoletePath);
return { type: 'gitlab', groupId: group.id };
} catch {
// Doesn't exist — create it
@@ -1684,7 +1684,7 @@ export class SyncManager {
return { type: 'gitlab', groupId: newGroup.id };
} catch (createErr: any) {
if (String(createErr).includes('409') || String(createErr).includes('already')) {
const group = await client.getGroupByPath(obsoletePath);
const group = await client.getGroup(obsoletePath);
return { type: 'gitlab', groupId: group.id };
}
throw createErr;

View File

@@ -144,43 +144,27 @@ export class PipelinesHandler {
/**
* Current mode: running/pending always shown, plus recent pipelines within timeRange.
* Makes two parallel aggregation passes to ensure active pipelines are never missed:
* 1. Recent pipelines (no status filter) — for time-range display
* 2. Active pipelines (status: 'running') — guarantees we catch all running ones
* Fetches a generous page of recent pipelines per project (100), then splits into
* active (running/pending/waiting) and the rest. Active are always shown; the rest
* is filtered by timeRange.
*/
private async fetchCurrentPipelines(
provider: BaseProvider,
timeRange: string,
): Promise<interfaces.data.IPipeline[]> {
const projects = await provider.getProjects();
// Two parallel fetches: recent + explicitly active
const [recentPipelines, activePipelines] = await Promise.all([
this.fetchAggregatedPipelines(provider, projects, { perPage: 50 }),
this.fetchAggregatedPipelines(provider, projects, { status: 'running', perPage: 50 }),
]);
// Merge and deduplicate (active first so they take precedence)
const seenIds = new Set<string>();
const merged: interfaces.data.IPipeline[] = [];
for (const p of [...activePipelines, ...recentPipelines]) {
const key = `${p.connectionId}:${p.projectId}:${p.id}`;
if (!seenIds.has(key)) {
seenIds.add(key);
merged.push(p);
}
}
const allPipelines = await this.fetchAggregatedPipelines(provider, projects, { perPage: 100 });
// Running/pending pipelines are always shown regardless of time
const active = merged.filter(
const active = allPipelines.filter(
(p) => p.status === 'running' || p.status === 'pending' || p.status === 'waiting',
);
const rest = merged.filter(
const rest = allPipelines.filter(
(p) => p.status !== 'running' && p.status !== 'pending' && p.status !== 'waiting',
);
const filteredRest = this.filterByTimeRange(rest, timeRange);
// Final dedup (active pipelines may also appear in filtered rest)
// Deduplicate (active pipelines may also appear in filtered rest)
const activeIds = new Set(active.map((p) => `${p.connectionId}:${p.projectId}:${p.id}`));
const uniqueRest = filteredRest.filter(
(p) => !activeIds.has(`${p.connectionId}:${p.projectId}:${p.id}`),

View File

@@ -18,115 +18,46 @@ export class GiteaProvider extends BaseProvider {
}
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
// Use org-scoped listing when groupFilterId is set
const fetchFn = this.groupFilterId
? (o: IListOptions) => this.client.getOrgRepos(this.groupFilterId!, o)
: (o: IListOptions) => this.client.getRepos(o);
// If caller explicitly requests a specific page, respect it (no auto-pagination)
if (opts?.page) {
const repos = await fetchFn(opts);
return repos.map((r) => this.mapProject(r));
}
const allRepos: plugins.giteaClient.IGiteaRepository[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const repos = await fetchFn({ ...opts, page, perPage });
allRepos.push(...repos);
if (repos.length < perPage) break;
page++;
}
return allRepos.map((r) => this.mapProject(r));
const repos = this.groupFilterId
? await (await this.client.getOrg(this.groupFilterId)).getRepos(opts)
: await this.client.getRepos(opts);
return repos.map((r) => this.mapProject(r));
}
async getGroups(opts?: IListOptions): Promise<interfaces.data.IGroup[]> {
// When groupFilterId is set, return only that single org
if (this.groupFilterId) {
const org = await this.client.getOrg(this.groupFilterId);
return [this.mapGroup(org)];
}
// If caller explicitly requests a specific page, respect it (no auto-pagination)
if (opts?.page) {
const orgs = await this.client.getOrgs(opts);
return orgs.map((o) => this.mapGroup(o));
}
const allOrgs: plugins.giteaClient.IGiteaOrganization[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const orgs = await this.client.getOrgs({ ...opts, page, perPage });
allOrgs.push(...orgs);
if (orgs.length < perPage) break;
page++;
}
return allOrgs.map((o) => this.mapGroup(o));
const orgs = await this.client.getOrgs(opts);
return orgs.map((o) => this.mapGroup(o));
}
async getGroupProjects(groupId: string, opts?: IListOptions): Promise<interfaces.data.IProject[]> {
if (opts?.page) {
const repos = await this.client.getOrgRepos(groupId, opts);
return repos.map((r) => this.mapProject(r));
}
const allRepos: plugins.giteaClient.IGiteaRepository[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const repos = await this.client.getOrgRepos(groupId, { ...opts, page, perPage });
allRepos.push(...repos);
if (repos.length < perPage) break;
page++;
}
return allRepos.map((r) => this.mapProject(r));
const org = await this.client.getOrg(groupId);
const repos = await org.getRepos(opts);
return repos.map((r) => this.mapProject(r));
}
// --- Branches / Tags ---
async getBranches(projectFullPath: string, opts?: IListOptions): Promise<interfaces.data.IBranch[]> {
if (opts?.page) {
const branches = await this.client.getRepoBranches(projectFullPath, opts);
return branches.map((b) => ({ name: b.name, commitSha: b.commit.id }));
}
const all: interfaces.data.IBranch[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const branches = await this.client.getRepoBranches(projectFullPath, { ...opts, page, perPage });
all.push(...branches.map((b) => ({ name: b.name, commitSha: b.commit.id })));
if (branches.length < perPage) break;
page++;
}
return all;
const repo = await this.client.getRepo(projectFullPath);
const branches = await repo.getBranches(opts);
return branches.map((b) => ({ name: b.name, commitSha: b.commitSha }));
}
async getTags(projectFullPath: string, opts?: IListOptions): Promise<interfaces.data.ITag[]> {
if (opts?.page) {
const tags = await this.client.getRepoTags(projectFullPath, opts);
return tags.map((t) => ({ name: t.name, commitSha: t.commit.sha }));
}
const all: interfaces.data.ITag[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const tags = await this.client.getRepoTags(projectFullPath, { ...opts, page, perPage });
all.push(...tags.map((t) => ({ name: t.name, commitSha: t.commit.sha })));
if (tags.length < perPage) break;
page++;
}
return all;
const repo = await this.client.getRepo(projectFullPath);
const tags = await repo.getTags(opts);
return tags.map((t) => ({ name: t.name, commitSha: t.commitSha }));
}
// --- Project Secrets ---
async getProjectSecrets(projectId: string): Promise<interfaces.data.ISecret[]> {
const secrets = await this.client.getRepoSecrets(projectId);
const repo = await this.client.getRepo(projectId);
const secrets = await repo.getSecrets();
return secrets.map((s) => this.mapSecret(s, 'project', projectId));
}
@@ -135,7 +66,8 @@ export class GiteaProvider extends BaseProvider {
key: string,
value: string,
): Promise<interfaces.data.ISecret> {
await this.client.setRepoSecret(projectId, key, value);
const repo = await this.client.getRepo(projectId);
await repo.setSecret(key, value);
return { key, value: '***', protected: false, masked: true, scope: 'project', scopeId: projectId, scopeName: projectId, connectionId: this.connectionId, environment: '*' };
}
@@ -148,13 +80,15 @@ export class GiteaProvider extends BaseProvider {
}
async deleteProjectSecret(projectId: string, key: string): Promise<void> {
await this.client.deleteRepoSecret(projectId, key);
const repo = await this.client.getRepo(projectId);
await repo.deleteSecret(key);
}
// --- Group Secrets ---
async getGroupSecrets(groupId: string): Promise<interfaces.data.ISecret[]> {
const secrets = await this.client.getOrgSecrets(groupId);
const org = await this.client.getOrg(groupId);
const secrets = await org.getSecrets();
return secrets.map((s) => this.mapSecret(s, 'group', groupId));
}
@@ -163,7 +97,8 @@ export class GiteaProvider extends BaseProvider {
key: string,
value: string,
): Promise<interfaces.data.ISecret> {
await this.client.setOrgSecret(groupId, key, value);
const org = await this.client.getOrg(groupId);
await org.setSecret(key, value);
return { key, value: '***', protected: false, masked: true, scope: 'group', scopeId: groupId, scopeName: groupId, connectionId: this.connectionId, environment: '*' };
}
@@ -176,7 +111,8 @@ export class GiteaProvider extends BaseProvider {
}
async deleteGroupSecret(groupId: string, key: string): Promise<void> {
await this.client.deleteOrgSecret(groupId, key);
const org = await this.client.getOrg(groupId);
await org.deleteSecret(key);
}
// --- Pipelines (Action Runs) ---
@@ -185,7 +121,8 @@ export class GiteaProvider extends BaseProvider {
projectId: string,
opts?: IPipelineListOptions,
): Promise<interfaces.data.IPipeline[]> {
const runs = await this.client.getActionRuns(projectId, {
const repo = await this.client.getRepo(projectId);
const runs = await repo.getActionRuns({
page: opts?.page,
perPage: opts?.perPage,
status: opts?.status,
@@ -199,90 +136,101 @@ export class GiteaProvider extends BaseProvider {
projectId: string,
pipelineId: string,
): Promise<interfaces.data.IPipelineJob[]> {
const jobs = await this.client.getActionRunJobs(projectId, Number(pipelineId));
return jobs.map((j) => this.mapJob(j, pipelineId));
// Use the client's internal method directly to avoid an extra getRepo call
const jobs = await this.client.requestGetActionRunJobs(projectId, Number(pipelineId));
return jobs.map((j) => {
const resolvedStatus = plugins.giteaClient.resolveGiteaStatus(j.status, j.conclusion);
return this.mapJob(resolvedStatus, j, pipelineId);
});
}
async getJobLog(projectId: string, jobId: string): Promise<string> {
return this.client.getJobLog(projectId, Number(jobId));
return this.client.requestGetJobLog(projectId, Number(jobId));
}
async retryPipeline(projectId: string, pipelineId: string): Promise<void> {
await this.client.rerunAction(projectId, Number(pipelineId));
// Fetch the run to get its workflow path, then dispatch
const run = await this.client.requestGetActionRun(projectId, Number(pipelineId));
const wfId = plugins.giteaClient.extractWorkflowIdFromPath(run.path);
const ref = run.head_branch || 'main';
if (!wfId) {
throw new Error(`Cannot retry: no workflow ID found in path "${run.path}"`);
}
await this.client.requestDispatchWorkflow(projectId, wfId, ref);
}
async cancelPipeline(projectId: string, pipelineId: string): Promise<void> {
await this.client.cancelAction(projectId, Number(pipelineId));
async cancelPipeline(_projectId: string, _pipelineId: string): Promise<void> {
throw new Error('Cancel is not supported by Gitea 1.25');
}
// --- Mappers ---
private mapProject(r: plugins.giteaClient.IGiteaRepository): interfaces.data.IProject {
private mapProject(r: plugins.giteaClient.GiteaRepository): interfaces.data.IProject {
return {
id: r.full_name || String(r.id),
name: r.name || '',
fullPath: r.full_name || '',
description: r.description || '',
defaultBranch: r.default_branch || 'main',
webUrl: r.html_url || '',
id: r.fullName || String(r.id),
name: r.name,
fullPath: r.fullName,
description: r.description,
defaultBranch: r.defaultBranch,
webUrl: r.htmlUrl,
connectionId: this.connectionId,
visibility: r.private ? 'private' : 'public',
topics: r.topics || [],
lastActivity: r.updated_at || '',
visibility: r.isPrivate ? 'private' : 'public',
topics: r.topics,
lastActivity: r.updatedAt,
};
}
private mapGroup(o: plugins.giteaClient.IGiteaOrganization): interfaces.data.IGroup {
private mapGroup(o: plugins.giteaClient.GiteaOrganization): interfaces.data.IGroup {
return {
id: o.name || String(o.id),
name: o.name || '',
fullPath: o.name || '',
description: o.description || '',
name: o.name,
fullPath: o.name,
description: o.description,
webUrl: `${this.baseUrl}/${o.name}`,
connectionId: this.connectionId,
visibility: o.visibility || 'public',
projectCount: o.repo_count || 0,
projectCount: o.repoCount,
};
}
private mapSecret(s: plugins.giteaClient.IGiteaSecret, scope: 'project' | 'group', scopeId: string, scopeName?: string): interfaces.data.ISecret {
private mapSecret(s: plugins.giteaClient.GiteaSecret, scope: 'project' | 'group', scopeId: string): interfaces.data.ISecret {
return {
key: s.name || '',
key: s.name,
value: '***',
protected: false,
masked: true,
scope,
scopeId,
scopeName: scopeName || scopeId,
scopeName: scopeId,
connectionId: this.connectionId,
environment: '*',
};
}
private mapPipeline(r: plugins.giteaClient.IGiteaActionRun, projectId: string, projectName?: string): interfaces.data.IPipeline {
private mapPipeline(r: plugins.giteaClient.GiteaActionRun, projectId: string): interfaces.data.IPipeline {
return {
id: String(r.id),
projectId,
projectName: projectName || projectId,
projectName: projectId,
connectionId: this.connectionId,
status: this.mapStatus(r.status || r.conclusion),
ref: r.head_branch || '',
sha: r.head_sha || '',
webUrl: r.html_url || '',
duration: r.run_duration || 0,
createdAt: r.created_at || '',
status: this.mapStatus(r.resolvedStatus),
ref: r.ref,
sha: r.headSha,
webUrl: r.htmlUrl,
duration: r.duration,
createdAt: r.startedAt,
source: r.event || 'push',
};
}
private mapJob(j: plugins.giteaClient.IGiteaActionRunJob, pipelineId: string): interfaces.data.IPipelineJob {
private mapJob(resolvedStatus: string, j: plugins.giteaClient.IGiteaActionRunJob, pipelineId: string): interfaces.data.IPipelineJob {
return {
id: String(j.id),
pipelineId,
name: j.name || '',
stage: j.name || 'default',
status: this.mapStatus(j.status || j.conclusion),
duration: j.run_duration || 0,
status: this.mapStatus(resolvedStatus),
duration: plugins.giteaClient.computeDuration(j.started_at, j.completed_at),
};
}

View File

@@ -18,130 +18,47 @@ export class GitLabProvider extends BaseProvider {
}
async getProjects(opts?: IListOptions): Promise<interfaces.data.IProject[]> {
if (this.groupFilterId) {
// Auto-paginate group-scoped project listing
if (opts?.page) {
const projects = await this.client.getGroupProjects(this.groupFilterId, opts);
return projects.map((p) => this.mapProject(p));
}
const allProjects: plugins.gitlabClient.IGitLabProject[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const projects = await this.client.getGroupProjects(this.groupFilterId, { ...opts, page, perPage });
allProjects.push(...projects);
if (projects.length < perPage) break;
page++;
}
return allProjects.map((p) => this.mapProject(p));
}
if (opts?.page) {
const projects = await this.client.getProjects(opts);
return projects.map((p) => this.mapProject(p));
}
const allProjects: plugins.gitlabClient.IGitLabProject[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const projects = await this.client.getProjects({ ...opts, page, perPage });
allProjects.push(...projects);
if (projects.length < perPage) break;
page++;
}
return allProjects.map((p) => this.mapProject(p));
const projects = this.groupFilterId
? await (await this.client.getGroup(this.groupFilterId)).getProjects(opts)
: await this.client.getProjects(opts);
return projects.map((p) => this.mapProject(p));
}
async getGroups(opts?: IListOptions): Promise<interfaces.data.IGroup[]> {
if (this.groupFilterId) {
// Auto-paginate descendant groups listing
if (opts?.page) {
const groups = await this.client.getDescendantGroups(this.groupFilterId, opts);
return groups.map((g) => this.mapGroup(g));
}
const allGroups: plugins.gitlabClient.IGitLabGroup[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const groups = await this.client.getDescendantGroups(this.groupFilterId, { ...opts, page, perPage });
allGroups.push(...groups);
if (groups.length < perPage) break;
page++;
}
return allGroups.map((g) => this.mapGroup(g));
const group = await this.client.getGroup(this.groupFilterId);
const descendants = await group.getDescendantGroups(opts);
return descendants.map((g) => this.mapGroup(g));
}
if (opts?.page) {
const groups = await this.client.getGroups(opts);
return groups.map((g) => this.mapGroup(g));
}
const allGroups: plugins.gitlabClient.IGitLabGroup[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const groups = await this.client.getGroups({ ...opts, page, perPage });
allGroups.push(...groups);
if (groups.length < perPage) break;
page++;
}
return allGroups.map((g) => this.mapGroup(g));
const groups = await this.client.getGroups(opts);
return groups.map((g) => this.mapGroup(g));
}
async getGroupProjects(groupId: string, opts?: IListOptions): Promise<interfaces.data.IProject[]> {
if (opts?.page) {
const projects = await this.client.getGroupProjects(groupId, opts);
return projects.map((p) => this.mapProject(p));
}
const allProjects: plugins.gitlabClient.IGitLabProject[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const projects = await this.client.getGroupProjects(groupId, { ...opts, page, perPage });
allProjects.push(...projects);
if (projects.length < perPage) break;
page++;
}
return allProjects.map((p) => this.mapProject(p));
const group = await this.client.getGroup(groupId);
const projects = await group.getProjects(opts);
return projects.map((p) => this.mapProject(p));
}
// --- Branches / Tags ---
async getBranches(projectFullPath: string, opts?: IListOptions): Promise<interfaces.data.IBranch[]> {
if (opts?.page) {
const branches = await this.client.getRepoBranches(projectFullPath, opts);
return branches.map((b) => ({ name: b.name, commitSha: b.commit.id }));
}
const all: interfaces.data.IBranch[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const branches = await this.client.getRepoBranches(projectFullPath, { ...opts, page, perPage });
all.push(...branches.map((b) => ({ name: b.name, commitSha: b.commit.id })));
if (branches.length < perPage) break;
page++;
}
return all;
const project = await this.client.getProject(projectFullPath);
const branches = await project.getBranches(opts);
return branches.map((b) => ({ name: b.name, commitSha: b.commitSha }));
}
async getTags(projectFullPath: string, opts?: IListOptions): Promise<interfaces.data.ITag[]> {
if (opts?.page) {
const tags = await this.client.getRepoTags(projectFullPath, opts);
return tags.map((t) => ({ name: t.name, commitSha: t.commit.id }));
}
const all: interfaces.data.ITag[] = [];
const perPage = opts?.perPage || 50;
let page = 1;
while (true) {
const tags = await this.client.getRepoTags(projectFullPath, { ...opts, page, perPage });
all.push(...tags.map((t) => ({ name: t.name, commitSha: t.commit.id })));
if (tags.length < perPage) break;
page++;
}
return all;
const project = await this.client.getProject(projectFullPath);
const tags = await project.getTags(opts);
return tags.map((t) => ({ name: t.name, commitSha: t.commitSha }));
}
// --- Project Secrets (CI/CD Variables) ---
async getProjectSecrets(projectId: string): Promise<interfaces.data.ISecret[]> {
const vars = await this.client.getProjectVariables(projectId);
const project = await this.client.getProject(projectId);
const vars = await project.getVariables();
return vars.map((v) => this.mapVariable(v, 'project', projectId));
}
@@ -150,7 +67,8 @@ export class GitLabProvider extends BaseProvider {
key: string,
value: string,
): Promise<interfaces.data.ISecret> {
const v = await this.client.createProjectVariable(projectId, key, value);
const project = await this.client.getProject(projectId);
const v = await project.createVariable(key, value);
return this.mapVariable(v, 'project', projectId);
}
@@ -159,18 +77,21 @@ export class GitLabProvider extends BaseProvider {
key: string,
value: string,
): Promise<interfaces.data.ISecret> {
const v = await this.client.updateProjectVariable(projectId, key, value);
const project = await this.client.getProject(projectId);
const v = await project.updateVariable(key, value);
return this.mapVariable(v, 'project', projectId);
}
async deleteProjectSecret(projectId: string, key: string): Promise<void> {
await this.client.deleteProjectVariable(projectId, key);
const project = await this.client.getProject(projectId);
await project.deleteVariable(key);
}
// --- Group Secrets (CI/CD Variables) ---
async getGroupSecrets(groupId: string): Promise<interfaces.data.ISecret[]> {
const vars = await this.client.getGroupVariables(groupId);
const group = await this.client.getGroup(groupId);
const vars = await group.getVariables();
return vars.map((v) => this.mapVariable(v, 'group', groupId));
}
@@ -179,7 +100,8 @@ export class GitLabProvider extends BaseProvider {
key: string,
value: string,
): Promise<interfaces.data.ISecret> {
const v = await this.client.createGroupVariable(groupId, key, value);
const group = await this.client.getGroup(groupId);
const v = await group.createVariable(key, value);
return this.mapVariable(v, 'group', groupId);
}
@@ -188,12 +110,14 @@ export class GitLabProvider extends BaseProvider {
key: string,
value: string,
): Promise<interfaces.data.ISecret> {
const v = await this.client.updateGroupVariable(groupId, key, value);
const group = await this.client.getGroup(groupId);
const v = await group.updateVariable(key, value);
return this.mapVariable(v, 'group', groupId);
}
async deleteGroupSecret(groupId: string, key: string): Promise<void> {
await this.client.deleteGroupVariable(groupId, key);
const group = await this.client.getGroup(groupId);
await group.deleteVariable(key);
}
// --- Pipelines ---
@@ -202,7 +126,8 @@ export class GitLabProvider extends BaseProvider {
projectId: string,
opts?: IPipelineListOptions,
): Promise<interfaces.data.IPipeline[]> {
const pipelines = await this.client.getPipelines(projectId, {
const project = await this.client.getProject(projectId);
const pipelines = await project.getPipelines({
page: opts?.page,
perPage: opts?.perPage,
status: opts?.status,
@@ -216,83 +141,82 @@ export class GitLabProvider extends BaseProvider {
projectId: string,
pipelineId: string,
): Promise<interfaces.data.IPipelineJob[]> {
const jobs = await this.client.getPipelineJobs(projectId, Number(pipelineId));
const jobs = await this.client.requestGetPipelineJobs(projectId, Number(pipelineId));
return jobs.map((j) => this.mapJob(j, pipelineId));
}
async getJobLog(projectId: string, jobId: string): Promise<string> {
return this.client.getJobLog(projectId, Number(jobId));
return this.client.requestGetJobLog(projectId, Number(jobId));
}
async retryPipeline(projectId: string, pipelineId: string): Promise<void> {
await this.client.retryPipeline(projectId, Number(pipelineId));
await this.client.requestRetryPipeline(projectId, Number(pipelineId));
}
async cancelPipeline(projectId: string, pipelineId: string): Promise<void> {
await this.client.cancelPipeline(projectId, Number(pipelineId));
await this.client.requestCancelPipeline(projectId, Number(pipelineId));
}
// --- Mappers ---
private mapProject(p: plugins.gitlabClient.IGitLabProject): interfaces.data.IProject {
private mapProject(p: plugins.gitlabClient.GitLabProject): interfaces.data.IProject {
return {
id: String(p.id),
name: p.name || '',
fullPath: p.path_with_namespace || '',
description: p.description || '',
defaultBranch: p.default_branch || 'main',
webUrl: p.web_url || '',
name: p.name,
fullPath: p.fullPath,
description: p.description,
defaultBranch: p.defaultBranch,
webUrl: p.webUrl,
connectionId: this.connectionId,
visibility: p.visibility || 'private',
topics: p.topics || [],
lastActivity: p.last_activity_at || '',
visibility: p.visibility,
topics: p.topics,
lastActivity: p.lastActivityAt,
};
}
private mapGroup(g: plugins.gitlabClient.IGitLabGroup): interfaces.data.IGroup {
private mapGroup(g: plugins.gitlabClient.GitLabGroup): interfaces.data.IGroup {
return {
id: String(g.id),
name: g.name || '',
fullPath: g.full_path || '',
description: g.description || '',
webUrl: g.web_url || '',
name: g.name,
fullPath: g.fullPath,
description: g.description,
webUrl: g.webUrl,
connectionId: this.connectionId,
visibility: g.visibility || 'private',
visibility: g.visibility,
projectCount: 0,
};
}
private mapVariable(
v: plugins.gitlabClient.IGitLabVariable,
v: plugins.gitlabClient.GitLabVariable,
scope: 'project' | 'group',
scopeId: string,
scopeName?: string,
): interfaces.data.ISecret {
return {
key: v.key || '',
key: v.key,
value: v.value || '***',
protected: v.protected || false,
masked: v.masked || false,
protected: v.protected,
masked: v.masked,
scope,
scopeId,
scopeName: scopeName || scopeId,
scopeName: scopeId,
connectionId: this.connectionId,
environment: v.environment_scope || '*',
environment: v.environmentScope,
};
}
private mapPipeline(p: plugins.gitlabClient.IGitLabPipeline, projectId: string, projectName?: string): interfaces.data.IPipeline {
private mapPipeline(p: plugins.gitlabClient.GitLabPipeline, projectId: string): interfaces.data.IPipeline {
return {
id: String(p.id),
projectId,
projectName: projectName || projectId,
projectName: projectId,
connectionId: this.connectionId,
status: (p.status || 'pending') as interfaces.data.TPipelineStatus,
ref: p.ref || '',
sha: p.sha || '',
webUrl: p.web_url || '',
duration: p.duration || 0,
createdAt: p.created_at || '',
ref: p.ref,
sha: p.sha,
webUrl: p.webUrl,
duration: p.duration,
createdAt: p.createdAt,
source: p.source || 'push',
};
}

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/gitops',
version: '2.12.0',
version: '2.13.0',
description: 'GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs'
}