Compare commits

..

15 Commits

Author SHA1 Message Date
5d18e53e30 update 2025-12-14 01:42:14 +00:00
9ad5222b95 update to deno 2025-12-13 13:27:51 +00:00
dba2e2ae68 chore: Remove deprecated Gitea workflows and add new CI/CD configurations 2025-10-28 06:38:02 +00:00
6c8095260d 6.0.1
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-26 17:44:43 +00:00
95c4ae3b16 fix(tests): Migrate tests to Deno native runner and update Deno config 2025-10-26 17:44:43 +00:00
536ebca6ab 6.0.0
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-10-26 15:23:56 +00:00
88f64536c2 BREAKING CHANGE(szci): Rename project from npmci to szci and migrate runtime to Deno; add compiled binaries, installer and wrapper; update imports, env handling and package metadata 2025-10-26 15:23:56 +00:00
4854d27a19 4.1.37
Some checks failed
Default (tags) / security (push) Failing after 15s
Default (tags) / test (push) Successful in 1m28s
Default (tags) / metadata (push) Failing after 11s
Default (tags) / release (push) Failing after 22s
2024-11-17 01:01:22 +01:00
75a0e8a7d8 fix(docker): Enhanced base image extraction logic from Dockerfile 2024-11-17 01:01:22 +01:00
43eb19f772 4.1.36
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Successful in 1m26s
Default (tags) / metadata (push) Failing after 11s
Default (tags) / release (push) Failing after 21s
2024-11-17 00:50:43 +01:00
dc2665d250 fix(docker): Improve logging for Dockerfile build order with base image details. 2024-11-17 00:50:43 +01:00
00f324e151 4.1.35
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Successful in 1m32s
Default (tags) / metadata (push) Failing after 10s
Default (tags) / release (push) Failing after 21s
2024-11-17 00:32:56 +01:00
e38cc40f11 fix(docker): Fix Dockerfile dependency sorting and enhance environment variable handling for GitHub repos 2024-11-17 00:32:56 +01:00
e9e8acafe4 4.1.34 2024-11-05 02:39:53 +01:00
c763db40bb fix(connector): Remove unused typedrequest implementation in cloudlyconnector 2024-11-05 02:39:53 +01:00
69 changed files with 2793 additions and 7728 deletions

View File

@@ -0,0 +1,31 @@
## SZCI {{VERSION}}
Pre-compiled binaries for multiple platforms.
### Installation
#### Option 1: Via npm (recommended)
```bash
npm install -g @ship.zone/szci
```
#### Option 2: Via installer script
```bash
curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash
```
#### Option 3: Direct binary download
Download the appropriate binary for your platform from the assets below and make it executable.
### Supported Platforms
- Linux x86_64 (x64)
- Linux ARM64 (aarch64)
- macOS x86_64 (Intel)
- macOS ARM64 (Apple Silicon)
- Windows x86_64
### Checksums
SHA256 checksums are provided in `SHA256SUMS.txt` for binary verification.
### npm Package
The npm package includes automatic binary detection and installation for your platform.

122
.gitea/workflows/README.md Normal file
View File

@@ -0,0 +1,122 @@
# SZCI Gitea CI/CD Workflows
This directory contains Gitea Actions workflows for automated CI/CD of the SZCI project.
## Workflows
### 1. CI (`ci.yml`)
**Trigger:** Push to `master` branch or pull requests
**Purpose:** Continuous integration checks on every push
**Jobs:**
- **Type Check & Lint** - Validates TypeScript types and code style
- **Build Test** - Compiles binary for Linux x64 and tests execution
- **Build All Platforms** - Compiles all 5 platform binaries and uploads as artifacts
**Usage:** Automatically runs on every commit to ensure code quality
---
### 2. Release (`release.yml`)
**Trigger:** Push of version tags (e.g., `v5.0.0`)
**Purpose:** Creates GitHub releases with pre-compiled binaries
**Jobs:**
1. Validates deno.json version matches git tag
2. Compiles binaries for all 5 platforms
3. Generates SHA256 checksums
4. Creates Gitea release with binaries as assets
5. Cleans up old releases (keeps last 3)
**Usage:**
```bash
# Update version in deno.json to 6.0.1
# Then create and push tag:
git tag v6.0.1
git push origin v6.0.1
```
**Output:** Release at `https://code.foss.global/ship.zone/szci/releases/tag/v6.0.1`
---
### 3. NPM Publish (`npm-publish.yml`)
**Trigger:** Push of version tags (e.g., `v5.0.0`)
**Purpose:** Publishes package to npm registry
**Jobs:**
1. Validates deno.json version matches git tag
2. Compiles all binaries
3. Syncs package.json version
4. Creates npm package
5. Publishes to npm with access token
6. Verifies publication
**Prerequisites:**
- `NPM_TOKEN` must be set in Gitea secrets
**Usage:** Same as release workflow (automatically triggered by version tags)
**Output:** Package at `https://www.npmjs.com/package/@ship.zone/szci`
---
## Release Process
To create a new release:
1. **Update version in deno.json:**
```json
{
"version": "6.0.1"
}
```
2. **Commit the version change:**
```bash
git add deno.json
git commit -m "6.0.1"
```
3. **Create and push tag:**
```bash
git tag v6.0.1
git push origin master
git push origin v6.0.1
```
4. **Workflows run automatically:**
- `release.yml` creates Gitea release with binaries
- `npm-publish.yml` publishes to npm
5. **Verify:**
- Check https://code.foss.global/ship.zone/szci/releases
- Check https://www.npmjs.com/package/@ship.zone/szci
---
## Secrets Required
Configure these in Gitea repository settings:
- `GITHUB_TOKEN` - Gitea access token (auto-provided)
- `NPM_TOKEN` - npm publish token (must be configured)
---
## Binary Artifacts
Each workflow produces binaries for:
- Linux x86_64
- Linux ARM64
- macOS x86_64
- macOS ARM64
- Windows x86_64
Total size per release: ~4GB (5 binaries × ~800MB each)

82
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,82 @@
name: CI
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
check:
name: Type Check & Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Check TypeScript types
run: deno check mod.ts
- name: Lint code
run: deno lint
continue-on-error: true
- name: Format check
run: deno fmt --check
continue-on-error: true
build:
name: Build Test (Current Platform)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Compile for current platform
run: |
echo "Testing compilation for Linux x86_64..."
deno compile --allow-all --no-check \
--output szci-test \
--target x86_64-unknown-linux-gnu mod.ts
- name: Test binary execution
run: |
chmod +x szci-test
./szci-test --version
build-all:
name: Build All Platforms
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Compile all platform binaries
run: bash scripts/compile-all.sh
- name: Upload all binaries as artifact
uses: actions/upload-artifact@v3
with:
name: szci-binaries.zip
path: dist/binaries/*
retention-days: 30

View File

@@ -1,66 +0,0 @@
name: Default (not tags)
on:
push:
tags-ignore:
- '**'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
- name: Run npm prepare
run: npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build

View File

@@ -1,108 +0,0 @@
name: Default (tags)
on:
push:
tags:
- '*'
env:
IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
NPMCI_LOGIN_DOCKER_GITEA: ${{secrets.NPMCI_DOCKER_REGISTRYURL_DEFAULT}}|${{ gitea.repository_owner }}|${{ secrets.GITEA_TOKEN }}
jobs:
security:
runs-on: ubuntu-latest
continue-on-error: true
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Install pnpm and npmci
run: |
pnpm install -g pnpm
pnpm install -g @shipzone/npmci
- name: Run npm prepare
run: npmci npm prepare
- name: Audit production dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --prod
continue-on-error: true
- name: Audit development dependencies
run: |
npmci command npm config set registry https://registry.npmjs.org
npmci command pnpm audit --audit-level=high --dev
continue-on-error: true
test:
if: ${{ always() }}
needs: security
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Test stable
run: |
npmci node install stable
npmci npm install
npmci npm test
- name: Test build
run: |
npmci node install stable
npmci npm install
npmci npm build
release:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v3
- name: Release
run: |
npmci node install stable
npmci npm publish
metadata:
needs: test
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
container:
image: ${{ env.IMAGE }}
continue-on-error: true
steps:
- uses: actions/checkout@v3
- name: Code quality
run: |
npmci command npm install -g typescript
npmci npm prepare
npmci npm install
- name: Trigger
run: npmci trigger
- name: Build docs and upload artifacts
run: |
npmci node install stable
npmci npm install
pnpm install -g @git.zone/tsdoc
npmci command tsdoc
continue-on-error: true

View File

@@ -0,0 +1,129 @@
name: Publish to npm
on:
push:
tags:
- 'v*'
jobs:
npm-publish:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Setup Node.js for npm publishing
uses: actions/setup-node@v4
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org/'
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
echo "Publishing version: $VERSION"
- name: Verify deno.json version matches tag
run: |
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
TAG_VERSION="${{ steps.version.outputs.version_number }}"
echo "deno.json version: $DENO_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
echo "ERROR: Version mismatch!"
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
exit 1
fi
- name: Compile binaries for npm package
run: |
echo "Compiling binaries for npm package..."
deno task compile
echo ""
echo "Binary sizes:"
ls -lh dist/binaries/
- name: Generate SHA256 checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS
cat SHA256SUMS
cd ../..
- name: Sync package.json version
run: |
VERSION="${{ steps.version.outputs.version_number }}"
echo "Syncing package.json to version ${VERSION}..."
npm version ${VERSION} --no-git-tag-version --allow-same-version
echo "package.json version: $(grep '"version"' package.json | head -1)"
- name: Create npm package
run: |
echo "Creating npm package..."
npm pack
echo ""
echo "Package created:"
ls -lh *.tgz
- name: Test local installation
run: |
echo "Testing local package installation..."
PACKAGE_FILE=$(ls *.tgz)
npm install -g ${PACKAGE_FILE}
echo ""
echo "Testing szci command:"
szci --version || echo "Note: Binary execution may fail in CI environment"
echo ""
echo "Checking installed files:"
npm ls -g @ship.zone/szci || true
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
echo "Publishing to npm registry..."
npm publish --access public
echo ""
echo "✅ Successfully published @ship.zone/szci to npm!"
echo ""
echo "Package info:"
npm view @ship.zone/szci
- name: Verify npm package
run: |
echo "Waiting for npm propagation..."
sleep 30
echo ""
echo "Verifying published package..."
npm view @ship.zone/szci
echo ""
echo "Testing installation from npm:"
npm install -g @ship.zone/szci
echo ""
echo "Package installed successfully!"
which szci || echo "Binary location check skipped"
- name: Publish Summary
run: |
echo "================================================"
echo " npm Publish Complete!"
echo "================================================"
echo ""
echo "✅ Package: @ship.zone/szci"
echo "✅ Version: ${{ steps.version.outputs.version }}"
echo ""
echo "Installation:"
echo " npm install -g @ship.zone/szci"
echo ""
echo "Registry:"
echo " https://www.npmjs.com/package/@ship.zone/szci"
echo ""

View File

@@ -0,0 +1,248 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Deno
uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Get version from tag
id: version
run: |
VERSION=${GITHUB_REF#refs/tags/}
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_number=${VERSION#v}" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Verify deno.json version matches tag
run: |
DENO_VERSION=$(grep -o '"version": "[^"]*"' deno.json | cut -d'"' -f4)
TAG_VERSION="${{ steps.version.outputs.version_number }}"
echo "deno.json version: $DENO_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$DENO_VERSION" != "$TAG_VERSION" ]; then
echo "ERROR: Version mismatch!"
echo "deno.json has version $DENO_VERSION but tag is $TAG_VERSION"
exit 1
fi
- name: Compile binaries for all platforms
run: |
echo "================================================"
echo " SZCI Release Compilation"
echo " Version: ${{ steps.version.outputs.version }}"
echo "================================================"
echo ""
# Clean up old binaries and create fresh directory
rm -rf dist/binaries
mkdir -p dist/binaries
echo "→ Cleaned old binaries from dist/binaries"
echo ""
# Linux x86_64
echo "→ Compiling for Linux x86_64..."
deno compile --allow-all --no-check \
--output dist/binaries/szci-linux-x64 \
--target x86_64-unknown-linux-gnu mod.ts
echo " ✓ Linux x86_64 complete"
# Linux ARM64
echo "→ Compiling for Linux ARM64..."
deno compile --allow-all --no-check \
--output dist/binaries/szci-linux-arm64 \
--target aarch64-unknown-linux-gnu mod.ts
echo " ✓ Linux ARM64 complete"
# macOS x86_64
echo "→ Compiling for macOS x86_64..."
deno compile --allow-all --no-check \
--output dist/binaries/szci-macos-x64 \
--target x86_64-apple-darwin mod.ts
echo " ✓ macOS x86_64 complete"
# macOS ARM64
echo "→ Compiling for macOS ARM64..."
deno compile --allow-all --no-check \
--output dist/binaries/szci-macos-arm64 \
--target aarch64-apple-darwin mod.ts
echo " ✓ macOS ARM64 complete"
# Windows x86_64
echo "→ Compiling for Windows x86_64..."
deno compile --allow-all --no-check \
--output dist/binaries/szci-windows-x64.exe \
--target x86_64-pc-windows-msvc mod.ts
echo " ✓ Windows x86_64 complete"
echo ""
echo "All binaries compiled successfully!"
ls -lh dist/binaries/
- name: Generate SHA256 checksums
run: |
cd dist/binaries
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
cd ../..
- name: Extract changelog for this version
id: changelog
run: |
VERSION="${{ steps.version.outputs.version }}"
# Check if changelog.md exists
if [ ! -f changelog.md ]; then
echo "No changelog.md found, using default release notes"
cat > /tmp/release_notes.md << EOF
## SZCI $VERSION
Pre-compiled binaries for multiple platforms.
### Installation
Use the installation script:
\`\`\`bash
curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash
\`\`\`
Or download the binary for your platform and make it executable.
### Supported Platforms
- Linux x86_64 (x64)
- Linux ARM64 (aarch64)
- macOS x86_64 (Intel)
- macOS ARM64 (Apple Silicon)
- Windows x86_64
### Checksums
SHA256 checksums are provided in SHA256SUMS.txt
EOF
else
# Try to extract section for this version from changelog.md
awk "/## \[$VERSION\]/,/## \[/" changelog.md | sed '$d' > /tmp/release_notes.md || cat > /tmp/release_notes.md << EOF
## SZCI $VERSION
See changelog.md for full details.
### Installation
Use the installation script:
\`\`\`bash
curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash
\`\`\`
EOF
fi
echo "Release notes:"
cat /tmp/release_notes.md
- name: Delete existing release if it exists
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Checking for existing release $VERSION..."
# Try to get existing release by tag
EXISTING_RELEASE_ID=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/ship.zone/szci/releases/tags/$VERSION" \
| jq -r '.id // empty')
if [ -n "$EXISTING_RELEASE_ID" ]; then
echo "Found existing release (ID: $EXISTING_RELEASE_ID), deleting..."
curl -X DELETE -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/ship.zone/szci/releases/$EXISTING_RELEASE_ID"
echo "Existing release deleted"
sleep 2
else
echo "No existing release found, proceeding with creation"
fi
- name: Create Gitea Release
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_NOTES=$(cat /tmp/release_notes.md)
# Create the release
echo "Creating release for $VERSION..."
RELEASE_ID=$(curl -X POST -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
"https://code.foss.global/api/v1/repos/ship.zone/szci/releases" \
-d "{
\"tag_name\": \"$VERSION\",
\"name\": \"SZCI $VERSION\",
\"body\": $(jq -Rs . /tmp/release_notes.md),
\"draft\": false,
\"prerelease\": false
}" | jq -r '.id')
echo "Release created with ID: $RELEASE_ID"
# Upload binaries as release assets
for binary in dist/binaries/*; do
filename=$(basename "$binary")
echo "Uploading $filename..."
curl -X POST -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$binary" \
"https://code.foss.global/api/v1/repos/ship.zone/szci/releases/$RELEASE_ID/assets?name=$filename"
done
echo "All assets uploaded successfully"
- name: Clean up old releases
run: |
echo "Cleaning up old releases (keeping only last 3)..."
# Fetch all releases sorted by creation date
RELEASES=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/ship.zone/szci/releases" | \
jq -r 'sort_by(.created_at) | reverse | .[3:] | .[].id')
# Delete old releases
if [ -n "$RELEASES" ]; then
echo "Found releases to delete:"
for release_id in $RELEASES; do
echo " Deleting release ID: $release_id"
curl -X DELETE -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"https://code.foss.global/api/v1/repos/ship.zone/szci/releases/$release_id"
done
echo "Old releases deleted successfully"
else
echo "No old releases to delete (less than 4 releases total)"
fi
echo ""
- name: Release Summary
run: |
echo "================================================"
echo " Release ${{ steps.version.outputs.version }} Complete!"
echo "================================================"
echo ""
echo "Binaries published:"
ls -lh dist/binaries/
echo ""
echo "Release URL:"
echo "https://code.foss.global/ship.zone/szci/releases/tag/${{ steps.version.outputs.version }}"
echo ""
echo "Installation command:"
echo "curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash"
echo ""

4
.gitignore vendored
View File

@@ -17,4 +17,8 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# deno
deno.lock
.deno/
# custom # custom

View File

@@ -5,9 +5,9 @@
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"npmci": { "szci": {
"type": "object", "type": "object",
"description": "settings for npmci" "description": "settings for szci"
}, },
"gitzone": { "gitzone": {
"type": "object", "type": "object",

108
bin/szci-wrapper.js Executable file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* SZCI npm wrapper
* This script executes the appropriate pre-compiled binary based on the current platform
*/
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
import { platform, arch } from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Get the binary name for the current platform
*/
function getBinaryName() {
const plat = platform();
const architecture = arch();
// Map Node's platform/arch to our binary naming
const platformMap = {
'darwin': 'macos',
'linux': 'linux',
'win32': 'windows'
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64'
};
const mappedPlatform = platformMap[plat];
const mappedArch = archMap[architecture];
if (!mappedPlatform || !mappedArch) {
console.error(`Error: Unsupported platform/architecture: ${plat}/${architecture}`);
console.error('Supported platforms: Linux, macOS, Windows');
console.error('Supported architectures: x64, arm64');
process.exit(1);
}
// Construct binary name
let binaryName = `szci-${mappedPlatform}-${mappedArch}`;
if (plat === 'win32') {
binaryName += '.exe';
}
return binaryName;
}
/**
* Execute the binary
*/
function executeBinary() {
const binaryName = getBinaryName();
const binaryPath = join(__dirname, '..', 'dist', 'binaries', binaryName);
// Check if binary exists
if (!existsSync(binaryPath)) {
console.error(`Error: Binary not found at ${binaryPath}`);
console.error('This might happen if:');
console.error('1. The postinstall script failed to run');
console.error('2. The platform is not supported');
console.error('3. The package was not installed correctly');
console.error('');
console.error('Try reinstalling the package:');
console.error(' npm uninstall -g @ship.zone/szci');
console.error(' npm install -g @ship.zone/szci');
process.exit(1);
}
// Spawn the binary with all arguments passed through
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: 'inherit',
shell: false
});
// Handle child process events
child.on('error', (err) => {
console.error(`Error executing szci: ${err.message}`);
process.exit(1);
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
} else {
process.exit(code || 0);
}
});
// Forward signals to child process
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
signals.forEach(signal => {
process.on(signal, () => {
if (!child.killed) {
child.kill(signal);
}
});
});
}
// Execute
executeBinary();

