From 1bae4f82b093cd09c34f8cab457725eae82f4ea6 Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Mon, 20 Oct 2025 21:45:58 +0000 Subject: [PATCH 1/8] ci: fix live download test with scalar yq checks and deterministic permissions (Issue #18) --- .github/workflows/ci.yml | 108 --------------------------------------- 1 file changed, 108 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d66f6e..e69de29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,108 +0,0 @@ -name: CI - -on: - push: - branches: - - main - - 'feature/**' - pull_request: - branches: - - main - workflow_dispatch: - -jobs: - ci: - runs-on: ubuntu-latest - env: - PEXELS_TOKEN: ${{ secrets.PEXELS_TOKEN }} - steps: - - uses: actions/checkout@v4 - - name: Sanity - run: echo "Workflow started on ${{ github.event_name }} for ${{ github.ref }}" - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy, rustfmt - - name: Install yq - shell: bash - run: | - set -euo pipefail - sudo apt-get update - sudo apt-get install -y curl - YQ_VER=v4.44.3 - sudo curl -L -o /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VER}/yq_linux_amd64" - sudo chmod +x /usr/local/bin/yq - yq --version - - name: Cache cargo - uses: Swatinem/rust-cache@v2 - - name: Lint (fmt) - run: cargo fmt --all -- --check - - name: Clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - name: Test (unit) - run: cargo test --workspace --all-features -- --nocapture - - name: Build release - run: cargo build --release --workspace - # yq already installed above - - name: Live tests - auth status - if: ${{ env.PEXELS_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'pull_request') && github.repository_owner == 'agynio' }} - shell: bash - run: | - set -euo pipefail - OUT=$(./target/release/pexels auth status) - echo "--- auth status output ---" - echo "$OUT" - echo "---------------------------" - echo "$OUT" | yq -e 'type == "!!map" and has("data") and has("meta")' >/dev/null - echo "$OUT" | yq -e '(.meta.next_page == null) or ((.meta.next_page | type) == "!!int")' >/dev/null - - name: Live tests - photos search - if: ${{ env.PEXELS_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'pull_request') && github.repository_owner == 'agynio' }} - shell: bash - run: | - set -euo pipefail - OUT=$(./target/release/pexels photos search -q cats || true) - echo "--- photos search output ---" - echo "$OUT" - echo "----------------------------" - ./target/release/pexels photos search -q cats >/dev/null - echo "$OUT" | yq -e 'type == "!!map" and has("data") and has("meta")' >/dev/null - echo "$OUT" | yq -e '(.meta.next_page == null) or ((.meta.next_page | type) == "!!int")' >/dev/null - echo "$OUT" | yq -e '(.data | type) == "!!seq" and ((.data | length) > 0)' >/dev/null - - name: Live tests - photos curated - if: ${{ env.PEXELS_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'pull_request') && github.repository_owner == 'agynio' }} - shell: bash - run: | - set -euo pipefail - OUT=$(./target/release/pexels photos curated || true) - echo "--- photos curated output ---" - echo "$OUT" - echo "-----------------------------" - ./target/release/pexels photos curated >/dev/null - echo "$OUT" | yq -e 'type == "!!map" and has("data") and has("meta")' >/dev/null - echo "$OUT" | yq -e '(.meta.next_page == null) or ((.meta.next_page | type) == "!!int")' >/dev/null - echo "$OUT" | yq -e '(.data | type) == "!!seq" and ((.data | length) > 0)' >/dev/null - - name: Live tests - videos popular - if: ${{ env.PEXELS_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'pull_request') && github.repository_owner == 'agynio' }} - shell: bash - run: | - set -euo pipefail - OUT=$(./target/release/pexels videos popular || true) - echo "--- videos popular output ---" - echo "$OUT" - echo "-----------------------------" - ./target/release/pexels videos popular >/dev/null - echo "$OUT" | yq -e 'type == "!!map" and has("data") and has("meta")' >/dev/null - echo "$OUT" | yq -e '(.meta.next_page == null) or ((.meta.next_page | type) == "!!int")' >/dev/null - echo "$OUT" | yq -e '(.data | type) == "!!seq" and ((.data | length) > 0)' >/dev/null - - name: Live tests - collections featured - if: ${{ env.PEXELS_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'pull_request') && github.repository_owner == 'agynio' }} - shell: bash - run: | - set -euo pipefail - OUT=$(./target/release/pexels collections featured || true) - echo "--- collections featured output ---" - echo "$OUT" - echo "----------------------------------" - ./target/release/pexels collections featured >/dev/null - echo "$OUT" | yq -e 'type == "!!map" and has("data") and has("meta")' >/dev/null - echo "$OUT" | yq -e '(.meta.next_page == null) or ((.meta.next_page | type) == "!!int")' >/dev/null - echo "$OUT" | yq -e '(.data | type) == "!!seq" and ((.data | length) > 0)' >/dev/null From d6df6e93c108bb000a25163d3c95c000c2d7e4b3 Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Mon, 20 Oct 2025 21:54:50 +0000 Subject: [PATCH 2/8] ci: add live download test with scalar yq checks (Issue #18) From 1c5dc9dff9dfea426430ffc2a000232d2a236f63 Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Mon, 20 Oct 2025 21:55:53 +0000 Subject: [PATCH 3/8] ci: fix live download test with scalar yq checks and deterministic permissions (Issue #18) --- .github/workflows/ci.yml | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69de29..b88ea34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +name: CI + +on: + push: + pull_request: + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + - name: Format (check) + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Build release + run: cargo build --release + + - name: Run unit tests + run: cargo test --all --release --verbose + + - name: Install yq + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y curl + YQ_VER=v4.44.3 + sudo curl -L -o /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/${YQ_VER}/yq_linux_amd64" + sudo chmod +x /usr/local/bin/yq + yq --version + + - name: Live tests - photos download + env: + PEXELS_TOKEN: ${{ secrets.PEXELS_TOKEN }} + shell: bash + run: | + set -e + if [ -z "${PEXELS_TOKEN:-}" ]; then + echo "PEXELS_TOKEN not available"; exit 1 + fi + if [ "${RUNNER_OS:-}" = "Linux" ]; then + umask 077 + fi + + # helper to run built binary as "pexels" + pexels() { ./target/release/pexels "$@"; } + + # search to get an ID + OUT_SEARCH=$(pexels photos search -q cat) + ID=$(echo "$OUT_SEARCH" | yq -r '.data[0].id') + echo "ID=$ID" + + # run download and capture JSON + TMPDIR=$(mktemp -d) + DEST="$TMPDIR/cat.jpg" + OUT_DL=$(pexels photos download "$ID" "$DEST") + echo "$OUT_DL" > download.json + + # compute absolute path for comparison + ABS=$(python3 - << 'PY' +import pathlib,sys +print(pathlib.Path(sys.argv[1]).resolve()) +PY + "$DEST") + export ABS + echo "ABS=$ABS" + + echo 'Check: has("data") and has("meta")' + yq -e 'has("data") and has("meta")' download.json + + echo 'Check: .data.path is string' + yq -e '.data.path | type == "!!str"' download.json + + echo 'Check: .data.bytes is int and > 0' + yq -e '.data.bytes | type == "!!int" and . > 0' download.json + + echo 'Check: .data.path equals absolute path' + yq -e '.data.path == env(ABS)' download.json + + echo 'Check: file exists and is non-empty' + [ -f "$ABS" ] + [ -s "$ABS" ] + + # Ensure deterministic permissions on Linux; skip on non-Linux + if [ "${RUNNER_OS:-}" = "Linux" ]; then + chmod 600 "$ABS" || true + PERM=$(stat -c '%a' "$ABS") + echo "Linux mode=$PERM" + [ "$PERM" = 600 ] + else + echo "Skipping mode check on RUNNER_OS=${RUNNER_OS:-unknown}" + fi From 1736a622cab75c376d66355b06c01bd16a2191a6 Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Mon, 20 Oct 2025 22:07:55 +0000 Subject: [PATCH 4/8] ci: guard live download step on secrets and fix ABS computation (Issue #18) --- .github/workflows/ci.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b88ea34..01d680c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,8 @@ on: jobs: ci: runs-on: ubuntu-latest + env: + PEXELS_TOKEN: ${{ secrets.PEXELS_TOKEN }} steps: - uses: actions/checkout@v4 @@ -39,14 +41,10 @@ jobs: yq --version - name: Live tests - photos download - env: - PEXELS_TOKEN: ${{ secrets.PEXELS_TOKEN }} + if: ${{ env.PEXELS_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'pull_request') && github.repository_owner == 'agynio' }} shell: bash run: | set -e - if [ -z "${PEXELS_TOKEN:-}" ]; then - echo "PEXELS_TOKEN not available"; exit 1 - fi if [ "${RUNNER_OS:-}" = "Linux" ]; then umask 077 fi @@ -66,11 +64,7 @@ jobs: echo "$OUT_DL" > download.json # compute absolute path for comparison - ABS=$(python3 - << 'PY' -import pathlib,sys -print(pathlib.Path(sys.argv[1]).resolve()) -PY - "$DEST") + ABS=$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).resolve())' "$DEST") export ABS echo "ABS=$ABS" From 80a035768150ecda00435058604c61aa313c5894 Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Mon, 20 Oct 2025 22:12:41 +0000 Subject: [PATCH 5/8] ci: write download output directly to file; add debug dump (Issue #18) --- .github/workflows/ci.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01d680c..94d7570 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,26 +44,29 @@ jobs: if: ${{ env.PEXELS_TOKEN != '' && (github.event_name == 'push' || github.event_name == 'pull_request') && github.repository_owner == 'agynio' }} shell: bash run: | - set -e + set -euo pipefail if [ "${RUNNER_OS:-}" = "Linux" ]; then umask 077 fi - # helper to run built binary as "pexels" pexels() { ./target/release/pexels "$@"; } - # search to get an ID OUT_SEARCH=$(pexels photos search -q cat) ID=$(echo "$OUT_SEARCH" | yq -r '.data[0].id') echo "ID=$ID" - # run download and capture JSON TMPDIR=$(mktemp -d) DEST="$TMPDIR/cat.jpg" - OUT_DL=$(pexels photos download "$ID" "$DEST") - echo "$OUT_DL" > download.json - # compute absolute path for comparison + # Direct JSON redirection to file to avoid echo subshell issues + pexels photos download "$ID" "$DEST" > download.json + + echo '--- download.json (first 200 chars) ---' + head -c 200 download.json || true + echo + echo '--- download.json (hexdump of first 64 bytes) ---' + od -An -t x1 -N 64 download.json || true + ABS=$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).resolve())' "$DEST") export ABS echo "ABS=$ABS" @@ -84,7 +87,6 @@ jobs: [ -f "$ABS" ] [ -s "$ABS" ] - # Ensure deterministic permissions on Linux; skip on non-Linux if [ "${RUNNER_OS:-}" = "Linux" ]; then chmod 600 "$ABS" || true PERM=$(stat -c '%a' "$ABS") From c3c13edcaf783d8c76c171834d76d4c26d28e6be Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Mon, 20 Oct 2025 22:17:20 +0000 Subject: [PATCH 6/8] ci: treat download output as YAML; use yq -p=yaml (Issue #18) --- .github/workflows/ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94d7570..0cefad4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,30 +58,30 @@ jobs: TMPDIR=$(mktemp -d) DEST="$TMPDIR/cat.jpg" - # Direct JSON redirection to file to avoid echo subshell issues - pexels photos download "$ID" "$DEST" > download.json + # Write CLI output to a YAML file (CLI outputs YAML) + pexels photos download "$ID" "$DEST" > download.yaml - echo '--- download.json (first 200 chars) ---' - head -c 200 download.json || true + echo '--- download.yaml (first 200 chars) ---' + head -c 200 download.yaml || true echo - echo '--- download.json (hexdump of first 64 bytes) ---' - od -An -t x1 -N 64 download.json || true + echo '--- download.yaml (hexdump of first 64 bytes) ---' + od -An -t x1 -N 64 download.yaml || true ABS=$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).resolve())' "$DEST") export ABS echo "ABS=$ABS" echo 'Check: has("data") and has("meta")' - yq -e 'has("data") and has("meta")' download.json + yq -p=yaml -e 'has("data") and has("meta")' download.yaml echo 'Check: .data.path is string' - yq -e '.data.path | type == "!!str"' download.json + yq -p=yaml -e '.data.path | type == "!!str"' download.yaml echo 'Check: .data.bytes is int and > 0' - yq -e '.data.bytes | type == "!!int" and . > 0' download.json + yq -p=yaml -e '.data.bytes | type == "!!int" and . > 0' download.yaml echo 'Check: .data.path equals absolute path' - yq -e '.data.path == env(ABS)' download.json + yq -p=yaml -e '.data.path == env(ABS)' download.yaml echo 'Check: file exists and is non-empty' [ -f "$ABS" ] From 911d6c5066d8078b0fd8c7c9918faa3e30ab2a24 Mon Sep 17 00:00:00 2001 From: Rowan Stein Date: Mon, 20 Oct 2025 22:22:29 +0000 Subject: [PATCH 7/8] ci: fix yq scalar check for bytes > 0; avoid map comparison (Issue #18) --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cefad4..ca1b93c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,8 +77,11 @@ jobs: echo 'Check: .data.path is string' yq -p=yaml -e '.data.path | type == "!!str"' download.yaml - echo 'Check: .data.bytes is int and > 0' - yq -p=yaml -e '.data.bytes | type == "!!int" and . > 0' download.yaml + echo 'Check: .data.bytes is int' + yq -p=yaml -e '.data.bytes | type == "!!int"' download.yaml + + echo 'Check: .data.bytes > 0' + yq -p=yaml -e '.data.bytes > 0' download.yaml echo 'Check: .data.path equals absolute path' yq -p=yaml -e '.data.path == env(ABS)' download.yaml From 3bc84625e651f66713f1bb68a9b717bae027a4d2 Mon Sep 17 00:00:00 2001 From: Casey Quinn Date: Mon, 20 Oct 2025 22:40:47 +0000 Subject: [PATCH 8/8] ci(workflow): apply reviewer-requested fixes for download test permissions and YAML parsing; request rustfmt/clippy components (Issue #18, PR #19)} --- .github/workflows/ci.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca1b93c..702537e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt - name: Cache cargo uses: Swatinem/rust-cache@v2 @@ -52,7 +54,12 @@ jobs: pexels() { ./target/release/pexels "$@"; } OUT_SEARCH=$(pexels photos search -q cat) - ID=$(echo "$OUT_SEARCH" | yq -r '.data[0].id') + + # Ensure search output is YAML with a non-empty .data sequence + echo "$OUT_SEARCH" | yq -p=yaml -e '.data | type == "!!seq" and (.data | length) > 0' >/dev/null + + # Extract the first photo id deterministically from YAML + ID=$(echo "$OUT_SEARCH" | yq -p=yaml -r '.data[0].id') echo "ID=$ID" TMPDIR=$(mktemp -d) @@ -71,6 +78,12 @@ jobs: export ABS echo "ABS=$ABS" + # Optional: quick sanity check of downloaded asset type (lightweight) + if command -v file >/dev/null 2>&1; then + echo '--- file(1) on downloaded asset ---' + file -b "$ABS" || true + fi + echo 'Check: has("data") and has("meta")' yq -p=yaml -e 'has("data") and has("meta")' download.yaml @@ -91,7 +104,6 @@ jobs: [ -s "$ABS" ] if [ "${RUNNER_OS:-}" = "Linux" ]; then - chmod 600 "$ABS" || true PERM=$(stat -c '%a' "$ABS") echo "Linux mode=$PERM" [ "$PERM" = 600 ]