fix(platform): add .gitattributes for Windows CRLF line ending issues… #362
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: "CI" | |
| permissions: | |
| contents: read | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| schedule: | |
| - cron: '47 5 * * 0' | |
| env: | |
| python_version: "3.13" | |
| defaults: | |
| run: | |
| shell: 'bash --noprofile --norc -Eeuo pipefail {0}' | |
| jobs: | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Checkout the repository | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: 'false' | |
| - name: Bootstrap repository | |
| uses: ./.github/actions/bootstrap | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| python-version: ${{ env.python_version }} | |
| - name: Lint | |
| run: task -v lint | |
| test: | |
| name: Test | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout the repository | |
| uses: actions/checkout@v6 | |
| # Necessary for hooks to succeed during tests for commits/schedule | |
| if: github.event_name != 'pull_request' | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: 'false' | |
| - name: Checkout the repository | |
| uses: actions/checkout@v6 | |
| # Necessary for hooks to succeed during tests for PRs | |
| if: github.event_name == 'pull_request' | |
| with: | |
| ref: ${{ github.event.pull_request.head.ref }} | |
| fetch-depth: 0 | |
| persist-credentials: 'false' | |
| - name: Bootstrap repository | |
| uses: ./.github/actions/bootstrap | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| python-version: ${{ env.python_version }} | |
| - name: Validate the repo | |
| run: task -v validate | |
| - name: Install license compliance tool | |
| run: | | |
| mkdir "${RUNNER_TEMP}/bin" | |
| # Install grant via curl until official Docker image is available | |
| # See: https://github.com/anchore/grant/issues/222 | |
| curl -sSfL https://raw.githubusercontent.com/anchore/grant/main/install.sh | sh -s -- -b "${RUNNER_TEMP}/bin" | |
| chmod +x "${RUNNER_TEMP}/bin/grant" | |
| echo "${RUNNER_TEMP}/bin" | tee -a "${GITHUB_PATH}" | |
| - name: Run the tests | |
| run: task -v test | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Run SBOM generation | |
| run: task -v sbom | |
| - name: Upload SBOM artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: sbom-files | |
| path: | | |
| sbom.*.json | |
| if-no-files-found: error | |
| - name: Check license compliance | |
| run: task -v license-check | |
| - name: Upload license check results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: license-check-results | |
| path: license-check.json | |
| if-no-files-found: error | |
| - name: Run vulnerability scan | |
| run: task -v vulnscan | |
| - name: Upload vulnerability scan results | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: vuln-scan-results | |
| path: vulns.json | |
| if-no-files-found: error | |
| windows-smoke-test: | |
| name: Windows Smoke Test | |
| runs-on: windows-latest | |
| steps: | |
| - name: Setup uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: ${{ env.python_version }} | |
| enable-cache: false | |
| ignore-empty-workdir: true | |
| - name: Install Task | |
| uses: go-task/setup-task@v2 | |
| with: | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate project from template | |
| shell: bash | |
| env: | |
| RUN_POST_HOOK: 'true' | |
| SKIP_GIT_PUSH: 'true' | |
| TEMPLATE_REPO: ${{ github.repository }} | |
| TEMPLATE_REF: ${{ github.head_ref || github.ref_name }} | |
| run: | | |
| git config --global user.name "CI Automation" | |
| git config --global user.email "ci@zenable.io" | |
| # Use the same command as README.md (gh: syntax). On Windows, if the | |
| # default branch still has NTFS-illegal paths (pre-merge), the gh: clone | |
| # fails because cookiecutter clones main first. Fall back to a local | |
| # clone in that case. After merge, the gh: command succeeds directly. | |
| uvx --with gitpython cookiecutter \ | |
| "gh:${TEMPLATE_REPO}" \ | |
| --checkout "${TEMPLATE_REF}" \ | |
| --no-input project_name="ci-test-project" \ | |
| --output-dir "$RUNNER_TEMP" \ | |
| || { | |
| echo "::warning::gh: clone failed (expected pre-merge on Windows). Using local clone." | |
| template_dir="$RUNNER_TEMP/template-repo" | |
| git clone --no-checkout "https://github.com/${TEMPLATE_REPO}.git" "$template_dir" | |
| git -C "$template_dir" checkout "${TEMPLATE_REF}" | |
| uvx --with gitpython cookiecutter "$template_dir" \ | |
| --no-input project_name="ci-test-project" \ | |
| --output-dir "$RUNNER_TEMP" | |
| } | |
| - name: Verify generated project | |
| shell: pwsh | |
| run: | | |
| $project = Join-Path $env:RUNNER_TEMP "ci-test-project" | |
| # Verify the project directory was created | |
| if (-not (Test-Path $project)) { | |
| Write-Error "Project directory not found at $project" | |
| exit 1 | |
| } | |
| # Verify key files exist | |
| $requiredFiles = @( | |
| "pyproject.toml", | |
| "Taskfile.yml", | |
| "Dockerfile", | |
| "CLAUDE.md", | |
| ".github/project.yml", | |
| ".github/workflows/ci.yml" | |
| ) | |
| foreach ($file in $requiredFiles) { | |
| $filePath = Join-Path $project $file | |
| if (-not (Test-Path $filePath)) { | |
| Write-Error "Required file missing: $file" | |
| exit 1 | |
| } | |
| } | |
| # Verify no unrendered cookiecutter variables remain | |
| $pattern = '\{\{\s*cookiecutter\.' | |
| $matches = Get-ChildItem -Path $project -Recurse -File -Exclude '.git' | | |
| Where-Object { $_.FullName -notmatch '[\\/]\.git[\\/]' } | | |
| Select-String -Pattern $pattern | |
| if ($matches) { | |
| Write-Error "Unrendered cookiecutter variables found:" | |
| $matches | ForEach-Object { Write-Error $_.ToString() } | |
| exit 1 | |
| } | |
| # Verify git repo was initialized and has a commit | |
| $gitDir = Join-Path $project ".git" | |
| if (-not (Test-Path $gitDir)) { | |
| Write-Error "Git repository not initialized" | |
| exit 1 | |
| } | |
| Push-Location $project | |
| $commitCount = git rev-list --count HEAD 2>$null | |
| Pop-Location | |
| if ($commitCount -lt 1) { | |
| Write-Error "No commits found in generated project" | |
| exit 1 | |
| } | |
| Write-Host "Windows smoke test passed: project generated and verified successfully" | |
| - name: Verify shell scripts have LF line endings | |
| shell: pwsh | |
| run: | | |
| $project = Join-Path $env:RUNNER_TEMP "ci-test-project" | |
| $failed = $false | |
| # Check all .sh files for CRLF | |
| Get-ChildItem -Path $project -Recurse -Filter "*.sh" | ForEach-Object { | |
| $bytes = [System.IO.File]::ReadAllBytes($_.FullName) | |
| $content = [System.Text.Encoding]::UTF8.GetString($bytes) | |
| if ($content -match "`r`n") { | |
| Write-Error "$($_.Name) has CRLF line endings - this breaks bash on Windows" | |
| $failed = $true | |
| } | |
| } | |
| # Check Dockerfile | |
| $dockerfile = Join-Path $project "Dockerfile" | |
| if (Test-Path $dockerfile) { | |
| $bytes = [System.IO.File]::ReadAllBytes($dockerfile) | |
| $content = [System.Text.Encoding]::UTF8.GetString($bytes) | |
| if ($content -match "`r`n") { | |
| Write-Error "Dockerfile has CRLF line endings - this breaks Docker builds" | |
| $failed = $true | |
| } | |
| } | |
| if ($failed) { exit 1 } | |
| Write-Host "All shell scripts and Dockerfile have correct LF line endings" | |
| - name: Setup WSL with Docker | |
| shell: bash | |
| run: | | |
| wsl --install -d Ubuntu-24.04 --no-launch | |
| wsl -d Ubuntu-24.04 -u root -- bash -ec " | |
| apt-get update -qq | |
| apt-get install -y -qq curl ca-certificates >/dev/null | |
| curl -fsSL https://get.docker.com | sh -s -- --quiet | |
| service docker start | |
| " | |
| # Create docker wrappers that route to WSL's Docker. | |
| # - .bat for Task's mvdan/sh (Go's exec.LookPath needs a Windows extension) | |
| # - bash script for Git Bash steps | |
| mkdir -p "$HOME/bin" | |
| printf '@wsl -d Ubuntu-24.04 -u root -- docker %%*\r\n' > "$HOME/bin/docker.bat" | |
| cat > "$HOME/bin/docker" << 'WRAPPER' | |
| #!/bin/bash | |
| exec wsl -d Ubuntu-24.04 -u root -- docker "$@" | |
| WRAPPER | |
| chmod +x "$HOME/bin/docker" | |
| echo "$HOME/bin" >> "$GITHUB_PATH" | |
| - name: Initialize generated project | |
| shell: bash | |
| run: | | |
| cd "$RUNNER_TEMP/ci-test-project" | |
| task -v init | |
| - name: Run unit tests | |
| shell: bash | |
| # Integration tests require Docker (Linux images) which is not | |
| # available on Windows runners; those are covered by the Linux CI job. | |
| run: | | |
| cd "$RUNNER_TEMP/ci-test-project" | |
| task -v unit-test | |
| - name: Build Docker image | |
| shell: bash | |
| run: | | |
| cd "$RUNNER_TEMP/ci-test-project" | |
| task -v build | |
| - name: Verify Docker image | |
| shell: bash | |
| run: | | |
| docker run --rm zenable-io/ci-test-project:latest --version | |
| docker run --rm zenable-io/ci-test-project:latest --help | |
| - name: Verify zenable CLI | |
| shell: bash | |
| run: | | |
| export PATH="$HOME/.zenable/bin:$PATH" | |
| zenable version | |
| finalizer: | |
| # This gives us something to set as required in the repo settings. Some projects use dynamic fan-outs using matrix strategies and the fromJSON function, so | |
| # you can't hard-code what _should_ run vs not. Having a finalizer simplifies that so you can just check that the finalizer succeeded, and if so, your | |
| # requirements have been met | |
| # Example: https://x.com/JonZeolla/status/1877344137713766516 | |
| name: Finalize the pipeline | |
| runs-on: ubuntu-24.04 | |
| # Keep this aligned with the above jobs | |
| needs: [lint, test, windows-smoke-test] | |
| if: always() # Ensure it runs even if "needs" fails or is cancelled | |
| steps: | |
| - name: Check for failed or cancelled jobs | |
| run: | | |
| if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" || | |
| "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | |
| echo "One or more required jobs failed or was cancelled. Marking finalizer as failed." | |
| exit 1 | |
| fi | |
| - name: Checkout the repository | |
| uses: actions/checkout@v6 | |
| - name: Scan workflow logs for warnings and errors | |
| run: scripts/scan_workflow_logs.sh ${{ github.run_id }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Finalize | |
| run: echo "Pipeline complete!" |