View File

@@ -1,5 +1,52 @@
# Changelog # Changelog
## 2025-10-26 - 6.0.1 - fix(tests)
Migrate tests to Deno native runner and update Deno config
- Convert test suites from tap/tapbundle to Deno.test and @std/assert
- Replace CommonJS-style runtime imports with .ts module imports for Deno (test files updated)
- Use Deno.env.set to configure test environment variables and restore working directory after tests
- Update test/test.cloudly.ts to import CloudlyConnector directly and disable TLS verification for tests
- Adjust deno.json version field (6.0.0 -> 5.0.0) as part of Deno configuration changes
- Add local project .claude/settings.local.json for tooling permissions
## 2025-10-26 - 6.0.0 - BREAKING CHANGE(szci)
Rename project from npmci to szci and migrate runtime to Deno; add compiled binaries, installer and wrapper; update imports, env handling and package metadata
- Major rename/refactor: Npmci -> Szci across the codebase (classes, filenames, modules and exports).
- Migrate runtime to Deno: add deno.json, mod.ts entry point, use Deno.std imports and .ts module imports throughout.
- Add compilation and distribution tooling: scripts/compile-all.sh, scripts/install-binary.js, bin/szci-wrapper.js and dist/binaries layout for prebuilt executables.
- Package metadata updated: package.json renamed/rewritten for @ship.zone/szci and bumped to 5.0.0, updated publishConfig, files and scripts.
- Environment API changes: replaced process.env usages with Deno.env accessors and updated path constants (Szci paths).
- Refactored helper modules: npmci.bash -> szci.bash, updated smartshell/bash wrappers and other manager modules to Deno patterns.
- Tests and imports updated to new module paths and package layout; .gitignore updated to ignore deno artifacts.
- Breaking changes to callers: CLI name, class names, programmatic API, binary installation and environment handling have changed and may require updates in integrations and CI configurations.
## 2024-11-17 - 4.1.37 - fix(docker)
Enhanced base image extraction logic from Dockerfile
- Improved dockerBaseImage to accurately extract base images considering ARG variables.
- Added support for parsing Dockerfile content without external libraries.
- Enhanced error handling for missing FROM instructions.
## 2024-11-17 - 4.1.36 - fix(docker)
Improve logging for Dockerfile build order with base image details.
- Enhance logging in Dockerfile sorting process to include base image information.
## 2024-11-17 - 4.1.35 - fix(docker)
Fix Dockerfile dependency sorting and enhance environment variable handling for GitHub repos
- Refined the algorithm for sorting Dockerfiles based on dependencies to ensure proper build order.
- Enhanced environment variable handling in the NpmciEnv class to support conditional assignments.
- Updated various dependencies in package.json for improved performance and compatibility.
- Added error handling to circular dependency detection in Dockerfile sorting.
## 2024-11-05 - 4.1.34 - fix(connector)
Remove unused typedrequest implementation in cloudlyconnector
- Removed commented out code that initialized typedrequest in CloudlyConnector.
## 2024-11-05 - 4.1.33 - fix(core) ## 2024-11-05 - 4.1.33 - fix(core)
Updated dependencies and improved npm preparation logic. Updated dependencies and improved npm preparation logic.

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
import * as cliTool from './ts/index.js';
cliTool.runCli();

4
cli.js
View File

@@ -1,4 +0,0 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
const cliTool = await import('./dist_ts/index.js');
cliTool.runCli();

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env node
process.env.CLI_CALL = 'true';
import * as tsrun from '@git.zone/tsrun';
tsrun.runPath('./cli.child.js', import.meta.url);

67
deno.json Normal file
View File

@@ -0,0 +1,67 @@
{
"name": "@ship.zone/szci",
"version": "6.0.1",
"exports": "./mod.ts",
"nodeModulesDir": "auto",
"tasks": {
"dev": "deno run --allow-all mod.ts",
"compile": "deno task compile:all",
"compile:all": "bash scripts/compile-all.sh",
"test": "deno test --allow-all test/",
"test:watch": "deno test --allow-all --watch test/",
"check": "deno check mod.ts",
"fmt": "deno fmt",
"lint": "deno lint"
},
"lint": {
"rules": {
"tags": [
"recommended"
]
}
},
"fmt": {
"useTabs": false,
"lineWidth": 100,
"indentWidth": 2,
"semiColons": true,
"singleQuote": true
},
"compilerOptions": {
"lib": [
"deno.window"
],
"strict": true
},
"imports": {
"@std/path": "jsr:@std/path@^1.0.0",
"@std/fmt": "jsr:@std/fmt@^1.0.0",
"@std/cli": "jsr:@std/cli@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.1.10",
"@push.rocks/lik": "npm:@push.rocks/lik@^6.1.0",
"@push.rocks/npmextra": "npm:@push.rocks/npmextra@^5.1.2",
"@push.rocks/projectinfo": "npm:@push.rocks/projectinfo@^5.0.2",
"@push.rocks/qenv": "npm:@push.rocks/qenv@^6.0.2",
"@push.rocks/smartanalytics": "npm:@push.rocks/smartanalytics@^2.0.15",
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.11",
"@push.rocks/smartdelay": "npm:@push.rocks/smartdelay@^3.0.5",
"@push.rocks/smartenv": "npm:@push.rocks/smartenv@^5.0.5",
"@push.rocks/smartfile": "npm:@push.rocks/smartfile@^11.0.21",
"@push.rocks/smartgit": "npm:@push.rocks/smartgit@^3.1.1",
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.0.7",
"@push.rocks/smartlog-destination-local": "npm:@push.rocks/smartlog-destination-local@^9.0.0",
"@push.rocks/smartobject": "npm:@push.rocks/smartobject@^1.0.12",
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^5.0.11",
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.0.4",
"@push.rocks/smartrequest": "npm:@push.rocks/smartrequest@^2.0.23",
"@push.rocks/smartshell": "npm:@push.rocks/smartshell@^3.0.6",
"@push.rocks/smartsocket": "npm:@push.rocks/smartsocket@^2.0.22",
"@push.rocks/smartssh": "npm:@push.rocks/smartssh@^2.0.1",
"@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.0.8",
"@push.rocks/smartexpect": "npm:@push.rocks/smartexpect@^1.0.15",
"@serve.zone/api": "npm:@serve.zone/api@^4.3.11",
"@tsclass/tsclass": "npm:@tsclass/tsclass@^4.1.2",
"through2": "npm:through2@^4.0.2"
}
}

257
install.sh Executable file
View File

