BREAKING CHANGE(szci): delegate Docker operations to @git.zone/tsdocker, remove internal Docker managers and deprecated modules, simplify CLI and env var handling

This commit is contained in:
2026-02-06 16:12:41 +00:00
parent 5d18e53e30
commit 9d295f2633
28 changed files with 346 additions and 1181 deletions

View File

@@ -1,5 +1,18 @@
# Changelog # Changelog
## 2026-02-06 - 7.0.0 - BREAKING CHANGE(szci)
delegate Docker operations to @git.zone/tsdocker, remove internal Docker managers and deprecated modules, simplify CLI and env var handling
- Delegate all Docker actions to @git.zone/tsdocker via npx; SzciDockerManager now bridges SZCI_LOGIN_DOCKER* and CI_JOB_TOKEN to DOCKER_REGISTRY_N and invokes npx @git.zone/tsdocker for build/login/push/pull/test.
- Removed internal Docker implementation: Dockerfile, DockerRegistry, RegistryStorage, related helpers and plugin wrappers removed from ts/manager.docker.
- Removed Cloudron manager and other deprecated modules and their plugin shims: manager.cloudron, mod_clean, mod_command, mod_precheck, mod_trigger (and corresponding CLI commands: cloudron, clean, command, precheck, trigger).
- CLI and exports simplified: Dockerfile export removed from mod.ts; Szci CLI now delegates docker command to the simplified SzciDockerManager.
- Updated environment handling: bridges SZCI_LOGIN_DOCKER* → DOCKER_REGISTRY_N and auto-bridges GitLab CI CI_JOB_TOKEN to DOCKER_REGISTRY_0.
- Node.js default mappings updated: stable→22, lts→20, legacy→18.
- Dependencies and plugins cleaned-up: removed unused/obsolete deps (e.g. @push.rocks/lik, smartdelay, through2) from deno.json and szci.plugins.ts.
- Docs updated (readme.md, readme.hints.md) to reflect architecture, tsdocker delegation, env var bridging and migration notes from npmci.
- BREAKING: CI configs and any workflows relying on internal Docker classes or the removed CLI commands must be updated to use tsdocker and the new env var/command flows.
## 2025-10-26 - 6.0.1 - fix(tests) ## 2025-10-26 - 6.0.1 - fix(tests)
Migrate tests to Deno native runner and update Deno config Migrate tests to Deno native runner and update Deno config

View File

@@ -39,14 +39,11 @@
"@std/cli": "jsr:@std/cli@^1.0.0", "@std/cli": "jsr:@std/cli@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.0", "@std/assert": "jsr:@std/assert@^1.0.0",
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.1.10", "@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/npmextra": "npm:@push.rocks/npmextra@^5.1.2",
"@push.rocks/projectinfo": "npm:@push.rocks/projectinfo@^5.0.2", "@push.rocks/projectinfo": "npm:@push.rocks/projectinfo@^5.0.2",
"@push.rocks/qenv": "npm:@push.rocks/qenv@^6.0.2", "@push.rocks/qenv": "npm:@push.rocks/qenv@^6.0.2",
"@push.rocks/smartanalytics": "npm:@push.rocks/smartanalytics@^2.0.15", "@push.rocks/smartanalytics": "npm:@push.rocks/smartanalytics@^2.0.15",
"@push.rocks/smartcli": "npm:@push.rocks/smartcli@^4.0.11", "@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/smartfile": "npm:@push.rocks/smartfile@^11.0.21",
"@push.rocks/smartgit": "npm:@push.rocks/smartgit@^3.1.1", "@push.rocks/smartgit": "npm:@push.rocks/smartgit@^3.1.1",
"@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.0.7", "@push.rocks/smartlog": "npm:@push.rocks/smartlog@^3.0.7",
@@ -61,7 +58,6 @@
"@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.0.8", "@push.rocks/smartstring": "npm:@push.rocks/smartstring@^4.0.8",
"@push.rocks/smartexpect": "npm:@push.rocks/smartexpect@^1.0.15", "@push.rocks/smartexpect": "npm:@push.rocks/smartexpect@^1.0.15",
"@serve.zone/api": "npm:@serve.zone/api@^4.3.11", "@serve.zone/api": "npm:@serve.zone/api@^4.3.11",
"@tsclass/tsclass": "npm:@tsclass/tsclass@^4.1.2", "@tsclass/tsclass": "npm:@tsclass/tsclass@^4.1.2"
"through2": "npm:through2@^4.0.2"
} }
} }

1
mod.ts
View File

