Add Docker image and GitHub Action for qtmesh CLI - #184
Conversation
The curl installer places sentry-cli in /usr/local/bin which is not in Git Bash's PATH on Windows runners. Use the full path instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enables running qtmesh CLI commands via Docker without local installation. The image installs the release .deb in Ubuntu 22.04 with Xvfb for headless Ogre GL rendering. - Dockerfile: single-stage Ubuntu 22.04, COPY .deb approach - docker-entrypoint.sh: auto-starts Xvfb, routes CLI/MCP commands - docker-publish.yml: builds and pushes on release (ghcr.io + Docker Hub) - .github/actions/qtmesh: reusable composite action for CI pipelines - .dockerignore: excludes build artifacts and dev files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bae8dd440b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| # Create qtmesh symlink for CLI mode detection | ||
| # (binary name must start with "qtmesh" but NOT contain "editor") | ||
| RUN ln -sf /usr/share/qtmesheditor/qtmesheditor /usr/local/bin/qtmesh |
There was a problem hiding this comment.
Route CLI through packaged launcher script
qtmesh is symlinked directly to /usr/share/qtmesheditor/qtmesheditor, but the Debian package is built to launch via /usr/bin/qtmesheditor (which exports runtime env like LD_LIBRARY_PATH before exec). Because the entrypoint sends info|fix|convert|anim|--help|--version through qtmesh, the container can fail at runtime with missing Qt/Ogre shared libraries even though the package installed successfully.
Useful? React with 👍 / 👎.
| run: | | ||
| VERSION="${{ steps.version.outputs.version }}" | ||
| TAGS="${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}:${VERSION}" | ||
| TAGS="${TAGS},${{ env.REGISTRY_GHCR }}/${{ env.IMAGE_NAME }}:latest" |
There was a problem hiding this comment.
Skip publishing :latest for version-pinned dispatch builds
This step always appends the GHCR :latest tag, including workflow_dispatch runs where inputs.version is explicitly set to an older release. Rebuilding or backfilling an old tag would therefore overwrite ghcr.io/...:latest with stale binaries, regressing consumers that pull latest.
Useful? React with 👍 / 👎.
| - name: Extract version from CMakeLists.txt | ||
| id: version | ||
| run: | | ||
| VERSION=$(grep 'project(QtMeshEditor VERSION' CMakeLists.txt | sed 's/.*VERSION \([0-9.]*\).*/\1/') |
There was a problem hiding this comment.
Derive image version from the selected release artifact
The workflow downloads the .deb for the requested release tag, but then computes VERSION from the checked-out branch’s CMakeLists.txt. On workflow_dispatch for non-current tags, this can publish labels/tags that claim one version while containing another release’s binary, making image provenance unreliable.
Useful? React with 👍 / 👎.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds Docker packaging and entrypoint for QtMeshEditor, a reusable GitHub Actions composite action for running qtmesh, CI workflows to build/tag/publish container images, updates .dockerignore, and bumps project version to 2.11.1; also creates a packaging symlink for the qtmesh CLI. Changes
Sequence Diagram(s)sequenceDiagram
participant Release as Release/Event
participant GH_Actions as GitHub Actions
participant GH_CLI as gh CLI
participant Buildx as Docker Buildx
participant Registry as Registry (ghcr.io / Docker Hub)
Release->>GH_Actions: trigger (release or workflow_dispatch)
GH_Actions->>GH_CLI: download .deb release assets
GH_Actions->>Buildx: build image with tags & OCI labels
Buildx->>Registry: push image tags
GH_Actions->>Registry: run container --help to verify
sequenceDiagram
participant Workflow as Workflow Job
participant Composite as .github/actions/qtmesh
participant Docker as Docker (runner)
participant Container as qtmesheditor image
Workflow->>Composite: invoke (command, input-file, options, image-tag)
Composite->>Docker: run container with workspace mount
Docker->>Container: execute qtmesh / qtmesheditor
Container-->>Composite: stdout captured
Composite-->>Workflow: output `result`
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/actions/qtmesh/action.yml:
- Around line 30-44: The current run step interpolates inputs directly into a
shell string (OUTPUT, OUTPUT_FLAG, ${{ inputs.command }}, ${{ inputs.options }})
which allows word-splitting and injection and also runs the container as root;
change it to build safe arrays and quoted expansions and add a non-root user:
construct a bash array for the docker command and a separate array for the
container command (e.g., DOCKER_ARGS=(--rm -v "${{ github.workspace
}}:/workspace" --user "$(id -u):$(id -g)"
"ghcr.io/fernandotonon/qtmesheditor:${{ inputs.image-tag }}" ) and CMD_ARGS=(
"${{ inputs.command }}" "/workspace/${{ inputs.input-file }}" ), use read -r -a
OPTS <<< "${{ inputs.options }}" to split options into an array if present and
append them to CMD_ARGS, handle OUTPUT_FLAG by appending "-o" and
"/workspace/${{ inputs.output-file }}" to CMD_ARGS only when inputs.output-file
is non-empty, then execute docker with: OUTPUT=$(docker run "${DOCKER_ARGS[@]}"
"${CMD_ARGS[@]}") so all inputs are passed as separate, quoted argv elements and
files created inside the workspace are owned by the calling user instead of
root.
In @.github/workflows/docker-publish.yml:
- Around line 39-50: The workflow currently downloads all matching .deb assets
which can produce multiple files and break Docker's COPY *.deb; modify the
"Download .deb from release" step to select a single asset and rename it to a
deterministic filename (e.g., qtmesheditor.deb) after download (use the TAG
variable and gh release download options or filter/rename the single matched
file), and then update the Dockerfile to COPY that deterministic filename (COPY
qtmesheditor.deb /tmp/qtmesheditor.deb) instead of using a glob; ensure the
workflow echoes/ls only the renamed file to verify.
- Around line 52-86: The workflow currently extracts VERSION in the "Extract
version from CMakeLists.txt" step (id: version) and later computes tags in the
"Compute Docker tags" step (id: tags), which causes tags to reflect the
checked-out branch instead of the selected release artifact; change the logic so
VERSION is derived from the selected release artifact or release tag (use the
downloaded asset filename or github.event.release.tag_name when a release is
targeted) and only set the additional :latest tag when the release being built
is the latest/current release; update the version extraction step (id: version)
to prefer the release/tag source and adjust the tag computation step (id: tags)
accordingly so published images are named from the release artifact, not from
the checked-out branch.
In `@docker-entrypoint.sh`:
- Around line 7-12: The startup loop that probes Xvfb using "for i in $(seq 1
10); do xdpyinfo -display :99 &>/dev/null && break; sleep 0.2; done" should fail
fast if the probe never succeeds: after that loop check the probe result (e.g.
run xdpyinfo -display :99 once more or test whether the loop broke) and if it
still fails, emit a clear error message and exit with a non‑zero status (use
exit 1) so the container stops immediately instead of continuing to later
Qt/Ogre steps; update the block around the Xvfb start and probe to perform this
check and exit on timeout.
In `@Dockerfile`:
- Around line 1-30: The Dockerfile currently leaves the container running as
root; add a non-root runtime user and switch to it before ENTRYPOINT to reduce
blast radius. Create a dedicated user/group (e.g., qtmesh user) and set
ownership of /workspace and any runtime-required files (and the installed app
path) to that user, then add USER <username> before ENTRYPOINT; ensure
docker-entrypoint.sh remains executable and does not require root-only
operations (or performs privileged setup and then drops privileges). Update any
references in docker-entrypoint.sh that assume root to work under the new user
or perform the privilege-drop there if needed.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8f81218c-b048-4d42-9d3e-d338bd798a83
📒 Files selected for processing (5)
.dockerignore.github/actions/qtmesh/action.yml.github/workflows/docker-publish.ymlDockerfiledocker-entrypoint.sh
- Dockerfile: add non-root user (qtmesh, uid 10001), use deterministic COPY filename, symlink qtmesh through launcher script (sets LD_LIBRARY_PATH) - docker-entrypoint.sh: fail fast with clear error if Xvfb never starts, use absolute paths through /usr/bin/qtmesh launcher - docker-publish.yml: rename downloaded .deb to deterministic filename, extract version from .deb metadata (not CMakeLists.txt), only tag :latest for release events or unversioned dispatch - action.yml: prevent shell injection by using env vars and bash arrays, run container with --user to match host UID - deploy.yml: add /usr/bin/qtmesh symlink to .deb package so the qtmesh CLI command works after apt install Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/actions/qtmesh/action.yml:
- Around line 54-56: The heredoc uses the generic delimiter "EOF" which can
collide with OUTPUT content; update the three lines that write to GITHUB_OUTPUT
(the lines echoing "result<<EOF", echo "$OUTPUT", and echo "EOF") to use a
unique delimiter instead (e.g., "QTMESH_RESULT_<unique>" or a generated
timestamp/UUID) so the opening and closing heredoc markers match and cannot
appear in $OUTPUT; ensure the modified opening marker ("result<<DELIM") and the
final marker ("DELIM") are identical and unlikely to appear in command output.
- Around line 30-52: The docker image tag is injected unsafely via `${{
inputs.image-tag }}` in the docker run call; instead pass the image tag through
the action env (e.g., add INPUT_IMAGE_TAG: ${{ inputs.image-tag }} alongside the
other INPUT_* env entries) and reference it inside the run script as
"$INPUT_IMAGE_TAG" when invoking docker (replace the literal "ghcr.io/...:${{
inputs.image-tag }}" with a construct using the env var), so the value is
handled by the shell-safe variable expansion like the other inputs and not
pre-interpolated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d6f77565-6f35-4941-bb10-d9f60711487a
📒 Files selected for processing (5)
.github/actions/qtmesh/action.yml.github/workflows/deploy.yml.github/workflows/docker-publish.ymlDockerfiledocker-entrypoint.sh
🚧 Files skipped from review as they are similar to previous changes (2)
- .github/workflows/docker-publish.yml
- docker-entrypoint.sh
| - id: run | ||
| shell: bash | ||
| env: | ||
| INPUT_COMMAND: ${{ inputs.command }} | ||
| INPUT_FILE: ${{ inputs.input-file }} | ||
| INPUT_OUTPUT_FILE: ${{ inputs.output-file }} | ||
| INPUT_OPTIONS: ${{ inputs.options }} | ||
| run: | | ||
| # Build command args safely via arrays to prevent injection | ||
| cmd=("$INPUT_COMMAND" "/workspace/$INPUT_FILE") | ||
| if [ -n "$INPUT_OUTPUT_FILE" ]; then | ||
| cmd+=("-o" "/workspace/$INPUT_OUTPUT_FILE") | ||
| fi | ||
| if [ -n "$INPUT_OPTIONS" ]; then | ||
| read -r -a opts <<< "$INPUT_OPTIONS" | ||
| cmd+=("${opts[@]}") | ||
| fi | ||
|
|
||
| OUTPUT=$(docker run --rm \ | ||
| --user "$(id -u):$(id -g)" \ | ||
| -v "${{ github.workspace }}:/workspace" \ | ||
| "ghcr.io/fernandotonon/qtmesheditor:${{ inputs.image-tag }}" \ | ||
| "${cmd[@]}") |
There was a problem hiding this comment.
Shell injection mitigations are well-implemented, but image-tag input bypasses the safe pattern.
The env-var + array approach for command, input-file, output-file, and options correctly prevents shell injection. However, inputs.image-tag at line 51 is still directly interpolated via ${{ }}, which expands before bash runs and could allow metacharacter injection if a workflow passes an untrusted value.
🛡️ Suggested fix: Pass image-tag through env as well
env:
INPUT_COMMAND: ${{ inputs.command }}
INPUT_FILE: ${{ inputs.input-file }}
INPUT_OUTPUT_FILE: ${{ inputs.output-file }}
INPUT_OPTIONS: ${{ inputs.options }}
+ INPUT_IMAGE_TAG: ${{ inputs.image-tag }}
run: |
# Build command args safely via arrays to prevent injection
cmd=("$INPUT_COMMAND" "/workspace/$INPUT_FILE")
if [ -n "$INPUT_OUTPUT_FILE" ]; then
cmd+=("-o" "/workspace/$INPUT_OUTPUT_FILE")
fi
if [ -n "$INPUT_OPTIONS" ]; then
read -r -a opts <<< "$INPUT_OPTIONS"
cmd+=("${opts[@]}")
fi
OUTPUT=$(docker run --rm \
--user "$(id -u):$(id -g)" \
-v "${{ github.workspace }}:/workspace" \
- "ghcr.io/fernandotonon/qtmesheditor:${{ inputs.image-tag }}" \
+ "ghcr.io/fernandotonon/qtmesheditor:${INPUT_IMAGE_TAG}" \
"${cmd[@]}")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - id: run | |
| shell: bash | |
| env: | |
| INPUT_COMMAND: ${{ inputs.command }} | |
| INPUT_FILE: ${{ inputs.input-file }} | |
| INPUT_OUTPUT_FILE: ${{ inputs.output-file }} | |
| INPUT_OPTIONS: ${{ inputs.options }} | |
| run: | | |
| # Build command args safely via arrays to prevent injection | |
| cmd=("$INPUT_COMMAND" "/workspace/$INPUT_FILE") | |
| if [ -n "$INPUT_OUTPUT_FILE" ]; then | |
| cmd+=("-o" "/workspace/$INPUT_OUTPUT_FILE") | |
| fi | |
| if [ -n "$INPUT_OPTIONS" ]; then | |
| read -r -a opts <<< "$INPUT_OPTIONS" | |
| cmd+=("${opts[@]}") | |
| fi | |
| OUTPUT=$(docker run --rm \ | |
| --user "$(id -u):$(id -g)" \ | |
| -v "${{ github.workspace }}:/workspace" \ | |
| "ghcr.io/fernandotonon/qtmesheditor:${{ inputs.image-tag }}" \ | |
| "${cmd[@]}") | |
| - id: run | |
| shell: bash | |
| env: | |
| INPUT_COMMAND: ${{ inputs.command }} | |
| INPUT_FILE: ${{ inputs.input-file }} | |
| INPUT_OUTPUT_FILE: ${{ inputs.output-file }} | |
| INPUT_OPTIONS: ${{ inputs.options }} | |
| INPUT_IMAGE_TAG: ${{ inputs.image-tag }} | |
| run: | | |
| # Build command args safely via arrays to prevent injection | |
| cmd=("$INPUT_COMMAND" "/workspace/$INPUT_FILE") | |
| if [ -n "$INPUT_OUTPUT_FILE" ]; then | |
| cmd+=("-o" "/workspace/$INPUT_OUTPUT_FILE") | |
| fi | |
| if [ -n "$INPUT_OPTIONS" ]; then | |
| read -r -a opts <<< "$INPUT_OPTIONS" | |
| cmd+=("${opts[@]}") | |
| fi | |
| OUTPUT=$(docker run --rm \ | |
| --user "$(id -u):$(id -g)" \ | |
| -v "${{ github.workspace }}:/workspace" \ | |
| "ghcr.io/fernandotonon/qtmesheditor:${INPUT_IMAGE_TAG}" \ | |
| "${cmd[@]}") |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/actions/qtmesh/action.yml around lines 30 - 52, The docker image tag
is injected unsafely via `${{ inputs.image-tag }}` in the docker run call;
instead pass the image tag through the action env (e.g., add INPUT_IMAGE_TAG:
${{ inputs.image-tag }} alongside the other INPUT_* env entries) and reference
it inside the run script as "$INPUT_IMAGE_TAG" when invoking docker (replace the
literal "ghcr.io/...:${{ inputs.image-tag }}" with a construct using the env
var), so the value is handled by the shell-safe variable expansion like the
other inputs and not pre-interpolated.
| echo "result<<EOF" >> "$GITHUB_OUTPUT" | ||
| echo "$OUTPUT" >> "$GITHUB_OUTPUT" | ||
| echo "EOF" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
Heredoc delimiter "EOF" could collide with command output.
If the qtmesh command outputs a line containing only "EOF", the GitHub Actions output will be truncated. Consider using a more unique delimiter.
🔧 Suggested fix: Use a unique delimiter
- echo "result<<EOF" >> "$GITHUB_OUTPUT"
+ DELIM="EOF_$(date +%s%N)"
+ echo "result<<$DELIM" >> "$GITHUB_OUTPUT"
echo "$OUTPUT" >> "$GITHUB_OUTPUT"
- echo "EOF" >> "$GITHUB_OUTPUT"
+ echo "$DELIM" >> "$GITHUB_OUTPUT"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| echo "result<<EOF" >> "$GITHUB_OUTPUT" | |
| echo "$OUTPUT" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| DELIM="EOF_$(date +%s%N)" | |
| echo "result<<$DELIM" >> "$GITHUB_OUTPUT" | |
| echo "$OUTPUT" >> "$GITHUB_OUTPUT" | |
| echo "$DELIM" >> "$GITHUB_OUTPUT" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/actions/qtmesh/action.yml around lines 54 - 56, The heredoc uses the
generic delimiter "EOF" which can collide with OUTPUT content; update the three
lines that write to GITHUB_OUTPUT (the lines echoing "result<<EOF", echo
"$OUTPUT", and echo "EOF") to use a unique delimiter instead (e.g.,
"QTMESH_RESULT_<unique>" or a generated timestamp/UUID) so the opening and
closing heredoc markers match and cannot appear in $OUTPUT; ensure the modified
opening marker ("result<<DELIM") and the final marker ("DELIM") are identical
and unlikely to appear in command output.
|



Summary
qtmeshCLI commands without local installationdocker-entrypoint.shthat auto-starts Xvfb for headless Ogre GL rendering and routes commands to the correct binarydocker-publish.ymlworkflow that builds and pushes the image to ghcr.io (always) and Docker Hub (on release) when a release is published.github/actions/qtmesh/for easy integration into CI pipelinesUsage
GitHub Action:
Required secrets for Docker Hub
DOCKERHUB_USERNAME— Docker Hub usernameDOCKERHUB_TOKEN— Docker Hub access tokenghcr.io uses the built-in
GITHUB_TOKEN(no setup needed).Test plan
.deb, place in repo root, build image locally:docker build -t qtmesheditor:test .--help,--version,info,convert,fix,anim --listworkflow_dispatchto verify image builds and pushes to ghcr.io🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Chores