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
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1222,8 +1222,8 @@ jobs:
}
continue-on-error: true

- name: Scan media/ via GitHub Action
uses: ./.github/actions/qtmesh
- name: Scan media/ via GitHub Action (root action.yml)
uses: ./
with:
command: scan
input-file: .
Expand Down
30 changes: 29 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,34 @@ The `winget-publish` CI job uses `wingetcreate --submit` to automatically submit

Manual alternative: `./scripts/update-winget.sh <version>` generates the manifest locally.

## GitHub Action (Marketplace)

The `qtmesh` CLI is published as a GitHub Action on the [GitHub Actions Marketplace](https://github.com/marketplace/actions/qtmesh). The `action.yml` lives at the repo root.

```yaml
- uses: fernandotonon/QtMeshEditor@v1
with:
command: scan
input-file: ./assets
options: --fail-on warning
```

**Key files:**
- `action.yml` — root-level action definition (required for marketplace)
- `.github/actions/qtmesh/action.yml` — legacy local action (kept for backward compatibility)
- Docker image: `ghcr.io/fernandotonon/qtmesh` (built from this repo on each release)
- Redirect repo: `fernandotonon/qtmesh` points users to this repo

**When to update action.yml:**
- New CLI subcommand added → update `command` description
- Subcommand flags change → update `options` description
- Docker image name/registry changes → update the `docker run` command
- **No update needed** for: bug fixes, GUI changes, MCP tools, or features that don't change CLI interface

The action uses `image-tag: latest` by default, so users automatically get fixes without version bumps.

**Marketplace publishing:** When creating a GitHub Release, check "Publish this Action to the GitHub Marketplace". The `v1` tag should be kept pointing to the latest stable commit (force-push tag on each release).

## CI/CD

GitHub Actions workflow in `.github/workflows/deploy.yml` builds for Windows (MinGW), macOS, and Linux. Tests run on Linux with SonarCloud coverage. Releases auto-update the Homebrew cask, WinGet package, Snap Store, and Docker image.
GitHub Actions workflow in `.github/workflows/deploy.yml` builds for Windows (MinGW), macOS, and Linux. Tests run on Linux with SonarCloud coverage. Releases auto-update the Homebrew cask, WinGet package, Snap Store, and Docker image. A `scan-assets-docker` job runs the `fernandotonon/qtmesh` action on the repo's own test assets to validate the Docker image and scan pipeline on every push/PR.
74 changes: 74 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: 'qtmesh'
description: 'Validate, convert, and optimize 3D assets in CI. Scan for issues, convert 40+ formats, and more.'
author: 'Fernando Tonon'

branding:
icon: 'box'
color: 'blue'

inputs:
command:
description: 'Subcommand: scan, info, validate, convert, fix, anim, lod, pose'
required: true
input-file:
description: 'Input file or directory path (relative to workspace)'
required: true
output-file:
description: 'Output file path (for convert/fix/anim/pose)'
required: false
options:
description: 'Additional CLI flags (e.g. --json, --fail-on warning, --resample 30)'
required: false
image-tag:
description: 'Docker image tag (default: latest)'
required: false
default: 'latest'

outputs:
result:
description: 'Command stdout (text or JSON depending on --json flag)'
value: ${{ steps.run.outputs.result }}
exit-code:
description: 'Exit code (0 = success, 1 = issues found for scan/validate)'
value: ${{ steps.run.outputs.exit-code }}

runs:
using: 'composite'
steps:
- 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

set +e
OUTPUT=$(docker run --rm \
--user "$(id -u):$(id -g)" \
-v "${{ github.workspace }}:/workspace" \
"ghcr.io/fernandotonon/qtmesh:${{ inputs.image-tag }}" \
"${cmd[@]}" 2>&1)
EXIT_CODE=$?
set -e

echo "result<<EOF" >> "$GITHUB_OUTPUT"
echo "$OUTPUT" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "exit-code=$EXIT_CODE" >> "$GITHUB_OUTPUT"

# Print output for workflow logs
echo "$OUTPUT"

# Propagate exit code for scan/validate (exit 1 = issues found)
exit $EXIT_CODE
67 changes: 50 additions & 17 deletions website/src/DocsApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ qtmesh pose <file> --animation <name> --count N -o <pattern>`}
]}
/>

<CmdSection id="cmd-scan" name="scan" description={<>Recursively scan a directory for 3D asset issues. Think of it as <strong>ESLint for 3D assets</strong>. Checks format restrictions, complexity limits, naming conventions, skeleton/animation content, and more. Supports YAML configuration, scoped rules per folder, JSON/SARIF output, and auto-fix.</>}
<CmdSection id="cmd-scan" name="scan" description={<>Recursively scan a directory for 3D asset issues. Think of it as <strong>ESLint for 3D assets</strong>. Checks format restrictions, complexity limits, naming conventions, skeleton/animation content, and more. Supports YAML configuration, scoped rules per folder, JSON output, and auto-fix. Also available as a <a href="https://github.com/marketplace/actions/qtmesh" className={s.link}>GitHub Action</a>: <Code>fernandotonon/QtMeshEditor@v1</Code>.</>}
synopsis={`qtmesh scan [path] [options]`}
options={[
['--config <file>', 'Config file path (default: qtmesh.yml, qtmesh.yaml, qtmesh.json)'],
Expand All @@ -373,8 +373,7 @@ qtmesh pose <file> --animation <name> --count N -o <pattern>`}
examples={[
'qtmesh scan ./assets',
'qtmesh scan ./assets --config qtmesh.yml --fail-on warning',
'qtmesh scan ./assets --json --report .qtmesh/report.json',
'qtmesh scan ./assets --sarif report.sarif',
'qtmesh scan ./assets --json --report report.json',
'qtmesh scan ./assets --fix --dry-run',
'qtmesh scan ./assets --include "*.fbx,*.glb" --exclude "**/vendor/**"',
]}
Expand Down Expand Up @@ -683,29 +682,63 @@ docker run --rm -v $(pwd):/workspace ghcr.io/fernandotonon/qtmesh \\

