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 }}