Compare commits

...

28 Commits

Author SHA1 Message Date
461c4bb5a9 v2.2.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 18:56:43 +00:00
3c3662d935 fix(config): migrate configuration loading to smartconfig and update build tooling compatibility 2026-03-24 18:56:43 +00:00
c97306b22a v2.2.3
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 17:00:15 +00:00
7af0c59708 fix(config): update workflow repository URL handling and package config file references 2026-03-24 17:00:15 +00:00
3a4d510304 v2.2.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 16:12:35 +00:00
b1f135a5f4 fix(config): rename npmextra configuration file to .smartconfig.json 2026-03-24 16:12:35 +00:00
30a5749fab v2.2.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 15:07:15 +00:00
100f37b857 fix(config): switch configuration loading from npmextra to smartconfig 2026-03-24 15:07:15 +00:00
93cf2ee7bf v2.2.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-19 10:18:10 +00:00
8cf8e43e59 feat(cli/buildx): add pull control for builds and isolate buildx builders per project 2026-03-19 10:18:10 +00:00
3e4558abc5 v2.1.0
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-15 20:15:12 +00:00
3e0eb5e198 feat(cli): add global remote builder configuration and native SSH buildx nodes for multi-platform builds 2026-03-15 20:15:12 +00:00
732e9e5cac v2.0.2
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 4m2s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 20:11:47 +00:00
5bf1779243 fix(repo): no changes to commit 2026-03-12 20:11:47 +00:00
4908c21b84 v2.0.1
Some checks failed
Default (tags) / security (push) Successful in 32s
Default (tags) / test (push) Failing after 4m8s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 20:02:50 +00:00
b1e2f0d8ea fix(repository): no changes to commit 2026-03-12 20:02:50 +00:00
d815915135 v2.0.0
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-12 10:50:34 +00:00
0f445b4c86 BREAKING CHANGE(cli): remove legacy container test runner and make the default command show the man page 2026-03-12 10:50:34 +00:00
8f0514d10e v1.17.4
Some checks failed
Default (tags) / security (push) Successful in 30s
Default (tags) / test (push) Failing after 4m14s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 12:59:31 +00:00
e1cf1768da fix(): no changes 2026-02-07 12:59:31 +00:00
4d32d5e71e v1.17.3
Some checks failed
Default (tags) / security (push) Successful in 41s
Default (tags) / test (push) Failing after 4m5s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 12:58:44 +00:00
a4552498ac fix(registry): increase default maxRetries in fetchWithRetry from 3 to 6 to improve resilience when fetching registry resources 2026-02-07 12:58:44 +00:00
4585801f32 v1.17.2
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Failing after 4m12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 12:34:37 +00:00
3dc75f5cda fix(registry): improve HTTP fetch retry logging, backoff calculation, and token-cache warning 2026-02-07 12:34:37 +00:00
7591e0ed90 v1.17.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 12:29:43 +00:00
d2c2a4c4dd fix(registrycopy): add fetchWithRetry wrapper to apply timeouts, retries with exponential backoff, and token cache handling; use it for registry HTTP requests 2026-02-07 12:29:43 +00:00
89cd93cdff v1.17.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 4m0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-02-07 12:02:17 +00:00
10aee5d4c5 feat(tsdocker): add Dockerfile filtering, optional skip-build flow, and fallback Docker config credential loading 2026-02-07 12:02:17 +00:00
26 changed files with 3553 additions and 3680 deletions

View File

@@ -7,7 +7,7 @@ on:
env: env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitlab.com/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}} NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}} NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}} NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}

View File

@@ -7,7 +7,7 @@ on:
env: env:
IMAGE: code.foss.global/host.today/ht-docker-node:npmci IMAGE: code.foss.global/host.today/ht-docker-node:npmci
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitlab.com/${{gitea.repository}}.git
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}} NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}} NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}} NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}

4
.gitignore vendored
View File

@@ -16,6 +16,10 @@ node_modules/
dist/ dist/
dist_*/ dist_*/
# rust
rust/target/
dist_rust/
# AI # AI
.claude/ .claude/
.serena/ .serena/

View File

