Skip to content

Commit

Permalink
CI: Customise documentation deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
has2k1 committed Dec 18, 2023
1 parent 2bd12f8 commit f6138ea
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 49 deletions.
196 changes: 196 additions & 0 deletions .github/utils/_repo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#!/usr/bin/env python
from __future__ import annotations

import os
import re
import shlex
from subprocess import PIPE, Popen
from typing import Sequence

# https://docs.github.com/en/actions/learn-github-actions/variables
# #default-environment-variables
GITHUB_VARS = [
"GITHUB_REF_NAME", # main, dev, v0.1.0, v0.1.3a1
"GITHUB_REF_TYPE", # "branch" or "tag"
"GITHUB_REPOSITORY", # has2k1/scikit-misc
"GITHUB_SERVER_URL", # https://github.com
"GITHUB_SHA", # commit shasum
"GITHUB_WORKSPACE", # /home/runner/work/scikit-misc/scikit-misc
"GITHUB_EVENT_NAME", # push, schedule, workflow_dispatch, ...
]


count = r"(?:[0-9]|[1-9][0-9]+)"
DESCRIBE_PATTERN = re.compile(
r"^v"
rf"(?P<version>{count}\.{count}\.{count})"
rf"(?P<pre>(a|b|rc|alpha|beta){count})?"
r"(-(?P<commits>\d+)-g(?P<hash>[a-z0-9]+))?"
r"(?P<dirty>-dirty)?"
r"$"
)

# Define a releasable version to be valid according to PEP440
# and is a semver
RELEASE_TAG_PATTERN = re.compile(r"^v" rf"{count}\.{count}\.{count}" r"$")

# Prerelease version
PRE_RELEASE_TAG_PATTERN = re.compile(
r"^v"
rf"{count}\.{count}\.{count}"
r"(?:"
rf"(?:a|b|rc|alpha|beta){count}"
r")"
r"$"
)


def run(cmd: str | Sequence[str]) -> str:
if isinstance(cmd, str) and os.name == "posix":
cmd = shlex.split(cmd)
with Popen(
cmd, stdin=PIPE, stderr=PIPE, stdout=PIPE, text=True, encoding="utf-8"
) as p:
stdout, _ = p.communicate()
return stdout.strip()


class Git:
@staticmethod
def checkout(committish):
"""
Return True if inside a git repo
"""
res = run(f"git checkout {committish}")
return res

@staticmethod
def commit_subjects(n=1) -> list[str]:
"""
Return a list n of commit subjects
"""
output = run(
f"git log --oneline --no-merges --pretty='format:%s' -{n}"
)
return output.split("\n")[:n]

@staticmethod
def commit_messages(n=1) -> list[str]:
"""
Return a list n of commit messages
"""
sep = "______ MESSAGE _____"
output = run(
f"git log --no-merges --pretty='format:%B{sep}' -{n}"
).strip()
if output.endswith(sep):
output = output[: -len(sep)]
return output.split(sep)[:n]

@staticmethod
def commit_subject() -> str:
"""
Commit subject
"""
return Git.commit_subjects(1)[0]

@staticmethod
def commit_message() -> str:
"""
Commit message
"""
return Git.commit_messages(1)[0]

@staticmethod
def is_repo():
"""
Return True if inside a git repo
"""
res = run("git rev-parse --is-inside-work-tree")
return res == "return"

@staticmethod
def fetch_tags() -> str:
"""
Fetch all tags
"""
return run("git fetch --tags --force")

@staticmethod
def is_shallow() -> bool:
"""
Return True if current repo is shallow
"""
res = run("git rev-parse --is-shallow-repository")
return res == "true"

@staticmethod
def deepen(n: int = 1) -> str:
"""
Fetch n commits beyond the shallow limit
"""
return run(f"git fetch --deepen={n}")

@staticmethod
def describe() -> str:
"""
Git describe to determine version
"""
return run("git describe --dirty --tags --long --match '*[0-9]*'")

@staticmethod
def can_describe() -> bool:
"""
Return True if repo can be "described" from a semver tag
"""
return bool(DESCRIBE_PATTERN.match(Git.describe()))

@staticmethod
def get_tag_at_commit(committish: str) -> str:
"""
Get tag of a given commit
"""
return run(f"git describe --exact-match {committish}")

@staticmethod
def tag_message(tag: str) -> str:
"""
Get the message of a tag
"""
return run(f"git tag -l --format='%(subject)' {tag}")

@staticmethod
def is_annotated(tag: str) -> bool:
"""
Return true if tag is annotated tag
"""
# LHS prints to stderr and returns nothing when
# tag is an empty string
return run(f"git cat-file -t {tag}") == "tag"

@staticmethod
def shallow_checkout(branch: str, url: str, depth: int = 1) -> str:
"""
Shallow clone upto n commits
"""
_branch = f"--branch={branch}"
_depth = f"--depth={depth}"
return run(f"git clone {_depth} {_branch} {url} .")

