Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16cd0bbd87 | |||
| cc83743f9a |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-06 - 1.8.0 - feat(build)
|
||||||
|
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
|
||||||
|
- Add ICacheEntry and ICacheData interfaces and a cached flag to IBuildCommandOptions
|
||||||
|
- Integrate cached mode in TsDockerManager: skip builds on cache hits, verify image presence, record builds on misses, and still perform dependency tagging
|
||||||
|
- Expose --cached option in CLI to enable the cached build flow
|
||||||
|
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tsdocker",
|
"name": "@git.zone/tsdocker",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "develop npm modules cross platform with docker",
|
"description": "develop npm modules cross platform with docker",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tsdocker',
|
name: '@git.zone/tsdocker',
|
||||||
version: '1.7.0',
|
version: '1.8.0',
|
||||||
description: 'develop npm modules cross platform with docker'
|
description: 'develop npm modules cross platform with docker'
|
||||||
}
|
}
|
||||||
|
|||||||
117
ts/classes.tsdockercache.ts
Normal file
117
ts/classes.tsdockercache.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import * as crypto from 'crypto';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as plugins from './tsdocker.plugins.js';
|
||||||
|
import * as paths from './tsdocker.paths.js';
|
||||||
|
import { logger } from './tsdocker.logging.js';
|
||||||
|
import type { ICacheData, ICacheEntry } from './interfaces/index.js';
|
||||||
|
|
||||||
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
|
executor: 'bash',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages content-hash-based build caching for Dockerfiles.
|
||||||
|
* Cache is stored in .nogit/tsdocker_support.json.
|
||||||
|
*/
|
||||||
|
export class TsDockerCache {
|
||||||
|
private cacheFilePath: string;
|
||||||
|
private data: ICacheData;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.cacheFilePath = path.join(paths.cwd, '.nogit', 'tsdocker_support.json');
|
||||||
|
this.data = { version: 1, entries: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads cache data from disk. Falls back to empty cache on missing/corrupt file.
|
||||||
|
*/
|
||||||
|
public load(): void {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(this.cacheFilePath, 'utf-8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (parsed && parsed.version === 1 && parsed.entries) {
|
||||||
|
this.data = parsed;
|
||||||
|
} else {
|
||||||
|
logger.log('warn', '[cache] Cache file has unexpected format, starting fresh');
|
||||||
|
this.data = { version: 1, entries: {} };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Missing or corrupt file — start fresh
|
||||||
|
this.data = { version: 1, entries: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves cache data to disk. Creates .nogit directory if needed.
|
||||||
|
*/
|
||||||
|
public save(): void {
|
||||||
|
const dir = path.dirname(this.cacheFilePath);
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes SHA-256 hash of Dockerfile content.
|
||||||
|
*/
|
||||||
|
public computeContentHash(content: string): string {
|
||||||
|
return crypto.createHash('sha256').update(content).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a build can be skipped for the given Dockerfile.
|
||||||
|
* Logs detailed diagnostics and returns true if the build should be skipped.
|
||||||
|
*/
|
||||||
|
public async shouldSkipBuild(cleanTag: string, content: string): Promise<boolean> {
|
||||||
|
const contentHash = this.computeContentHash(content);
|
||||||
|
const entry = this.data.entries[cleanTag];
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
console.log(`[cache] ${cleanTag}:`);
|
||||||
|
console.log(`[cache] Content hash: ${contentHash}`);
|
||||||
|
console.log(`[cache] Stored hash: (none)`);
|
||||||
|
console.log(`[cache] Hash match: no`);
|
||||||
|
console.log(`→ Building ${cleanTag}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashMatch = entry.contentHash === contentHash;
|
||||||
|
console.log(`[cache] ${cleanTag}:`);
|
||||||
|
console.log(`[cache] Content hash: ${contentHash}`);
|
||||||
|
console.log(`[cache] Stored hash: ${entry.contentHash}`);
|
||||||
|
console.log(`[cache] Image ID: ${entry.imageId}`);
|
||||||
|
console.log(`[cache] Hash match: ${hashMatch ? 'yes' : 'no'}`);
|
||||||
|
|
||||||
|
if (!hashMatch) {
|
||||||
|
console.log(`→ Building ${cleanTag}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash matches — verify the image still exists locally
|
||||||
|
const inspectResult = await smartshellInstance.exec(
|
||||||
|
`docker image inspect ${entry.imageId} > /dev/null 2>&1`
|
||||||
|
);
|
||||||
|
const available = inspectResult.exitCode === 0;
|
||||||
|
console.log(`[cache] Available: ${available ? 'yes' : 'no'}`);
|
||||||
|
|
||||||
|
if (available) {
|
||||||
|
console.log(`→ Skipping build for ${cleanTag} (cache hit)`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`→ Building ${cleanTag} (image no longer available)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records a successful build in the cache.
|
||||||
|
*/
|
||||||
|
public recordBuild(cleanTag: string, content: string, imageId: string, buildTag: string): void {
|
||||||
|
this.data.entries[cleanTag] = {
|
||||||
|
contentHash: this.computeContentHash(content),
|
||||||
|
imageId,
|
||||||
|
buildTag,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { logger } from './tsdocker.logging.js';
|
|||||||
import { Dockerfile } from './classes.dockerfile.js';
|
import { Dockerfile } from './classes.dockerfile.js';
|
||||||
import { DockerRegistry } from './classes.dockerregistry.js';
|
import { DockerRegistry } from './classes.dockerregistry.js';
|
||||||
import { RegistryStorage } from './classes.registrystorage.js';
|
import { RegistryStorage } from './classes.registrystorage.js';
|
||||||
|
import { TsDockerCache } from './classes.tsdockercache.js';
|
||||||
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
|
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
|
||||||
|
|
||||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||||
@@ -136,11 +137,54 @@ export class TsDockerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `Building ${toBuild.length} Dockerfiles...`);
|
logger.log('info', `Building ${toBuild.length} Dockerfiles...`);
|
||||||
await Dockerfile.buildDockerfiles(toBuild, {
|
|
||||||
platform: options?.platform,
|
if (options?.cached) {
|
||||||
timeout: options?.timeout,
|
// === CACHED MODE: skip builds for unchanged Dockerfiles ===
|
||||||
noCache: options?.noCache,
|
logger.log('info', '=== CACHED MODE ACTIVE ===');
|
||||||
});
|
const cache = new TsDockerCache();
|
||||||
|
cache.load();
|
||||||
|
|
||||||
|
for (const dockerfileArg of toBuild) {
|
||||||
|
const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
|
||||||
|
if (skip) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache miss — build this Dockerfile
|
||||||
|
await dockerfileArg.build({
|
||||||
|
platform: options?.platform,
|
||||||
|
timeout: options?.timeout,
|
||||||
|
noCache: options?.noCache,
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageId = await dockerfileArg.getId();
|
||||||
|
cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform dependency tagging for all Dockerfiles (even cache hits, since tags may be stale)
|
||||||
|
for (const dockerfileArg of toBuild) {
|
||||||
|
const dependentBaseImages = new Set<string>();
|
||||||
|
for (const other of toBuild) {
|
||||||
|
if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
|
||||||
|
dependentBaseImages.add(other.baseImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fullTag of dependentBaseImages) {
|
||||||
|
logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
|
||||||
|
await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.save();
|
||||||
|
} else {
|
||||||
|
// === STANDARD MODE: build all via static helper ===
|
||||||
|
await Dockerfile.buildDockerfiles(toBuild, {
|
||||||
|
platform: options?.platform,
|
||||||
|
timeout: options?.timeout,
|
||||||
|
noCache: options?.noCache,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.log('success', 'All Dockerfiles built successfully');
|
logger.log('success', 'All Dockerfiles built successfully');
|
||||||
|
|
||||||
return toBuild;
|
return toBuild;
|
||||||
|
|||||||
@@ -77,4 +77,17 @@ 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)
|
||||||
|
cached?: boolean; // Skip builds when Dockerfile content hasn't changed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICacheEntry {
|
||||||
|
contentHash: string; // SHA-256 hex of Dockerfile content
|
||||||
|
imageId: string; // Docker image ID (sha256:...)
|
||||||
|
buildTag: string;
|
||||||
|
timestamp: number; // Unix ms
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICacheData {
|
||||||
|
version: 1;
|
||||||
|
entries: { [cleanTag: string]: ICacheEntry };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ export let run = () => {
|
|||||||
if (argvArg.cache === false) {
|
if (argvArg.cache === false) {
|
||||||
buildOptions.noCache = true;
|
buildOptions.noCache = true;
|
||||||
}
|
}
|
||||||
|
if (argvArg.cached) {
|
||||||
|
buildOptions.cached = true;
|
||||||
|
}
|
||||||
|
|
||||||
await manager.build(buildOptions);
|
await manager.build(buildOptions);
|
||||||
logger.log('success', 'Build completed successfully');
|
logger.log('success', 'Build completed successfully');
|
||||||
@@ -142,6 +145,9 @@ export let run = () => {
|
|||||||
if (argvArg.cache === false) {
|
if (argvArg.cache === false) {
|
||||||
buildOptions.noCache = true;
|
buildOptions.noCache = true;
|
||||||
}
|
}
|
||||||
|
if (argvArg.cached) {
|
||||||
|
buildOptions.cached = true;
|
||||||
|
}
|
||||||
await manager.build(buildOptions);
|
await manager.build(buildOptions);
|
||||||
|
|
||||||
// Run tests
|
// Run tests
|
||||||
|
|||||||
Reference in New Issue
Block a user