From 5a8ef9be1c3632cd198b2f6cf513cb76a6679441 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sat, 7 Mar 2026 21:38:53 +0100 Subject: [PATCH 1/7] ci: add self-review workflow using reviewforge Adds automated PR review using AxeForging/reviewforge with Gemini AI to provide code review feedback on pull requests. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/review.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/review.yml 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 From 190ab4b4d41120559e3c12140eef9ae74edfc215 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sat, 7 Mar 2026 21:41:29 +0100 Subject: [PATCH 2/7] fix: allow doc/ directory and image files in structlint config The banner.png in doc/ was causing self-validation to fail because: - Only docs/** was allowed, not doc/** - Image file extensions (png, jpg, svg) were not in allowed patterns Co-Authored-By: Claude Opus 4.6 --- .structlint.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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*" From 6ee83ce72db8c863fa31c3339b8d945392ee8c01 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sat, 7 Mar 2026 21:49:08 +0100 Subject: [PATCH 3/7] feat: add GitHub Action and reusable workflow for structlint - Add action.yml composite action that downloads pre-built binary and runs validation (no Go setup required for consumers) - Add reusable workflow (.github/workflows/structlint.yml) for org-wide standardized validation via workflow_call - Update README with GitHub Action, reusable workflow, and input docs Co-Authored-By: Claude Opus 4.6 --- .github/workflows/structlint.yml | 55 ++++++++++++++++++ README.md | 97 +++++++++++++++++++++++++++++++- action.yml | 75 ++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/structlint.yml create mode 100644 action.yml diff --git a/.github/workflows/structlint.yml b/.github/workflows/structlint.yml new file mode 100644 index 0000000..fef768f --- /dev/null +++ b/.github/workflows/structlint.yml @@ -0,0 +1,55 @@ +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 + +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 }} + + - 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/README.md b/README.md index 34f4d9d..8aee169 100644 --- a/README.md +++ b/README.md @@ -459,12 +459,107 @@ 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 +``` + +
+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)_ | + +
+
-GitHub Actions +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 +``` + +
+ +### 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` | + +
+ +
+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..7b5ecc8 --- /dev/null +++ b/action.yml @@ -0,0 +1,75 @@ +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 + +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 structlint.tar.gz + tar -xzf structlint.tar.gz + chmod +x structlint + echo "structlint ${VERSION} installed successfully" + + - name: Run StructLint + shell: bash + run: | + ARGS="validate --config ${{ inputs.config }}" + + if [ "${{ inputs.path }}" != "." ]; then + ARGS="${ARGS} --path ${{ inputs.path }}" + fi + + if [ -n "${{ inputs.json-output }}" ]; then + ARGS="${ARGS} --json-output ${{ inputs.json-output }}" + fi + + if [ "${{ inputs.log-level }}" != "info" ]; then + ARGS="${ARGS} --log-level ${{ inputs.log-level }}" + fi + + if [ "${{ inputs.silent }}" == "true" ]; then + ARGS="${ARGS} --silent" + fi + + ./structlint ${ARGS} From f3bd6425f206d0d18f4eac7a5e6a88d77e4109e1 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sat, 7 Mar 2026 21:57:13 +0100 Subject: [PATCH 4/7] feat: add PR comment and job summary support to structlint action - Action now generates GitHub Actions Job Summary with validation results - Add comment-on-pr input to post/update results as a PR comment - Comments are upserted (updated on push, not duplicated) - Add validate.yml workflow for self-testing the action on PRs - Update reusable workflow with comment-on-pr support - Update README with PR comment documentation and examples Co-Authored-By: Claude Opus 4.6 --- .github/workflows/structlint.yml | 14 ++++ .github/workflows/validate.yml | 22 +++++++ README.md | 34 ++++++++++ action.yml | 109 ++++++++++++++++++++++++++++++- 4 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/structlint.yml b/.github/workflows/structlint.yml index fef768f..eee1d7b 100644 --- a/.github/workflows/structlint.yml +++ b/.github/workflows/structlint.yml @@ -31,6 +31,18 @@ on: 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: @@ -46,6 +58,8 @@ jobs: 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 != '' }} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..3f93efd --- /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: AxeForging/structlint@main + with: + config: .structlint.yaml + comment-on-pr: "true" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 8aee169..4673698 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,8 @@ jobs: | `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)_ |
@@ -509,6 +511,37 @@ jobs: +
+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: @@ -537,6 +570,7 @@ jobs: | `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` | diff --git a/action.yml b/action.yml index 7b5ecc8..dc50e16 100644 --- a/action.yml +++ b/action.yml @@ -24,6 +24,13 @@ inputs: 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" @@ -52,16 +59,17 @@ runs: echo "structlint ${VERSION} installed successfully" - name: Run StructLint + id: validate shell: bash run: | - ARGS="validate --config ${{ inputs.config }}" + ARGS="validate --config ${{ inputs.config }} --json-output /tmp/structlint-report.json" if [ "${{ inputs.path }}" != "." ]; then ARGS="${ARGS} --path ${{ inputs.path }}" fi if [ -n "${{ inputs.json-output }}" ]; then - ARGS="${ARGS} --json-output ${{ inputs.json-output }}" + ARGS="${ARGS}" fi if [ "${{ inputs.log-level }}" != "info" ]; then @@ -72,4 +80,99 @@ runs: ARGS="${ARGS} --silent" fi - ./structlint ${ARGS} + EXIT_CODE=0 + ./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 + MD=$(cat <> "$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="${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 }} From 8678f0e7ac815bf3714b03272094367bd1d12677 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sat, 7 Mar 2026 22:01:26 +0100 Subject: [PATCH 5/7] fix: use local action reference and fix markdown formatting - Use `uses: ./` in validate.yml so the action works before merging to main (standard pattern for self-testing GitHub Actions) - Fix markdown heredoc indentation that would render as code blocks - Remove dead code block for json-output arg Co-Authored-By: Claude Opus 4.6 --- .github/workflows/validate.yml | 2 +- action.yml | 40 ++++++++++++++++------------------ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 3f93efd..89db374 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: AxeForging/structlint@main + - uses: ./ with: config: .structlint.yaml comment-on-pr: "true" diff --git a/action.yml b/action.yml index dc50e16..2966077 100644 --- a/action.yml +++ b/action.yml @@ -68,10 +68,6 @@ runs: ARGS="${ARGS} --path ${{ inputs.path }}" fi - if [ -n "${{ inputs.json-output }}" ]; then - ARGS="${ARGS}" - fi - if [ "${{ inputs.log-level }}" != "info" ]; then ARGS="${ARGS} --log-level ${{ inputs.log-level }}" fi @@ -112,27 +108,29 @@ runs: fi # Build markdown - MD=$(cat < /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[]? | "- `\(.)`")\n"' "$REPORT") - MD="${MD} - -
- Violation Details - - ${VIOLATIONS} - -
" + { + 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 From 601694d0851de2ddddfa71e58f34fd8d87e98a87 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sat, 7 Mar 2026 22:02:16 +0100 Subject: [PATCH 6/7] fix: download structlint binary to /tmp to avoid self-scan The binary and tarball were downloaded into the workspace directory, causing structlint to flag them as violations. Moving to /tmp keeps them out of the scan path. Co-Authored-By: Claude Opus 4.6 --- action.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/action.yml b/action.yml index 2966077..6c4a5de 100644 --- a/action.yml +++ b/action.yml @@ -53,9 +53,9 @@ runs: URL="https://github.com/AxeForging/structlint/releases/download/${VERSION}/structlint-${OS}-${ARCH}.tar.gz" echo "Downloading structlint ${VERSION} from ${URL}..." - curl -sSL "$URL" -o structlint.tar.gz - tar -xzf structlint.tar.gz - chmod +x structlint + 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 @@ -77,7 +77,7 @@ runs: fi EXIT_CODE=0 - ./structlint ${ARGS} || EXIT_CODE=$? + /tmp/structlint ${ARGS} || EXIT_CODE=$? # Copy to user-specified path if requested if [ -n "${{ inputs.json-output }}" ]; then @@ -153,8 +153,7 @@ runs: PR_NUMBER="${{ github.event.pull_request.number }}" REPO="${{ github.repository }}" COMMENT_TAG="" - BODY="${COMMENT_TAG} - $(cat /tmp/structlint-comment.md)" + 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" \ From a97f856308bed129618c4a508c7e68da04092b64 Mon Sep 17 00:00:00 2001 From: Lucas Machado Date: Sat, 7 Mar 2026 22:03:13 +0100 Subject: [PATCH 7/7] fix: group violation examples under single header in PR comment The jq query was producing a separate header per example instead of listing all examples under one grouped header. Co-Authored-By: Claude Opus 4.6 --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 6c4a5de..404433c 100644 --- a/action.yml +++ b/action.yml @@ -120,7 +120,7 @@ runs: # Add violations detail if any if [ "$FAILURES" -gt 0 ]; then - VIOLATIONS=$(jq -r '.summary.violations[]? | "### ⚠️ \(.description) (\(.count))\n\(.examples[]? | "- `\(.)`")\n"' "$REPORT") + VIOLATIONS=$(jq -r '.summary.violations[]? | "### ⚠️ \(.description) (\(.count))\n" + ([.examples[]? | "- `\(.)`"] | join("\n")) + "\n"' "$REPORT") { echo "" echo "
"