Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,25 @@ permissions:
contents: read

jobs:
changes:
runs-on: ubuntu-latest
outputs:
docs_only: ${{ steps.scope.outputs.docs_only }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Check changed files
id: scope
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: python3 scripts/ci_docs_only.py

package:
needs: changes
if: ${{ needs.changes.outputs.docs_only != 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -26,6 +44,8 @@ jobs:
test "$(cd /tmp && /tmp/aiython-wheel-smoke/bin/aiython /tmp/aiython-wheel-smoke/plain.py)" = 2

test:
needs: changes
if: ${{ needs.changes.outputs.docs_only != 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -62,6 +82,8 @@ jobs:
if-no-files-found: warn

native-hooks:
needs: changes
if: ${{ needs.changes.outputs.docs_only != 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand Down Expand Up @@ -100,16 +122,25 @@ jobs:
required:
name: CI
if: ${{ always() }}
needs: [package, test, native-hooks, docs]
needs: [changes, package, test, native-hooks, docs]
runs-on: ubuntu-latest
steps:
- name: Require every job to pass
- name: Require the right jobs to pass
env:
CHANGES: ${{ needs.changes.result }}
DOCS_ONLY: ${{ needs.changes.outputs.docs_only }}
PACKAGE: ${{ needs.package.result }}
TEST: ${{ needs.test.result }}
NATIVE_HOOKS: ${{ needs.native-hooks.result }}
DOCS: ${{ needs.docs.result }}
run: |
for result in "$PACKAGE" "$TEST" "$NATIVE_HOOKS" "$DOCS"; do
test "$result" = success || exit 1
test "$CHANGES" = success
test "$DOCS" = success
if [ "$DOCS_ONLY" = true ]; then
expected=skipped
else
expected=success
fi
for result in "$PACKAGE" "$TEST" "$NATIVE_HOOKS"; do
test "$result" = "$expected" || exit 1
done
43 changes: 43 additions & 0 deletions scripts/ci_docs_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Decide whether a CI run only changed documentation files."""

from __future__ import annotations

import os
import re
import subprocess
from pathlib import PurePosixPath


DOCUMENT_ASSETS = {".gif", ".ico", ".jpeg", ".jpg", ".mp3", ".mp4", ".pdf", ".png", ".svg", ".webp"}


def is_docs_only(paths: list[str]) -> bool:
"""Return true only for a nonempty set of documentation and static assets."""
return bool(paths) and all(
path.endswith(".md")
or path == "zensical.toml"
or (
path.startswith(("docs/", "assets/readme/"))
and PurePosixPath(path).suffix.lower() in DOCUMENT_ASSETS
)
for path in paths
)


def changed_paths(base: str, head: str) -> list[str]:
if not all(re.fullmatch(r"[0-9a-f]{40}", sha) for sha in (base, head)):
raise ValueError("CI needs two commit SHAs to inspect changed paths")
if base == "0" * 40:
return [] # A new branch has no earlier commit; run the full suite.
output = subprocess.check_output(
["git", "diff", "--name-only", "-z", "--no-renames", base, head]
)
return [os.fsdecode(path) for path in output.split(b"\0") if path]


if __name__ == "__main__":
paths = changed_paths(os.environ["BASE_SHA"], os.environ["HEAD_SHA"])
docs_only = str(is_docs_only(paths)).lower()
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"docs_only={docs_only}\n")
print(f"Changed files: {len(paths)}; documentation only: {docs_only}")
35 changes: 35 additions & 0 deletions tests/test_ci_docs_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import unittest

from scripts.ci_docs_only import is_docs_only


class DocsOnlyScopeTests(unittest.TestCase):
def test_docs_and_readme_assets(self):
self.assertTrue(
is_docs_only(
[
"README.md",
"README.pypi.md",
"docs/getting-started.md",
"docs/assets/icon.png",
"assets/readme/runtime-debug.gif",
".github/ISSUE_TEMPLATE/bug_report.md",
"zensical.toml",
]
)
)

def test_empty_or_code_changes_run_full_suite(self):
for paths in (
[],
["src/aiython/runtime.py"],
["README.md", "src/aiython/runtime.py"],
["docs/example.py"],
["assets/readme/build_runtime_debug.py"],
["examples/recipes/03_loop.py"],
[".github/workflows/tests.yml"],
["pyproject.toml"],
["uv.lock"],
):
with self.subTest(paths=paths):
self.assertFalse(is_docs_only(paths))
Loading