forked from admin/VolcanicAgenticCore
feat(docker): Dockerfile for the Telegram host
Two-stage build, agent tools, Node 24, claude CLI, data dirs owned by app, DOTNET_EnableDiagnostics=0. Adds the MCP spec's deferred test that the Dockerfile's PLAYWRIGHT_MCP_VERSION equals PlaywrightPreset.DefaultPackage. Spec change: the image keeps root as its user and the entrypoint drops to app with setpriv. With USER app, compose's docker-init (PID 1) runs as app and its /proc/1/environ exposes every secret to the agent's bash. The spec also lists the csprojs the restore layer copies (GoogleCalendar, Mcp, Scheduler included) and sets HOME=/home/app in the image.
This commit is contained in:
17
.dockerignore
Normal file
17
.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
**/TestResults
|
||||||
|
**/*.user
|
||||||
|
**/Properties/launchSettings.json
|
||||||
|
.git
|
||||||
|
.vs
|
||||||
|
.idea
|
||||||
|
.claude
|
||||||
|
.volcanic
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
tests
|
||||||
|
samples
|
||||||
|
docs
|
||||||
|
*.md
|
||||||
|
docker-compose.yml
|
||||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Dockerfile text eol=lf
|
||||||
|
.dockerignore text eol=lf
|
||||||
|
docker-compose.yml text eol=lf
|
||||||
|
.env.example text eol=lf
|
||||||
76
Dockerfile
Normal file
76
Dockerfile
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# The Telegram host as one container. User guide: docs/docker.md.
|
||||||
|
# Spec: docs/superpowers/specs/2026-09-26-docker-deployment-design.md
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Restore layer: only the project files the host needs, cached until one of them changes.
|
||||||
|
COPY Directory.Build.props VolcanicAgenticCore.slnx ./
|
||||||
|
COPY src/Volcanic.AgenticCore.Abstractions/Volcanic.AgenticCore.Abstractions.csproj src/Volcanic.AgenticCore.Abstractions/
|
||||||
|
COPY src/Volcanic.AgenticCore/Volcanic.AgenticCore.csproj src/Volcanic.AgenticCore/
|
||||||
|
COPY extensions/Volcanic.AgenticCore.Extensions.ClaudeCode/Volcanic.AgenticCore.Extensions.ClaudeCode.csproj extensions/Volcanic.AgenticCore.Extensions.ClaudeCode/
|
||||||
|
COPY extensions/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.csproj extensions/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge/
|
||||||
|
COPY extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar/Volcanic.AgenticCore.Extensions.GoogleCalendar.csproj extensions/Volcanic.AgenticCore.Extensions.GoogleCalendar/
|
||||||
|
COPY extensions/Volcanic.AgenticCore.Extensions.Mcp/Volcanic.AgenticCore.Extensions.Mcp.csproj extensions/Volcanic.AgenticCore.Extensions.Mcp/
|
||||||
|
COPY extensions/Volcanic.AgenticCore.Extensions.Scheduler/Volcanic.AgenticCore.Extensions.Scheduler.csproj extensions/Volcanic.AgenticCore.Extensions.Scheduler/
|
||||||
|
COPY hosts/Volcanic.AgenticCore.Telegram/Volcanic.AgenticCore.Telegram.csproj hosts/Volcanic.AgenticCore.Telegram/
|
||||||
|
RUN dotnet restore hosts/Volcanic.AgenticCore.Telegram/Volcanic.AgenticCore.Telegram.csproj
|
||||||
|
|
||||||
|
COPY src/ src/
|
||||||
|
COPY extensions/ extensions/
|
||||||
|
COPY hosts/ hosts/
|
||||||
|
# Framework-dependent: claude starts the bridge as `dotnet <Bridge>.dll`, so the loose dll and its runtimeconfig must exist.
|
||||||
|
RUN dotnet publish hosts/Volcanic.AgenticCore.Telegram/Volcanic.AgenticCore.Telegram.csproj -c Release --no-restore -o /out \
|
||||||
|
&& test -f /out/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.dll \
|
||||||
|
&& test -f /out/Volcanic.AgenticCore.Extensions.ClaudeCode.Bridge.runtimeconfig.json
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/runtime:10.0-noble
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG NODE_MAJOR=24
|
||||||
|
ARG CLAUDE_CODE_VERSION=2.1
|
||||||
|
ARG INSTALL_PLAYWRIGHT=false
|
||||||
|
# Must equal the version in PlaywrightPreset.DefaultPackage (the single source of truth);
|
||||||
|
# PlaywrightPresetTests.Dockerfile_version_matches_DefaultPackage fails when they differ.
|
||||||
|
ARG PLAYWRIGHT_MCP_VERSION=0.0.82
|
||||||
|
|
||||||
|
# What the agent's bash tool can run, then Node from NodeSource (Ubuntu noble's nodejs is too old).
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
bash git ripgrep python3 python-is-python3 python3-venv curl ca-certificates tzdata jq less procps openssh-client gnupg \
|
||||||
|
&& mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# The global prefix stays root-owned, so claude's auto-updater can't replace it at runtime: an upgrade changes what
|
||||||
|
# system/init validation sees and invalidates the prompt cache, so it only arrives with a rebuild.
|
||||||
|
RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
||||||
|
&& claude --version \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
# Set even without Playwright, so an MCP server started with the host environment finds browsers here.
|
||||||
|
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
|
||||||
|
# INSTALL_PLAYWRIGHT block (T2)
|
||||||
|
|
||||||
|
# The entrypoint hard-codes uid/gid 1654. Named volumes copy this ownership on first mount.
|
||||||
|
RUN test "$(id -u app)" = 1654 && test "$(id -g app)" = 1654 \
|
||||||
|
&& mkdir -p /data/workspaces /data/agent /data/claude /home/app \
|
||||||
|
&& chown -R app:app /data /home/app
|
||||||
|
|
||||||
|
COPY --from=build /out /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV Telegram__WorkspaceRoot=/data/workspaces \
|
||||||
|
VOLCANIC_AGENT_DIR=/data/agent \
|
||||||
|
CLAUDE_CONFIG_DIR=/data/claude \
|
||||||
|
HOME=/home/app \
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT=1 \
|
||||||
|
DOTNET_NOLOGO=1 \
|
||||||
|
TZ=UTC \
|
||||||
|
DOTNET_EnableDiagnostics=0
|
||||||
|
|
||||||
|
# No USER: PID 1 (docker-init with compose's init: true) must stay root, or the agent could read every secret from
|
||||||
|
# /proc/1/environ. setpriv drops to app and execs dotnet, so Environment.ProcessPath is the muxer the bridge needs.
|
||||||
|
ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]
|
||||||
@@ -21,12 +21,12 @@ build and to the Claude Code provider's subscription-only rules.
|
|||||||
| Files | `Dockerfile`, `.dockerignore`, `docker-compose.yml`, `.env.example` at the repo root; user doc `docs/docker.md` |
|
| Files | `Dockerfile`, `.dockerignore`, `docker-compose.yml`, `.env.example` at the repo root; user doc `docs/docker.md` |
|
||||||
| Build | Multi-stage: `mcr.microsoft.com/dotnet/sdk:10.0-noble` → `mcr.microsoft.com/dotnet/runtime:10.0-noble` |
|
| Build | Multi-stage: `mcr.microsoft.com/dotnet/sdk:10.0-noble` → `mcr.microsoft.com/dotnet/runtime:10.0-noble` |
|
||||||
| Publish | Framework-dependent `dotnet publish -c Release` of the Telegram host only. No self-contained, no AOT, no single-file: `claude` launches the bridge as `dotnet <Bridge>.dll`, so the `dotnet` muxer and a loose `Bridge.dll` + `.runtimeconfig.json` must exist |
|
| Publish | Framework-dependent `dotnet publish -c Release` of the Telegram host only. No self-contained, no AOT, no single-file: `claude` launches the bridge as `dotnet <Bridge>.dll`, so the `dotnet` muxer and a loose `Bridge.dll` + `.runtimeconfig.json` must exist |
|
||||||
| Entry | `ENTRYPOINT ["dotnet", "Volcanic.AgenticCore.Telegram.dll"]`, so `Environment.ProcessPath` is the muxer `BridgeLocator.FindDotnet` wants. No entrypoint script |
|
| Entry | `ENTRYPOINT ["setpriv", "--reuid=1654", "--regid=1654", "--init-groups", "--inh-caps=-all", "--", "dotnet", "Volcanic.AgenticCore.Telegram.dll"]`: `setpriv` (util-linux, already in the base image) drops to `app` and execs `dotnet`, so `Environment.ProcessPath` is still the muxer `BridgeLocator.FindDotnet` wants. No entrypoint script |
|
||||||
| Agent tools | `bash`, `git`, `ripgrep`, `python3` (+ `python-is-python3`, `python3-venv`), `curl`, `ca-certificates`, `tzdata`, `jq`, `less`, `procps`, `openssh-client` |
|
| Agent tools | `bash`, `git`, `ripgrep`, `python3` (+ `python-is-python3`, `python3-venv`), `curl`, `ca-certificates`, `tzdata`, `jq`, `less`, `procps`, `openssh-client` |
|
||||||
| Node | Node.js 24 from NodeSource (`ARG NODE_MAJOR=24`); Ubuntu noble's `nodejs` is too old |
|
| Node | Node.js 24 from NodeSource (`ARG NODE_MAJOR=24`); Ubuntu noble's `nodejs` is too old |
|
||||||
| Claude CLI | `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}`, `ARG CLAUDE_CODE_VERSION=2.1` (newest 2.1.x at build time; set an exact version to pin). Updated by rebuilding, never in place |
|
| Claude CLI | `npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}`, `ARG CLAUDE_CODE_VERSION=2.1` (newest 2.1.x at build time; set an exact version to pin). Updated by rebuilding, never in place |
|
||||||
| Playwright | `ARG INSTALL_PLAYWRIGHT=false` in the Dockerfile; compose passes `true`. Installs `@playwright/mcp` globally at the version pinned by the MCP spec, and Chromium + OS deps under `/opt/ms-playwright`. Compose points the preset at the preinstalled `playwright-mcp` (no npx download) and adds `--no-sandbox` |
|
| Playwright | `ARG INSTALL_PLAYWRIGHT=false` in the Dockerfile; compose passes `true`. Installs `@playwright/mcp` globally at the version pinned by the MCP spec, and Chromium + OS deps under `/opt/ms-playwright`. Compose points the preset at the preinstalled `playwright-mcp` (no npx download) and adds `--no-sandbox` |
|
||||||
| User | The runtime image's built-in non-root `app` user (`$APP_UID`, 1654), `HOME=/home/app` |
|
| User | The image's default user stays root so that PID 1 (compose's `init: true`, i.e. `docker-init`) is root-owned; the bot and everything it spawns run as the base image's non-root `app` user (1654, `$APP_UID`), `HOME=/home/app`. With `USER app`, `docker-init` runs as `app` and its `/proc/1/environ` (the whole container environment, secrets included) is readable by the agent's `bash` (checked on Docker 29) |
|
||||||
| Data | Three named volumes under `/data`: `workspaces`, `agent`, `claude` |
|
| Data | Three named volumes under `/data`: `workspaces`, `agent`, `claude` |
|
||||||
| Config | Environment only, set in Coolify. Required: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_IDS`. Optional: `OPENAI_API_KEY`, `STT_MODEL`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `DEFAULT_TIMEZONE`, `PROCESS_ENV_ALLOW`, `TZ`, `PLAYWRIGHT_ENABLED`, `PLAYWRIGHT_HEADLESS`, `PLAYWRIGHT_BROWSER`, `PLAYWRIGHT_EXTRA_ARGS`, `PLAYWRIGHT_COMMAND` |
|
| Config | Environment only, set in Coolify. Required: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ALLOWED_USER_IDS`. Optional: `OPENAI_API_KEY`, `STT_MODEL`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `DEFAULT_TIMEZONE`, `PROCESS_ENV_ALLOW`, `TZ`, `PLAYWRIGHT_ENABLED`, `PLAYWRIGHT_HEADLESS`, `PLAYWRIGHT_BROWSER`, `PLAYWRIGHT_EXTRA_ARGS`, `PLAYWRIGHT_COMMAND` |
|
||||||
| Healthcheck | None (see below) |
|
| Healthcheck | None (see below) |
|
||||||
@@ -42,8 +42,7 @@ the image build.
|
|||||||
Build stage (`sdk:10.0-noble`):
|
Build stage (`sdk:10.0-noble`):
|
||||||
|
|
||||||
1. `WORKDIR /src`. Copy `Directory.Build.props`, `VolcanicAgenticCore.slnx` and only the `*.csproj` files the host
|
1. `WORKDIR /src`. Copy `Directory.Build.props`, `VolcanicAgenticCore.slnx` and only the `*.csproj` files the host
|
||||||
needs (Abstractions, core, ClaudeCode, ClaudeCode.Bridge, Telegram, plus the Scheduler and GoogleCalendar
|
needs (Abstractions, core, ClaudeCode, ClaudeCode.Bridge, GoogleCalendar, Mcp, Scheduler, Telegram), then
|
||||||
extensions once the host references them), then
|
|
||||||
`dotnet restore hosts/Volcanic.AgenticCore.Telegram` — a cached layer until a project file changes.
|
`dotnet restore hosts/Volcanic.AgenticCore.Telegram` — a cached layer until a project file changes.
|
||||||
2. Copy `src/`, `extensions/`, `hosts/` and `dotnet publish hosts/Volcanic.AgenticCore.Telegram -c Release
|
2. Copy `src/`, `extensions/`, `hosts/` and `dotnet publish hosts/Volcanic.AgenticCore.Telegram -c Release
|
||||||
--no-restore -o /out`. `TreatWarningsAsErrors` applies, so a warning fails the image build as it fails `dotnet build`.
|
--no-restore -o /out`. `TreatWarningsAsErrors` applies, so a warning fails the image build as it fails `dotnet build`.
|
||||||
@@ -67,15 +66,17 @@ Runtime stage (`runtime:10.0-noble`), as root:
|
|||||||
process inherits the environment. `ARG PLAYWRIGHT_MCP_VERSION` defaults to the exact version in the MCP spec's
|
process inherits the environment. `ARG PLAYWRIGHT_MCP_VERSION` defaults to the exact version in the MCP spec's
|
||||||
`PlaywrightPreset.DefaultPackage`, which is the single source of truth. A unit test in that spec fails when the two
|
`PlaywrightPreset.DefaultPackage`, which is the single source of truth. A unit test in that spec fails when the two
|
||||||
differ, so bumping the version means changing both in one commit. Compose does not override this arg.
|
differ, so bumping the version means changing both in one commit. Compose does not override this arg.
|
||||||
4. `mkdir -p /data/workspaces /data/agent /data/claude && chown -R app:app /data`. Named volumes copy this
|
4. `mkdir -p /data/workspaces /data/agent /data/claude /home/app && chown -R app:app /data /home/app`. Named volumes
|
||||||
ownership on first mount, which is what lets a non-root process write to them.
|
copy this ownership on first mount, which is what lets a non-root process write to them.
|
||||||
5. `COPY --from=build /out /app`, `WORKDIR /app`, `USER $APP_UID`.
|
5. `COPY --from=build /out /app`, `WORKDIR /app`. No `USER` (see the User row); an earlier step checks that `app` is
|
||||||
|
uid/gid 1654, which the entrypoint hard-codes.
|
||||||
6. Environment baked into the image (compose and Coolify may override):
|
6. Environment baked into the image (compose and Coolify may override):
|
||||||
|
|
||||||
| Variable | Value | Why |
|
| Variable | Value | Why |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `Telegram__WorkspaceRoot` | `/data/workspaces` | Chat `<id>` works in `/data/workspaces/<id>` |
|
| `Telegram__WorkspaceRoot` | `/data/workspaces` | Chat `<id>` works in `/data/workspaces/<id>` |
|
||||||
| `VOLCANIC_AGENT_DIR` | `/data/agent` | Sessions, `extensions/*.dll`, `settings.json`, `auth.json` |
|
| `VOLCANIC_AGENT_DIR` | `/data/agent` | Sessions, `extensions/*.dll`, `settings.json`, `auth.json` |
|
||||||
|
| `HOME` | `/home/app` | The image user is root, so Docker would otherwise set `/root` |
|
||||||
| `CLAUDE_CONFIG_DIR` | `/data/claude` | Moves both `~/.claude/` and `~/.claude.json` (credentials and onboarding state) into one volume. Already on the provider's env allow-list, so every `claude` child and `claude auth login` see it |
|
| `CLAUDE_CONFIG_DIR` | `/data/claude` | Moves both `~/.claude/` and `~/.claude.json` (credentials and onboarding state) into one volume. Already on the provider's env allow-list, so every `claude` child and `claude auth login` see it |
|
||||||
| `DOTNET_CLI_TELEMETRY_OPTOUT`, `DOTNET_NOLOGO` | `1` | Quiet |
|
| `DOTNET_CLI_TELEMETRY_OPTOUT`, `DOTNET_NOLOGO` | `1` | Quiet |
|
||||||
| `TZ` | `UTC` | Overridable |
|
| `TZ` | `UTC` | Overridable |
|
||||||
@@ -153,11 +154,12 @@ volumes:
|
|||||||
`*_API_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD` or their `Section__Key` forms) are removed from the environment of
|
`*_API_KEY`, `*_TOKEN`, `*_SECRET`, `*_PASSWORD` or their `Section__Key` forms) are removed from the environment of
|
||||||
the agent's `bash`, scheduled scripts and MCP servers by the process environment hook. To give the agent a
|
the agent's `bash`, scheduled scripts and MCP servers by the process environment hook. To give the agent a
|
||||||
credential on purpose (say `GH_TOKEN` for `gh`), add it in Coolify and list its name in `PROCESS_ENV_ALLOW`; an MCP
|
credential on purpose (say `GH_TOKEN` for `gh`), add it in Coolify and list its name in `PROCESS_ENV_ALLOW`; an MCP
|
||||||
server gets one through its own `env` entry instead. On top of that, the bot (PID 1 in the container) makes itself
|
server gets one through its own `env` entry instead. On top of that, the bot (the only child of the root-owned
|
||||||
non-dumpable at startup, so the agent can't read `/proc/1/environ` or attach to it, and the configured secrets'
|
PID 1) makes itself non-dumpable at startup, so the agent can't read its `/proc/<pid>/environ` or attach to it,
|
||||||
values are replaced by `[REDACTED]` in tool output (process-environment-hook spec §8–§9). The side effect is no
|
PID 1's own `/proc/1/environ` is root-only, and the configured secrets' values are replaced by `[REDACTED]` in
|
||||||
core dumps and no `dotnet-dump`/`dotnet-trace` attach; `ProcessEnvironment__HardenHostProcess=false` turns it off
|
tool output (process-environment-hook spec §8–§9). The side effect is no core dumps and no
|
||||||
for a debugging session. The container stays the real isolation boundary: none of this is a sandbox.
|
`dotnet-dump`/`dotnet-trace` attach; `ProcessEnvironment__HardenHostProcess=false` turns it off for a debugging
|
||||||
|
session. The container stays the real isolation boundary: none of this is a sandbox.
|
||||||
- Playwright: `PLAYWRIGHT_COMMAND=playwright-mcp` runs the globally installed, version-pinned server, so the first
|
- Playwright: `PLAYWRIGHT_COMMAND=playwright-mcp` runs the globally installed, version-pinned server, so the first
|
||||||
browser call in a chat doesn't wait on an npx download. `PLAYWRIGHT_EXTRA_ARGS` defaults to `--no-sandbox`:
|
browser call in a chat doesn't wait on an npx download. `PLAYWRIGHT_EXTRA_ARGS` defaults to `--no-sandbox`:
|
||||||
Chromium's sandbox needs user namespaces, which Docker's default seccomp profile denies to the non-root `app` user,
|
Chromium's sandbox needs user namespaces, which Docker's default seccomp profile denies to the non-root `app` user,
|
||||||
@@ -190,9 +192,9 @@ shows the service as running without health status; that is acceptable.
|
|||||||
`/data/claude`. `/claude-code-doctor` should then report logged in, `claude.ai`, bridge ok.
|
`/data/claude`. `/claude-code-doctor` should then report logged in, `claude.ai`, bridge ok.
|
||||||
4. Later expiries use the existing owner re-login flow; nothing Docker-specific.
|
4. Later expiries use the existing owner re-login flow; nothing Docker-specific.
|
||||||
|
|
||||||
Fallback without the bot: `docker exec -it <container> claude auth login --claudeai` (runs as `app`, same
|
Fallback without the bot: `docker exec -u app -it <container> claude auth login --claudeai` (same `CLAUDE_CONFIG_DIR`;
|
||||||
`CLAUDE_CONFIG_DIR`). Copying an existing `.credentials.json` into the `claude` volume also works but is not the
|
`docker exec` defaults to root, whose credentials the bot couldn't read). Copying an existing `.credentials.json` into
|
||||||
documented path.
|
the `claude` volume also works but is not the documented path.
|
||||||
|
|
||||||
## Extensions and project context
|
## Extensions and project context
|
||||||
|
|
||||||
|
|||||||
@@ -88,4 +88,24 @@ public class PlaywrightPresetTests
|
|||||||
Assert.Contains("browser_close", without, StringComparison.Ordinal);
|
Assert.Contains("browser_close", without, StringComparison.Ordinal);
|
||||||
Assert.Contains("passwords", without, StringComparison.Ordinal);
|
Assert.Contains("passwords", without, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dockerfile_version_matches_DefaultPackage()
|
||||||
|
{
|
||||||
|
var dockerfile = File.ReadAllText(RepoFile("Dockerfile"));
|
||||||
|
var matches = Regex.Matches(dockerfile, @"^ARG PLAYWRIGHT_MCP_VERSION=(?<version>\S+)\s*$", RegexOptions.Multiline);
|
||||||
|
|
||||||
|
var match = Assert.Single(matches);
|
||||||
|
Assert.Equal(PlaywrightPreset.PackageVersion, match.Groups["version"].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A file in the repository root, found by walking up from the test output to <c>VolcanicAgenticCore.slnx</c>.</summary>
|
||||||
|
private static string RepoFile(string name)
|
||||||
|
{
|
||||||
|
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(dir.FullName, "VolcanicAgenticCore.slnx"))) return Path.Combine(dir.FullName, name);
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("VolcanicAgenticCore.slnx not found above " + AppContext.BaseDirectory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user