Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Last-known-good update state survives watch errors** ([#543](https://github.com/CodesWhat/drydock/issues/543)). A failed watch cycle β€” registry timeout, rate limit, transient network error β€” no longer erases the previous successful comparison. The stored `result`, `updateAvailable`, `updateKind`, and current release notes are preserved alongside the recorded error until a later cycle succeeds, so the UI keeps showing the last known update state with the error annotated next to it instead of a blank.

- Portwing WS Welcome frames now report the actual server version β€” the endpoint had hard-coded `1.5.0` since v1.5.0, so v1.5.1/v1.5.2/v1.6.0-rc.1 servers all identified as `1.5.0` to agents ([#550](https://github.com/CodesWhat/drydock/pull/550) review finding)

## [1.6.0-rc.1] β€” 2026-07-15

### Added
Expand Down
38 changes: 38 additions & 0 deletions app/api/portwing-ws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { createHash, sign as cryptoSign, generateKeyPairSync } from 'node:crypto';
import type { IncomingMessage } from 'node:http';
import type { Socket } from 'node:net';
import { getVersion } from '../configuration/index.js';
import type { AgentKeyRecord } from '../store/agent-keys.js';
import type { NameBindingRecord } from '../store/name-bindings.js';
import * as nameBindingsStore from '../store/name-bindings.js';
Expand All @@ -24,6 +25,7 @@ import {

vi.mock('../configuration/index.js', () => ({
getServerConfiguration: vi.fn(() => ({})),
getVersion: vi.fn(() => '9.9.9-test'),
}));

// Hoisted so tests can assert on the durable-flush call (Fix 1: new-bind
Expand Down Expand Up @@ -319,6 +321,42 @@ function buildHello(

// ---- Tests ----

test('reports the canonical configuration version in the welcome frame', async () => {
clearNonceCacheForTesting();

const { privateKey, pubkeyBase64, keyId } = generateKeyPair();
const ts = Math.floor(Date.now() / 1000);
const nonce = 'd2b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6';
const sig = signHello(privateKey, ts, nonce);
const record: AgentKeyRecord = {
keyId,
pubkey: pubkeyBase64,
label: 'test',
createdAt: new Date().toISOString(),
revokedAt: null,
};

const { gateway, getUpgradedWs } = createGateway(record);
gateway.handleUpgrade(
createRequest('/api/portwing/ws'),
createMockSocket() as unknown as Socket,
Buffer.alloc(0),
);
const ws = getUpgradedWs()!;

sendMessageToGateway(ws, buildHello(keyId, ts, nonce, sig));
// Bounded wait instead of a fixed sleep β€” the async Ed25519 verification
// makes frame timing scheduler-dependent under CI load.
await vi.waitFor(() => {
expect(ws.sentMessages.length).toBeGreaterThan(0);
});

const welcome = JSON.parse(ws.sentMessages[0]) as {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
data: { config: { drydockVersion: string } };
};
expect(welcome.data.config.drydockVersion).toBe(getVersion());
});

describe('PORTWING_WS_ROUTE_PATTERN', () => {
test('matches canonical /api/portwing/ws', () => {
expect(PORTWING_WS_ROUTE_PATTERN.test('/api/portwing/ws')).toBe(true);
Expand Down
10 changes: 5 additions & 5 deletions app/api/portwing-ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
type WebSocketLike,
} from '../agent/EdgeAgentAdapter.js';
import { getAgent } from '../agent/manager.js';
import { getServerConfiguration } from '../configuration/index.js';
import { getServerConfiguration, getVersion } from '../configuration/index.js';
import logger from '../log/index.js';
import * as agentKeys from '../store/agent-keys.js';
import { save as saveStore } from '../store/index.js';
Expand Down Expand Up @@ -334,15 +334,15 @@ function sendErrorAndClose(
ws.close(closeCode, code);
}

// Package version sourced at module init time from the app package.json.
// This is the server-side version operators can verify externally.
// Server version reported in the welcome frame β€” the canonical getVersion()
// (DD_VERSION override, else package.json), cached after first read.
let _drydockVersion: string | undefined;
function drydockVersion(): string {
if (_drydockVersion !== undefined) {
return _drydockVersion;
}
// Populated by injectDrydockVersionForTesting() in tests, or from package.json at runtime.
_drydockVersion = '1.5.0';
// Populated by injectDrydockVersionForTesting() in tests, or from getVersion() at runtime.
_drydockVersion = getVersion();
return _drydockVersion;
}

Expand Down