Compare commits

...

6 Commits

Author SHA1 Message Date
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
6 changed files with 135 additions and 9 deletions

View File

@@ -1,5 +1,30 @@
# Changelog
## 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)
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.

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsdocker",
"version": "1.16.0",
"version": "1.17.2",
"private": false,
"description": "develop npm modules cross platform with docker",
"main": "dist_ts/index.js",

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsdocker',
version: '1.16.0',
version: '1.17.2',
description: 'develop npm modules cross platform with docker'
}

View File

@@ -20,6 +20,53 @@ interface ITokenCache {
export class RegistryCopy {
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 = 3,
): 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.
* Supports base64-encoded "auth" field in the config.
@@ -109,7 +156,7 @@ export class RegistryCopy {
}
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
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');
}
const tokenResp = await fetch(tokenUrl.toString(), { headers });
const tokenResp = await this.fetchWithRetry(tokenUrl.toString(), { headers }, 30_000);
if (!tokenResp.ok) {
const body = await tokenResp.text();
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
}
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}`;
}
const putResp = await fetch(putUrl, {
const putResp = await this.fetchWithRetry(putUrl, {
method: 'PUT',
headers: putHeaders,
body: blobData,
});
}, 300_000);
if (!putResp.ok) {
const body = await putResp.text();

View File

@@ -7,6 +7,7 @@ import { RegistryStorage } from './classes.registrystorage.js';
import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js';
import { TsDockerSession } from './classes.tsdockersession.js';
import { RegistryCopy } from './classes.registrycopy.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -76,6 +77,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 +127,27 @@ export class TsDockerManager {
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.
* When options.patterns is provided, only matching Dockerfiles (and their dependencies) are built.

View File

@@ -110,8 +110,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);
}
// Get registry from --registry flag
const registryArg = argvArg.registry as string | undefined;