Merge pull request #26 from logos-co/doctests

add doctests
This commit is contained in:
Iuri Matias
2026-06-16 16:02:30 -04:00
committed by GitHub
8 changed files with 1241 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
name: lgx Doc-Tests
# Runs the executable lgx doc-tests (doctests/*.test.yaml) end-to-end via the
# shared doctest CLI: each spec builds lgx from the commit under test, then
# creates / adds / verifies / extracts / merges / signs .lgx packages and asserts
# on lgx's output. If the docs drift from the tool, this job fails.
#
# ──────────────────────────────────────────────────────────────────────────────
# One-time setup required for the clickable report links to work:
#
# 1. Repo Settings → Pages → "Build and deployment" → Source: "Deploy from a
# branch", Branch: `gh-pages` / `(root)`. (The publish-report job creates
# the gh-pages branch on its first run.)
# 2. Nothing else — GITHUB_TOKEN already has the permissions granted below.
#
# Each run publishes the HTML reports to:
# https://<owner>.github.io/<repo>/pr-<N>/<os>/ (pull requests)
# https://<owner>.github.io/<repo>/main/<os>/ (pushes to main/master)
# and (for PRs) posts/updates a comment with the links.
#
# Note: pull requests opened from forks get a read-only GITHUB_TOKEN, so the
# Pages push and PR comment are skipped for them — the downloadable artifact is
# still produced. PRs from branches in this repo get the full clickable links.
# ──────────────────────────────────────────────────────────────────────────────
on:
pull_request:
branches: [master, main]
push:
branches: [master, main]
concurrency:
group: doctests-${{ github.ref }}
cancel-in-progress: true
jobs:
doctests:
name: Run lgx doc-tests (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Setup Cachix
uses: cachix/cachix-action@v15
with:
name: logos-co
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
# Resolve the commit under test. For pull requests this is the PR's head
# commit (not the synthetic merge commit); for pushes it's the pushed
# commit. Passed to --release-for below so the doc-tests build THIS commit
# of logos-package instead of the latest published flake.
#
# Fork PRs are the exception: their head commit lives in the fork, not in
# logos-co/logos-package, so nix could not fetch
# `github:logos-co/logos-package/<sha>`. We blank the SHA for forks
# (--release-for repo= → pins that repo to latest), so the doc-tests still
# run for fork PRs, just against master as before.
- name: Resolve commit under test
id: commit
shell: bash
run: |
if [ "${{ github.event_name }}" = "pull_request" ] && \
[ "${{ github.event.pull_request.head.repo.fork }}" = "true" ]; then
echo "sha=" >> "$GITHUB_OUTPUT"
echo "Fork PR detected — doc-tests will run against latest master."
else
echo "sha=${{ github.event.pull_request.head.sha || github.sha }}" >> "$GITHUB_OUTPUT"
fi
# The runner is the shared `doctest` CLI, invoked directly via its flake
# (github:logos-co/logos-doctest). The flake bundles Python + PyYAML, so no
# pip install step is needed.
#
# All specs are passed to a SINGLE `doctest run` so they land in one HTML
# report — the report's top-right dropdown switches between them. (Running
# one spec per invocation would emit a separate file each and lose the
# dropdown.) --continue-on-fail makes the run walk every spec/step so the
# report is complete; the job still exits non-zero if any step failed.
#
# --release-for pins the {release} placeholder for this one repo to the
# commit under test, so `github:logos-co/logos-package{release}` in each
# spec becomes `.../<sha>` — the doc-tests exercise this PR/push rather
# than master. An empty sha (fork PR) leaves the URL on latest.
- name: Run every doc-test
run: |
mkdir -p "${{ runner.temp }}/reports"
nix run github:logos-co/logos-doctest -- run \
doctests/*.test.yaml \
--verbose \
--continue-on-fail \
--release-for "logos-package=${{ steps.commit.outputs.sha }}" \
--report "${{ runner.temp }}/reports/index.html"
- name: Verify markdown generation works
run: |
for spec in doctests/*.test.yaml; do
name="$(basename "${spec%.test.yaml}")"
nix run github:logos-co/logos-doctest -- generate "$spec" \
--release-for "logos-package=${{ steps.commit.outputs.sha }}" \
-o "/tmp/${name}.md"
test -s "/tmp/${name}.md"
echo "Generated /tmp/${name}.md ($(wc -l < "/tmp/${name}.md") lines)"
done
- name: Upload execution reports
if: always()
uses: actions/upload-artifact@v4
with:
name: lgx-doctest-reports-${{ matrix.os }}
path: ${{ runner.temp }}/reports/
if-no-files-found: warn
publish-report:
name: Publish reports to GitHub Pages
needs: doctests
# Run even when tests fail — a failing run is exactly when you want to open
# the report. Skip on forks, where GITHUB_TOKEN can't push or comment.
if: ${{ always() && github.event.pull_request.head.repo.fork != true }}
runs-on: ubuntu-latest
permissions:
contents: write # push to the gh-pages branch
pull-requests: write # post/update the PR comment
# Serialize Pages pushes so two refs can't race on the gh-pages branch.
concurrency:
group: gh-pages-publish
cancel-in-progress: false
steps:
- name: Download all reports
uses: actions/download-artifact@v4
with:
path: artifacts
# No `name:` → downloads every artifact into artifacts/<name>/...
- name: Arrange site directory
id: arrange
shell: bash
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="pr-${{ github.event.pull_request.number }}"
else
BASE="main"
fi
echo "base=$BASE" >> "$GITHUB_OUTPUT"
mkdir -p "site/$BASE"
found=""
for os in ubuntu-latest macos-latest; do
src="artifacts/lgx-doctest-reports-$os"
# Each OS produces a single combined report (index.html) whose
# built-in dropdown switches between specs — just publish it as-is.
if [ -f "$src/index.html" ]; then
mkdir -p "site/$BASE/$os"
cp "$src/index.html" "site/$BASE/$os/index.html"
found="$found $os"
fi
done
echo "found=$found" >> "$GITHUB_OUTPUT"
# Top-level landing page for this ref linking to each OS.
{
echo "<!doctype html><meta charset=utf-8>"
echo "<title>lgx doc-test reports — $BASE</title>"
echo "<style>body{font:16px system-ui;margin:40px;max-width:640px}a{color:#2563eb}</style>"
echo "<h1>lgx doc-test reports</h1>"
echo "<p><strong>$BASE</strong> · commit <code>${GITHUB_SHA::7}</code></p><ul>"
for os in ubuntu-latest macos-latest; do
if [ -d "site/$BASE/$os" ]; then
echo "<li><a href=\"./$os/\">$os</a></li>"
fi
done
echo "</ul>"
} > "site/$BASE/index.html"
- name: Deploy to gh-pages
if: steps.arrange.outputs.found != ''
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./site
keep_files: true # don't wipe other PRs' directories
commit_message: "Publish lgx doc-test reports for ${{ steps.arrange.outputs.base }} (${{ github.sha }})"
- name: Comment on PR with report links
if: ${{ github.event_name == 'pull_request' && steps.arrange.outputs.found != '' }}
uses: actions/github-script@v7
with:
script: |
const base = "${{ steps.arrange.outputs.base }}";
const owner = context.repo.owner;
const repo = context.repo.repo;
const root = `https://${owner}.github.io/${repo}/${base}`;
const oses = "${{ steps.arrange.outputs.found }}".trim().split(/\s+/).filter(Boolean);
const links = oses.map(os => `- [\`${os}\` reports](${root}/${os}/)`).join("\n");
const marker = "<!-- lgx-doctest-report-links -->";
const body =
`${marker}\n` +
`### 📊 lgx doc-test reports\n\n` +
`\`lgx\` built from this commit, packaging and signing real \`.lgx\` files — ` +
`rendered docs alongside the commands actually run and their output ` +
`(updated each run, commit \`${context.sha.slice(0,7)}\`):\n\n` +
`${links}\n\n` +
`_Pages can take a minute to update after the run finishes._`;
const { data: comments } = await github.rest.issues.listComments({
owner, repo, issue_number: context.issue.number, per_page: 100,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: context.issue.number, body });
}
+12
View File
@@ -2,4 +2,16 @@ build/
.DS_Store
Testing/
result
result-*
.idea/
# doctests/run.sh local-run artifacts (the specs normally run in a temp dir,
# but these can appear if you run the lgx commands by hand in the repo).
lgx
keys/
trusted/
extracted/
variant
ext
did.txt
*.lgx
+1
View File
@@ -405,3 +405,4 @@ make -j$(nproc)
cd build
ctest --output-on-failure
```
+259
View File
@@ -0,0 +1,259 @@
name: "Packaging a Module with lgx"
output: lgx-cli.md
release: ""
intro: |
[`lgx`](https://github.com/logos-co/logos-package) is the Logos **package tool**.
An `.lgx` file is just a **gzipped tar archive** with a `manifest.json` at its root
and one or more platform payloads under `variants/<platform>/`. `lgx` is the CLI that
builds, inspects, extracts, merges, and (in the companion
[signing doc-test](lgx-signing.md)) signs those archives — a self-contained C++ tool
with no Qt and no module runtime behind it.
This doc-test builds **this** `lgx` commit and drives the full packaging lifecycle:
1. Build the `lgx` CLI from this repository's flake.
2. `create` an empty package and read its `manifest`.
3. `add` this machine's platform variant and `verify` the result.
4. `extract` a variant back out, and inspect the raw archive with `tar`.
5. `merge` two single-variant packages into one multi-variant `.lgx`.
Every command is the real binary built from the commit under test, so a green run is
evidence that this change keeps the packaging workflow working end-to-end.
what_you_build: "A `greeter.lgx` package built, inspected, and extracted with `lgx`, plus a multi-variant `widget.lgx` assembled with `lgx merge`."
what_you_learn:
- What an `.lgx` package is on disk (a gzipped tar of a manifest + a `variants/` tree)
- How to create a package and read its manifest with `lgx create` / `lgx manifest`
- How to add a platform variant, verify structural validity, and extract it back out
- How to merge per-platform single-variant packages into one multi-variant `.lgx`
prerequisites:
- |
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- "A Linux or macOS machine."
sections:
- title: "Build the lgx CLI"
step: true
text: |
`lgx` ships as a C++ CLI. Build it straight from the flake's `#lgx` output and
link the result as `./lgx`, so the binary lands at `./lgx/bin/lgx`.
> The `{release}` in the URL is what pins the build to a specific commit: the
> doc-test runner expands it to a concrete ref. Locally that is this checkout's
> `HEAD` (see `run.sh`); in CI it is the commit being tested. With no pin it falls
> back to the latest `master`. Developing against a local checkout? Replace the
> GitHub reference with `.`, e.g. `nix build '.#lgx' -o lgx`.
steps:
- title: "Build lgx"
run: "nix build 'github:logos-co/logos-package{release}#lgx' -o lgx"
code_block: |
# From inside the clone this is simply: nix build '.#lgx' -o lgx
nix build 'github:logos-co/logos-package{release}#lgx' -o lgx
post_text: "The `-o lgx` flag names the result symlink, so the executable is at `./lgx/bin/lgx`."
- title: "Confirm it runs"
run: "./lgx/bin/lgx --version"
expect_contains:
- "lgx version"
check_file: "lgx/bin/lgx"
- title: "Detect the platform variant"
step: true
text: |
An `.lgx` package carries one or more **platform variants** — `linux-x86_64`,
`darwin-arm64`, and so on — each holding the payload for that platform. Detect this
machine's variant once (and the matching shared-library extension, `.so` on Linux /
`.dylib` on macOS) and stash both in files so the packaging steps below can reuse
them.
steps:
- title: "Write the variant and library extension to ./variant and ./ext"
run: |
case "$(uname -s) $(uname -m)" in
"Linux x86_64") echo linux-x86_64 > variant; echo so > ext ;;
"Linux aarch64") echo linux-arm64 > variant; echo so > ext ;;
"Darwin arm64") echo darwin-arm64 > variant; echo dylib > ext ;;
"Darwin x86_64") echo darwin-x86_64 > variant; echo dylib > ext ;;
*) echo "unsupported platform: $(uname -sm)" >&2; exit 1 ;;
esac
echo "variant: $(cat variant) ext: $(cat ext)"
expect_contains:
- "variant:"
- title: "Create a package"
step: true
text: |
`lgx create <name>` writes an empty skeleton package — a `manifest.json` with
sensible defaults plus an empty `variants/` directory — to `<name>.lgx`.
steps:
- title: "Create greeter.lgx"
run: "./lgx/bin/lgx create greeter"
expect_contains:
- "Created package: greeter.lgx"
check_file: "greeter.lgx"
- title: "Read the fresh manifest"
text: |
`lgx manifest` prints the embedded `manifest.json` in human-readable form. A
freshly created package starts at version `0.0.1`, manifest schema `0.3.0`, with
no type, no variants, and no signature yet.
run: "./lgx/bin/lgx manifest greeter.lgx"
code_block: "lgx manifest greeter.lgx"
expect_contains:
- "greeter"
- "0.0.1"
- "Manifest ver.:"
- "0.3.0"
- "Variants: (none)"
- "Signed: no"
- title: "Add a platform variant"
step: true
text: |
`lgx add` drops a payload into the package under `variants/<variant>/` and records
the entry point in the manifest's `main` map. We add a stub shared library for this
machine's variant. (In a real build the payload is the module's compiled `.so` /
`.dylib`; `lgx` does not load it — it just packages it.)
steps:
- title: "Add this machine's variant"
run: |
V="$(cat variant)"; E="$(cat ext)"
echo 'stub library' > "libgreeter.$E"
./lgx/bin/lgx add greeter.lgx --variant "$V" --files "libgreeter.$E"
code_block: |
# On Linux x86_64 this expands to:
echo 'stub library' > libgreeter.so
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
expect_contains:
- "Added variant"
- "to greeter.lgx"
post_text: "Confirm the archive layout — a `manifest.json` beside the `variants/` tree:"
extra_run:
run: "tar -tzf greeter.lgx"
post_text: |
```
manifest.json
variants/
variants/<platform>/
variants/<platform>/libgreeter.so # .dylib on macOS
```
- title: "See the variant in the manifest"
text: |
The manifest now lists the variant and points its `main` entry at the file we
added. A content hash (`Root hash`) is recomputed on every save.
run: "./lgx/bin/lgx manifest greeter.lgx"
code_block: "lgx manifest greeter.lgx"
expect_contains:
- "Root hash:"
- "Variants:"
- "libgreeter."
- title: "Verify the package"
step: true
text: |
`lgx verify` validates a package against the LGX spec — it recomputes the content
hashes (the Merkle tree) and checks them against the manifest, then reports the
signature state. Our package is unsigned, so verification passes and notes it is
unsigned. The exit code is `0` on success, so `verify` is safe to gate CI on.
steps:
- title: "Verify greeter.lgx"
run: "./lgx/bin/lgx verify greeter.lgx"
code_block: "lgx verify greeter.lgx"
expect_contains:
- "Package structure is valid: greeter.lgx"
- "Package is unsigned"
- title: "Extract a variant"
step: true
text: |
`lgx extract` unpacks a variant's payload back onto disk — the inverse of `add`.
Files land under `<output>/<variant>/`.
steps:
- title: "Extract this machine's variant"
run: |
V="$(cat variant)"
./lgx/bin/lgx extract greeter.lgx --variant "$V" --output ./extracted
code_block: |
lgx extract greeter.lgx --variant linux-x86_64 --output ./extracted
expect_contains:
- "Extracted variant"
- "./extracted"
post_text: "The payload is now on disk under `./extracted/<variant>/`:"
extra_run:
run: |
V="$(cat variant)"
ls "./extracted/$V"
code_block: "ls ./extracted/<variant>"
- title: "Merge per-platform packages"
step: true
text: |
Real releases ship one `.lgx` covering every platform. A common pattern is to build
a **single-variant** package on each platform's CI runner, then `lgx merge` them
into one multi-variant archive. The inputs must share identical metadata (only the
variant-specific `main` entries differ). We simulate it here by hand-building a
Linux and a macOS package for the same module and merging them.
steps:
- title: "Build two single-variant packages"
run: |
# A Linux-only package...
./lgx/bin/lgx create widget && mv widget.lgx widget-linux.lgx
echo 'linux stub' > libwidget.so
./lgx/bin/lgx add widget-linux.lgx --variant linux-x86_64 --files libwidget.so
# ...and a macOS-only package for the same module.
./lgx/bin/lgx create widget && mv widget.lgx widget-darwin.lgx
echo 'darwin stub' > libwidget.dylib
./lgx/bin/lgx add widget-darwin.lgx --variant darwin-arm64 --files libwidget.dylib
expect_contains:
- "Added variant"
- title: "Merge them into one .lgx"
text: |
`lgx merge` writes a single package carrying both variants. The variants are
listed alphabetically in the summary.
run: "./lgx/bin/lgx merge widget-linux.lgx widget-darwin.lgx -o widget.lgx"
code_block: "lgx merge widget-linux.lgx widget-darwin.lgx -o widget.lgx"
expect_contains:
- "Merged 2 packages into widget.lgx (darwin-arm64, linux-x86_64)"
- title: "Confirm both variants are present"
text: "The merged manifest carries both platforms, and the package verifies:"
run: "./lgx/bin/lgx manifest widget.lgx"
code_block: "lgx manifest widget.lgx"
expect_contains:
- "darwin-arm64"
- "linux-x86_64"
- run: "./lgx/bin/lgx verify widget.lgx"
code_block: "lgx verify widget.lgx"
expect_contains:
- "Package structure is valid: widget.lgx"
- title: "Recap"
text: |
You built `lgx` from this commit and ran a full packaging lifecycle — creating a
package, adding a platform variant, verifying it, extracting it, and merging
per-platform packages into one multi-variant `.lgx`:
| Command | What it does |
|---|---|
| `create <name>` | Write an empty skeleton `<name>.lgx` |
| `add <pkg> -v <variant> -f <path>` | Add a payload for a variant (records `main`) |
| `manifest <pkg> [--json]` | Print the embedded `manifest.json` (`--json` for raw bytes) |
| `verify <pkg>` | Validate structure + content hashes, report signature state |
| `extract <pkg> -v <variant> -o <dir>` | Unpack a variant's payload to disk |
| `merge <pkg…> -o <out>` | Combine single-variant packages into one multi-variant `.lgx` |
Signing, signature inspection, and trusted-key management are covered in the
companion [signing doc-test](lgx-signing.md).
+230
View File
@@ -0,0 +1,230 @@
name: "Signing and Trusting an lgx Package"
output: lgx-signing.md
release: ""
intro: |
[`lgx`](https://github.com/logos-co/logos-package) packages can be **signed** so
consumers can verify who produced them. Signing uses an **Ed25519** keypair whose public
half is published as a [`did:jwk:`](https://github.com/quartzjer/did-jwk/blob/main/spec.md)
decentralized identifier; `lgx sign` writes a `manifest.sig` next to the manifest, and
`lgx verify` checks it. A **keyring** of trusted DIDs lets `verify` go one step further
and report whether the signer is one you trust.
This doc-test builds **this** `lgx` commit and runs the full signing-and-trust lifecycle:
1. Build `lgx` and create a small package to sign.
2. `keygen` an Ed25519 keypair (printing its `did:jwk:` DID).
3. `sign` the package and inspect the signature with `manifest` and `signature`.
4. `verify` it — first as an *untrusted* signer, then after adding the DID to a keyring.
Every key directory is passed explicitly (`--output-dir`, `--keys-dir`, `--keyring-dir`)
so the whole flow is self-contained and never touches your real `~/.config/logos`.
what_you_build: "A signed `greeter.lgx`, an Ed25519 signing key, and a trusted-key keyring that makes `lgx verify` recognise the signer."
what_you_learn:
- How to generate an Ed25519 signing keypair and its `did:jwk:` DID with `lgx keygen`
- How to sign a package and read back its signature with `lgx sign` / `lgx signature`
- How `lgx verify` reports signature validity and distinguishes trusted from untrusted signers
- How to manage trusted DIDs with `lgx keyring add` / `list`
prerequisites:
- |
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- "A Linux or macOS machine."
sections:
- title: "Build lgx and create a package to sign"
step: true
text: |
Build the `lgx` CLI from this commit, then create a one-variant package — the thing
we will sign. (The [packaging doc-test](lgx-cli.md) covers `create` / `add` in
detail; here they are just setup.)
> The `{release}` placeholder pins the build to the commit under test (this
> checkout's `HEAD` locally, the PR/push commit in CI). Against a local checkout use
> `nix build '.#lgx' -o lgx`.
steps:
- title: "Build lgx"
run: "nix build 'github:logos-co/logos-package{release}#lgx' -o lgx"
code_block: |
# From inside the clone this is simply: nix build '.#lgx' -o lgx
nix build 'github:logos-co/logos-package{release}#lgx' -o lgx
check_file: "lgx/bin/lgx"
- title: "Create and populate greeter.lgx"
text: |
Detect this machine's variant (and shared-library extension), create the
package, and add a stub payload so there is content to sign.
run: |
case "$(uname -s) $(uname -m)" in
"Linux x86_64") V=linux-x86_64; E=so ;;
"Linux aarch64") V=linux-arm64; E=so ;;
"Darwin arm64") V=darwin-arm64; E=dylib ;;
"Darwin x86_64") V=darwin-x86_64; E=dylib ;;
*) echo "unsupported platform: $(uname -sm)" >&2; exit 1 ;;
esac
./lgx/bin/lgx create greeter
echo 'stub library' > "libgreeter.$E"
./lgx/bin/lgx add greeter.lgx --variant "$V" --files "libgreeter.$E"
code_block: |
lgx create greeter
echo 'stub library' > libgreeter.so # .dylib on macOS
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
expect_contains:
- "Added variant"
check_file: "greeter.lgx"
- title: "Generate a signing key"
step: true
text: |
`lgx keygen` creates an Ed25519 keypair and writes three files — the secret key
(`.jwk`), the public key (`.pub`), and the DID string (`.did`). It prints the
`did:jwk:` DID to stdout. We point `--output-dir` at a local `./keys` directory and
stash the DID in a file so later steps can refer to it.
steps:
- title: "Run keygen into ./keys"
text: |
`keygen` prints the DID to stdout and also writes it to `./keys/demo-key.did`. We
copy that file to `./did.txt` so later steps can pass the exact DID without
re-deriving it.
run: |
./lgx/bin/lgx keygen --name demo-key --output-dir ./keys
cp ./keys/demo-key.did did.txt
code_block: |
lgx keygen --name demo-key --output-dir ./keys
# → did:jwk:eyJjcnYiOiJFZDI1NTE5Ii... (also saved to ./keys/demo-key.did)
expect_contains:
- "did:jwk:"
- "Keypair generated:"
- "demo-key.{jwk,pub,did}"
check_file: "keys/demo-key.jwk"
post_text: |
The keypair now lives under `./keys/` as `demo-key.{jwk,pub,did}`. The `.jwk`
file is the **secret** key — keep it safe; only the DID is meant to be shared.
- title: "Sign the package"
step: true
text: |
`lgx sign` validates the package, then writes a `manifest.sig` containing the
signer's DID, an Ed25519 signature over the manifest bytes, and optional
self-asserted signer metadata (`--name` / `--url`). We pass `--keys-dir ./keys` so it
finds the key we just generated.
steps:
- title: "Sign greeter.lgx"
run: |
./lgx/bin/lgx sign greeter.lgx --key demo-key --keys-dir ./keys \
--name "Logos Demo" --url "https://logos.co"
code_block: |
lgx sign greeter.lgx --key demo-key --keys-dir ./keys \
--name "Logos Demo" --url "https://logos.co"
expect_contains:
- "Package signed: greeter.lgx"
- "Signer DID:"
- title: "See the signature in the manifest"
text: |
`lgx manifest` now reports the package as signed, along with the signer's DID and
self-asserted name/URL.
run: "./lgx/bin/lgx manifest greeter.lgx"
code_block: "lgx manifest greeter.lgx"
expect_contains:
- "Signed: yes"
- "Signer DID:"
- "Logos Demo (self-asserted)"
- title: "Read the raw signature"
text: |
`lgx signature` prints the raw `manifest.sig` JSON — byte-identical to the file
inside the `.lgx`. It carries the algorithm, the signer DID, the base64
signature, and the signer metadata. Unsigned packages print nothing and exit 0,
so tooling tells "unsigned" from "error" by the exit status, not the output.
run: "./lgx/bin/lgx signature greeter.lgx"
code_block: "lgx signature greeter.lgx"
expect_contains:
- '"algorithm": "ed25519"'
- '"did":'
- '"signature":'
- title: "Verify an untrusted signer"
step: true
text: |
`lgx verify` always checks structure and (for signed packages) the signature itself.
Trust is separate: with no matching entry in the keyring, the signature is reported
**valid** but the signer is flagged as **not** trusted. We point `--keyring-dir` at an
empty `./trusted` directory to make that explicit.
steps:
- title: "Verify against an empty keyring"
run: |
mkdir -p trusted
./lgx/bin/lgx verify greeter.lgx --keyring-dir ./trusted
code_block: |
mkdir -p trusted
lgx verify greeter.lgx --keyring-dir ./trusted
expect_contains:
- "Package structure is valid: greeter.lgx"
- "Signature is valid"
- "Signer DID is NOT in trusted keyring"
- title: "Trust the signer"
step: true
text: |
`lgx keyring add` records a DID as trusted under a short name. We add the DID we
stashed earlier (still in `did.txt`) to the `./trusted` keyring, then list it.
steps:
- title: "Add the DID to the keyring"
run: |
./lgx/bin/lgx keyring add logos-demo "$(cat did.txt)" \
--display-name "Logos Demo" --url "https://logos.co" --dir ./trusted
code_block: |
lgx keyring add logos-demo "$DID" \
--display-name "Logos Demo" --url "https://logos.co" --dir ./trusted
expect_contains:
- "Added trusted key: logos-demo"
- "DID: did:jwk:"
- title: "List trusted keys"
run: "./lgx/bin/lgx keyring list --dir ./trusted"
code_block: "lgx keyring list --dir ./trusted"
expect_contains:
- "logos-demo (Logos Demo)"
- "DID: did:jwk:"
- title: "Verify a trusted signer"
step: true
text: |
Re-run `verify` against the populated keyring. Now that the signer's DID is trusted,
`verify` adds a final line naming the trusted key — the same valid signature, now
recognised.
steps:
- title: "Verify against the populated keyring"
run: "./lgx/bin/lgx verify greeter.lgx --keyring-dir ./trusted"
code_block: "lgx verify greeter.lgx --keyring-dir ./trusted"
expect_contains:
- "Signature is valid"
- "Signer is trusted: logos-demo"
- title: "Recap"
text: |
You built `lgx` from this commit and ran the full signing-and-trust lifecycle —
generating a key, signing a package, inspecting the signature, and verifying it both
as an untrusted and a trusted signer:
| Command | What it does |
|---|---|
| `keygen --name <n> --output-dir <dir>` | Generate an Ed25519 keypair; print its `did:jwk:` DID |
| `sign <pkg> --key <n> --keys-dir <dir> [--name …] [--url …]` | Write `manifest.sig` with the signer's DID |
| `signature <pkg>` | Print the raw `manifest.sig` JSON (unsigned → empty + exit 0) |
| `verify <pkg> [--keyring-dir <dir>]` | Validate structure + signature; report trust |
| `keyring add <name> <did> [--dir <dir>]` | Trust a DID under a short name |
| `keyring list [--dir <dir>]` | List trusted DIDs |
Creating, adding variants, extracting, and merging packages are covered in the
companion [packaging doc-test](lgx-cli.md).
+255
View File
@@ -0,0 +1,255 @@
# Packaging a Module with lgx
[`lgx`](https://github.com/logos-co/logos-package) is the Logos **package tool**.
An `.lgx` file is just a **gzipped tar archive** with a `manifest.json` at its root
and one or more platform payloads under `variants/<platform>/`. `lgx` is the CLI that
builds, inspects, extracts, merges, and (in the companion
[signing doc-test](lgx-signing.md)) signs those archives — a self-contained C++ tool
with no Qt and no module runtime behind it.
This doc-test builds **this** `lgx` commit and drives the full packaging lifecycle:
1. Build the `lgx` CLI from this repository's flake.
2. `create` an empty package and read its `manifest`.
3. `add` this machine's platform variant and `verify` the result.
4. `extract` a variant back out, and inspect the raw archive with `tar`.
5. `merge` two single-variant packages into one multi-variant `.lgx`.
Every command is the real binary built from the commit under test, so a green run is
evidence that this change keeps the packaging workflow working end-to-end.
**What you'll build:** A `greeter.lgx` package built, inspected, and extracted with `lgx`, plus a multi-variant `widget.lgx` assembled with `lgx merge`.
**What you'll learn:**
- What an `.lgx` package is on disk (a gzipped tar of a manifest + a `variants/` tree)
- How to create a package and read its manifest with `lgx create` / `lgx manifest`
- How to add a platform variant, verify structural validity, and extract it back out
- How to merge per-platform single-variant packages into one multi-variant `.lgx`
## Prerequisites
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- A Linux or macOS machine.
---
## Step 1: Build the lgx CLI
`lgx` ships as a C++ CLI. Build it straight from the flake's `#lgx` output and
link the result as `./lgx`, so the binary lands at `./lgx/bin/lgx`.
> The `` in the URL is what pins the build to a specific commit: the
> doc-test runner expands it to a concrete ref. Locally that is this checkout's
> `HEAD` (see `run.sh`); in CI it is the commit being tested. With no pin it falls
> back to the latest `master`. Developing against a local checkout? Replace the
> GitHub reference with `.`, e.g. `nix build '.#lgx' -o lgx`.
### 1.1 Build lgx
```bash
# From inside the clone this is simply: nix build '.#lgx' -o lgx
nix build 'github:logos-co/logos-package/41cae9e4546899eb11912372588a6707ca43efe4#lgx' -o lgx
```
The `-o lgx` flag names the result symlink, so the executable is at `./lgx/bin/lgx`.
### 1.2 Confirm it runs
```bash
./lgx/bin/lgx --version
```
---
## Step 2: Detect the platform variant
An `.lgx` package carries one or more **platform variants**`linux-x86_64`,
`darwin-arm64`, and so on — each holding the payload for that platform. Detect this
machine's variant once (and the matching shared-library extension, `.so` on Linux /
`.dylib` on macOS) and stash both in files so the packaging steps below can reuse
them.
### 2.1 Write the variant and library extension to ./variant and ./ext
```bash
case "$(uname -s) $(uname -m)" in
"Linux x86_64") echo linux-x86_64 > variant; echo so > ext ;;
"Linux aarch64") echo linux-arm64 > variant; echo so > ext ;;
"Darwin arm64") echo darwin-arm64 > variant; echo dylib > ext ;;
"Darwin x86_64") echo darwin-x86_64 > variant; echo dylib > ext ;;
*) echo "unsupported platform: $(uname -sm)" >&2; exit 1 ;;
esac
echo "variant: $(cat variant) ext: $(cat ext)"
```
---
## Step 3: Create a package
`lgx create <name>` writes an empty skeleton package — a `manifest.json` with
sensible defaults plus an empty `variants/` directory — to `<name>.lgx`.
### 3.1 Create greeter.lgx
```bash
./lgx/bin/lgx create greeter
```
### 3.2 Read the fresh manifest
`lgx manifest` prints the embedded `manifest.json` in human-readable form. A
freshly created package starts at version `0.0.1`, manifest schema `0.3.0`, with
no type, no variants, and no signature yet.
```bash
lgx manifest greeter.lgx
```
---
## Step 4: Add a platform variant
`lgx add` drops a payload into the package under `variants/<variant>/` and records
the entry point in the manifest's `main` map. We add a stub shared library for this
machine's variant. (In a real build the payload is the module's compiled `.so` /
`.dylib`; `lgx` does not load it — it just packages it.)
### 4.1 Add this machine's variant
```bash
# On Linux x86_64 this expands to:
echo 'stub library' > libgreeter.so
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
```
Confirm the archive layout — a `manifest.json` beside the `variants/` tree:
```bash
tar -tzf greeter.lgx
```
```
manifest.json
variants/
variants/<platform>/
variants/<platform>/libgreeter.so # .dylib on macOS
```
### 4.2 See the variant in the manifest
The manifest now lists the variant and points its `main` entry at the file we
added. A content hash (`Root hash`) is recomputed on every save.
```bash
lgx manifest greeter.lgx
```
---
## Step 5: Verify the package
`lgx verify` validates a package against the LGX spec — it recomputes the content
hashes (the Merkle tree) and checks them against the manifest, then reports the
signature state. Our package is unsigned, so verification passes and notes it is
unsigned. The exit code is `0` on success, so `verify` is safe to gate CI on.
### 5.1 Verify greeter.lgx
```bash
lgx verify greeter.lgx
```
---
## Step 6: Extract a variant
`lgx extract` unpacks a variant's payload back onto disk — the inverse of `add`.
Files land under `<output>/<variant>/`.
### 6.1 Extract this machine's variant
```bash
lgx extract greeter.lgx --variant linux-x86_64 --output ./extracted
```
The payload is now on disk under `./extracted/<variant>/`:
```bash
ls ./extracted/<variant>
```
---
## Step 7: Merge per-platform packages
Real releases ship one `.lgx` covering every platform. A common pattern is to build
a **single-variant** package on each platform's CI runner, then `lgx merge` them
into one multi-variant archive. The inputs must share identical metadata (only the
variant-specific `main` entries differ). We simulate it here by hand-building a
Linux and a macOS package for the same module and merging them.
### 7.1 Build two single-variant packages
```bash
# A Linux-only package...
./lgx/bin/lgx create widget && mv widget.lgx widget-linux.lgx
echo 'linux stub' > libwidget.so
./lgx/bin/lgx add widget-linux.lgx --variant linux-x86_64 --files libwidget.so
# ...and a macOS-only package for the same module.
./lgx/bin/lgx create widget && mv widget.lgx widget-darwin.lgx
echo 'darwin stub' > libwidget.dylib
./lgx/bin/lgx add widget-darwin.lgx --variant darwin-arm64 --files libwidget.dylib
```
### 7.2 Merge them into one .lgx
`lgx merge` writes a single package carrying both variants. The variants are
listed alphabetically in the summary.
```bash
lgx merge widget-linux.lgx widget-darwin.lgx -o widget.lgx
```
### 7.3 Confirm both variants are present
The merged manifest carries both platforms, and the package verifies:
```bash
lgx manifest widget.lgx
```
```bash
lgx verify widget.lgx
```
---
## Recap
You built `lgx` from this commit and ran a full packaging lifecycle — creating a
package, adding a platform variant, verifying it, extracting it, and merging
per-platform packages into one multi-variant `.lgx`:
| Command | What it does |
|---|---|
| `create <name>` | Write an empty skeleton `<name>.lgx` |
| `add <pkg> -v <variant> -f <path>` | Add a payload for a variant (records `main`) |
| `manifest <pkg> [--json]` | Print the embedded `manifest.json` (`--json` for raw bytes) |
| `verify <pkg>` | Validate structure + content hashes, report signature state |
| `extract <pkg> -v <variant> -o <dir>` | Unpack a variant's payload to disk |
| `merge <pkg…> -o <out>` | Combine single-variant packages into one multi-variant `.lgx` |
Signing, signature inspection, and trusted-key management are covered in the
companion [signing doc-test](lgx-signing.md).
+199
View File
@@ -0,0 +1,199 @@
# Signing and Trusting an lgx Package
[`lgx`](https://github.com/logos-co/logos-package) packages can be **signed** so
consumers can verify who produced them. Signing uses an **Ed25519** keypair whose public
half is published as a [`did:jwk:`](https://github.com/quartzjer/did-jwk/blob/main/spec.md)
decentralized identifier; `lgx sign` writes a `manifest.sig` next to the manifest, and
`lgx verify` checks it. A **keyring** of trusted DIDs lets `verify` go one step further
and report whether the signer is one you trust.
This doc-test builds **this** `lgx` commit and runs the full signing-and-trust lifecycle:
1. Build `lgx` and create a small package to sign.
2. `keygen` an Ed25519 keypair (printing its `did:jwk:` DID).
3. `sign` the package and inspect the signature with `manifest` and `signature`.
4. `verify` it — first as an *untrusted* signer, then after adding the DID to a keyring.
Every key directory is passed explicitly (`--output-dir`, `--keys-dir`, `--keyring-dir`)
so the whole flow is self-contained and never touches your real `~/.config/logos`.
**What you'll build:** A signed `greeter.lgx`, an Ed25519 signing key, and a trusted-key keyring that makes `lgx verify` recognise the signer.
**What you'll learn:**
- How to generate an Ed25519 signing keypair and its `did:jwk:` DID with `lgx keygen`
- How to sign a package and read back its signature with `lgx sign` / `lgx signature`
- How `lgx verify` reports signature validity and distinguishes trusted from untrusted signers
- How to manage trusted DIDs with `lgx keyring add` / `list`
## Prerequisites
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- A Linux or macOS machine.
---
## Step 1: Build lgx and create a package to sign
Build the `lgx` CLI from this commit, then create a one-variant package — the thing
we will sign. (The [packaging doc-test](lgx-cli.md) covers `create` / `add` in
detail; here they are just setup.)
> The `` placeholder pins the build to the commit under test (this
> checkout's `HEAD` locally, the PR/push commit in CI). Against a local checkout use
> `nix build '.#lgx' -o lgx`.
### 1.1 Build lgx
```bash
# From inside the clone this is simply: nix build '.#lgx' -o lgx
nix build 'github:logos-co/logos-package/41cae9e4546899eb11912372588a6707ca43efe4#lgx' -o lgx
```
### 1.2 Create and populate greeter.lgx
Detect this machine's variant (and shared-library extension), create the
package, and add a stub payload so there is content to sign.
```bash
lgx create greeter
echo 'stub library' > libgreeter.so # .dylib on macOS
lgx add greeter.lgx --variant linux-x86_64 --files libgreeter.so
```
---
## Step 2: Generate a signing key
`lgx keygen` creates an Ed25519 keypair and writes three files — the secret key
(`.jwk`), the public key (`.pub`), and the DID string (`.did`). It prints the
`did:jwk:` DID to stdout. We point `--output-dir` at a local `./keys` directory and
stash the DID in a file so later steps can refer to it.
### 2.1 Run keygen into ./keys
`keygen` prints the DID to stdout and also writes it to `./keys/demo-key.did`. We
copy that file to `./did.txt` so later steps can pass the exact DID without
re-deriving it.
```bash
lgx keygen --name demo-key --output-dir ./keys
# → did:jwk:eyJjcnYiOiJFZDI1NTE5Ii... (also saved to ./keys/demo-key.did)
```
The keypair now lives under `./keys/` as `demo-key.{jwk,pub,did}`. The `.jwk`
file is the **secret** key — keep it safe; only the DID is meant to be shared.
---
## Step 3: Sign the package
`lgx sign` validates the package, then writes a `manifest.sig` containing the
signer's DID, an Ed25519 signature over the manifest bytes, and optional
self-asserted signer metadata (`--name` / `--url`). We pass `--keys-dir ./keys` so it
finds the key we just generated.
### 3.1 Sign greeter.lgx
```bash
lgx sign greeter.lgx --key demo-key --keys-dir ./keys \
--name "Logos Demo" --url "https://logos.co"
```
### 3.2 See the signature in the manifest
`lgx manifest` now reports the package as signed, along with the signer's DID and
self-asserted name/URL.
```bash
lgx manifest greeter.lgx
```
### 3.3 Read the raw signature
`lgx signature` prints the raw `manifest.sig` JSON — byte-identical to the file
inside the `.lgx`. It carries the algorithm, the signer DID, the base64
signature, and the signer metadata. Unsigned packages print nothing and exit 0,
so tooling tells "unsigned" from "error" by the exit status, not the output.
```bash
lgx signature greeter.lgx
```
---
## Step 4: Verify an untrusted signer
`lgx verify` always checks structure and (for signed packages) the signature itself.
Trust is separate: with no matching entry in the keyring, the signature is reported
**valid** but the signer is flagged as **not** trusted. We point `--keyring-dir` at an
empty `./trusted` directory to make that explicit.
### 4.1 Verify against an empty keyring
```bash
mkdir -p trusted
lgx verify greeter.lgx --keyring-dir ./trusted
```
---
## Step 5: Trust the signer
`lgx keyring add` records a DID as trusted under a short name. We add the DID we
stashed earlier (still in `did.txt`) to the `./trusted` keyring, then list it.
### 5.1 Add the DID to the keyring
```bash
lgx keyring add logos-demo "$DID" \
--display-name "Logos Demo" --url "https://logos.co" --dir ./trusted
```
### 5.2 List trusted keys
```bash
lgx keyring list --dir ./trusted
```
---
## Step 6: Verify a trusted signer
Re-run `verify` against the populated keyring. Now that the signer's DID is trusted,
`verify` adds a final line naming the trusted key — the same valid signature, now
recognised.
### 6.1 Verify against the populated keyring
```bash
lgx verify greeter.lgx --keyring-dir ./trusted
```
---
## Recap
You built `lgx` from this commit and ran the full signing-and-trust lifecycle —
generating a key, signing a package, inspecting the signature, and verifying it both
as an untrusted and a trusted signer:
| Command | What it does |
|---|---|
| `keygen --name <n> --output-dir <dir>` | Generate an Ed25519 keypair; print its `did:jwk:` DID |
| `sign <pkg> --key <n> --keys-dir <dir> [--name …] [--url …]` | Write `manifest.sig` with the signer's DID |
| `signature <pkg>` | Print the raw `manifest.sig` JSON (unsigned → empty + exit 0) |
| `verify <pkg> [--keyring-dir <dir>]` | Validate structure + signature; report trust |
| `keyring add <name> <did> [--dir <dir>]` | Trust a DID under a short name |
| `keyring list [--dir <dir>]` | List trusted DIDs |
Creating, adding variants, extracting, and merging packages are covered in the
companion [packaging doc-test](lgx-cli.md).
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
#
# Execute every lgx doc-test end-to-end and regenerate its Markdown.
#
# The runner is the shared `doctest` CLI
# (https://github.com/logos-co/logos-doctest), invoked directly via its flake.
# For each *.test.yaml in this directory, `doctest run` executes every command in
# a temp directory (building lgx, creating/signing/verifying .lgx packages) and
# asserts on the output; `doctest generate` renders the same spec to Markdown
# under outputs/ (the *.test.yaml spec is the source of truth).
#
# To run against a local logos-doctest checkout instead of the published flake,
# set DOCTEST, e.g.: DOCTEST="nix run path:../../logos-doctest --" ./run.sh
#
set -euo pipefail
# Run from this doctests/ directory regardless of where the script is invoked from.
cd "$(dirname "$0")"
# The doctest CLI. Override by exporting DOCTEST (space-separated command).
read -r -a DOCTEST <<< "${DOCTEST:-nix run github:logos-co/logos-doctest --}"
# Build the doc-tests against THIS repo's current commit rather than the latest
# published flake. Each spec's `github:logos-co/logos-package{release}` URL is
# pinned to $COMMIT via --release-for, so the run exercises exactly what's checked
# out here. Override by exporting COMMIT (e.g. a tag), or set COMMIT="" to fall
# back to latest.
#
# Note: nix fetches the commit from the GitHub remote, so $COMMIT must be pushed
# to logos-co/logos-package. A local-only / uncommitted HEAD won't resolve;
# export COMMIT="" (or push first) in that case.
COMMIT="${COMMIT-$(git rev-parse HEAD)}"
RELEASE_FOR=()
if [ -n "${COMMIT}" ]; then
RELEASE_FOR=(--release-for "logos-package=${COMMIT}")
echo "==> Pinning logos-package to ${COMMIT}"
else
echo "==> COMMIT empty; building logos-package from latest"
fi
mkdir -p outputs
for spec in *.test.yaml; do
name="$(basename "${spec%.test.yaml}")"
echo "==> Running ${spec}"
# ${RELEASE_FOR[@]+...} guards the expansion so an empty array doesn't trip
# `set -u` on older bash (e.g. macOS's stock 3.2).
"${DOCTEST[@]}" run "${spec}" --verbose --continue-on-fail \
${RELEASE_FOR[@]+"${RELEASE_FOR[@]}"}
echo "==> Generating outputs/${name}.md"
"${DOCTEST[@]}" generate "${spec}" \
${RELEASE_FOR[@]+"${RELEASE_FOR[@]}"} \
-o "outputs/${name}.md"
done
echo "==> Done. Rendered docs are in doctests/outputs/"