diff --git a/.github/workflows/TestGroups.yml b/.github/workflows/TestGroups.yml new file mode 100644 index 0000000..88ae25c --- /dev/null +++ b/.github/workflows/TestGroups.yml @@ -0,0 +1,159 @@ +name: "Reusable Test Groups Workflow" + +on: + workflow_call: + inputs: + exclude: + description: "JSON array of test/ subdirectories to exclude from auto-discovery" + default: '["setup", "gpu"]' + required: false + type: string + julia-version: + description: "JSON array of Julia versions to test (e.g. '[\"min\", \"1\"]')" + default: '["min", "1"]' + required: false + type: string + os: + description: "JSON array of runner operating systems" + default: '["ubuntu-latest", "macos-latest", "windows-latest"]' + required: false + type: string + fast: + description: "Collapse matrix to ubuntu-latest + julia 1, and append --fast to test args" + default: false + required: false + type: boolean + nthreads: + description: "Number of Julia threads" + default: 2 + required: false + type: number + timeout-minutes: + description: "Maximum time per job in minutes" + default: 60 + required: false + type: number + localregistry: + description: "Newline-separated list of local registry URLs to add before building" + default: "" + required: false + type: string + cache: + description: "Enable julia-actions/cache" + default: true + required: false + type: boolean + buildpkg: + description: "Enable julia-actions/julia-buildpkg" + default: true + required: false + type: boolean + coverage: + description: "Collect and upload coverage (restricted to ubuntu-latest + julia 1)" + default: true + required: false + type: boolean + coverage-directories: + description: "Comma-separated directories for julia-processcoverage" + default: "src,ext" + required: false + type: string + secrets: + CODECOV_TOKEN: + required: false + +jobs: + setup: + name: "Discover test groups" + runs-on: ubuntu-latest + outputs: + groups: ${{ steps.discover.outputs.groups }} + versions: ${{ steps.discover.outputs.versions }} + systems: ${{ steps.discover.outputs.systems }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Discover test groups + id: discover + shell: bash + run: | + GROUPS=$(find test -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \ + | sort \ + | jq -R -s -c --argjson exc '${{ inputs.exclude }}' \ + 'split("\n")[:-1] | map(select(. as $g | $exc | index($g) | not))') + echo "groups=$GROUPS" >> "$GITHUB_OUTPUT" + + if [[ "${{ inputs.fast }}" == "true" ]]; then + echo 'versions=["1"]' >> "$GITHUB_OUTPUT" + echo 'systems=["ubuntu-latest"]' >> "$GITHUB_OUTPUT" + else + echo 'versions=${{ inputs.julia-version }}' >> "$GITHUB_OUTPUT" + echo 'systems=${{ inputs.os }}' >> "$GITHUB_OUTPUT" + fi + + test: + needs: setup + name: "${{ matrix.group }} (${{ matrix.version }}, ${{ matrix.os }})" + runs-on: ${{ matrix.os }} + timeout-minutes: ${{ inputs.timeout-minutes }} + strategy: + fail-fast: false + matrix: + group: ${{ fromJSON(needs.setup.outputs.groups) }} + version: ${{ fromJSON(needs.setup.outputs.versions) }} + os: ${{ fromJSON(needs.setup.outputs.systems) }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: "Setup Julia ${{ matrix.version }}" + uses: julia-actions/setup-julia@v3 + with: + version: "${{ matrix.version }}" + + - name: Restore cache + uses: julia-actions/cache@v3 + if: inputs.cache + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build package + uses: julia-actions/julia-buildpkg@v1 + if: inputs.buildpkg + with: + localregistry: "${{ inputs.localregistry }}" + + - name: "Run ${{ matrix.group }} tests" + uses: julia-actions/julia-runtest@v1 + with: + test_args: "${{ matrix.group }}${{ inputs.fast && ' --fast' || '' }}" + coverage: "${{ inputs.coverage && matrix.os == 'ubuntu-latest' && matrix.version == '1' }}" + env: + JULIA_NUM_THREADS: "${{ inputs.nthreads }}" + + - name: Process coverage + uses: julia-actions/julia-processcoverage@v1 + if: inputs.coverage && matrix.os == 'ubuntu-latest' && matrix.version == '1' + with: + directories: "${{ inputs.coverage-directories }}" + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v6 + if: inputs.coverage && matrix.os == 'ubuntu-latest' && matrix.version == '1' + with: + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + + ci-success: + name: ci-success + needs: test + if: always() + runs-on: ubuntu-latest + steps: + - name: Check all tests passed + run: | + if [[ "${{ contains(needs.test.result, 'failure') || contains(needs.test.result, 'cancelled') }}" == "true" ]]; then + exit 1 + fi diff --git a/README.md b/README.md index 33c7851..7deaf88 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,99 @@ -# ITensorActions +# QuantumKitHubActions -Shared workflows for the ITensors Julia packages +Shared workflows for the QuantumKitHub Julia packages + +## Test Groups + +The TestGroups workflow auto-discovers test groups from subdirectories of `test/` and runs them in parallel across a matrix of Julia versions and operating systems. Each subdirectory becomes a separate parallel job; groups are passed to the test suite via `test_args` and dispatched by [ParallelTestRunner.jl](https://github.com/QuantumKitHub/ParallelTestRunner.jl). + +### Test directory structure + +Organize your `test/` directory with one subfolder per test group. Place shared setup code in a `test/setup/` directory (excluded by default): + +``` +test/ +├── setup/ # shared utilities, excluded from test groups +├── core/ +│ └── runtests.jl +├── extensions/ +│ └── runtests.jl +└── runtests.jl # ParallelTestRunner.jl entry point +``` + +A minimal `test/runtests.jl` using ParallelTestRunner.jl: + +```julia +using ParallelTestRunner +ParallelTestRunner.runtests() +``` + +### Example workflow + +```yaml +name: "Tests" + +on: + push: + branches: + - main + tags: '*' + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +jobs: + tests: + uses: "QuantumKitHub/QuantumKitHubActions/.github/workflows/TestGroups.yml@main" + with: + fast: ${{ github.event.pull_request.draft == true }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} +``` + +### Branch protection + +Add `ci-success` as a required status check in your repository's branch protection rules. This single stable check name reflects the combined result of all parallel test jobs regardless of how many groups exist. + +### Inputs + +| Input | Type | Default | Description | +|---|---|---|---| +| `exclude` | string | `'["setup", "gpu"]'` | JSON array of `test/` subdirectory names to exclude from discovery | +| `julia-version` | string | `'["min", "1"]'` | JSON array of Julia versions to test | +| `os` | string | `'["ubuntu-latest", "macos-latest", "windows-latest"]'` | JSON array of runner OSes | +| `fast` | boolean | `false` | Collapse matrix to `ubuntu-latest` + julia `1`, append `--fast` to test args | +| `nthreads` | number | `2` | Julia thread count per job | +| `timeout-minutes` | number | `60` | Per-job timeout | +| `localregistry` | string | `""` | Newline-separated local registry URLs | +| `cache` | boolean | `true` | Enable `julia-actions/cache` | +| `buildpkg` | boolean | `true` | Enable `julia-actions/julia-buildpkg` | +| `coverage` | boolean | `true` | Collect and upload coverage (only on `ubuntu-latest` + julia `1`) | +| `coverage-directories` | string | `"src,ext"` | Directories for `julia-processcoverage` | + +**Secrets:** `CODECOV_TOKEN` (optional, only needed when `coverage: true`) + +### Fast path + +When `fast: true`, the matrix collapses to a single OS and Julia version and `--fast` is appended to each group's test args. Wire it to draft PR detection for quick feedback during development: + +```yaml +with: + fast: ${{ github.event.pull_request.draft == true }} +``` + +ParallelTestRunner.jl passes `--fast` through to individual test files, which can use it to skip slow or expensive tests. + +### Excluding folders + +Override `exclude` to control which subdirectories are skipped: + +```yaml +with: + exclude: '["setup", "gpu", "cuda"]' +``` ## Tests