diff --git a/.github/komizo-action-source.json b/.github/komizo-action-source.json new file mode 100644 index 0000000..34636a1 --- /dev/null +++ b/.github/komizo-action-source.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "platform_revision": "1b740f28682948a2d5eb4613744bb262d2056e6c", + "action_revision": "2ca94e2fd4bb05f41bb228e9c366e5cc733c75e6" +} diff --git a/.github/readme-source.json b/.github/readme-source.json new file mode 100644 index 0000000..1e1152b --- /dev/null +++ b/.github/readme-source.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "sha256": "b0008a20f0336bdc0cade112310947098eadf2e3ce51e9f65fc07d0be8c8c47d", + "history": { + "forbidden_commits": [ + "0eb77f25b1b2c0df30fb7a80abb435bf5bd65724" + ] + } +} diff --git a/.github/scripts/check-documentation.py b/.github/scripts/check-documentation.py new file mode 100644 index 0000000..ae3dbd9 --- /dev/null +++ b/.github/scripts/check-documentation.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Check tracked documentation layout and the reviewed usage README digest.""" +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import subprocess +import sys + +MARKDOWN = {'.md', '.markdown', '.mdown', '.mkd', '.mkdn', '.mdwn', '.mdx', '.rmd', '.qmd'} +OTHER_DOCS = {'.rst', '.adoc', '.asciidoc'} + + +def check(root, include_history=True, history_required=False): + raw = subprocess.check_output(['git', 'ls-files', '--stage', '-z'], cwd=root) + entries = {} + errors = [] + for record in raw.split(b'\0'): + if not record: + continue + metadata, encoded = record.split(b'\t', 1) + mode, _oid, stage = metadata.decode().split() + name = encoded.decode('utf-8', 'surrogateescape') + if stage != '0': + errors.append('unmerged index entry: ' + name) + entries[name] = mode + path = PurePosixPath(name) + if name != 'README.md' and (path.suffix.lower() in MARKDOWN | OTHER_DOCS + or path.name.lower().startswith('readme.')): + errors.append('documentation must live in the private docs repository: ' + name) + if entries.get('README.md') != '100644' and entries.get('README.md') != '100755': + errors.append('a regular, tracked root README.md is required') + else: + readme = root / 'README.md' + if readme.is_symlink() or not readme.is_file(): + errors.append('root README.md must be a regular file') + else: + try: + body = readme.read_bytes() + if not any(line.startswith('# ') for line in body.decode('utf-8').splitlines()): + errors.append('root README.md needs a product heading') + policy_path = root / '.github/readme-source.json' + if policy_path.is_symlink(): + raise ValueError('README source policy cannot be a symlink') + policy = json.loads(policy_path.read_text()) + if policy.get('version') != 1: + raise ValueError('unsupported README source policy') + if hashlib.sha256(body).hexdigest() != policy.get('sha256'): + errors.append('README differs from its reviewed publication; update the private source first') + history = policy.get('history') + if history_required and history is None: + raise ValueError('public repositories require a history boundary') + if include_history and history is not None: + if set(history) != {'forbidden_commits'} or not isinstance(history['forbidden_commits'], list) or not history['forbidden_commits']: + raise ValueError('invalid public history boundary') + forbidden = set(history['forbidden_commits']) + if any(not isinstance(oid, str) or len(oid) != 40 or any(c not in '0123456789abcdef' for c in oid) for oid in forbidden): + raise ValueError('invalid original history identity') + shallow = subprocess.check_output(['git', 'rev-parse', '--is-shallow-repository'], cwd=root, text=True).strip() + if shallow != 'false': + raise ValueError('public history verification requires fetch-depth: 0') + ancestors = set(subprocess.check_output(['git', 'rev-list', 'HEAD'], cwd=root, text=True).splitlines()) + if ancestors & forbidden: + errors.append('original history was reintroduced; transplant changes onto the cleaned main history') + patterns = [':(icase)*'+suffix for suffix in sorted(MARKDOWN)] + past = subprocess.check_output(['git', 'log', '--format=', '--name-only', '-z', 'HEAD', '--', *patterns], cwd=root) + extra = {name.decode('utf-8', 'surrogateescape') for name in past.split(b'\0') if name and name != b'README.md'} + errors.extend('historical Markdown must remain in private docs: '+name for name in sorted(extra)) + + except (OSError, ValueError, UnicodeError) as error: + errors.append('invalid README source policy or README: ' + str(error)) + return errors + + +def main(): + root = Path(subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True).strip()) + errors = check(root, history_required=os.environ.get('REQUIRE_CLEAN_HISTORY') == 'true') + for error in errors: + print(error, file=sys.stderr) + if errors: + return 1 + print('Documentation policy passed: one root README.md, reviewed source digest matches.') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 5d3fc39..6e96715 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -26,7 +26,7 @@ jobs: - name: Check out source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - name: Set up locked komizo - uses: nicodes/komizo/setup@40086036977a5fc0119d6c4c1d5eb6ae3ab3b4b9 + uses: nicodes/komizo/setup@2ca94e2fd4bb05f41bb228e9c366e5cc733c75e6 env: GH_TOKEN: ${{ github.token }} - name: Validate and build the redirect diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a43775e..d57bf54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: with: persist-credentials: false - name: Set up locked komizo - uses: nicodes/komizo/setup@40086036977a5fc0119d6c4c1d5eb6ae3ab3b4b9 + uses: nicodes/komizo/setup@2ca94e2fd4bb05f41bb228e9c366e5cc733c75e6 env: GH_TOKEN: ${{ github.token }} timeout-minutes: 5 @@ -52,7 +52,7 @@ jobs: retention-days: 7 include-hidden-files: true - name: Check project - uses: nicodes/komizo/run@40086036977a5fc0119d6c4c1d5eb6ae3ab3b4b9 + uses: nicodes/komizo/run@2ca94e2fd4bb05f41bb228e9c366e5cc733c75e6 with: target: check - name: Upload artifacts diff --git a/.github/workflows/dependency-policy.yml b/.github/workflows/dependency-policy.yml index ac4a432..a7f0e0f 100644 --- a/.github/workflows/dependency-policy.yml +++ b/.github/workflows/dependency-policy.yml @@ -45,7 +45,7 @@ jobs: ref: main timeout-minutes: 3 - name: Set up locked komizo - uses: nicodes/komizo/setup@40086036977a5fc0119d6c4c1d5eb6ae3ab3b4b9 + uses: nicodes/komizo/setup@2ca94e2fd4bb05f41bb228e9c366e5cc733c75e6 env: GH_TOKEN: ${{ github.token }} timeout-minutes: 5 diff --git a/.github/workflows/documentation-policy.yml b/.github/workflows/documentation-policy.yml new file mode 100644 index 0000000..479ac0b --- /dev/null +++ b/.github/workflows/documentation-policy.yml @@ -0,0 +1,23 @@ +name: Documentation policy +on: + push: + pull_request: + merge_group: + workflow_dispatch: +permissions: + contents: read +jobs: + documentation-policy: + name: Documentation policy + runs-on: ubuntu-24.04 + timeout-minutes: 3 + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + fetch-depth: ${{ github.event.repository.private && 1 || 0 }} + - name: Check documentation boundary + env: + REQUIRE_CLEAN_HISTORY: ${{ !github.event.repository.private }} + run: python3 .github/scripts/check-documentation.py diff --git a/README.md b/README.md index a908cf9..4680d25 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ + + # aviorstudio.github.io -For current client installation and usage, see https://github.com/aviorstudio/aviorstudio.github.io#readme. +Static redirect to [Avior Studio](https://www.avior.studio/). + +Install the [komizo release](https://github.com/nicodes/komizo#install) matching `komizo.lock`, then run `komizo check`. Use `komizo describe` to inspect the applicable stages. diff --git a/komizo.lock b/komizo.lock index dcb4ed7..6a851a4 100644 --- a/komizo.lock +++ b/komizo.lock @@ -1,8 +1,8 @@ { "version": 1, - "platform": "0.2.4", - "platform_revision": "40086036977a5fc0119d6c4c1d5eb6ae3ab3b4b9", + "platform": "0.2.7", + "platform_revision": "1b740f28682948a2d5eb4613744bb262d2056e6c", "manifest_sha256": "f605f9b8126a0fdab262650c7812be90cbba5c472cdb335c8ef527dc2407888e", - "runtime_sha256": "000c1991c455b7e1269646e40c05ebf4a1b0717197c34838d2b7669430b3e862", + "runtime_sha256": "e85ed5057a15b6432600b5596aacbc967179122757378b61dc4247a101db8f9b", "recipe_sha256": "33ac62cab7dbea38c303cabc99c60ac1adf33ef21bd0b2b5c62bcb22fcac2235" }