mirror of
https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git
synced 2026-11-03 01:13:24 +00:00
Compare commits
3 Commits
dependabot
...
chore/enti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a585e6be13 | ||
|
|
c5b8afde48 | ||
|
|
8e9bd478b4 |
5
.entire/.gitignore
vendored
Normal file
5
.entire/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
tmp/
|
||||
settings.local.json
|
||||
metadata/
|
||||
logs/
|
||||
redactors/local/
|
||||
9
.entire/settings.json
Normal file
9
.entire/settings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"telemetry": true,
|
||||
"checkpoints": {
|
||||
"primary": {
|
||||
"type": "git-refs"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ DSH_VERSION=dsh-v0.2.1-rc.5
|
||||
# Full upstream commit SHA that DSH_VERSION resolves to; the main image build
|
||||
# fails if the clone's HEAD differs (guards against a moved tag).
|
||||
DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1
|
||||
RELEASE_REVISION=
|
||||
RELEASE_REVISION=r6
|
||||
PNPM_VERSION=11.28.5
|
||||
# Container port, and the host port published by "make run".
|
||||
DSH_PORT=3080
|
||||
|
||||
585
.opencode/plugins/entire.ts
Normal file
585
.opencode/plugins/entire.ts
Normal file
@@ -0,0 +1,585 @@
|
||||
// Entire CLI plugin for OpenCode
|
||||
// Auto-generated by `entire enable --agent opencode`
|
||||
// Do not edit manually — changes will be overwritten on next install.
|
||||
//
|
||||
// Uses node:child_process so hooks work under both Bun (OpenCode CLI/TUI)
|
||||
// and Node (OpenCode Desktop's Electron sidecar). See #2014.
|
||||
import { spawn, spawnSync } from "node:child_process"
|
||||
import type { Plugin } from "@opencode-ai/plugin"
|
||||
|
||||
export const EntirePlugin: Plugin = async ({ directory }) => {
|
||||
// Track seen user messages to fire turn-start only once per message
|
||||
const seenUserMessages = new Set<string>()
|
||||
// Track current session ID for message events (which don't include sessionID)
|
||||
let currentSessionID: string | null = null
|
||||
// Track the model used by the most recent assistant message
|
||||
let currentModel: string | null = null
|
||||
// In-memory store for message metadata (role, tokens, etc.)
|
||||
const messageStore = new Map<string, any>()
|
||||
// One-time model-context injection captured from the turn-start hook's stdout,
|
||||
// applied on the next LLM call via experimental.chat.system.transform.
|
||||
let pendingInjection: string | null = null
|
||||
// Child (subagent) sessions. OpenCode's task tool runs each subagent as a
|
||||
// real session, so without this set a child would register as the user's
|
||||
// session: its write would take the checkpoint and the parent would log
|
||||
// "no files modified". Learned from session.* events' `parentID`, and from the child
|
||||
// ID surfaced in the parent's task part / tool.execute.after — the latter
|
||||
// two also cover a child session resumed via `task_id` from an earlier
|
||||
// process, whose own session.created predates this plugin instance. These
|
||||
// sets live for the process and are cleared only on server.instance.disposed.
|
||||
const childSessions = new Set<string>()
|
||||
// child session ID -> the top-level (user's) session it descends from. With
|
||||
// subagent_depth > 1 a child can itself call the task tool; that
|
||||
// grandchild's task is recorded on the top-level session, the only one
|
||||
// Entire tracks, keyed by its own callID like any other task.
|
||||
const rootOf = new Map<string, string>()
|
||||
// child session ID -> the session that launched it, so aborting a session
|
||||
// can end the background calls it launched.
|
||||
const parentOf = new Map<string, string>()
|
||||
// The per-call maps and sets below are keyed by callKey: the calling
|
||||
// session plus the hook key. A callID is unique only within one session
|
||||
// (some providers number calls per conversation), so a child's own task
|
||||
// call can share one with its parent's running call.
|
||||
// Call keys already announced via subagent-start (the running part update
|
||||
// repeats), so a late running update cannot announce a task again after its
|
||||
// stop. Cleared for a key when a new call starts under it (a reused callID).
|
||||
const announcedTasks = new Set<string>()
|
||||
// Call key -> Date.now() at tool.execute.before, sent as started_at. A
|
||||
// child resumed via `task_id` holds every earlier call's messages too; this
|
||||
// is what lets the CLI keep only the ones this call produced. The hook key
|
||||
// is the part's callID, except for a command subtask (`subtask: true`),
|
||||
// whose before/after hooks carry the task part's own id instead.
|
||||
const taskStartedAt = new Map<string, number>()
|
||||
// Announced tasks not yet stopped: call key -> its subagent-stop payload.
|
||||
// Every stop path takes the entry out, so a task stops at most once.
|
||||
const liveTasks = new Map<string, Record<string, unknown>>()
|
||||
// Call key of a task part id -> call key of its callID, to map a command
|
||||
// subtask's hook key to the callID its subagent-start used.
|
||||
const partCallID = new Map<string, string>()
|
||||
// Call keys whose stop fired before their running part was announced. The
|
||||
// late announcement is then skipped rather than opening a task that would
|
||||
// never stop.
|
||||
const stoppedBeforeStart = new Set<string>()
|
||||
// Call keys of tasks stopped from their error part, so a
|
||||
// tool.execute.after that still arrives does not stop them again.
|
||||
const stoppedOnError = new Set<string>()
|
||||
// Background children (OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS, or a
|
||||
// foreground task promoted to the background): child ID -> subagent-stop
|
||||
// payloads held back until the child's own session goes idle. A queue: a
|
||||
// resume via `task_id` that joins a running child is queued behind the
|
||||
// current run and runs as its own busy -> idle cycle, so each idle ends the
|
||||
// oldest held call.
|
||||
const backgroundTasks = new Map<string, Record<string, unknown>[]>()
|
||||
// Sessions whose last session.status was not idle. An idle only ends a
|
||||
// held call when it follows a busy period: an errored run reports idle
|
||||
// twice.
|
||||
const busySessions = new Set<string>()
|
||||
// Children whose held stops all end at their next idle: an abort reached
|
||||
// them mid-run. Cancelling a job does not cancel a joined run already
|
||||
// executing, so the child's own idle, not the abort, is when it ends.
|
||||
const drainAtIdle = new Set<string>()
|
||||
|
||||
/**
|
||||
* Build the shell command for a hook invocation.
|
||||
* Uses sh -c so the command is guarded by a `command -v` probe: when the
|
||||
* entire binary is not on PATH the hook exits 0 rather than failing the
|
||||
* surrounding OpenCode operation.
|
||||
*/
|
||||
function hookCmd(hookName: string): string[] {
|
||||
return ["sh", "-c", `if ! command -v entire >/dev/null 2>&1; then exit 0; fi; exec entire hooks opencode ${hookName}`]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipe JSON payload to an entire hooks command (async).
|
||||
* Errors are logged but never thrown — plugin failures must not crash OpenCode.
|
||||
*/
|
||||
async function callHook(hookName: string, payload: Record<string, unknown>) {
|
||||
try {
|
||||
const json = JSON.stringify(payload)
|
||||
const [cmd, ...args] = hookCmd(hookName)
|
||||
await new Promise<void>((resolve) => {
|
||||
const proc = spawn(cmd, args, {
|
||||
cwd: directory,
|
||||
stdio: ["pipe", "ignore", "ignore"],
|
||||
})
|
||||
proc.on("error", () => resolve())
|
||||
proc.on("close", () => resolve())
|
||||
proc.stdin?.end(json + "\n")
|
||||
})
|
||||
} catch {
|
||||
// Silently ignore — plugin failures must not crash OpenCode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous variant for hooks that must complete before subsequent agent work
|
||||
* or process exit. `turn-start` must finish initializing session state before a
|
||||
* fast mid-turn commit can hit git hooks, and `turn-end` / `session-end` must
|
||||
* finish before `opencode run` tears down its event loop.
|
||||
*/
|
||||
function callHookSync(hookName: string, payload: Record<string, unknown>) {
|
||||
try {
|
||||
const json = JSON.stringify(payload)
|
||||
const [cmd, ...args] = hookCmd(hookName)
|
||||
spawnSync(cmd, args, {
|
||||
cwd: directory,
|
||||
input: json + "\n",
|
||||
stdio: ["pipe", "ignore", "ignore"],
|
||||
})
|
||||
} catch {
|
||||
// Silently ignore — plugin failures must not crash OpenCode
|
||||
}
|
||||
}
|
||||
|
||||
// parseInjectedContext scans a hook's stdout for Entire's injection envelope
|
||||
// ({"inject_context":"..."}) and returns the text to inject, or null.
|
||||
function parseInjectedContext(stdout: string): string | null {
|
||||
if (!stdout) return null
|
||||
for (const line of stdout.split("\n")) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed.startsWith("{")) continue
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as { inject_context?: unknown }
|
||||
if (typeof parsed.inject_context === "string" && parsed.inject_context.length > 0) {
|
||||
return parsed.inject_context
|
||||
}
|
||||
} catch {
|
||||
// not our envelope — ignore
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// fireTurnStart fires turn-start synchronously (state must be ready before
|
||||
// mid-turn commits) and stashes any model-context injection emitted on stdout
|
||||
// for experimental.chat.system.transform to apply. Entire emits the injection
|
||||
// at most once per session, so pendingInjection is set on the first turn only.
|
||||
function fireTurnStart(payload: Record<string, unknown>) {
|
||||
try {
|
||||
const json = JSON.stringify(payload)
|
||||
const [cmd, ...args] = hookCmd("turn-start")
|
||||
const proc = spawnSync(cmd, args, {
|
||||
cwd: directory,
|
||||
input: json + "\n",
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "ignore"],
|
||||
})
|
||||
const out = typeof proc.stdout === "string" ? proc.stdout : ""
|
||||
const injected = parseInjectedContext(out)
|
||||
if (injected) pendingInjection = injected
|
||||
} catch {
|
||||
// Silently ignore — plugin failures must not crash OpenCode
|
||||
}
|
||||
}
|
||||
|
||||
// trackChild records a child and the top-level session it descends from.
|
||||
function trackChild(childID: string, parentID: string) {
|
||||
childSessions.add(childID)
|
||||
if (!rootOf.has(childID)) rootOf.set(childID, rootOf.get(parentID) ?? parentID)
|
||||
if (!parentOf.has(childID)) parentOf.set(childID, parentID)
|
||||
}
|
||||
|
||||
// callKey scopes a hook key (a callID or a task part id) to the session
|
||||
// that made the call.
|
||||
function callKey(sessionID: string, hookKey: string): string {
|
||||
return sessionID + "\0" + hookKey
|
||||
}
|
||||
|
||||
// topLevelSession maps a session to the top-level session it belongs to.
|
||||
function topLevelSession(sessionID: string): string {
|
||||
return rootOf.get(sessionID) ?? sessionID
|
||||
}
|
||||
|
||||
// announceTask fires subagent-start the first time a task part is running
|
||||
// with its child bound (state.metadata.sessionId). tool.execute.before fires
|
||||
// earlier but has no child ID yet, and session.created for the child can
|
||||
// interleave with a sibling's, so neither is a safe join. The running update
|
||||
// repeats, hence once per call key. Called for task parts in child sessions
|
||||
// too, so a nested subagent is announced on the top-level session.
|
||||
function announceTask(part: any) {
|
||||
if (!(part?.type === "tool" && part.tool === "task" && part.callID &&
|
||||
part.state?.status === "running" && part.state?.metadata?.sessionId)) return
|
||||
const sessionID = part.sessionID ?? currentSessionID
|
||||
if (!sessionID) return
|
||||
const key = callKey(sessionID, part.callID)
|
||||
const partKey = callKey(sessionID, part.id)
|
||||
if (announcedTasks.has(key)) return
|
||||
announcedTasks.add(key)
|
||||
trackChild(part.state.metadata.sessionId, sessionID)
|
||||
// Without the before hook (the plugin loaded mid-call), the part's own
|
||||
// start, OpenCode's clock before the child's prompt, still bounds it.
|
||||
const startedAt = taskStartedAt.get(key) ?? taskStartedAt.get(partKey) ?? part.state?.time?.start ?? 0
|
||||
if (stoppedBeforeStart.delete(key) || stoppedBeforeStart.delete(partKey)) {
|
||||
forgetTaskKeys(part, sessionID)
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
session_id: topLevelSession(sessionID),
|
||||
tool_use_id: part.callID,
|
||||
subagent_id: part.state.metadata.sessionId,
|
||||
subagent_type: part.state?.input?.subagent_type ?? "",
|
||||
task_description: part.state?.input?.description ?? "",
|
||||
started_at: startedAt,
|
||||
}
|
||||
liveTasks.set(key, payload)
|
||||
if (part.id) partCallID.set(partKey, key)
|
||||
callHookSync("subagent-start", payload)
|
||||
}
|
||||
|
||||
// announcedKey maps a call key to the call key the task was announced under.
|
||||
function announcedKey(key: string): string {
|
||||
return partCallID.get(key) ?? key
|
||||
}
|
||||
|
||||
// takeLiveTask removes and returns the announced, not yet stopped task a
|
||||
// call key belongs to.
|
||||
function takeLiveTask(key: string): Record<string, unknown> | undefined {
|
||||
const announced = announcedKey(key)
|
||||
partCallID.delete(key)
|
||||
const payload = liveTasks.get(announced)
|
||||
liveTasks.delete(announced)
|
||||
return payload
|
||||
}
|
||||
|
||||
function forgetTaskKeys(part: any, sessionID: string) {
|
||||
taskStartedAt.delete(callKey(sessionID, part.callID))
|
||||
if (part.id) taskStartedAt.delete(callKey(sessionID, part.id))
|
||||
}
|
||||
|
||||
// stopFailedTask fires subagent-stop for an announced task whose part
|
||||
// ended in error without tool.execute.after: OpenCode skips that hook when
|
||||
// the call is aborted (Esc, `opencode run` teardown) or its execute throws.
|
||||
// A task that failed before its child was bound was never announced, so
|
||||
// nothing fires for it.
|
||||
function stopFailedTask(part: any) {
|
||||
if (!(part?.type === "tool" && part.tool === "task" && part.callID &&
|
||||
part.state?.status === "error")) return
|
||||
const sessionID = part.sessionID ?? currentSessionID ?? ""
|
||||
const key = callKey(sessionID, part.callID)
|
||||
const partKey = callKey(sessionID, part.id)
|
||||
const payload = takeLiveTask(key)
|
||||
forgetTaskKeys(part, sessionID)
|
||||
stoppedBeforeStart.delete(key)
|
||||
if (part.id) {
|
||||
partCallID.delete(partKey)
|
||||
stoppedBeforeStart.delete(partKey)
|
||||
}
|
||||
if (!payload) return
|
||||
stoppedOnError.add(key)
|
||||
if (part.id) stoppedOnError.add(partKey)
|
||||
callHookSync("subagent-stop", payload)
|
||||
}
|
||||
|
||||
// finishBackgroundTask fires the oldest held subagent-stop for a background
|
||||
// child whose run went idle: its work finished, failed, or was aborted
|
||||
// (`opencode run` exiting with the child still running). Each case ends the
|
||||
// call; a later call queued on the same child ends at its own run's idle.
|
||||
function finishBackgroundTask(childID: string) {
|
||||
const payloads = backgroundTasks.get(childID)
|
||||
const payload = payloads?.shift()
|
||||
if (!payload) return
|
||||
if (payloads.length === 0) backgroundTasks.delete(childID)
|
||||
callHookSync("subagent-stop", payload)
|
||||
}
|
||||
|
||||
// drainBackgroundTasks fires every held subagent-stop for a child whose
|
||||
// background job is over. A job that fails or is cancelled drops the runs
|
||||
// still queued on it, so their calls get no idle of their own; left held,
|
||||
// the next resume of the child would end them at its idle instead.
|
||||
function drainBackgroundTasks(childID: string) {
|
||||
const payloads = backgroundTasks.get(childID)
|
||||
if (!payloads) return
|
||||
backgroundTasks.delete(childID)
|
||||
for (const payload of payloads) callHookSync("subagent-stop", payload)
|
||||
}
|
||||
|
||||
// A background job's end reaches the session that launched it as a
|
||||
// synthetic text part: <task id="<child>" state="completed|error">.
|
||||
const backgroundResult = /^<task id="([^"]+)" state="(completed|error)">/
|
||||
|
||||
// backgroundSessionAborted ends the held calls of an aborted session and of
|
||||
// the children it launched (Esc cancels its background jobs and reports no
|
||||
// result): at once for an idle child, at its next idle for a busy one.
|
||||
function backgroundSessionAborted(sessionID: string) {
|
||||
const targets = [sessionID]
|
||||
for (const [childID, parentID] of parentOf) if (parentID === sessionID) targets.push(childID)
|
||||
for (const target of targets) {
|
||||
if (!backgroundTasks.has(target)) continue
|
||||
if (busySessions.has(target)) drainAtIdle.add(target)
|
||||
else drainBackgroundTasks(target)
|
||||
}
|
||||
}
|
||||
|
||||
function resetSessionTracking(sessionID: string) {
|
||||
if (currentSessionID === sessionID) {
|
||||
return false
|
||||
}
|
||||
seenUserMessages.clear()
|
||||
messageStore.clear()
|
||||
currentModel = null
|
||||
currentSessionID = sessionID
|
||||
// Drop any turn-start injection captured for the prior session so it
|
||||
// can't leak into the new session's system prompt.
|
||||
pendingInjection = null
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
// Apply the one-time Entire context injection captured at turn-start by
|
||||
// appending it to the system prompt for this LLM call.
|
||||
"experimental.chat.system.transform": async (input: { sessionID?: string }, output: { system: string[] }) => {
|
||||
if (input?.sessionID && childSessions.has(input.sessionID)) return
|
||||
if (pendingInjection && Array.isArray(output.system)) {
|
||||
output.system.push(pendingInjection)
|
||||
pendingInjection = null
|
||||
}
|
||||
},
|
||||
// Subagent call start. No child ID exists yet, so this only records the
|
||||
// time; subagent-start fires from the running task part below.
|
||||
"tool.execute.before": async (input) => {
|
||||
try {
|
||||
if (input.tool !== "task") return
|
||||
const key = callKey(input.sessionID, input.callID)
|
||||
// A new call under this key: some providers number calls per
|
||||
// conversation, so an earlier call's markers must not dedupe or
|
||||
// swallow this one's start and stop.
|
||||
announcedTasks.delete(key)
|
||||
stoppedBeforeStart.delete(key)
|
||||
stoppedOnError.delete(key)
|
||||
taskStartedAt.set(key, Date.now())
|
||||
} catch {
|
||||
// Silently ignore — plugin failures must not crash OpenCode
|
||||
}
|
||||
},
|
||||
// Subagent completion. tool.execute.after for the task tool fires once, with
|
||||
// the child session ID in output.metadata: at true completion for a
|
||||
// foreground task, immediately for a background one (metadata.background),
|
||||
// whose stop is held until the child goes idle. Synchronous: `opencode run`
|
||||
// exits on the parent's idle right after this, and an async hook would be
|
||||
// killed before it finished.
|
||||
"tool.execute.after": async (input, output) => {
|
||||
try {
|
||||
if (input.tool !== "task") return
|
||||
const key = callKey(input.sessionID, input.callID)
|
||||
const announced = takeLiveTask(key)
|
||||
if (stoppedOnError.delete(key) && !announced) {
|
||||
// Already stopped: its part ended in error first.
|
||||
taskStartedAt.delete(key)
|
||||
return
|
||||
}
|
||||
// A failed command subtask fires this hook with no output; its
|
||||
// announced child still ends here.
|
||||
const childID = output?.metadata?.sessionId ?? announced?.subagent_id
|
||||
if (!childID) {
|
||||
taskStartedAt.delete(key)
|
||||
return
|
||||
}
|
||||
trackChild(childID, input.sessionID)
|
||||
const startedAt = taskStartedAt.get(key) ?? 0
|
||||
taskStartedAt.delete(key)
|
||||
if (!announced) stoppedBeforeStart.add(key)
|
||||
// A child's own task call (subagent_depth > 1) is recorded on the
|
||||
// top-level session: the child has no Entire session of its own.
|
||||
const payload = announced ?? {
|
||||
session_id: topLevelSession(input.sessionID),
|
||||
tool_use_id: input.callID,
|
||||
subagent_id: childID,
|
||||
subagent_type: input.args?.subagent_type ?? "",
|
||||
task_description: input.args?.description ?? "",
|
||||
started_at: startedAt,
|
||||
}
|
||||
if (output?.metadata?.background === true) {
|
||||
backgroundTasks.set(childID, [...(backgroundTasks.get(childID) ?? []), payload])
|
||||
return
|
||||
}
|
||||
callHookSync("subagent-stop", payload)
|
||||
} catch {
|
||||
// Silently ignore — plugin failures must not crash OpenCode
|
||||
}
|
||||
},
|
||||
event: async ({ event }) => {
|
||||
try {
|
||||
const props = (event as any).properties
|
||||
const info = props?.info
|
||||
if (event.type.startsWith("session.") && info?.parentID && info?.id) trackChild(info.id, info.parentID)
|
||||
if (event.type === "message.part.updated") {
|
||||
announceTask(props?.part)
|
||||
stopFailedTask(props?.part)
|
||||
const part = props?.part
|
||||
// Only a part in a tracked session: synthetic text also carries
|
||||
// attachment contents, which could spell the same envelope.
|
||||
const sid = part?.sessionID
|
||||
const tracked = sid === currentSessionID || childSessions.has(sid) || [...parentOf.values()].includes(sid)
|
||||
if (tracked && part?.type === "text" && part.synthetic === true && typeof part.text === "string") {
|
||||
const result = backgroundResult.exec(part.text)
|
||||
if (result) drainBackgroundTasks(result[1])
|
||||
}
|
||||
}
|
||||
// An aborted message, reported on session.error, or on the message
|
||||
// itself when the run was interrupted outside the processor.
|
||||
if (event.type === "session.error" && props?.error?.name === "MessageAbortedError" && props?.sessionID) {
|
||||
backgroundSessionAborted(props.sessionID)
|
||||
}
|
||||
if (event.type === "message.updated" && info?.error?.name === "MessageAbortedError" && info?.sessionID) {
|
||||
backgroundSessionAborted(info.sessionID)
|
||||
}
|
||||
if (event.type === "session.status" && props?.sessionID) {
|
||||
if (props?.status?.type !== "idle") busySessions.add(props.sessionID)
|
||||
else if (busySessions.delete(props.sessionID)) {
|
||||
if (drainAtIdle.delete(props.sessionID)) drainBackgroundTasks(props.sessionID)
|
||||
else finishBackgroundTask(props.sessionID)
|
||||
}
|
||||
}
|
||||
const eventSessionID: string | undefined =
|
||||
props?.sessionID ?? info?.sessionID ?? info?.id ?? props?.part?.sessionID
|
||||
if (eventSessionID && childSessions.has(eventSessionID)) return
|
||||
switch (event.type) {
|
||||
case "session.created": {
|
||||
const session = info
|
||||
if (!session?.id) break
|
||||
// Reset per-session tracking state when switching sessions.
|
||||
if (resetSessionTracking(session.id)) {
|
||||
await callHook("session-start", {
|
||||
session_id: session.id,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "message.updated": {
|
||||
const msg = info
|
||||
if (!msg) break
|
||||
|
||||
if (msg.sessionID && resetSessionTracking(msg.sessionID)) {
|
||||
callHookSync("session-start", {
|
||||
session_id: msg.sessionID,
|
||||
})
|
||||
}
|
||||
|
||||
// Store message metadata (role, time, tokens, etc.)
|
||||
messageStore.set(msg.id, msg)
|
||||
// Track model from assistant messages
|
||||
if (msg.role === "assistant" && msg.modelID) {
|
||||
currentModel = msg.modelID
|
||||
}
|
||||
|
||||
// Fallback: some opencode run flows commit before any message.part.updated
|
||||
// event is delivered for the user's prompt. Start the turn from the
|
||||
// user message itself so git hooks see an ACTIVE session in time.
|
||||
if (msg.role === "user" && !seenUserMessages.has(msg.id)) {
|
||||
seenUserMessages.add(msg.id)
|
||||
const sessionID = msg.sessionID ?? currentSessionID
|
||||
if (sessionID) {
|
||||
fireTurnStart({
|
||||
session_id: sessionID,
|
||||
prompt: "",
|
||||
model: currentModel ?? "",
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "message.part.updated": {
|
||||
const part = props?.part
|
||||
if (!part?.messageID) break
|
||||
|
||||
// Fire turn-start on the first text part of a new user message
|
||||
const msg = messageStore.get(part.messageID)
|
||||
if (msg?.role === "user" && part.type === "text" && !seenUserMessages.has(msg.id)) {
|
||||
seenUserMessages.add(msg.id)
|
||||
const sessionID = msg.sessionID ?? currentSessionID
|
||||
if (sessionID) {
|
||||
// A synthetic part is OpenCode writing, not the user: a
|
||||
// background task's result arrives this way. The parent does
|
||||
// take a turn on it, so the turn still starts, with no prompt.
|
||||
fireTurnStart({
|
||||
session_id: sessionID,
|
||||
prompt: part.synthetic === true ? "" : (part.text ?? ""),
|
||||
model: currentModel ?? "",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case "session.status": {
|
||||
// session.status fires in both TUI and non-interactive (run) mode.
|
||||
// session.idle is deprecated and not reliably emitted in run mode.
|
||||
if (props?.status?.type !== "idle") break
|
||||
const sessionID = props?.sessionID ?? currentSessionID
|
||||
if (!sessionID) break
|
||||
// Use sync variant: `opencode run` exits on the same idle event,
|
||||
// so an async hook would be killed before completing.
|
||||
callHookSync("turn-end", {
|
||||
session_id: sessionID,
|
||||
model: currentModel ?? "",
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "session.compacted": {
|
||||
const sessionID = props?.sessionID
|
||||
if (!sessionID) break
|
||||
await callHook("compaction", {
|
||||
session_id: sessionID,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "session.deleted": {
|
||||
const session = info
|
||||
if (!session?.id) break
|
||||
seenUserMessages.clear()
|
||||
messageStore.clear()
|
||||
currentSessionID = null
|
||||
pendingInjection = null
|
||||
// childSessions/announcedTasks are NOT cleared here: a child's own
|
||||
// deletion never reaches this case (the guard above returns first),
|
||||
// so this only ever fires for an unrelated top-level session, and
|
||||
// clearing here would drop a live child back to top-level mid-task.
|
||||
// Use sync variant: session-end may fire during shutdown.
|
||||
callHookSync("session-end", {
|
||||
session_id: session.id,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "server.instance.disposed": {
|
||||
// Fires when OpenCode shuts down (TUI close or `opencode run` exit).
|
||||
// session.deleted only fires on explicit user deletion, not on quit,
|
||||
// so this is the only reliable way to end sessions on exit.
|
||||
if (!currentSessionID) break
|
||||
const sessionID = currentSessionID
|
||||
seenUserMessages.clear()
|
||||
messageStore.clear()
|
||||
currentSessionID = null
|
||||
pendingInjection = null
|
||||
childSessions.clear()
|
||||
rootOf.clear()
|
||||
parentOf.clear()
|
||||
announcedTasks.clear()
|
||||
taskStartedAt.clear()
|
||||
liveTasks.clear()
|
||||
partCallID.clear()
|
||||
stoppedBeforeStart.clear()
|
||||
stoppedOnError.clear()
|
||||
// A background child still running here is ended by the Go side's
|
||||
// SessionEnd sweep, which completes every live task record.
|
||||
backgroundTasks.clear()
|
||||
busySessions.clear()
|
||||
drainAtIdle.clear()
|
||||
// Use sync variant: this is the last event before process exit.
|
||||
callHookSync("session-end", {
|
||||
session_id: sessionID,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — plugin failures must not crash OpenCode
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ needs kind plus the agent-sandbox CRDs (see the example README).
|
||||
## Versioning and releases
|
||||
|
||||
- Release identity = DSH_VERSION plus an optional -rN repository revision.
|
||||
The current identity is dsh-v0.2.1-rc.5 (first release at this DSH ref, so no
|
||||
The current identity is dsh-v0.2.1-rc.5-r6 (first release at this DSH ref, so no
|
||||
suffix); a repository-only change at the same ref publishes -r2.
|
||||
- Channels: release tags are immutable; latest follows the newest release;
|
||||
main is the development channel for both image packages.
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -3,6 +3,39 @@
|
||||
All notable changes to this repository are documented here.
|
||||
Generated by git-cliff from Conventional Commits; do not edit this file by hand.
|
||||
|
||||
## [dsh-v0.2.1-rc.5-r6] - 2026-10-31
|
||||
|
||||
### Added
|
||||
|
||||
- **example:** Persist the agent workspace on the PVC via subPath (#34) (367294b)
|
||||
- **example:** Start DSH's folder picker on the persisted workspace (#36) (82ee0d0)
|
||||
|
||||
### CI
|
||||
|
||||
- **workflows:** Streamline CI and remove redundant builds (#31) (dcb5b43)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Align readmes, docs and agent guidance with current behavior (#37) (24f23e0)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **gateway:** Answer client-aborted requests with 499, not a traceback (#32) (cbcc56f)
|
||||
- Ship bubblewrap for DSH's command sandbox, keep agents usable without it (#35) (a09d4ac)
|
||||
- **ci:** Harden workflow token scope and download integrity (#39) (2f959e6)
|
||||
- **gateway:** Replace token cookies with revocable sessions and harden abuse controls (#40) (1a929e3)
|
||||
|
||||
### Maintenance
|
||||
|
||||
- **changelog:** Refresh for dsh-v0.2.1-rc.5 (#30) (b57916d)
|
||||
- Bump docker/login-action (#28) (8c34f43)
|
||||
- **deps:** Bump agent-sandbox to v1.0.9 (#38) (4c073ee)
|
||||
- Improve package installation and add Python depend… (#42) (8e9bd47)
|
||||
|
||||
### Other
|
||||
|
||||
- Add continue-on-error to CHANGELOG PR step (97c5632)
|
||||
|
||||
## [dsh-v0.2.1-rc.5] - 2026-10-30
|
||||
|
||||
### Added
|
||||
|
||||
@@ -68,7 +68,7 @@ RUN cd /opt/dsh \
|
||||
FROM $NODE_RUNTIME_IMAGE AS runtime
|
||||
|
||||
ARG DSH_VERSION=dsh-v0.2.1-rc.5
|
||||
ARG RELEASE_REVISION=
|
||||
ARG RELEASE_REVISION=r6
|
||||
ARG PNPM_VERSION=11.28.5
|
||||
ARG VCS_REF=unknown
|
||||
|
||||
@@ -106,8 +106,11 @@ RUN apt-get update \
|
||||
tini \
|
||||
gh \
|
||||
bubblewrap \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
|
||||
&& apt-get autoclean -y \
|
||||
&& install -d -o node -g node -m 0755 /workspace "$DSH_HOME" \
|
||||
&& corepack enable \
|
||||
&& corepack prepare "pnpm@$PNPM_VERSION" --activate \
|
||||
@@ -160,6 +163,6 @@ WORKDIR /workspace
|
||||
USER node
|
||||
|
||||
EXPOSE 3080
|
||||
VOLUME ["/home/node/.dsh"]
|
||||
VOLUME ["/home/node/.dsh", "/workspace"]
|
||||
STOPSIGNAL SIGTERM
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/dsh-entrypoint"]
|
||||
|
||||
12
README.md
12
README.md
@@ -20,7 +20,7 @@ docker run -d --name dsh-web \
|
||||
-p 3080:3080 \
|
||||
-v dsh-home:/home/node/.dsh \
|
||||
-e DEEPSEEK_API_KEY \
|
||||
ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
|
||||
ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
|
||||
```
|
||||
|
||||
DSH Web is **not usable at a bare `/`**. Without its launch-token cookie it
|
||||
@@ -45,7 +45,7 @@ docker run -d --name dsh-web -p 18081:3080 \
|
||||
-e DSH_PUBLIC_URL=http://localhost:18081 \
|
||||
-v dsh-home:/home/node/.dsh \
|
||||
-e DEEPSEEK_API_KEY \
|
||||
ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
|
||||
ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
|
||||
# then open http://localhost:18081/?token=...
|
||||
```
|
||||
|
||||
@@ -94,20 +94,20 @@ ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway
|
||||
|
||||
| Tag | Contents | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `dsh-v0.2.1-rc.5` | DSH Web, port `3080` | **Current release identity.** Immutable: once published it never moves. |
|
||||
| `dsh-v0.2.1-rc.5-r6` | DSH Web, port `3080` | **Current release identity.** Immutable: once published it never moves. |
|
||||
| `latest` | DSH Web | Most recent release; same digest as the current identity. |
|
||||
| `main` | DSH Web | Development channel rebuilt when image-definition files change on `main`. Not a release; not for production. |
|
||||
|
||||
The example gateway is published under its nested path with the same release
|
||||
identity, e.g.
|
||||
`ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5`.
|
||||
`ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5-r6`.
|
||||
|
||||
Release identities, channels and rollback rules are explained in
|
||||
[docs/versioning.md](docs/versioning.md). Production deployments should pin a
|
||||
digest:
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
|
||||
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
|
||||
# image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox@sha256:<digest>
|
||||
```
|
||||
|
||||
@@ -188,7 +188,7 @@ required checks and releases: [docs/gitflow.md](docs/gitflow.md).
|
||||
|
||||
The release identity is the upstream DSH ref plus an optional repository
|
||||
revision: `DSH_VERSION` and `RELEASE_REVISION` in [versions.env](versions.env).
|
||||
The current identity is **`dsh-v0.2.1-rc.5`** — the first release at this
|
||||
The current identity is **`dsh-v0.2.1-rc.5-r6`** — the first release at this
|
||||
upstream DSH ref, so it carries no repository revision. Tags are immutable; a
|
||||
broken release is fixed by publishing `-r(N+1)`, never by re-tagging.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ gets a DSH Web container running before anything else.
|
||||
| [main-image.md](main-image.md) | The primary artifact: build args, runtime environment, entrypoint, probes, volumes/ports/signals, non-root contract, image names, tags and digests. |
|
||||
| [examples.md](examples.md) | The example contract: required layout, manifest rules, exposure variants, components and image naming, scripts, docs, and how to add a new example. |
|
||||
| [gitflow.md](gitflow.md) | Branches, protected `main`, squash merges, Conventional PR titles, required CI checks, release branches, hotfixes and the one-time GitHub settings checklist. |
|
||||
| [versioning.md](versioning.md) | Release identity (`DSH ref` + optional `-rN`), the current identity `dsh-v0.2.1-rc.5`, why identities are immutable, channels, compatibility matrix, bumping pins, the release checklist, rollback, git-cliff and digest pinning. |
|
||||
| [versioning.md](versioning.md) | Release identity (`DSH ref` + optional `-rN`), the current identity `dsh-v0.2.1-rc.5-r6`, why identities are immutable, channels, compatibility matrix, bumping pins, the release checklist, rollback, git-cliff and digest pinning. |
|
||||
| [workflow-security.md](workflow-security.md) | GitHub Actions security posture: least-privilege tokens, pinned actions, untrusted-input handling, download integrity checks, artifact hygiene, the latest audit record and residual repository-setting recommendations. |
|
||||
|
||||
## Project skills
|
||||
|
||||
@@ -39,12 +39,12 @@ To reproduce the equivalent by hand:
|
||||
|
||||
```bash
|
||||
docker build \
|
||||
--build-arg DSH_VERSION=dsh-v0.2.1-rc.5 \
|
||||
--build-arg DSH_VERSION=dsh-v0.2.1-rc.5-r6 \
|
||||
--build-arg DSH_COMMIT=b830ec950faba0a46dda564be3e2d854afed94f1 \
|
||||
--build-arg RELEASE_REVISION= \
|
||||
--build-arg PNPM_VERSION=11.28.5 \
|
||||
--build-arg VCS_REF="$(git rev-parse --short HEAD)" \
|
||||
-t ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5 .
|
||||
-t ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6 .
|
||||
```
|
||||
|
||||
### Build args
|
||||
@@ -209,7 +209,7 @@ GitHub Container Registry:
|
||||
|
||||
| Tag | Meaning |
|
||||
| --- | --- |
|
||||
| `dsh-v0.2.1-rc.5` | Current release identity. **Immutable**: once published it never moves. |
|
||||
| `dsh-v0.2.1-rc.5-r6` | Current release identity. **Immutable**: once published it never moves. |
|
||||
| `latest` | Most recent release; points at the same digest as the current identity. |
|
||||
| `main` | Development channel rebuilt when image-definition files change on `main`; not a release, not for production. |
|
||||
|
||||
@@ -218,7 +218,7 @@ Release identities, channels and rollback rules are defined in
|
||||
pin a digest:
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
|
||||
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
|
||||
# ... pin the digest in your deployment manifests:
|
||||
# image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox@sha256:<digest>
|
||||
```
|
||||
@@ -226,7 +226,7 @@ docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbo
|
||||
## Verifying an image
|
||||
|
||||
```bash
|
||||
docker inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5 \
|
||||
docker inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6 \
|
||||
--format '{{json .Config.Labels}}'
|
||||
# org.opencontainers.image.title / .version / .source
|
||||
# io.sredevopsorg.dsh.version / io.sredevopsorg.release.revision
|
||||
|
||||
@@ -15,12 +15,12 @@ release identity = DSH_VERSION + optional "-" + RELEASE_REVISION
|
||||
|
||||
| Piece | Meaning |
|
||||
| --- | --- |
|
||||
| `DSH_VERSION` | The upstream DeepSeek Harness git ref being packaged, e.g. `dsh-v0.2.1-rc.5`. It mirrors upstream's own tag. |
|
||||
| `DSH_VERSION` | The upstream DeepSeek Harness git ref being packaged, e.g. `dsh-v0.2.1-rc.5-r6`. It mirrors upstream's own tag. |
|
||||
| `RELEASE_REVISION` | An optional repository revision at that DSH ref: empty for the first release, `rN` (`N >= 2`) for a repository-only re-release. |
|
||||
|
||||
Examples:
|
||||
|
||||
- `DSH_VERSION=dsh-v0.2.1-rc.5`, `RELEASE_REVISION=` → identity `dsh-v0.2.1-rc.5`
|
||||
- `DSH_VERSION=dsh-v0.2.1-rc.5-r6`, `RELEASE_REVISION=` → identity `dsh-v0.2.1-rc.5-r6`
|
||||
(first release at that upstream ref).
|
||||
- `DSH_VERSION=<ref>`, `RELEASE_REVISION=rN` → identity `<ref>-rN` (for example
|
||||
documentation, CI or gateway changes on top of an already-released ref; no DSH
|
||||
@@ -36,7 +36,7 @@ with an empty revision); a repository-only change at the same DSH ref increments
|
||||
change what an existing deployment runs. A fix therefore publishes a new identity;
|
||||
see the rules above.
|
||||
|
||||
The current identity is `dsh-v0.2.1-rc.5`, the first release at this upstream
|
||||
The current identity is `dsh-v0.2.1-rc.5-r6`, the first release at this upstream
|
||||
ref, so it carries no revision suffix. The identity is stored in
|
||||
[versions.env](../versions.env) and consumed everywhere: the Dockerfile labels
|
||||
(version and revision), the example manifests, the Makefile and the release
|
||||
@@ -46,7 +46,7 @@ workflow.
|
||||
|
||||
| Channel | Image tag | Meaning |
|
||||
| --- | --- | --- |
|
||||
| Release | `dsh-v0.2.1-rc.5` | The current immutable release identity. What production should pin (preferably by digest). |
|
||||
| Release | `dsh-v0.2.1-rc.5-r6` | The current immutable release identity. What production should pin (preferably by digest). |
|
||||
| `latest` | `latest` | Alias for the most recent release; moves only when a new release is published. |
|
||||
| `main` | `main` | Development channel, rebuilt when image or gateway sources change on `main`. Never treat it as a release; it exists so `main` can be tested. |
|
||||
|
||||
@@ -60,7 +60,7 @@ Values from [versions.env](../versions.env) at the current identity:
|
||||
| Component | Pin | Source |
|
||||
| --- | --- | --- |
|
||||
| Upstream DSH source | `DSH_REPO` | [versions.env](../versions.env) |
|
||||
| DSH ref | `dsh-v0.2.1-rc.5` | `DSH_VERSION` |
|
||||
| DSH ref | `dsh-v0.2.1-rc.5-r6` | `DSH_VERSION` |
|
||||
| DSH commit | `b830ec950faba0a46dda564be3e2d854afed94f1` | `DSH_COMMIT`; the main image build fails unless the clone's `HEAD` matches |
|
||||
| Repository revision | `(empty)` | `RELEASE_REVISION` |
|
||||
| pnpm | `11.28.5` | `PNPM_VERSION` |
|
||||
@@ -176,7 +176,7 @@ Release identities are immutable in theory; deployments should still pin the
|
||||
_resolved_ digest for reproducibility and supply-chain safety:
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
|
||||
docker buildx imagetools inspect ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
|
||||
# then, in the deployment:
|
||||
# image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox@sha256:<digest>
|
||||
```
|
||||
|
||||
@@ -270,7 +270,7 @@ kubectl delete -f manifests/40-claim-rejected.yaml
|
||||
```bash
|
||||
# Published image (on kind, load it into the cluster; on a real cluster the
|
||||
# kubelet pulls it from GHCR):
|
||||
GW=ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5
|
||||
GW=ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5-r6
|
||||
docker pull "$GW"
|
||||
kind load docker-image "$GW" --name <kind-cluster>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ It does four things:
|
||||
The DSH version pin lives in [`versions.env`](../../../versions.env)
|
||||
(`DSH_VERSION` plus the optional repository `RELEASE_REVISION`); the example
|
||||
manifests and published images use the release identity built from it (currently
|
||||
`dsh-v0.2.1-rc.5`). DSH binds `:3080`, gates the UI and `/api` behind a
|
||||
`dsh-v0.2.1-rc.5-r6`). DSH binds `:3080`, gates the UI and `/api` behind a
|
||||
`dsh-auth-*` cookie and requires a WebSocket at `/api/remote.mux`.
|
||||
|
||||
## Flow
|
||||
|
||||
@@ -73,7 +73,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: dsh-web
|
||||
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
|
||||
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: web
|
||||
|
||||
@@ -101,7 +101,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: gateway
|
||||
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5
|
||||
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox/dsh-agents-as-a-service/gateway:dsh-v0.2.1-rc.5-r6
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
@@ -56,7 +56,7 @@ spec:
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: dsh-web
|
||||
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5
|
||||
image: ghcr.io/sredevopsorg/deepseek-kubernetes-sandbox:dsh-v0.2.1-rc.5-r6
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: web
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# "make check".
|
||||
#
|
||||
# ./scripts/set-version.sh dsh-v0.2.2-rc.1 # new DSH ref, no revision
|
||||
# ./scripts/set-version.sh dsh-v0.2.1-rc.5 r3 # same ref, new repo revision
|
||||
# ./scripts/set-version.sh dsh-v0.2.1-rc.5-r6 r3 # same ref, new repo revision
|
||||
#
|
||||
# For a new DSH ref the script resolves the tag's commit with "git ls-remote"
|
||||
# (peeled ^{} commit first, tag ref as fallback) and rewrites DSH_COMMIT in
|
||||
|
||||
@@ -18,9 +18,9 @@ PNPM_VERSION=11.28.5
|
||||
# Release identity = DSH_VERSION plus an optional repository revision.
|
||||
# An empty RELEASE_REVISION is the first release at this DSH ref; rN (N >= 2)
|
||||
# marks a repository-only re-release at the same DSH ref. This is the first
|
||||
# release at dsh-v0.2.1-rc.5, so the identity is the plain tag; a later
|
||||
# release at dsh-v0.2.1-rc.5-r6, so the identity is the plain tag; a later
|
||||
# repository-only change at the same ref becomes -r2.
|
||||
RELEASE_REVISION=
|
||||
RELEASE_REVISION=r6
|
||||
|
||||
# Base images for the main DSH Web image (root Dockerfile), pinned by digest.
|
||||
NODE_BUILD_IMAGE=docker.io/node:24-trixie@sha256:8c761eca2bf7eec965f946cf47b8090143cb41f45f2f8699af0cc0298d2d2d19
|
||||
|
||||
Reference in New Issue
Block a user