@@ -1,7 +1,7 @@
{ {
"json.schemas": [ "json.schemas": [
{ {
"fileMatch": ["/npmextra.json"], "fileMatch": ["/smartconfig.json"],
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -1,6 +0,0 @@
FROM hosttoday/ht-docker-node:npmci
RUN yarn global add @git.zone/tsdocker
COPY ./ /workspace
WORKDIR /workspace
ENV CI=true
CMD ["tsdocker","runinside"];

View File

@@ -1,6 +1,110 @@
# Changelog # Changelog
## 2026-03-24 - 2.2.4 - fix(config)
migrate configuration loading to smartconfig and update build tooling compatibility
- switch configuration loading and documentation to .smartconfig.json
- upgrade build and test dependencies for tsbuild 4.4.0 and TypeScript 6 compatibility
- remove deprecated tsconfig baseUrl and paths settings and add an lru-cache override to avoid type issues
## 2026-03-24 - 2.2.3 - fix(config)
update workflow repository URL handling and package config file references
- Switches Gitea workflow repository URLs to use gitlab.com explicitly for authenticated CI git operations.
- Replaces the published config file entry from npmextra.json to .smartconfig.json in package metadata.
- Adds Rust build output directories to .gitignore.
- Refreshes changelog and README formatting to match the current smartconfig-based configuration.
## 2026-03-24 - 2.2.2 - fix(config)
rename npmextra configuration file to .smartconfig.json
- Moves the existing project metadata and release configuration from npmextra.json to .smartconfig.json without changing its contents.
## 2026-03-24 - 2.2.1 - fix(config)
switch configuration loading from npmextra to smartconfig
- replace the @push.rocks/npmextra dependency with @push.rocks/smartconfig
- update config initialization to use Smartconfig for the @git.zone/tsdocker settings
- refresh CLI help text to reference smartconfig.json instead of npmextra.json
## 2026-03-19 - 2.2.0 - feat(cli/buildx)
add pull control for builds and isolate buildx builders per project
- adds a new pull build option with --no-pull CLI support and defaults builds to refreshing base images with --pull
- passes the selected buildx builder explicitly into build commands instead of relying on global docker buildx use state
- generates project-hashed builder suffixes so concurrent runs from different project directories do not share the same local builder
- updates session logging to include project hash and builder suffix for easier build diagnostics
## 2026-03-15 - 2.1.0 - feat(cli)
add global remote builder configuration and native SSH buildx nodes for multi-platform builds
- adds a new `tsdocker config` command with subcommands to add, remove, list, and show remote builder definitions
- introduces global config support for remote builders stored under `~/.git.zone/tsdocker/config.json`
- builds can now create multi-node buildx setups with remote SSH builders and open reverse SSH tunnels so remote nodes can push to the local staging registry
- updates the README and CLI help to document remote builder configuration and native cross-platform build workflows
## 2026-03-12 - 2.0.2 - fix(repo)
no changes to commit
## 2026-03-12 - 2.0.1 - fix(repository)
no changes to commit
## 2026-03-12 - 2.0.0 - BREAKING CHANGE(cli)
remove legacy container test runner and make the default command show the man page
- Removes legacy testing and VS Code commands, including `runinside`, `vscode`, generated Dockerfile assets, and related configuration fields (`baseImage`, `command`, `dockerSock`, `keyValueObject`)
- Simplifies configuration and dependencies by dropping qenv-based env loading and unused legacy packages
- Updates CLI and documentation to reflect default help output and the current build/push-focused workflow
## 2026-02-07 - 1.17.4 - fix()
no changes
## 2026-02-07 - 1.17.3 - fix(registry)
increase default maxRetries in fetchWithRetry from 3 to 6 to improve resilience when fetching registry resources
- Changed default maxRetries from 3 to 6 in ts/classes.registrycopy.ts
- Reduces failures from transient network or registry errors by allowing more retry attempts
- No API or behavior changes besides the increased default retry count
## 2026-02-07 - 1.17.2 - fix(registry)
improve HTTP fetch retry logging, backoff calculation, and token-cache warning
- Include HTTP method in logs and normalize method to uppercase for consistency
- Log retry attempts with method, URL and calculated exponential backoff delay
- Compute and reuse exponential backoff delay variable instead of inline calculation
- Log error when a 5xx response persists after all retry attempts and when fetch ultimately fails
- Add a warning log when clearing cached token after a 401 response
## 2026-02-07 - 1.17.1 - fix(registrycopy)
add fetchWithRetry wrapper to apply timeouts, retries with exponential backoff, and token cache handling; use it for registry HTTP requests
- Introduces fetchWithRetry(url, options, timeoutMs, maxRetries) to wrap fetch with AbortSignal timeout, exponential backoff retries, and retry behavior only for network errors and 5xx responses
- Replaces direct fetch calls for registry /v2 checks, token requests, and blob uploads with fetchWithRetry (30s for auth/token checks, 300s for blob operations)
- Clears token cache entry when a 401 response is received so the next attempt re-authenticates
- Adds logging on retry attempts and backoff delays to improve robustness and observability
## 2026-02-07 - 1.17.0 - feat(tsdocker)
add Dockerfile filtering, optional skip-build flow, and fallback Docker config credential loading
- Add TsDockerManager.filterDockerfiles(patterns) to filter discovered Dockerfiles by glob-style patterns and warn when no matches are found
- Allow skipping image build with --no-build (argvArg.build === false): discover Dockerfiles and apply filters without performing build
- Fallback to load Docker registry credentials from ~/.docker/config.json via RegistryCopy.getDockerConfigCredentials when env vars do not provide credentials
- Import RegistryCopy and add info/warn logs when credentials are loaded or missing
## 2026-02-07 - 1.16.0 - feat(core) ## 2026-02-07 - 1.16.0 - feat(core)
Introduce per-invocation TsDockerSession and session-aware local registry and build orchestration; stream and parse buildx output for improved logging and visibility; detect Docker topology and add CI-safe cleanup; update README with multi-arch, parallel-build, caching, and local registry usage and new CLI flags. Introduce per-invocation TsDockerSession and session-aware local registry and build orchestration; stream and parse buildx output for improved logging and visibility; detect Docker topology and add CI-safe cleanup; update README with multi-arch, parallel-build, caching, and local registry usage and new CLI flags.
- Add TsDockerSession to allocate unique ports, container names and builder suffixes for concurrent runs (especially in CI). - Add TsDockerSession to allocate unique ports, container names and builder suffixes for concurrent runs (especially in CI).
@@ -13,6 +117,7 @@ Introduce per-invocation TsDockerSession and session-aware local registry and bu
- Large README improvements: multi-arch flow, persistent local registry, parallel builds, caching, new CLI and clean flags, and examples for CI integration. - Large README improvements: multi-arch flow, persistent local registry, parallel builds, caching, new CLI and clean flags, and examples for CI integration.
## 2026-02-07 - 1.15.1 - fix(registry) ## 2026-02-07 - 1.15.1 - fix(registry)
use persistent local registry and OCI Distribution API image copy for pushes use persistent local registry and OCI Distribution API image copy for pushes
- Adds RegistryCopy class implementing the OCI Distribution API to copy images (including multi-arch manifest lists) from the local registry to remote registries. - Adds RegistryCopy class implementing the OCI Distribution API to copy images (including multi-arch manifest lists) from the local registry to remote registries.
@@ -24,6 +129,7 @@ use persistent local registry and OCI Distribution API image copy for pushes
- Breaking change: registry usage and push behavior changed (config.push ignored and local registry mandatory) — bump major version. - Breaking change: registry usage and push behavior changed (config.push ignored and local registry mandatory) — bump major version.
## 2026-02-07 - 1.15.0 - feat(clean) ## 2026-02-07 - 1.15.0 - feat(clean)
Make the `clean` command interactive: add smartinteract prompts, docker context detection, and selective resource removal with support for --all and -y auto-confirm Make the `clean` command interactive: add smartinteract prompts, docker context detection, and selective resource removal with support for --all and -y auto-confirm
- Adds dependency @push.rocks/smartinteract and exposes it from the plugins module - Adds dependency @push.rocks/smartinteract and exposes it from the plugins module
@@ -33,6 +139,7 @@ Make the `clean` command interactive: add smartinteract prompts, docker context
- Replaces blunt shell commands with safer, interactive selection and adds improved error handling and logging - Replaces blunt shell commands with safer, interactive selection and adds improved error handling and logging
## 2026-02-07 - 1.14.0 - feat(build) ## 2026-02-07 - 1.14.0 - feat(build)
add level-based parallel builds with --parallel and configurable concurrency add level-based parallel builds with --parallel and configurable concurrency
- Introduces --parallel and --parallel=<n> CLI flags to enable level-based parallel Docker builds (default concurrency 4). - Introduces --parallel and --parallel=<n> CLI flags to enable level-based parallel Docker builds (default concurrency 4).
@@ -43,6 +150,7 @@ add level-based parallel builds with --parallel and configurable concurrency
- Updates documentation (readme.hints.md) with usage examples and implementation notes. - Updates documentation (readme.hints.md) with usage examples and implementation notes.
## 2026-02-07 - 1.13.0 - feat(docker) ## 2026-02-07 - 1.13.0 - feat(docker)
add Docker context detection, rootless support, and context-aware buildx registry handling add Docker context detection, rootless support, and context-aware buildx registry handling
- Introduce DockerContext class to detect current Docker context and rootless mode and to log warnings and context info - Introduce DockerContext class to detect current Docker context and rootless mode and to log warnings and context info
@@ -52,6 +160,7 @@ add Docker context detection, rootless support, and context-aware buildx registr
- Pass isRootless into local registry startup and build pipeline; emit rootless-specific warnings and registry reachability hint - Pass isRootless into local registry startup and build pipeline; emit rootless-specific warnings and registry reachability hint
## 2026-02-06 - 1.12.0 - feat(docker) ## 2026-02-06 - 1.12.0 - feat(docker)
add detailed logging for buildx, build commands, local registry, and local dependency info add detailed logging for buildx, build commands, local registry, and local dependency info
- Log startup of local registry including a note about buildx dependency bridging - Log startup of local registry including a note about buildx dependency bridging
@@ -61,6 +170,7 @@ add detailed logging for buildx, build commands, local registry, and local depen
- Non-functional change: purely adds informational logging to improve observability during builds - Non-functional change: purely adds informational logging to improve observability during builds
## 2026-02-06 - 1.11.0 - feat(docker) ## 2026-02-06 - 1.11.0 - feat(docker)
start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network start temporary local registry for buildx dependency resolution and ensure buildx builder uses host network
- Introduce a temporary local registry (localhost:5234) with start/stop helpers and push support to expose local images for buildx - Introduce a temporary local registry (localhost:5234) with start/stop helpers and push support to expose local images for buildx
@@ -71,6 +181,7 @@ start temporary local registry for buildx dependency resolution and ensure build
- Ensure buildx builder is created with --driver-opt network=host and recreate existing builder if it lacks host network to allow registry access from build containers - Ensure buildx builder is created with --driver-opt network=host and recreate existing builder if it lacks host network to allow registry access from build containers
## 2026-02-06 - 1.10.0 - feat(classes.dockerfile) ## 2026-02-06 - 1.10.0 - feat(classes.dockerfile)
support using a local base image as a build context in buildx commands support using a local base image as a build context in buildx commands
- Adds --build-context flag mapping base image to docker-image://<localTag> when localBaseImageDependent && localBaseDockerfile are set - Adds --build-context flag mapping base image to docker-image://<localTag> when localBaseImageDependent && localBaseDockerfile are set
@@ -78,6 +189,7 @@ support using a local base image as a build context in buildx commands
- Logs an info message indicating the local build context mapping - Logs an info message indicating the local build context mapping
## 2026-02-06 - 1.9.0 - feat(build) ## 2026-02-06 - 1.9.0 - feat(build)
add verbose build output, progress logging, and timing for builds/tests add verbose build output, progress logging, and timing for builds/tests
- Add 'verbose' option to build/test flows (interfaces, CLI, and method signatures) to allow streaming raw docker build output or run silently - Add 'verbose' option to build/test flows (interfaces, CLI, and method signatures) to allow streaming raw docker build output or run silently
@@ -88,6 +200,7 @@ add verbose build output, progress logging, and timing for builds/tests
- Use silent exec variants when verbose is false and stream exec when verbose is true - Use silent exec variants when verbose is false and stream exec when verbose is true
## 2026-02-06 - 1.8.0 - feat(build) ## 2026-02-06 - 1.8.0 - feat(build)
add optional content-hash based build cache to skip rebuilding unchanged Dockerfiles add optional content-hash based build cache to skip rebuilding unchanged Dockerfiles
- Introduce TsDockerCache to compute SHA-256 of Dockerfile content and persist cache to .nogit/tsdocker_support.json - Introduce TsDockerCache to compute SHA-256 of Dockerfile content and persist cache to .nogit/tsdocker_support.json
@@ -97,12 +210,14 @@ add optional content-hash based build cache to skip rebuilding unchanged Dockerf
- Cache records store contentHash, imageId, buildTag and timestamp - Cache records store contentHash, imageId, buildTag and timestamp
## 2026-02-06 - 1.7.0 - feat(cli) ## 2026-02-06 - 1.7.0 - feat(cli)
add CLI version display using commitinfo add CLI version display using commitinfo
- Imported commitinfo from './00_commitinfo_data.js' and called tsdockerCli.addVersion(commitinfo.version) to surface package/commit version in the Smartcli instance - Imported commitinfo from './00_commitinfo_data.js' and called tsdockerCli.addVersion(commitinfo.version) to surface package/commit version in the Smartcli instance
- Change made in ts/tsdocker.cli.ts — small user-facing CLI enhancement; no breaking changes - Change made in ts/tsdocker.cli.ts — small user-facing CLI enhancement; no breaking changes
## 2026-02-06 - 1.6.0 - feat(docker) ## 2026-02-06 - 1.6.0 - feat(docker)
add support for no-cache builds and tag built images for local dependency resolution add support for no-cache builds and tag built images for local dependency resolution
- Introduce IBuildCommandOptions.noCache to control --no-cache behavior - Introduce IBuildCommandOptions.noCache to control --no-cache behavior
@@ -112,6 +227,7 @@ add support for no-cache builds and tag built images for local dependency resolu
- Log tagging actions and execute docker tag via smartshellInstance - Log tagging actions and execute docker tag via smartshellInstance
## 2026-02-06 - 1.5.0 - feat(build) ## 2026-02-06 - 1.5.0 - feat(build)
add support for selective builds, platform override and build timeout add support for selective builds, platform override and build timeout
- Introduce IBuildCommandOptions with patterns, platform and timeout to control build behavior - Introduce IBuildCommandOptions with patterns, platform and timeout to control build behavior
@@ -121,6 +237,7 @@ add support for selective builds, platform override and build timeout
- Implement streaming exec with timeout to kill long-running builds and surface timeout errors - Implement streaming exec with timeout to kill long-running builds and surface timeout errors
## 2026-02-04 - 1.4.3 - fix(dockerfile) ## 2026-02-04 - 1.4.3 - fix(dockerfile)
fix matching of base images to local Dockerfiles by stripping registry prefixes when comparing image references fix matching of base images to local Dockerfiles by stripping registry prefixes when comparing image references
- Added Dockerfile.extractRepoVersion(imageRef) to normalize image references by removing registry prefixes (detects registries containing '.' or ':' or 'localhost'). - Added Dockerfile.extractRepoVersion(imageRef) to normalize image references by removing registry prefixes (detects registries containing '.' or ':' or 'localhost').
@@ -128,13 +245,15 @@ fix matching of base images to local Dockerfiles by stripping registry prefixes
- Prevents mismatches when baseImage includes a registry (e.g. "host.today/repo:version") so it correctly matches a local cleanTag like "repo:version". - Prevents mismatches when baseImage includes a registry (e.g. "host.today/repo:version") so it correctly matches a local cleanTag like "repo:version".
## 2026-01-21 - 1.4.2 - fix(classes.dockerfile) ## 2026-01-21 - 1.4.2 - fix(classes.dockerfile)
use a single top-level fs import instead of requiring fs inside methods use a single top-level fs import instead of requiring fs inside methods
- Added top-level import: import * as fs from 'fs' in ts/classes.dockerfile.ts - Added top-level import: import \* as fs from 'fs' in ts/classes.dockerfile.ts
- Removed inline require('fs') calls and replaced with the imported fs in constructor and test() to keep imports consistent - Removed inline require('fs') calls and replaced with the imported fs in constructor and test() to keep imports consistent
- No behavioral change expected; this is a cleanup/refactor to standardize module usage - No behavioral change expected; this is a cleanup/refactor to standardize module usage
## 2026-01-20 - 1.4.1 - fix(docs) ## 2026-01-20 - 1.4.1 - fix(docs)
update README: expand usage, installation, quick start, features, troubleshooting and migration notes update README: expand usage, installation, quick start, features, troubleshooting and migration notes
- Expanded README content: new Quick Start, Installation examples, and detailed Features section (containerized testing, smart Docker builds, multi-registry push, multi-architecture support, zero-config start) - Expanded README content: new Quick Start, Installation examples, and detailed Features section (containerized testing, smart Docker builds, multi-registry push, multi-architecture support, zero-config start)
@@ -143,6 +262,7 @@ update README: expand usage, installation, quick start, features, troubleshootin
- Documentation-only change — no source code modified - Documentation-only change — no source code modified
## 2026-01-20 - 1.4.0 - feat(tsdocker) ## 2026-01-20 - 1.4.0 - feat(tsdocker)
add multi-registry and multi-arch Docker build/push/pull manager, registry storage, Dockerfile handling, and new CLI commands add multi-registry and multi-arch Docker build/push/pull manager, registry storage, Dockerfile handling, and new CLI commands
- Introduce TsDockerManager orchestrator to discover, sort, build, test, push and pull Dockerfiles - Introduce TsDockerManager orchestrator to discover, sort, build, test, push and pull Dockerfiles
@@ -155,6 +275,7 @@ add multi-registry and multi-arch Docker build/push/pull manager, registry stora
- Update README and readme.hints with new features, configuration examples and command list - Update README and readme.hints with new features, configuration examples and command list
## 2026-01-19 - 1.3.0 - feat(packaging) ## 2026-01-19 - 1.3.0 - feat(packaging)
Rename package scope to @git.zone and migrate to ESM; rename CLI/config keys, update entrypoints and imports, bump Node requirement to 18, and adjust scripts/dependencies Rename package scope to @git.zone and migrate to ESM; rename CLI/config keys, update entrypoints and imports, bump Node requirement to 18, and adjust scripts/dependencies
- Package renamed to @git.zone/tsdocker (scope change) — consumers must update package reference. - Package renamed to @git.zone/tsdocker (scope change) — consumers must update package reference.

View File

@@ -1,8 +1,8 @@
{ {
"name": "@git.zone/tsdocker", "name": "@git.zone/tsdocker",
"version": "1.16.0", "version": "2.2.4",
"private": false, "private": false,
"description": "develop npm modules cross platform with docker", "description": "A comprehensive Docker build tool for TypeScript projects with multi-arch support, multi-registry push, and CI-safe session isolation.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"bin": { "bin": {
@@ -11,13 +11,6 @@
"scripts": { "scripts": {
"test": "(npm run build)", "test": "(npm run build)",
"build": "(tsbuild)", "build": "(tsbuild)",
"testIntegration": "(npm run clean && npm run setupCheck && npm run testStandard)",
"testStandard": "(cd test/ && tsx ../ts/index.ts)",
"testClean": "(cd test/ && tsx ../ts/index.ts clean --all)",
"testVscode": "(cd test/ && tsx ../ts/index.ts vscode)",
"clean": "(rm -rf test/)",
"compile": "(npmts --notest)",
"setupCheck": "(git clone https://gitlab.com/sandboxzone/sandbox-npmts.git test/)",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
"repository": { "repository": {
@@ -25,7 +18,16 @@
"url": "https://gitlab.com/gitzone/tsdocker.git" "url": "https://gitlab.com/gitzone/tsdocker.git"
}, },
"keywords": [ "keywords": [
"docker" "docker",
"typescript",
"buildx",
"multi-arch",
"multi-registry",
"oci",
"container",
"ci-cd",
"docker-build",
"cross-platform"
], ],
"author": "Lossless GmbH", "author": "Lossless GmbH",
"license": "MIT", "license": "MIT",
@@ -34,27 +36,22 @@
}, },
"homepage": "https://gitlab.com/gitzone/tsdocker#readme", "homepage": "https://gitlab.com/gitzone/tsdocker#readme",
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.6", "@git.zone/tstest": "^3.5.1",
"@types/node": "^25.0.9" "@types/node": "^25.5.0"
}, },
"dependencies": { "dependencies": {
"@push.rocks/lik": "^6.2.2", "@push.rocks/lik": "^6.4.0",
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartanalytics": "^2.0.15",
"@push.rocks/smartcli": "^4.0.20", "@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartfs": "^1.3.1", "@push.rocks/smartconfig": "^6.0.1",
"@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartinteract": "^2.0.16", "@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartlog-destination-local": "^9.0.2", "@push.rocks/smartlog-destination-local": "^9.0.2",
"@push.rocks/smartlog-source-ora": "^1.0.9", "@push.rocks/smartlog-source-ora": "^1.0.9",
"@push.rocks/smartopen": "^2.0.0", "@push.rocks/smartshell": "^3.3.8"
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartshell": "^3.3.0",
"@push.rocks/smartstring": "^4.1.0"
}, },
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34", "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"type": "module", "type": "module",
@@ -67,10 +64,12 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
"npmextra.json", ".smartconfig.json",
"readme.md" "readme.md"
], ],
"pnpm": { "pnpm": {
"overrides": {} "overrides": {
"lru-cache": ">=11.0.0"
}
} }
} }

5721
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
## Module Purpose ## Module Purpose
tsdocker is a comprehensive Docker development and building tool. It provides: tsdocker is a comprehensive Docker development and building tool. It provides:
- Testing npm modules in clean Docker environments (legacy feature)
- Building Dockerfiles with dependency ordering - Building Dockerfiles with dependency ordering
- Multi-registry push/pull support - Multi-registry push/pull support
- Multi-architecture builds (amd64/arm64) - Multi-architecture builds (amd64/arm64)
@@ -11,8 +11,8 @@ tsdocker is a comprehensive Docker development and building tool. It provides:
## New CLI Commands (2026-01-19) ## New CLI Commands (2026-01-19)
| Command | Description | | Command | Description |
|---------|-------------| | -------------------------- | ---------------------------------------------- |
| `tsdocker` | Run tests in container (legacy default behavior) | | `tsdocker` | Show usage / man page |
| `tsdocker build` | Build all Dockerfiles with dependency ordering | | `tsdocker build` | Build all Dockerfiles with dependency ordering |
| `tsdocker push [registry]` | Push images to configured registries | | `tsdocker push [registry]` | Push images to configured registries |
| `tsdocker pull <registry>` | Pull images from registry | | `tsdocker pull <registry>` | Pull images from registry |
@@ -20,11 +20,10 @@ tsdocker is a comprehensive Docker development and building tool. It provides:
| `tsdocker login` | Login to configured registries | | `tsdocker login` | Login to configured registries |
| `tsdocker list` | List discovered Dockerfiles and dependencies | | `tsdocker list` | List discovered Dockerfiles and dependencies |
| `tsdocker clean --all` | Clean up Docker environment | | `tsdocker clean --all` | Clean up Docker environment |
| `tsdocker vscode` | Start VS Code in Docker |
## Configuration ## Configuration
Configure in `package.json` under `@git.zone/tsdocker`: Configure in `.smartconfig.json` under `@git.zone/tsdocker`:
```json ```json
{ {
@@ -45,9 +44,6 @@ Configure in `package.json` under `@git.zone/tsdocker`:
### Configuration Options ### Configuration Options
- `baseImage`: Base Docker image for testing (legacy)
- `command`: Command to run in container (legacy)
- `dockerSock`: Mount Docker socket (legacy)
- `registries`: Array of registry URLs to push to - `registries`: Array of registry URLs to push to
- `registryRepoMap`: Map registry URLs to different repo paths - `registryRepoMap`: Map registry URLs to different repo paths
- `buildArgEnvMap`: Map Docker build ARGs to environment variables - `buildArgEnvMap`: Map Docker build ARGs to environment variables
@@ -78,8 +74,6 @@ ts/
├── tsdocker.cli.ts (CLI commands) ├── tsdocker.cli.ts (CLI commands)
├── tsdocker.config.ts (configuration) ├── tsdocker.config.ts (configuration)
├── tsdocker.plugins.ts (plugin imports) ├── tsdocker.plugins.ts (plugin imports)
├── tsdocker.docker.ts (legacy test runner)
├── tsdocker.snippets.ts (Dockerfile generation)
├── classes.dockerfile.ts (Dockerfile management) ├── classes.dockerfile.ts (Dockerfile management)
├── classes.dockerregistry.ts (registry authentication) ├── classes.dockerregistry.ts (registry authentication)
├── classes.registrystorage.ts (registry storage) ├── classes.registrystorage.ts (registry storage)
@@ -113,6 +107,7 @@ Implementation: `Dockerfile.computeLevels()` groups topologically sorted Dockerf
All builds now go through a persistent local registry (`localhost:5234`) with volume storage at `.nogit/docker-registry/`. Pushes use the `RegistryCopy` class (`ts/classes.registrycopy.ts`) which implements the OCI Distribution API to copy images (including multi-arch manifest lists) from the local registry to remote registries. This replaces the old `docker tag + docker push` approach that only worked for single-platform images. All builds now go through a persistent local registry (`localhost:5234`) with volume storage at `.nogit/docker-registry/`. Pushes use the `RegistryCopy` class (`ts/classes.registrycopy.ts`) which implements the OCI Distribution API to copy images (including multi-arch manifest lists) from the local registry to remote registries. This replaces the old `docker tag + docker push` approach that only worked for single-platform images.
Key classes: Key classes:
- `RegistryCopy` — HTTP-based OCI image copy (auth, blob transfer, manifest handling) - `RegistryCopy` — HTTP-based OCI image copy (auth, blob transfer, manifest handling)
- `Dockerfile.push()` — Now delegates to `RegistryCopy.copyImage()` - `Dockerfile.push()` — Now delegates to `RegistryCopy.copyImage()`
- `Dockerfile.needsLocalRegistry()` — Always returns true - `Dockerfile.needsLocalRegistry()` — Always returns true
@@ -122,12 +117,16 @@ The `config.push` field is now a no-op (kept for backward compat).
## Build Status ## Build Status
- Build: ✅ Passes - Build: ✅ Passes (TypeScript 6 via tsbuild 4.4.0)
- Legacy test functionality preserved
- New Docker build functionality added
## Previous Upgrades (2025-11-22) ## Previous Upgrades
### 2026-03-24
- Upgraded `@git.zone/tsbuild` from 4.3.0 to 4.4.0 (TypeScript 6)
- Removed deprecated `baseUrl`/`paths` from tsconfig.json
- Added pnpm override `lru-cache: ">=11.0.0"` to fix TS6 type incompatibility with lru-cache@10.x
### 2025-11-22
- Updated all @git.zone/_ dependencies to @git.zone/_ scope - Updated all @git.zone/_ dependencies to @git.zone/_ scope
- Updated all @pushrocks/_ dependencies to @push.rocks/_ scope - Updated all @pushrocks/_ dependencies to @push.rocks/_ scope
- Migrated from smartfile v8 to smartfs v1.1.0 - Migrated from smartfile v8 to smartfs v1.1.0

291
readme.md
View File

@@ -12,13 +12,15 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
### 🎯 Key Capabilities ### 🎯 Key Capabilities
- 🧪 **Containerized Testing** — Run your tests in pristine Docker environments
- 🏗️ **Smart Docker Builds** — Automatically discover, sort, and build Dockerfiles by dependency - 🏗️ **Smart Docker Builds** — Automatically discover, sort, and build Dockerfiles by dependency
- 🌍 **True Multi-Architecture** — Build for `amd64` and `arm64` simultaneously with Docker Buildx - 🌍 **True Multi-Architecture** — Build for `amd64` and `arm64` simultaneously with Docker Buildx
- 🚀 **Multi-Registry Push** — Ship to Docker Hub, GitLab, GitHub Container Registry, and more via OCI Distribution API - 🚀 **Multi-Registry Push** — Ship to Docker Hub, GitLab, GitHub Container Registry, and more via OCI Distribution API
-**Parallel Builds** — Level-based parallel builds with configurable concurrency -**Parallel Builds** — Level-based parallel builds with configurable concurrency
- 🗄️ **Persistent Local Registry** — All images flow through a local OCI registry with persistent storage - 🗄️ **Persistent Local Registry** — All images flow through a local OCI registry with persistent storage
- 📦 **Build Caching** — Skip unchanged Dockerfiles with content-hash caching - 📦 **Build Caching** — Skip unchanged Dockerfiles with content-hash caching
- 🎯 **Dockerfile Filtering** — Build or push only specific Dockerfiles using glob patterns
- 🔁 **Resilient Push** — Automatic retry with exponential backoff, timeouts, and token refresh for rock-solid pushes
- 🏭 **CI-Safe Isolation** — Unique sessions per invocation prevent collisions in parallel CI pipelines
- 🔧 **Zero Config Start** — Works out of the box, scales with your needs - 🔧 **Zero Config Start** — Works out of the box, scales with your needs
## Installation ## Installation
@@ -33,16 +35,6 @@ pnpm install --save-dev @git.zone/tsdocker
## Quick Start ## Quick Start
### 🧪 Run Tests in Docker
The simplest use case — run your tests in a clean container:
```bash
tsdocker
```
This pulls your configured base image, mounts your project, and executes your test command in isolation.
### 🏗️ Build Docker Images ### 🏗️ Build Docker Images
Got `Dockerfile` files? Build them all with automatic dependency ordering: Got `Dockerfile` files? Build them all with automatic dependency ordering:
@@ -52,6 +44,7 @@ tsdocker build
``` ```
tsdocker will: tsdocker will:
1. 🔍 Discover all `Dockerfile*` files in your project 1. 🔍 Discover all `Dockerfile*` files in your project
2. 📊 Analyze `FROM` dependencies between them 2. 📊 Analyze `FROM` dependencies between them
3. 🔄 Sort them topologically 3. 🔄 Sort them topologically
@@ -68,28 +61,47 @@ tsdocker push
# Push to a specific registry # Push to a specific registry
tsdocker push --registry=registry.gitlab.com tsdocker push --registry=registry.gitlab.com
# Push without rebuilding (use existing images in local registry)
tsdocker push --no-build
``` ```
Under the hood, `tsdocker push` uses the **OCI Distribution API** to copy images directly from the local registry to remote registries. This means multi-arch manifest lists are preserved end-to-end — no more single-platform-only pushes. Under the hood, `tsdocker push` uses the **OCI Distribution API** to copy images directly from the local registry to remote registries. This means multi-arch manifest lists are preserved end-to-end — no more single-platform-only pushes. Every request is protected with **automatic retry** (up to 6 attempts with exponential backoff) and **5-minute timeouts**, so transient network issues don't kill your push mid-transfer.
### 🎯 Build Only Specific Dockerfiles
Target specific Dockerfiles by name pattern — dependencies are resolved automatically:
```bash
# Build only the base image
tsdocker build Dockerfile_base
# Build anything matching a glob pattern
tsdocker build Dockerfile_app*
# Push specific images only (skip build phase)
tsdocker push --no-build Dockerfile_api Dockerfile_web
```
## CLI Commands ## CLI Commands
| Command | Description | | Command | Description |
|---------|-------------| | -------------------------- | ------------------------------------------------------------ |
| `tsdocker` | Run tests in a fresh Docker container (legacy mode) | | `tsdocker` | Show usage / man page |
| `tsdocker build` | Build all Dockerfiles with dependency ordering | | `tsdocker build` | Build all Dockerfiles with dependency ordering |
| `tsdocker push` | Build + push images to configured registries | | `tsdocker push` | Build + push images to configured registries |
| `tsdocker pull <registry>` | Pull images from a specific registry | | `tsdocker pull <registry>` | Pull images from a specific registry |
| `tsdocker test` | Build + run container test scripts (`test_*.sh`) | | `tsdocker test` | Build + run container test scripts (`test_*.sh`) |
| `tsdocker login` | Authenticate with configured registries | | `tsdocker login` | Authenticate with configured registries |
| `tsdocker list` | Display discovered Dockerfiles and their dependencies | | `tsdocker list` | Display discovered Dockerfiles and their dependencies |
| `tsdocker config` | Manage global tsdocker configuration (remote builders, etc.) |
| `tsdocker clean` | Interactively clean Docker environment | | `tsdocker clean` | Interactively clean Docker environment |
| `tsdocker vscode` | Launch containerized VS Code in browser |
### Build Flags ### Build Flags
| Flag | Description | | Flag | Description |
|------|-------------| | ------------------------ | ------------------------------------------------------------------------------- |
| `<patterns>` | Positional Dockerfile name patterns (e.g. `Dockerfile_base`, `Dockerfile_app*`) |
| `--platform=linux/arm64` | Override build platform for a single architecture | | `--platform=linux/arm64` | Override build platform for a single architecture |
| `--timeout=600` | Build timeout in seconds | | `--timeout=600` | Build timeout in seconds |
| `--no-cache` | Force rebuild without Docker layer cache | | `--no-cache` | Force rebuild without Docker layer cache |
@@ -99,16 +111,42 @@ Under the hood, `tsdocker push` uses the **OCI Distribution API** to copy images
| `--parallel=8` | Parallel builds with custom concurrency | | `--parallel=8` | Parallel builds with custom concurrency |
| `--context=mycontext` | Use a specific Docker context | | `--context=mycontext` | Use a specific Docker context |
### Push Flags
| Flag | Description |
| ------------------ | ------------------------------------------------------------------- |
| `<patterns>` | Positional Dockerfile name patterns to select which images to push |
| `--registry=<url>` | Push to a single specific registry instead of all configured |
| `--no-build` | Skip the build phase; only push existing images from local registry |
### Config Subcommands
| Subcommand | Description |
| ---------------- | ----------------------------------- |
| `add-builder` | Add or update a remote builder node |
| `remove-builder` | Remove a remote builder by name |
| `list-builders` | List all configured remote builders |
| `show` | Show the full global configuration |
**`add-builder` flags:**
| Flag | Description |
| ------------------ | --------------------------------------------------------- |
| `--name=<name>` | Builder name (e.g. `arm64-builder`) |
| `--host=<user@ip>` | SSH host (e.g. `armbuilder@192.168.1.100`) |
| `--platform=<p>` | Target platform (e.g. `linux/arm64`) |
| `--ssh-key=<path>` | SSH key path (optional, uses SSH agent/config by default) |
### Clean Flags ### Clean Flags
| Flag | Description | | Flag | Description |
|------|-------------| | ------- | -------------------------------------------------- |
| `--all` | Include all images and volumes (not just dangling) | | `--all` | Include all images and volumes (not just dangling) |
| `-y` | Auto-confirm all prompts | | `-y` | Auto-confirm all prompts |
## Configuration ## Configuration
Configure tsdocker in your `package.json` or `npmextra.json` under the `@git.zone/tsdocker` key: Configure tsdocker in your `.smartconfig.json` under the `@git.zone/tsdocker` key:
```json ```json
{ {
@@ -131,23 +169,13 @@ Configure tsdocker in your `package.json` or `npmextra.json` under the `@git.zon
#### Build & Push Options #### Build & Push Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
|--------|------|---------|-------------| | ----------------- | ---------- | ----------------- | ---------------------------------------------- |
| `registries` | `string[]` | `[]` | Registry URLs to push to | | `registries` | `string[]` | `[]` | Registry URLs to push to |
| `registryRepoMap` | `object` | `{}` | Map registries to different repository paths | | `registryRepoMap` | `object` | `{}` | Map registries to different repository paths |
| `buildArgEnvMap` | `object` | `{}` | Map Docker build ARGs to environment variables | | `buildArgEnvMap` | `object` | `{}` | Map Docker build ARGs to environment variables |
| `platforms` | `string[]` | `["linux/amd64"]` | Target architectures for multi-arch builds | | `platforms` | `string[]` | `["linux/amd64"]` | Target architectures for multi-arch builds |
| `testDir` | `string` | `./test` | Directory containing test scripts | | `testDir` | `string` | `./test` | Directory containing test scripts |
#### Legacy Testing Options
These options configure the `tsdocker` default command (containerized test runner):
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `baseImage` | `string` | `hosttoday/ht-docker-node:npmdocker` | Docker image for test environment |
| `command` | `string` | `npmci npm test` | Command to run inside the container |
| `dockerSock` | `boolean` | `false` | Mount Docker socket for DinD scenarios |
## Architecture: How tsdocker Works ## Architecture: How tsdocker Works
tsdocker uses a **local OCI registry** as the canonical store for all built images. This design solves fundamental problems with Docker's local daemon, which cannot hold multi-architecture manifest lists. tsdocker uses a **local OCI registry** as the canonical store for all built images. This design solves fundamental problems with Docker's local daemon, which cannot hold multi-architecture manifest lists.
@@ -155,47 +183,91 @@ tsdocker uses a **local OCI registry** as the canonical store for all built imag
### 📐 Build Flow ### 📐 Build Flow
``` ```
┌─────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────
│ tsdocker build │ │ tsdocker build │
│ │ │ │
│ 1. Start local registry (localhost:5234) │ 1. Start local registry (localhost:<dynamic-port>)
│ └── Persistent volume: .nogit/docker-registry/ │ └── Persistent volume: .nogit/docker-registry/
│ │ │ │
│ 2. For each Dockerfile (topological order): │ │ 2. For each Dockerfile (topological order): │
│ ├── Multi-platform: buildx --push → registry │ │ ├── Multi-platform: buildx --push → registry │
│ └── Single-platform: docker build → registry │ │ └── Single-platform: docker build → registry │
│ │ │ │
│ 3. Stop local registry (data persists on disk) │ │ 3. Stop local registry (data persists on disk) │
└─────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────
``` ```
### 📤 Push Flow ### 📤 Push Flow
``` ```
┌──────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────
│ tsdocker push │ │ tsdocker push │
│ │ │ │
│ 1. Start local registry (loads persisted data) │ │ 1. Start local registry (loads persisted data) │
│ │ │ │
│ 2. For each image × each remote registry: │ │ 2. For each image × each remote registry: │
│ └── OCI Distribution API copy: │ └── OCI Distribution API copy (with retry):
│ ├── Fetch manifest (single or multi-arch) │ │ ├── Fetch manifest (single or multi-arch) │
│ ├── Copy blobs (skip if already exist) │ │ ├── Copy blobs (skip if already exist) │
│ ├── Retry up to 6× with exponential backoff │
│ └── Push manifest with destination tag │ │ └── Push manifest with destination tag │
│ │ │ │
│ 3. Stop local registry │ │ 3. Stop local registry │
└──────────────────────────────────────────────────┘ └────────────────────────────────────────────────────────
``` ```
### 🔑 Why a Local Registry? ### 🔑 Why a Local Registry?
| Problem | Solution | | Problem | Solution |
|---------|----------| | --------------------------------------------------- | ------------------------------------------------------------------- |
| `docker buildx --load` fails for multi-arch images | `buildx --push` to local registry works for any number of platforms | | `docker buildx --load` fails for multi-arch images | `buildx --push` to local registry works for any number of platforms |
| `docker push` only pushes single-platform manifests | OCI API copy preserves full manifest lists (multi-arch) | | `docker push` only pushes single-platform manifests | OCI API copy preserves full manifest lists (multi-arch) |
| Images lost between build and push phases | Persistent storage at `.nogit/docker-registry/` survives restarts | | Images lost between build and push phases | Persistent storage at `.nogit/docker-registry/` survives restarts |
| Redundant blob uploads on incremental pushes | HEAD checks skip blobs that already exist on the remote | | Redundant blob uploads on incremental pushes | HEAD checks skip blobs that already exist on the remote |
### 🔁 Resilient Push
The OCI Distribution API client wraps every HTTP request with:
- **Timeouts** — 5-minute timeout for blob operations, 30-second timeout for auth/metadata calls via `AbortSignal.timeout()`
- **Automatic Retry** — Up to 6 attempts with exponential backoff (1s → 2s → 4s → 8s → 16s → 32s)
- **Smart Retry Logic** — Retries on network errors (`ECONNRESET`, `fetch failed`) and 5xx server errors; does NOT retry 4xx client errors
- **Token Refresh** — On 401 responses, the cached auth token is cleared so the next retry re-authenticates automatically
This means transient issues like stale connection pools, brief network blips, or token expiry during long multi-arch pushes (56+ blob operations) are handled gracefully instead of killing the entire transfer.
### 🏭 CI-Safe Session Isolation
Every tsdocker invocation gets its own **session** with unique:
- **Session ID** — Random 8-char hex (override with `TSDOCKER_SESSION_ID`)
- **Registry port** — Dynamically allocated (override with `TSDOCKER_REGISTRY_PORT`)
- **Registry container** — Named `tsdocker-registry-<sessionId>`
- **Builder suffix** — In CI, the buildx builder gets a `-<sessionId>` suffix to prevent collisions
This prevents resource conflicts when multiple CI jobs run tsdocker in parallel. Auto-detected CI systems:
| Environment Variable | CI System |
| -------------------- | -------------- |
| `GITEA_ACTIONS` | Gitea Actions |
| `GITHUB_ACTIONS` | GitHub Actions |
| `GITLAB_CI` | GitLab CI |
| `CI` | Generic CI |
In local dev, no suffix is added — keeping a persistent builder for faster rebuilds.
### 🔍 Docker Context & Topology Detection
tsdocker automatically detects your Docker environment topology:
| Topology | Detection | Meaning |
| -------------- | ---------------------------------- | ----------------------------------------------------- |
| `local` | Default | Standard Docker installation on the host |
| `socket-mount` | `/.dockerenv` exists | Running inside a container with Docker socket mounted |
| `dind` | `DOCKER_HOST` starts with `tcp://` | Docker-in-Docker setup |
Context-aware builder names (`tsdocker-builder-<context>`) prevent conflicts across Docker contexts. Rootless Docker configurations trigger appropriate warnings.
## Registry Authentication ## Registry Authentication
### Environment Variables ### Environment Variables
@@ -213,7 +285,7 @@ export DOCKER_REGISTRY_PASSWORD="password"
### Docker Config Fallback ### Docker Config Fallback
When pushing, tsdocker will also read credentials from `~/.docker/config.json` if no explicit credentials are provided via environment variables. This means `docker login` credentials work automatically. When pushing, tsdocker will also read credentials from `~/.docker/config.json` if no explicit credentials are provided via environment variables. This means `docker login` credentials work automatically. Docker Hub special cases (`docker.io`, `index.docker.io`, `registry-1.docker.io`) are all recognized.
### Login Command ### Login Command
@@ -238,10 +310,57 @@ Build for multiple platforms using Docker Buildx:
``` ```
tsdocker automatically: tsdocker automatically:
- Sets up a Buildx builder with `--driver-opt network=host` (so buildx can reach the local registry) - Sets up a Buildx builder with `--driver-opt network=host` (so buildx can reach the local registry)
- Pushes multi-platform images to the local registry via `buildx --push` - Pushes multi-platform images to the local registry via `buildx --push`
- Copies the full manifest list (including all platform variants) to remote registries on `tsdocker push` - Copies the full manifest list (including all platform variants) to remote registries on `tsdocker push`
### 🖥️ Native Remote Builders
Instead of relying on slow QEMU emulation for cross-platform builds, tsdocker can use **native remote machines** via SSH as build nodes. For example, use a real arm64 machine for `linux/arm64` builds:
```bash
# Add a remote arm64 builder
tsdocker config add-builder \
--name=arm64-builder \
--host=armbuilder@192.168.1.100 \
--platform=linux/arm64 \
--ssh-key=~/.ssh/id_ed25519
# List configured builders
tsdocker config list-builders
# Remove a builder
tsdocker config remove-builder --name=arm64-builder
# Show full global config
tsdocker config show
```
Global configuration is stored at `~/.git.zone/tsdocker/config.json`.
**How it works:**
When remote builders are configured and the project's `platforms` includes a matching platform, tsdocker automatically:
1. Creates a **multi-node buildx builder** — local node for `linux/amd64`, remote SSH node for `linux/arm64`
2. Opens **SSH reverse tunnels** so the remote builder can push to the local staging registry
3. Builds natively on each platform's hardware — no QEMU overhead
4. Tears down tunnels after the build completes
```
[Local machine] [Remote arm64 machine]
registry:2 on localhost:PORT <──── SSH reverse tunnel ──── localhost:PORT
BuildKit (amd64) ──push──> BuildKit (arm64) ──push──>
localhost:PORT localhost:PORT (tunneled)
```
**Prerequisites for the remote machine:**
- Docker installed and running
- A user with Docker group access (no sudo needed)
- SSH key access configured
### ⚡ Parallel Builds ### ⚡ Parallel Builds
Speed up builds by building independent images concurrently: Speed up builds by building independent images concurrently:
@@ -264,12 +383,32 @@ tsdocker groups Dockerfiles into **dependency levels** using topological analysi
tsdocker discovers files matching `Dockerfile*`: tsdocker discovers files matching `Dockerfile*`:
| File Name | Version Tag | | File Name | Version Tag |
|-----------|-------------| | ------------------------ | --------------------------- |
| `Dockerfile` | `latest` | | `Dockerfile` | `latest` |
| `Dockerfile_v1.0.0` | `v1.0.0` | | `Dockerfile_v1.0.0` | `v1.0.0` |
| `Dockerfile_alpine` | `alpine` | | `Dockerfile_alpine` | `alpine` |
| `Dockerfile_##version##` | Uses `package.json` version | | `Dockerfile_##version##` | Uses `package.json` version |
### 🎯 Dockerfile Filtering
Build or push only the Dockerfiles you need. Positional arguments are matched against Dockerfile basenames as glob patterns:
```bash
# Build a single Dockerfile
tsdocker build Dockerfile_base
# Glob patterns with * and ? wildcards
tsdocker build Dockerfile_app*
# Multiple patterns
tsdocker build Dockerfile_base Dockerfile_web
# Push specific images without rebuilding
tsdocker push --no-build Dockerfile_api
```
When filtering for `build`, **dependencies are auto-resolved**: if `Dockerfile_app` depends on `Dockerfile_base`, specifying only `Dockerfile_app` will automatically include `Dockerfile_base` in the build order.
### 🔗 Dependency-Aware Builds ### 🔗 Dependency-Aware Builds
If you have multiple Dockerfiles that depend on each other: If you have multiple Dockerfiles that depend on each other:
@@ -347,20 +486,6 @@ Use different repository names for different registries:
When pushing, tsdocker maps the local repo name to the registry-specific path. For example, a locally built `myproject:latest` becomes `registry.gitlab.com/mygroup/myproject:latest` and `docker.io/myuser/myproject:latest`. When pushing, tsdocker maps the local repo name to the registry-specific path. For example, a locally built `myproject:latest` becomes `registry.gitlab.com/mygroup/myproject:latest` and `docker.io/myuser/myproject:latest`.
### 🐳 Docker-in-Docker Testing
Test Docker-related tools by mounting the Docker socket:
```json
{
"@git.zone/tsdocker": {
"baseImage": "docker:latest",
"command": "docker version && docker ps",
"dockerSock": true
}
}
```
### 📋 Listing Dockerfiles ### 📋 Listing Dockerfiles
Inspect your project's Dockerfiles and their relationships: Inspect your project's Dockerfiles and their relationships:
@@ -370,6 +495,7 @@ tsdocker list
``` ```
Output: Output:
``` ```
Discovered Dockerfiles: Discovered Dockerfiles:
======================== ========================
@@ -434,7 +560,7 @@ build-and-push:
- npm install -g @git.zone/tsdocker - npm install -g @git.zone/tsdocker
- tsdocker push - tsdocker push
variables: variables:
DOCKER_REGISTRY_1: "registry.gitlab.com|$CI_REGISTRY_USER|$CI_REGISTRY_PASSWORD" DOCKER_REGISTRY_1: 'registry.gitlab.com|$CI_REGISTRY_USER|$CI_REGISTRY_PASSWORD'
``` ```
**GitHub Actions:** **GitHub Actions:**
@@ -446,9 +572,22 @@ build-and-push:
tsdocker login tsdocker login
tsdocker push tsdocker push
env: env:
DOCKER_REGISTRY_1: "ghcr.io|${{ github.actor }}|${{ secrets.GITHUB_TOKEN }}" DOCKER_REGISTRY_1: 'ghcr.io|${{ github.actor }}|${{ secrets.GITHUB_TOKEN }}'
``` ```
**Gitea Actions:**
```yaml
- name: Build and Push
run: |
npm install -g @git.zone/tsdocker
tsdocker push
env:
DOCKER_REGISTRY_1: 'gitea.example.com|${{ secrets.REGISTRY_USER }}|${{ secrets.REGISTRY_PASSWORD }}'
```
tsdocker auto-detects all three CI systems and enables session isolation automatically — no extra configuration needed.
## TypeScript API ## TypeScript API
tsdocker can also be used programmatically: tsdocker can also be used programmatically:
@@ -458,10 +597,6 @@ import { TsDockerManager } from '@git.zone/tsdocker/dist_ts/classes.tsdockermana
import type { ITsDockerConfig } from '@git.zone/tsdocker/dist_ts/interfaces/index.js'; import type { ITsDockerConfig } from '@git.zone/tsdocker/dist_ts/interfaces/index.js';
const config: ITsDockerConfig = { const config: ITsDockerConfig = {
baseImage: 'node:20',
command: 'npm test',
dockerSock: false,
keyValueObject: {},
registries: ['docker.io'], registries: ['docker.io'],
platforms: ['linux/amd64', 'linux/arm64'], platforms: ['linux/amd64', 'linux/arm64'],
}; };
@@ -472,6 +607,25 @@ await manager.build({ parallel: true });
await manager.push(); await manager.push();
``` ```
## Environment Variables
### CI & Session Control
| Variable | Description |
| ------------------------ | -------------------------------------------------------------------------- |
| `TSDOCKER_SESSION_ID` | Override the auto-generated session ID (default: random 8-char hex) |
| `TSDOCKER_REGISTRY_PORT` | Override the dynamically allocated local registry port |
| `CI` | Generic CI detection (also `GITHUB_ACTIONS`, `GITLAB_CI`, `GITEA_ACTIONS`) |
### Registry Credentials
| Variable | Description |
| ------------------------------------------------ | ---------------------------------------------- |
| `DOCKER_REGISTRY_1` through `DOCKER_REGISTRY_10` | Pipe-delimited: `registry\|username\|password` |
| `DOCKER_REGISTRY_URL` | Registry URL for single-registry setup |
| `DOCKER_REGISTRY_USER` | Username for single-registry setup |
| `DOCKER_REGISTRY_PASSWORD` | Password for single-registry setup |
## Requirements ## Requirements
- **Docker** — Docker Engine 20+ or Docker Desktop - **Docker** — Docker Engine 20+ or Docker Desktop
@@ -507,6 +661,15 @@ tsdocker login
tsdocker also falls back to `~/.docker/config.json` — ensure you've run `docker login` for your target registries. tsdocker also falls back to `~/.docker/config.json` — ensure you've run `docker login` for your target registries.
### Push fails with "fetch failed"
tsdocker automatically retries failed requests up to 6 times with exponential backoff. If pushes still fail:
- Check network connectivity to the target registry
- Verify your credentials haven't expired
- Look for retry log messages (`fetch failed (attempt X/6)`) to diagnose the pattern
- Large layers may need longer timeouts — the default 5-minute timeout per request should cover most cases
### Circular dependency detected ### Circular dependency detected
Review your Dockerfiles' `FROM` statements — you have images depending on each other in a loop. Review your Dockerfiles' `FROM` statements — you have images depending on each other in a loop.
@@ -522,16 +685,6 @@ node_modules
dist_ts dist_ts
``` ```
## Migration from Legacy
Previously published as `npmdocker`, now `@git.zone/tsdocker`:
| Old | New |
|-----|-----|
| `npmdocker` command | `tsdocker` command |
| `"npmdocker"` config key | `"@git.zone/tsdocker"` config key |
| CommonJS | ESM with `.js` imports |
## License and Legal Information ## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsdocker', name: '@git.zone/tsdocker',
version: '1.16.0', version: '2.2.4',
description: 'develop npm modules cross platform with docker' description: 'A comprehensive Docker build tool for TypeScript projects with multi-arch support, multi-registry push, and CI-safe session isolation.'
} }

View File

@@ -266,12 +266,15 @@ export class Dockerfile {
public static async buildDockerfiles( public static async buildDockerfiles(
sortedArrayArg: Dockerfile[], sortedArrayArg: Dockerfile[],
session: TsDockerSession, session: TsDockerSession,
options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number }, options?: { platform?: string; timeout?: number; noCache?: boolean; pull?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number; onRegistryStarted?: () => Promise<void>; onBeforeRegistryStop?: () => Promise<void> },
): Promise<Dockerfile[]> { ): Promise<Dockerfile[]> {
const total = sortedArrayArg.length; const total = sortedArrayArg.length;
const overallStart = Date.now(); const overallStart = Date.now();
await Dockerfile.startLocalRegistry(session, options?.isRootless); await Dockerfile.startLocalRegistry(session, options?.isRootless);
if (options?.onRegistryStarted) {
await options.onRegistryStarted();
}
try { try {
if (options?.parallel) { if (options?.parallel) {
@@ -351,6 +354,9 @@ export class Dockerfile {
} }
} }
} finally { } finally {
if (options?.onBeforeRegistryStop) {
await options.onBeforeRegistryStop();
}
await Dockerfile.stopLocalRegistry(session); await Dockerfile.stopLocalRegistry(session);
} }
@@ -662,13 +668,14 @@ export class Dockerfile {
/** /**
* Builds the Dockerfile * Builds the Dockerfile
*/ */
public async build(options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean }): Promise<number> { public async build(options?: { platform?: string; timeout?: number; noCache?: boolean; pull?: boolean; verbose?: boolean }): Promise<number> {
const startTime = Date.now(); const startTime = Date.now();
const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef); const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef);
const config = this.managerRef.config; const config = this.managerRef.config;
const platformOverride = options?.platform; const platformOverride = options?.platform;
const timeout = options?.timeout; const timeout = options?.timeout;
const noCacheFlag = options?.noCache ? ' --no-cache' : ''; const noCacheFlag = options?.noCache ? ' --no-cache' : '';
const pullFlag = options?.pull !== false ? ' --pull' : '';
const verbose = options?.verbose ?? false; const verbose = options?.verbose ?? false;
let buildContextFlag = ''; let buildContextFlag = '';
@@ -683,23 +690,24 @@ export class Dockerfile {
} }
let buildCommand: string; let buildCommand: string;
const builderFlag = this.managerRef.currentBuilderName ? ` --builder ${this.managerRef.currentBuilderName}` : '';
if (platformOverride) { if (platformOverride) {
// Single platform override via buildx // Single platform override via buildx
buildCommand = `docker buildx build --progress=plain --platform ${platformOverride}${noCacheFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; buildCommand = `docker buildx build${builderFlag} --progress=plain --platform ${platformOverride}${noCacheFlag}${pullFlag}${buildContextFlag} --load -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
logger.log('info', `Build: buildx --platform ${platformOverride} --load`); logger.log('info', `Build: buildx --platform ${platformOverride} --load`);
} else if (config.platforms && config.platforms.length > 1) { } else if (config.platforms && config.platforms.length > 1) {
// Multi-platform build using buildx — always push to local registry // Multi-platform build using buildx — always push to local registry
const platformString = config.platforms.join(','); const platformString = config.platforms.join(',');
const registryHost = this.session?.config.registryHost || 'localhost:5234'; const registryHost = this.session?.config.registryHost || 'localhost:5234';
const localTag = `${registryHost}/${this.buildTag}`; const localTag = `${registryHost}/${this.buildTag}`;
buildCommand = `docker buildx build --progress=plain --platform ${platformString}${noCacheFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`; buildCommand = `docker buildx build${builderFlag} --progress=plain --platform ${platformString}${noCacheFlag}${pullFlag}${buildContextFlag} -t ${localTag} -f ${this.filePath} ${buildArgsString} --push .`;
this.localRegistryTag = localTag; this.localRegistryTag = localTag;
logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`); logger.log('info', `Build: buildx --platform ${platformString} --push to local registry`);
} else { } else {
// Standard build // Standard build
const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown'; const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
buildCommand = `docker build --progress=plain --label="version=${versionLabel}"${noCacheFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`; buildCommand = `docker build --progress=plain --label="version=${versionLabel}"${noCacheFlag}${pullFlag} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
logger.log('info', 'Build: docker build (standard)'); logger.log('info', 'Build: docker build (standard)');
} }

View File

@@ -0,0 +1,76 @@
import * as fs from 'fs';
import * as plugins from './tsdocker.plugins.js';
import { logger } from './tsdocker.logging.js';
import type { IGlobalConfig, IRemoteBuilder } from './interfaces/index.js';
const CONFIG_DIR = plugins.path.join(
process.env.HOME || process.env.USERPROFILE || '~',
'.git.zone',
'tsdocker',
);
const CONFIG_PATH = plugins.path.join(CONFIG_DIR, 'config.json');
const DEFAULT_CONFIG: IGlobalConfig = {
remoteBuilders: [],
};
export class GlobalConfig {
static getConfigPath(): string {
return CONFIG_PATH;
}
static load(): IGlobalConfig {
try {
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
const parsed = JSON.parse(raw);
return {
...DEFAULT_CONFIG,
...parsed,
};
} catch {
return { ...DEFAULT_CONFIG };
}
}
static save(config: IGlobalConfig): void {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
}
static addBuilder(builder: IRemoteBuilder): void {
const config = GlobalConfig.load();
const existing = config.remoteBuilders.findIndex((b) => b.name === builder.name);
if (existing >= 0) {
config.remoteBuilders[existing] = builder;
logger.log('info', `Updated remote builder: ${builder.name}`);
} else {
config.remoteBuilders.push(builder);
logger.log('info', `Added remote builder: ${builder.name}`);
}
GlobalConfig.save(config);
}
static removeBuilder(name: string): void {
const config = GlobalConfig.load();
const before = config.remoteBuilders.length;
config.remoteBuilders = config.remoteBuilders.filter((b) => b.name !== name);
if (config.remoteBuilders.length < before) {
logger.log('info', `Removed remote builder: ${name}`);
} else {
logger.log('warn', `Remote builder not found: ${name}`);
}
GlobalConfig.save(config);
}
static getBuilders(): IRemoteBuilder[] {
return GlobalConfig.load().remoteBuilders;
}
/**
* Returns remote builders that match any of the requested platforms
*/
static getBuildersForPlatforms(platforms: string[]): IRemoteBuilder[] {
const builders = GlobalConfig.getBuilders();
return builders.filter((b) => platforms.includes(b.platform));
}
}

View File

@@ -20,6 +20,53 @@ interface ITokenCache {
export class RegistryCopy { export class RegistryCopy {
private tokenCache: ITokenCache = {}; private tokenCache: ITokenCache = {};
/**
* Wraps fetch() with timeout (via AbortSignal) and retry with exponential backoff.
* Retries on network errors and 5xx; does NOT retry on 4xx client errors.
* On 401, clears the token cache entry so the next attempt re-authenticates.
*/
private async fetchWithRetry(
url: string,
options: RequestInit & { duplex?: string },
timeoutMs: number = 300_000,
maxRetries: number = 6,
): Promise<Response> {
const method = (options.method || 'GET').toUpperCase();
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
if (attempt > 1) {
logger.log('info', `Retry ${attempt}/${maxRetries} for ${method} ${url}`);
}
const resp = await fetch(url, {
...options,
signal: AbortSignal.timeout(timeoutMs),
});
// Retry on 5xx server errors (but not 4xx)
if (resp.status >= 500 && attempt < maxRetries) {
const delay = 1000 * Math.pow(2, attempt - 1);
logger.log('warn', `${method} ${url} returned ${resp.status}, retrying in ${delay}ms (attempt ${attempt}/${maxRetries})...`);
await new Promise(r => setTimeout(r, delay));
continue;
}
if (resp.status >= 500) {
logger.log('error', `${method} ${url} returned ${resp.status} after ${maxRetries} attempts, giving up`);
}
return resp;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
const delay = 1000 * Math.pow(2, attempt - 1);
logger.log('warn', `${method} ${url} failed (attempt ${attempt}/${maxRetries}): ${lastError.message}, retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
} else {
logger.log('error', `${method} ${url} failed after ${maxRetries} attempts: ${lastError.message}`);
}
}
}
throw lastError!;
}
/** /**
* Reads Docker credentials from ~/.docker/config.json for a given registry. * Reads Docker credentials from ~/.docker/config.json for a given registry.
* Supports base64-encoded "auth" field in the config. * Supports base64-encoded "auth" field in the config.
@@ -109,7 +156,7 @@ export class RegistryCopy {
} }
try { try {
const checkResp = await fetch(`${apiBase}/v2/`, { method: 'GET' }); const checkResp = await this.fetchWithRetry(`${apiBase}/v2/`, { method: 'GET' }, 30_000);
if (checkResp.ok) return null; // No auth needed if (checkResp.ok) return null; // No auth needed
const wwwAuth = checkResp.headers.get('www-authenticate') || ''; const wwwAuth = checkResp.headers.get('www-authenticate') || '';
@@ -131,7 +178,7 @@ export class RegistryCopy {
headers['Authorization'] = 'Basic ' + Buffer.from(`${creds.username}:${creds.password}`).toString('base64'); headers['Authorization'] = 'Basic ' + Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
} }
const tokenResp = await fetch(tokenUrl.toString(), { headers }); const tokenResp = await this.fetchWithRetry(tokenUrl.toString(), { headers }, 30_000);
if (!tokenResp.ok) { if (!tokenResp.ok) {
const body = await tokenResp.text(); const body = await tokenResp.text();
throw new Error(`Token request failed (${tokenResp.status}): ${body}`); throw new Error(`Token request failed (${tokenResp.status}): ${body}`);
@@ -189,7 +236,16 @@ export class RegistryCopy {
fetchOptions.duplex = 'half'; // Required for streaming body in Node fetchOptions.duplex = 'half'; // Required for streaming body in Node
} }
return fetch(url, fetchOptions); const resp = await this.fetchWithRetry(url, fetchOptions, 300_000);
// Token expired — clear cache so next call re-authenticates
if (resp.status === 401 && token) {
const cacheKey = `${registry}/${`repository:${repo}:${actions}`}`;
logger.log('warn', `Got 401 for ${registry}${path} — clearing cached token for ${cacheKey}`);
delete this.tokenCache[cacheKey];
}
return resp;
} }
/** /**
@@ -320,11 +376,11 @@ export class RegistryCopy {
putHeaders['Authorization'] = `Bearer ${token}`; putHeaders['Authorization'] = `Bearer ${token}`;
} }
const putResp = await fetch(putUrl, { const putResp = await this.fetchWithRetry(putUrl, {
method: 'PUT', method: 'PUT',
headers: putHeaders, headers: putHeaders,
body: blobData, body: blobData,
}); }, 300_000);
if (!putResp.ok) { if (!putResp.ok) {
const body = await putResp.text(); const body = await putResp.text();

77
ts/classes.sshtunnel.ts Normal file
View File

@@ -0,0 +1,77 @@
import * as plugins from './tsdocker.plugins.js';
import { logger } from './tsdocker.logging.js';
import type { IRemoteBuilder } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
/**
* Manages SSH reverse tunnels for remote builder nodes.
* Opens tunnels so that the local staging registry (localhost:<port>)
* is accessible as localhost:<port> on each remote machine.
*/
export class SshTunnelManager {
private tunnelPids: number[] = [];
/**
* Opens a reverse SSH tunnel to make localPort accessible on the remote machine.
* ssh -f -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes
* -R <localPort>:localhost:<localPort> [-i keyPath] user@host
*/
async openTunnel(builder: IRemoteBuilder, localPort: number): Promise<void> {
const keyOpt = builder.sshKeyPath ? `-i ${builder.sshKeyPath} ` : '';
const cmd = [
'ssh -f -N',
'-o StrictHostKeyChecking=no',
'-o ExitOnForwardFailure=yes',
`-R ${localPort}:localhost:${localPort}`,
`${keyOpt}${builder.host}`,
].join(' ');
logger.log('info', `Opening SSH tunnel to ${builder.host} for port ${localPort}...`);
const result = await smartshellInstance.exec(cmd);
if (result.exitCode !== 0) {
throw new Error(
`Failed to open SSH tunnel to ${builder.host}: ${result.stderr || 'unknown error'}`
);
}
// Find the PID of the tunnel process we just started
const pidResult = await smartshellInstance.exec(
`pgrep -f "ssh.*-R ${localPort}:localhost:${localPort}.*${builder.host}" | tail -1`
);
if (pidResult.exitCode === 0 && pidResult.stdout.trim()) {
const pid = parseInt(pidResult.stdout.trim(), 10);
if (!isNaN(pid)) {
this.tunnelPids.push(pid);
logger.log('ok', `SSH tunnel to ${builder.host} established (PID ${pid})`);
}
}
}
/**
* Opens tunnels for all provided remote builders
*/
async openTunnels(builders: IRemoteBuilder[], localPort: number): Promise<void> {
for (const builder of builders) {
await this.openTunnel(builder, localPort);
}
}
/**
* Closes all tunnel processes
*/
async closeAll(): Promise<void> {
for (const pid of this.tunnelPids) {
try {
process.kill(pid, 'SIGTERM');
logger.log('info', `Closed SSH tunnel (PID ${pid})`);
} catch {
// Process may have already exited
}
}
this.tunnelPids = [];
}
}

View File

@@ -7,7 +7,10 @@ import { RegistryStorage } from './classes.registrystorage.js';
import { TsDockerCache } from './classes.tsdockercache.js'; import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js'; import { DockerContext } from './classes.dockercontext.js';
import { TsDockerSession } from './classes.tsdockersession.js'; import { TsDockerSession } from './classes.tsdockersession.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js'; import { RegistryCopy } from './classes.registrycopy.js';
import { GlobalConfig } from './classes.globalconfig.js';
import { SshTunnelManager } from './classes.sshtunnel.js';
import type { ITsDockerConfig, IBuildCommandOptions, IRemoteBuilder } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
@@ -22,7 +25,10 @@ export class TsDockerManager {
public projectInfo: any; public projectInfo: any;
public dockerContext: DockerContext; public dockerContext: DockerContext;
public session!: TsDockerSession; public session!: TsDockerSession;
public currentBuilderName?: string;
private dockerfiles: Dockerfile[] = []; private dockerfiles: Dockerfile[] = [];
private activeRemoteBuilders: IRemoteBuilder[] = [];
private sshTunnelManager?: SshTunnelManager;
constructor(config: ITsDockerConfig) { constructor(config: ITsDockerConfig) {
this.config = config; this.config = config;
@@ -76,6 +82,22 @@ export class TsDockerManager {
} }
} }
} }
// Fallback: check ~/.docker/config.json if env vars didn't provide credentials
if (!this.registryStorage.getRegistryByUrl(registryUrl)) {
const dockerConfigCreds = RegistryCopy.getDockerConfigCredentials(registryUrl);
if (dockerConfigCreds) {
const registry = new DockerRegistry({
registryUrl,
username: dockerConfigCreds.username,
password: dockerConfigCreds.password,
});
this.registryStorage.addRegistry(registry);
logger.log('info', `Loaded credentials for ${registryUrl} from ~/.docker/config.json`);
} else {
logger.log('warn', `No credentials found for ${registryUrl} (checked env vars and ~/.docker/config.json)`);
}
}
} }
} }
@@ -110,6 +132,27 @@ export class TsDockerManager {
return this.dockerfiles; return this.dockerfiles;
} }
/**
* Filters discovered Dockerfiles by name patterns (glob-style).
* Mutates this.dockerfiles in place.
*/
public filterDockerfiles(patterns: string[]): void {
const matched = this.dockerfiles.filter((df) => {
const basename = plugins.path.basename(df.filePath);
return patterns.some((pattern) => {
if (pattern.includes('*') || pattern.includes('?')) {
const regexStr = '^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
return new RegExp(regexStr).test(basename);
}
return basename === pattern;
});
});
if (matched.length === 0) {
logger.log('warn', `No Dockerfiles matched patterns: ${patterns.join(', ')}`);
}
this.dockerfiles = matched;
}
/** /**
* Builds discovered Dockerfiles in dependency order. * Builds discovered Dockerfiles in dependency order.
* When options.patterns is provided, only matching Dockerfiles (and their dependencies) are built. * When options.patterns is provided, only matching Dockerfiles (and their dependencies) are built.
@@ -197,6 +240,7 @@ export class TsDockerManager {
const total = toBuild.length; const total = toBuild.length;
const overallStart = Date.now(); const overallStart = Date.now();
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless); await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
await this.openRemoteTunnels();
try { try {
if (options?.parallel) { if (options?.parallel) {
@@ -223,6 +267,7 @@ export class TsDockerManager {
platform: options?.platform, platform: options?.platform,
timeout: options?.timeout, timeout: options?.timeout,
noCache: options?.noCache, noCache: options?.noCache,
pull: options?.pull,
verbose: options?.verbose, verbose: options?.verbose,
}); });
logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`); logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`);
@@ -268,6 +313,7 @@ export class TsDockerManager {
platform: options?.platform, platform: options?.platform,
timeout: options?.timeout, timeout: options?.timeout,
noCache: options?.noCache, noCache: options?.noCache,
pull: options?.pull,
verbose: options?.verbose, verbose: options?.verbose,
}); });
logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`); logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
@@ -294,6 +340,7 @@ export class TsDockerManager {
} }
} }
} finally { } finally {
await this.closeRemoteTunnels();
await Dockerfile.stopLocalRegistry(this.session); await Dockerfile.stopLocalRegistry(this.session);
} }
@@ -305,10 +352,13 @@ export class TsDockerManager {
platform: options?.platform, platform: options?.platform,
timeout: options?.timeout, timeout: options?.timeout,
noCache: options?.noCache, noCache: options?.noCache,
pull: options?.pull,
verbose: options?.verbose, verbose: options?.verbose,
isRootless: this.dockerContext.contextInfo?.isRootless, isRootless: this.dockerContext.contextInfo?.isRootless,
parallel: options?.parallel, parallel: options?.parallel,
parallelConcurrency: options?.parallelConcurrency, parallelConcurrency: options?.parallelConcurrency,
onRegistryStarted: () => this.openRemoteTunnels(),
onBeforeRegistryStop: () => this.closeRemoteTunnels(),
}); });
} }
@@ -335,35 +385,120 @@ export class TsDockerManager {
} }
/** /**
* Ensures Docker buildx is set up for multi-architecture builds * Ensures Docker buildx is set up for multi-architecture builds.
* When remote builders are configured in the global config, creates a multi-node
* builder with native nodes instead of relying on QEMU emulation.
*/ */
private async ensureBuildx(): Promise<void> { private async ensureBuildx(): Promise<void> {
const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || ''); const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || '');
const platforms = this.config.platforms?.join(', ') || 'default'; const platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx [${platforms}]...`); logger.log('info', `Setting up Docker buildx [${platforms}]...`);
logger.log('info', `Builder: ${builderName}`); logger.log('info', `Builder: ${builderName}`);
// Check for remote builders matching our target platforms
const requestedPlatforms = this.config.platforms || ['linux/amd64'];
const remoteBuilders = GlobalConfig.getBuildersForPlatforms(requestedPlatforms);
if (remoteBuilders.length > 0) {
await this.ensureBuildxWithRemoteNodes(builderName, requestedPlatforms, remoteBuilders);
} else {
await this.ensureBuildxLocal(builderName);
}
this.currentBuilderName = builderName;
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
}
/**
* Creates a multi-node buildx builder with local + remote SSH nodes.
*/
private async ensureBuildxWithRemoteNodes(
builderName: string,
requestedPlatforms: string[],
remoteBuilders: IRemoteBuilder[],
): Promise<void> {
const remotePlatforms = new Set(remoteBuilders.map((b) => b.platform));
const localPlatforms = requestedPlatforms.filter((p) => !remotePlatforms.has(p));
logger.log('info', `Remote builders: ${remoteBuilders.map((b) => `${b.name} (${b.platform} @ ${b.host})`).join(', ')}`);
if (localPlatforms.length > 0) {
logger.log('info', `Local platforms: ${localPlatforms.join(', ')}`);
}
// Always recreate the builder to ensure correct node topology
await smartshellInstance.execSilent(`docker buildx rm ${builderName} 2>/dev/null || true`);
// Create the local node
const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : '';
await smartshellInstance.exec(
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host${localPlatformFlag}`
);
// Append remote nodes
for (const builder of remoteBuilders) {
logger.log('info', `Appending remote node: ${builder.name} (${builder.platform}) via ssh://${builder.host}`);
const appendResult = await smartshellInstance.exec(
`docker buildx create --append --name ${builderName} --driver docker-container --driver-opt network=host --platform ${builder.platform} --node ${builder.name} ssh://${builder.host}`
);
if (appendResult.exitCode !== 0) {
throw new Error(`Failed to append remote builder ${builder.name}: ${appendResult.stderr}`);
}
}
// Bootstrap all nodes
await smartshellInstance.exec(`docker buildx inspect --builder ${builderName} --bootstrap`);
// Store active remote builders for SSH tunnel setup during build
this.activeRemoteBuilders = remoteBuilders;
}
/**
* Creates a single-node local buildx builder (original behavior, uses QEMU for cross-platform).
*/
private async ensureBuildxLocal(builderName: string): Promise<void> {
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`); const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
if (inspectResult.exitCode !== 0) { if (inspectResult.exitCode !== 0) {
logger.log('info', 'Creating new buildx builder with host network...'); logger.log('info', 'Creating new buildx builder with host network...');
await smartshellInstance.exec( await smartshellInstance.exec(
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use` `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host`
); );
await smartshellInstance.exec('docker buildx inspect --bootstrap'); await smartshellInstance.exec(`docker buildx inspect --builder ${builderName} --bootstrap`);
} else { } else {
const inspectOutput = inspectResult.stdout || ''; const inspectOutput = inspectResult.stdout || '';
if (!inspectOutput.includes('network=host')) { if (!inspectOutput.includes('network=host')) {
logger.log('info', 'Recreating buildx builder with host network (migration)...'); logger.log('info', 'Recreating buildx builder with host network (migration)...');
await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`); await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`);
await smartshellInstance.exec( await smartshellInstance.exec(
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use` `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host`
); );
await smartshellInstance.exec('docker buildx inspect --bootstrap'); await smartshellInstance.exec(`docker buildx inspect --builder ${builderName} --bootstrap`);
} else {
await smartshellInstance.exec(`docker buildx use ${builderName}`);
} }
} }
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`); this.activeRemoteBuilders = [];
}
/**
* Opens SSH reverse tunnels for remote builders so they can reach the local registry.
*/
private async openRemoteTunnels(): Promise<void> {
if (this.activeRemoteBuilders.length === 0) return;
this.sshTunnelManager = new SshTunnelManager();
await this.sshTunnelManager.openTunnels(
this.activeRemoteBuilders,
this.session.config.registryPort,
);
}
/**
* Closes any active SSH tunnels.
*/
private async closeRemoteTunnels(): Promise<void> {
if (this.sshTunnelManager) {
await this.sshTunnelManager.closeAll();
this.sshTunnelManager = undefined;
}
} }
/** /**

View File

@@ -4,6 +4,7 @@ import { logger } from './tsdocker.logging.js';
export interface ISessionConfig { export interface ISessionConfig {
sessionId: string; sessionId: string;
projectHash: string;
registryPort: number; registryPort: number;
registryHost: string; registryHost: string;
registryContainerName: string; registryContainerName: string;
@@ -17,8 +18,8 @@ export interface ISessionConfig {
* Generates unique ports, container names, and builder names so that * Generates unique ports, container names, and builder names so that
* concurrent CI jobs on the same Docker host don't collide. * concurrent CI jobs on the same Docker host don't collide.
* *
* In local (non-CI) dev the builder suffix is empty, preserving the * In local (non-CI) dev the builder suffix contains a project hash so
* persistent builder behavior. * that concurrent runs in different project directories use separate builders.
*/ */
export class TsDockerSession { export class TsDockerSession {
public config: ISessionConfig; public config: ISessionConfig;
@@ -34,16 +35,18 @@ export class TsDockerSession {
public static async create(): Promise<TsDockerSession> { public static async create(): Promise<TsDockerSession> {
const sessionId = const sessionId =
process.env.TSDOCKER_SESSION_ID || crypto.randomBytes(4).toString('hex'); process.env.TSDOCKER_SESSION_ID || crypto.randomBytes(4).toString('hex');
const projectHash = crypto.createHash('sha256').update(process.cwd()).digest('hex').substring(0, 8);
const registryPort = await TsDockerSession.allocatePort(); const registryPort = await TsDockerSession.allocatePort();
const registryHost = `localhost:${registryPort}`; const registryHost = `localhost:${registryPort}`;
const registryContainerName = `tsdocker-registry-${sessionId}`; const registryContainerName = `tsdocker-registry-${sessionId}`;
const { isCI, ciSystem } = TsDockerSession.detectCI(); const { isCI, ciSystem } = TsDockerSession.detectCI();
const builderSuffix = isCI ? `-${sessionId}` : ''; const builderSuffix = isCI ? `-${projectHash}-${sessionId}` : `-${projectHash}`;
const config: ISessionConfig = { const config: ISessionConfig = {
sessionId, sessionId,
projectHash,
registryPort, registryPort,
registryHost, registryHost,
registryContainerName, registryContainerName,
@@ -99,9 +102,10 @@ export class TsDockerSession {
logger.log('info', '=== TSDOCKER SESSION ==='); logger.log('info', '=== TSDOCKER SESSION ===');
logger.log('info', `Session ID: ${c.sessionId}`); logger.log('info', `Session ID: ${c.sessionId}`);
logger.log('info', `Registry: ${c.registryHost} (container: ${c.registryContainerName})`); logger.log('info', `Registry: ${c.registryHost} (container: ${c.registryContainerName})`);
logger.log('info', `Project hash: ${c.projectHash}`);
logger.log('info', `Builder suffix: ${c.builderSuffix}`);
if (c.isCI) { if (c.isCI) {
logger.log('info', `CI detected: ${c.ciSystem}`); logger.log('info', `CI detected: ${c.ciSystem}`);
logger.log('info', `Builder suffix: ${c.builderSuffix}`);
} }
} }
} }

View File

@@ -1,15 +1,7 @@
/** /**
* Configuration interface for tsdocker * Configuration interface for tsdocker
* Extends legacy config with new Docker build capabilities
*/ */
export interface ITsDockerConfig { export interface ITsDockerConfig {
// Legacy (backward compatible)
baseImage: string;
command: string;
dockerSock: boolean;
keyValueObject: { [key: string]: any };
// New Docker build config
registries?: string[]; registries?: string[];
registryRepoMap?: { [registry: string]: string }; registryRepoMap?: { [registry: string]: string };
buildArgEnvMap?: { [dockerArg: string]: string }; buildArgEnvMap?: { [dockerArg: string]: string };
@@ -77,6 +69,7 @@ export interface IBuildCommandOptions {
platform?: string; // Single platform override (e.g., 'linux/arm64') platform?: string; // Single platform override (e.g., 'linux/arm64')
timeout?: number; // Build timeout in seconds timeout?: number; // Build timeout in seconds
noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache) noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
pull?: boolean; // Pull fresh base images before building (default: true)
cached?: boolean; // Skip builds when Dockerfile content hasn't changed cached?: boolean; // Skip builds when Dockerfile content hasn't changed
verbose?: boolean; // Stream raw docker build output (default: silent) verbose?: boolean; // Stream raw docker build output (default: silent)
context?: string; // Explicit Docker context name (--context flag) context?: string; // Explicit Docker context name (--context flag)
@@ -103,3 +96,20 @@ export interface IDockerContextInfo {
dockerHost?: string; // value of DOCKER_HOST env var, if set dockerHost?: string; // value of DOCKER_HOST env var, if set
topology?: 'socket-mount' | 'dind' | 'local'; topology?: 'socket-mount' | 'dind' | 'local';
} }
/**
* A remote builder node for native cross-platform builds
*/
export interface IRemoteBuilder {
name: string; // e.g., "arm64-builder"
host: string; // e.g., "armbuilder@192.168.190.216"
platform: string; // e.g., "linux/arm64"
sshKeyPath?: string; // e.g., "~/.ssh/id_ed25519"
}
/**
* Global tsdocker configuration stored at ~/.git.zone/tsdocker/config.json
*/
export interface IGlobalConfig {
remoteBuilders: IRemoteBuilder[];
}

View File

@@ -3,27 +3,98 @@ import * as paths from './tsdocker.paths.js';
// modules // modules
import * as ConfigModule from './tsdocker.config.js'; import * as ConfigModule from './tsdocker.config.js';
import * as DockerModule from './tsdocker.docker.js';
import { logger, ora } from './tsdocker.logging.js'; import { logger, ora } from './tsdocker.logging.js';
import { TsDockerManager } from './classes.tsdockermanager.js'; import { TsDockerManager } from './classes.tsdockermanager.js';
import { DockerContext } from './classes.dockercontext.js'; import { DockerContext } from './classes.dockercontext.js';
import { GlobalConfig } from './classes.globalconfig.js';
import type { IBuildCommandOptions } from './interfaces/index.js'; import type { IBuildCommandOptions } from './interfaces/index.js';
import { commitinfo } from './00_commitinfo_data.js'; import { commitinfo } from './00_commitinfo_data.js';
const tsdockerCli = new plugins.smartcli.Smartcli(); const tsdockerCli = new plugins.smartcli.Smartcli();
tsdockerCli.addVersion(commitinfo.version); tsdockerCli.addVersion(commitinfo.version);
const printManPage = () => {
const manPage = `
TSDOCKER(1) User Commands TSDOCKER(1)
NAME
tsdocker - build, test, and push Docker images
VERSION
${commitinfo.version}
SYNOPSIS
tsdocker <command> [options]
COMMANDS
build [patterns...] [flags] Build Dockerfiles in dependency order
push [patterns...] [flags] Build and push images to registries
pull <registry-url> Pull images from a registry
test [flags] Build and run container test scripts
login Authenticate with configured registries
list List discovered Dockerfiles
config <subcommand> [flags] Manage global tsdocker configuration
clean [-y] [--all] Interactive Docker resource cleanup
BUILD / PUSH OPTIONS
--platform=<p> Target platform (e.g. linux/arm64)
--timeout=<s> Build timeout in seconds
--no-cache Rebuild without Docker layer cache
--no-pull Skip pulling latest base images (use cached)
--cached Skip builds when Dockerfile is unchanged
--verbose Stream raw docker build output
--parallel[=<n>] Parallel builds (optional concurrency limit)
--context=<name> Docker context to use
PUSH-ONLY OPTIONS
--registry=<url> Push to a specific registry
--no-build Push already-built images (skip build step)
CLEAN OPTIONS
-y Auto-confirm all prompts
--all Include all images and volumes (not just dangling)
CONFIG SUBCOMMANDS
add-builder Add a remote builder node
--name=<n> Builder name (e.g. arm64-builder)
--host=<h> SSH host (e.g. user@192.168.1.100)
--platform=<p> Platform (e.g. linux/arm64)
--ssh-key=<path> SSH key path (optional)
remove-builder Remove a remote builder by name
--name=<n> Builder name to remove
list-builders List all configured remote builders
show Show full global config
CONFIGURATION
Configure via smartconfig.json under the "@git.zone/tsdocker" key:
registries Array of registry URLs to push to
registryRepoMap Map of registry URL to repo path overrides
buildArgEnvMap Map of Docker build-arg names to env var names
platforms Array of target platforms (default: ["linux/amd64"])
push Boolean, auto-push after build
testDir Directory containing test_*.sh scripts
Global config is stored at ~/.git.zone/tsdocker/config.json
and managed via the "config" command.
EXAMPLES
tsdocker build
tsdocker build Dockerfile_app --platform=linux/arm64
tsdocker push --registry=ghcr.io
tsdocker test --verbose
tsdocker clean -y --all
tsdocker config add-builder --name=arm64 --host=user@host --platform=linux/arm64
tsdocker config list-builders
`;
console.log(manPage);
};
export let run = () => { export let run = () => {
// Default command: run tests in container (legacy behavior) // Default command: print man page
tsdockerCli.standardCommand().subscribe(async argvArg => { tsdockerCli.standardCommand().subscribe(async () => {
const configArg = await ConfigModule.run().then(DockerModule.run); printManPage();
if (configArg.exitCode === 0) {
logger.log('success', 'container ended all right!');
} else {
logger.log('error', `container ended with error! Exit Code is ${configArg.exitCode}`);
process.exit(1);
}
}); });
/** /**
@@ -50,6 +121,8 @@ export let run = () => {
if (argvArg.cache === false) { if (argvArg.cache === false) {
buildOptions.noCache = true; buildOptions.noCache = true;
} }
// --pull is default true; --no-pull sets pull=false
buildOptions.pull = argvArg.pull !== false;
if (argvArg.cached) { if (argvArg.cached) {
buildOptions.cached = true; buildOptions.cached = true;
} }
@@ -100,6 +173,7 @@ export let run = () => {
if (argvArg.cache === false) { if (argvArg.cache === false) {
buildOptions.noCache = true; buildOptions.noCache = true;
} }
buildOptions.pull = argvArg.pull !== false;
if (argvArg.verbose) { if (argvArg.verbose) {
buildOptions.verbose = true; buildOptions.verbose = true;
} }
@@ -110,8 +184,15 @@ export let run = () => {
} }
} }
// Build images first (if not already built) // Build images first, unless --no-build is set
if (argvArg.build === false) {
await manager.discoverDockerfiles();
if (buildOptions.patterns?.length) {
manager.filterDockerfiles(buildOptions.patterns);
}
} else {
await manager.build(buildOptions); await manager.build(buildOptions);
}
// Get registry from --registry flag // Get registry from --registry flag
const registryArg = argvArg.registry as string | undefined; const registryArg = argvArg.registry as string | undefined;
@@ -166,6 +247,7 @@ export let run = () => {
if (argvArg.cache === false) { if (argvArg.cache === false) {
buildOptions.noCache = true; buildOptions.noCache = true;
} }
buildOptions.pull = argvArg.pull !== false;
if (argvArg.cached) { if (argvArg.cached) {
buildOptions.cached = true; buildOptions.cached = true;
} }
@@ -222,21 +304,73 @@ export let run = () => {
}); });
/** /**
* this command is executed inside docker and meant for use from outside docker * Manage global tsdocker configuration (remote builders, etc.)
* Usage: tsdocker config <subcommand> [--name=...] [--host=...] [--platform=...] [--ssh-key=...]
*/ */
tsdockerCli.addCommand('runinside').subscribe(async argvArg => { tsdockerCli.addCommand('config').subscribe(async argvArg => {
logger.log('ok', 'Allright. We are now in Docker!'); try {
ora.text('now trying to run your specified command'); const subcommand = argvArg._[1] as string;
const configArg = await ConfigModule.run();
const smartshellInstance = new plugins.smartshell.Smartshell({ switch (subcommand) {
executor: 'bash' case 'add-builder': {
}); const name = argvArg.name as string;
ora.stop(); const host = argvArg.host as string;
await smartshellInstance.exec(configArg.command).then(response => { const platform = argvArg.platform as string;
if (response.exitCode !== 0) { const sshKeyPath = argvArg['ssh-key'] as string | undefined;
if (!name || !host || !platform) {
logger.log('error', 'Required: --name, --host, --platform');
logger.log('info', 'Usage: tsdocker config add-builder --name=arm64-builder --host=user@host --platform=linux/arm64 [--ssh-key=~/.ssh/id_ed25519]');
process.exit(1);
}
GlobalConfig.addBuilder({ name, host, platform, sshKeyPath });
logger.log('success', `Remote builder "${name}" configured: ${platform} via ssh://${host}`);
break;
}
case 'remove-builder': {
const name = argvArg.name as string;
if (!name) {
logger.log('error', 'Required: --name');
logger.log('info', 'Usage: tsdocker config remove-builder --name=arm64-builder');
process.exit(1);
}
GlobalConfig.removeBuilder(name);
logger.log('success', `Remote builder "${name}" removed`);
break;
}
case 'list-builders': {
const builders = GlobalConfig.getBuilders();
if (builders.length === 0) {
logger.log('info', 'No remote builders configured');
} else {
logger.log('info', `${builders.length} remote builder(s):`);
for (const b of builders) {
const keyInfo = b.sshKeyPath ? ` (key: ${b.sshKeyPath})` : '';
logger.log('info', ` ${b.name}: ${b.platform} via ssh://${b.host}${keyInfo}`);
}
}
break;
}
case 'show': {
const config = GlobalConfig.load();
logger.log('info', `Config file: ${GlobalConfig.getConfigPath()}`);
console.log(JSON.stringify(config, null, 2));
break;
}
default:
logger.log('error', `Unknown config subcommand: ${subcommand || '(none)'}`);
logger.log('info', 'Available: add-builder, remove-builder, list-builders, show');
process.exit(1);
}
} catch (err) {
logger.log('error', `Config failed: ${(err as Error).message}`);
process.exit(1); process.exit(1);
} }
});
}); });
tsdockerCli.addCommand('clean').subscribe(async argvArg => { tsdockerCli.addCommand('clean').subscribe(async argvArg => {
@@ -436,19 +570,5 @@ export let run = () => {
} }
}); });
tsdockerCli.addCommand('vscode').subscribe(async argvArg => {
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash'
});
logger.log('ok', `Starting vscode in cwd ${paths.cwd}`);
await smartshellInstance.execAndWaitForLine(
`docker run -p 127.0.0.1:8443:8443 -v "${
paths.cwd
}:/home/coder/project" registry.gitlab.com/hosttoday/ht-docker-vscode --allow-http --no-auth`,
/Connected to shared process/
);
await plugins.smartopen.openUrl('testing-vscode.git.zone:8443');
});
tsdockerCli.startParse(); tsdockerCli.startParse();
}; };

View File

@@ -1,34 +1,10 @@
import * as plugins from './tsdocker.plugins.js'; import * as plugins from './tsdocker.plugins.js';
import * as paths from './tsdocker.paths.js'; import * as paths from './tsdocker.paths.js';
import * as fs from 'fs';
import type { ITsDockerConfig } from './interfaces/index.js'; import type { ITsDockerConfig } from './interfaces/index.js';
// Re-export ITsDockerConfig as IConfig for backward compatibility const buildConfig = async (): Promise<ITsDockerConfig> => {
export type IConfig = ITsDockerConfig & { const smartconfig = new plugins.smartconfig.Smartconfig(paths.cwd);
exitCode?: number; const config = smartconfig.dataFor<ITsDockerConfig>('@git.zone/tsdocker', {
};
const getQenvKeyValueObject = async () => {
let qenvKeyValueObjectArray: { [key: string]: string | number };
if (fs.existsSync(plugins.path.join(paths.cwd, 'qenv.yml'))) {
qenvKeyValueObjectArray = new plugins.qenv.Qenv(paths.cwd, '.nogit/').keyValueObject;
} else {
qenvKeyValueObjectArray = {};
}
return qenvKeyValueObjectArray;
};
const buildConfig = async (qenvKeyValueObjectArg: { [key: string]: string | number }) => {
const npmextra = new plugins.npmextra.Npmextra(paths.cwd);
const config = npmextra.dataFor<IConfig>('@git.zone/tsdocker', {
// Legacy options (backward compatible)
baseImage: 'hosttoday/ht-docker-node:npmdocker',
init: 'rm -rf node_nodules/ && yarn install',
command: 'npmci npm test',
dockerSock: false,
keyValueObject: qenvKeyValueObjectArg,
// New Docker build options
registries: [], registries: [],
registryRepoMap: {}, registryRepoMap: {},
buildArgEnvMap: {}, buildArgEnvMap: {},
@@ -39,7 +15,6 @@ const buildConfig = async (qenvKeyValueObjectArg: { [key: string]: string | numb
return config; return config;
}; };
export let run = async (): Promise<IConfig> => { export let run = async (): Promise<ITsDockerConfig> => {
const config = await getQenvKeyValueObject().then(buildConfig); return buildConfig();
return config;
}; };

View File

@@ -1,169 +0,0 @@
import * as plugins from './tsdocker.plugins.js';
import * as paths from './tsdocker.paths.js';
import * as snippets from './tsdocker.snippets.js';
import { logger, ora } from './tsdocker.logging.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash'
});
// interfaces
import type { IConfig } from './tsdocker.config.js';
let config: IConfig;
/**
* the docker data used to build the internal testing container
*/
const dockerData = {
imageTag: 'npmdocker-temp-image:latest',
containerName: 'npmdocker-temp-container',
dockerProjectMountString: '',
dockerSockString: '',
dockerEnvString: ''
};
/**
* check if docker is available
*/
const checkDocker = () => {
const done = plugins.smartpromise.defer();
ora.text('checking docker...');
if (smartshellInstance.exec('which docker')) {
logger.log('ok', 'Docker found!');
done.resolve();
} else {
done.reject(new Error('docker not found on this machine'));
}
return done.promise;
};
/**
* builds the Dockerfile according to the config in the project
*/
const buildDockerFile = async () => {
const done = plugins.smartpromise.defer();
ora.text('building Dockerfile...');
const dockerfile: string = snippets.dockerfileSnippet({
baseImage: config.baseImage,
command: config.command
});
logger.log('info', `Base image is: ${config.baseImage}`);
logger.log('info', `Command is: ${config.command}`);
await plugins.smartfs.file(plugins.path.join(paths.cwd, 'npmdocker')).write(dockerfile);
logger.log('ok', 'Dockerfile created!');
ora.stop();
done.resolve();
return done.promise;
};
/**
* builds the Dockerimage from the built Dockerfile
*/
const buildDockerImage = async () => {
logger.log('info', 'pulling latest base image from registry...');
await smartshellInstance.exec(`docker pull ${config.baseImage}`);
ora.text('building Dockerimage...');
const execResult = await smartshellInstance.execSilent(
`docker build --load -f npmdocker -t ${dockerData.imageTag} ${paths.cwd}`
);
if (execResult.exitCode !== 0) {
console.log(execResult.stdout);
process.exit(1);
}
logger.log('ok', 'Dockerimage built!');
};
const buildDockerProjectMountString = async () => {
if (process.env.CI !== 'true') {
dockerData.dockerProjectMountString = `-v ${paths.cwd}:/workspace`;
}
};
/**
* builds an environment string that docker cli understands
*/
const buildDockerEnvString = async () => {
for (const key of Object.keys(config.keyValueObject)) {
const envString = (dockerData.dockerEnvString =
dockerData.dockerEnvString + `-e ${key}=${config.keyValueObject[key]} `);
}
};
/**
* creates string to mount the docker.sock inside the testcontainer
*/
const buildDockerSockString = async () => {
if (config.dockerSock) {
dockerData.dockerSockString = `-v /var/run/docker.sock:/var/run/docker.sock`;
}
};
/**
* creates a container by running the built Dockerimage
*/
const runDockerImage = async () => {
const done = plugins.smartpromise.defer();
ora.text('starting Container...');
ora.stop();
logger.log('info', 'now running Dockerimage');
config.exitCode = (await smartshellInstance.exec(
`docker run ${dockerData.dockerProjectMountString} ${dockerData.dockerSockString} ${
dockerData.dockerEnvString
} --name ${dockerData.containerName} ${dockerData.imageTag}`
)).exitCode;
};
/**
* cleans up: deletes the test container
*/
const deleteDockerContainer = async () => {
await smartshellInstance.execSilent(`docker rm -f ${dockerData.containerName}`);
};
/**
* cleans up deletes the test image
*/
const deleteDockerImage = async () => {
await smartshellInstance.execSilent(`docker rmi ${dockerData.imageTag}`).then(async response => {
if (response.exitCode !== 0) {
console.log(response.stdout);
}
});
};
const preClean = async () => {
await deleteDockerImage()
.then(deleteDockerContainer)
.then(async () => {
logger.log('ok', 'ensured clean Docker environment!');
});
};
const postClean = async () => {
await deleteDockerContainer()
.then(deleteDockerImage)
.then(async () => {
logger.log('ok', 'cleaned up!');
});
await plugins.smartfs.file(paths.npmdockerFile).delete();
};
export let run = async (configArg: IConfig): Promise<IConfig> => {
config = configArg;
const resultConfig = await checkDocker()
.then(preClean)
.then(buildDockerFile)
.then(buildDockerImage)
.then(buildDockerProjectMountString)
.then(buildDockerEnvString)
.then(buildDockerSockString)
.then(runDockerImage)
.then(postClean)
.catch(err => {
console.log(err);
});
return config;
};

View File

@@ -11,4 +11,3 @@ export let cwd = process.cwd();
export let packageBase = plugins.path.join(__dirname, '../'); export let packageBase = plugins.path.join(__dirname, '../');
export let assets = plugins.path.join(packageBase, 'assets/'); export let assets = plugins.path.join(packageBase, 'assets/');
fs.mkdirSync(assets, { recursive: true }); fs.mkdirSync(assets, { recursive: true });
export let npmdockerFile = plugins.path.join(cwd, 'npmdocker');

View File

@@ -1,36 +1,28 @@
// push.rocks scope // push.rocks scope
import * as lik from '@push.rocks/lik'; import * as lik from '@push.rocks/lik';
import * as npmextra from '@push.rocks/npmextra'; import * as smartconfig from '@push.rocks/smartconfig';
import * as path from 'path'; import * as path from 'path';
import * as projectinfo from '@push.rocks/projectinfo'; import * as projectinfo from '@push.rocks/projectinfo';
import * as smartpromise from '@push.rocks/smartpromise';
import * as qenv from '@push.rocks/qenv';
import * as smartcli from '@push.rocks/smartcli'; import * as smartcli from '@push.rocks/smartcli';
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs'; import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
import * as smartlog from '@push.rocks/smartlog'; import * as smartlog from '@push.rocks/smartlog';
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local'; import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
import * as smartlogSouceOra from '@push.rocks/smartlog-source-ora'; import * as smartlogSouceOra from '@push.rocks/smartlog-source-ora';
import * as smartopen from '@push.rocks/smartopen';
import * as smartinteract from '@push.rocks/smartinteract'; import * as smartinteract from '@push.rocks/smartinteract';
import * as smartshell from '@push.rocks/smartshell'; import * as smartshell from '@push.rocks/smartshell';
import * as smartstring from '@push.rocks/smartstring';
// Create smartfs instance // Create smartfs instance
export const smartfs = new SmartFs(new SmartFsProviderNode()); export const smartfs = new SmartFs(new SmartFsProviderNode());
export { export {
lik, lik,
npmextra, smartconfig,
path, path,
projectinfo, projectinfo,
smartpromise,
qenv,
smartcli, smartcli,
smartinteract, smartinteract,
smartlog, smartlog,
smartlogDestinationLocal, smartlogDestinationLocal,
smartlogSouceOra, smartlogSouceOra,
smartopen,
smartshell, smartshell,
smartstring
}; };

View File

@@ -1,34 +0,0 @@
import * as plugins from './tsdocker.plugins.js';
export interface IDockerfileSnippet {
baseImage: string;
command: string;
}
let getMountSolutionString = (optionsArg: IDockerfileSnippet) => {
if (process.env.CI) {
return 'COPY ./ /workspace';
} else {
return '# not copying workspcae since not in CI';
}
};
let getGlobalPreparationString = (optionsArg: IDockerfileSnippet) => {
// Always install tsdocker to ensure the latest version is available
return 'RUN npm install -g @git.zone/tsdocker';
};
export let dockerfileSnippet = (optionsArg: IDockerfileSnippet): string => {
return plugins.smartstring.indent.normalize(
`
FROM ${optionsArg.baseImage}
# For info about what tsdocker does read the docs at https://gitzone.github.io/tsdocker
${getGlobalPreparationString(optionsArg)}
${getMountSolutionString(optionsArg)}
WORKDIR /workspace
ENV CI=true
ENTRYPOINT ["tsdocker"]
CMD ["runinside"]
`
);
};

View File

@@ -5,8 +5,7 @@
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"baseUrl": ".", "useDefineForClassFields": false
"paths": {}
}, },
"exclude": ["dist_*/**/*.d.ts"] "exclude": ["dist_*/**/*.d.ts"]
} }