@@ -0,0 +1,257 @@
#!/bin/bash
# SZCI Installer Script (v5.0+)
# Downloads and installs pre-compiled SZCI binary from Gitea releases
#
# Usage:
# Direct piped installation (recommended):
# curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash
#
# With version specification:
# curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash -s -- --version v5.0.0
#
# Options:
# -h, --help Show this help message
# --version VERSION Install specific version (e.g., v5.0.0)
# --install-dir DIR Installation directory (default: /opt/szci)
set -e
# Default values
SHOW_HELP=0
SPECIFIED_VERSION=""
INSTALL_DIR="/opt/szci"
GITEA_BASE_URL="https://code.foss.global"
GITEA_REPO="ship.zone/szci"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
SHOW_HELP=1
shift
;;
--version)
SPECIFIED_VERSION="$2"
shift 2
;;
--install-dir)
INSTALL_DIR="$2"
shift 2
;;
*)
echo "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
esac
done
if [ $SHOW_HELP -eq 1 ]; then
echo "SZCI Installer Script (v5.0+)"
echo "Downloads and installs pre-compiled SZCI binary"
echo ""
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " --version VERSION Install specific version (e.g., v5.0.0)"
echo " --install-dir DIR Installation directory (default: /opt/szci)"
echo ""
echo "Examples:"
echo " # Install latest version"
echo " curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash"
echo ""
echo " # Install specific version"
echo " curl -sSL https://code.foss.global/ship.zone/szci/raw/branch/master/install.sh | sudo bash -s -- --version v5.0.0"
exit 0
fi
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root (sudo bash install.sh or pipe to sudo bash)"
exit 1
fi
# Helper function to detect OS and architecture
detect_platform() {
local os=$(uname -s)
local arch=$(uname -m)
# Map OS
case "$os" in
Linux)
os_name="linux"
;;
Darwin)
os_name="macos"
;;
MINGW*|MSYS*|CYGWIN*)
os_name="windows"
;;
*)
echo "Error: Unsupported operating system: $os"
echo "Supported: Linux, macOS, Windows"
exit 1
;;
esac
# Map architecture
case "$arch" in
x86_64|amd64)
arch_name="x64"
;;
aarch64|arm64)
arch_name="arm64"
;;
*)
echo "Error: Unsupported architecture: $arch"
echo "Supported: x86_64/amd64 (x64), aarch64/arm64 (arm64)"
exit 1
;;
esac
# Construct binary name
if [ "$os_name" = "windows" ]; then
echo "szci-${os_name}-${arch_name}.exe"
else
echo "szci-${os_name}-${arch_name}"
fi
}
# Get latest release version from Gitea API
get_latest_version() {
echo "Fetching latest release version from Gitea..." >&2
local api_url="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}/releases/latest"
local response=$(curl -sSL "$api_url" 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$response" ]; then
echo "Error: Failed to fetch latest release information from Gitea API" >&2
echo "URL: $api_url" >&2
exit 1
fi
# Extract tag_name from JSON response
local version=$(echo "$response" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
if [ -z "$version" ]; then
echo "Error: Could not determine latest version from API response" >&2
exit 1
fi
echo "$version"
}
# Main installation process
echo "================================================"
echo " SZCI Installation Script (v5.0+)"
echo " Serve Zone CI/CD Tool"
echo "================================================"
echo ""
# Detect platform
BINARY_NAME=$(detect_platform)
echo "Detected platform: $BINARY_NAME"
echo ""
# Determine version to install
if [ -n "$SPECIFIED_VERSION" ]; then
VERSION="$SPECIFIED_VERSION"
echo "Installing specified version: $VERSION"
else
VERSION=$(get_latest_version)
echo "Installing latest version: $VERSION"
fi
echo ""
# Construct download URL
DOWNLOAD_URL="${GITEA_BASE_URL}/${GITEA_REPO}/releases/download/${VERSION}/${BINARY_NAME}"
echo "Download URL: $DOWNLOAD_URL"
echo ""
# Clean installation directory - ensure only binary exists
if [ -d "$INSTALL_DIR" ]; then
echo "Cleaning installation directory: $INSTALL_DIR"
rm -rf "$INSTALL_DIR"
fi
# Create fresh installation directory
echo "Creating installation directory: $INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# Download binary
echo "Downloading SZCI binary..."
TEMP_FILE="$INSTALL_DIR/szci.download"
curl -sSL "$DOWNLOAD_URL" -o "$TEMP_FILE"
if [ $? -ne 0 ]; then
echo "Error: Failed to download binary from $DOWNLOAD_URL"
echo ""
echo "Please check:"
echo " 1. Your internet connection"
echo " 2. The specified version exists: ${GITEA_BASE_URL}/${GITEA_REPO}/releases"
echo " 3. The platform binary is available for this release"
rm -f "$TEMP_FILE"
exit 1
fi
# Check if download was successful (file exists and not empty)
if [ ! -s "$TEMP_FILE" ]; then
echo "Error: Downloaded file is empty or does not exist"
rm -f "$TEMP_FILE"
exit 1
fi
# Move to final location
BINARY_PATH="$INSTALL_DIR/szci"
mv "$TEMP_FILE" "$BINARY_PATH"
if [ $? -ne 0 ] || [ ! -f "$BINARY_PATH" ]; then
echo "Error: Failed to move binary to $BINARY_PATH"
rm -f "$TEMP_FILE" 2>/dev/null
exit 1
fi
# Make executable
chmod +x "$BINARY_PATH"
if [ $? -ne 0 ]; then
echo "Error: Failed to make binary executable"
exit 1
fi
echo "Binary installed successfully to: $BINARY_PATH"
echo ""
# Check if /usr/local/bin is in PATH
if [[ ":$PATH:" == *":/usr/local/bin:"* ]]; then
BIN_DIR="/usr/local/bin"
else
BIN_DIR="/usr/bin"
fi
# Create symlink for global access
ln -sf "$BINARY_PATH" "$BIN_DIR/szci"
echo "Symlink created: $BIN_DIR/szci -> $BINARY_PATH"
echo ""
echo "================================================"
echo " SZCI Installation Complete!"
echo "================================================"
echo ""
echo "Installation details:"
echo " Binary location: $BINARY_PATH"
echo " Symlink location: $BIN_DIR/szci"
echo " Version: $VERSION"
echo ""
echo "Get started:"
echo " szci --version"
echo " szci --help"
echo " szci node install stable # Install Node.js"
echo " szci docker build # Build Docker images"
echo " szci npm test # Run tests"
echo ""
echo "For CI/CD integration examples, visit:"
echo " https://code.foss.global/ship.zone/szci"
echo ""

48
mod.ts Normal file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env -S deno run --allow-all
/**
* SZCI - Serve Zone CI/CD Tool
*
* A command-line tool for streamlining Node.js and Docker workflows
* within CI environments, particularly GitLab CI, GitHub CI, and Gitea CI.
*
* Required Permissions:
* - --allow-net: Network access for Docker registries, npm, git operations
* - --allow-read: Read configuration files, Dockerfiles, package.json
* - --allow-write: Write configuration files, build artifacts
* - --allow-run: Execute system commands (docker, git, npm, ssh)
* - --allow-sys: Access system information (OS details)
* - --allow-env: Read/write environment variables
*
* @module
*/
import { Szci } from './ts/szci.classes.szci.ts';
/**
* Main entry point for the SZCI application
* Parses command-line arguments and executes the requested command
*/
async function main(): Promise<void> {
// Create Szci instance
const szciInstance = new Szci();
// Start the CLI
// Deno.args is already 0-indexed (unlike Node's process.argv which starts at index 2)
// The smartcli library may expect process.argv format, so we might need to prepend placeholders
await szciInstance.start();
}
// Execute main and handle errors
if (import.meta.main) {
try {
await main();
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
Deno.exit(1);
}
}
// Export for programmatic use
export { Szci } from './ts/szci.classes.szci.ts';
export { Dockerfile } from './ts/manager.docker/mod.classes.dockerfile.ts';

View File

@@ -1,21 +1,21 @@
{ {
"npmci": { "@ship.zone/szci": {
"npmGlobalTools": [], "npmGlobalTools": [],
"npmAccessLevel": "public", "npmAccessLevel": "public",
"npmRegistryUrl": "registry.npmjs.org" "npmRegistryUrl": "registry.npmjs.org"
}, },
"npmdocker": { "@git.zone/tsdocker": {
"baseImage": "hosttoday/ht-docker-node:npmci", "baseImage": "hosttoday/ht-docker-node:npmci",
"command": "npmci test stable" "command": "szci test stable"
}, },
"gitzone": { "@git.zone/cli": {
"projectType": "npm", "projectType": "npm",
"module": { "module": {
"githost": "gitlab.com", "githost": "gitlab.com",
"gitscope": "ship.zone", "gitscope": "ship.zone",
"gitrepo": "npmci", "gitrepo": "szci",
"description": "A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities.", "description": "A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities.",
"npmPackagename": "@ship.zone/npmci", "npmPackagename": "@ship.zone/szci",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
"Node.js", "Node.js",

View File

@@ -1,79 +1,7 @@
{ {
"name": "@ship.zone/npmci", "name": "@ship.zone/szci",
"version": "4.1.33", "version": "6.0.1",
"private": false, "description": "Serve Zone CI - A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities. Powered by Deno with standalone executables.",
"description": "A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"bin": {
"npmci": "cli.js"
},
"scripts": {
"test": "tstest test/",
"build": "tsbuild --allowimplicitany && (npm run testVersion)",
"testVersion": "(cd test/assets/ && node ../../cli.js -v)",
"buildDocs": "tsdoc"
},
"repository": {
"type": "git",
"url": "git+ssh://git@gitlab.com/gitzone/npmci.git"
},
"author": "Lossless GmbH",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/gitzone/npmci/issues"
},
"homepage": "https://gitlab.com/gitzone/npmci#README",
"devDependencies": {
"@git.zone/tsbuild": "^2.2.0",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^1.0.77",
"@push.rocks/tapbundle": "^5.3.0",
"@types/node": "^22.8.7"
},
"dependencies": {
"@api.global/typedrequest": "^3.1.10",
"@push.rocks/lik": "^6.1.0",
"@push.rocks/npmextra": "^5.0.23",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.0.2",
"@push.rocks/smartanalytics": "^2.0.15",
"@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^5.0.5",
"@push.rocks/smartfile": "^11.0.21",
"@push.rocks/smartgit": "^3.1.1",
"@push.rocks/smartlog": "^3.0.7",
"@push.rocks/smartlog-destination-local": "^9.0.0",
"@push.rocks/smartobject": "^1.0.12",
"@push.rocks/smartpath": "^5.0.11",
"@push.rocks/smartpromise": "^4.0.4",
"@push.rocks/smartrequest": "^2.0.18",
"@push.rocks/smartshell": "^3.0.6",
"@push.rocks/smartsocket": "^2.0.22",
"@push.rocks/smartssh": "^2.0.1",
"@push.rocks/smartstring": "^4.0.8",
"@serve.zone/api": "^4.3.1",
"@tsclass/tsclass": "^4.1.2",
"@types/through2": "^2.0.38",
"through2": "^4.0.2"
},
"files": [
"ts/**/*",
"ts_web/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_ts/**/*",
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
"readme.md"
],
"browserslist": [
"last 1 chrome versions"
],
"keywords": [ "keywords": [
"Node.js", "Node.js",
"Docker", "Docker",
@@ -84,11 +12,56 @@
"automation", "automation",
"npm", "npm",
"TypeScript", "TypeScript",
"Deno",
"cloud", "cloud",
"SSH", "SSH",
"registry", "registry",
"container management", "container management",
"continuous integration", "continuous integration",
"continuous deployment" "continuous deployment"
] ],
"homepage": "https://code.foss.global/ship.zone/szci",
"bugs": {
"url": "https://code.foss.global/ship.zone/szci/issues"
},
"repository": {
"type": "git",
"url": "git+https://code.foss.global/ship.zone/szci.git"
},
"author": "Lossless GmbH",
"license": "MIT",
"type": "module",
"bin": {
"szci": "./bin/szci-wrapper.js"
},
"scripts": {
"postinstall": "node scripts/install-binary.js",
"prepublishOnly": "echo 'Publishing SZCI binaries to npm...'",
"test": "echo 'Tests are run with Deno: deno task test'",
"build": "echo 'no build needed'"
},
"files": [
"bin/",
"scripts/install-binary.js",
"readme.md",
"license",
"changelog.md"
],
"engines": {
"node": ">=14.0.0"
},
"os": [
"darwin",
"linux",
"win32"
],
"cpu": [
"x64",
"arm64"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
} }

6301
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,31 @@
- focus on cli usage in CI environments. - focus on cli usage in CI environments.
- show Gitlab CI, GitHub CI and Gitea CI examples. - show Gitlab CI, GitHub CI and Gitea CI examples.
## Deno Migration Status
The project has been fully migrated from Node.js to Deno runtime.
### Environment Variables
All environment variables have been rebranded from NPMCI_* to SZCI_*:
| Old Name | New Name |
|----------|----------|
| `NPMCI_COMPUTED_REPOURL` | `SZCI_COMPUTED_REPOURL` |
| `NPMCI_URL_CLOUDLY` | `SZCI_URL_CLOUDLY` |
| `NPMCI_GIT_GITHUBTOKEN` | `SZCI_GIT_GITHUBTOKEN` |
| `NPMCI_GIT_GITHUBGROUP` | `SZCI_GIT_GITHUBGROUP` |
| `NPMCI_GIT_GITHUB` | `SZCI_GIT_GITHUB` |
| `NPMCI_TRIGGER_*` | `SZCI_TRIGGER_*` |
| `NPMCI_LOGIN_CLOUDRON` | `SZCI_LOGIN_CLOUDRON` |
| `NPMCI_SSHKEY_*` | `SZCI_SSHKEY_*` |
| `NPMCI_LOGIN_DOCKER*` | `SZCI_LOGIN_DOCKER*` |
| `NPMCI_TOKEN_NPM*` | `SZCI_TOKEN_NPM*` |
| `NPMTS_TEST` | `SZCI_TEST` |
| `DEBUG_NPMCI` | `DEBUG_SZCI` |
### Runtime
- Uses Deno APIs (`Deno.env`, `Deno.cwd`, `Deno.exit`)
- Logger runtime set to 'deno'
- Dynamic imports use `.ts` extensions

671
readme.md
View File

@@ -1,30 +1,115 @@
# @ship.zone/npmci # @ship.zone/szci
A tool to enhance Node.js and Docker workflows within GitLab CI, providing various CI/CD utilities.
## Install **Serve Zone CI** - A powerful CI/CD tool for streamlining Node.js and Docker workflows within CI environments (GitLab CI, GitHub CI, Gitea CI). Now powered by Deno with standalone executables.
To install `@ship.zone/npmci`, you can use npm or yarn: ## ✨ Features
- 🚀 **Standalone Executables** - No Node.js installation required
- 🐳 **Docker Integration** - Build, tag, and push Docker images
- 📦 **NPM Management** - Install, test, and publish npm packages
- 🔧 **Node.js Version Management** - Install and switch between Node versions
- 🔐 **SSH Key Management** - Deploy SSH keys from environment variables
- ☁️ **Multi-Registry Support** - Push to multiple Docker registries
- 🎯 **Cross-Platform** - Binaries for Linux, macOS, and Windows
## 📥 Installation
### NPM (Recommended)
```sh ```sh
# Using npm npm install -g @ship.zone/szci
npm install @ship.zone/npmci
# Using yarn
yarn add @ship.zone/npmci
``` ```
## Usage The package will automatically download the appropriate pre-compiled binary for your platform.
`npmci` is designed to streamline CI/CD processes, particularly in Docker and Node.js environments. The following sections illustrate its usage in various scenarios, from handling Node versions to building Docker images and more. ### From Source (Deno Required)
### 1. Integration with GitLab CI, GitHub CI, and Gitea CI ```sh
git clone https://code.foss.global/ship.zone/szci.git
cd szci
deno task compile
```
#### GitLab CI ## 🚀 Quick Start
An example of integrating `npmci` into a GitLab CI configuration could look like this: ```sh
# Install Node.js
szci node install stable
# Install dependencies
szci npm install
# Build Docker images
szci docker build
# Run tests
szci npm test
# Push Docker images
szci docker push registry.example.com
```
## 📖 Usage
### Node.js Management
```sh
# Install specific Node.js version
szci node install lts
szci node install stable
szci node install 18
# Install from .nvmrc
szci node install legacy
```
### NPM Commands
```sh
# Install dependencies
szci npm install
# Run tests
szci npm test
# Publish package
szci npm publish
```
### Docker Workflows
```sh
# Prepare Docker environment
szci docker prepare
# Build all Dockerfiles
szci docker build
# Push to registry
szci docker push registry.example.com
# Pull from registry
szci docker pull registry.example.com
# Test Dockerfiles
szci docker test
```
### SSH Key Management
```sh
# Deploy SSH keys from environment
szci ssh prepare
```
Set environment variables like `NPMCI_SSHKEY_1`, `NPMCI_SSHKEY_2`, etc.
## 🔧 CI/CD Integration
### GitLab CI
```yaml ```yaml
image: hosttoday/ht-docker-node:npmci image: denoland/deno:alpine
stages: stages:
- prepare - prepare
@@ -32,463 +117,191 @@ stages:
- test - test
- deploy - deploy
default: variables:
before_script: SZCI_VERSION: "latest"
- npmci node install stable
- npmci npm install before_script:
- deno install --allow-all --global --name szci https://code.foss.global/ship.zone/szci/raw/branch/master/mod.ts
# OR use the npm package:
# - npm install -g @ship.zone/szci
prepare: prepare:
stage: prepare stage: prepare
script: script:
- npmci prepare npm - szci node install stable
- npmci prepare docker - szci npm install
build: build:
stage: build stage: build
script: script:
- npmci docker build - szci docker build
test: test:
stage: test stage: test
script: script:
- npmci npm test - szci npm test
deploy: deploy:
stage: deploy stage: deploy
script: script:
- npmci publish npm - szci docker push $CI_REGISTRY
- npmci docker push only:
- master
environment:
name: production
url: http://example.com
``` ```
#### GitHub Actions ### GitHub Actions
Similarly, you can set up `npmci` in GitHub Actions:
```yaml ```yaml
name: CI Pipeline name: CI Pipeline
on: on:
push: push:
branches: branches: [main]
- main
jobs: jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install -g @ship.zone/npmci
- run: npmci node install stable
- run: npmci npm install
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: prepare
steps: steps:
- run: npmci docker build - uses: actions/checkout@v3
test: - name: Install SZCI
runs-on: ubuntu-latest run: npm install -g @ship.zone/szci
needs: build
steps:
- run: npmci npm test
deploy: - name: Setup Node.js
runs-on: ubuntu-latest run: szci node install stable
needs: test
steps: - name: Install Dependencies
- run: npmci publish npm run: szci npm install
- run: npmci docker push
- name: Build Docker Images
run: szci docker build
- name: Run Tests
run: szci npm test
- name: Push Images
if: github.ref == 'refs/heads/main'
run: szci docker push ghcr.io
``` ```
#### Gitea CI ## ⚙️ Configuration
Lastly, for Gitea CI: Create an `npmextra.json` file in your project root:
```yaml ```json
image: hosttoday/ht-docker-node:npmci {
"npmci": {
pipelines: "npmGlobalTools": [],
default: "npmAccessLevel": "public",
- step: "npmRegistryUrl": "registry.npmjs.org",
name: Prepare "dockerRegistries": [
image: hosttoday/ht-docker-node:npmci "registry.gitlab.com"
commands: ],
- npmci node install stable "dockerRegistryRepoMap": {
- npmci npm install "registry.gitlab.com": "mygroup/myrepo"
- npmci prepare npm },
- npmci prepare docker "dockerBuildargEnvMap": {
"ARG_NAME": "ENV_VAR_NAME"
- step:
name: Build
image: hosttoday/ht-docker-node:npmci
commands:
- npmci docker build
- step:
name: Test
image: hosttoday/ht-docker-node:npmci
commands:
- npmci npm test
- step:
name: Deploy
image: hosttoday/ht-docker-node:npmci
commands:
- npmci publish npm
- npmci docker push
```
### 2. Handle Node Versions
One of the core features of `npmci` is managing Node versions in your CI environment. You can specify which version of Node to install:
```typescript
import { Npmci } from '@ship.zone/npmci';
async function manageNodeVersions() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.nodejsManager.handleCli({
_: ['node', 'install', 'stable'] // Installs the latest stable version
});
await npmciInstance.nodejsManager.handleCli({
_: ['node', 'install', 'lts'] // Installs the Long-Term Support (LTS) version
});
await npmciInstance.nodejsManager.handleCli({
_: ['node', 'install', 'legacy'] // Installs a legacy version
});
await npmciInstance.nodejsManager.handleCli({
_: ['node', 'install', '14.17.0'] // Install a specific version of Node
});
}
manageNodeVersions().then(() => console.log('Node versions managed successfully.'));
```
### 3. Handling npm and Yarn Tasks
`npmci` provides numerous utilities to streamline npm and yarn workflow tasks within a CI/CD pipeline.
```typescript
import { Npmci } from '@ship.zone/npmci';
async function manageNpmTasks() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.npmManager.handleCli({ _: ['npm', 'install'] }); // Installs dependencies
await npmciInstance.npmManager.handleCli({ _: ['npm', 'test'] }); // Runs tests
await npmciInstance.npmManager.handleCli({ _: ['npm', 'publish'] }); // Publishes the package
}
manageNpmTasks().then(() => console.log('Npm tasks handled successfully.'));
```
### 4. Docker Task Handling
`npmci` simplifies Docker operations, particularly in building, testing, and publishing Docker images.
**Prepare Docker Environment:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function prepareDocker() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'prepare'] }); // Prepares Docker environment
}
prepareDocker().then(() => console.log('Docker environment prepared successfully.'));
```
**Building Docker Images:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function buildDockerImages() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'build'] }); // Builds Docker images
}
buildDockerImages().then(() => console.log('Docker images built successfully.'));
```
**Testing Docker Images:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function testDockerImages() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'test'] }); // Tests Docker images
}
testDockerImages().then(() => console.log('Docker images tested successfully.'));
```
**Publishing Docker Images:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function pushDockerImages() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'push'] }); // Pushes Docker images to registry
}
pushDockerImages().then(() => console.log('Docker images pushed successfully.'));
```
### 5. Managing Docker Registries
`npmci` can handle multiple Docker registries and allows for easy integration within your CI pipeline.
**Logging in to Docker Registries:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function loginToDockerRegistries() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'login'] }); // Logs into all configured Docker registries
}
loginToDockerRegistries().then(() => console.log('Logged into Docker registries.'));
```
**Pulling Docker Images:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function pullDockerImages() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'pull', 'registry.gitlab.com/mygroup/myrepo'] }); // Pulls Docker images from a registry
}
pullDockerImages().then(() => console.log('Docker images pulled successfully.'));
```
### 6. SSH Key Management
`npmci` also simplifies the management of SSH keys, which is crucial for accessing private repositories and servers.
**Preparing SSH Keys:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function prepareSshKeys() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.sshManager.handleCli({ _: ['ssh', 'prepare'] }); // Prepares SSH keys from environment variables
}
prepareSshKeys().then(() => console.log('SSH keys prepared successfully.'));
```
### 7. Cloudron Integration
For users deploying applications on Cloudron, `npmci` provides a set of utilities for automating Cloudron tasks.
**Deploying to Cloudron:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function deployToCloudron() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.cloudronManager.handleCli({
_: ['cloudron', 'deploy']
}); // Deploys application to Cloudron platform
}
deployToCloudron().then(() => console.log('Deployment to Cloudron completed.'));
```
**Preparing Cloudron Manifest:**
Before deployment, replace version placeholders in the Cloudron Manifest:
```typescript
import { Npmci } from '@ship.zone/npmci';
import * as fs from 'fs';
import * as path from 'path';
async function prepareCloudronManifest(version: string) {
const manifestPath = path.join(process.cwd(), "CloudronManifest.json");
let manifestFile = fs.readFileSync(manifestPath, { encoding: 'utf-8' });
manifestFile = manifestFile.replace(/##version##/g, version);
fs.writeFileSync(manifestPath, manifestFile);
console.log('CloudronManifest prepared');
}
async function deployWithPreparedManifest() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await prepareCloudronManifest('1.0.0');
await npmciInstance.cloudronManager.handleCli({
_: ['cloudron', 'deploy']
}); // Deploys application to Cloudron platform
}
deployWithPreparedManifest().then(() => console.log('Deployment to Cloudron with manifest preparation completed.'));
```
### 8. Webhook Triggers
`npmci` supports webhook triggers, allowing you to trigger builds and other activities based on various conditions.
**Triggering Webhooks:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function triggerWebhooks() {
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.triggerManager.handleCli({
_: ['trigger']
}); // Triggers webhooks based on environment variables
}
triggerWebhooks().then(() => console.log('Webhooks triggered successfully.'));
```
### 9. Using the bash Helper
`npmci` includes a bash helper for executing commands within a bash shell, useful for various custom tasks.
**Using bash to Execute Commands:**
```typescript
import { bash } from '@ship.zone/npmci';
async function runCustomBashCommand(command: string) {
const output = await bash(command);
console.log('Command output:', output);
}
runCustomBashCommand('echo Hello World').then(() => console.log('Custom command executed successfully.'));
```
### Full Features and Use Cases
Below is a comprehensive set of features and use cases supported by `npmci`. This section ensures you can take full advantage of the library's capabilities in multiple scenarios.
### Comprehensive Docker Workflow
**Step-by-step Docker Image Handling:**
1. **Detect and Build All Dockerfiles:**
```typescript
import { Npmci } from '@ship.zone/npmci';
async function detectAndBuildDockerfiles() {
const npmciInstance = new Npmci();
await npmciInstance.start();
const dockerfiles = await npmciInstance.dockerManager.getDockerfiles();
console.log('Dockerfiles detected:', dockerfiles.map(d => d.filePath));
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'build'] });
console.log('Dockerfiles built successfully.');
} }
}
}
```
detectAndBuildDockerfiles().then(() => console.log('Docker detection and build process completed.')); ## 🐳 Docker Registry Authentication
SZCI supports automatic authentication with:
- GitLab CI Registry (via `CI_JOB_TOKEN`)
- Custom registries via environment variables
Set `NPMCI_LOGIN_DOCKER*` environment variables:
```sh
NPMCI_LOGIN_DOCKER_1="registry.example.com|username|password"
NPMCI_LOGIN_DOCKER_2="another-registry.com|user2|pass2"
```
## 🏗️ Development
### Prerequisites
- Deno 1.40+ installed
### Building from Source
```sh
# Clone the repository
git clone https://code.foss.global/ship.zone/szci.git
cd szci
# Compile for all platforms
deno task compile
# Compile for current platform only
deno compile --allow-all --output szci mod.ts
# Run tests
deno task test
# Development mode
deno task dev --help
```
### Testing
SZCI uses Deno's native test framework:
```sh
# Run all tests
deno task test
# Run tests in watch mode
deno task test:watch
# Run specific test file
deno test --allow-all test/test.cloudly.ts
```
## 📦 Binary Sizes
The standalone executables are approximately:
- Linux x64: ~800MB
- Linux ARM64: ~800MB
- macOS x64: ~796MB
- macOS ARM64: ~796MB
- Windows x64: ~804MB
Sizes include all dependencies and the Deno runtime.
## 🔄 Migration from npmci
If you're upgrading from the old `@ship.zone/npmci` package:
1. Update package references:
```sh
npm uninstall -g @ship.zone/npmci
npm install -g @ship.zone/szci
``` ```
2. **Test All Dockerfiles:** 2. Update CI configuration files - replace `npmci` with `szci`
```typescript 3. The command interface remains the same, only the binary name changed
import { Npmci } from '@ship.zone/npmci';
async function testAllDockerfiles() { ## 📝 License
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'test'] });
console.log('Dockerfiles tested successfully.');
}
testAllDockerfiles().then(() => console.log('Docker testing process completed.')); MIT © Lossless GmbH
```
3. **Push Dockerfiles to a Registry:** ## 🔗 Links
```typescript - [Repository](https://code.foss.global/ship.zone/szci)
import { Npmci } from '@ship.zone/npmci'; - [Issues](https://code.foss.global/ship.zone/szci/issues)
- [Changelog](./changelog.md)
async function pushDockerfilesToRegistry() { ## 🤝 Contributing
const npmciInstance = new Npmci();
await npmciInstance.start();
await npmciInstance.dockerManager.handleCli({ _: ['docker', 'push'] }); Contributions are welcome! Please feel free to submit a Pull Request.
console.log('Dockerfiles pushed to registry successfully.');
}
pushDockerfilesToRegistry().then(() => console.log('Docker push process completed.')); ---
```
**Dockerfile Class Example:** **Built with Deno 🦕**
Here's a snippet showcasing how the `Dockerfile` class can be used to handle Dockerfile-specific operations:
```typescript
import { Dockerfile } from '@ship.zone/npmci';
async function handleDockerfileOperations() {
// Initialize Dockerfile instances
const dockerfile1 = new Dockerfile(/* required parameters */);
const dockerfile2 = new Dockerfile(/* required parameters */);
// Read and sort Dockerfiles
const dockerfiles = await Dockerfile.readDockerfiles(/* required parameters */);
const sortedDockerfiles = await Dockerfile.sortDockerfiles(dockerfiles);
// Build and Test Dockerfiles
await Dockerfile.buildDockerfiles(sortedDockerfiles);
await Dockerfile.testDockerfiles(sortedDockerfiles);
// Push Dockerfile images to a registry
for (const dockerfile of sortedDockerfiles) {
await dockerfile.push(/* registry and tag parameters */);
}
console.log('Dockerfile operations completed successfully.');
}
handleDockerfileOperations().then(() => console.log('Dockerfile processing flow completed.'));
```
This completes the comprehensive guide to `@ship.zone/npmci`. With the examples and explanations provided, you should be able to harness the full power and flexibility of the library to streamline your CI/CD processes effectively.
undefined

66
scripts/compile-all.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/bin/bash
set -e
# Get version from deno.json
VERSION=$(cat deno.json | grep -o '"version": *"[^"]*"' | cut -d'"' -f4)
BINARY_DIR="dist/binaries"
echo "================================================"
echo " SZCI Compilation Script"
echo " Version: ${VERSION}"
echo "================================================"
echo ""
echo "Compiling for all supported platforms..."
echo ""
# Clean up old binaries and create fresh directory
rm -rf "$BINARY_DIR"
mkdir -p "$BINARY_DIR"
echo "→ Cleaned old binaries from $BINARY_DIR"
echo ""
# Linux x86_64
echo "→ Compiling for Linux x86_64..."
deno compile --allow-all --no-check --output "$BINARY_DIR/szci-linux-x64" \
--target x86_64-unknown-linux-gnu mod.ts
echo " ✓ Linux x86_64 complete"
echo ""
# Linux ARM64
echo "→ Compiling for Linux ARM64..."
deno compile --allow-all --no-check --output "$BINARY_DIR/szci-linux-arm64" \
--target aarch64-unknown-linux-gnu mod.ts
echo " ✓ Linux ARM64 complete"
echo ""
# macOS x86_64
echo "→ Compiling for macOS x86_64..."
deno compile --allow-all --no-check --output "$BINARY_DIR/szci-macos-x64" \
--target x86_64-apple-darwin mod.ts
echo " ✓ macOS x86_64 complete"
echo ""
# macOS ARM64
echo "→ Compiling for macOS ARM64..."
deno compile --allow-all --no-check --output "$BINARY_DIR/szci-macos-arm64" \
--target aarch64-apple-darwin mod.ts
echo " ✓ macOS ARM64 complete"
echo ""
# Windows x86_64
echo "→ Compiling for Windows x86_64..."
deno compile --allow-all --no-check --output "$BINARY_DIR/szci-windows-x64.exe" \
--target x86_64-pc-windows-msvc mod.ts
echo " ✓ Windows x86_64 complete"
echo ""
echo "================================================"
echo " Compilation Summary"
echo "================================================"
echo ""
ls -lh "$BINARY_DIR/" | tail -n +2
echo ""
echo "✓ All binaries compiled successfully!"
echo ""
echo "Binary location: $BINARY_DIR/"
echo ""

