diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml
new file mode 100644
index 0000000..8ef5e34
--- /dev/null
+++ b/.github/workflows/review.yml
@@ -0,0 +1,26 @@
+name: Self-Review
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ branches:
+ - main
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ review:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: AxeForging/reviewforge@main
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ AI_PROVIDER: gemini
+ AI_MODEL: gemini-2.5-flash
+ AI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ SHOW_TOKEN_USAGE: true
+ INCREMENTAL: false
+ PERSONA: ""
+ REVIEW_RULES: concise
diff --git a/.github/workflows/structlint.yml b/.github/workflows/structlint.yml
new file mode 100644
index 0000000..eee1d7b
--- /dev/null
+++ b/.github/workflows/structlint.yml
@@ -0,0 +1,69 @@
+name: StructLint
+
+on:
+ workflow_call:
+ inputs:
+ config:
+ description: "Path to .structlint.yaml config file"
+ type: string
+ default: ".structlint.yaml"
+ path:
+ description: "Directory to validate"
+ type: string
+ default: "."
+ json-output:
+ description: "Path to write JSON report"
+ type: string
+ default: ""
+ log-level:
+ description: "Log level: debug, info, warn, error"
+ type: string
+ default: "info"
+ silent:
+ description: "Silent mode - only exit code"
+ type: boolean
+ default: false
+ version:
+ description: "Version of structlint to use"
+ type: string
+ default: ""
+ upload-report:
+ description: "Upload JSON report as artifact"
+ type: boolean
+ default: false
+ comment-on-pr:
+ description: "Post validation results as a PR comment"
+ type: boolean
+ default: false
+ secrets:
+ github_token:
+ description: "GitHub token for PR comments (required if comment-on-pr is true)"
+ required: false
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: AxeForging/structlint@main
+ with:
+ config: ${{ inputs.config }}
+ path: ${{ inputs.path }}
+ json-output: ${{ inputs.json-output }}
+ log-level: ${{ inputs.log-level }}
+ silent: ${{ inputs.silent }}
+ version: ${{ inputs.version }}
+ comment-on-pr: ${{ inputs.comment-on-pr }}
+ GITHUB_TOKEN: ${{ secrets.github_token || github.token }}
+
+ - name: Upload report
+ if: ${{ inputs.upload-report && inputs.json-output != '' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: structlint-report
+ path: ${{ inputs.json-output }}
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..89db374
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,22 @@
+name: Validate Structure
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ branches:
+ - main
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ structlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./
+ with:
+ config: .structlint.yaml
+ comment-on-pr: "true"
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.structlint.yaml b/.structlint.yaml
index a6c7030..cd27984 100644
--- a/.structlint.yaml
+++ b/.structlint.yaml
@@ -7,6 +7,7 @@ dir_structure:
- "cmd/**" # Command entry points
- "internal/**" # Internal packages
- "test/**" # Test files
+ - "doc/**" # Documentation
- "docs/**" # Documentation
- "scripts/**" # Build and utility scripts
- "bin/**" # Built binaries
@@ -41,6 +42,9 @@ file_naming_pattern:
# Documentation
- "*.md"
- "*.txt"
+ - "*.png"
+ - "*.jpg"
+ - "*.svg"
- "README*"
- "LICENSE*"
- "CHANGELOG*"
diff --git a/README.md b/README.md
index 34f4d9d..4673698 100644
--- a/README.md
+++ b/README.md
@@ -459,12 +459,141 @@ structlint completion fish > ~/.config/fish/completions/structlint.fish
## CI/CD Integration
+### GitHub Action
+
+The simplest way to use structlint in CI — no Go setup required:
+
+```yaml
+name: Validate Structure
+on: [push, pull_request]
+
+jobs:
+ structlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: AxeForging/structlint@main
+ with:
+ config: .structlint.yaml
+```
+
-GitHub Actions
+Action Inputs
+
+| Input | Description | Default |
+|-------|-------------|---------|
+| `config` | Path to config file | `.structlint.yaml` |
+| `path` | Directory to validate | `.` |
+| `json-output` | Path for JSON report | _(none)_ |
+| `log-level` | `debug`, `info`, `warn`, `error` | `info` |
+| `silent` | Exit code only, no output | `false` |
+| `version` | Structlint version to use | _(latest)_ |
+| `comment-on-pr` | Post results as a PR comment | `false` |
+| `GITHUB_TOKEN` | GitHub token (required for PR comments) | _(none)_ |
+
+
+
+
+Action with JSON Report
+
+```yaml
+- uses: AxeForging/structlint@main
+ with:
+ config: .structlint.yaml
+ json-output: structlint-report.json
+
+- uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: structlint-report
+ path: structlint-report.json
+```
+
+
+
+
+Action with PR Comments
+
+Posts validation results directly on your pull request and writes a GitHub Actions Job Summary:
```yaml
name: Validate Structure
+on:
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+ pull-requests: write
+jobs:
+ structlint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: AxeForging/structlint@main
+ with:
+ config: .structlint.yaml
+ comment-on-pr: "true"
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+```
+
+The comment is updated on each push (not duplicated), and includes a collapsible violation details section.
+
+
+
+### Reusable Workflow
+
+For organizations that want a standardized setup across all repos:
+
+```yaml
+# In your repo's .github/workflows/structlint.yml
+name: Validate Structure
+on: [push, pull_request]
+
+jobs:
+ structlint:
+ uses: AxeForging/structlint/.github/workflows/structlint.yml@main
+ with:
+ config: .structlint.yaml
+```
+
+
+Reusable Workflow Inputs
+
+| Input | Type | Description | Default |
+|-------|------|-------------|---------|
+| `config` | `string` | Path to config file | `.structlint.yaml` |
+| `path` | `string` | Directory to validate | `.` |
+| `json-output` | `string` | Path for JSON report | _(none)_ |
+| `log-level` | `string` | `debug`, `info`, `warn`, `error` | `info` |
+| `silent` | `boolean` | Exit code only | `false` |
+| `version` | `string` | Structlint version | _(latest)_ |
+| `upload-report` | `boolean` | Upload JSON report as artifact | `false` |
+| `comment-on-pr` | `boolean` | Post results as a PR comment | `false` |
+
+
+
+
+Reusable Workflow with Report Upload
+
+```yaml
+jobs:
+ structlint:
+ uses: AxeForging/structlint/.github/workflows/structlint.yml@main
+ with:
+ config: .structlint.yaml
+ json-output: report.json
+ upload-report: true
+```
+
+
+
+
+Manual GitHub Actions (without the action)
+
+```yaml
+name: Validate Structure
on: [push, pull_request]
jobs:
diff --git a/action.yml b/action.yml
new file mode 100644
index 0000000..404433c
--- /dev/null
+++ b/action.yml
@@ -0,0 +1,175 @@
+name: "StructLint"
+description: "Validate and enforce directory structure and file naming patterns"
+author: "AxeForging"
+inputs:
+ config:
+ description: "Path to .structlint.yaml config file"
+ required: false
+ default: ".structlint.yaml"
+ path:
+ description: "Directory to validate"
+ required: false
+ default: "."
+ json-output:
+ description: "Path to write JSON report (empty = no report)"
+ required: false
+ log-level:
+ description: "Log level: debug, info, warn, error"
+ required: false
+ default: "info"
+ silent:
+ description: "Silent mode - only exit code, no output"
+ required: false
+ default: "false"
+ version:
+ description: "Version of structlint to use (e.g. v0.2.0). Defaults to latest."
+ required: false
+ comment-on-pr:
+ description: "Post validation results as a PR comment (requires pull_request event and GITHUB_TOKEN)"
+ required: false
+ default: "false"
+ GITHUB_TOKEN:
+ description: "GitHub token for PR comments (required if comment-on-pr is true)"
+ required: false
+
+runs:
+ using: "composite"
+ steps:
+ - name: Download StructLint
+ shell: bash
+ run: |
+ OS="linux"
+ ARCH="amd64"
+ if [[ "${{ runner.os }}" == "macOS" ]]; then OS="darwin"; fi
+ if [[ "${{ runner.arch }}" == "ARM64" ]]; then ARCH="arm64"; fi
+
+ VERSION="${{ inputs.version }}"
+ if [ -z "$VERSION" ]; then
+ VERSION="${{ github.action_ref }}"
+ fi
+ if [ -z "$VERSION" ] || [ "$VERSION" == "main" ]; then
+ VERSION="v0.2.0"
+ fi
+
+ URL="https://github.com/AxeForging/structlint/releases/download/${VERSION}/structlint-${OS}-${ARCH}.tar.gz"
+ echo "Downloading structlint ${VERSION} from ${URL}..."
+ curl -sSL "$URL" -o /tmp/structlint.tar.gz
+ tar -xzf /tmp/structlint.tar.gz -C /tmp
+ chmod +x /tmp/structlint
+ echo "structlint ${VERSION} installed successfully"
+
+ - name: Run StructLint
+ id: validate
+ shell: bash
+ run: |
+ ARGS="validate --config ${{ inputs.config }} --json-output /tmp/structlint-report.json"
+
+ if [ "${{ inputs.path }}" != "." ]; then
+ ARGS="${ARGS} --path ${{ inputs.path }}"
+ fi
+
+ if [ "${{ inputs.log-level }}" != "info" ]; then
+ ARGS="${ARGS} --log-level ${{ inputs.log-level }}"
+ fi
+
+ if [ "${{ inputs.silent }}" == "true" ]; then
+ ARGS="${ARGS} --silent"
+ fi
+
+ EXIT_CODE=0
+ /tmp/structlint ${ARGS} || EXIT_CODE=$?
+
+ # Copy to user-specified path if requested
+ if [ -n "${{ inputs.json-output }}" ]; then
+ cp /tmp/structlint-report.json "${{ inputs.json-output }}"
+ fi
+
+ echo "exit_code=${EXIT_CODE}" >> "$GITHUB_OUTPUT"
+
+ - name: Generate Summary
+ if: always()
+ shell: bash
+ run: |
+ REPORT="/tmp/structlint-report.json"
+ if [ ! -f "$REPORT" ]; then
+ echo "No report file found, skipping summary."
+ exit 0
+ fi
+
+ SUCCESSES=$(jq -r '.successes' "$REPORT")
+ FAILURES=$(jq -r '.failures' "$REPORT")
+
+ if [ "$FAILURES" -eq 0 ]; then
+ STATUS_ICON="✅"
+ STATUS_TEXT="All checks passed"
+ else
+ STATUS_ICON="❌"
+ STATUS_TEXT="${FAILURES} violation(s) found"
+ fi
+
+ # Build markdown
+ {
+ echo "## ${STATUS_ICON} StructLint Validation"
+ echo ""
+ echo "| Metric | Count |"
+ echo "|--------|-------|"
+ echo "| Checks passed | ${SUCCESSES} |"
+ echo "| Violations | ${FAILURES} |"
+ } > /tmp/structlint-md.tmp
+ MD=$(cat /tmp/structlint-md.tmp)
+
+ # Add violations detail if any
+ if [ "$FAILURES" -gt 0 ]; then
+ VIOLATIONS=$(jq -r '.summary.violations[]? | "### ⚠️ \(.description) (\(.count))\n" + ([.examples[]? | "- `\(.)`"] | join("\n")) + "\n"' "$REPORT")
+ {
+ echo ""
+ echo ""
+ echo "Violation Details
"
+ echo ""
+ echo "$VIOLATIONS"
+ echo ""
+ echo " "
+ } >> /tmp/structlint-md.tmp
+ MD=$(cat /tmp/structlint-md.tmp)
+ fi
+
+ # Write to job summary
+ echo "$MD" >> "$GITHUB_STEP_SUMMARY"
+
+ # Save for PR comment step
+ echo "$MD" > /tmp/structlint-comment.md
+
+ - name: Comment on PR
+ if: always() && inputs.comment-on-pr == 'true' && github.event_name == 'pull_request'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ inputs.GITHUB_TOKEN }}
+ run: |
+ if [ ! -f /tmp/structlint-comment.md ]; then
+ echo "No comment file found, skipping."
+ exit 0
+ fi
+
+ PR_NUMBER="${{ github.event.pull_request.number }}"
+ REPO="${{ github.repository }}"
+ COMMENT_TAG=""
+ BODY="$(printf '%s\n%s' "$COMMENT_TAG" "$(cat /tmp/structlint-comment.md)")"
+
+ # Check for existing structlint comment to update
+ EXISTING_COMMENT_ID=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
+ --jq ".[] | select(.body | startswith(\"${COMMENT_TAG}\")) | .id" 2>/dev/null | head -1)
+
+ if [ -n "$EXISTING_COMMENT_ID" ]; then
+ gh api "repos/${REPO}/issues/comments/${EXISTING_COMMENT_ID}" \
+ -X PATCH -f body="$BODY"
+ echo "Updated existing PR comment."
+ else
+ gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \
+ -X POST -f body="$BODY"
+ echo "Created new PR comment."
+ fi
+
+ - name: Exit with validation result
+ if: always()
+ shell: bash
+ run: exit ${{ steps.validate.outputs.exit_code }}