optional Webhook column on the stacks list with a git stack's webhook id and copy-URL button

This commit is contained in:
Jarek Krochmalski committed 2026-08-27 07:34:37 +02:00
1 parent b04bc504f9
commit e82b1aea89
4 files changed
+91 -12

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
/**
* Build the `.env.dockhand` override-file contents for a git stack.
*
* A git stack's compose commonly references `env_file: [.env.dockhand]` (the
* documented way to consume Dockhand-managed non-secret vars). The file must exist
* for `docker compose up` to parse - even when there are ZERO panel vars, in which
* case it is just the header line (#1336). Pure: no FS, no DB, so both the local
* (writeFileSync) and Hawser (in-memory stackFiles) deploy paths share one source of
* truth and are unit-testable.
*/
const HEADER = '# Auto-generated by Dockhand. Do not edit - changes will be overwritten on next deploy.\n';
export function buildDockhandOverrideFile(envVars: Record<string, string>): string {
const lines = Object.entries(envVars).map(([k, v]) => `${k}=${v}`);
return HEADER + lines.join('\n') + (lines.length ? '\n' : '');
}
+12 -12
View File
@@ -51,6 +51,7 @@ import { cleanPem } from '$lib/utils/pem';
import { rewriteComposeVolumePaths, getHostDataDir } from './host-path';
import { getOrderValue } from './container-labels';
import { pendingRowsToClear } from './pending-updates-core';
import { buildDockhandOverrideFile } from './dockhand-override-file';
// =============================================================================
// TYPES
@@ -1247,17 +1248,17 @@ async function executeLocalCompose(
args.push('--env-file', customEnvPath);
}
// For git stacks: write non-secret overrides to .env.dockhand and add as second --env-file
// For git stacks: write non-secret overrides to .env.dockhand and add as second --env-file.
// Docker Compose applies env files in order, so later files override earlier ones.
// This lets the repo's .env provide defaults while our overrides take precedence.
// Secrets are still injected via shell env only (never written to disk).
// Only written when useOverrideFile is true (git stacks). Internal/adopted stacks
// already have their non-secrets in the .env file written by the UI.
// ONLY when there ARE overrides: any --env-file on the CLI suppresses Compose's
// auto-discovery of the adjacent default .env, so writing one for a zero-var stack
// would blank a subdir compose's own .env interpolation (#1136). Internal/adopted
// stacks already have their non-secrets in the .env file written by the UI.
if (useOverrideFile && envVars && Object.keys(envVars).length > 0) {
const overrideEnvPath = join(composeFileDir, '.env.dockhand');
const header = '# Auto-generated by Dockhand. Do not edit - changes will be overwritten on next deploy.\n';
const lines = Object.entries(envVars).map(([k, v]) => `${k}=${v}`);
writeFileSync(overrideEnvPath, header + lines.join('\n') + '\n');
writeFileSync(overrideEnvPath, buildDockhandOverrideFile(envVars));
args.push('--env-file', overrideEnvPath);
}
@@ -1705,13 +1706,12 @@ async function executeComposeCommand(
}
}
// For git stacks: generate .env.dockhand with non-secret DB overrides
// This mirrors executeLocalCompose behavior (lines 1017-1023).
// envVars contains only the DB overrides (not merged repo .env values from hawserEnvVars).
// For git stacks: generate .env.dockhand with non-secret DB overrides.
// ONLY when there ARE overrides: the agent adds it as --env-file, which
// suppresses Compose's adjacent-.env auto-discovery, so an empty file would
// blank a subdir compose's own .env interpolation (#1136).
if (useOverrideFile && envVars && Object.keys(envVars).length > 0) {
const header = '# Auto-generated by Dockhand. Do not edit - changes will be overwritten on next deploy.\n';
const lines = Object.entries(envVars).map(([k, v]) => `${k}=${v}`);
hawserStackFiles = { ...(hawserStackFiles || {}), '.env.dockhand': header + lines.join('\n') + '\n' };
hawserStackFiles = { ...(hawserStackFiles || {}), '.env.dockhand': buildDockhandOverrideFile(envVars) };
console.log(`[Stack:${stackName}] Including .env.dockhand override file for Hawser (${Object.keys(envVars).length} vars)`);
}
+33
View File
@@ -0,0 +1,33 @@
/**
* buildDockhandOverrideFile: the .env.dockhand contents shared by both git-stack
* deploy paths (local writeFileSync + Hawser stackFiles). A zero-var git stack must
* still produce a (header-only) file so a compose referencing env_file: .env.dockhand
* can resolve it (#1336).
*/
import { describe, it, expect } from 'bun:test';
import { buildDockhandOverrideFile } from '../src/lib/server/dockhand-override-file';
describe('buildDockhandOverrideFile', () => {
it('returns a header-only file (no trailing blank line) for ZERO vars', () => {
const out = buildDockhandOverrideFile({});
expect(out).toBe(
'# Auto-generated by Dockhand. Do not edit - changes will be overwritten on next deploy.\n'
);
// exactly one trailing newline - no empty value line that could clear a repo var
expect(out.endsWith('\n')).toBe(true);
expect(out.endsWith('\n\n')).toBe(false);
});
it('writes KEY=value lines under the header', () => {
const out = buildDockhandOverrideFile({ A: '1', B: 'two' });
expect(out).toContain('A=1');
expect(out).toContain('B=two');
expect(out.startsWith('# Auto-generated')).toBe(true);
expect(out.endsWith('\n')).toBe(true);
});
it('keeps values verbatim (including = and spaces)', () => {
const out = buildDockhandOverrideFile({ URL: 'https://x/y?a=b c' });
expect(out).toContain('URL=https://x/y?a=b c');
});
});
@@ -0,0 +1,29 @@
/**
* A column with defaultVisible:false is hidden by default (user can enable it in
* grid preferences); every other column stays visible. The stacks 'webhook' column
* relies on this so it does not clutter the list for users without git webhooks (#845).
*/
import { describe, it, expect } from 'bun:test';
import { getDefaultColumnPreferences, getConfigurableColumns } from '../src/lib/config/grid-columns';
describe('getDefaultColumnPreferences honors defaultVisible', () => {
it('hides a column marked defaultVisible:false', () => {
const prefs = getDefaultColumnPreferences('stacks');
const webhook = prefs.find((p) => p.id === 'webhook');
expect(webhook).toBeDefined();
expect(webhook!.visible).toBe(false);
});
it('leaves all other configurable columns visible', () => {
const prefs = getDefaultColumnPreferences('stacks');
for (const p of prefs) {
if (p.id === 'webhook') continue;
expect(p.visible).toBe(true);
}
});
it('the webhook column is configurable (user can toggle it), not fixed', () => {
const configurable = getConfigurableColumns('stacks').map((c) => c.id);
expect(configurable).toContain('webhook');
});
});