@staticmethod
def is_release():
"""
Return True if event is a release
"""
ref = os.environ.get("GITHUB_REF_NAME", "")
ref_type = os.environ.get("GITHUB_REF_TYPE", "")
return ref_type == "tag" and bool(RELEASE_TAG_PATTERN.match(ref))

@staticmethod
def branch():
"""
Return event branch
"""
ref = os.environ.get("GITHUB_REF_NAME", "")
ref_type = os.environ.get("GITHUB_REF_TYPE", "")
return ref if ref_type == "branch" else ""
50 changes: 50 additions & 0 deletions .github/utils/please.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import os
import re
import sys
from typing import Callable, TypeAlias

from _repo import Git

Ask: TypeAlias = Callable[[], bool | str]
Do: TypeAlias = Callable[[], str]


def can_i_deploy_documentation() -> bool:
"""
Return True if documentation should be deployed
"""
return Git.is_release() and Git.branch() in ("main", "dev")


def where_can_i_deploy_documentation() -> str:
"""
Return branch to deploy documentation to
"""
if Git.is_release():
return "website"
return "gh-pages" if Git.branch() == "main" else "gh-branches"


def process_request(arg: str) -> str:
if arg in REQUESTS:
result = REQUESTS.get(arg, lambda: False)()
if not isinstance(result, str):
result = str(result).lower()
else:
result = ACTIONS.get(arg, lambda: "")()
return result


REQUESTS: dict[str, Ask] = {
"can_i_deploy_documentation": can_i_deploy_documentation,
"where_can_i_deploy_documentation": where_can_i_deploy_documentation,
}


ACTIONS: dict[str, Do] = {}


if __name__ == "__main__":
if len(sys.argv) == 2:
arg = sys.argv[1]
print(process_request(arg))
50 changes: 42 additions & 8 deletions .github/workflows/documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,39 @@ name: Documentation

on:
workflow_dispatch:
push:
branches: ["main", "dev", "doc", "dev-*"]
pull_request:
release:
types: [published]
workflow_call:

jobs:
build:
parse_commit_info:
runs-on: ubuntu-latest
outputs:
can_deploy: ${{ steps.decide.outputs.can_deploy }}
deploy_to: ${{ steps.decide.outputs.deploy_to }}

steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Copy build utils
run: |
cp -r .github/utils ../utils
- name: Decide Whether to Build and/or Release
id: decide
run: |
set -xe
CAN_DEPLOY=$(python ../utils/please.py can_i_deploy_documentation)
DEPLOY_TO=$(python ../utils/please.py where_can_i_deploy_documentation)
echo "can_deploy=$CAN_DEPLOY" >> $GITHUB_OUTPUT
echo "deploy_to=$DEPLOY_TO" >> $GITHUB_OUTPUT
echo github.ref ${{ github.ref }}
build-documentation:
runs-on: ubuntu-latest
needs: parse_commit_info

strategy:
matrix:
Expand All @@ -35,7 +59,7 @@ jobs:
pip install git+https://github.com/has2k1/mizani.git@main
python -m pip install ".[doc]"
pip install -r requirements/doc.txt
quarto add --no-prompt has2k1/issuey
cd doc; quarto add --no-prompt has2k1/issuey; popd
- name: Environment Information
shell: bash
Expand All @@ -46,9 +70,19 @@ jobs:
- name: Build docs
run: |
make doc
cd doc; make doc; popd
- name: Environment Information
shell: bash
run: |
ls -la doc
ls -la doc/reference
ls -la doc/examples
ls -la doc/tutorials
- name: Deploy to Github Pages
uses: JamesIves/github-pages-deploy-action@v4
if: contains(needs.parse_commit_info.outputs.can_deploy, 'true')
with:
folder: doc/_site
branch: ${{ needs.parse_commit_info.outputs.deploy_to }}
46 changes: 5 additions & 41 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,44 +123,8 @@ jobs:
shell: bash
run: make typecheck

documentation:
runs-on: ubuntu-latest

# We want to run on external PRs, but not on our own internal PRs as they'll be run
# by the push to the branch.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository

strategy:
matrix:
python-version: [3.11]

steps:
- name: Checkout Code
uses: actions/checkout@v3

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install Quarto
uses: quarto-dev/quarto-actions/setup@v2
with:
version: "1.4.528"

- name: Install Packages
shell: bash
run: |
sudo apt-get install pandoc
pip install git+https://github.com/has2k1/mizani.git@main
pip install ".[test,doc]"
pip install -r requirements/doc.txt
quarto add --no-prompt has2k1/issuey
- name: Environment Information
shell: bash
run: pip list

- name: Build documentation
run: |
make doc
call-build-documentation:
if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name != github.repository
uses: ./.github/workflows/documentation.yml

0 comments on commit f6138ea

Please sign in to comment.