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
27 changes: 27 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
.git/
.gitmodules
.gitattributes
.editorconfig
.cdidx/
.github/
.agents/
.agent_harness/
.claude/
.codex/
.vs/
.vscode/
.idea/
TestResults/
**/bin/
**/obj/
**/.DS_Store
*.user

tests/
tools/
docs/
changelog.d/
CodeIndex.sln
CHANGELOG.md
README.md
AGENTS.md
AGENT_GUIDE.md
CLAUDE.md
DEVELOPER_GUIDE.md
SELF_IMPROVEMENT.md
TESTING_GUIDE.md
*.md
!COMMERCIAL_LICENSE.md
!INTEGRATION_POLICY.md
!TRADEMARKS.md
16 changes: 16 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,18 @@ jobs:
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Extract container build metadata
id: container-metadata
run: |
set -euo pipefail
echo "commit=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT"
echo "date=$(git show -s --format=%cd --date=format:%Y-%m-%d HEAD)" >> "$GITHUB_OUTPUT"
if git diff-index --quiet HEAD -- ":/" ":(top,exclude,glob)**/packages.lock.json"; then
echo "dirty=clean" >> "$GITHUB_OUTPUT"
else
echo "dirty=dirty" >> "$GITHUB_OUTPUT"
fi

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4

Expand All @@ -1104,6 +1116,10 @@ jobs:
provenance: mode=max
sbom: true
tags: ${{ steps.image-tags.outputs.tags }}
build-args: |
CDIDX_BUILD_COMMIT=${{ steps.container-metadata.outputs.commit }}
CDIDX_BUILD_DATE=${{ steps.container-metadata.outputs.date }}
CDIDX_BUILD_DIRTY=${{ steps.container-metadata.outputs.dirty }}

publish-homebrew:
if: github.repository == 'Widthdom/CodeIndex'
Expand Down
31 changes: 25 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
# Base image digests are multi-arch manifest list digests. Refresh with:
# docker buildx imagetools inspect mcr.microsoft.com/dotnet/<image>:8.0-alpine
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:d9f4f4a5d99a43799b500ee1365c370e3233822fbe7d43666715d9b5b5cda2ab AS build

WORKDIR /src
COPY . .
COPY Directory.Build.props nuget.config version.json ./
COPY src/CodeIndex/CodeIndex.csproj src/CodeIndex/packages.lock.json src/CodeIndex/
RUN dotnet restore src/CodeIndex/CodeIndex.csproj
COPY src/CodeIndex/ src/CodeIndex/
COPY LICENSE COMMERCIAL_LICENSE.md INTEGRATION_POLICY.md TRADEMARKS.md ./
COPY LICENSES/ LICENSES/

ARG TARGETARCH=amd64
RUN dotnet restore src/CodeIndex/CodeIndex.csproj
ARG CDIDX_BUILD_COMMIT=unknown
ARG CDIDX_BUILD_DATE
ARG CDIDX_BUILD_DIRTY=unknown
RUN case "$TARGETARCH" in \
amd64) rid="linux-musl-x64" ;; \
arm64) rid="linux-musl-arm64" ;; \
*) echo "Unsupported container architecture: $TARGETARCH" >&2; exit 1 ;; \
esac && \
build_date="${CDIDX_BUILD_DATE:-$(date -u +%Y-%m-%d)}" && \
dotnet publish src/CodeIndex/CodeIndex.csproj \
--configuration Release \
--runtime "$rid" \
--self-contained true \
-p:PublishSingleFile=true \
-p:PublishTrimmed=true \
-p:CdidxBuildCommitOverride="$CDIDX_BUILD_COMMIT" \
-p:CdidxBuildDateOverride="$build_date" \
-p:CdidxBuildDirtyOverride="$CDIDX_BUILD_DIRTY" \
--output /out

FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS runtime
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine@sha256:7ec14bf41e70f3ca60f7b369b077636f642a0e6867caf28677d970e0abd9c6e6 AS runtime

RUN apk add --no-cache ca-certificates
COPY scripts/docker-entrypoint.sh /usr/local/bin/cdidx-entrypoint
RUN apk add --no-cache ca-certificates su-exec \
&& addgroup -S -g 10001 cdidx \
&& adduser -S -D -H -u 10001 -G cdidx -h /repo cdidx \
&& mkdir -p /repo \
&& chown cdidx:cdidx /repo \
&& chmod 0755 /usr/local/bin/cdidx-entrypoint