<section className={s.section} id="github-actions">
<h2 className={s.sectionTitle}>GitHub Actions</h2>
<h3 className={s.subsection}>Reusable Action (scan example)</h3>
<CodeBlock lang="yaml">{`- uses: fernandotonon/QtMeshEditor/.github/actions/qtmesh@9cfc829e8b255994ef92ba228c687e3dd2254119
<p className={s.para}>
The <Code>qtmesh</Code> action is available on the <a href="https://github.com/marketplace/actions/qtmesh" className={s.link}>GitHub Actions Marketplace</a>. Add it to any workflow with one line.
</p>

<h3 className={s.subsection}>Scan assets on every PR</h3>
<CodeBlock lang="yaml">{`name: Validate 3D Assets
on: [pull_request]

jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: fernandotonon/QtMeshEditor@v1
with:
command: scan
input-file: ./assets
options: --fail-on warning`}</CodeBlock>

<h3 className={s.subsection}>Convert and validate</h3>
<CodeBlock lang="yaml">{`- uses: fernandotonon/QtMeshEditor@v1
with:
command: validate
input-file: ./models/character.fbx

- uses: fernandotonon/QtMeshEditor@v1
with:
command: convert
input-file: ./models/character.fbx
output-file: ./output/character.glb2

- uses: fernandotonon/QtMeshEditor@v1
with:
command: scan
input-file: assets
options: --config qtmesh.yml --sarif scan-report.sarif --report scan-report.json --fail-on error`}</CodeBlock>
command: anim
input-file: ./animations/dance.fbx
output-file: ./output/dance_optimized.fbx
options: --resample 30`}</CodeBlock>

<h3 className={s.subsection}>Get mesh info as JSON</h3>
<CodeBlock lang="yaml">{`- uses: fernandotonon/QtMeshEditor@v1
id: info
with:
command: info
input-file: ./models/character.fbx
options: --json

- run: echo "\${{ steps.info.outputs.result }}"`}</CodeBlock>

<h3 className={s.subsection}>Direct Docker Usage</h3>
<h3 className={s.subsection}>Direct Docker usage</h3>
<CodeBlock lang="yaml">{`- name: Scan assets
run: |
docker run --rm \\
-v \${{ github.workspace }}:/workspace \\
ghcr.io/fernandotonon/qtmesh:latest \\
scan /workspace/assets \\
--config /workspace/qtmesh.yml \\
--sarif /workspace/scan-report.sarif \\
--fail-on error

- name: Upload SARIF to GitHub
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: scan-report.sarif`}</CodeBlock>
--fail-on error`}</CodeBlock>
</section>

<section className={s.section} id="gitlab-ci">
Expand Down
11 changes: 6 additions & 5 deletions website/src/data/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const links = {
issues: 'https://github.com/fernandotonon/QtMeshEditor/issues',
forum: 'https://forums.ogre3d.org/viewtopic.php?t=76016',
license: 'https://opensource.org/license/mit',
actions: 'https://github.com/fernandotonon/QtMeshEditor/tree/master/.github/actions/qtmesh'
actions: 'https://github.com/fernandotonon/qtmesh',
marketplace: 'https://github.com/marketplace/actions/qtmesh'
};

export const media = {
Expand Down Expand Up @@ -76,18 +77,18 @@ export const useCases = [
body: 'Combine Mixamo, Unreal exports, and DCC clips into clean output files for engine ingestion.'
},
{
title: 'Run in CI/CD',
body: 'Use the same commands in local scripts, Docker containers, and GitHub Actions workflows.'
title: 'GitHub Actions (Marketplace)',
body: 'Add `fernandotonon/QtMeshEditor@v1` to any workflow. Scan assets on every PR, convert formats, validate meshes — one line in your YAML.'
}
];

export const pipelineExamples = {
scan: `# Preview workflow (actively evolving)\nqtmesh scan ./assets --fail-on error\nqtmesh scan ./assets --sarif reports/qtmesh.sarif --report reports/qtmesh.json`,
scan: `qtmesh scan ./assets --fail-on error\nqtmesh scan ./assets --json --report reports/qtmesh.json`,
fix: `qtmesh fix model.fbx -o fixed.fbx\nqtmesh fix model.fbx --all -o fixed.fbx`,
convert: `qtmesh convert model.fbx -o model.glb2\nqtmesh convert model.dae -o model.mesh`,
merge: `qtmesh anim base.fbx \\\n --merge walk.fbx run.fbx jump.fbx idle.fbx \\\n -o merged.fbx`,
docker: `docker run --rm --user "$(id -u):$(id -g)" -v $(pwd):/workspace \\\n ghcr.io/fernandotonon/qtmesh scan ./assets --fail-on error`,
githubAction: `- uses: fernandotonon/QtMeshEditor/.github/actions/qtmesh@c56db1ce3f2b29960cc1c9f82f5be98a51594138\n with:\n command: scan\n input-file: assets\n options: --fail-on error`,
githubAction: `# GitHub Actions Marketplace: fernandotonon/qtmesh\n- uses: fernandotonon/QtMeshEditor@v1\n with:\n command: scan\n input-file: ./assets\n options: --fail-on warning`,
scanFixConvert: `qtmesh scan ./assets --fail-on error\nqtmesh fix character.fbx --all -o character_fixed.fbx\nqtmesh convert character_fixed.fbx -o character.glb2`
};

Expand Down
Loading