228
scripts/install-binary.js Normal file
View File

@@ -0,0 +1,228 @@
#!/usr/bin/env node
/**
* SZCI npm postinstall script
* Downloads the appropriate binary for the current platform from repository releases
*/
import { platform, arch } from 'os';
import { existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import https from 'https';
import { pipeline } from 'stream';
import { promisify } from 'util';
import { createWriteStream } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const streamPipeline = promisify(pipeline);
// Configuration
const REPO_BASE = 'https://code.foss.global/ship.zone/szci';
const VERSION = process.env.npm_package_version || '4.1.37';
function getBinaryInfo() {
const plat = platform();
const architecture = arch();
const platformMap = {
'darwin': 'macos',
'linux': 'linux',
'win32': 'windows'
};
const archMap = {
'x64': 'x64',
'arm64': 'arm64'
};
const mappedPlatform = platformMap[plat];
const mappedArch = archMap[architecture];
if (!mappedPlatform || !mappedArch) {
return { supported: false, platform: plat, arch: architecture };
}
let binaryName = `szci-${mappedPlatform}-${mappedArch}`;
if (plat === 'win32') {
binaryName += '.exe';
}
return {
supported: true,
platform: mappedPlatform,
arch: mappedArch,
binaryName,
originalPlatform: plat
};
}
function downloadFile(url, destination) {
return new Promise((resolve, reject) => {
console.log(`Downloading from: ${url}`);
// Follow redirects
const download = (url, redirectCount = 0) => {
if (redirectCount > 5) {
reject(new Error('Too many redirects'));
return;
}
https.get(url, (response) => {
if (response.statusCode === 301 || response.statusCode === 302) {
console.log(`Following redirect to: ${response.headers.location}`);
download(response.headers.location, redirectCount + 1);
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`));
return;
}
const totalSize = parseInt(response.headers['content-length'], 10);
let downloadedSize = 0;
let lastProgress = 0;
response.on('data', (chunk) => {
downloadedSize += chunk.length;
const progress = Math.round((downloadedSize / totalSize) * 100);
// Only log every 10% to reduce noise
if (progress >= lastProgress + 10) {
console.log(`Download progress: ${progress}%`);
lastProgress = progress;
}
});
const file = createWriteStream(destination);
pipeline(response, file, (err) => {
if (err) {
reject(err);
} else {
console.log('Download complete!');
resolve();
}
});
}).on('error', reject);
};
download(url);
});
}
async function main() {
console.log('===========================================');
console.log(' SZCI - Binary Installation');
console.log('===========================================');
console.log('');
const binaryInfo = getBinaryInfo();
if (!binaryInfo.supported) {
console.error(`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`);
console.error('');
console.error('Supported platforms:');
console.error(' • Linux (x64, arm64)');
console.error(' • macOS (x64, arm64)');
console.error(' • Windows (x64)');
console.error('');
console.error('If you believe your platform should be supported, please file an issue:');
console.error(' https://code.foss.global/ship.zone/szci/issues');
process.exit(1);
}
console.log(`Platform: ${binaryInfo.platform} (${binaryInfo.originalPlatform})`);
console.log(`Architecture: ${binaryInfo.arch}`);
console.log(`Binary: ${binaryInfo.binaryName}`);
console.log(`Version: ${VERSION}`);
console.log('');
// Create dist/binaries directory if it doesn't exist
const binariesDir = join(__dirname, '..', 'dist', 'binaries');
if (!existsSync(binariesDir)) {
console.log('Creating binaries directory...');
mkdirSync(binariesDir, { recursive: true });
}
const binaryPath = join(binariesDir, binaryInfo.binaryName);
// Check if binary already exists and skip download
if (existsSync(binaryPath)) {
console.log('✓ Binary already exists, skipping download');
} else {
// Construct download URL
// Try release URL first, fall back to raw branch if needed
const releaseUrl = `${REPO_BASE}/releases/download/v${VERSION}/${binaryInfo.binaryName}`;
const fallbackUrl = `${REPO_BASE}/raw/branch/master/dist/binaries/${binaryInfo.binaryName}`;
console.log('Downloading platform-specific binary...');
console.log('This may take a moment depending on your connection speed.');
console.log('');
try {
// Try downloading from release
await downloadFile(releaseUrl, binaryPath);
} catch (err) {
console.log(`Release download failed: ${err.message}`);
console.log('Trying fallback URL...');
try {
// Try fallback URL
await downloadFile(fallbackUrl, binaryPath);
} catch (fallbackErr) {
console.error(`❌ Error: Failed to download binary`);
console.error(` Primary URL: ${releaseUrl}`);
console.error(` Fallback URL: ${fallbackUrl}`);
console.error('');
console.error('This might be because:');
console.error('1. The release has not been created yet');
console.error('2. Network connectivity issues');
console.error('3. The version specified does not exist');
console.error('');
console.error('You can try:');
console.error('1. Installing from source: https://code.foss.global/ship.zone/szci');
console.error('2. Downloading the binary manually from the releases page');
console.error('3. Building from source with: deno task compile');
// Clean up partial download
if (existsSync(binaryPath)) {
unlinkSync(binaryPath);
}
process.exit(1);
}
}
console.log(`✓ Binary downloaded successfully`);
}
// On Unix-like systems, ensure the binary is executable
if (binaryInfo.originalPlatform !== 'win32') {
try {
console.log('Setting executable permissions...');
chmodSync(binaryPath, 0o755);
console.log('✓ Binary permissions updated');
} catch (err) {
console.error(`⚠️ Warning: Could not set executable permissions: ${err.message}`);
console.error(' You may need to manually run:');
console.error(` chmod +x ${binaryPath}`);
}
}
console.log('');
console.log('✅ SZCI installation completed successfully!');
console.log('');
console.log('You can now use SZCI by running:');
console.log(' szci --help');
console.log('');
console.log('===========================================');
}
// Run the installation
main().catch(err => {
console.error(`❌ Installation failed: ${err.message}`);
process.exit(1);
});

View File

@@ -1 +1,9 @@
{} {
"gitzone": {
"module": {
"githost": "code.foss.global",
"gitscope": "mygroup",
"gitrepo": "myrepo"
}
}
}

View File

@@ -1,10 +1,21 @@
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; // Disable TLS certificate validation for testing
import { tap, expect } from '@push.rocks/tapbundle'; Deno.env.set('NODE_TLS_REJECT_UNAUTHORIZED', '0');
Deno.env.set('SZCI_TEST', 'true');
Deno.env.set('CI_REPOSITORY_URL', 'https://yyyyyy:xxxxxxxx@gitlab.com/mygroup/myrepo.git');
import * as cloudlyConnectorMod from '../ts/connector.cloudly/cloudlyconnector.js'; import { CloudlyConnector } from '../ts/connector.cloudly/cloudlyconnector.ts';
import { Szci } from '../ts/szci.classes.szci.ts';
tap.test('should be able to announce a container to cloudly', async () => { Deno.test({
const cloudlyConnector = new cloudlyConnectorMod.CloudlyConnector(null); name: 'should be able to announce a container to cloudly',
sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
// Create a proper Szci instance for the connector
const szciInstance = new Szci();
await szciInstance.start();
const cloudlyConnector = new CloudlyConnector(szciInstance);
await cloudlyConnector.announceDockerContainer( await cloudlyConnector.announceDockerContainer(
{ {
registryUrl: 'registry.losssless.com', registryUrl: 'registry.losssless.com',
@@ -14,13 +25,5 @@ tap.test('should be able to announce a container to cloudly', async () => {
}, },
'cloudly.lossless.one' 'cloudly.lossless.one'
); );
},
}); });
tap.test('should close the program despite socket timeout', async (toolsArg) => {
// TODO: remove when unreffed timeouts in webrequest have been solved.
toolsArg.delayFor(0).then(() => {
process.exit();
});
});
tap.start();

View File

@@ -1,107 +1,131 @@
import { tap, expect } from '@push.rocks/tapbundle'; import { assertEquals, assertExists } from '@std/assert';
import * as path from 'path'; import * as path from '@std/path';
import * as smartpath from '@push.rocks/smartpath'; import * as smartpath from '@push.rocks/smartpath';
process.env.NPMTS_TEST = 'true'; // Set up test environment with the NEW SZCI environment variables
process.env.NPMCI_URL_CLOUDLY = 'localhost'; Deno.env.set('SZCI_TEST', 'true');
Deno.env.set('SZCI_URL_CLOUDLY', 'localhost');
Deno.env.set('CI_REPOSITORY_URL', 'https://yyyyyy:xxxxxxxx@gitlab.com/mygroup/myrepo.git');
Deno.env.set('CI_BUILD_TOKEN', 'kjlkjfiudofiufs');
Deno.env.set('SZCI_LOGIN_DOCKER', 'docker.io|someuser|somepass');
Deno.env.set('SZCI_SSHKEY_1', 'hostString|somePrivKey|##');
// set up environment // Get the test assets directory
process.env.CI_REPOSITORY_URL = 'https://yyyyyy:xxxxxxxx@gitlab.com/mygroup/myrepo.git'; const testAssetsDir = path.join(smartpath.get.dirnameFromImportMetaUrl(import.meta.url), 'assets/');
process.env.CI_BUILD_TOKEN = 'kjlkjfiudofiufs';
// Docker // Save original cwd and change to test assets
process.env.NPMCI_LOGIN_DOCKER = 'docker.io|someuser|somepass'; const originalCwd = Deno.cwd();
Deno.chdir(testAssetsDir);
// SSH env import type { Dockerfile } from '../ts/manager.docker/mod.classes.dockerfile.ts';
process.env.NPMCI_SSHKEY_1 = 'hostString|somePrivKey|##'; import { Szci } from '../ts/szci.classes.szci.ts';
import * as DockerfileModule from '../ts/manager.docker/mod.classes.dockerfile.ts';
process.cwd = () => {
return path.join(smartpath.get.dirnameFromImportMetaUrl(import.meta.url), 'assets/');
};
import type * as npmciTypes from '../ts/index.js';
const npmci = await import('../ts/index.js');
// ====== // ======
// Docker // Docker
// ====== // ======
let dockerfile1: npmciTypes.Dockerfile; let dockerfile1: Dockerfile;
let dockerfile2: npmciTypes.Dockerfile; let dockerfile2: Dockerfile;
let sortableArray: npmciTypes.Dockerfile[]; let sortableArray: Dockerfile[];
tap.test('should return valid Dockerfiles', async () => { Deno.test('should return valid Dockerfiles', async () => {
const npmciInstance = new npmci.Npmci(); const szciInstance = new Szci();
await npmciInstance.start(); await szciInstance.start();
dockerfile1 = new npmci.Dockerfile(npmciInstance.dockerManager, { dockerfile1 = new DockerfileModule.Dockerfile(szciInstance.dockerManager, {
filePath: './Dockerfile', filePath: './Dockerfile',
read: true, read: true,
}); });
dockerfile2 = new npmci.Dockerfile(npmciInstance.dockerManager, { dockerfile2 = new DockerfileModule.Dockerfile(szciInstance.dockerManager, {
filePath: './Dockerfile_sometag1', filePath: './Dockerfile_sometag1',
read: true, read: true,
}); });
expect(dockerfile1.version).toEqual('latest'); assertEquals(dockerfile1.version, 'latest');
return expect(dockerfile2.version).toEqual('sometag1'); assertEquals(dockerfile2.version, 'sometag1');
}); });
tap.test('should read a directory of Dockerfiles', async () => { Deno.test('should read a directory of Dockerfiles', async () => {
const npmciInstance = new npmci.Npmci(); const szciInstance = new Szci();
await npmciInstance.start(); await szciInstance.start();
return npmci.Dockerfile.readDockerfiles(npmciInstance.dockerManager).then( const readDockerfilesArray = await DockerfileModule.Dockerfile.readDockerfiles(
async (readDockerfilesArrayArg: npmciTypes.Dockerfile[]) => { szciInstance.dockerManager
sortableArray = readDockerfilesArrayArg;
return expect(readDockerfilesArrayArg[1].version).toEqual('sometag1');
}
); );
sortableArray = readDockerfilesArray;
// The test assets directory should have multiple Dockerfiles
assertExists(readDockerfilesArray, 'readDockerfilesArray should exist');
assertEquals(readDockerfilesArray.length > 0, true, 'Should find at least one Dockerfile');
// Find the sometag1 dockerfile
const sometag1Dockerfile = readDockerfilesArray.find(df => df.version === 'sometag1');
assertExists(sometag1Dockerfile, 'Should find Dockerfile_sometag1');
assertEquals(sometag1Dockerfile?.version, 'sometag1');
}); });
tap.test('should sort an array of Dockerfiles', async () => { Deno.test('should sort an array of Dockerfiles', async () => {
return npmci.Dockerfile.sortDockerfiles(sortableArray).then( // Use the sortableArray from previous test, or create a new one if empty
async (sortedArrayArg: npmciTypes.Dockerfile[]) => { if (!sortableArray || sortableArray.length === 0) {
console.log(sortedArrayArg); const szciInstance = new Szci();
await szciInstance.start();
sortableArray = await DockerfileModule.Dockerfile.readDockerfiles(szciInstance.dockerManager);
} }
);
const sortedArray = await DockerfileModule.Dockerfile.sortDockerfiles(sortableArray);
assertExists(sortedArray, 'sortedArray should exist');
console.log('Sorted dockerfiles:', sortedArray.map(df => df.cleanTag));
}); });
tap.test('should build all Dockerfiles', async () => { Deno.test({
const npmciInstance = new npmci.Npmci(); name: 'should build all Dockerfiles',
await npmciInstance.start(); // Allow resource leaks since smartshell creates background processes
return npmciInstance.dockerManager.handleCli({ sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
const szciInstance = new Szci();
await szciInstance.start();
await szciInstance.dockerManager.handleCli({
_: ['docker', 'build'], _: ['docker', 'build'],
}); });
},
}); });
tap.test('should test all Dockerfiles', async () => { Deno.test({
const npmciInstance = new npmci.Npmci(); name: 'should test all Dockerfiles',
await npmciInstance.start(); // Allow resource leaks since smartshell creates background processes
return npmciInstance.dockerManager.handleCli({ sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
const szciInstance = new Szci();
await szciInstance.start();
await szciInstance.dockerManager.handleCli({
_: ['docker', 'test'], _: ['docker', 'test'],
}); });
},
}); });
tap.test('should test dockerfiles', async () => { Deno.test({
const npmciInstance = new npmci.Npmci(); name: 'should login docker daemon',
await npmciInstance.start(); // Allow resource leaks since smartshell creates background processes
return npmciInstance.dockerManager.handleCli({ sanitizeResources: false,
_: ['docker', 'test'], sanitizeOps: false,
}); fn: async () => {
}); const szciInstance = new Szci();
await szciInstance.start();
tap.test('should login docker daemon', async () => { await szciInstance.dockerManager.handleCli({
const npmciInstance = new npmci.Npmci();
await npmciInstance.start();
return npmciInstance.dockerManager.handleCli({
_: ['docker', 'login'], _: ['docker', 'login'],
}); });
},
}); });
// === // ===
// SSH // SSH
// === // ===
tap.test('should prepare SSH keys', async () => { Deno.test('should prepare SSH keys', async () => {
const npmciModSsh = await import('../ts/mod_ssh/index.js'); // Ensure test mode is set so we don't actually write to disk
return await npmciModSsh.handleCli({ Deno.env.set('SZCI_TEST', 'true');
const szciModSsh = await import('../ts/mod_ssh/index.ts');
await szciModSsh.handleCli({
_: ['ssh', 'prepare'], _: ['ssh', 'prepare'],
}); });
}); });
@@ -109,25 +133,27 @@ tap.test('should prepare SSH keys', async () => {
// ==== // ====
// node // node
// ==== // ====
tap.test('should install a certain version of node', async () => { Deno.test({
const npmciInstance = new npmci.Npmci(); name: 'should install a certain version of node',
await npmciInstance.start(); // Allow resource leaks for this test since nvm creates background processes
await npmciInstance.nodejsManager.handleCli({ sanitizeResources: false,
sanitizeOps: false,
fn: async () => {
const szciInstance = new Szci();
await szciInstance.start();
await szciInstance.nodejsManager.handleCli({
_: ['node', 'install', 'stable'], _: ['node', 'install', 'stable'],
}); });
await npmciInstance.nodejsManager.handleCli({ await szciInstance.nodejsManager.handleCli({
_: ['node', 'install', 'lts'], _: ['node', 'install', 'lts'],
}); });
await npmciInstance.nodejsManager.handleCli({ await szciInstance.nodejsManager.handleCli({
_: ['node', 'install', 'legacy'], _: ['node', 'install', 'legacy'],
}); });
},
}); });
// make sure test ends all right // Restore original working directory after all tests
tap.test('reset paths', async () => { Deno.test('reset paths', () => {
process.cwd = () => { Deno.chdir(originalCwd);
return path.join(__dirname, '../');
};
}); });
tap.start();

View File

@@ -2,7 +2,7 @@
* autocreated commitinfo by @push.rocks/commitinfo * autocreated commitinfo by @push.rocks/commitinfo
*/ */
export const commitinfo = { export const commitinfo = {
name: '@ship.zone/npmci', name: '@ship.zone/szci',
version: '4.1.33', version: '6.0.1',
description: 'A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities.' description: 'Serve Zone CI - A tool to streamline Node.js and Docker workflows within CI environments, particularly GitLab CI, providing various CI/CD utilities. Powered by Deno with standalone executables.'
} }

View File

@@ -1,23 +1,23 @@
import * as plugins from '../npmci.plugins.js'; import * as plugins from '../szci.plugins.ts';
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
/** /**
* connects to cloudly * connects to cloudly
*/ */
export class CloudlyConnector { export class CloudlyConnector {
public npmciRef: Npmci; public szciRef: Szci;
constructor(npmciRefArg: Npmci) { constructor(szciRefArg: Szci) {
this.npmciRef = npmciRefArg; this.szciRef = szciRefArg;
} }
public async announceDockerContainer( public async announceDockerContainer(
optionsArg: plugins.tsclass.container.IContainer, optionsArg: plugins.tsclass.container.IContainer,
testCloudlyUrlArg?: string testCloudlyUrlArg?: string
) { ) {
const cloudlyUrl = testCloudlyUrlArg || this.npmciRef.npmciConfig.getConfig().urlCloudly; const cloudlyUrl = testCloudlyUrlArg || this.szciRef.szciConfig.getConfig().urlCloudly;
if (!cloudlyUrl) { if (!cloudlyUrl) {
logger.log( logger.log(
'warn', 'warn',
@@ -26,14 +26,6 @@ export class CloudlyConnector {
return; return;
} }
const typedrequest = // lets push to cloudly here
new plugins.typedrequest.TypedRequest<plugins.servezoneInterfaces.requests.IRequest_InformAboutNewContainerImage>(
`https://${cloudlyUrl}/typedrequest`,
'servezonestandard_InformAboutNewContainerVersion'
);
const response = await typedrequest.fire({
containerImageInfo: optionsArg,
});
} }
} }

View File

@@ -1,10 +1,10 @@
import { Npmci } from './npmci.classes.npmci.js'; import { Szci } from './szci.classes.szci.ts';
import { Dockerfile } from './manager.docker/mod.classes.dockerfile.js'; import { Dockerfile } from './manager.docker/mod.classes.dockerfile.ts';
export const npmciInstance = new Npmci(); export const szciInstance = new Szci();
export { Dockerfile, Npmci }; export { Dockerfile, Szci };
export const runCli = async () => { export const runCli = async () => {
await npmciInstance.start(); await szciInstance.start();
}; };

View File

@@ -1,14 +1,14 @@
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { bash } from '../npmci.bash.js'; import { bash } from '../szci.bash.ts';
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
export class NpmciCloudronManager { export class SzciCloudronManager {
public npmciRef: Npmci; public szciRef: Szci;
constructor(npmciArg: Npmci) { constructor(szciArg: Szci) {
this.npmciRef = npmciArg; this.szciRef = szciArg;
} }
/** /**
@@ -23,12 +23,12 @@ export class NpmciCloudronManager {
await this.deploy(); await this.deploy();
break; break;
default: default:
logger.log('error', `>>npmci cloudron ...<< action >>${action}<< not supported`); logger.log('error', `>>szci cloudron ...<< action >>${action}<< not supported`);
} }
} else { } else {
logger.log( logger.log(
'info', 'info',
`>>npmci cloudron ...<< cli arguments invalid... Please read the documentation.` `>>szci cloudron ...<< cli arguments invalid... Please read the documentation.`
); );
} }
}; };
@@ -44,14 +44,18 @@ export class NpmciCloudronManager {
logger.log('ok', 'cloudron cli installed'); logger.log('ok', 'cloudron cli installed');
// lets set the version in the CloudronManifest file // lets set the version in the CloudronManifest file
await this.prepareCloudronManifest(this.npmciRef.npmciConfig.getConfig().projectInfo.npm.version); await this.prepareCloudronManifest(this.szciRef.szciConfig.getConfig().projectInfo.npm.version);
logger.log('ok', 'CloudronManifest prepared'); logger.log('ok', 'CloudronManifest prepared');
// lets figure out the docker image tag // lets figure out the docker image tag
const dockerImageTag = await this.npmciRef.npmciConfig.kvStorage.readKey('latestPushedDockerTag'); const dockerImageTag = await this.szciRef.szciConfig.kvStorage.readKey('latestPushedDockerTag');
const appName = this.npmciRef.npmciConfig.getConfig().cloudronAppName; const appName = this.szciRef.szciConfig.getConfig().cloudronAppName;
const cloudronEnvVar = process.env.NPMCI_LOGIN_CLOUDRON; const cloudronEnvVar = Deno.env.get("SZCI_LOGIN_CLOUDRON");
if (!cloudronEnvVar) {
logger.log('error', 'SZCI_LOGIN_CLOUDRON environment variable is not set');
Deno.exit(1);
}
const cloudronServer = cloudronEnvVar.split('|')[0]; const cloudronServer = cloudronEnvVar.split('|')[0];
const cloudronToken = cloudronEnvVar.split('|')[1]; const cloudronToken = cloudronEnvVar.split('|')[1];
await bash(`cloudron update --server ${cloudronServer} --token ${cloudronToken} --image ${dockerImageTag} --app ${appName}`); await bash(`cloudron update --server ${cloudronServer} --token ${cloudronToken} --image ${dockerImageTag} --app ${appName}`);

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,20 +1,20 @@
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { bash } from '../npmci.bash.js'; import { bash } from '../szci.bash.ts';
// classes // classes
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
import { Dockerfile } from './mod.classes.dockerfile.js'; import { Dockerfile } from './mod.classes.dockerfile.ts';
import { DockerRegistry } from './mod.classes.dockerregistry.js'; import { DockerRegistry } from './mod.classes.dockerregistry.ts';
import { RegistryStorage } from './mod.classes.registrystorage.js'; import { RegistryStorage } from './mod.classes.registrystorage.ts';
export class NpmciDockerManager { export class SzciDockerManager {
public npmciRef: Npmci; public szciRef: Szci;
public npmciRegistryStorage = new RegistryStorage(); public szciRegistryStorage = new RegistryStorage();
constructor(npmciArg: Npmci) { constructor(szciArg: Szci) {
this.npmciRef = npmciArg; this.szciRef = szciArg;
} }
/** /**
@@ -42,12 +42,12 @@ export class NpmciDockerManager {
await this.pull(argvArg); await this.pull(argvArg);
break; break;
default: default:
logger.log('error', `>>npmci docker ...<< action >>${action}<< not supported`); logger.log('error', `>>szci docker ...<< action >>${action}<< not supported`);
} }
} else { } else {
logger.log( logger.log(
'info', 'info',
`>>npmci docker ...<< cli arguments invalid... Please read the documentation.` `>>szci docker ...<< cli arguments invalid... Please read the documentation.`
); );
} }
}; };
@@ -69,7 +69,7 @@ export class NpmciDockerManager {
*/ */
public login = async () => { public login = async () => {
await this.prepare(); await this.prepare();
await this.npmciRegistryStorage.loginAll(); await this.szciRegistryStorage.loginAll();
}; };
/** /**
@@ -77,27 +77,27 @@ export class NpmciDockerManager {
*/ */
public prepare = async () => { public prepare = async () => {
// Always login to GitLab Registry // Always login to GitLab Registry
if (process.env.GITLAB_CI) { if (Deno.env.get("GITLAB_CI")) {
console.log('gitlab ci detected'); console.log('gitlab ci detected');
if (!process.env.CI_JOB_TOKEN || process.env.CI_JOB_TOKEN === '') { if (!Deno.env.get("CI_JOB_TOKEN") || Deno.env.get("CI_JOB_TOKEN") === '') {
logger.log('error', 'Running in Gitlab CI, but no registry token specified by gitlab!'); logger.log('error', 'Running in Gitlab CI, but no registry token specified by gitlab!');
process.exit(1); Deno.exit(1);
} }
this.npmciRegistryStorage.addRegistry( this.szciRegistryStorage.addRegistry(
new DockerRegistry({ new DockerRegistry({
registryUrl: 'registry.gitlab.com', registryUrl: 'registry.gitlab.com',
username: 'gitlab-ci-token', username: 'gitlab-ci-token',
password: process.env.CI_JOB_TOKEN, password: Deno.env.get("CI_JOB_TOKEN")!,
}) })
); );
} }
// handle registries // handle registries
await plugins.smartobject.forEachMinimatch( await plugins.smartobject.forEachMinimatch(
process.env, Deno.env.toObject(),
'NPMCI_LOGIN_DOCKER*', 'SZCI_LOGIN_DOCKER*',
async (envString: string) => { async (envString: string) => {
this.npmciRegistryStorage.addRegistry(DockerRegistry.fromEnvString(envString)); this.szciRegistryStorage.addRegistry(DockerRegistry.fromEnvString(envString));
} }
); );
return; return;
@@ -115,14 +115,14 @@ export class NpmciDockerManager {
if (argvArg._.length >= 3 && argvArg._[2] !== 'npmextra') { if (argvArg._.length >= 3 && argvArg._[2] !== 'npmextra') {
dockerRegistryUrls.push(argvArg._[2]); dockerRegistryUrls.push(argvArg._[2]);
} else { } else {
if (this.npmciRef.npmciConfig.getConfig().dockerRegistries.length === 0) { if (this.szciRef.szciConfig.getConfig().dockerRegistries.length === 0) {
logger.log( logger.log(
'warn', 'warn',
`There are no docker registries listed in npmextra.json! This is strange!` `There are no docker registries listed in npmextra.json! This is strange!`
); );
} }
dockerRegistryUrls = dockerRegistryUrls.concat( dockerRegistryUrls = dockerRegistryUrls.concat(
this.npmciRef.npmciConfig.getConfig().dockerRegistries this.szciRef.szciConfig.getConfig().dockerRegistries
); );
} }
@@ -137,7 +137,7 @@ export class NpmciDockerManager {
const dockerfileArray = await Dockerfile.readDockerfiles(this) const dockerfileArray = await Dockerfile.readDockerfiles(this)
.then(Dockerfile.sortDockerfiles) .then(Dockerfile.sortDockerfiles)
.then(Dockerfile.mapDockerfiles); .then(Dockerfile.mapDockerfiles);
const dockerRegistryToPushTo = await this.npmciRegistryStorage.getRegistryByUrl( const dockerRegistryToPushTo = await this.szciRegistryStorage.getRegistryByUrl(
dockerRegistryUrl dockerRegistryUrl
); );
if (!dockerRegistryToPushTo) { if (!dockerRegistryToPushTo) {
@@ -145,7 +145,7 @@ export class NpmciDockerManager {
'error', 'error',
`Cannot push to registry ${dockerRegistryUrl}, because it was not found in the authenticated registry list.` `Cannot push to registry ${dockerRegistryUrl}, because it was not found in the authenticated registry list.`
); );
process.exit(1); Deno.exit(1);
} }
for (const dockerfile of dockerfileArray) { for (const dockerfile of dockerfileArray) {
await dockerfile.push(dockerRegistryToPushTo, suffix); await dockerfile.push(dockerRegistryToPushTo, suffix);
@@ -163,7 +163,7 @@ export class NpmciDockerManager {
if (argvArg._.length >= 4) { if (argvArg._.length >= 4) {
suffix = argvArg._[3]; suffix = argvArg._[3];
} }
const localDockerRegistry = await this.npmciRegistryStorage.getRegistryByUrl(registryUrlArg); const localDockerRegistry = await this.szciRegistryStorage.getRegistryByUrl(registryUrlArg);
const dockerfileArray = await Dockerfile.readDockerfiles(this) const dockerfileArray = await Dockerfile.readDockerfiles(this)
.then(Dockerfile.sortDockerfiles) .then(Dockerfile.sortDockerfiles)
.then(Dockerfile.mapDockerfiles); .then(Dockerfile.mapDockerfiles);

View File

@@ -1,16 +1,16 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import { bash } from '../npmci.bash.js'; import { bash } from '../szci.bash.ts';
import { DockerRegistry } from './mod.classes.dockerregistry.js'; import { DockerRegistry } from './mod.classes.dockerregistry.ts';
import * as helpers from './mod.helpers.js'; import * as helpers from './mod.helpers.ts';
import { NpmciDockerManager } from './index.js'; import { SzciDockerManager } from './index.ts';
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
/** /**
* class Dockerfile represents a Dockerfile on disk in npmci * class Dockerfile represents a Dockerfile on disk in szci
*/ */
export class Dockerfile { export class Dockerfile {
// STATIC // STATIC
@@ -20,16 +20,16 @@ export class Dockerfile {
* @returns Promise<Dockerfile[]> * @returns Promise<Dockerfile[]>
*/ */
public static async readDockerfiles( public static async readDockerfiles(
npmciDockerManagerRefArg: NpmciDockerManager szciDockerManagerRefArg: SzciDockerManager
): Promise<Dockerfile[]> { ): Promise<Dockerfile[]> {
const fileTree = await plugins.smartfile.fs.listFileTree(paths.cwd, 'Dockerfile*'); const fileTree = await plugins.smartfile.fs.listFileTree(paths.getCwd(), 'Dockerfile*');
// create the Dockerfile array // create the Dockerfile array
const readDockerfilesArray: Dockerfile[] = []; const readDockerfilesArray: Dockerfile[] = [];
logger.log('info', `found ${fileTree.length} Dockerfiles:`); logger.log('info', `found ${fileTree.length} Dockerfiles:`);
console.log(fileTree); console.log(fileTree);
for (const dockerfilePath of fileTree) { for (const dockerfilePath of fileTree) {
const myDockerfile = new Dockerfile(npmciDockerManagerRefArg, { const myDockerfile = new Dockerfile(szciDockerManagerRefArg, {
filePath: dockerfilePath, filePath: dockerfilePath,
read: true, read: true,
}); });
@@ -40,43 +40,76 @@ export class Dockerfile {
} }
/** /**
* sorts Dockerfiles into a dependency chain * Sorts Dockerfiles into a build order based on dependencies.
* @param sortableArrayArg an array of instances of class Dockerfile * @param dockerfiles An array of Dockerfile instances.
* @returns Promise<Dockerfile[]> * @returns A Promise that resolves to a sorted array of Dockerfiles.
*/ */
public static async sortDockerfiles(sortableArrayArg: Dockerfile[]): Promise<Dockerfile[]> { public static async sortDockerfiles(dockerfiles: Dockerfile[]): Promise<Dockerfile[]> {
const done = plugins.smartpromise.defer<Dockerfile[]>(); logger.log('info', 'Sorting Dockerfiles based on dependencies...');
logger.log('info', 'sorting Dockerfiles:');
const sortedArray: Dockerfile[] = []; // Map from cleanTag to Dockerfile instance for quick lookup
const cleanTagsOriginal = Dockerfile.cleanTagsArrayFunction(sortableArrayArg, sortedArray); const tagToDockerfile = new Map<string, Dockerfile>();
let sorterFunctionCounter: number = 0; dockerfiles.forEach((dockerfile) => {
const sorterFunction = () => { tagToDockerfile.set(dockerfile.cleanTag, dockerfile);
sortableArrayArg.forEach((dockerfileArg) => {
const cleanTags = Dockerfile.cleanTagsArrayFunction(sortableArrayArg, sortedArray);
if (
cleanTags.indexOf(dockerfileArg.baseImage) === -1 &&
sortedArray.indexOf(dockerfileArg) === -1
) {
sortedArray.push(dockerfileArg);
}
if (cleanTagsOriginal.indexOf(dockerfileArg.baseImage) !== -1) {
dockerfileArg.localBaseImageDependent = true;
}
}); });
if (sortableArrayArg.length === sortedArray.length) {
let counter = 1; // Build the dependency graph
for (const dockerfile of sortedArray) { const graph = new Map<Dockerfile, Dockerfile[]>();
logger.log('info', `tag ${counter}: -> ${dockerfile.cleanTag}`); dockerfiles.forEach((dockerfile) => {
counter++; const dependencies: Dockerfile[] = [];
const baseImage = dockerfile.baseImage;
// Check if the baseImage is among the local Dockerfiles
if (tagToDockerfile.has(baseImage)) {
const baseDockerfile = tagToDockerfile.get(baseImage)!;
dependencies.push(baseDockerfile);
dockerfile.localBaseImageDependent = true;
dockerfile.localBaseDockerfile = baseDockerfile;
} }
done.resolve(sortedArray);
} else if (sorterFunctionCounter < 10) { graph.set(dockerfile, dependencies);
sorterFunctionCounter++; });
sorterFunction();
// Perform topological sort
const sortedDockerfiles: Dockerfile[] = [];
const visited = new Set<Dockerfile>();
const tempMarked = new Set<Dockerfile>();
const visit = (dockerfile: Dockerfile) => {
if (tempMarked.has(dockerfile)) {
throw new Error(`Circular dependency detected involving ${dockerfile.cleanTag}`);
}
if (!visited.has(dockerfile)) {
tempMarked.add(dockerfile);
const dependencies = graph.get(dockerfile) || [];
dependencies.forEach((dep) => visit(dep));
tempMarked.delete(dockerfile);
visited.add(dockerfile);
sortedDockerfiles.push(dockerfile);
} }
}; };
sorterFunction();
return done.promise; try {
dockerfiles.forEach((dockerfile) => {
if (!visited.has(dockerfile)) {
visit(dockerfile);
}
});
} catch (error) {
logger.log('error', (error as Error).message);
throw error;
}
// Log the sorted order
sortedDockerfiles.forEach((dockerfile, index) => {
logger.log(
'info',
`Build order ${index + 1}: ${dockerfile.cleanTag}
with base image ${dockerfile.baseImage}`
);
});
return sortedDockerfiles;
} }
/** /**
@@ -120,7 +153,10 @@ export class Dockerfile {
* returns a version for a docker file * returns a version for a docker file
* @execution SYNC * @execution SYNC
*/ */
public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNameArg: string): string { public static dockerFileVersion(
dockerfileInstanceArg: Dockerfile,
dockerfileNameArg: string
): string {
let versionString: string; let versionString: string;
const versionRegex = /Dockerfile_(.+)$/; const versionRegex = /Dockerfile_(.+)$/;
const regexResultArray = versionRegex.exec(dockerfileNameArg); const regexResultArray = versionRegex.exec(dockerfileNameArg);
@@ -131,25 +167,76 @@ public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNam
} }
versionString = versionString.replace( versionString = versionString.replace(
'##version##', '##version##',
dockerfileInstanceArg.npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().projectInfo.npm.version dockerfileInstanceArg.szciDockerManagerRef.szciRef.szciConfig.getConfig().projectInfo.npm
.version
); );
return versionString; return versionString;
} }
/** /**
* returns the docker base image for a Dockerfile * Extracts the base image from a Dockerfile content without using external libraries.
* @param dockerfileContentArg The content of the Dockerfile as a string.
* @returns The base image specified in the first FROM instruction.
*/ */
public static dockerBaseImage(dockerfileContentArg: string): string { public static dockerBaseImage(dockerfileContentArg: string): string {
const baseImageRegex = /FROM\s([a-zA-z0-9\/\-\:]*)\n?/; const lines = dockerfileContentArg.split(/\r?\n/);
const regexResultArray = baseImageRegex.exec(dockerfileContentArg); const args: { [key: string]: string } = {};
return regexResultArray[1];
for (const line of lines) {
const trimmedLine = line.trim();
// Skip empty lines and comments
if (trimmedLine === '' || trimmedLine.startsWith('#')) {
continue;
}
// Match ARG instructions
const argMatch = trimmedLine.match(/^ARG\s+([^\s=]+)(?:=(.*))?$/i);
if (argMatch) {
const argName = argMatch[1];
const argValue = argMatch[2] !== undefined ? argMatch[2] : Deno.env.get(argName) || '';
args[argName] = argValue;
continue;
}
// Match FROM instructions
const fromMatch = trimmedLine.match(/^FROM\s+(.+?)(?:\s+AS\s+[^\s]+)?$/i);
if (fromMatch) {
let baseImage = fromMatch[1].trim();
// Substitute variables in the base image name
baseImage = Dockerfile.substituteVariables(baseImage, args);
return baseImage;
}
}
throw new Error('No FROM instruction found in Dockerfile');
}
/**
* Substitutes variables in a string, supporting default values like ${VAR:-default}.
* @param str The string containing variables.
* @param vars The object containing variable values.
* @returns The string with variables substituted.
*/
private static substituteVariables(str: string, vars: { [key: string]: string }): string {
return str.replace(/\${([^}:]+)(:-([^}]+))?}/g, (_, varName, __, defaultValue) => {
if (vars[varName] !== undefined) {
return vars[varName];
} else if (defaultValue !== undefined) {
return defaultValue;
} else {
return '';
}
});
} }
/** /**
* returns the docker tag * returns the docker tag
*/ */
public static getDockerTagString( public static getDockerTagString(
npmciDockerManagerRef: NpmciDockerManager, szciDockerManagerRef: SzciDockerManager,
registryArg: string, registryArg: string,
repoArg: string, repoArg: string,
versionArg: string, versionArg: string,
@@ -157,7 +244,7 @@ public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNam
): string { ): string {
// determine wether the repo should be mapped accordingly to the registry // determine wether the repo should be mapped accordingly to the registry
const mappedRepo = const mappedRepo =
npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().dockerRegistryRepoMap[registryArg]; szciDockerManagerRef.szciRef.szciConfig.getConfig().dockerRegistryRepoMap[registryArg];
const repo = (() => { const repo = (() => {
if (mappedRepo) { if (mappedRepo) {
return mappedRepo; return mappedRepo;
@@ -177,67 +264,51 @@ public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNam
} }
public static async getDockerBuildArgs( public static async getDockerBuildArgs(
npmciDockerManagerRef: NpmciDockerManager szciDockerManagerRef: SzciDockerManager
): Promise<string> { ): Promise<string> {
logger.log('info', 'checking for env vars to be supplied to the docker build'); logger.log('info', 'checking for env vars to be supplied to the docker build');
let buildArgsString: string = ''; let buildArgsString: string = '';
for (const dockerArgKey of Object.keys( for (const dockerArgKey of Object.keys(
npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().dockerBuildargEnvMap szciDockerManagerRef.szciRef.szciConfig.getConfig().dockerBuildargEnvMap
)) { )) {
const dockerArgOuterEnvVar = const dockerArgOuterEnvVar =
npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().dockerBuildargEnvMap[dockerArgKey]; szciDockerManagerRef.szciRef.szciConfig.getConfig().dockerBuildargEnvMap[dockerArgKey];
logger.log( logger.log(
'note', 'note',
`docker ARG "${dockerArgKey}" maps to outer env var "${dockerArgOuterEnvVar}"` `docker ARG "${dockerArgKey}" maps to outer env var "${dockerArgOuterEnvVar}"`
); );
const targetValue = process.env[dockerArgOuterEnvVar]; const targetValue = Deno.env.get(dockerArgOuterEnvVar);
buildArgsString = `${buildArgsString} --build-arg ${dockerArgKey}="${targetValue}"`; buildArgsString = `${buildArgsString} --build-arg ${dockerArgKey}="${targetValue}"`;
} }
return buildArgsString; return buildArgsString;
} }
/**
*
*/
public static cleanTagsArrayFunction(
dockerfileArrayArg: Dockerfile[],
trackingArrayArg: Dockerfile[]
): string[] {
const cleanTagsArray: string[] = [];
dockerfileArrayArg.forEach((dockerfileArg) => {
if (trackingArrayArg.indexOf(dockerfileArg) === -1) {
cleanTagsArray.push(dockerfileArg.cleanTag);
}
});
return cleanTagsArray;
}
// INSTANCE // INSTANCE
public npmciDockerManagerRef: NpmciDockerManager; public szciDockerManagerRef: SzciDockerManager;
public filePath: string; public filePath!: string;
public repo: string; public repo: string;
public version: string; public version: string;
public cleanTag: string; public cleanTag: string;
public buildTag: string; public buildTag: string;
public pushTag: string; public pushTag!: string;
public containerName: string; public containerName: string;
public content: string; public content!: string;
public baseImage: string; public baseImage: string;
public localBaseImageDependent: boolean; public localBaseImageDependent: boolean;
public localBaseDockerfile: Dockerfile; public localBaseDockerfile!: Dockerfile;
constructor( constructor(
dockerManagerRefArg: NpmciDockerManager, dockerManagerRefArg: SzciDockerManager,
options: { filePath?: string; fileContents?: string | Buffer; read?: boolean } options: { filePath?: string; fileContents?: string | Uint8Array; read?: boolean }
) { ) {
this.npmciDockerManagerRef = dockerManagerRefArg; this.szciDockerManagerRef = dockerManagerRefArg;
this.filePath = options.filePath; this.filePath = options.filePath!;
this.repo = this.repo =
this.npmciDockerManagerRef.npmciRef.npmciEnv.repo.user + this.szciDockerManagerRef.szciRef.szciEnv.repo.user +
'/' + '/' +
this.npmciDockerManagerRef.npmciRef.npmciEnv.repo.repo; this.szciDockerManagerRef.szciRef.szciEnv.repo.repo;
this.version = Dockerfile.dockerFileVersion(this, plugins.path.parse(options.filePath).base); this.version = Dockerfile.dockerFileVersion(this, plugins.path.parse(this.filePath).base);
this.cleanTag = this.repo + ':' + this.version; this.cleanTag = this.repo + ':' + this.version;
this.buildTag = this.cleanTag; this.buildTag = this.cleanTag;
@@ -254,9 +325,9 @@ public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNam
*/ */
public async build() { public async build() {
logger.log('info', 'now building Dockerfile for ' + this.cleanTag); logger.log('info', 'now building Dockerfile for ' + this.cleanTag);
const buildArgsString = await Dockerfile.getDockerBuildArgs(this.npmciDockerManagerRef); const buildArgsString = await Dockerfile.getDockerBuildArgs(this.szciDockerManagerRef);
const buildCommand = `docker build --label="version=${ const buildCommand = `docker build --label="version=${
this.npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().projectInfo.npm.version this.szciDockerManagerRef.szciRef.szciConfig.getConfig().projectInfo.npm.version
}" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; }" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
await bash(buildCommand); await bash(buildCommand);
return; return;
@@ -265,9 +336,9 @@ public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNam
/** /**
* pushes the Dockerfile to a registry * pushes the Dockerfile to a registry
*/ */
public async push(dockerRegistryArg: DockerRegistry, versionSuffix: string = null) { public async push(dockerRegistryArg: DockerRegistry, versionSuffix?: string) {
this.pushTag = Dockerfile.getDockerTagString( this.pushTag = Dockerfile.getDockerTagString(
this.npmciDockerManagerRef, this.szciDockerManagerRef,
dockerRegistryArg.registryUrl, dockerRegistryArg.registryUrl,
this.repo, this.repo,
this.version, this.version,
@@ -279,21 +350,24 @@ public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNam
await bash(`docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`) await bash(`docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`)
).split('@')[1]; ).split('@')[1];
console.log(`The image ${this.pushTag} has digest ${imageDigest}`); console.log(`The image ${this.pushTag} has digest ${imageDigest}`);
await this.npmciDockerManagerRef.npmciRef.cloudlyConnector.announceDockerContainer({ await this.szciDockerManagerRef.szciRef.cloudlyConnector.announceDockerContainer({
registryUrl: this.pushTag, registryUrl: this.pushTag,
tag: this.buildTag, tag: this.buildTag,
labels: [], labels: [],
version: this.npmciDockerManagerRef.npmciRef.npmciConfig.getConfig().projectInfo.npm.version, version: this.szciDockerManagerRef.szciRef.szciConfig.getConfig().projectInfo.npm.version,
}); });
await this.npmciDockerManagerRef.npmciRef.npmciConfig.kvStorage.writeKey('latestPushedDockerTag', this.pushTag) await this.szciDockerManagerRef.szciRef.szciConfig.kvStorage.writeKey(
'latestPushedDockerTag',
this.pushTag
);
} }
/** /**
* pulls the Dockerfile from a registry * pulls the Dockerfile from a registry
*/ */
public async pull(registryArg: DockerRegistry, versionSuffixArg: string = null) { public async pull(registryArg: DockerRegistry, versionSuffixArg?: string) {
const pullTag = Dockerfile.getDockerTagString( const pullTag = Dockerfile.getDockerTagString(
this.npmciDockerManagerRef, this.szciDockerManagerRef,
registryArg.registryUrl, registryArg.registryUrl,
this.repo, this.repo,
this.version, this.version,
@@ -307,18 +381,18 @@ public static dockerFileVersion(dockerfileInstanceArg: Dockerfile, dockerfileNam
* tests the Dockerfile; * tests the Dockerfile;
*/ */
public async test() { public async test() {
const testFile: string = plugins.path.join(paths.NpmciTestDir, 'test_' + this.version + '.sh'); const testFile: string = plugins.path.join(paths.SzciTestDir, 'test_' + this.version + '.sh');
const testFileExists: boolean = plugins.smartfile.fs.fileExistsSync(testFile); const testFileExists: boolean = plugins.smartfile.fs.fileExistsSync(testFile);
if (testFileExists) { if (testFileExists) {
// run tests // run tests
await bash( await bash(
`docker run --name npmci_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /npmci_test"` `docker run --name szci_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /szci_test"`
); );
await bash(`docker cp ${testFile} npmci_test_container:/npmci_test/test.sh`); await bash(`docker cp ${testFile} szci_test_container:/szci_test/test.sh`);
await bash(`docker commit npmci_test_container npmci_test_image`); await bash(`docker commit szci_test_container szci_test_image`);
await bash(`docker run --entrypoint="bash" npmci_test_image -x /npmci_test/test.sh`); await bash(`docker run --entrypoint="bash" szci_test_image -x /szci_test/test.sh`);
await bash(`docker rm npmci_test_container`); await bash(`docker rm szci_test_container`);
await bash(`docker rmi --force npmci_test_image`); await bash(`docker rmi --force szci_test_image`);
} else { } else {
logger.log('warn', 'skipping tests for ' + this.cleanTag + ' because no testfile was found!'); logger.log('warn', 'skipping tests for ' + this.cleanTag + ' because no testfile was found!');
} }

View File

@@ -1,6 +1,6 @@
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import { bash } from '../npmci.bash.js'; import { bash } from '../szci.bash.ts';
export interface IDockerRegistryConstructorOptions { export interface IDockerRegistryConstructorOptions {
registryUrl: string; registryUrl: string;
@@ -23,8 +23,7 @@ export class DockerRegistry {
const dockerRegexResultArray = envString.split('|'); const dockerRegexResultArray = envString.split('|');
if (dockerRegexResultArray.length !== 3) { if (dockerRegexResultArray.length !== 3) {
logger.log('error', 'malformed docker env var...'); logger.log('error', 'malformed docker env var...');
process.exit(1); Deno.exit(1);
return;
} }
const registryUrl = dockerRegexResultArray[0].replace('https://', '').replace('http://', ''); const registryUrl = dockerRegexResultArray[0].replace('https://', '').replace('http://', '');
const username = dockerRegexResultArray[1]; const username = dockerRegexResultArray[1];

View File

@@ -1,7 +1,7 @@
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import { DockerRegistry } from './mod.classes.dockerregistry.js'; import { DockerRegistry } from './mod.classes.dockerregistry.ts';
export class RegistryStorage { export class RegistryStorage {
objectMap = new plugins.lik.ObjectMap<DockerRegistry>(); objectMap = new plugins.lik.ObjectMap<DockerRegistry>();

View File

@@ -1,5 +1,5 @@
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { Dockerfile } from './mod.classes.dockerfile.js'; import { Dockerfile } from './mod.classes.dockerfile.ts';

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,13 +1,13 @@
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import { bash, bashNoError } from '../npmci.bash.js'; import { bash, bashNoError } from '../szci.bash.ts';
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
export class NpmciGitManager { export class SzciGitManager {
public npmciRef: Npmci; public szciRef: Szci;
constructor(npmciRefArg: Npmci) { constructor(szciRefArg: Szci) {
this.npmciRef = npmciRefArg; this.szciRef = szciRefArg;
} }
/** /**
@@ -22,20 +22,20 @@ export class NpmciGitManager {
await this.mirror(); await this.mirror();
break; break;
default: default:
logger.log('error', `npmci git -> action >>${action}<< not supported!`); logger.log('error', `szci git -> action >>${action}<< not supported!`);
} }
} else { } else {
logger.log('info', `npmci git -> cli arguments invalid! Please read the documentation.`); logger.log('info', `szci git -> cli arguments invalid! Please read the documentation.`);
} }
}; };
public mirror = async () => { public mirror = async () => {
const githubToken = process.env.NPMCI_GIT_GITHUBTOKEN; const githubToken = Deno.env.get("SZCI_GIT_GITHUBTOKEN");
const githubUser = process.env.NPMCI_GIT_GITHUBGROUP || this.npmciRef.npmciEnv.repo.user; const githubUser = Deno.env.get("SZCI_GIT_GITHUBGROUP") || this.szciRef.szciEnv.repo.user;
const githubRepo = process.env.NPMCI_GIT_GITHUB || this.npmciRef.npmciEnv.repo.repo; const githubRepo = Deno.env.get("SZCI_GIT_GITHUB") || this.szciRef.szciEnv.repo.repo;
if ( if (
this.npmciRef.npmciConfig.getConfig().projectInfo.npm.packageJson.private === true || this.szciRef.szciConfig.getConfig().projectInfo.npm.packageJson.private === true ||
this.npmciRef.npmciConfig.getConfig().npmAccessLevel === 'private' this.szciRef.szciConfig.getConfig().npmAccessLevel === 'private'
) { ) {
logger.log( logger.log(
'warn', 'warn',
@@ -64,8 +64,8 @@ export class NpmciGitManager {
// remove old mirrors // remove old mirrors
await bashNoError('git remote rm mirror'); await bashNoError('git remote rm mirror');
} else { } else {
logger.log('error', `cannot find NPMCI_GIT_GITHUBTOKEN env var!`); logger.log('error', `cannot find SZCI_GIT_GITHUBTOKEN env var!`);
process.exit(1); Deno.exit(1);
} }
}; };
} }

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,15 +1,15 @@
import * as plugins from '../npmci.plugins.js'; import * as plugins from '../szci.plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import { bash, bashNoError, nvmAvailable } from '../npmci.bash.js'; import { bash, bashNoError, nvmAvailable } from '../szci.bash.ts';
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
export class NpmciNodeJsManager { export class SzciNodeJsManager {
public npmciRef: Npmci; public szciRef: Szci;
constructor(npmciRefArg: Npmci) { constructor(szciRefArg: Szci) {
this.npmciRef = npmciRefArg; this.szciRef = szciRefArg;
} }
/** /**
@@ -24,15 +24,15 @@ export class NpmciNodeJsManager {
await this.install(argvArg._[2]); await this.install(argvArg._[2]);
break; break;
default: default:
logger.log('error', `>>npmci node ...<< action >>${action}<< not supported`); logger.log('error', `>>szci node ...<< action >>${action}<< not supported`);
process.exit(1); Deno.exit(1);
} }
} else { } else {
logger.log( logger.log(
'error', 'error',
`>>npmci node ...<< cli arguments invalid... Please read the documentation.` `>>szci node ...<< cli arguments invalid... Please read the documentation.`
); );
process.exit(1); Deno.exit(1);
} }
} }
@@ -64,7 +64,7 @@ export class NpmciNodeJsManager {
await bash('npm -v'); await bash('npm -v');
// lets look for further config // lets look for further config
const config = await this.npmciRef.npmciConfig.getConfig(); const config = await this.szciRef.szciConfig.getConfig();
logger.log('info', 'Now checking for needed global npm tools...'); logger.log('info', 'Now checking for needed global npm tools...');
for (const npmTool of config.npmGlobalTools) { for (const npmTool of config.npmGlobalTools) {
logger.log('info', `Checking for global "${npmTool}"`); logger.log('info', `Checking for global "${npmTool}"`);

View File

@@ -1,15 +1,15 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import { bash, bashNoError, nvmAvailable } from '../npmci.bash.js'; import { bash, bashNoError, nvmAvailable } from '../szci.bash.ts';
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
export class NpmciNpmManager { export class SzciNpmManager {
public npmciRef: Npmci; public szciRef: Szci;
constructor(npmciRefArg: Npmci) { constructor(szciRefArg: Szci) {
this.npmciRef = npmciRefArg; this.szciRef = szciRefArg;
} }
/** /**
@@ -36,15 +36,15 @@ export class NpmciNpmManager {
await this.publish(); await this.publish();
break; break;
default: default:
logger.log('error', `>>npmci npm ...<< action >>${action}<< not supported`); logger.log('error', `>>szci npm ...<< action >>${action}<< not supported`);
process.exit(1); Deno.exit(1);
} }
} else { } else {
logger.log( logger.log(
'info', 'info',
`>>npmci npm ...<< cli arguments invalid... Please read the documentation.` `>>szci npm ...<< cli arguments invalid... Please read the documentation.`
); );
process.exit(1); Deno.exit(1);
} }
} }
@@ -53,11 +53,11 @@ export class NpmciNpmManager {
*/ */
public async prepare() { public async prepare() {
logger.log('info', 'running >>npm prepare<<'); logger.log('info', 'running >>npm prepare<<');
const config = this.npmciRef.npmciConfig.getConfig(); const config = this.szciRef.szciConfig.getConfig();
let npmrcFileString: string = ''; let npmrcFileString: string = '';
await plugins.smartobject.forEachMinimatch( await plugins.smartobject.forEachMinimatch(
process.env, Deno.env.toObject(),
'NPMCI_TOKEN_NPM*', 'SZCI_TOKEN_NPM*',
(npmEnvArg: string) => { (npmEnvArg: string) => {
if (!npmEnvArg) { if (!npmEnvArg) {
logger.log('note','found empty token...'); logger.log('note','found empty token...');
@@ -83,14 +83,14 @@ export class NpmciNpmManager {
logger.log('info', 'found one or more access tokens'); logger.log('info', 'found one or more access tokens');
} else { } else {
logger.log('error', 'no access token found! Exiting!'); logger.log('error', 'no access token found! Exiting!');
process.exit(1); Deno.exit(1);
} }
// lets save it to disk // lets save it to disk
plugins.smartfile.memory.toFsSync(npmrcFileString, '/root/.npmrc'); plugins.smartfile.memory.toFsSync(npmrcFileString, '/root/.npmrc');
// lets set the cache directory // lets set the cache directory
await bash(`npm config set cache ${paths.NpmciCacheDir} --global `); await bash(`npm config set cache ${paths.SzciCacheDir} --global `);
return; return;
} }
@@ -103,11 +103,11 @@ export class NpmciNpmManager {
let npmAccessCliString = ``; let npmAccessCliString = ``;
let npmRegistryCliString = ``; let npmRegistryCliString = ``;
let publishVerdaccioAsWell = false; let publishVerdaccioAsWell = false;
const config = this.npmciRef.npmciConfig.getConfig(); const config = this.szciRef.szciConfig.getConfig();
const availableRegistries: string[] = []; const availableRegistries: string[] = [];
await plugins.smartobject.forEachMinimatch( await plugins.smartobject.forEachMinimatch(
process.env, Deno.env.toObject(),
'NPMCI_TOKEN_NPM*', 'SZCI_TOKEN_NPM*',
(npmEnvArg: string) => { (npmEnvArg: string) => {
availableRegistries.push(npmEnvArg.split('|')[0]); availableRegistries.push(npmEnvArg.split('|')[0]);
} }
@@ -127,7 +127,7 @@ export class NpmciNpmManager {
npmRegistryCliString = `--registry=https://${config.npmRegistryUrl}`; npmRegistryCliString = `--registry=https://${config.npmRegistryUrl}`;
} else { } else {
logger.log('error', `no registry url specified. Can't publish!`); logger.log('error', `no registry url specified. Can't publish!`);
process.exit(1); Deno.exit(1);
} }
let publishCommand = `npm publish ${npmAccessCliString} ${npmRegistryCliString} `; let publishCommand = `npm publish ${npmAccessCliString} ${npmRegistryCliString} `;
@@ -166,7 +166,7 @@ export class NpmciNpmManager {
logger.log('success', `Nice!!! The build for the publication was successfull!`); logger.log('success', `Nice!!! The build for the publication was successfull!`);
logger.log('info', `Lets clean up so we don't publish any packages that don't belong to us:`); logger.log('info', `Lets clean up so we don't publish any packages that don't belong to us:`);
// -> clean up before we publish stuff // -> clean up before we publish stuff
await bashNoError(`rm -r ./.npmci_cache`); await bashNoError(`rm -r ./.szci_cache`);
await bash(`rm -r ./node_modules`); await bash(`rm -r ./node_modules`);
logger.log('success', `Cleaned up!:`); logger.log('success', `Cleaned up!:`);

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,10 +1,20 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { logger } from '../szci.logging.ts';
/** /**
* cleans npmci config files * Cleans szci config files from the project directory
*/ */
export let clean = async (): Promise<void> => { export const clean = async (): Promise<void> => {
plugins.smartfile.fs.removeSync(paths.NpmciPackageConfig); try {
return; if (plugins.smartfile.fs.fileExistsSync(paths.SzciPackageConfig)) {
plugins.smartfile.fs.removeSync(paths.SzciPackageConfig);
logger.log('ok', 'Cleaned szci config files');
} else {
logger.log('info', 'No szci config files to clean');
}
} catch (error) {
logger.log('error', `Failed to clean config files: ${(error as Error).message}`);
throw error;
}
}; };

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,15 +1,30 @@
import * as plugins from './mod.plugins.js'; import { bash } from '../szci.bash.ts';
import { bash } from '../npmci.bash.js'; import { logger } from '../szci.logging.ts';
export let command = async () => { /**
let wrappedCommand: string = ''; * Executes a wrapped command passed via CLI arguments.
let argvArray = process.argv; * Usage: szci command <your-command-here>
for (let i = 3; i < argvArray.length; i++) { *
wrappedCommand = wrappedCommand + argvArray[i]; * This allows running arbitrary commands through szci's bash wrapper
if (i + 1 !== argvArray.length) { * which handles nvm and other environment setup.
wrappedCommand = wrappedCommand + ' '; */
} export const command = async (): Promise<void> => {
// Skip 'deno', 'mod.ts', 'command' and get the rest
const commandArgs = Deno.args.slice(1); // Skip 'command'
if (commandArgs.length === 0) {
logger.log('error', 'No command specified. Usage: szci command <your-command>');
Deno.exit(1);
} }
const wrappedCommand = commandArgs.join(' ');
logger.log('info', `Executing command: ${wrappedCommand}`);
try {
await bash(wrappedCommand); await bash(wrappedCommand);
return; logger.log('ok', 'Command executed successfully');
} catch (error) {
logger.log('error', `Command failed: ${(error as Error).message}`);
throw error;
}
}; };

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,15 +1,20 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.ts';
import * as paths from '../npmci.paths.js'; import * as paths from '../szci.paths.ts';
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import { Npmci } from '../npmci.classes.npmci.js'; import { Szci } from '../szci.classes.szci.ts';
export const handleCli = async (npmciRefArg: Npmci, argvArg: any) => { export const handleCli = async (szciRefArg: Szci, argvArg: any) => {
logger.log('info', 'checking execution context'); logger.log('info', 'checking execution context');
const presentRunnerTags = process.env.CI_RUNNER_TAGS.split(',').map((stringArg) => const ciRunnerTags = Deno.env.get("CI_RUNNER_TAGS");
if (!ciRunnerTags) {
logger.log('error', 'CI_RUNNER_TAGS environment variable is not set');
Deno.exit(1);
}
const presentRunnerTags = ciRunnerTags.split(',').map((stringArg) =>
stringArg.trim() stringArg.trim()
); );
let allDesiredGitlabRunnerTagsPresent = true; let allDesiredGitlabRunnerTagsPresent = true;
for (const desiredRunnerTag of npmciRefArg.npmciConfig.getConfig().gitlabRunnerTags) { for (const desiredRunnerTag of szciRefArg.szciConfig.getConfig().gitlabRunnerTags) {
if (!presentRunnerTags.includes(desiredRunnerTag)) { if (!presentRunnerTags.includes(desiredRunnerTag)) {
allDesiredGitlabRunnerTagsPresent = false; allDesiredGitlabRunnerTagsPresent = false;
logger.log( logger.log(
@@ -19,6 +24,6 @@ export const handleCli = async (npmciRefArg: Npmci, argvArg: any) => {
} }
} }
if (!allDesiredGitlabRunnerTagsPresent) { if (!allDesiredGitlabRunnerTagsPresent) {
process.exit(1); Deno.exit(1);
} }
}; };

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,64 +1,97 @@
import { logger } from '../npmci.logging.js'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
let sshInstance: plugins.smartssh.SshInstance; let sshInstance: plugins.smartssh.SshInstance;
export let handleCli = async (argvArg: any) => { /**
* Interface for CLI arguments
*/
interface ICliArgs {
_: string[];
[key: string]: unknown;
}
/**
* Handle SSH CLI commands
*/
export const handleCli = async (argvArg: ICliArgs): Promise<void> => {
if (argvArg._.length >= 2) { if (argvArg._.length >= 2) {
const action: string = argvArg._[1]; const action = argvArg._[1];
switch (action) { switch (action) {
case 'prepare': case 'prepare':
await prepare(); await prepare();
break; break;
default: default:
logger.log('error', `action >>${action}<< not supported`); logger.log('error', `action >>${action}<< not supported`);
process.exit(1); Deno.exit(1);
} }
} else { } else {
logger.log('error', `>>npmci ssh ...<< please specify an action!`); logger.log('error', `>>szci ssh ...<< please specify an action!`);
process.exit(1); Deno.exit(1);
} }
}; };
/** /**
* checks if not undefined * Checks if a string value is defined and not a placeholder
*/ */
const notUndefined = (stringArg: string) => { const isValidValue = (value: string | undefined): boolean => {
return stringArg && stringArg !== 'undefined' && stringArg !== '##'; return Boolean(value && value !== 'undefined' && value !== '##');
}; };
/** /**
* checks for ENV vars in form of NPMCI_SSHKEY_* and deploys any found ones * Checks for ENV vars in form of SZCI_SSHKEY_* and deploys any found ones
*/ */
export let prepare = async () => { export const prepare = async (): Promise<void> => {
sshInstance = new plugins.smartssh.SshInstance(); // init ssh instance sshInstance = new plugins.smartssh.SshInstance();
plugins.smartobject.forEachMinimatch(process.env, 'NPMCI_SSHKEY_*', evaluateSshEnv);
if (!process.env.NPMTS_TEST) { // Get all env vars and filter for SSH keys
const envVars = Deno.env.toObject();
const sshKeyEnvVars = Object.entries(envVars).filter(([key]) =>
key.startsWith('SZCI_SSHKEY_')
);
// Process each SSH key env var
for (const [key, value] of sshKeyEnvVars) {
logger.log('info', `Processing SSH key from ${key}`);
addSshKeyFromEnvVar(value);
}
// Only write to disk if not in test mode
if (!Deno.env.get('SZCI_TEST')) {
try {
sshInstance.writeToDisk(); sshInstance.writeToDisk();
logger.log('ok', 'SSH keys written to disk');
} catch (error) {
logger.log('error', `Failed to write SSH keys: ${(error as Error).message}`);
throw error;
}
} else { } else {
logger.log('info', 'In test mode, so not storing SSH keys to disk!'); logger.log('info', 'In test mode, so not storing SSH keys to disk!');
} }
}; };
/** /**
* gets called for each found SSH ENV Var and deploys it * Parses an SSH key env var and adds it to the SSH instance
* Format: host|privKeyBase64|pubKeyBase64
*/ */
const evaluateSshEnv = async (sshkeyEnvVarArg: string) => { const addSshKeyFromEnvVar = (sshkeyEnvVarArg: string): void => {
const sshEnvArray = sshkeyEnvVarArg.split('|'); const [host, privKeyBase64, pubKeyBase64] = sshkeyEnvVarArg.split('|');
const sshKey = new plugins.smartssh.SshKey(); const sshKey = new plugins.smartssh.SshKey();
logger.log('info', 'Found SSH identity for ' + sshEnvArray[1]);
if (notUndefined(sshEnvArray[0])) { logger.log('info', `Found SSH identity for ${host || 'unknown host'}`);
if (isValidValue(host)) {
logger.log('info', '---> host defined!'); logger.log('info', '---> host defined!');
sshKey.host = sshEnvArray[0]; sshKey.host = host;
} }
if (notUndefined(sshEnvArray[1])) { if (isValidValue(privKeyBase64)) {
logger.log('info', '---> privKey defined!'); logger.log('info', '---> privKey defined!');
sshKey.privKeyBase64 = sshEnvArray[1]; sshKey.privKeyBase64 = privKeyBase64;
} }
if (notUndefined(sshEnvArray[2])) { if (isValidValue(pubKeyBase64)) {
logger.log('info', '---> pubKey defined!'); logger.log('info', '---> pubKey defined!');
sshKey.pubKeyBase64 = sshEnvArray[2]; sshKey.pubKeyBase64 = pubKeyBase64;
} }
sshInstance.addKey(sshKey); sshInstance.addKey(sshKey);
return;
}; };

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,43 +1,101 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.ts';
import { bash } from '../npmci.bash.js'; import { logger } from '../szci.logging.ts';
import { logger } from '../npmci.logging.js';
const triggerValueRegex = /**
/^([a-zA-Z0-9\.]*)\|([a-zA-Z0-9\.]*)\|([a-zA-Z0-9\.]*)\|([a-zA-Z0-9\.]*)\|?([a-zA-Z0-9\.\-\/]*)/; * Interface for parsed trigger configuration
*/
interface ITriggerConfig {
domain: string;
projectId: string;
triggerToken: string;
refName: string;
triggerName: string;
}
export let trigger = async () => { /**
* Regex to parse trigger env var format:
* domain|projectId|triggerToken|refName|triggerName (optional)
*/
const TRIGGER_VALUE_REGEX =
/^([a-zA-Z0-9.]+)\|([a-zA-Z0-9.]+)\|([a-zA-Z0-9.]+)\|([a-zA-Z0-9.]+)\|?([a-zA-Z0-9.\-/]*)$/;
/**
* Execute all configured triggers from environment variables
*/
export const trigger = async (): Promise<void> => {
logger.log('info', 'now running triggers'); logger.log('info', 'now running triggers');
await plugins.smartobject.forEachMinimatch(process.env, 'NPMCI_TRIGGER_*', evaluateTrigger);
// Get all env vars and filter for triggers
const envVars = Deno.env.toObject();
const triggerEnvVars = Object.entries(envVars).filter(([key]) =>
key.startsWith('SZCI_TRIGGER_')
);
if (triggerEnvVars.length === 0) {
logger.log('info', 'no triggers configured');
return;
}
// Process each trigger
for (const [key, value] of triggerEnvVars) {
logger.log('info', `Processing trigger from ${key}`);
await executeTrigger(value);
}
logger.log('ok', `executed ${triggerEnvVars.length} trigger(s)`);
}; };
const evaluateTrigger = async (triggerEnvVarArg) => { /**
const triggerRegexResultArray = triggerValueRegex.exec(triggerEnvVarArg); * Parse a trigger env var string into a config object
const regexDomain = triggerRegexResultArray[1]; */
const regexProjectId = triggerRegexResultArray[2]; const parseTriggerConfig = (triggerEnvVar: string): ITriggerConfig | null => {
const regexProjectTriggerToken = triggerRegexResultArray[3]; const match = TRIGGER_VALUE_REGEX.exec(triggerEnvVar);
const regexRefName = triggerRegexResultArray[4]; if (!match) {
let regexTriggerName; return null;
if (triggerRegexResultArray.length === 6) {
regexTriggerName = triggerRegexResultArray[5];
} else {
regexTriggerName = 'Unnamed Trigger';
} }
logger.log('info', 'Found Trigger!');
logger.log('info', 'triggering build for ref ' + regexRefName + ' of ' + regexTriggerName); return {
plugins.smartrequest.postFormData( domain: match[1],
'https://gitlab.com/api/v3/projects/' + regexProjectId + '/trigger/builds', projectId: match[2],
triggerToken: match[3],
refName: match[4],
triggerName: match[5] || 'Unnamed Trigger',
};
};
/**
* Execute a single trigger by calling the GitLab API
*/
const executeTrigger = async (triggerEnvVar: string): Promise<void> => {
const config = parseTriggerConfig(triggerEnvVar);
if (!config) {
logger.log('error', 'malformed trigger env var, expected format: domain|projectId|token|ref|name');
return;
}
logger.log('info', `Found Trigger: ${config.triggerName}`);
logger.log('info', `Triggering build for ref "${config.refName}" of "${config.triggerName}"`);
try {
await plugins.smartrequest.postFormData(
`https://${config.domain}/api/v3/projects/${config.projectId}/trigger/builds`,
{}, {},
[ [
{ {
name: 'token', name: 'token',
payload: regexProjectTriggerToken, payload: config.triggerToken,
type: 'string', type: 'string',
}, },
{ {
name: 'ref', name: 'ref',
payload: regexRefName, payload: config.refName,
type: 'string', type: 'string',
}, },
] ]
); );
logger.log('ok', `Trigger "${config.triggerName}" executed successfully`);
} catch (error) {
logger.log('error', `Failed to execute trigger: ${(error as Error).message}`);
}
}; };

View File

@@ -1 +1 @@
export * from '../npmci.plugins.js'; export * from '../szci.plugins.ts';

View File

@@ -1,61 +0,0 @@
import * as plugins from './npmci.plugins.js';
// env
import { NpmciEnv } from './npmci.classes.npmcienv.js';
import { NpmciInfo } from './npmci.classes.npmciinfo.js';
import { NpmciCli } from './npmci.classes.npmcicli.js';
import { NpmciConfig } from './npmci.classes.npmciconfig.js';
// connectors
import { CloudlyConnector } from './connector.cloudly/cloudlyconnector.js';
// managers
import { NpmciCloudronManager } from './manager.cloudron/index.js';
import { NpmciDockerManager } from './manager.docker/index.js';
import { NpmciGitManager } from './manager.git/index.js';
import { NpmciNodeJsManager } from './manager.nodejs/index.js';
import { NpmciNpmManager } from './manager.npm/index.js';
export class Npmci {
public analytics: plugins.smartanalytics.Analytics;
public cloudlyConnector: CloudlyConnector;
public npmciEnv: NpmciEnv;
public npmciInfo: NpmciInfo;
public npmciConfig: NpmciConfig;
public npmciCli: NpmciCli;
// managers
public cloudronManager: NpmciCloudronManager;
public dockerManager: NpmciDockerManager;
public gitManager: NpmciGitManager;
public nodejsManager: NpmciNodeJsManager;
public npmManager: NpmciNpmManager;
constructor() {
this.analytics = new plugins.smartanalytics.Analytics({
apiEndPoint: 'https://pubapi.lossless.one/analytics',
projectId: 'gitzone',
appName: 'npmci',
});
}
public async start() {
this.cloudlyConnector = new CloudlyConnector(this);
this.npmciEnv = new NpmciEnv(this);
this.npmciInfo = new NpmciInfo(this);
await this.npmciInfo.printToConsole();
this.npmciCli = new NpmciCli(this);
this.npmciConfig = new NpmciConfig(this);
await this.npmciConfig.init();
// managers
this.cloudronManager = new NpmciCloudronManager(this);
this.dockerManager = new NpmciDockerManager(this);
this.gitManager = new NpmciGitManager(this);
this.nodejsManager = new NpmciNodeJsManager(this);
this.npmManager = new NpmciNpmManager(this);
this.npmciCli.startParse();
}
}

View File

@@ -1,78 +0,0 @@
import * as plugins from './npmci.plugins.js';
import * as paths from './npmci.paths.js';
import { logger } from './npmci.logging.js';
import { Npmci } from './npmci.classes.npmci.js';
/**
* the main config interface for npmci
*/
export interface INpmciOptions {
projectInfo: plugins.projectinfo.ProjectInfo;
// npm
npmGlobalTools: string[];
npmAccessLevel?: 'private' | 'public';
npmRegistryUrl: string;
// docker
dockerRegistries: string[];
dockerRegistryRepoMap: { [key: string]: string };
dockerBuildargEnvMap: { [key: string]: string };
// gitlab
gitlabRunnerTags: string[];
// urls
urlCloudly: string;
// cloudron
cloudronAppName?: string;
}
/**
* a config class for Npmci
*/
export class NpmciConfig {
public npmciRef: Npmci;
public npmciNpmextra: plugins.npmextra.Npmextra;
public kvStorage: plugins.npmextra.KeyValueStore;
public npmciQenv: plugins.qenv.Qenv;
private configObject: INpmciOptions;
constructor(npmciRefArg: Npmci) {
this.npmciRef = npmciRefArg;
}
public async init() {
this.npmciNpmextra = new plugins.npmextra.Npmextra(paths.cwd);
this.kvStorage = new plugins.npmextra.KeyValueStore({
typeArg: 'userHomeDir',
identityArg: `.npmci_${this.npmciRef.npmciEnv.repo.user}_${this.npmciRef.npmciEnv.repo.repo}`,
});
this.npmciQenv = new plugins.qenv.Qenv(
paths.NpmciProjectDir,
paths.NpmciProjectNogitDir,
false
);
this.configObject = {
projectInfo: new plugins.projectinfo.ProjectInfo(paths.cwd),
npmGlobalTools: [],
dockerRegistries: [],
dockerRegistryRepoMap: {},
npmAccessLevel: 'private',
npmRegistryUrl: 'registry.npmjs.org',
gitlabRunnerTags: [],
dockerBuildargEnvMap: {},
urlCloudly: await this.npmciQenv.getEnvVarOnDemand('NPMCI_URL_CLOUDLY'),
};
this.configObject = this.npmciNpmextra.dataFor<INpmciOptions>('npmci', this.configObject);
}
public getConfig(): INpmciOptions {
return this.configObject;
}
}

View File

@@ -1,23 +0,0 @@
import * as plugins from './npmci.plugins.js';
import { Npmci } from './npmci.classes.npmci.js';
export class NpmciEnv {
public npmciRef: Npmci;
public repoString: string;
public repo: plugins.smartstring.GitRepo;
constructor(npmciRefArg: Npmci) {
this.npmciRef = npmciRefArg;
if (process.env.GITLAB_CI) {
this.repoString = process.env.CI_REPOSITORY_URL;
}
if (process.env.NPMCI_COMPUTED_REPOURL) {
this.repoString = process.env.NPMCI_COMPUTED_REPOURL;
}
if (!this.repoString) {
this.repoString = 'https://undefined:undefined@github.com/undefined/undefined.git';
}
this.repo = new plugins.smartstring.GitRepo(this.repoString);
}
}

View File

@@ -1,17 +0,0 @@
import * as plugins from './npmci.plugins.js';
import * as paths from './npmci.paths.js';
import { logger } from './npmci.logging.js';
import { Npmci } from './npmci.classes.npmci.js';
export class NpmciInfo {
public npmciRef: Npmci;
public projectInfo = new plugins.projectinfo.ProjectinfoNpm(paths.NpmciPackageRoot);
constructor(npmciArg: Npmci) {
this.npmciRef = npmciArg;
}
public async printToConsole() {
await logger.log('info', `npmci version: ${this.projectInfo.version}`);
}
}

View File

@@ -1,16 +0,0 @@
import * as plugins from './npmci.plugins.js';
export const cwd = process.cwd();
// package paths
export const NpmciPackageRoot = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'../'
);
export const NpmciPackageConfig = plugins.path.join(NpmciPackageRoot, './config.json');
// project paths
export const NpmciProjectDir = cwd;
export const NpmciProjectNogitDir = plugins.path.join(NpmciProjectDir, './.nogit');
export const NpmciTestDir = plugins.path.join(cwd, './test');
export const NpmciCacheDir = plugins.path.join(cwd, './.npmci_cache');

View File

@@ -1,15 +1,15 @@
import { logger } from './npmci.logging.js'; import { logger } from './szci.logging.ts';
import * as plugins from './npmci.plugins.js'; import * as plugins from './szci.plugins.ts';
import * as paths from './npmci.paths.js'; import * as paths from './szci.paths.ts';
/** /**
* wether nvm is available or not * wether nvm is available or not
*/ */
export let nvmAvailable = plugins.smartpromise.defer<boolean>(); export let nvmAvailable = plugins.smartpromise.defer<boolean>();
/** /**
* the smartshell instance for npmci * the smartshell instance for szci
*/ */
const npmciSmartshell = new plugins.smartshell.Smartshell({ const szciSmartshell = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
sourceFilePaths: [], sourceFilePaths: [],
}); });
@@ -19,16 +19,16 @@ const npmciSmartshell = new plugins.smartshell.Smartshell({
*/ */
const checkToolsAvailable = async () => { const checkToolsAvailable = async () => {
// check for nvm // check for nvm
if (!process.env.NPMTS_TEST) { if (!Deno.env.get('SZCI_TEST')) {
if ( if (
(await npmciSmartshell.execSilent(`bash -c "source /usr/local/nvm/nvm.sh"`)).exitCode === 0 (await szciSmartshell.execSilent(`bash -c "source /usr/local/nvm/nvm.sh"`)).exitCode === 0
) { ) {
npmciSmartshell.shellEnv.addSourceFiles([`/usr/local/nvm/nvm.sh`]); szciSmartshell.shellEnv.addSourceFiles([`/usr/local/nvm/nvm.sh`]);
nvmAvailable.resolve(true); nvmAvailable.resolve(true);
} else if ( } else if (
(await npmciSmartshell.execSilent(`bash -c "source ~/.nvm/nvm.sh"`)).exitCode === 0 (await szciSmartshell.execSilent(`bash -c "source ~/.nvm/nvm.sh"`)).exitCode === 0
) { ) {
npmciSmartshell.shellEnv.addSourceFiles([`~/.nvm/nvm.sh`]); szciSmartshell.shellEnv.addSourceFiles([`~/.nvm/nvm.sh`]);
nvmAvailable.resolve(true); nvmAvailable.resolve(true);
} else { } else {
nvmAvailable.resolve(false); nvmAvailable.resolve(false);
@@ -46,7 +46,7 @@ checkToolsAvailable();
*/ */
export let bash = async (commandArg: string, retryArg: number = 2): Promise<string> => { export let bash = async (commandArg: string, retryArg: number = 2): Promise<string> => {
await nvmAvailable.promise; // make sure nvm check has run await nvmAvailable.promise; // make sure nvm check has run
let execResult: plugins.smartshell.IExecResult; let execResult!: plugins.smartshell.IExecResult;
// determine if we fail // determine if we fail
let failOnError: boolean = true; let failOnError: boolean = true;
@@ -55,20 +55,20 @@ export let bash = async (commandArg: string, retryArg: number = 2): Promise<stri
retryArg = 0; retryArg = 0;
} }
if (!process.env.NPMTS_TEST) { if (!Deno.env.get('SZCI_TEST')) {
// NPMTS_TEST is used during testing // SZCI_TEST is used during testing
for (let i = 0; i <= retryArg; i++) { for (let i = 0; i <= retryArg; i++) {
if (process.env.DEBUG_NPMCI === 'true') { if (Deno.env.get('DEBUG_SZCI') === 'true') {
console.log(commandArg); console.log(commandArg);
} }
execResult = await npmciSmartshell.exec(commandArg); execResult = await szciSmartshell.exec(commandArg);
// determine how bash reacts to error and success // determine how bash reacts to error and success
if (execResult.exitCode !== 0 && i === retryArg) { if (execResult.exitCode !== 0 && i === retryArg) {
// something went wrong and retries are exhausted // something went wrong and retries are exhausted
if (failOnError) { if (failOnError) {
logger.log('error', 'something went wrong and retries are exhausted'); logger.log('error', 'something went wrong and retries are exhausted');
process.exit(1); Deno.exit(1);
} }
} else if (execResult.exitCode === 0) { } else if (execResult.exitCode === 0) {
// everything went fine, or no error wanted // everything went fine, or no error wanted

61
ts/szci.classes.szci.ts Normal file
View File

@@ -0,0 +1,61 @@
import * as plugins from './szci.plugins.ts';
// env
import { SzciEnv } from './szci.classes.szcienv.ts';
import { SzciInfo } from './szci.classes.szciinfo.ts';
import { SzciCli } from './szci.classes.szcicli.ts';
import { SzciConfig } from './szci.classes.szciconfig.ts';
// connectors
import { CloudlyConnector } from './connector.cloudly/cloudlyconnector.ts';
// managers
import { SzciCloudronManager } from './manager.cloudron/index.ts';
import { SzciDockerManager } from './manager.docker/index.ts';
import { SzciGitManager } from './manager.git/index.ts';
import { SzciNodeJsManager } from './manager.nodejs/index.ts';
import { SzciNpmManager } from './manager.npm/index.ts';
export class Szci {
public analytics: plugins.smartanalytics.Analytics;
public cloudlyConnector!: CloudlyConnector;
public szciEnv!: SzciEnv;
public szciInfo!: SzciInfo;
public szciConfig!: SzciConfig;
public szciCli!: SzciCli;
// managers
public cloudronManager!: SzciCloudronManager;
public dockerManager!: SzciDockerManager;
public gitManager!: SzciGitManager;
public nodejsManager!: SzciNodeJsManager;
public npmManager!: SzciNpmManager;
constructor() {
this.analytics = new plugins.smartanalytics.Analytics({
apiEndPoint: 'https://pubapi.lossless.one/analytics',
projectId: 'gitzone',
appName: 'szci',
});
}
public async start() {
this.cloudlyConnector = new CloudlyConnector(this);
this.szciEnv = new SzciEnv(this);
this.szciInfo = new SzciInfo(this);
await this.szciInfo.printToConsole();
this.szciCli = new SzciCli(this);
this.szciConfig = new SzciConfig(this);
await this.szciConfig.init();
// managers
this.cloudronManager = new SzciCloudronManager(this);
this.dockerManager = new SzciDockerManager(this);
this.gitManager = new SzciGitManager(this);
this.nodejsManager = new SzciNodeJsManager(this);
this.npmManager = new SzciNpmManager(this);
this.szciCli.startParse();
}
}

View File

@@ -1,89 +1,89 @@
import { logger } from './npmci.logging.js'; import { logger } from './szci.logging.ts';
import * as plugins from './npmci.plugins.js'; import * as plugins from './szci.plugins.ts';
import * as paths from './npmci.paths.js'; import * as paths from './szci.paths.ts';
import { Npmci } from './npmci.classes.npmci.js'; import { Szci } from './szci.classes.szci.ts';
export class NpmciCli { export class SzciCli {
public npmciRef: Npmci; public szciRef: Szci;
public smartcli: plugins.smartcli.Smartcli; public smartcli: plugins.smartcli.Smartcli;
constructor(npmciArg: Npmci) { constructor(szciArg: Szci) {
this.npmciRef = npmciArg; this.szciRef = szciArg;
this.smartcli = new plugins.smartcli.Smartcli(); this.smartcli = new plugins.smartcli.Smartcli();
this.smartcli.addVersion(this.npmciRef.npmciInfo.projectInfo.version); this.smartcli.addVersion(this.szciRef.szciInfo.version);
// clean // clean
this.smartcli.addCommand('clean').subscribe( this.smartcli.addCommand('clean').subscribe(
async (argv) => { async (argv) => {
const modClean = await import('./mod_clean/index.js'); const modClean = await import('./mod_clean/index.ts');
await modClean.clean(); await modClean.clean();
}, },
(err) => { (err) => {
console.log(err); console.log(err);
process.exit(1); Deno.exit(1);
} }
); );
// cloudron // cloudron
this.smartcli.addCommand('cloudron').subscribe( this.smartcli.addCommand('cloudron').subscribe(
async (argv) => { async (argv) => {
await this.npmciRef.cloudronManager.handleCli(argv); await this.szciRef.cloudronManager.handleCli(argv);
}, },
(err) => { (err) => {
console.log(err); console.log(err);
process.exit(1); Deno.exit(1);
} }
); );
// command // command
this.smartcli.addCommand('command').subscribe( this.smartcli.addCommand('command').subscribe(
async (argv) => { async (argv) => {
const modCommand = await import('./mod_command/index.js'); const modCommand = await import('./mod_command/index.ts');
await modCommand.command(); await modCommand.command();
}, },
(err) => { (err) => {
console.log(err); console.log(err);
process.exit(1); Deno.exit(1);
} }
); );
// git // git
this.smartcli.addCommand('git').subscribe( this.smartcli.addCommand('git').subscribe(
async (argvArg) => { async (argvArg) => {
await this.npmciRef.gitManager.handleCli(argvArg); await this.szciRef.gitManager.handleCli(argvArg);
}, },
(err) => { (err) => {
console.log(err); console.log(err);
process.exit(1); Deno.exit(1);
} }
); );
// build // build
this.smartcli.addCommand('docker').subscribe( this.smartcli.addCommand('docker').subscribe(
async (argvArg) => { async (argvArg) => {
await this.npmciRef.dockerManager.handleCli(argvArg); await this.szciRef.dockerManager.handleCli(argvArg);
}, },
(err) => { (err) => {
console.log(err); console.log(err);
process.exit(1); Deno.exit(1);
} }
); );
// node // node
this.smartcli.addCommand('node').subscribe( this.smartcli.addCommand('node').subscribe(
async (argvArg) => { async (argvArg) => {
await this.npmciRef.nodejsManager.handleCli(argvArg); await this.szciRef.nodejsManager.handleCli(argvArg);
}, },
(err) => { (err) => {
console.log(err); console.log(err);
process.exit(1); Deno.exit(1);
} }
); );
// npm // npm
this.smartcli.addCommand('npm').subscribe( this.smartcli.addCommand('npm').subscribe(
async (argvArg) => { async (argvArg) => {
await this.npmciRef.npmManager.handleCli(argvArg); await this.szciRef.npmManager.handleCli(argvArg);
}, },
(err) => { (err) => {
console.log(err); console.log(err);
@@ -91,25 +91,25 @@ export class NpmciCli {
); );
this.smartcli.addCommand('precheck').subscribe(async (argvArg) => { this.smartcli.addCommand('precheck').subscribe(async (argvArg) => {
const modPrecheck = await import('./mod_precheck/index.js'); const modPrecheck = await import('./mod_precheck/index.ts');
await modPrecheck.handleCli(this.npmciRef, argvArg); await modPrecheck.handleCli(this.szciRef, argvArg);
}); });
// trigger // trigger
this.smartcli.addCommand('ssh').subscribe(async (argvArg) => { this.smartcli.addCommand('ssh').subscribe(async (argvArg) => {
const modSsh = await import('./mod_ssh/index.js'); const modSsh = await import('./mod_ssh/index.ts');
await modSsh.handleCli(argvArg); await modSsh.handleCli(argvArg);
}); });
// trigger // trigger
this.smartcli.addCommand('trigger').subscribe( this.smartcli.addCommand('trigger').subscribe(
async (argv) => { async (argv) => {
const modTrigger = await import('./mod_trigger/index.js'); const modTrigger = await import('./mod_trigger/index.ts');
await modTrigger.trigger(); await modTrigger.trigger();
}, },
(err) => { (err) => {
console.log(err); console.log(err);
process.exit(1); Deno.exit(1);
} }
); );
} }

View File

@@ -0,0 +1,78 @@
import * as plugins from './szci.plugins.ts';
import * as paths from './szci.paths.ts';
import { logger } from './szci.logging.ts';
import { Szci } from './szci.classes.szci.ts';
/**
* the main config interface for szci
*/
export interface ISzciOptions {
projectInfo: plugins.projectinfo.ProjectInfo;
// npm
npmGlobalTools: string[];
npmAccessLevel?: 'private' | 'public';
npmRegistryUrl: string;
// docker
dockerRegistries: string[];
dockerRegistryRepoMap: { [key: string]: string };
dockerBuildargEnvMap: { [key: string]: string };
// gitlab
gitlabRunnerTags: string[];
// urls
urlCloudly?: string;
// cloudron
cloudronAppName?: string;
}
/**
* a config class for Szci
*/
export class SzciConfig {
public szciRef: Szci;
public szciNpmextra!: plugins.npmextra.Npmextra;
public kvStorage!: plugins.npmextra.KeyValueStore;
public szciQenv!: plugins.qenv.Qenv;
private configObject!: ISzciOptions;
constructor(szciRefArg: Szci) {
this.szciRef = szciRefArg;
}
public async init() {
this.szciNpmextra = new plugins.npmextra.Npmextra(paths.cwd);
this.kvStorage = new plugins.npmextra.KeyValueStore({
typeArg: 'userHomeDir',
identityArg: `.szci_${this.szciRef.szciEnv.repo.user}_${this.szciRef.szciEnv.repo.repo}`,
});
this.szciQenv = new plugins.qenv.Qenv(
paths.SzciProjectDir,
paths.SzciProjectNogitDir,
false
);
this.configObject = {
projectInfo: new plugins.projectinfo.ProjectInfo(paths.cwd),
npmGlobalTools: [],
dockerRegistries: [],
dockerRegistryRepoMap: {},
npmAccessLevel: 'private',
npmRegistryUrl: 'registry.npmjs.org',
gitlabRunnerTags: [],
dockerBuildargEnvMap: {},
urlCloudly: await this.szciQenv.getEnvVarOnDemand('SZCI_URL_CLOUDLY'),
};
this.configObject = this.szciNpmextra.dataFor<ISzciOptions>('@ship.zone/szci', this.configObject);
}
public getConfig(): ISzciOptions {
return this.configObject;
}
}

View File

@@ -0,0 +1,25 @@
import * as plugins from './szci.plugins.ts';
import { Szci } from './szci.classes.szci.ts';
export class SzciEnv {
public szciRef: Szci;
public repoString: string;
public repo: plugins.smartstring.GitRepo;
constructor(szciRefArg: Szci) {
this.szciRef = szciRefArg;
// Determine repo string from environment
let repoUrl: string | undefined;
if (Deno.env.get("GITLAB_CI")) {
repoUrl = Deno.env.get("CI_REPOSITORY_URL");
}
if (!repoUrl && Deno.env.get("SZCI_COMPUTED_REPOURL")) {
repoUrl = Deno.env.get("SZCI_COMPUTED_REPOURL");
}
this.repoString = repoUrl || 'https://undefined:undefined@github.com/undefined/undefined.git';
this.repo = new plugins.smartstring.GitRepo(this.repoString);
}
}

View File

@@ -0,0 +1,18 @@
import * as plugins from './szci.plugins.ts';
import * as paths from './szci.paths.ts';
import { logger } from './szci.logging.ts';
import { Szci } from './szci.classes.szci.ts';
import denoConfig from '../deno.json' with { type: 'json' };
export class SzciInfo {
public szciRef: Szci;
public version = denoConfig.version;
constructor(szciArg: Szci) {
this.szciRef = szciArg;
}
public async printToConsole() {
await logger.log('info', `szci version: ${this.version}`);
}
}

View File

@@ -1,4 +1,4 @@
import * as plugins from './npmci.plugins.js'; import * as plugins from './szci.plugins.ts';
export const logger = new plugins.smartlog.Smartlog({ export const logger = new plugins.smartlog.Smartlog({
logContext: { logContext: {
@@ -6,7 +6,7 @@ export const logger = new plugins.smartlog.Smartlog({
companyunit: 'Some Unit', companyunit: 'Some Unit',
containerName: 'Some ContainerName', containerName: 'Some ContainerName',
environment: 'test', environment: 'test',
runtime: 'node', runtime: 'deno',
zone: 'Some Zone', zone: 'Some Zone',
}, },
}); });

31
ts/szci.paths.ts Normal file
View File

@@ -0,0 +1,31 @@
import * as plugins from './szci.plugins.ts';
/**
* Get current working directory (evaluated at call time, not module load time)
*/
export const getCwd = (): string => Deno.cwd();
/**
* Current working directory - use getCwd() if you need the live value after chdir
* @deprecated Use getCwd() for dynamic cwd resolution
*/
export const cwd = Deno.cwd();
// package paths
export const SzciPackageRoot = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'../'
);
export const SzciPackageConfig = plugins.path.join(SzciPackageRoot, './config.json');
// project paths - use functions for dynamic resolution
export const getSzciProjectDir = (): string => getCwd();
export const getSzciProjectNogitDir = (): string => plugins.path.join(getCwd(), './.nogit');
export const getSzciTestDir = (): string => plugins.path.join(getCwd(), './test');
export const getSzciCacheDir = (): string => plugins.path.join(getCwd(), './.szci_cache');
// Static paths (for backwards compatibility - captured at module load)
export const SzciProjectDir = cwd;
export const SzciProjectNogitDir = plugins.path.join(SzciProjectDir, './.nogit');
export const SzciTestDir = plugins.path.join(cwd, './test');
export const SzciCacheDir = plugins.path.join(cwd, './.szci_cache');

View File

@@ -1,5 +1,5 @@
// node native // Deno std libraries
import * as path from 'path'; import * as path from '@std/path';
export { path }; export { path };

View File

@@ -1,14 +0,0 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
},
"exclude": [
"dist_*/**/*.d.ts"
]
}

64
uninstall.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# SZCI Uninstaller Script
# Removes SZCI from the system (installed via install.sh)
#
# Usage:
# sudo bash uninstall.sh
set -e
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "Please run as root (sudo bash uninstall.sh)"
exit 1
fi
echo "================================================"
echo " SZCI Uninstaller"
echo " Serve Zone CI/CD Tool Removal"
echo "================================================"
echo ""
# Default installation directory
INSTALL_DIR="/opt/szci"
# Step 1: Remove global symlinks
echo "Checking for global symlinks..."
if [ -L "/usr/local/bin/szci" ]; then
echo "Removing symlink: /usr/local/bin/szci"
rm -f /usr/local/bin/szci
fi
if [ -L "/usr/bin/szci" ]; then
echo "Removing symlink: /usr/bin/szci"
rm -f /usr/bin/szci
fi
# Step 2: Remove installation directory
if [ -d "$INSTALL_DIR" ]; then
echo "Removing installation directory: $INSTALL_DIR"
rm -rf "$INSTALL_DIR"
else
echo "Installation directory not found: $INSTALL_DIR"
fi
echo ""
echo "================================================"
echo " SZCI Uninstallation Complete!"
echo "================================================"
echo ""
# Check for npm global installation
NODE_PATH=$(which node 2>/dev/null || true)
if [ -n "$NODE_PATH" ]; then
NPM_PATH=$(dirname "$NODE_PATH")/npm
if [ -x "$NPM_PATH" ]; then
echo "If you installed SZCI via npm, you may want to uninstall it with:"
echo " npm uninstall -g @ship.zone/szci"
echo ""
fi
fi
echo "SZCI has been removed from your system."
echo ""