diff --git a/.github/scripts/doc-version-selector.css b/.github/scripts/doc-version-selector.css new file mode 100644 index 0000000..3372b45 --- /dev/null +++ b/.github/scripts/doc-version-selector.css @@ -0,0 +1,27 @@ +#c2pa-doc-version-selector { + display: inline-flex; + align-items: center; + gap: 0.4rem; + white-space: nowrap; +} + +#c2pa-doc-version-selector select { + max-width: 10rem; + padding: 0.25rem; + border: 1px solid currentColor; + border-radius: 0.25rem; + color: inherit; + background: transparent; + font: inherit; +} + +#c2pa-doc-version-selector option { + color: CanvasText; + background: Canvas; +} + +@media print { + #c2pa-doc-version-selector { + display: none; + } +} diff --git a/.github/scripts/doc-version-selector.js b/.github/scripts/doc-version-selector.js new file mode 100644 index 0000000..2e04db3 --- /dev/null +++ b/.github/scripts/doc-version-selector.js @@ -0,0 +1,29 @@ +(function () { + "use strict"; + + const script = document.currentScript; + const versions = JSON.parse(script.dataset.versions); + const assets = new URL(".", script.src); + const root = new URL(script.dataset.prefix ? "../../" : "./", assets); + const container = document.getElementById("library-version"); + if (!container) { + throw new Error("Dokka did not provide a library-version header slot."); + } + const label = document.createElement("label"); + label.id = "c2pa-doc-version-selector"; + label.textContent = "Version: "; + const select = document.createElement("select"); + select.setAttribute("aria-label", "API documentation version"); + for (const tag of versions) { + const option = document.createElement("option"); + option.value = new URL("versions/" + tag + "/", root).href; + option.textContent = tag; + option.selected = tag === script.dataset.version; + select.appendChild(option); + } + select.addEventListener("change", function () { + window.location.assign(select.value); + }); + label.appendChild(select); + container.replaceChildren(label); +})(); diff --git a/.github/scripts/generate-versioned-docs.sh b/.github/scripts/generate-versioned-docs.sh new file mode 100644 index 0000000..16824e7 --- /dev/null +++ b/.github/scripts/generate-versioned-docs.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PACKAGE_INDEX=c2pa-android/org.contentauth.c2pa/index.html + +fail() { + printf '%s\n' "$*" >&2 + return 1 +} + +load_versions() { + local filename="$1" + jq -e --arg pattern '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9][A-Za-z0-9.-]*)?\z' ' + if type != "object" then false + elif (.versions | type) != "array" then false + else .versions | length > 0 and + all(.[]; if type == "string" then test($pattern) else false end) and + (length == (unique | length)) + end + ' "$filename" >/dev/null || { fail "Documentation versions must be unique release tags in a non-empty list."; return 1; } + jq -r '.versions[]' "$filename" +} + +verify_release() { + local tag="$1" release + release="$(gh api "repos/contentauth/c2pa-android/releases/tags/$tag")" || return 1 + printf '%s' "$release" | jq -e --arg tag "$tag" ' + type == "object" and .tag_name == $tag and .draft == false + ' >/dev/null || { fail "$tag is not a published GitHub release."; return 1; } +} + +export_release() { + local root="$1" tag="$2" destination="$3" commit entry mode path + commit="$(git -C "$root" rev-parse --verify "refs/tags/$tag^{commit}")" || return 1 + git -C "$root" ls-tree -rz --full-tree "$commit" > "$destination.tree" || return 1 + while IFS= read -r -d '' entry; do + mode="${entry%% *}" + path="${entry#*$'\t'}" + case "$mode" in + 100644|100755) ;; + *) fail "Unsafe source entry in release $tag: $path"; return 1 ;; + esac + case "/$path/" in + //*|*/../*|*/./*|*/.git/*|*\\*|*:*) fail "Unsafe source path in release $tag: $path"; return 1 ;; + esac + done < "$destination.tree" + git -C "$root" archive --format=tar "$commit" > "$destination.tar" || return 1 + tar -xf "$destination.tar" -C "$destination" || return 1 + local required + for required in gradlew gradle/wrapper/gradle-wrapper.jar library/build.gradle.kts library/MODULE.md; do + [[ -f "$destination/$required" ]] || { fail "Release $tag is missing its Gradle wrapper or Kotlin API sources."; return 1; } + done + [[ -n "$(find "$destination/library/src/main/kotlin" -type f -name '*.kt' -print -quit)" ]] || { + fail "Release $tag is missing Kotlin API sources."; return 1; + } +} + +build_docs() { + local stage="$1" + ( + cd "$stage" + unset GH_TOKEN GITHUB_TOKEN + ./gradlew --no-daemon --console=plain :library:dokkaGeneratePublicationHtml --rerun-tasks + ) || return 1 + [[ -f "$stage/build/docs/index.html" && -f "$stage/build/docs/$PACKAGE_INDEX" ]] || { + fail "Dokka did not generate the library and C2PA package landing pages."; return 1; + } +} + +render_docs() { + local source="$1" output="$2" tag="$3" versions="$4" prefix="$5" + local page relative directory assets additions escaped_tag escaped_versions escaped_prefix + mkdir -p "$output" + cp -R "$source/." "$output/" + escaped_tag="$(printf '%s' "$tag" | jq -Rr '@html')" + escaped_versions="$(printf '%s' "$versions" | jq -Rr '@html')" + escaped_prefix="$(printf '%s' "$prefix" | jq -Rr '@html')" + find "$output" -type f -name '*.html' -print0 > "$output.pages" + while IFS= read -r -d '' page; do + relative="${page#"$output/"}" + [[ "$relative" != navigation.html ]] || continue + assets="" + directory="$relative" + while [[ "$directory" == */* ]]; do + assets="../$assets" + directory="${directory#*/}" + done + additions=" +" + DOC_VERSION_ADDITIONS="$additions" awk ' + BEGIN { additions = ENVIRON["DOC_VERSION_ADDITIONS"] } + { + if (index($0, "id=\"library-version\"")) slot = 1 + position = index($0, "") + if (position) { + head = 1 + printf "%s%s\n%s\n", substr($0, 1, position - 1), additions, substr($0, position) + } else print + } + END { if (!head || !slot) exit 1 } + ' "$page" > "$page.tmp" || { fail "Dokka page is missing its head or version slot: $relative"; return 1; } + mv "$page.tmp" "$page" + done < "$output.pages" + rm "$output.pages" + cp "$ROOT/.github/scripts/doc-version-selector.js" "$ROOT/.github/scripts/doc-version-selector.css" "$output/" +} + +build_site() ( + set -euo pipefail + local versions=() tag versions_json workspace output stage default_docs="" + mkdir -p "$ROOT/build" + output="$ROOT/build/docs-versioned" + workspace="$(mktemp -d "$ROOT/build/versioned-docs-XXXXXX")" + trap 'status=$?; if [[ -d "$workspace/previous" && ! -e "$output" ]]; then mv "$workspace/previous" "$output" || status=1; fi; rm -rf -- "$workspace"; exit "$status"' EXIT + load_versions "$ROOT/docs/versions.json" > "$workspace/versions" + while IFS= read -r tag; do versions+=("$tag"); done < "$workspace/versions" + versions_json="$(jq -c '.versions' "$ROOT/docs/versions.json")" + for tag in "${versions[@]}"; do verify_release "$tag"; done + for tag in "${versions[@]}"; do + stage="$workspace/$tag" + mkdir "$stage" + export_release "$ROOT" "$tag" "$stage" + build_docs "$stage" + [[ "$tag" != "${versions[0]}" ]] || default_docs="$stage/build/docs" + render_docs "$stage/build/docs" "$workspace/site/versions/$tag" "$tag" "$versions_json" "versions/$tag" + done + render_docs "$default_docs" "$workspace/default" "${versions[0]}" "$versions_json" "" + cp -R "$workspace/default/." "$workspace/site/" + touch "$workspace/site/.nojekyll" + if [[ -e "$output" ]]; then mv "$output" "$workspace/previous"; fi + mv "$workspace/site" "$output" + printf 'Versioned Dokka documentation generated at: %s\n' "$output" +) + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + build_site +fi \ No newline at end of file diff --git a/.github/scripts/test-doc-version-selector.cjs b/.github/scripts/test-doc-version-selector.cjs new file mode 100644 index 0000000..287ad66 --- /dev/null +++ b/.github/scripts/test-doc-version-selector.cjs @@ -0,0 +1,73 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const test = require("node:test"); +const vm = require("node:vm"); + +const source = fs.readFileSync(path.join(__dirname, "doc-version-selector.js"), "utf8"); +const versions = ["0.0.15", "0.0.14"]; + +function element() { + return { + children: [], + appendChild(child) { this.children.push(child); }, + replaceChildren(child) { this.children = [child]; }, + setAttribute(name, value) { this[name] = value; }, + addEventListener(event, listener) { this[event] = listener; } + }; +} + +for (const base of [ + "https://contentauth.github.io/c2pa-android/", + "https://docs.example.test/nested/sdk/" +]) { + for (const prefix of ["", ...versions.map(tag => "versions/" + tag)]) { + test("Dokka version selector: " + base + (prefix || "default"), function () { + const tag = prefix ? prefix.split("/").pop() : versions[0]; + const container = element(); + let destination; + vm.runInNewContext(source, { + URL, + document: { + currentScript: { + src: new URL((prefix ? prefix + "/" : "") + "doc-version-selector.js", base).href, + dataset: { versions: JSON.stringify(versions), version: tag, prefix } + }, + getElementById(id) { + assert.equal(id, "library-version"); + return container; + }, + createElement: element + }, + window: { location: { assign(url) { destination = url; } } } + }); + assert.equal(container.children.length, 1); + const label = container.children[0]; + assert.equal(label.id, "c2pa-doc-version-selector"); + const select = label.children[0]; + assert.equal(select["aria-label"], "API documentation version"); + assert.equal(select.children.length, versions.length); + for (const [index, option] of select.children.entries()) { + assert.equal(option.value, base + "versions/" + versions[index] + "/"); + assert.equal(option.selected, versions[index] === tag); + assert.equal(option.textContent, versions[index]); + } + select.value = select.children[1].value; + select.change(); + assert.equal(destination, base + "versions/0.0.14/"); + }); + } +} + +test("A missing Dokka version slot fails explicitly", function () { + assert.throws(() => vm.runInNewContext(source, { + URL, + document: { + currentScript: { + src: "https://contentauth.github.io/c2pa-android/doc-version-selector.js", + dataset: { versions: JSON.stringify(versions), version: versions[0], prefix: "" } + }, + getElementById() { return null; } + } + }), /Dokka did not provide a library-version header slot/); +}); diff --git a/.github/scripts/test-versioned-docs.sh b/.github/scripts/test-versioned-docs.sh new file mode 100644 index 0000000..c97e764 --- /dev/null +++ b/.github/scripts/test-versioned-docs.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +set -euo pipefail + +TEST_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/test-versioned-docs.sh" +source "$(dirname "$TEST_SCRIPT")/generate-versioned-docs.sh" +SOURCE_ROOT="$ROOT" + +gh() { + [[ "$1" == api ]] || return 1 + printf '{"tag_name":"%s","draft":false}\n' "${2##*/}" +} + +if [[ "${1:-}" == --build ]]; then + ROOT="$2" + build_site + exit +fi + +TEST_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/versioned-docs-tests-XXXXXX")" +trap 'rm -rf -- "$TEST_ROOT"' EXIT + +expect_failure() { + if "$@" > "$ROOT/failure.log" 2>&1; then + cat "$ROOT/failure.log" + fail "Expected command to fail: $*" + return 1 + fi +} + +make_dokka() { + local output="$1" relative + mkdir -p "$output/scripts" + for relative in index.html c2pa-android/org.contentauth.c2pa/index.html c2pa-android/org.contentauth.c2pa/-reader/index.html; do + mkdir -p "$(dirname "$output/$relative")" + printf '%s\n' 'C2PA
' > "$output/$relative" + done + printf '%s\n' '
Navigation
' > "$output/navigation.html" + printf '%s\n' '// Existing asset' > "$output/scripts/main.js" +} + +configure_root() { + mkdir -p "$ROOT/docs" "$ROOT/.github/scripts" "$ROOT/build/docs-versioned" "$ROOT/build/docs" + printf '%s\n' '{"versions":["0.0.15","0.0.14"]}' > "$ROOT/docs/versions.json" + cp "$SOURCE_ROOT/.github/scripts/doc-version-selector.js" "$SOURCE_ROOT/.github/scripts/doc-version-selector.css" "$ROOT/.github/scripts/" + printf 'Existing site\n' > "$ROOT/build/docs-versioned/previous.html" + printf 'Working-tree site\n' > "$ROOT/build/docs/working.html" +} + +commit_sources() { + git -C "$ROOT" add -- gradlew gradle library + git -C "$ROOT" -c user.name=Fixture -c user.email=fixture@example.invalid -c commit.gpgsign=false commit -qm fixture +} + +prepare_releases() { + configure_root + git -C "$ROOT" init -q + mkdir -p "$ROOT/gradle/wrapper" "$ROOT/library/src/main/kotlin" + printf fixture > "$ROOT/gradle/wrapper/gradle-wrapper.jar" + printf fixture > "$ROOT/library/build.gradle.kts" + printf '# Module\n' > "$ROOT/library/MODULE.md" + printf 'class Reader\n' > "$ROOT/library/src/main/kotlin/Reader.kt" + { + printf '#!/usr/bin/env bash\nset -euo pipefail\n' + declare -f make_dokka + printf '%s\n' '[[ -z "${GH_TOKEN+x}" && -z "${GITHUB_TOKEN+x}" ]] || exit 70' 'if [[ -f library/FAIL_BUILD ]]; then exit 71; fi' 'make_dokka "$PWD/build/docs"' + } > "$ROOT/gradlew" + chmod +x "$ROOT/gradlew" + commit_sources + git -C "$ROOT" tag 0.0.15 + git -C "$ROOT" tag 0.0.14 +} + +test_versions_preserve_order() { + printf '%s\n' '{"versions":["0.0.16-beta.1","0.0.15"]}' > "$ROOT/config.json" + [[ "$(load_versions "$ROOT/config.json")" == $'0.0.16-beta.1\n0.0.15' ]] +} + +test_invalid_versions() { + local config + for config in '{}' '[]' '{"versions":"0.0.15"}' '{"versions":[]}' \ + '{"versions":["0.0.15","0.0.15"]}' '{"versions":["main"]}' \ + '{"versions":["../0.0.15"]}' '{"versions":["--help"]}' \ + '{"versions":["v0.0.15"]}' '{"versions":["0.0.15\n"]}' \ + '{"versions":[15]}' '{"versions":[null]}' 'not JSON'; do + printf '%s\n' "$config" > "$ROOT/config.json" + expect_failure load_versions "$ROOT/config.json" + done +} + +test_published_releases() { + verify_release 0.0.15 + local response + for response in '[]' '{"tag_name":"0.0.15"}' '{"tag_name":"0.0.15","draft":true}' '{"tag_name":"0.0.14","draft":false}'; do + gh() { printf '%s\n' "$response"; } + expect_failure verify_release 0.0.15 + done + gh() { return 1; } + expect_failure verify_release 0.0.15 +} + +test_export_and_permissions() { + prepare_releases + mkdir "$ROOT/stage" + export_release "$ROOT" 0.0.15 "$ROOT/stage" + [[ -x "$ROOT/stage/gradlew" && -f "$ROOT/stage/library/src/main/kotlin/Reader.kt" ]] + expect_failure export_release "$ROOT" 0.0.1 "$ROOT/stage" +} + +test_unsafe_sources() { + prepare_releases + local name tag=1 + for name in link 'unsafe\name' 'C:escape'; do + if [[ "$name" == link ]]; then ln -s ../outside "$ROOT/library/$name"; else touch "$ROOT/library/$name"; fi + commit_sources + git -C "$ROOT" tag "1.0.$tag" + mkdir "$ROOT/stage-$tag" + expect_failure export_release "$ROOT" "1.0.$tag" "$ROOT/stage-$tag" + [[ ! -e "$ROOT/stage-$tag/gradlew" ]] + rm "$ROOT/library/$name" + tag=$((tag + 1)) + done +} + +test_missing_sources() { + prepare_releases + rm "$ROOT/gradle/wrapper/gradle-wrapper.jar" + commit_sources + git -C "$ROOT" tag 1.0.0 + mkdir "$ROOT/stage" + expect_failure export_release "$ROOT" 1.0.0 "$ROOT/stage" +} + +test_build_removes_tokens() { + prepare_releases + export GH_TOKEN=fixture-token GITHUB_TOKEN=fixture-token + build_docs "$ROOT" + [[ "$GH_TOKEN" == fixture-token && "$GITHUB_TOKEN" == fixture-token ]] +} + +test_missing_dokka_output() { + printf '#!/usr/bin/env bash\nexit 0\n' > "$ROOT/gradlew" + chmod +x "$ROOT/gradlew" + expect_failure build_docs "$ROOT" + mkdir -p "$ROOT/build/docs" + touch "$ROOT/build/docs/index.html" + expect_failure build_docs "$ROOT" +} + +test_render_relative_assets() { + configure_root + make_dokka "$ROOT/source" + local prefix output + for prefix in '' versions/0.0.15 versions/0.0.14; do + output="$ROOT/output/${prefix:-default}" + render_docs "$ROOT/source" "$output" 0.0.15 '["0.0.15","0.0.14"]' "$prefix" + grep -Fq 'src="doc-version-selector.js"' "$output/index.html" + grep -Fq 'src="../../../doc-version-selector.js"' "$output/c2pa-android/org.contentauth.c2pa/-reader/index.html" + grep -Fq 'data-versions="["0.0.15","0.0.14"]"' "$output/index.html" + grep -Fq "data-prefix=\"$prefix\"" "$output/index.html" + cmp "$ROOT/source/navigation.html" "$output/navigation.html" + cmp "$ROOT/source/scripts/main.js" "$output/scripts/main.js" + [[ -f "$output/doc-version-selector.css" && -f "$output/doc-version-selector.js" ]] + done + ! grep -Fq doc-version-selector "$ROOT/source/index.html" +} + +test_invalid_dokka_pages() { + configure_root + make_dokka "$ROOT/source" + local page count=0 + for page in 'No slot' '
'; do + printf '%s\n' "$page" > "$ROOT/source/index.html" + expect_failure render_docs "$ROOT/source" "$ROOT/output-$count" 0.0.15 '["0.0.15"]' '' + count=$((count + 1)) + done +} + +test_complete_site() { + prepare_releases + GH_TOKEN=fixture-token GITHUB_TOKEN=fixture-token bash "$TEST_SCRIPT" --build "$ROOT" + [[ -f "$ROOT/build/docs-versioned/.nojekyll" && ! -e "$ROOT/build/docs-versioned/previous.html" ]] + local prefix + for prefix in '' versions/0.0.15 versions/0.0.14; do + grep -Fq "data-prefix=\"$prefix\"" "$ROOT/build/docs-versioned/$prefix/$PACKAGE_INDEX" + done + grep -Fq 'data-version="0.0.15"' "$ROOT/build/docs-versioned/index.html" + grep -Fq 'data-version="0.0.14"' "$ROOT/build/docs-versioned/versions/0.0.14/index.html" + [[ "$(cat "$ROOT/build/docs/working.html")" == 'Working-tree site' ]] + [[ -z "$(find "$ROOT/build" -name 'versioned-docs-*' -print)" ]] +} + +test_failed_build_preserves_site() { + prepare_releases + touch "$ROOT/library/FAIL_BUILD" + commit_sources + git -C "$ROOT" tag -f 0.0.14 >/dev/null + expect_failure bash "$TEST_SCRIPT" --build "$ROOT" + [[ "$(cat "$ROOT/build/docs-versioned/previous.html")" == 'Existing site' ]] + [[ "$(cat "$ROOT/build/docs/working.html")" == 'Working-tree site' ]] + [[ -z "$(find "$ROOT/build" -name 'versioned-docs-*' -print)" ]] +} + +tests=(test_versions_preserve_order test_invalid_versions test_published_releases + test_export_and_permissions test_unsafe_sources test_missing_sources + test_build_removes_tokens test_missing_dokka_output test_render_relative_assets + test_invalid_dokka_pages test_complete_site test_failed_build_preserves_site) +for test in "${tests[@]}"; do + ROOT="$TEST_ROOT/$test" + mkdir "$ROOT" + set +e + ( set -e; "$test" ) > "$ROOT/test.log" 2>&1 + status=$? + set -e + if [[ "$status" != 0 ]]; then + cat "$ROOT/test.log" + fail "FAIL: $test" + exit 1 + fi + printf 'PASS: %s\n' "$test" +done +printf '%s Bash tests passed.\n' "${#tests[@]}" \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8925b33..6ffa8da 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,18 +1,31 @@ name: Documentation on: + release: + types: [published] push: - tags: - - "[0-9]+.[0-9]+.[0-9]+" + branches: [main] + paths: + - "docs/versions.json" + - ".github/scripts/*doc*" + - ".github/workflows/docs.yml" + - ".github/workflows/release.yml" + - "Makefile" + pull_request: + paths: + - "docs/versions.json" + - ".github/scripts/*doc*" + - ".github/workflows/docs.yml" + - ".github/workflows/release.yml" + - "Makefile" workflow_dispatch: + workflow_call: permissions: contents: read - pages: write - id-token: write concurrency: - group: "pages" + group: "pages-${{ github.event_name == 'pull_request' && github.ref || 'deploy' }}" cancel-in-progress: false jobs: @@ -21,6 +34,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: + ref: ${{ github.event_name == 'release' && 'main' || github.ref }} + filter: tree:0 + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Check documentation tooling + run: bash --version && jq --version - name: Resolve JDK version id: java @@ -45,16 +70,25 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - - name: Build Documentation - run: make docs + - name: Test documentation tooling + run: make docs-test + + - name: Build versioned documentation + env: + GH_TOKEN: ${{ github.token }} + run: make docs-versioned - name: Upload Documentation Artifact uses: actions/upload-pages-artifact@v5 with: - path: ./build/docs + path: ./build/docs-versioned deploy: name: Deploy to GitHub Pages + if: github.event_name != 'pull_request' + permissions: + pages: write + id-token: write needs: build-docs runs-on: ubuntu-latest environment: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e29eb56..39d7a8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -154,3 +154,12 @@ jobs: See the [commit history](https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.tag }}...${{ github.event.inputs.version }}) for a full list of changes. env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-docs: + name: Publish versioned API documentation + needs: build-and-release + permissions: + contents: read + pages: write + id-token: write + uses: ./.github/workflows/docs.yml diff --git a/.gitignore b/.gitignore index d6a097e..f036bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ library/src/main/jni/c2pa.h # Generated files *.aar +__pycache__/ # OS files .DS_Store diff --git a/Makefile b/Makefile index f63de48..68713cb 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: all clean setup library publish download-binaries tests coverage help test-app example-app \ run-test-app run-example-app signing-server-start signing-server-stop signing-server-status \ - signing-server-build tests-with-server lint format docs docs-clean + signing-server-build tests-with-server lint format docs docs-clean docs-versioned docs-test # Optional. Build the native libraries from archives in this directory instead of # downloading them from a c2pa-rs release. Expects the four @@ -101,7 +101,15 @@ docs: @echo "Output: build/docs/index.html" @echo "To view: open build/docs/index.html" -# Clean generated documentation +# Generate API documentation from selected published releases +docs-versioned: + bash .github/scripts/generate-versioned-docs.sh + +docs-test: + bash .github/scripts/test-versioned-docs.sh + node --test .github/scripts/test-doc-version-selector.cjs + +# Clean generated working-tree documentation docs-clean: @echo "Cleaning generated documentation..." @rm -rf build/docs @@ -244,6 +252,12 @@ help: @echo "Publishing:" @echo " publish - Publish library to GitHub packages" @echo "" + @echo "Documentation:" + @echo " docs - Generate API docs from the working tree" + @echo " docs-versioned - Generate default and versioned docs from published releases" + @echo " docs-test - Test the documentation build tooling and version selector" + @echo " docs-clean - Clean generated working-tree documentation" + @echo "" @echo "Usage examples:" @echo " make tests - Run all tests (start server first if testing hardware signing)" @echo " make tests-with-server - Run all tests with automatic server management" diff --git a/README.md b/README.md index 1f261c9..6055a84 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,59 @@ re-extracts automatically. This is also how the `track/c2pa-rs-main` branch is built in CI. +## API documentation + +The [published API documentation](https://contentauth.github.io/c2pa-android/) +is built from the published release tags listed in [docs/versions.json](docs/versions.json), +not from the current working tree. The first version in that list is the default. +The documentation header includes a version selector. + +- Default: +- Version 0.0.15: +- Version 0.0.14: + +The existing direct package URL + +continues to serve the default version. + +### Building documentation locally + +To build the selected releases, install Bash 3.2 or newer, `jq`, Git, the GitHub CLI +(`gh`), and the JDK and Android SDK required by those releases. Authenticate `gh` +or set `GH_TOKEN` to a token with read access to the repository, then run: + +```bash +git fetch origin --tags +make docs-versioned +``` + +The output is `build/docs-versioned/`, containing the default site and +`versions//` for every configured release, including the default. +All documentation and selector links are relative, so this directory can be +served over HTTP locally or hosted under `/c2pa-android/` on GitHub Pages. + +The builder verifies that each tag is a published GitHub release, exports its +sources to a temporary workspace, and runs that release's Gradle/Dokka +configuration. It preserves executable permissions on the Gradle wrapper and +does not pass `GH_TOKEN` or `GITHUB_TOKEN` to Gradle. Release builds execute the +tagged Gradle scripts, so only add trusted release tags. Temporary workspaces are +removed after success or failure, and existing versioned output is replaced only +after all selected releases build successfully. + +The existing `make docs` command still generates working-tree documentation in +`build/docs/`; it does not replace the versioned site. Run `make docs-test` to test +the versioning tooling and selector (Bash 3.2+, `jq`, Git, and Node.js 18+; no +Python, Android SDK, or JDK required for these unit tests). + +### Maintaining versions + +Add a published release tag to `docs/versions.json`; put it first to make it the +default. Publishing a release alone does not change the selected versions. +The documentation workflow validates pull requests and deploys on relevant +changes to `main`, manual dispatch, and published releases. The release workflow +also calls it explicitly after release completion, because events created with +`GITHUB_TOKEN` do not trigger other workflows. + ## Repository structure - `/library` - Android library module with C2PA Kotlin APIs and JNI bindings diff --git a/docs/versions.json b/docs/versions.json new file mode 100644 index 0000000..c82ef85 --- /dev/null +++ b/docs/versions.json @@ -0,0 +1,6 @@ +{ + "versions": [ + "0.0.15", + "0.0.14" + ] +}