WORKDIR /repo
COPY --from=build /out/ /usr/local/lib/cdidx/
COPY LICENSE COMMERCIAL_LICENSE.md INTEGRATION_POLICY.md TRADEMARKS.md /usr/local/lib/cdidx/
COPY LICENSES/ /usr/local/lib/cdidx/LICENSES/
RUN ln -s /usr/local/lib/cdidx/cdidx /usr/local/bin/cdidx

ENTRYPOINT ["cdidx"]
ENTRYPOINT ["/usr/local/bin/cdidx-entrypoint"]
CMD ["--help"]
17 changes: 17 additions & 0 deletions changelog.d/unreleased/3492.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: fixed
issues:
- 3492
affected:
- Dockerfile
- scripts/docker-entrypoint.sh
- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
---

## English

- **Docker images now pin .NET base images and drop privileges for `cdidx` (#3492)** — the container build uses explicit multi-arch MCR image digests, creates a dedicated `cdidx` UID, and starts through an entrypoint that runs `cdidx` as the `/repo` owner or the dedicated UID so bind-mounted repositories remain writable without keeping the CLI process as root.

## 日本語

- **Docker image が .NET base image を digest 固定し、`cdidx` 実行時に権限降格するようになりました (#3492)** — container build は MCR の multi-arch image digest を明示し、専用の `cdidx` UID を作成します。entrypoint は bind mount した repository を書き込めるよう、`/repo` の owner または専用 UID で `cdidx` を実行し、CLI process を root のままにしません。
19 changes: 19 additions & 0 deletions changelog.d/unreleased/3493.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
category: fixed
issues:
- 3493
affected:
- .dockerignore
- .github/workflows/release.yml
- Dockerfile
- src/CodeIndex/CodeIndex.csproj
- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
---

## English

- **Docker builds now use a narrower context (#3493)** — the build stage copies only restore, source, version, and license inputs needed for the runtime image, while `.dockerignore` excludes VCS metadata, tests, docs, workflow files, changelog fragments, tools, and agent artifacts from the build context. Release builds pass commit, date, and dirty metadata through explicit build args so the image keeps the same version provenance without shipping `.git/`.

## 日本語

- **Docker build context を絞り込みました (#3493)** — build stage は runtime image に必要な restore / source / version / license 入力だけをコピーし、`.dockerignore` は VCS metadata、tests、docs、workflow files、changelog fragments、tools、agent artifacts を build context から除外します。release build は commit/date/dirty metadata を明示的な build args で渡すため、`.git/` を含めなくても image の version provenance を維持します。
24 changes: 24 additions & 0 deletions scripts/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
set -eu

if [ "$(id -u)" -eq 0 ]; then
target_uid="${CDIDX_RUN_UID:-}"
target_gid="${CDIDX_RUN_GID:-}"

if [ -z "$target_uid" ] && [ -e /repo ]; then
target_uid="$(stat -c '%u' /repo)"
fi
if [ -z "$target_gid" ] && [ -e /repo ]; then
target_gid="$(stat -c '%g' /repo)"
fi

target_uid="${target_uid:-10001}"
target_gid="${target_gid:-10001}"

if [ "$target_uid" != "0" ]; then
export HOME=/repo
exec su-exec "${target_uid}:${target_gid}" cdidx "$@"
fi
fi

exec cdidx "$@"
14 changes: 10 additions & 4 deletions src/CodeIndex/CodeIndex.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
-->
<Target Name="StampCdidxBuildMetadata" BeforeTargets="GetAssemblyAttributes">
<Exec Command="git rev-parse --short=7 HEAD"
Condition="'$(CdidxBuildCommitOverride)' == '' or '$(CdidxBuildDirtyOverride)' == ''"
ConsoleToMSBuild="true"
ContinueOnError="true"
IgnoreExitCode="true"
Expand All @@ -99,6 +100,7 @@
<Output TaskParameter="ExitCode" PropertyName="CdidxGitShaExitCode" />
</Exec>
<Exec Command="git show -s --format=%cd --date=format:%Y-%m-%d HEAD"
Condition="'$(CdidxBuildDateOverride)' == ''"
ConsoleToMSBuild="true"
ContinueOnError="true"
IgnoreExitCode="true"
Expand All @@ -109,6 +111,7 @@
<Output TaskParameter="ExitCode" PropertyName="CdidxGitDateExitCode" />
</Exec>
<Exec Command="git diff-index --quiet HEAD -- &quot;:/&quot; &quot;:(top,exclude,glob)**/packages.lock.json&quot;"
Condition="'$(CdidxBuildDirtyOverride)' == ''"
ContinueOnError="true"
IgnoreExitCode="true"
EchoOff="true"
Expand All @@ -117,12 +120,15 @@
<Output TaskParameter="ExitCode" PropertyName="CdidxGitDirtyExitCode" />
</Exec>
<PropertyGroup>
<CdidxBuildDate Condition="'$(CdidxGitDateExitCode)' == '0' and '$(CdidxGitDateRaw)' != ''">$(CdidxGitDateRaw)</CdidxBuildDate>
<CdidxBuildDate Condition="'$(CdidxBuildDateOverride)' != ''">$(CdidxBuildDateOverride)</CdidxBuildDate>
<CdidxBuildDate Condition="'$(CdidxBuildDate)' == '' and '$(CdidxGitDateExitCode)' == '0' and '$(CdidxGitDateRaw)' != ''">$(CdidxGitDateRaw)</CdidxBuildDate>
<CdidxBuildDate Condition="'$(CdidxBuildDate)' == ''">$([System.DateTime]::UtcNow.ToString('yyyy-MM-dd'))</CdidxBuildDate>
<CdidxBuildCommit Condition="'$(CdidxGitShaExitCode)' == '0' and '$(CdidxGitShaRaw)' != ''">$(CdidxGitShaRaw)</CdidxBuildCommit>
<CdidxBuildCommit Condition="'$(CdidxBuildCommitOverride)' != ''">$(CdidxBuildCommitOverride)</CdidxBuildCommit>
<CdidxBuildCommit Condition="'$(CdidxBuildCommit)' == '' and '$(CdidxGitShaExitCode)' == '0' and '$(CdidxGitShaRaw)' != ''">$(CdidxGitShaRaw)</CdidxBuildCommit>
<CdidxBuildCommit Condition="'$(CdidxBuildCommit)' == ''">unknown</CdidxBuildCommit>
<CdidxBuildDirty Condition="'$(CdidxGitShaExitCode)' == '0' and '$(CdidxGitDirtyExitCode)' == '0'">clean</CdidxBuildDirty>
<CdidxBuildDirty Condition="'$(CdidxGitShaExitCode)' == '0' and '$(CdidxGitDirtyExitCode)' == '1'">dirty</CdidxBuildDirty>
<CdidxBuildDirty Condition="'$(CdidxBuildDirtyOverride)' != ''">$(CdidxBuildDirtyOverride)</CdidxBuildDirty>
<CdidxBuildDirty Condition="'$(CdidxBuildDirty)' == '' and '$(CdidxGitShaExitCode)' == '0' and '$(CdidxGitDirtyExitCode)' == '0'">clean</CdidxBuildDirty>
<CdidxBuildDirty Condition="'$(CdidxBuildDirty)' == '' and '$(CdidxGitShaExitCode)' == '0' and '$(CdidxGitDirtyExitCode)' == '1'">dirty</CdidxBuildDirty>
<CdidxBuildDirty Condition="'$(CdidxBuildDirty)' == ''">unknown</CdidxBuildDirty>
</PropertyGroup>
<ItemGroup>
Expand Down
43 changes: 41 additions & 2 deletions tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ public void ReleaseWorkflow_PublishesOfficialContainerImage()
var root = GetRepositoryRoot();
var workflow = File.ReadAllText(Path.Combine(root, ".github", "workflows", "release.yml"));
var dockerfile = File.ReadAllText(Path.Combine(root, "Dockerfile"));
var dockerignore = File.ReadAllText(Path.Combine(root, ".dockerignore"));
var entrypoint = File.ReadAllText(Path.Combine(root, "scripts", "docker-entrypoint.sh"));
var project = File.ReadAllText(Path.Combine(root, "src", "CodeIndex", "CodeIndex.csproj"));

Assert.Contains("publish-container:", workflow);
Expand All @@ -479,12 +481,49 @@ public void ReleaseWorkflow_PublishesOfficialContainerImage()
Assert.Contains("ghcr.io/widthdom/codeindex:${version}", workflow);
Assert.Contains("ghcr.io/widthdom/codeindex:latest", workflow);
Assert.Contains("tags: ${{ steps.image-tags.outputs.tags }}", workflow);
Assert.Contains("Extract container build metadata", workflow);
Assert.Contains("git rev-parse --short=7 HEAD", workflow);
Assert.Contains("git show -s --format=%cd --date=format:%Y-%m-%d HEAD", workflow);
Assert.Contains("CDIDX_BUILD_COMMIT=${{ steps.container-metadata.outputs.commit }}", workflow);
Assert.Contains("CDIDX_BUILD_DATE=${{ steps.container-metadata.outputs.date }}", workflow);
Assert.Contains("CDIDX_BUILD_DIRTY=${{ steps.container-metadata.outputs.dirty }}", workflow);
Assert.Contains("*-*) ;;", workflow);
Assert.Contains("FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build", dockerfile);
Assert.Contains("docker buildx imagetools inspect mcr.microsoft.com/dotnet/<image>:8.0-alpine", dockerfile);
Assert.Contains("FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:d9f4f4a5d99a43799b500ee1365c370e3233822fbe7d43666715d9b5b5cda2ab AS build", dockerfile);
Assert.Contains("FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine@sha256:7ec14bf41e70f3ca60f7b369b077636f642a0e6867caf28677d970e0abd9c6e6 AS runtime", dockerfile);
Assert.DoesNotContain("FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build", dockerfile);
Assert.DoesNotContain("FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine AS runtime", dockerfile);
Assert.Contains("COPY scripts/docker-entrypoint.sh /usr/local/bin/cdidx-entrypoint", dockerfile);
Assert.Contains("apk add --no-cache ca-certificates su-exec", dockerfile);
Assert.Contains("addgroup -S -g 10001 cdidx", dockerfile);
Assert.Contains("adduser -S -D -H -u 10001 -G cdidx -h /repo cdidx", dockerfile);
Assert.Contains("chown cdidx:cdidx /repo", dockerfile);
Assert.DoesNotContain("USER cdidx:cdidx", dockerfile);
Assert.Contains("stat -c '%u' /repo", entrypoint);
Assert.Contains("stat -c '%g' /repo", entrypoint);
Assert.Contains("su-exec \"${target_uid}:${target_gid}\" cdidx \"$@\"", entrypoint);
Assert.DoesNotContain("COPY . .", dockerfile);
Assert.Contains("COPY Directory.Build.props nuget.config version.json ./", dockerfile);
Assert.Contains("COPY src/CodeIndex/CodeIndex.csproj src/CodeIndex/packages.lock.json src/CodeIndex/", dockerfile);
Assert.Contains("COPY src/CodeIndex/ src/CodeIndex/", dockerfile);
Assert.Contains("ARG CDIDX_BUILD_COMMIT=unknown", dockerfile);
Assert.Contains("-p:CdidxBuildCommitOverride=\"$CDIDX_BUILD_COMMIT\"", dockerfile);
Assert.Contains("-p:CdidxBuildDateOverride=\"$build_date\"", dockerfile);
Assert.Contains("-p:CdidxBuildDirtyOverride=\"$CDIDX_BUILD_DIRTY\"", dockerfile);
Assert.Contains(".git/", dockerignore);
Assert.Contains("tests/", dockerignore);
Assert.Contains("tools/", dockerignore);
Assert.Contains("docs/", dockerignore);
Assert.Contains("changelog.d/", dockerignore);
Assert.Contains("*.md", dockerignore);
Assert.Contains("!COMMERCIAL_LICENSE.md", dockerignore);
Assert.Contains("ARG TARGETARCH=amd64", dockerfile);
Assert.Contains("linux-musl-x64", dockerfile);
Assert.Contains("linux-musl-arm64", dockerfile);
Assert.Contains("ENTRYPOINT [\"cdidx\"]", dockerfile);
Assert.Contains("ENTRYPOINT [\"/usr/local/bin/cdidx-entrypoint\"]", dockerfile);
Assert.Contains("CdidxBuildCommitOverride", project);
Assert.Contains("CdidxBuildDateOverride", project);
Assert.Contains("CdidxBuildDirtyOverride", project);
Assert.Contains("Microsoft.NET.ILLink.Tasks\" Version=\"8.", project);
Assert.DoesNotContain("Microsoft.NET.ILLink.Tasks\" Version=\"10.", project);
}
Expand Down
Loading