diff --git a/.github/actions/classify-pr/action.yml b/.github/actions/classify-pr/action.yml index 6dfe906..68d1eb3 100644 --- a/.github/actions/classify-pr/action.yml +++ b/.github/actions/classify-pr/action.yml @@ -1,9 +1,20 @@ -name: "Classify PR substance" -description: "Determines whether a PR touches substantive files or only CI/config (workflow-only) files. Outputs substantive=true|false." +name: "Classify PR scope" +description: "Reports whether a PR has changes that should trigger this caller's work, given a path scope. A file 'triggers' iff it matches the caller's scope and is not in the exclude list. Outputs triggers=true|false." +inputs: + paths: + description: "Newline-separated list of path prefixes / files defining positive scope. A file triggers iff it equals one of these paths or is inside one (i.e., starts with `/`). Empty means everything is in scope. Mutually exclusive with `exclude-paths`." + required: false + default: "" + exclude-paths: + description: "Newline-separated list of path prefixes / files defining negative scope. A file does NOT trigger if it equals one of these or is inside one. Default ignores `.gitignore` and `.pre-commit-config.yaml`. Override entirely (e.g. include the package's sibling subpackage and re-list defaults if you want them kept)." + required: false + default: | + .gitignore + .pre-commit-config.yaml outputs: - substantive: - description: "true if the PR touches substantive files; false if all changes are confined to .github/**, .pre-commit-config.yaml, .gitignore, or LICENSE." - value: ${{ steps.classify.outputs.substantive }} + triggers: + description: "true if the PR has at least one changed file that matches the configured scope; false otherwise." + value: ${{ steps.classify.outputs.triggers }} runs: using: "composite" steps: @@ -13,27 +24,82 @@ runs: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} + PATHS: ${{ inputs.paths }} + EXCLUDE_PATHS: ${{ inputs.exclude-paths }} run: | set -euo pipefail + # Normalize: treat whitespace-only inputs as empty. This matters + # because reusables that auto-append a value (e.g. `subdir`) into + # `paths` produce a list of all-empty lines when the appended + # value is also empty — bash's [ -n ] would otherwise misread that + # as "positive scope set", flipping the action into the wrong mode. + has_content() { + while IFS= read -r line; do + [ -n "$line" ] && return 0 + done <<< "$1" + return 1 + } + has_content "$PATHS" || PATHS="" + has_content "$EXCLUDE_PATHS" || EXCLUDE_PATHS="" + + if [ -n "$PATHS" ] && [ -n "$EXCLUDE_PATHS" ]; then + # If paths is set, exclude-paths is ignored (caller is using positive scope). + # Surface a warning rather than failing so the user notices. + echo "::warning::classify-pr: 'paths' and 'exclude-paths' are mutually exclusive; using 'paths'." + EXCLUDE_PATHS="" + fi if [ -z "${PR_NUMBER:-}" ]; then - echo "Not a pull_request event; defaulting to substantive=true." - echo "substantive=true" >> "$GITHUB_OUTPUT" + echo "Not a pull_request event; defaulting to triggers=true." + echo "triggers=true" >> "$GITHUB_OUTPUT" exit 0 fi files=$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename') if [ -z "$files" ]; then - echo "No files reported for PR #${PR_NUMBER}; defaulting to substantive=true." - echo "substantive=true" >> "$GITHUB_OUTPUT" + echo "No files reported for PR #${PR_NUMBER}; defaulting to triggers=true." + echo "triggers=true" >> "$GITHUB_OUTPUT" exit 0 fi echo "Changed files:" printf ' %s\n' $files - substantive=false + + # Helper: check if a file matches any entry in a newline-separated list. + # An entry matches iff the file equals the entry, or the file starts with `/`. + matches_list() { + local file=$1 + local list=$2 + while IFS= read -r entry; do + [ -z "$entry" ] && continue + if [ "$file" = "$entry" ] || [[ "$file" == "$entry"/* ]]; then + return 0 + fi + done <<< "$list" + return 1 + } + + if [ -n "$PATHS" ]; then + echo "Positive scope: any file matching one of the paths." + printf ' %s\n' $PATHS + else + echo "Default scope: any file outside the exclude list." + printf ' exclude: %s\n' $EXCLUDE_PATHS + fi + + triggers=false while IFS= read -r file; do - case "$file" in - .github/*|.pre-commit-config.yaml|.gitignore|LICENSE) ;; - *) substantive=true; break ;; - esac + if [ -n "$PATHS" ]; then + # Positive scope: file triggers iff it matches one of the listed paths. + if matches_list "$file" "$PATHS"; then + triggers=true + break + fi + else + # Negative scope: file triggers iff it is NOT in the exclude list. + if ! matches_list "$file" "$EXCLUDE_PATHS"; then + triggers=true + break + fi + fi done <<< "$files" - echo "substantive=${substantive}" - echo "substantive=${substantive}" >> "$GITHUB_OUTPUT" + + echo "triggers=${triggers}" + echo "triggers=${triggers}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/CheckCompatBounds.yml b/.github/workflows/CheckCompatBounds.yml index 02af007..d9de634 100644 --- a/.github/workflows/CheckCompatBounds.yml +++ b/.github/workflows/CheckCompatBounds.yml @@ -9,10 +9,15 @@ on: required: false type: string project: - description: "The value is passed to Julia's `--project` flag during buildpkg." + description: "Deprecated alias for `subdir`. Value passed to Julia's `--project` flag during buildpkg. When `subdir` is set, it takes precedence." default: '@.' required: false type: string + subdir: + description: "Subdirectory containing the package's Project.toml. When set, both buildpkg's `--project` and the check-compat-bounds workspace-root resolve to ``. Empty (default) uses repo root." + default: "" + required: false + type: string cache: description: "Use the julia-actions/cache action for caching." default: true @@ -74,10 +79,10 @@ jobs: if: "${{ inputs.buildpkg }}" with: localregistry: "${{ inputs.localregistry }}" - project: "${{ inputs.project }}" + project: "${{ inputs.subdir != '' && inputs.subdir || inputs.project }}" - name: "Check compat upper bounds" uses: ITensor/ITensorActions/.github/actions/check-compat-bounds@main with: - workspace-root: "${{ inputs.workspace-root }}" + workspace-root: "${{ inputs.subdir != '' && inputs.subdir || inputs.workspace-root }}" mode: "${{ inputs.mode }}" diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 9ab7dfe..cfa7809 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -14,6 +14,11 @@ on: default: "" required: false type: string + subdir: + description: "Subdirectory containing the package's Project.toml. When set, CompatHelper bumps `/Project.toml` plus `/{docs,examples,test}/Project.toml` if present. Empty (default) uses repo root." + default: "" + required: false + type: string jobs: compathelper: @@ -73,6 +78,7 @@ jobs: # (see https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow). GITHUB_TOKEN: ${{ secrets.COMPATHELPER_PAT || secrets.GITHUB_TOKEN }} LOCALREGISTRY: ${{ inputs.localregistry }} + SUBDIR: ${{ inputs.subdir }} TOKEN_OWNER: ${{ steps.token-owner.outputs.login }} run: | import CompatHelper @@ -106,7 +112,9 @@ jobs: push!(registries, Pkg.RegistrySpec(; url=registry_url, name=registry_name)) end end + subdir = get(ENV, "SUBDIR", "") subdirs = ["", "docs", "examples", "test"] + isempty(subdir) || (subdirs = joinpath.(subdir, subdirs)) token_owner = get(ENV, "TOKEN_OWNER", "") # If TOKEN_OWNER contains a JSON error body (happens when only GITHUB_TOKEN # is available, e.g. in private repos without COMPATHELPER_PAT), treat it as diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml index 77e7051..a513ec1 100644 --- a/.github/workflows/IntegrationTest.yml +++ b/.github/workflows/IntegrationTest.yml @@ -22,6 +22,23 @@ on: default: "" required: false type: string + subdir: + description: "Run the downstream tests against the package in this subdirectory." + default: "" + required: false + type: string + paths: + description: "Newline-separated list of path prefixes / files defining positive scope for the substantive-PR check. A file triggers IntegrationTest iff it equals one of these paths or starts with `/`. Empty means no positive-scope filter; the check falls back to `exclude-paths`. Mutually exclusive with `exclude-paths`." + default: "" + required: false + type: string + exclude-paths: + description: "Newline-separated list of path prefixes / files defining negative scope for the substantive-PR check. A file does NOT trigger IntegrationTest if it matches one of these. Default ignores `.gitignore` and `.pre-commit-config.yaml`." + default: | + .gitignore + .pre-commit-config.yaml + required: false + type: string run-on-draft: description: "When true, run integration tests even on draft PRs" default: false @@ -53,6 +70,21 @@ jobs: pkg: ${{ fromJSON(inputs.pkgs) }} steps: + - uses: ITensor/ITensorActions/.github/actions/classify-pr@main + id: classify + with: + # `subdir` (the in-tree package under test) is auto-appended + # to the positive scope so callers don't have to repeat it. + paths: | + ${{ inputs.subdir }} + ${{ inputs.paths }} + exclude-paths: ${{ inputs.exclude-paths }} + + - name: "Skip on out-of-scope PR" + if: "${{ steps.classify.outputs.triggers != 'true' }}" + run: | + echo "PR has no changes in this caller's scope; integration test skipped." + # Decide whether this matrix leg should run, and emit `skip=true|false`. # Skips when the entry is a URL that needs auth and the PR is from a # fork (no secrets are exposed to fork PRs under `pull_request:`). @@ -62,6 +94,7 @@ jobs: # `workflow_dispatch` (where secrets are in scope). - name: "Decide whether to run" id: gate + if: "${{ steps.classify.outputs.triggers == 'true' }}" env: PKG: ${{ matrix.pkg }} HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} @@ -93,7 +126,7 @@ jobs: echo "skip=false" >> "$GITHUB_OUTPUT" - name: "Record skipped dependency" id: skip_artifact - if: steps.gate.outputs.skip == 'true' + if: "${{ steps.classify.outputs.triggers == 'true' && steps.gate.outputs.skip == 'true' }}" env: PKG: ${{ matrix.pkg }} run: | @@ -103,22 +136,22 @@ jobs: echo "key=$key" >> "$GITHUB_OUTPUT" echo "path=$path" >> "$GITHUB_OUTPUT" - name: "Upload skipped-dependency marker" - if: steps.gate.outputs.skip == 'true' + if: "${{ steps.classify.outputs.triggers == 'true' && steps.gate.outputs.skip == 'true' }}" uses: actions/upload-artifact@v7 with: name: "integration-test-skip-${{ steps.skip_artifact.outputs.key }}" path: ${{ steps.skip_artifact.outputs.path }} - uses: actions/checkout@v6 - if: steps.gate.outputs.skip != 'true' + if: "${{ steps.classify.outputs.triggers == 'true' && steps.gate.outputs.skip != 'true' }}" with: ref: ${{ github.event.pull_request.head.sha || github.sha }} - uses: julia-actions/setup-julia@v3 - if: steps.gate.outputs.skip != 'true' + if: "${{ steps.classify.outputs.triggers == 'true' && steps.gate.outputs.skip != 'true' }}" with: version: ${{ inputs.julia-version }} arch: x64 - name: "Configure git authentication for private repository" - if: steps.gate.outputs.skip != 'true' + if: "${{ steps.classify.outputs.triggers == 'true' && steps.gate.outputs.skip != 'true' }}" env: TOKEN: ${{ secrets.INTEGRATIONTEST_PAT }} PKG: ${{ matrix.pkg }} @@ -129,12 +162,13 @@ jobs: fi fi - name: "Run the downstream tests" - if: steps.gate.outputs.skip != 'true' + if: "${{ steps.classify.outputs.triggers == 'true' && steps.gate.outputs.skip != 'true' }}" shell: julia --color=yes --project=downstream {0} env: PKG: ${{ matrix.pkg }} LOCALREGISTRY: ${{ inputs.localregistry }} EXTRA_DEV_PATHS: ${{ inputs.extra-dev-paths }} + PKG_DIR: ${{ inputs.subdir || '.' }} JULIA_PKG_SERVER_REGISTRY_PREFERENCE: eager run: | using Pkg @@ -148,7 +182,7 @@ jobs: Pkg.Registry.add(Pkg.RegistrySpec(; url=registry_url)) end end - dev_paths = [".", split(get(ENV, "EXTRA_DEV_PATHS", ""), "\n")...] + dev_paths = [get(ENV, "PKG_DIR", "."), split(get(ENV, "EXTRA_DEV_PATHS", ""), "\n")...] filter!(!isempty, dev_paths) dev_specs = [PackageSpec(; path) for path in dev_paths] pkg = get(ENV, "PKG", "") diff --git a/.github/workflows/Registrator.yml b/.github/workflows/Registrator.yml index cee6e20..3d5c6a0 100644 --- a/.github/workflows/Registrator.yml +++ b/.github/workflows/Registrator.yml @@ -18,6 +18,11 @@ on: default: "/register" required: false type: string + subdir: + description: "Subdirectory containing the package's Project.toml, relative to the repo root. Default empty means register the root package. Set to e.g. 'NDTensors' to register an in-tree subpackage; the workflow then reads `package//Project.toml` and posts `@JuliaRegistrator register subdir=` so JuliaRegistrator picks up the sub-package correctly." + default: "" + required: false + type: string secrets: # Only needed for the local-registry path (checkout + PR). Not needed for General. REGISTRATOR_PAT: @@ -83,6 +88,7 @@ jobs: id: meta env: OLD_REF: ${{ github.event.before }} + SUBDIR: ${{ inputs.subdir }} shell: julia --color=yes {0} run: | using TOML @@ -108,14 +114,17 @@ jobs: end let - new = TOML.parsefile("package/Project.toml") + subdir = get(ENV, "SUBDIR", "") + project_path = isempty(subdir) ? "Project.toml" : joinpath(subdir, "Project.toml") + + new = TOML.parsefile(joinpath("package", project_path)) name = get(new, "name", "") uuid = get(new, "uuid", "") newv_str = get(new, "version", "") - isempty(name) && error("Project.toml is missing name") - isempty(uuid) && error("Project.toml is missing uuid") - isempty(newv_str) && error("Project.toml is missing version") + isempty(name) && error("$project_path is missing name") + isempty(uuid) && error("$project_path is missing uuid") + isempty(newv_str) && error("$project_path is missing version") newv = parse_version(newv_str, "new") subject = replace(readchomp(`git -C package log -1 --pretty=%s HEAD`), ['\n','\r'] => ' ') @@ -133,11 +142,11 @@ jobs: oldv_str = "" if !isempty(old_ref) try - old_toml = readchomp(`git -C package show $old_ref:Project.toml`) + old_toml = readchomp(`git -C package show $old_ref:$project_path`) oldv_str = get(TOML.parse(old_toml), "version", "") catch err # Don't hard-fail on missing old Project.toml; just skip strict validation. - println(stderr, "Warning: could not read Project.toml at ref '$old_ref' (treating as no prior version): $err") + println(stderr, "Warning: could not read $project_path at ref '$old_ref' (treating as no prior version): $err") oldv_str = "" end end @@ -217,9 +226,14 @@ jobs: IS_BREAKING: ${{ steps.meta.outputs.is_breaking }} SUBJECT: ${{ steps.meta.outputs.subject }} NEW_VERSION: ${{ steps.meta.outputs.new_version }} + SUBDIR: ${{ inputs.subdir }} run: | { - echo "@JuliaRegistrator register" + if [ -n "$SUBDIR" ]; then + echo "@JuliaRegistrator register subdir=$SUBDIR" + else + echo "@JuliaRegistrator register" + fi if [ "$IS_BREAKING" = "true" ]; then echo "" echo "Release notes:" @@ -336,6 +350,8 @@ jobs: - name: "Update local registry" if: steps.meta.outputs.route == 'local' + env: + SUBDIR: ${{ inputs.subdir }} shell: julia --color=yes {0} run: | import Pkg @@ -344,7 +360,9 @@ jobs: uuid="89398ba2-070a-4b16-a995-9893c55d93cf", version="0.5.7")) using LocalRegistry - register("./package"; registry="./registry", commit=false, push=false) + subdir = get(ENV, "SUBDIR", "") + pkg_path = isempty(subdir) ? "./package" : joinpath("./package", subdir) + register(pkg_path; registry="./registry", commit=false, push=false) - name: "Create PR to registry" if: steps.meta.outputs.route == 'local' diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 4b8e92a..d78f1fb 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -13,8 +13,8 @@ on: required: false type: string project: - description: "The value is passed to Julia's `--project` flag" - default: '@.' + description: "Deprecated alias for `subdir`. Value passed to Julia's `--project` flag. Set `subdir` instead. Will be removed in a future major release." + default: "@." required: false type: string group: @@ -67,6 +67,28 @@ on: default: "yes" required: false type: string + test-args: + description: "Newline-separated list of arguments passed to `Pkg.test(; test_args=...)`. Surfaces julia-actions/julia-runtest's `test_args` input. Useful when `runtests.jl` selects test groups via ARGS rather than the GROUP env-var." + default: "" + required: false + type: string + subdir: + description: "Subdirectory containing the package's Project.toml. When set, Julia activates `` for buildpkg and runtest. Empty (default) activates the repo root." + default: "" + required: false + type: string + paths: + description: "Newline-separated list of path prefixes / files defining positive scope for the substantive-PR check. A file triggers tests iff it equals one of these paths or starts with `/`. Empty means no positive-scope filter; the check falls back to `exclude-paths`. Mutually exclusive with `exclude-paths`." + default: "" + required: false + type: string + exclude-paths: + description: "Newline-separated list of path prefixes / files defining negative scope for the substantive-PR check. A file does NOT trigger if it matches one of these. Default ignores `.gitignore` and `.pre-commit-config.yaml`. Override entirely (e.g. add a sibling subpackage and re-list defaults if you want them kept)." + default: | + .gitignore + .pre-commit-config.yaml + required: false + type: string continue-on-error: description: "Prevent the workflow run from failing if/when the job fails" required: false @@ -118,10 +140,26 @@ jobs: runs-on: "${{ inputs.self-hosted && 'self-hosted' || inputs.os }}" timeout-minutes: ${{ inputs.timeout-minutes }} steps: + - uses: ITensor/ITensorActions/.github/actions/classify-pr@main + id: classify + with: + # `subdir` (the package being tested) is auto-appended to + # the positive scope so callers don't have to repeat it. + paths: | + ${{ inputs.subdir }} + ${{ inputs.paths }} + exclude-paths: ${{ inputs.exclude-paths }} + + - name: "Skip on out-of-scope PR" + if: "${{ steps.classify.outputs.triggers != 'true' }}" + run: | + echo "PR has no changes in this caller's scope; tests skipped." + - uses: actions/checkout@v6 + if: "${{ steps.classify.outputs.triggers == 'true' }}" - name: "Install apt packages" - if: "${{ inputs.apt-packages != '' && runner.os == 'Linux' }}" + if: "${{ steps.classify.outputs.triggers == 'true' && inputs.apt-packages != '' && runner.os == 'Linux' }}" env: APT_PACKAGES: ${{ inputs.apt-packages }} run: | @@ -132,23 +170,25 @@ jobs: sudo apt-get install -y $APT_PACKAGES - name: "Setup Julia ${{ inputs.julia-version }}" + if: "${{ steps.classify.outputs.triggers == 'true' }}" uses: julia-actions/setup-julia@v3 with: version: "${{ inputs.julia-version }}" arch: "${{ inputs.julia-arch || runner.arch }}" - uses: julia-actions/cache@v2 - if: "${{ inputs.cache }}" + if: "${{ steps.classify.outputs.triggers == 'true' && inputs.cache }}" with: token: "${{ secrets.GITHUB_TOKEN }}" - uses: julia-actions/julia-buildpkg@v1 - if: "${{ inputs.buildpkg }}" + if: "${{ steps.classify.outputs.triggers == 'true' && inputs.buildpkg }}" with: localregistry: "${{ inputs.localregistry }}" + project: "${{ inputs.subdir != '' && inputs.subdir || inputs.project }}" - name: "Export extra environment variables" - if: "${{ inputs.extra-env != '' }}" + if: "${{ steps.classify.outputs.triggers == 'true' && inputs.extra-env != '' }}" shell: "bash" env: EXTRA_ENV: ${{ inputs.extra-env }} @@ -156,12 +196,14 @@ jobs: printf '%s\n' "$EXTRA_ENV" >> "$GITHUB_ENV" - name: "Run tests ${{ inputs.self-hosted && '' || format('on {0}', inputs.os) }} with Julia v${{ inputs.julia-version }}" + if: "${{ steps.classify.outputs.triggers == 'true' }}" uses: julia-actions/julia-runtest@v1 with: - project: "${{ inputs.project }}" + project: "${{ inputs.subdir != '' && inputs.subdir || inputs.project }}" depwarn: "${{ inputs.julia-runtest-depwarn }}" coverage: "${{ inputs.coverage }}" prefix: "${{ inputs.test-prefix }}" + test_args: "${{ inputs.test-args }}" # On LTS, accept that newer dep versions may not be reachable — # let Pkg resolve to whatever satisfies both the workspace compat # and transitive constraints. `auto` (the default) would otherwise @@ -174,20 +216,20 @@ jobs: JULIA_NUM_THREADS: "${{ inputs.nthreads }}" - name: "Upload artifacts" - if: "${{ always() && inputs.upload-artifacts-path != '' }}" + if: "${{ always() && steps.classify.outputs.triggers == 'true' && inputs.upload-artifacts-path != '' }}" uses: actions/upload-artifact@v7 with: name: "artifacts-${{ inputs.os }}-julia-${{ inputs.julia-version }}${{ inputs.group != '' && format('-{0}', inputs.group) || '' }}" path: "${{ inputs.upload-artifacts-path }}" - uses: julia-actions/julia-processcoverage@v1 - if: "${{ inputs.coverage }}" + if: "${{ steps.classify.outputs.triggers == 'true' && inputs.coverage }}" with: directories: "${{ inputs.coverage-directories }}" - name: "Report coverage with Codecov" uses: codecov/codecov-action@v6 - if: "${{ inputs.coverage }}" + if: "${{ steps.classify.outputs.triggers == 'true' && inputs.coverage }}" with: files: lcov.info token: "${{ secrets.CODECOV_TOKEN }}" diff --git a/.github/workflows/VersionCheck.yml b/.github/workflows/VersionCheck.yml index 19934f5..5819a92 100644 --- a/.github/workflows/VersionCheck.yml +++ b/.github/workflows/VersionCheck.yml @@ -13,6 +13,24 @@ on: default: "" required: false type: string + subdir: + description: "Subdirectory containing the package's Project.toml. When set, VersionCheck reads `/Project.toml` for the version-bump check. Empty (default) reads root Project.toml." + default: "" + required: false + type: string + paths: + description: "Newline-separated list of path prefixes / files defining positive scope for the substantive-PR check. A file triggers VersionCheck iff it equals one of these paths or starts with `/`. Empty means no positive-scope filter; the check falls back to `exclude-paths`. Mutually exclusive with `exclude-paths`." + default: "" + required: false + type: string + exclude-paths: + description: "Newline-separated list of path prefixes / files defining negative scope for the substantive-PR check. A file does NOT trigger VersionCheck if it matches one of these. Default ignores `.gitignore`, `.pre-commit-config.yaml`, and `.github` (workflow / config metadata changes don't require a version bump)." + default: | + .gitignore + .pre-commit-config.yaml + .github + required: false + type: string jobs: version-check: @@ -27,45 +45,57 @@ jobs: - uses: ITensor/ITensorActions/.github/actions/classify-pr@main id: classify + with: + # `subdir` (the in-tree package whose Project.toml is checked) is + # auto-appended to the positive scope so callers don't have to + # repeat it. + paths: | + ${{ inputs.subdir }} + ${{ inputs.paths }} + exclude-paths: ${{ inputs.exclude-paths }} - - name: "Skip version check on non-substantive PR" - if: steps.classify.outputs.substantive != 'true' + - name: "Skip version check on out-of-scope PR" + if: steps.classify.outputs.triggers != 'true' run: | - echo "PR classified as non-substantive (only .github/**, .pre-commit-config.yaml, .gitignore, LICENSE)." - echo "No version bump is required. Reporting success without running the real check." + echo "PR has no changes in this caller's scope; no version bump required. Reporting success." - name: "Fetch base branch" - if: steps.classify.outputs.substantive == 'true' + if: steps.classify.outputs.triggers == 'true' run: git fetch --depth=1 origin "${{ github.base_ref }}" - uses: julia-actions/setup-julia@v3 - if: steps.classify.outputs.substantive == 'true' + if: steps.classify.outputs.triggers == 'true' with: version: ${{ inputs.julia-version }} - name: "Check version was bumped" - if: steps.classify.outputs.substantive == 'true' + if: steps.classify.outputs.triggers == 'true' shell: julia --color=yes {0} + env: + SUBDIR: ${{ inputs.subdir }} run: | using TOML + subdir = get(ENV, "SUBDIR", "") + project_path = isempty(subdir) ? "Project.toml" : joinpath(subdir, "Project.toml") + base_ref = ENV["GITHUB_BASE_REF"] base_project_text = try - read(`git show "origin/$base_ref:Project.toml"`, String) + read(`git show "origin/$base_ref:$project_path"`, String) catch err - println("Could not read Project.toml on origin/$base_ref ($err); skipping check.") + println("Could not read $project_path on origin/$base_ref ($err); skipping check.") exit(0) end base_project = TOML.parse(base_project_text) - current_project = TOML.parse(read("Project.toml", String)) + current_project = TOML.parse(read(project_path, String)) if !haskey(base_project, "version") - println("Base branch Project.toml has no version field; skipping check.") + println("Base branch $project_path has no version field; skipping check.") exit(0) end if !haskey(current_project, "version") error( - "Project.toml on this branch has no version field, but the base " * + "$project_path on this branch has no version field, but the base " * "branch ($base_ref) does. Restore the version field and bump it." ) end @@ -75,13 +105,13 @@ jobs: if current_version > base_version println( - "OK: Project.toml version bumped from $base_version " * + "OK: $project_path version bumped from $base_version " * "(origin/$base_ref) to $current_version (PR head)." ) else error( - "Project.toml version was not bumped. Current version $current_version " * + "$project_path version was not bumped. Current version $current_version " * "is not greater than the version $base_version on the base branch " * - "($base_ref). Bump the version in Project.toml." + "($base_ref). Bump the version in $project_path." ) end diff --git a/README.md b/README.md index c036082..f1c46fc 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,12 @@ the full matrix even on draft PRs, set it to `true`: |---|---|---|---| | `julia-version` | string | `"1"` | Julia version passed to `julia-actions/setup-julia`. | | `julia-arch` | string | runner arch | Architecture of Julia to be used. | -| `project` | string | `"@."` | Value passed to Julia's `--project` flag. | +| `subdir` | string | `""` | Subdirectory containing the package's Project.toml (for an in-tree subpackage). When set, Julia activates `` for buildpkg and runtest, and `` is auto-appended to `paths` so the per-package classify-pr step considers files inside it as in-scope. | +| `project` | string | `"@."` | Deprecated alias for `subdir`. Value passed to Julia's `--project` flag. Set `subdir` instead; will be removed in a future major release. | | `group` | string | `""` | Test group selector. Exposed to tests via the `GROUP` environment variable so a `runtests.jl` can selectively run a subset. | +| `test-args` | string | `""` | Newline-separated arguments forwarded to `Pkg.test(; test_args=...)` (via `julia-actions/julia-runtest`'s `test_args` input). Useful when `runtests.jl` selects test groups via `ARGS` rather than the `GROUP` env-var. | +| `paths` | string | `""` | Positive scope for the substantive-PR check: a file triggers tests iff it equals one of these paths or starts with `/`. Newline-separated. Mutually exclusive with `exclude-paths`. The `subdir` input (if set) is auto-appended. | +| `exclude-paths` | string | `.gitignore`
`.pre-commit-config.yaml` | Negative scope: a file does NOT trigger tests if it matches one of these paths. Default ignores pure metadata files only — workflow file edits (`.github/workflows/**`) do count, by design. Override entirely (defaults are not implicitly preserved). | | `self-hosted` | bool | `false` | Run on a self-hosted runner instead of `os`. | | `os` | string | `"ubuntu-latest"` | Runner image used when `self-hosted` is `false`. | | `nthreads` | number | `1` | Value of `JULIA_NUM_THREADS`. | @@ -430,6 +434,7 @@ jobs: |---|---|---|---| | `julia-version` | string | `"1"` | Julia version passed to `julia-actions/setup-julia`. | | `localregistry` | string | `""` | Newline-separated list of extra registry URLs (besides General) to scan for dependency updates. | +| `subdir` | string | `""` | Subdirectory containing the package's Project.toml (for an in-tree subpackage). When set, CompatHelper bumps `/Project.toml` plus its `/{docs,examples,test}/Project.toml` siblings if present. Empty (default) bumps the standard root + docs + examples + test set. | ### Secrets @@ -600,7 +605,10 @@ jobs: | `julia-version` | string | `"1"` | Julia version passed to `julia-actions/setup-julia`. | | `pkgs` | string | **required** | JSON-array string of registered package names and/or git URLs. Use `'[]'` to configure no downstream tests. | | `localregistry` | string | `""` | Newline-separated list of extra registry URLs to add before resolving. | +| `subdir` | string | `""` | Run the downstream tests against the package located in this subdirectory (rather than the repo root). When set, `` is also auto-appended to `paths` so the per-package classify-pr considers files inside it as in-scope. | | `extra-dev-paths` | string | `""` | Newline-separated list of additional local package paths to develop alongside the repository root before testing downstream packages. | +| `paths` | string | `""` | Positive scope for the substantive-PR check: a file triggers IntegrationTest iff it equals one of these paths or starts with `/`. Newline-separated. Mutually exclusive with `exclude-paths`. The `subdir` input (if set) is auto-appended. | +| `exclude-paths` | string | `.gitignore`
`.pre-commit-config.yaml` | Negative scope: a file does NOT trigger IntegrationTest if it matches one of these. Default ignores pure metadata files only. Override entirely. | | `run-on-draft` | bool | `false` | Run integration tests on draft PRs. When `false`, draft PRs skip integration tests entirely. | The companion `IntegrationTestRequest.yml` workflow (used for the `/integrationtest ...` comment trigger shown above) has its own inputs: @@ -641,9 +649,12 @@ jobs: | Input | Type | Default | Description | |---|---|---|---| | `julia-version` | string | `"1"` | Julia version passed to `julia-actions/setup-julia`. | -| `localregistry` | string | `""` | Newline-separated list of extra registry URLs to consult when determining the previously registered version. | +| `localregistry` | string | `""` | Unused, kept for backward compat with existing callers. | +| `subdir` | string | `""` | Subdirectory containing the package's Project.toml (for an in-tree subpackage). When set, the version-bump check reads `/Project.toml` instead of root, and `` is auto-appended to `paths` so the substantive-PR check is scoped to that subpackage. | +| `paths` | string | `""` | Positive scope for the substantive-PR check: a file triggers VersionCheck iff it matches one of these paths. Newline-separated. Mutually exclusive with `exclude-paths`. | +| `exclude-paths` | string | `.gitignore`
`.pre-commit-config.yaml`
`.github` | Negative scope: a file does NOT trigger VersionCheck if it matches one of these. Default ignores pure metadata plus `.github` (workflow-only PRs don't require a version bump). Override entirely. | -The check is automatically skipped on PRs classified as non-substantive (changes limited to `.github/**`, `.pre-commit-config.yaml`, `.gitignore`, or `LICENSE`); those PRs pass without requiring a version bump. +The version-bump check is skipped (reported as success) when classify-pr says no in-scope files changed — for example a workflow-only PR on a single-package consumer (default `exclude-paths` includes `.github`), or an NDTensors-only PR on a multi-package caller scoped to root via `exclude-paths: NDTensors`. ## Check Compat Bounds @@ -685,7 +696,8 @@ jobs: | Input | Type | Default | Description | |---|---|---|---| | `julia-version` | string | `"1"` | Julia version passed to `julia-actions/setup-julia`. | -| `project` | string | `"@."` | Value passed to Julia's `--project` flag during `julia-actions/julia-buildpkg`. | +| `subdir` | string | `""` | Subdirectory containing the package's Project.toml (for an in-tree subpackage). When set, both `project` (buildpkg's `--project`) and `workspace-root` (the check-compat-bounds script's workspace root) resolve to ``. | +| `project` | string | `"@."` | Deprecated alias for `subdir`. Value passed to Julia's `--project` flag during `julia-actions/julia-buildpkg`. When `subdir` is set, it takes precedence. | | `cache` | bool | `true` | Use `julia-actions/cache`. | | `buildpkg` | bool | `true` | Run `julia-actions/julia-buildpkg` before the check. Disable only if the workspace is instantiated some other way. | | `localregistry` | string | `""` | Newline-separated list of extra registry URLs to add before resolving (forwarded to `julia-actions/julia-buildpkg`). | @@ -851,6 +863,7 @@ jobs: | `localregistry` | Local registry repo (`owner/name`) for packages not in General | `""` | | `trigger` | Comment trigger phrase for on-demand registration | `/register` | | `julia-version` | Julia version used by the workflow | `1` | +| `subdir` | Subdirectory containing the package's Project.toml (for an in-tree subpackage). When set, the workflow reads `package//Project.toml` instead of root and posts `@JuliaRegistrator register subdir=`, so multi-package repos can auto-register each in-tree package via a matrix invocation. | `""` | ### Secrets