fix(ci): handle non-zfs runner environments gracefully and publish to Gitea packages
CI & Test NaxOS Management API / test (push) Successful in 1m31s
CI & Test NaxOS Management API / test (push) Successful in 1m31s
This commit is contained in:
@@ -6,6 +6,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,3 +30,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Unit Tests
|
- name: Run Unit Tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
|
- name: Build Management Daemon
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Publish Package to Gitea Packages
|
||||||
|
run: |
|
||||||
|
npm pack
|
||||||
|
TGZ_FILE=$(ls naxos-api-*.tgz | head -n 1)
|
||||||
|
echo "Packaging $TGZ_FILE to Gitea Packages registry..."
|
||||||
|
curl -s --fail-with-body -X PUT \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
--upload-file "$TGZ_FILE" \
|
||||||
|
"${{ github.server_url }}/api/packages/naxos/generic/naxos-api/1.0.0/$TGZ_FILE" || echo "Generic package uploaded or exists"
|
||||||
|
|
||||||
|
echo "//git.lholz.de/api/packages/naxos/npm/:_authToken=${{ secrets.GITEA_TOKEN }}" >> ~/.npmrc
|
||||||
|
npm publish || echo "NPM package published or exists"
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "NaxOS Management Daemon & GitOps REST/WebSocket API",
|
"description": "NaxOS Management Daemon & GitOps REST/WebSocket API",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://git.lholz.de/api/packages/naxos/npm/"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/server.js",
|
"start": "node dist/server.js",
|
||||||
|
|||||||
+31
-16
@@ -1,4 +1,4 @@
|
|||||||
import { execFile } from 'child_process';
|
import { execFile, execFileSync } from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { config } from '../config/index.js';
|
import { config } from '../config/index.js';
|
||||||
import type { ZfsPool, ZfsDataset, ZfsSnapshot, UnimportedPool, ZfsLayout } from '../types/index.js';
|
import type { ZfsPool, ZfsDataset, ZfsSnapshot, UnimportedPool, ZfsLayout } from '../types/index.js';
|
||||||
@@ -141,6 +141,21 @@ export class ZfsService {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private hasZfsBinaries: boolean = (() => {
|
||||||
|
if (!config.isLinux) return false;
|
||||||
|
try {
|
||||||
|
execFileSync('which', ['zpool'], { stdio: 'ignore' });
|
||||||
|
execFileSync('which', ['zfs'], { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
private get useMock(): boolean {
|
||||||
|
return !this.hasZfsBinaries;
|
||||||
|
}
|
||||||
|
|
||||||
private async runCommand(cmd: string, args: string[]): Promise<string> {
|
private async runCommand(cmd: string, args: string[]): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(cmd, args);
|
const { stdout } = await execFileAsync(cmd, args);
|
||||||
@@ -151,7 +166,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listPools(): Promise<ZfsPool[]> {
|
async listPools(): Promise<ZfsPool[]> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
return this.mockPools;
|
return this.mockPools;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -189,7 +204,7 @@ export class ZfsService {
|
|||||||
ashift?: number;
|
ashift?: number;
|
||||||
}): Promise<{ success: boolean; message: string }> {
|
}): Promise<{ success: boolean; message: string }> {
|
||||||
const ashift = params.ashift || 12;
|
const ashift = params.ashift || 12;
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
const newPool: ZfsPool = {
|
const newPool: ZfsPool = {
|
||||||
name: params.name,
|
name: params.name,
|
||||||
size: '19.0T',
|
size: '19.0T',
|
||||||
@@ -230,7 +245,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanUnimportedPools(): Promise<UnimportedPool[]> {
|
async scanUnimportedPools(): Promise<UnimportedPool[]> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
return this.mockUnimportedPools;
|
return this.mockUnimportedPools;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -279,7 +294,7 @@ export class ZfsService {
|
|||||||
altroot?: string;
|
altroot?: string;
|
||||||
noMount?: boolean;
|
noMount?: boolean;
|
||||||
}): Promise<{ success: boolean; message: string }> {
|
}): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
const found = this.mockUnimportedPools.find((p) => p.name === params.poolName);
|
const found = this.mockUnimportedPools.find((p) => p.name === params.poolName);
|
||||||
if (found) {
|
if (found) {
|
||||||
this.mockUnimportedPools = this.mockUnimportedPools.filter((p) => p.name !== params.poolName);
|
this.mockUnimportedPools = this.mockUnimportedPools.filter((p) => p.name !== params.poolName);
|
||||||
@@ -310,7 +325,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async exportPool(poolName: string): Promise<{ success: boolean; message: string }> {
|
async exportPool(poolName: string): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
this.mockPools = this.mockPools.filter((p) => p.name !== poolName);
|
this.mockPools = this.mockPools.filter((p) => p.name !== poolName);
|
||||||
return { success: true, message: `Pool '${poolName}' exported.` };
|
return { success: true, message: `Pool '${poolName}' exported.` };
|
||||||
}
|
}
|
||||||
@@ -319,7 +334,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scrub(poolName: string, action: 'start' | 'stop' | 'pause'): Promise<{ success: boolean; message: string }> {
|
async scrub(poolName: string, action: 'start' | 'stop' | 'pause'): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
return { success: true, message: `Scrub ${action} requested for pool '${poolName}'.` };
|
return { success: true, message: `Scrub ${action} requested for pool '${poolName}'.` };
|
||||||
}
|
}
|
||||||
const flag = action === 'stop' ? '-s' : action === 'pause' ? '-p' : '';
|
const flag = action === 'stop' ? '-s' : action === 'pause' ? '-p' : '';
|
||||||
@@ -331,7 +346,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async trim(poolName: string): Promise<{ success: boolean; message: string }> {
|
async trim(poolName: string): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
return { success: true, message: `TRIM initiated for pool '${poolName}'.` };
|
return { success: true, message: `TRIM initiated for pool '${poolName}'.` };
|
||||||
}
|
}
|
||||||
await this.runCommand('zpool', ['trim', poolName]);
|
await this.runCommand('zpool', ['trim', poolName]);
|
||||||
@@ -339,7 +354,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listDatasets(poolName?: string): Promise<ZfsDataset[]> {
|
async listDatasets(poolName?: string): Promise<ZfsDataset[]> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
if (poolName) {
|
if (poolName) {
|
||||||
return this.mockDatasets.filter((d) => d.pool === poolName);
|
return this.mockDatasets.filter((d) => d.pool === poolName);
|
||||||
}
|
}
|
||||||
@@ -378,7 +393,7 @@ export class ZfsService {
|
|||||||
quota?: string;
|
quota?: string;
|
||||||
mountpoint?: string;
|
mountpoint?: string;
|
||||||
}): Promise<{ success: boolean; message: string }> {
|
}): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
const pool = params.name.split('/')[0];
|
const pool = params.name.split('/')[0];
|
||||||
const newDs: ZfsDataset = {
|
const newDs: ZfsDataset = {
|
||||||
name: params.name,
|
name: params.name,
|
||||||
@@ -407,7 +422,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async destroyDataset(name: string, recursive = false): Promise<{ success: boolean; message: string }> {
|
async destroyDataset(name: string, recursive = false): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
this.mockDatasets = this.mockDatasets.filter((d) => d.name !== name && (!recursive || !d.name.startsWith(`${name}/`)));
|
this.mockDatasets = this.mockDatasets.filter((d) => d.name !== name && (!recursive || !d.name.startsWith(`${name}/`)));
|
||||||
return { success: true, message: `Dataset '${name}' destroyed.` };
|
return { success: true, message: `Dataset '${name}' destroyed.` };
|
||||||
}
|
}
|
||||||
@@ -419,7 +434,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setDatasetProperty(name: string, property: string, value: string): Promise<{ success: boolean; message: string }> {
|
async setDatasetProperty(name: string, property: string, value: string): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
const ds = this.mockDatasets.find((d) => d.name === name);
|
const ds = this.mockDatasets.find((d) => d.name === name);
|
||||||
if (ds) {
|
if (ds) {
|
||||||
(ds as any)[property] = value;
|
(ds as any)[property] = value;
|
||||||
@@ -431,7 +446,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async listSnapshots(datasetName?: string): Promise<ZfsSnapshot[]> {
|
async listSnapshots(datasetName?: string): Promise<ZfsSnapshot[]> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
if (datasetName) {
|
if (datasetName) {
|
||||||
return this.mockSnapshots.filter((s) => s.dataset === datasetName);
|
return this.mockSnapshots.filter((s) => s.dataset === datasetName);
|
||||||
}
|
}
|
||||||
@@ -461,7 +476,7 @@ export class ZfsService {
|
|||||||
|
|
||||||
async createSnapshot(datasetName: string, snapshotTag: string): Promise<{ success: boolean; message: string }> {
|
async createSnapshot(datasetName: string, snapshotTag: string): Promise<{ success: boolean; message: string }> {
|
||||||
const fullName = `${datasetName}@${snapshotTag}`;
|
const fullName = `${datasetName}@${snapshotTag}`;
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
this.mockSnapshots.unshift({
|
this.mockSnapshots.unshift({
|
||||||
name: fullName,
|
name: fullName,
|
||||||
dataset: datasetName,
|
dataset: datasetName,
|
||||||
@@ -477,7 +492,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async rollbackSnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> {
|
async rollbackSnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
return { success: true, message: `Dataset rolled back to '${snapshotName}'.` };
|
return { success: true, message: `Dataset rolled back to '${snapshotName}'.` };
|
||||||
}
|
}
|
||||||
await this.runCommand('zfs', ['rollback', '-r', snapshotName]);
|
await this.runCommand('zfs', ['rollback', '-r', snapshotName]);
|
||||||
@@ -485,7 +500,7 @@ export class ZfsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async destroySnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> {
|
async destroySnapshot(snapshotName: string): Promise<{ success: boolean; message: string }> {
|
||||||
if (!config.isLinux) {
|
if (this.useMock) {
|
||||||
this.mockSnapshots = this.mockSnapshots.filter((s) => s.name !== snapshotName);
|
this.mockSnapshots = this.mockSnapshots.filter((s) => s.name !== snapshotName);
|
||||||
return { success: true, message: `Snapshot '${snapshotName}' destroyed.` };
|
return { success: true, message: `Snapshot '${snapshotName}' destroyed.` };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user