diff --git a/.github/workflows/build-tiles.yml b/.github/workflows/build-tiles.yml new file mode 100644 index 0000000..f7ea30f --- /dev/null +++ b/.github/workflows/build-tiles.yml @@ -0,0 +1,59 @@ +name: Build Tiles + +on: + workflow_dispatch: + inputs: + valhalla_ref: + description: "Valhalla ref to use for tile building (branch, tag, or SHA)" + default: master + required: false + +jobs: + build-valhalla: + uses: ./.github/workflows/build-valhalla.yml + with: + valhalla_ref: ${{ inputs.valhalla_ref }} + secrets: inherit + + build-tiles: + runs-on: ubuntu-latest + needs: build-valhalla + + steps: + - name: Sanitize ref for artifact name + id: sanitize + run: echo "ref_slug=$(echo '${{ inputs.valhalla_ref }}' | tr '/' '-')" >> $GITHUB_OUTPUT + + - name: Checkout PBF + uses: actions/checkout@v6 + with: + lfs: true + + - name: Download Valhalla wheel + uses: actions/download-artifact@v4 + with: + name: valhalla-wheel-${{ steps.sanitize.outputs.ref_slug }} + path: /tmp/valhalla-dist + + - name: Install Valhalla from wheel + run: pip install /tmp/valhalla-dist/*.whl + + - name: Build graph tiles + run: | + valhalla_build_config \ + --mjolnir-tile-dir "$(pwd)/valhalla_tiles" \ + --mjolnir-admin "$(pwd)/valhalla_tiles/admins.sqlite" \ + > valhalla.json + python -m valhalla valhalla_build_admins --config valhalla.json data/liechtenstein_graph.osm.pbf + python -m valhalla valhalla_build_tiles --config valhalla.json data/liechtenstein_graph.osm.pbf + find valhalla_tiles -name "*.gph" | wc -l + + - name: Upload tiles artifact + uses: actions/upload-artifact@v4 + with: + name: valhalla-tiles-${{ steps.sanitize.outputs.ref_slug }} + path: valhalla_tiles/ + retention-days: 90 + + - name: Print run ID + run: echo "tiles_run_id = ${{ github.run_id }}" diff --git a/.github/workflows/build-valhalla.yml b/.github/workflows/build-valhalla.yml new file mode 100644 index 0000000..ef4c8d8 --- /dev/null +++ b/.github/workflows/build-valhalla.yml @@ -0,0 +1,82 @@ +name: Build Valhalla + +on: + workflow_call: + inputs: + valhalla_ref: + description: "Valhalla git ref to build (branch, tag, or commit SHA)" + type: string + required: true + outputs: + wheel_artifact: + description: "Name of the uploaded wheel artifact" + value: ${{ jobs.build.outputs.wheel_artifact }} + +jobs: + build: + runs-on: ubuntu-latest + outputs: + wheel_artifact: ${{ steps.set-output.outputs.wheel_artifact }} + + steps: + - name: Sanitize ref for artifact name + id: sanitize + run: echo "ref_slug=$(echo '${{ inputs.valhalla_ref }}' | tr '/' '-')" >> $GITHUB_OUTPUT + + - name: Checkout Valhalla + uses: actions/checkout@v6 + with: + repository: valhalla/valhalla + ref: ${{ inputs.valhalla_ref }} + submodules: recursive + path: valhalla-src + + - name: Restore ccache + uses: tespkg/actions-cache/restore@v1 + with: + endpoint: ${{ secrets.HETZNER_S3_ENDPOINT }} + accessKey: ${{ secrets.HETZNER_S3_ACCESS_KEY }} + secretKey: ${{ secrets.HETZNER_S3_ACCESS_SECRET }} + bucket: rad-cache + path: ~/.cache/ccache + key: ccache-valhalla-${{ steps.sanitize.outputs.ref_slug }}-${{ github.sha }} + restore-keys: | + ccache-valhalla-${{ steps.sanitize.outputs.ref_slug }}- + ccache-valhalla-master- + + - name: Install system dependencies + run: bash valhalla-src/scripts/install-linux-deps.sh + + - name: Install Python build dependencies + run: pip install scikit-build-core pyproject-metadata setuptools-scm pybind11 + + - name: Build wheel + run: | + cd valhalla-src + pip wheel . --no-build-isolation --wheel-dir /tmp/valhalla-dist \ + -Ccmake.build-type=Release \ + -Ccmake.define.ENABLE_PYTHON_BINDINGS=ON \ + -Ccmake.define.ENABLE_TESTS=OFF \ + -Ccmake.define.ENABLE_SERVICES=OFF + + - name: Save ccache + if: always() + uses: tespkg/actions-cache/save@v1 + with: + endpoint: ${{ secrets.HETZNER_S3_ENDPOINT }} + accessKey: ${{ secrets.HETZNER_S3_ACCESS_KEY }} + secretKey: ${{ secrets.HETZNER_S3_ACCESS_SECRET }} + bucket: rad-cache + path: ~/.cache/ccache + key: ccache-valhalla-${{ steps.sanitize.outputs.ref_slug }}-${{ github.sha }} + + - name: Upload wheel artifact + uses: actions/upload-artifact@v4 + with: + name: valhalla-wheel-${{ steps.sanitize.outputs.ref_slug }} + path: /tmp/valhalla-dist/*.whl + retention-days: 7 + + - name: Set output + id: set-output + run: echo "wheel_artifact=valhalla-wheel-${{ steps.sanitize.outputs.ref_slug }}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/routing-regression.yml b/.github/workflows/routing-regression.yml new file mode 100644 index 0000000..de1e118 --- /dev/null +++ b/.github/workflows/routing-regression.yml @@ -0,0 +1,130 @@ +name: Routing Regression + +on: + workflow_dispatch: + inputs: + valhalla_ref_old: + description: "Baseline Valhalla ref (the 'before')" + default: master + required: true + valhalla_ref_new: + description: "New Valhalla ref to test (the 'after' — branch, tag, or SHA)" + required: true + tiles_run_id: + description: "Run ID from a successful build-tiles.yml run" + required: true + +jobs: + build-valhalla-old: + uses: ./.github/workflows/build-valhalla.yml + with: + valhalla_ref: ${{ inputs.valhalla_ref_old }} + secrets: inherit + + build-valhalla-new: + uses: ./.github/workflows/build-valhalla.yml + with: + valhalla_ref: ${{ inputs.valhalla_ref_new }} + secrets: inherit + + run-routes: + runs-on: ubuntu-latest + needs: [build-valhalla-old, build-valhalla-new] + strategy: + matrix: + router: + - name: old + ref: ${{ inputs.valhalla_ref_old }} + - name: new + ref: ${{ inputs.valhalla_ref_new }} + + steps: + - name: Sanitize ref for artifact name + id: sanitize + run: echo "ref_slug=$(echo '${{ matrix.router.ref }}' | tr '/' '-')" >> $GITHUB_OUTPUT + + - name: Checkout RAD + uses: actions/checkout@v6 + + - name: Checkout RAD-data + uses: actions/checkout@v6 + with: + repository: valhalla/RAD-data + path: RAD-data + token: ${{ secrets.RAD_DATA_TOKEN }} + + - name: Download Valhalla wheel + uses: actions/download-artifact@v4 + with: + name: valhalla-wheel-${{ steps.sanitize.outputs.ref_slug }} + path: /tmp/valhalla-dist + + - name: Install Valhalla from wheel + run: pip install /tmp/valhalla-dist/*.whl + + - name: Download tiles from build-tiles run + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh run download ${{ inputs.tiles_run_id }} \ + --repo "${{ github.repository }}" \ + --pattern "valhalla-tiles-*" \ + --dir valhalla_tiles + shopt -s dotglob + mv valhalla_tiles/valhalla-tiles-*/* valhalla_tiles/ 2>/dev/null || true + + - name: Run route requests + run: | + valhalla_build_config \ + --mjolnir-tile-dir "$(pwd)/valhalla_tiles" \ + > /tmp/valhalla.json + python3 scripts/run_routes.py \ + --config /tmp/valhalla.json \ + --requests RAD-data/requests/requests.jsonl \ + --output /tmp/responses-${{ matrix.router.name }}.jsonl + + - name: Upload responses artifact + uses: actions/upload-artifact@v4 + with: + name: responses-${{ matrix.router.name }} + path: /tmp/responses-${{ matrix.router.name }}.jsonl + retention-days: 7 + + diff-and-store: + runs-on: ubuntu-latest + needs: run-routes + permissions: + contents: write + + steps: + - name: Checkout RAD + uses: actions/checkout@v6 + + - name: Checkout RAD-data + uses: actions/checkout@v6 + with: + repository: valhalla/RAD-data + path: RAD-data + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Download responses + uses: actions/download-artifact@v4 + with: + pattern: responses-* + path: /tmp/responses + merge-multiple: false + + - name: Compute diff and push to RAD-data + run: | + python3 scripts/diff_responses.py \ + --old /tmp/responses/responses-old/responses-old.jsonl \ + --new /tmp/responses/responses-new/responses-new.jsonl \ + --old-ref "${{ inputs.valhalla_ref_old }}" \ + --new-ref "${{ inputs.valhalla_ref_new }}" \ + --output RAD-data/diffs/${{ github.run_id }}.json + cd RAD-data + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add diffs/${{ github.run_id }}.json + git commit -m "regression: ${{ inputs.valhalla_ref_old }} vs ${{ inputs.valhalla_ref_new }} (run ${{ github.run_id }})" + git push diff --git a/scripts/diff_responses.py b/scripts/diff_responses.py new file mode 100644 index 0000000..2fe63b2 --- /dev/null +++ b/scripts/diff_responses.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Usage: + python3 scripts/diff_responses.py \ + --old /tmp/responses/responses-old/responses-old.jsonl \ + --new /tmp/responses/responses-new/responses-new.jsonl \ + --old-ref master \ + --new-ref feature-branch \ + --output RAD-data/diffs/12345678.json +""" + +import argparse +import json +from datetime import UTC, datetime + + +def load_jsonl(path): + return [json.loads(line) for line in open(path)] + + +def diff_entry(old, new): + # TODO: expand with geometry comparison, maneuver diffs, etc. + old_dur = old["response"].get("trip", {}).get("summary", {}).get("time", None) + new_dur = new["response"].get("trip", {}).get("summary", {}).get("time", None) + return { + "request": old["request"], + "old_duration": old_dur, + "new_duration": new_dur, + "duration_delta_s": (new_dur - old_dur) if (old_dur and new_dur) else None, + "old_status": old["status"], + "new_status": new["status"], + } + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--old", required=True) + p.add_argument("--new", required=True) + p.add_argument("--old-ref", required=True) + p.add_argument("--new-ref", required=True) + p.add_argument("--output", required=True) + args = p.parse_args() + + old_rows = load_jsonl(args.old) + new_rows = load_jsonl(args.new) + + diffs = [diff_entry(o, n) for o, n in zip(old_rows, new_rows, strict=False)] + changed = [d for d in diffs if d["duration_delta_s"] not in (None, 0)] + + result = { + "meta": { + "old_ref": args.old_ref, + "new_ref": args.new_ref, + "generated_at": datetime.now(UTC).isoformat(), + "total_requests": len(diffs), + "changed_routes": len(changed), + }, + "diffs": diffs, + } + + with open(args.output, "w") as f: + json.dump(result, f, indent=2) + + print(f"Done: {len(changed)}/{len(diffs)} routes changed") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_routes.py b/scripts/run_routes.py new file mode 100644 index 0000000..bc60e3f --- /dev/null +++ b/scripts/run_routes.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +""" +Usage: + python3 scripts/run_routes.py \ + --config /tmp/valhalla.json \ + --requests RAD-data/requests/requests.jsonl \ + --output /tmp/responses-old.jsonl +""" + +import argparse +import json + +import valhalla + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--config", required=True) + p.add_argument("--requests", required=True) + p.add_argument("--output", required=True) + args = p.parse_args() + + config = args.config + actor = valhalla.Actor(config) + + with open(args.requests) as req_f, open(args.output, "w") as out_f: + for line in req_f: + request = json.loads(line) + try: + response = json.loads(actor.route(json.dumps(request))) + status = "ok" + except Exception as e: + response = {"error": str(e)} + status = "error" + out_f.write(json.dumps({"request": request, "response": response, "status": status}) + "\n") + + +if __name__ == "__main__": + main()