@@ -45,4 +45,3 @@ if (import.meta.main) {
// Export for programmatic use // Export for programmatic use
export { Szci } from './ts/szci.classes.szci.ts'; export { Szci } from './ts/szci.classes.szci.ts';
export { Dockerfile } from './ts/manager.docker/mod.classes.dockerfile.ts';

View File

@@ -1,5 +1,21 @@
- 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.
## Architecture
szci is a thin orchestrator that delegates heavy lifting to specialized tools:
- **Docker**: Delegates all operations to `@git.zone/tsdocker` via `npx`
- **NPM**: Thin wrapper around `pnpm` with .npmrc generation from `SZCI_TOKEN_NPM*` env vars
- **Node.js**: NVM-based version management (stable=22, lts=20, legacy=18)
- **SSH**: Deploys SSH keys from `SZCI_SSHKEY_*` env vars
- **Git**: GitHub mirroring via `SZCI_GIT_GITHUBTOKEN`
- **Cloudly**: Integration with serve.zone infrastructure
## Docker Env Var Bridging
szci bridges its own env var format to tsdocker's format before delegation:
- `SZCI_LOGIN_DOCKER*` -> `DOCKER_REGISTRY_N` (pipe-delimited: `url|user|pass`)
- GitLab CI: `CI_JOB_TOKEN` -> `DOCKER_REGISTRY_0` for `registry.gitlab.com`
## Deno Migration Status ## Deno Migration Status
@@ -16,8 +32,6 @@ All environment variables have been rebranded from NPMCI_* to SZCI_*:
| `NPMCI_GIT_GITHUBTOKEN` | `SZCI_GIT_GITHUBTOKEN` | | `NPMCI_GIT_GITHUBTOKEN` | `SZCI_GIT_GITHUBTOKEN` |
| `NPMCI_GIT_GITHUBGROUP` | `SZCI_GIT_GITHUBGROUP` | | `NPMCI_GIT_GITHUBGROUP` | `SZCI_GIT_GITHUBGROUP` |
| `NPMCI_GIT_GITHUB` | `SZCI_GIT_GITHUB` | | `NPMCI_GIT_GITHUB` | `SZCI_GIT_GITHUB` |
| `NPMCI_TRIGGER_*` | `SZCI_TRIGGER_*` |
| `NPMCI_LOGIN_CLOUDRON` | `SZCI_LOGIN_CLOUDRON` |
| `NPMCI_SSHKEY_*` | `SZCI_SSHKEY_*` | | `NPMCI_SSHKEY_*` | `SZCI_SSHKEY_*` |
| `NPMCI_LOGIN_DOCKER*` | `SZCI_LOGIN_DOCKER*` | | `NPMCI_LOGIN_DOCKER*` | `SZCI_LOGIN_DOCKER*` |
| `NPMCI_TOKEN_NPM*` | `SZCI_TOKEN_NPM*` | | `NPMCI_TOKEN_NPM*` | `SZCI_TOKEN_NPM*` |
@@ -29,3 +43,13 @@ All environment variables have been rebranded from NPMCI_* to SZCI_*:
- Uses Deno APIs (`Deno.env`, `Deno.cwd`, `Deno.exit`) - Uses Deno APIs (`Deno.env`, `Deno.cwd`, `Deno.exit`)
- Logger runtime set to 'deno' - Logger runtime set to 'deno'
- Dynamic imports use `.ts` extensions - Dynamic imports use `.ts` extensions
## Removed Modules (v7+)
The following were removed as dead/obsolete code:
- `mod_trigger` - Deprecated GitLab API v3
- `mod_precheck` - GitLab-specific runner tag validation
- `manager.cloudron` - Niche Cloudron deployment
- `mod_command` - Trivial bash wrapper
- `mod_clean` - Trivial config cleanup
- Docker manager internals (Dockerfile, DockerRegistry, RegistryStorage classes) - replaced by tsdocker delegation

410
readme.md
View File

@@ -1,28 +1,44 @@
# @ship.zone/szci # @ship.zone/szci
**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. A lightweight CI/CD orchestrator built with Deno that unifies Node.js, Docker, NPM, SSH, and Git workflows into a single CLI. Compiles to standalone binaries — no runtime required.
## ✨ Features ## Issue Reporting and Security
- 🚀 **Standalone Executables** - No Node.js installation required For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
- 🐳 **Docker Integration** - Build, tag, and push Docker images
- 📦 **NPM Management** - Install, test, and publish npm packages ## 🏗️ Architecture
- 🔧 **Node.js Version Management** - Install and switch between Node versions
- 🔐 **SSH Key Management** - Deploy SSH keys from environment variables szci is a **thin orchestrator** — it doesn't reinvent the wheel. Instead, it wires up best-in-class tools and handles the CI-specific glue:
- ☁️ **Multi-Registry Support** - Push to multiple Docker registries
- 🎯 **Cross-Platform** - Binaries for Linux, macOS, and Windows | Domain | What szci does | Delegates to |
|--------|---------------|-------------|
| 🐳 Docker | Bridges `SZCI_LOGIN_DOCKER*` env vars, auto-detects GitLab CI tokens | [`@git.zone/tsdocker`](https://code.foss.global/git.zone/tsdocker) |
| 📦 NPM | Generates `.npmrc` from `SZCI_TOKEN_NPM*` env vars, handles multi-registry publish | `pnpm` + `npm publish` |
| 🟢 Node.js | Manages Node versions via NVM with named aliases | `nvm` |
| 🔑 SSH | Deploys SSH keys from env vars to `~/.ssh` | `@push.rocks/smartssh` |
| 🔀 Git | Mirrors repos to GitHub | `git remote` |
```
CLI Input (Deno.args)
SmartCLI Router
├─ szci docker * → env var bridging → npx @git.zone/tsdocker
├─ szci npm * → .npmrc generation → pnpm / npm publish
├─ szci node * → version aliasing → nvm install
├─ szci git * → token injection → git push --mirror
└─ szci ssh * → key parsing → write to ~/.ssh
```
## 📥 Installation ## 📥 Installation
### NPM (Recommended) ### Via NPM (downloads pre-compiled binary)
```sh ```sh
npm install -g @ship.zone/szci npm install -g @ship.zone/szci
``` ```
The package will automatically download the appropriate pre-compiled binary for your platform. ### From Source
### From Source (Deno Required)
```sh ```sh
git clone https://code.foss.global/ship.zone/szci.git git clone https://code.foss.global/ship.zone/szci.git
@@ -30,86 +46,186 @@ cd szci
deno task compile deno task compile
``` ```
The compiled binaries land in `dist/binaries/` for Linux (x64/arm64), macOS (x64/arm64), and Windows (x64).
## 🚀 Quick Start ## 🚀 Quick Start
```sh ```sh
# Install Node.js # Setup Node.js environment
szci node install stable szci node install stable
# Install dependencies # Install project dependencies
szci npm install szci npm install
# Build Docker images # Build & push Docker images
szci docker build szci docker build
szci docker push registry.example.com
# Run tests # Run tests
szci npm test szci npm test
# Push Docker images
szci docker push registry.example.com
``` ```
## 📖 Usage ## 📖 CLI Reference
### Node.js Management ### `szci docker` — Docker Operations
All Docker commands delegate to `@git.zone/tsdocker` after bridging environment variables.
```sh ```sh
# Install specific Node.js version szci docker build # Build all Dockerfiles in cwd
szci node install lts szci docker login # Login to all configured registries
szci node install stable szci docker prepare # Alias for login
szci node install 18 szci docker push registry.example.com # Push images to a registry
szci docker pull registry.example.com # Pull images from a registry
# Install from .nvmrc szci docker test # Test Dockerfiles
szci node install legacy
``` ```
### NPM Commands **Env var bridging:** Before delegating, szci converts its own env var format to tsdocker's expected format:
```
SZCI_LOGIN_DOCKER_1 → DOCKER_REGISTRY_1
SZCI_LOGIN_DOCKER_2 → DOCKER_REGISTRY_2
...
```
In GitLab CI, `CI_JOB_TOKEN` is automatically bridged as `DOCKER_REGISTRY_0` for `registry.gitlab.com`.
### `szci npm` — NPM/pnpm Workflows
```sh ```sh
# Install dependencies szci npm install # Runs pnpm install
szci npm install szci npm build # Runs pnpm run build
szci npm test # Runs pnpm test
# Run tests szci npm prepare # Generates ~/.npmrc from SZCI_TOKEN_NPM* env vars
szci npm test szci npm publish # Full workflow: prepare → install → build → clean → npm publish
# Publish package
szci npm publish
``` ```
### Docker Workflows The `publish` command supports multi-registry publishing. If `npmAccessLevel` is `public` and a Verdaccio registry is configured, it publishes to both npm and Verdaccio automatically.
### `szci node` — Node.js Version Management
```sh ```sh
# Prepare Docker environment szci node install stable # Node.js 22
szci docker prepare szci node install lts # Node.js 20
szci node install legacy # Node.js 18
# Build all Dockerfiles szci node install 21 # Any specific version
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 Uses NVM under the hood (auto-detected at `/usr/local/nvm/nvm.sh` or `~/.nvm/nvm.sh`). After installing, it:
1. Sets the installed version as `nvm alias default`
2. Upgrades npm to latest
3. Installs any global tools listed in `npmextra.json``npmGlobalTools`
### `szci ssh` — SSH Key Deployment
```sh ```sh
# Deploy SSH keys from environment szci ssh prepare # Deploy SSH keys from env vars to ~/.ssh
szci ssh prepare
``` ```
Set environment variables like `NPMCI_SSHKEY_1`, `NPMCI_SSHKEY_2`, etc. Reads all `SZCI_SSHKEY_*` env vars and writes the keys to disk.
## 🔧 CI/CD Integration ### `szci git` — Git Mirroring
```sh
szci git mirror # Mirror repository to GitHub
```
Pushes all branches and tags to a GitHub mirror. Requires `SZCI_GIT_GITHUBTOKEN`. Refuses to mirror packages marked as `private` in `package.json`.
## ⚙️ Configuration
### `npmextra.json`
Place this in your project root to configure szci behavior:
```json
{
"@ship.zone/szci": {
"npmGlobalTools": ["typescript", "pnpm"],
"npmAccessLevel": "public",
"npmRegistryUrl": "registry.npmjs.org",
"urlCloudly": "https://cloudly.example.com"
}
}
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `npmGlobalTools` | `string[]` | `[]` | Global npm packages to install during `node install` |
| `npmAccessLevel` | `"public" \| "private"` | `"private"` | Access level for `npm publish` |
| `npmRegistryUrl` | `string` | `"registry.npmjs.org"` | Default npm registry |
| `urlCloudly` | `string?` | — | Cloudly endpoint URL (can also be set via `SZCI_URL_CLOUDLY`) |
## 🔐 Environment Variables
### Docker Registry Authentication
Pipe-delimited format: `registry|username|password`
```sh
SZCI_LOGIN_DOCKER_1="registry.example.com|myuser|mypass"
SZCI_LOGIN_DOCKER_2="ghcr.io|token|ghp_xxxx"
```
In **GitLab CI**, the `CI_JOB_TOKEN` is automatically used for `registry.gitlab.com` — no manual config needed.
### NPM Registry Authentication
Pipe-delimited format: `registry|token[|plain]`
```sh
# Base64-encoded token (default)
SZCI_TOKEN_NPM_1="registry.npmjs.org|dGhlLXRva2VuLWhlcmU="
# Plain text token
SZCI_TOKEN_NPM_2="verdaccio.example.com|the-token-here|plain"
```
### SSH Keys
Pipe-delimited format: `host|privKeyBase64|pubKeyBase64`
```sh
SZCI_SSHKEY_1="github.com|BASE64_PRIVATE_KEY|BASE64_PUBLIC_KEY"
SZCI_SSHKEY_2="gitlab.com|BASE64_PRIVATE_KEY|##" # Use ## to skip a field
```
### Git Mirroring
```sh
SZCI_GIT_GITHUBTOKEN="ghp_your_personal_access_token"
SZCI_GIT_GITHUBGROUP="your-org" # Defaults to repo owner
SZCI_GIT_GITHUB="your-repo" # Defaults to repo name
```
### Debugging & Testing
```sh
DEBUG_SZCI="true" # Log all shell commands before execution
SZCI_TEST="true" # Test mode: mocks bash execution, skips SSH disk writes
```
### Full Environment Variable Reference
| Variable | Purpose |
|----------|---------|
| `SZCI_LOGIN_DOCKER_*` | Docker registry credentials (pipe-delimited) |
| `SZCI_TOKEN_NPM_*` | NPM registry auth tokens (pipe-delimited) |
| `SZCI_SSHKEY_*` | SSH key pairs (pipe-delimited) |
| `SZCI_GIT_GITHUBTOKEN` | GitHub personal access token for mirroring |
| `SZCI_GIT_GITHUBGROUP` | GitHub org/user for mirror target |
| `SZCI_GIT_GITHUB` | GitHub repo name for mirror target |
| `SZCI_URL_CLOUDLY` | Cloudly endpoint URL |
| `SZCI_COMPUTED_REPOURL` | Override auto-detected repo URL |
| `DEBUG_SZCI` | Enable verbose shell command logging |
| `SZCI_TEST` | Enable test mode (mock execution) |
## 🔄 CI/CD Integration Examples
### GitLab CI ### GitLab CI
```yaml ```yaml
image: denoland/deno:alpine image: node:22
stages: stages:
- prepare - prepare
@@ -117,13 +233,8 @@ stages:
- test - test
- deploy - deploy
variables:
SZCI_VERSION: "latest"
before_script: before_script:
- deno install --allow-all --global --name szci https://code.foss.global/ship.zone/szci/raw/branch/master/mod.ts - npm install -g @ship.zone/szci
# OR use the npm package:
# - npm install -g @ship.zone/szci
prepare: prepare:
stage: prepare stage: prepare
@@ -149,10 +260,12 @@ deploy:
- master - master
``` ```
> 💡 In GitLab CI, `CI_JOB_TOKEN` is automatically detected — szci will login to `registry.gitlab.com` without any extra config.
### GitHub Actions ### GitHub Actions
```yaml ```yaml
name: CI Pipeline name: CI/CD
on: on:
push: push:
@@ -161,147 +274,104 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install SZCI - name: Install szci
run: npm install -g @ship.zone/szci run: npm install -g @ship.zone/szci
- name: Setup Node.js - name: Setup Node.js
run: szci node install stable run: szci node install stable
- name: Install Dependencies - name: Install & Build
run: szci npm install run: |
szci npm install
szci npm build
- name: Build Docker Images - name: Test
run: szci docker build
- name: Run Tests
run: szci npm test run: szci npm test
- name: Push Images - name: Push Docker
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
run: szci docker push ghcr.io run: szci docker push ghcr.io
env:
SZCI_LOGIN_DOCKER_1: "ghcr.io|${{ github.actor }}|${{ secrets.GITHUB_TOKEN }}"
``` ```
## ⚙️ Configuration ### Gitea CI (Woodpecker)
Create an `npmextra.json` file in your project root: ```yaml
steps:
```json - name: ci
{ image: node:22
"npmci": { commands:
"npmGlobalTools": [], - npm install -g @ship.zone/szci
"npmAccessLevel": "public", - szci node install stable
"npmRegistryUrl": "registry.npmjs.org", - szci npm install
"dockerRegistries": [ - szci docker build
"registry.gitlab.com" - szci npm test
],
"dockerRegistryRepoMap": {
"registry.gitlab.com": "mygroup/myrepo"
},
"dockerBuildargEnvMap": {
"ARG_NAME": "ENV_VAR_NAME"
}
}
}
``` ```
## 🐳 Docker Registry Authentication ## 🔄 Migration from npmci
SZCI supports automatic authentication with: Upgrading from `@ship.zone/npmci`? Three steps:
- GitLab CI Registry (via `CI_JOB_TOKEN`) 1. **Rename the binary** — replace `npmci` with `szci` in all CI scripts
- Custom registries via environment variables 2. **Rename env vars**`NPMCI_*``SZCI_*` (same formats, just the prefix changed)
3. **Docker ops** — now delegate to `@git.zone/tsdocker` (installed automatically via `npx`)
Set `NPMCI_LOGIN_DOCKER*` environment variables: | Old | New |
|-----|-----|
| `NPMCI_LOGIN_DOCKER*` | `SZCI_LOGIN_DOCKER*` |
| `NPMCI_TOKEN_NPM*` | `SZCI_TOKEN_NPM*` |
| `NPMCI_SSHKEY_*` | `SZCI_SSHKEY_*` |
| `NPMCI_GIT_GITHUBTOKEN` | `SZCI_GIT_GITHUBTOKEN` |
| `NPMCI_URL_CLOUDLY` | `SZCI_URL_CLOUDLY` |
| `DEBUG_NPMCI` | `DEBUG_SZCI` |
| `NPMTS_TEST` | `SZCI_TEST` |
## 🛠️ Development
```sh ```sh
NPMCI_LOGIN_DOCKER_1="registry.example.com|username|password" # Type check
NPMCI_LOGIN_DOCKER_2="another-registry.com|user2|pass2" deno task check
```
## 🏗️ 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 # Run tests
deno task test deno task test
# Development mode # Watch tests
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 deno task test:watch
# Run specific test file # Dev mode
deno test --allow-all test/test.cloudly.ts deno task dev -- --help
# Compile binaries for all platforms
deno task compile
# Format code
deno task fmt
# Lint
deno task lint
``` ```
## 📦 Binary Sizes ## License and Legal Information
The standalone executables are approximately: This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
- Linux x64: ~800MB
- Linux ARM64: ~800MB
- macOS x64: ~796MB
- macOS ARM64: ~796MB
- Windows x64: ~804MB
Sizes include all dependencies and the Deno runtime. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
## 🔄 Migration from npmci ### Trademarks
If you're upgrading from the old `@ship.zone/npmci` package: This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
1. Update package references: Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
```sh
npm uninstall -g @ship.zone/npmci
npm install -g @ship.zone/szci
```
2. Update CI configuration files - replace `npmci` with `szci` ### Company Information
3. The command interface remains the same, only the binary name changed Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
## 📝 License For any legal inquiries or further information, please contact us via email at hello@task.vc.
MIT © Lossless GmbH By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
## 🔗 Links
- [Repository](https://code.foss.global/ship.zone/szci)
- [Issues](https://code.foss.global/ship.zone/szci/issues)
- [Changelog](./changelog.md)
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
---
**Built with Deno 🦕**

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@ship.zone/szci', name: '@ship.zone/szci',
version: '6.0.1', version: '7.0.0',
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: '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,9 +1,8 @@
import { Szci } from './szci.classes.szci.ts'; import { Szci } from './szci.classes.szci.ts';
import { Dockerfile } from './manager.docker/mod.classes.dockerfile.ts';
export const szciInstance = new Szci(); export const szciInstance = new Szci();
export { Dockerfile, Szci }; export { Szci };
export const runCli = async () => { export const runCli = async () => {
await szciInstance.start(); await szciInstance.start();

View File

@@ -1,71 +0,0 @@
import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.ts';
import * as paths from '../szci.paths.ts';
import { bash } from '../szci.bash.ts';
import { Szci } from '../szci.classes.szci.ts';
export class SzciCloudronManager {
public szciRef: Szci;
constructor(szciArg: Szci) {
this.szciRef = szciArg;
}
/**
* handle cli input
* @param argvArg
*/
public handleCli = async (argvArg: any) => {
if (argvArg._.length >= 2) {
const action: string = argvArg._[1];
switch (action) {
case 'deploy':
await this.deploy();
break;
default:
logger.log('error', `>>szci cloudron ...<< action >>${action}<< not supported`);
}
} else {
logger.log(
'info',
`>>szci cloudron ...<< cli arguments invalid... Please read the documentation.`
);
}
};
/**
* Replaces the version string in CloudronManifest file
* @param versionArg
*/
public deploy = async () => {
logger.log('info', 'now deploying to cloudron...');
logger.log('info', 'installing cloudron cli...');
await bash(`pnpm install -g cloudron`);
logger.log('ok', 'cloudron cli installed');
// lets set the version in the CloudronManifest file
await this.prepareCloudronManifest(this.szciRef.szciConfig.getConfig().projectInfo.npm.version);
logger.log('ok', 'CloudronManifest prepared');
// lets figure out the docker image tag
const dockerImageTag = await this.szciRef.szciConfig.kvStorage.readKey('latestPushedDockerTag');
const appName = this.szciRef.szciConfig.getConfig().cloudronAppName;
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 cloudronToken = cloudronEnvVar.split('|')[1];
await bash(`cloudron update --server ${cloudronServer} --token ${cloudronToken} --image ${dockerImageTag} --app ${appName}`);
};
private prepareCloudronManifest = async (versionArg: string) => {
const manifestPath = plugins.path.join(paths.cwd, 'CloudronManifest.json');
let manifestFile = plugins.smartfile.fs.toStringSync(manifestPath);
manifestFile = manifestFile.replace(/##version##/g, versionArg);
plugins.smartfile.memory.toFsSync(manifestFile, manifestPath);
logger.log('info', 'Version replaced in CloudronManifest file');
}
}

View File

@@ -1 +0,0 @@
export * from '../szci.plugins.ts';

View File

@@ -1,190 +1,78 @@
import { logger } from '../szci.logging.ts'; import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.ts';
import * as paths from '../szci.paths.ts';
import { bash } from '../szci.bash.ts'; import { bash } from '../szci.bash.ts';
// classes
import { Szci } from '../szci.classes.szci.ts'; import { Szci } from '../szci.classes.szci.ts';
import { Dockerfile } from './mod.classes.dockerfile.ts';
import { DockerRegistry } from './mod.classes.dockerregistry.ts';
import { RegistryStorage } from './mod.classes.registrystorage.ts';
export class SzciDockerManager { export class SzciDockerManager {
public szciRef: Szci; public szciRef: Szci;
public szciRegistryStorage = new RegistryStorage();
constructor(szciArg: Szci) { constructor(szciArg: Szci) {
this.szciRef = szciArg; this.szciRef = szciArg;
} }
/** /**
* handle cli input * Bridges SZCI_LOGIN_DOCKER* env vars to DOCKER_REGISTRY_N format for tsdocker,
* @param argvArg * and handles GitLab CI registry auto-login.
*/
private bridgeEnvVars() {
const env = Deno.env.toObject();
// Bridge GitLab CI registry as DOCKER_REGISTRY_0
if (env['GITLAB_CI']) {
const ciJobToken = env['CI_JOB_TOKEN'];
if (!ciJobToken) {
logger.log('error', 'Running in GitLab CI, but no CI_JOB_TOKEN found!');
Deno.exit(1);
}
Deno.env.set('DOCKER_REGISTRY_0', `registry.gitlab.com|gitlab-ci-token|${ciJobToken}`);
}
// Bridge SZCI_LOGIN_DOCKER* → DOCKER_REGISTRY_N
let registryIndex = 1;
const sortedKeys = Object.keys(env)
.filter((key) => key.startsWith('SZCI_LOGIN_DOCKER'))
.sort();
for (const key of sortedKeys) {
Deno.env.set(`DOCKER_REGISTRY_${registryIndex}`, env[key]);
registryIndex++;
}
}
/**
* Handle cli input by bridging env vars and delegating to tsdocker.
*/ */
public handleCli = async (argvArg: any) => { public handleCli = async (argvArg: any) => {
if (argvArg._.length >= 2) { if (argvArg._.length < 2) {
const action: string = argvArg._[1];
switch (action) {
case 'build':
await this.build();
break;
case 'login':
case 'prepare':
await this.login();
break;
case 'test':
await this.test();
break;
case 'push':
await this.push(argvArg);
break;
case 'pull':
await this.pull(argvArg);
break;
default:
logger.log('error', `>>szci docker ...<< action >>${action}<< not supported`);
}
} else {
logger.log( logger.log(
'info', 'info',
`>>szci docker ...<< cli arguments invalid... Please read the documentation.` `>>szci docker ...<< cli arguments invalid... Please read the documentation.`
); );
}
};
/**
* builds a cwd of Dockerfiles by triggering a promisechain
*/
public build = async () => {
await this.prepare();
logger.log('info', 'now building Dockerfiles...');
await Dockerfile.readDockerfiles(this)
.then(Dockerfile.sortDockerfiles)
.then(Dockerfile.mapDockerfiles)
.then(Dockerfile.buildDockerfiles);
};
/**
* login to the DockerRegistries
*/
public login = async () => {
await this.prepare();
await this.szciRegistryStorage.loginAll();
};
/**
* logs in docker
*/
public prepare = async () => {
// Always login to GitLab Registry
if (Deno.env.get("GITLAB_CI")) {
console.log('gitlab ci detected');
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!');
Deno.exit(1);
}
this.szciRegistryStorage.addRegistry(
new DockerRegistry({
registryUrl: 'registry.gitlab.com',
username: 'gitlab-ci-token',
password: Deno.env.get("CI_JOB_TOKEN")!,
})
);
}
// handle registries
await plugins.smartobject.forEachMinimatch(
Deno.env.toObject(),
'SZCI_LOGIN_DOCKER*',
async (envString: string) => {
this.szciRegistryStorage.addRegistry(DockerRegistry.fromEnvString(envString));
}
);
return; return;
};
/**
* pushes an image towards a registry
* @param argvArg
*/
public push = async (argvArg: any) => {
await this.prepare();
let dockerRegistryUrls: string[] = [];
// lets parse the input of cli and npmextra
if (argvArg._.length >= 3 && argvArg._[2] !== 'npmextra') {
dockerRegistryUrls.push(argvArg._[2]);
} else {
if (this.szciRef.szciConfig.getConfig().dockerRegistries.length === 0) {
logger.log(
'warn',
`There are no docker registries listed in npmextra.json! This is strange!`
);
}
dockerRegistryUrls = dockerRegistryUrls.concat(
this.szciRef.szciConfig.getConfig().dockerRegistries
);
} }
// lets determine the suffix this.bridgeEnvVars();
let suffix = null;
if (argvArg._.length >= 4) {
suffix = argvArg._[3];
}
// lets push to the registries const action: string = argvArg._[1];
for (const dockerRegistryUrl of dockerRegistryUrls) { const extraArgs = argvArg._.slice(2).join(' ');
const dockerfileArray = await Dockerfile.readDockerfiles(this)
.then(Dockerfile.sortDockerfiles) switch (action) {
.then(Dockerfile.mapDockerfiles); case 'build':
const dockerRegistryToPushTo = await this.szciRegistryStorage.getRegistryByUrl( await bash(`npx @git.zone/tsdocker build ${extraArgs}`.trim());
dockerRegistryUrl break;
); case 'login':
if (!dockerRegistryToPushTo) { case 'prepare':
logger.log( await bash(`npx @git.zone/tsdocker login ${extraArgs}`.trim());
'error', break;
`Cannot push to registry ${dockerRegistryUrl}, because it was not found in the authenticated registry list.` case 'test':
); await bash(`npx @git.zone/tsdocker test ${extraArgs}`.trim());
Deno.exit(1); break;
} case 'push':
for (const dockerfile of dockerfileArray) { await bash(`npx @git.zone/tsdocker push ${extraArgs}`.trim());
await dockerfile.push(dockerRegistryToPushTo, suffix); break;
} case 'pull':
await bash(`npx @git.zone/tsdocker pull ${extraArgs}`.trim());
break;
default:
logger.log('error', `>>szci docker ...<< action >>${action}<< not supported`);
} }
}; };
/**
* pulls an image
*/
public pull = async (argvArg: any) => {
await this.prepare();
const registryUrlArg = argvArg._[2];
let suffix = null;
if (argvArg._.length >= 4) {
suffix = argvArg._[3];
}
const localDockerRegistry = await this.szciRegistryStorage.getRegistryByUrl(registryUrlArg);
const dockerfileArray = await Dockerfile.readDockerfiles(this)
.then(Dockerfile.sortDockerfiles)
.then(Dockerfile.mapDockerfiles);
for (const dockerfile of dockerfileArray) {
await dockerfile.pull(localDockerRegistry, suffix);
}
};
/**
* tests docker files
*/
public test = async () => {
await this.prepare();
return await Dockerfile.readDockerfiles(this).then(Dockerfile.testDockerfiles);
};
/**
* can be used to get the Dockerfiles in the directory
*/
getDockerfiles = async () => {
const dockerfiles = await Dockerfile.readDockerfiles(this);
return dockerfiles;
}
} }

View File

@@ -1,410 +0,0 @@
import * as plugins from './mod.plugins.ts';
import * as paths from '../szci.paths.ts';
import { logger } from '../szci.logging.ts';
import { bash } from '../szci.bash.ts';
import { DockerRegistry } from './mod.classes.dockerregistry.ts';
import * as helpers from './mod.helpers.ts';
import { SzciDockerManager } from './index.ts';
import { Szci } from '../szci.classes.szci.ts';
/**
* class Dockerfile represents a Dockerfile on disk in szci
*/
export class Dockerfile {
// STATIC
/**
* creates instance of class Dockerfile for all Dockerfiles in cwd
* @returns Promise<Dockerfile[]>
*/
public static async readDockerfiles(
szciDockerManagerRefArg: SzciDockerManager
): Promise<Dockerfile[]> {
const fileTree = await plugins.smartfile.fs.listFileTree(paths.getCwd(), 'Dockerfile*');
// create the Dockerfile array
const readDockerfilesArray: Dockerfile[] = [];
logger.log('info', `found ${fileTree.length} Dockerfiles:`);
console.log(fileTree);
for (const dockerfilePath of fileTree) {
const myDockerfile = new Dockerfile(szciDockerManagerRefArg, {
filePath: dockerfilePath,
read: true,
});
readDockerfilesArray.push(myDockerfile);
}
return readDockerfilesArray;
}
/**
* Sorts Dockerfiles into a build order based on dependencies.
* @param dockerfiles An array of Dockerfile instances.
* @returns A Promise that resolves to a sorted array of Dockerfiles.
*/
public static async sortDockerfiles(dockerfiles: Dockerfile[]): Promise<Dockerfile[]> {
logger.log('info', 'Sorting Dockerfiles based on dependencies...');
// Map from cleanTag to Dockerfile instance for quick lookup
const tagToDockerfile = new Map<string, Dockerfile>();
dockerfiles.forEach((dockerfile) => {
tagToDockerfile.set(dockerfile.cleanTag, dockerfile);
});
// Build the dependency graph
const graph = new Map<Dockerfile, Dockerfile[]>();
dockerfiles.forEach((dockerfile) => {
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;
}
graph.set(dockerfile, dependencies);
});
// 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);
}
};
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;
}
/**
* maps local Dockerfiles dependencies to the correspoding Dockerfile class instances
*/
public static async mapDockerfiles(sortedDockerfileArray: Dockerfile[]): Promise<Dockerfile[]> {
sortedDockerfileArray.forEach((dockerfileArg) => {
if (dockerfileArg.localBaseImageDependent) {
sortedDockerfileArray.forEach((dockfile2: Dockerfile) => {
if (dockfile2.cleanTag === dockerfileArg.baseImage) {
dockerfileArg.localBaseDockerfile = dockfile2;
}
});
}
});
return sortedDockerfileArray;
}
/**
* builds the correspoding real docker image for each Dockerfile class instance
*/
public static async buildDockerfiles(sortedArrayArg: Dockerfile[]) {
for (const dockerfileArg of sortedArrayArg) {
await dockerfileArg.build();
}
return sortedArrayArg;
}
/**
* tests all Dockerfiles in by calling class Dockerfile.test();
* @param sortedArrayArg Dockerfile[] that contains all Dockerfiles in cwd
*/
public static async testDockerfiles(sortedArrayArg: Dockerfile[]) {
for (const dockerfileArg of sortedArrayArg) {
await dockerfileArg.test();
}
return sortedArrayArg;
}
/**
* returns a version for a docker file
* @execution SYNC
*/
public static dockerFileVersion(
dockerfileInstanceArg: Dockerfile,
dockerfileNameArg: string
): string {
let versionString: string;
const versionRegex = /Dockerfile_(.+)$/;
const regexResultArray = versionRegex.exec(dockerfileNameArg);
if (regexResultArray && regexResultArray.length === 2) {
versionString = regexResultArray[1];
} else {
versionString = 'latest';
}
versionString = versionString.replace(
'##version##',
dockerfileInstanceArg.szciDockerManagerRef.szciRef.szciConfig.getConfig().projectInfo.npm
.version
);
return versionString;
}
/**
* 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 {
const lines = dockerfileContentArg.split(/\r?\n/);
const args: { [key: string]: string } = {};
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
*/
public static getDockerTagString(
szciDockerManagerRef: SzciDockerManager,
registryArg: string,
repoArg: string,
versionArg: string,
suffixArg?: string
): string {
// determine wether the repo should be mapped accordingly to the registry
const mappedRepo =
szciDockerManagerRef.szciRef.szciConfig.getConfig().dockerRegistryRepoMap[registryArg];
const repo = (() => {
if (mappedRepo) {
return mappedRepo;
} else {
return repoArg;
}
})();
// determine wether the version contais a suffix
let version = versionArg;
if (suffixArg) {
version = versionArg + '_' + suffixArg;
}
const tagString = `${registryArg}/${repo}:${version}`;
return tagString;
}
public static async getDockerBuildArgs(
szciDockerManagerRef: SzciDockerManager
): Promise<string> {
logger.log('info', 'checking for env vars to be supplied to the docker build');
let buildArgsString: string = '';
for (const dockerArgKey of Object.keys(
szciDockerManagerRef.szciRef.szciConfig.getConfig().dockerBuildargEnvMap
)) {
const dockerArgOuterEnvVar =
szciDockerManagerRef.szciRef.szciConfig.getConfig().dockerBuildargEnvMap[dockerArgKey];
logger.log(
'note',
`docker ARG "${dockerArgKey}" maps to outer env var "${dockerArgOuterEnvVar}"`
);
const targetValue = Deno.env.get(dockerArgOuterEnvVar);
buildArgsString = `${buildArgsString} --build-arg ${dockerArgKey}="${targetValue}"`;
}
return buildArgsString;
}
// INSTANCE
public szciDockerManagerRef: SzciDockerManager;
public filePath!: string;
public repo: string;
public version: string;
public cleanTag: string;
public buildTag: string;
public pushTag!: string;
public containerName: string;
public content!: string;
public baseImage: string;
public localBaseImageDependent: boolean;
public localBaseDockerfile!: Dockerfile;
constructor(
dockerManagerRefArg: SzciDockerManager,
options: { filePath?: string; fileContents?: string | Uint8Array; read?: boolean }
) {
this.szciDockerManagerRef = dockerManagerRefArg;
this.filePath = options.filePath!;
this.repo =
this.szciDockerManagerRef.szciRef.szciEnv.repo.user +
'/' +
this.szciDockerManagerRef.szciRef.szciEnv.repo.repo;
this.version = Dockerfile.dockerFileVersion(this, plugins.path.parse(this.filePath).base);
this.cleanTag = this.repo + ':' + this.version;
this.buildTag = this.cleanTag;
this.containerName = 'dockerfile-' + this.version;
if (options.filePath && options.read) {
this.content = plugins.smartfile.fs.toStringSync(plugins.path.resolve(options.filePath));
}
this.baseImage = Dockerfile.dockerBaseImage(this.content);
this.localBaseImageDependent = false;
}
/**
* builds the Dockerfile
*/
public async build() {
logger.log('info', 'now building Dockerfile for ' + this.cleanTag);
const buildArgsString = await Dockerfile.getDockerBuildArgs(this.szciDockerManagerRef);
const buildCommand = `docker build --label="version=${
this.szciDockerManagerRef.szciRef.szciConfig.getConfig().projectInfo.npm.version
}" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
await bash(buildCommand);
return;
}
/**
* pushes the Dockerfile to a registry
*/
public async push(dockerRegistryArg: DockerRegistry, versionSuffix?: string) {
this.pushTag = Dockerfile.getDockerTagString(
this.szciDockerManagerRef,
dockerRegistryArg.registryUrl,
this.repo,
this.version,
versionSuffix
);
await bash(`docker tag ${this.buildTag} ${this.pushTag}`);
await bash(`docker push ${this.pushTag}`);
const imageDigest = (
await bash(`docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`)
).split('@')[1];
console.log(`The image ${this.pushTag} has digest ${imageDigest}`);
await this.szciDockerManagerRef.szciRef.cloudlyConnector.announceDockerContainer({
registryUrl: this.pushTag,
tag: this.buildTag,
labels: [],
version: this.szciDockerManagerRef.szciRef.szciConfig.getConfig().projectInfo.npm.version,
});
await this.szciDockerManagerRef.szciRef.szciConfig.kvStorage.writeKey(
'latestPushedDockerTag',
this.pushTag
);
}
/**
* pulls the Dockerfile from a registry
*/
public async pull(registryArg: DockerRegistry, versionSuffixArg?: string) {
const pullTag = Dockerfile.getDockerTagString(
this.szciDockerManagerRef,
registryArg.registryUrl,
this.repo,
this.version,
versionSuffixArg
);
await bash(`docker pull ${pullTag}`);
await bash(`docker tag ${pullTag} ${this.buildTag}`);
}
/**
* tests the Dockerfile;
*/
public async test() {
const testFile: string = plugins.path.join(paths.SzciTestDir, 'test_' + this.version + '.sh');
const testFileExists: boolean = plugins.smartfile.fs.fileExistsSync(testFile);
if (testFileExists) {
// run tests
await bash(
`docker run --name szci_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /szci_test"`
);
await bash(`docker cp ${testFile} szci_test_container:/szci_test/test.sh`);
await bash(`docker commit szci_test_container szci_test_image`);
await bash(`docker run --entrypoint="bash" szci_test_image -x /szci_test/test.sh`);
await bash(`docker rm szci_test_container`);
await bash(`docker rmi --force szci_test_image`);
} else {
logger.log('warn', 'skipping tests for ' + this.cleanTag + ' because no testfile was found!');
}
}
/**
* gets the id of a Dockerfile
*/
public async getId() {
const containerId = await bash(
'docker inspect --type=image --format="{{.Id}}" ' + this.buildTag
);
return containerId;
}
}

View File

@@ -1,47 +0,0 @@
import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.ts';
import { bash } from '../szci.bash.ts';
export interface IDockerRegistryConstructorOptions {
registryUrl: string;
username: string;
password: string;
}
export class DockerRegistry {
public registryUrl: string;
public username: string;
public password: string;
constructor(optionsArg: IDockerRegistryConstructorOptions) {
this.registryUrl = optionsArg.registryUrl;
this.username = optionsArg.username;
this.password = optionsArg.password;
logger.log('info', `created DockerRegistry for ${this.registryUrl}`);
}
public static fromEnvString(envString: string): DockerRegistry {
const dockerRegexResultArray = envString.split('|');
if (dockerRegexResultArray.length !== 3) {
logger.log('error', 'malformed docker env var...');
Deno.exit(1);
}
const registryUrl = dockerRegexResultArray[0].replace('https://', '').replace('http://', '');
const username = dockerRegexResultArray[1];
const password = dockerRegexResultArray[2];
return new DockerRegistry({
registryUrl: registryUrl,
username: username,
password: password,
});
}
public async login() {
if (this.registryUrl === 'docker.io') {
await bash(`docker login -u ${this.username} -p ${this.password}`);
logger.log('info', 'Logged in to standard docker hub');
} else {
await bash(`docker login -u ${this.username} -p ${this.password} ${this.registryUrl}`);
}
logger.log('ok', `docker authenticated for ${this.registryUrl}!`);
}
}

View File

@@ -1,28 +0,0 @@
import { logger } from '../szci.logging.ts';
import * as plugins from './mod.plugins.ts';
import { DockerRegistry } from './mod.classes.dockerregistry.ts';
export class RegistryStorage {
objectMap = new plugins.lik.ObjectMap<DockerRegistry>();
constructor() {
// Nothing here
}
addRegistry(registryArg: DockerRegistry) {
this.objectMap.add(registryArg);
}
getRegistryByUrl(registryUrlArg: string) {
return this.objectMap.findSync((registryArg) => {
return registryArg.registryUrl === registryUrlArg;
});
}
async loginAll() {
await this.objectMap.forEach(async (registryArg) => {
await registryArg.login();
});
logger.log('success', 'logged in successfully into all available DockerRegistries!');
}
}

View File

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

View File

@@ -1 +0,0 @@
export * from '../szci.plugins.ts';

View File

@@ -44,11 +44,11 @@ export class SzciNodeJsManager {
logger.log('info', `now installing node version ${versionArg}`); logger.log('info', `now installing node version ${versionArg}`);
let version: string; let version: string;
if (versionArg === 'stable') { if (versionArg === 'stable') {
version = '18'; version = '22';
} else if (versionArg === 'lts') { } else if (versionArg === 'lts') {
version = '16'; version = '20';
} else if (versionArg === 'legacy') { } else if (versionArg === 'legacy') {
version = '14'; version = '18';
} else { } else {
version = versionArg; version = versionArg;
} }

View File

@@ -1,20 +0,0 @@
import * as plugins from './mod.plugins.ts';
import * as paths from '../szci.paths.ts';
import { logger } from '../szci.logging.ts';
/**
* Cleans szci config files from the project directory
*/
export const clean = async (): Promise<void> => {
try {
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 +0,0 @@
export * from '../szci.plugins.ts';

View File

@@ -1,30 +0,0 @@
import { bash } from '../szci.bash.ts';
import { logger } from '../szci.logging.ts';
/**
* Executes a wrapped command passed via CLI arguments.
* Usage: szci command <your-command-here>
*
* This allows running arbitrary commands through szci's bash wrapper
* which handles nvm and other environment setup.
*/
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);
logger.log('ok', 'Command executed successfully');
} catch (error) {
logger.log('error', `Command failed: ${(error as Error).message}`);
throw error;
}
};

View File

@@ -1 +0,0 @@
export * from '../szci.plugins.ts';

View File

@@ -1,29 +0,0 @@
import * as plugins from './plugins.ts';
import * as paths from '../szci.paths.ts';
import { logger } from '../szci.logging.ts';
import { Szci } from '../szci.classes.szci.ts';
export const handleCli = async (szciRefArg: Szci, argvArg: any) => {
logger.log('info', 'checking execution context');
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()
);
let allDesiredGitlabRunnerTagsPresent = true;
for (const desiredRunnerTag of szciRefArg.szciConfig.getConfig().gitlabRunnerTags) {
if (!presentRunnerTags.includes(desiredRunnerTag)) {
allDesiredGitlabRunnerTagsPresent = false;
logger.log(
'error',
`Desired runnerRag ${desiredRunnerTag} is missing in current execution context.`
);
}
}
if (!allDesiredGitlabRunnerTagsPresent) {
Deno.exit(1);
}
};

