Jika pipeline build Anda tidak dapat berjalan tanpa Anda membuka terminal dan mengetik make tangle, Anda tidak memiliki program literate. Anda memiliki buku harian dengan kompiler yang terpasang.
Seluruh poin dari literate programming adalah bahwa prosa dan kode berbagi sumber kebenaran tunggal. File Markdown adalah artefaknya. Segala sesuatu yang lain — sumber yang dapat dieksekusi, dokumentasi yang dirender, file pengujian — adalah turunan. Artefak turunan termasuk dalam CI, bukan dalam memori kerja Anda.
Pertanyaannya bukan apakah CI dapat weave dan tangle. Tentu saja bisa. Setiap mesin yang menjalankan Python dan pandoc dapat melakukannya. Pertanyaannya adalah apakah struktur repository Anda memungkinkan CI memiliki file turunan, atau apakah Anda masih berpura-pura bahwa file .py adalah kode sumber padahal sebenarnya adalah output build.
Apa yang sebenarnya dilakukan weave dan tangle
Donald Knuth menyebut dua operasi itu weave dan tangle, dan nama-namanya cukup membingungkan sehingga orang menghindari topic tersebut sepenuhnya.
tangle mengambil dokumen naratif — file .md atau .w — dan mengekstrak blok kode dalam urutan yang diharapkan kompiler. Ia menghasilkan file sumber yang dapat dijalankan. weave melakukan kebalikannya. Ia mengambil dokumen yang sama dan menghasilkan dokumentasi yang dapat dibaca manusia, dengan kode pretty-printed, referensi silang, dan daftar isi.
Keduanya adalah transformasi deterministik. Mereka mengambil satu input dan menghasilkan output yang konsisten. Itulah definisi dari sesuatu yang harus ditangani oleh CI.
Mengapa sebagian besar tim membangun pipeline secara terbalik
Alur kerja literate programming yang tipikal terlihat seperti ini. Anda menulis algorithm.md. Anda menjalankan skrip lokal untuk mengekstrak algorithm.py. Anda menjalankan pengujian terhadap algorithm.py. Anda melakukan commit baik algorithm.md maupun algorithm.py ke Git. Kemudian Anda membuka pull request.
Ini sudah rusak. Anda memiliki dua salinan logika yang sama dalam kontrol versi. Jika seorang pengulas menyarankan perubahan pada algorithm.py, Anda harus ingat untuk mem-portingnya kembali ke algorithm.md. Jika Anda mengedit algorithm.md dan lupa untuk melakukan re-tangle, file .py yang di-commit sudah usang. Sumber kebenaran tunggal adalah fiksi.
Alur kerja yang benar lebih sederhana. Anda hanya melakukan commit file .md. CI menjalankan tangle untuk menghasilkan file .py, lalu menjalankan suite pengujian terhadapnya. Jika pengujian lolos, CI secara opsional menjalankan weave untuk menerbitkan dokumentasi. File .py tidak pernah menyentuh kontrol versi. Itu adalah output build, seperti file .o atau gambar Docker.
Pipeline CI yang berfungsi untuk Markdown-ke-Python
Berikut adalah alur kerja GitHub Actions lengkap yang memperlakukan file Markdown sebagai sumber kebenaran tunggal. Simpan sebagai .github/workflows/literate.yml:
name: Tangle and Test
on:
push:
paths:
- "**/*.md"
pull_request:
paths:
- "**/*.md"
jobs:
tangle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Tangle code from Markdown
run: python scripts/tangle.py src/*.md --out build/
- name: Run tests on extracted code
run: python -m pytest build/ -v
- name: Upload generated source as artifact
uses: actions/upload-artifact@v4
with:
name: generated-source
path: build/
Filter paths penting. Alur kerja ini hanya berjalan ketika file Markdown berubah, karena Markdown adalah satu-satunya input yang penting.
Skrip tangle.py adalah ekstraktor tiga puluh baris yang sama yang akan Anda jalankan secara lokal. Berikut adalah versi yang menangani banyak file dan direktori output:
import argparse
import re
import sys
from pathlib import Path
def tangle(md_path: Path, out_dir: Path) -> None:
content = md_path.read_text()
files: dict[str, list[str]] = {}
for block in re.findall(r"```python\n(.*?)```", content, re.DOTALL):
lines = block.strip().split("\n")
if lines and lines[0].startswith("# "):
filename = lines[0][2:].strip()
code = lines[1:]
else:
filename = "module.py"
code = lines
files.setdefault(filename, []).extend(code)
out_dir.mkdir(parents=True, exist_ok=True)
for filename, code_lines in files.items():
(out_dir / filename).write_text("\n".join(code_lines) + "\n")
def main() -> None:
parser = argparse.ArgumentParser(description="Extract code from Markdown files.")
parser.add_argument("inputs", nargs="+", type=Path, help="Markdown files to process")
parser.add_argument("--out", type=Path, required=True, help="Output directory")
args = parser.parse_args()
for md in args.inputs:
tangle(md, args.out)
if __name__ == "__main__":
main()
Jalankan secara lokal untuk memverifikasi:
$ python scripts/tangle.py src/calculator.md --out build/
$ python -m pytest build/ -v
Pekerjaan CI melakukan hal yang sama persis. Satu-satunya perbedaan adalah CI menjalankannya dalam lingkungan yang bersih, sehingga file .py yang sudah usang yang tersembunyi di direktori kerja Anda tidak dapat menipu suite pengujian.
Menenun dokumentasi dalam pipeline yang sama
tangle menghasilkan kode. weave menghasilkan dokumentasi. Keduanya dapat berjalan di CI.
Untuk weave, Anda membutuhkan alat yang mengubah Markdown menjadi format yang dapat dipresentasikan. Pandoc adalah pilihan yang membosankan namun dapat diandalkan. Jika Anda menginginkan referensi silang, penyorotan sintaksis, dan daftar isi, template kecil akan membantu Anda mencapainya:
- name: Weave documentation
run: |
mkdir -p docs/output
pandoc src/*.md \
--from markdown \
--to html5 \
--standalone \
--toc \
--highlight-style=tango \
--output docs/output/index.html
- name: Publish to GitHub Pages
uses: actions/upload-pages-artifact@v3
with:
path: docs/output/
HTML yang dirender juga merupakan artefak turunan. Anda dapat menerbitkannya ke GitHub Pages, Netlify, atau bucket S3 pada setiap merge ke main. File Markdown tetap kanonis. HTML adalah apa yang dilihat pembaca.
Kompromis antara commit versus generate
Beberapa tim menolak hal ini. Mereka menginginkan file .py di Git karena membuat git grep berfungsi dan karena UI tinjauan kode GitHub memahami sintaks Python lebih baik daripada Markdown.
Ini adalah masalah nyata, tetapi masalah tooling, bukan masalah arsitektur. Melakukan commit kode yang dihasilkan karena alat tinjauan kode Anda buruk seperti melakukan commit node_modules karena pengelola package Anda lambat. Ini berfungsi, tetapi menimbulkan utang.
Jika Anda benar-benar membutuhkan file .py di repository, hasilkan di CI dan commit kembali dengan akun bot. Berikut polanya:
- Seorang pengembang mendorong perubahan ke
algorithm.md. - CI menjalankan
tangle.pydan menghasilkanalgorithm.py. - Jika kode yang diekstrak berbeda dari yang ada di
main, CI membuka pull request dengan file.pyyang diperbarui. - Seorang manusia meninjau diff yang dihasilkan dan menggabungkannya.
Ini menjaga file yang dihasilkan dalam kontrol versi tanpa membiarkannya menyimpang. Bot menjadi sumber kebenaran tunggal untuk output yang dihasilkan, dan bot hanya mengubah apa yang diminta oleh file Markdown.
Kapan otomasi CI sepadan, dan kapan tidak
Weave dan tangle otomatis unggul ketika prosa panjang, kode kompleks, dan beberapa orang mengedit dokumen. Kode penelitian, dokumentasi perpustakaan, dan implementasi algoritma semuanya mendapat manfaat dari file Markdown kanonis yang divalidasi oleh CI pada setiap push.
Ini tidak sepadan untuk skrip kecil atau kode aplikasi yang banyak boilerplate-nya. Jika file Markdown Anda adalah dua paragraf dan fungsi dua puluh baris, overhead CI, menit runner, dan biaya model mental melebihi masalah sinkronisasi yang Anda selesaikan. Cukup tulis docstring yang baik.
Kasus lain di mana ini gagal adalah ketika tooling bahasa Anda menganggap file .py sebagai utama. Debugger, profiler, dan alat cakupan biasanya ingin menunjuk ke sumber yang dihasilkan, bukan ke Markdown. Anda dapat mengatasinya dengan menghasilkan file secara lokal sebelum debugging, tetapi itu adalah gesekan. Jika tim Anda tinggal di debugger, literate programming mungkin bukan pilihan yang tepat terlepas dari seberapa baik pipeline CI Anda.
FAQ
Haruskah saya melakukan commit file .py yang dihasilkan atau memasukkannya ke .gitignore?
Masukkan ke .gitignore. Jika dihasilkan secara deterministik dari Markdown, mereka adalah artefak build. Satu-satunya pengecualian adalah jika sistem eksternal, seperti registry package, tidak dapat menjalankan langkah tangle Anda. Dalam hal ini, gunakan pola commit bot yang dijelaskan di atas.
Bagaimana jika dokumen literate saya mencampur banyak bahasa?
Skrip tangle.py bersifat language-agnostic. Gunakan # filename.rs untuk blok Rust, # filename.go untuk blok Go, dan seterusnya. CI menjalankan skrip yang sama untuk setiap bahasa. Anda hanya membutuhkan langkah pengujian per bahasa dalam alur kerja.
Apakah ini berfungsi dengan chunk kode bernama seperti WEB Knuth?
Alat modern seperti Entangled mendukung chunk bernama dan pelacakan dependency. Jika Anda membutuhkan tingkat ekspresivitas itu, gunakan Entangled di CI alih-alih skrip kustom. Struktur pipeline sama. Hanya alat ekstraksi yang berubah.
Bagaimana cara menangani impor yang mencakup banyak file Markdown?
Setiap file Markdown di-tangle ke direktori outputnya sendiri. Jika math.md mendefinisikan add dan geometry.md mengimpornya, baik tangle kedua file ke direktori build yang sama atau strukturkan kode Anda sebagai package dengan impor relatif. Pekerjaan CI dapat men-tangle semua file sebelum menjalankan pengujian, sehingga dependency antar file terselesaikan secara alami.
Jadikan file Markdown Anda satu-satunya file yang penting
Siapkan pipeline sekali. Commit file .md. Biarkan CI menangani sisanya. Jika pengujian gagal, kegagalan menunjuk ke baris Markdown yang menghasilkan kode, bukan ke file yang dihasilkan yang seharusnya tidak ada dalam kontrol versi sama sekali.
Tujuan dari literate programming adalah sumber kebenaran tunggal. Sumber kebenaran yang memerlukan manusia untuk mengingat langkah manual bukanlah sumber kebenaran. Itu adalah saran.