View File

@@ -1 +0,0 @@
export * from '../szci.plugins.ts';

View File

@@ -1,101 +0,0 @@
import * as plugins from './mod.plugins.ts';
import { logger } from '../szci.logging.ts';
/**
* Interface for parsed trigger configuration
*/
interface ITriggerConfig {
domain: string;
projectId: string;
triggerToken: string;
refName: string;
triggerName: string;
}
/**
* 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');
// 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)`);
};
/**
* Parse a trigger env var string into a config object
*/
const parseTriggerConfig = (triggerEnvVar: string): ITriggerConfig | null => {
const match = TRIGGER_VALUE_REGEX.exec(triggerEnvVar);
if (!match) {
return null;
}
return {
domain: match[1],
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',
payload: config.triggerToken,
type: 'string',
},
{
name: 'ref',
payload: config.refName,
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 +0,0 @@
export * from '../szci.plugins.ts';

View File

@@ -11,7 +11,6 @@ import { SzciConfig } from './szci.classes.szciconfig.ts';
import { CloudlyConnector } from './connector.cloudly/cloudlyconnector.ts'; import { CloudlyConnector } from './connector.cloudly/cloudlyconnector.ts';
// managers // managers
import { SzciCloudronManager } from './manager.cloudron/index.ts';
import { SzciDockerManager } from './manager.docker/index.ts'; import { SzciDockerManager } from './manager.docker/index.ts';
import { SzciGitManager } from './manager.git/index.ts'; import { SzciGitManager } from './manager.git/index.ts';
import { SzciNodeJsManager } from './manager.nodejs/index.ts'; import { SzciNodeJsManager } from './manager.nodejs/index.ts';
@@ -27,7 +26,6 @@ export class Szci {
public szciCli!: SzciCli; public szciCli!: SzciCli;
// managers // managers
public cloudronManager!: SzciCloudronManager;
public dockerManager!: SzciDockerManager; public dockerManager!: SzciDockerManager;
public gitManager!: SzciGitManager; public gitManager!: SzciGitManager;
public nodejsManager!: SzciNodeJsManager; public nodejsManager!: SzciNodeJsManager;
@@ -51,7 +49,6 @@ export class Szci {
await this.szciConfig.init(); await this.szciConfig.init();
// managers // managers
this.cloudronManager = new SzciCloudronManager(this);
this.dockerManager = new SzciDockerManager(this); this.dockerManager = new SzciDockerManager(this);
this.gitManager = new SzciGitManager(this); this.gitManager = new SzciGitManager(this);
this.nodejsManager = new SzciNodeJsManager(this); this.nodejsManager = new SzciNodeJsManager(this);

View File

@@ -12,34 +12,10 @@ export class SzciCli {
this.smartcli = new plugins.smartcli.Smartcli(); this.smartcli = new plugins.smartcli.Smartcli();
this.smartcli.addVersion(this.szciRef.szciInfo.version); this.smartcli.addVersion(this.szciRef.szciInfo.version);
// clean // docker
this.smartcli.addCommand('clean').subscribe( this.smartcli.addCommand('docker').subscribe(
async (argv) => { async (argvArg) => {
const modClean = await import('./mod_clean/index.ts'); await this.szciRef.dockerManager.handleCli(argvArg);
await modClean.clean();
},
(err) => {
console.log(err);
Deno.exit(1);
}
);
// cloudron
this.smartcli.addCommand('cloudron').subscribe(
async (argv) => {
await this.szciRef.cloudronManager.handleCli(argv);
},
(err) => {
console.log(err);
Deno.exit(1);
}
);
// command
this.smartcli.addCommand('command').subscribe(
async (argv) => {
const modCommand = await import('./mod_command/index.ts');
await modCommand.command();
}, },
(err) => { (err) => {
console.log(err); console.log(err);
@@ -58,17 +34,6 @@ export class SzciCli {
} }
); );
// build
this.smartcli.addCommand('docker').subscribe(
async (argvArg) => {
await this.szciRef.dockerManager.handleCli(argvArg);
},
(err) => {
console.log(err);
Deno.exit(1);
}
);
// node // node
this.smartcli.addCommand('node').subscribe( this.smartcli.addCommand('node').subscribe(
async (argvArg) => { async (argvArg) => {
@@ -90,28 +55,11 @@ export class SzciCli {
} }
); );
this.smartcli.addCommand('precheck').subscribe(async (argvArg) => { // ssh
const modPrecheck = await import('./mod_precheck/index.ts');
await modPrecheck.handleCli(this.szciRef, argvArg);
});
// trigger
this.smartcli.addCommand('ssh').subscribe(async (argvArg) => { this.smartcli.addCommand('ssh').subscribe(async (argvArg) => {
const modSsh = await import('./mod_ssh/index.ts'); const modSsh = await import('./mod_ssh/index.ts');
await modSsh.handleCli(argvArg); await modSsh.handleCli(argvArg);
}); });
// trigger
this.smartcli.addCommand('trigger').subscribe(
async (argv) => {
const modTrigger = await import('./mod_trigger/index.ts');
await modTrigger.trigger();
},
(err) => {
console.log(err);
Deno.exit(1);
}
);
} }
public startParse = () => { public startParse = () => {

View File

@@ -15,19 +15,8 @@ export interface ISzciOptions {
npmAccessLevel?: 'private' | 'public'; npmAccessLevel?: 'private' | 'public';
npmRegistryUrl: string; npmRegistryUrl: string;
// docker
dockerRegistries: string[];
dockerRegistryRepoMap: { [key: string]: string };
dockerBuildargEnvMap: { [key: string]: string };
// gitlab
gitlabRunnerTags: string[];
// urls // urls
urlCloudly?: string; urlCloudly?: string;
// cloudron
cloudronAppName?: string;
} }
/** /**
@@ -61,12 +50,8 @@ export class SzciConfig {
this.configObject = { this.configObject = {
projectInfo: new plugins.projectinfo.ProjectInfo(paths.cwd), projectInfo: new plugins.projectinfo.ProjectInfo(paths.cwd),
npmGlobalTools: [], npmGlobalTools: [],
dockerRegistries: [],
dockerRegistryRepoMap: {},
npmAccessLevel: 'private', npmAccessLevel: 'private',
npmRegistryUrl: 'registry.npmjs.org', npmRegistryUrl: 'registry.npmjs.org',
gitlabRunnerTags: [],
dockerBuildargEnvMap: {},
urlCloudly: await this.szciQenv.getEnvVarOnDemand('SZCI_URL_CLOUDLY'), urlCloudly: await this.szciQenv.getEnvVarOnDemand('SZCI_URL_CLOUDLY'),
}; };
this.configObject = this.szciNpmextra.dataFor<ISzciOptions>('@ship.zone/szci', this.configObject); this.configObject = this.szciNpmextra.dataFor<ISzciOptions>('@ship.zone/szci', this.configObject);

View File

@@ -14,12 +14,10 @@ import * as servezoneApi from '@serve.zone/api';
export { servezoneApi }; export { servezoneApi };
// @push.rocks // @push.rocks
import * as lik from '@push.rocks/lik';
import * as npmextra from '@push.rocks/npmextra'; import * as npmextra from '@push.rocks/npmextra';
import * as projectinfo from '@push.rocks/projectinfo'; import * as projectinfo from '@push.rocks/projectinfo';
import * as qenv from '@push.rocks/qenv'; import * as qenv from '@push.rocks/qenv';
import * as smartanalytics from '@push.rocks/smartanalytics'; import * as smartanalytics from '@push.rocks/smartanalytics';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfile from '@push.rocks/smartfile'; import * as smartfile from '@push.rocks/smartfile';
import * as smartcli from '@push.rocks/smartcli'; import * as smartcli from '@push.rocks/smartcli';
import * as smartgit from '@push.rocks/smartgit'; import * as smartgit from '@push.rocks/smartgit';
@@ -35,12 +33,10 @@ import * as smartssh from '@push.rocks/smartssh';
import * as smartstring from '@push.rocks/smartstring'; import * as smartstring from '@push.rocks/smartstring';
export { export {
lik,
npmextra, npmextra,
projectinfo, projectinfo,
qenv, qenv,
smartanalytics, smartanalytics,
smartdelay,
smartfile, smartfile,
smartgit, smartgit,
smartcli, smartcli,
@@ -61,7 +57,4 @@ import * as tsclass from '@tsclass/tsclass';
export { tsclass }; export { tsclass };
// third party
import * as through2 from 'through2';
export { through2 };