From 8a993e035e15d0b6457028ba1dce2a757c3ff0c4 Mon Sep 17 00:00:00 2001 From: Kaspar Lyngsie Date: Thu, 14 May 2026 13:36:45 +0200 Subject: [PATCH] feat: wire up docker update script --- .github/workflows/automatic-version-sync.yml | 162 ------------------ .github/workflows/ci.yml | 7 +- .github/workflows/update-deps-docker.yml | 33 ++++ Dockerfile | 5 +- Makefile | 29 +--- infrastructure/sync_versions/Cargo.toml | 15 -- .../get_latest_meilisearch_version.rs | 33 ---- .../sync_versions/src/handlers/mod.rs | 21 --- infrastructure/sync_versions/src/main.rs | 47 ----- .../tests/get_latest_meilisearch_version.rs | 16 -- 10 files changed, 47 insertions(+), 321 deletions(-) delete mode 100644 .github/workflows/automatic-version-sync.yml create mode 100644 .github/workflows/update-deps-docker.yml delete mode 100644 infrastructure/sync_versions/Cargo.toml delete mode 100644 infrastructure/sync_versions/src/handlers/get_latest_meilisearch_version.rs delete mode 100644 infrastructure/sync_versions/src/handlers/mod.rs delete mode 100644 infrastructure/sync_versions/src/main.rs delete mode 100644 infrastructure/sync_versions/tests/get_latest_meilisearch_version.rs diff --git a/.github/workflows/automatic-version-sync.yml b/.github/workflows/automatic-version-sync.yml deleted file mode 100644 index 8ddd1c6..0000000 --- a/.github/workflows/automatic-version-sync.yml +++ /dev/null @@ -1,162 +0,0 @@ -name: Automatic Version Synchronization - -on: - workflow_dispatch: - schedule: - - cron: "0 0 * * 0" # Run every Sunday at midnight - -permissions: - contents: write - pull-requests: write - -jobs: - get-current-local-meilisearch-docker-version: - runs-on: ubuntu-latest - outputs: - local_version: ${{ steps.get-local-version.outputs.local_version }} - - steps: - - &checkout-code - name: Checkout code (shallow) - uses: actions/checkout@v6 - - - name: Get current local Meilisearch Docker version - id: get-local-version - run: | - LOCAL_VERSION=$(sed -nE 's/^FROM[[:space:]]+getmeili\/meilisearch:([^[:space:]]+).*/\1/p' Dockerfile) - if [ -z "$LOCAL_VERSION" ]; then - echo "Failed to extract Meilisearch image tag from Dockerfile." >&2 - exit 1 - fi - - echo "local_version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT" - - echo "::notice::Current local Meilisearch Docker version: $LOCAL_VERSION" - echo "## 🐳 Current Local Meilisearch Version" >> $GITHUB_STEP_SUMMARY - echo "**Version:** \`$LOCAL_VERSION\`" >> $GITHUB_STEP_SUMMARY - - get-newest-meilisearch-docker-version: - runs-on: ubuntu-latest - outputs: - latest_version: ${{ steps.get-latest-version.outputs.latest_version }} - - steps: - - *checkout-code - - - name: Read Rust toolchain version - id: rust-version - run: echo "version=$(grep '^channel' rust-toolchain.toml | sed 's/.*"\(.*\)".*/\1/')" >> "$GITHUB_OUTPUT" - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ steps.rust-version.outputs.version }} - - - name: Cache Cargo dependencies - uses: actions/cache@v5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - infrastructure/sync_versions/target - key: v1-cargo-sync-versions-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('infrastructure/sync_versions/Cargo.lock') }} - restore-keys: v1-cargo-sync-versions-${{ hashFiles('rust-toolchain.toml') }}- - - - name: Build sync_versions - run: | - cargo build \ - --manifest-path infrastructure/sync_versions/Cargo.toml \ - --release - - - name: Get latest version of Meilisearch Docker image - id: get-latest-version - env: - RUST_LOG: debug - run: | - LATEST_VERSION=$(./infrastructure/sync_versions/target/release/sync_versions get-latest-meilisearch-version) - if [ -z "$LATEST_VERSION" ]; then - echo "Failed to extract latest Meilisearch Docker version from Docker Hub" >&2 - exit 1 - fi - - echo "latest_version=$LATEST_VERSION" >> "$GITHUB_OUTPUT" - - echo "::notice::Latest Meilisearch Docker version available: $LATEST_VERSION" - echo "## 🚀 Latest Meilisearch Version" >> $GITHUB_STEP_SUMMARY - echo "**Version:** \`$LATEST_VERSION\`" >> $GITHUB_STEP_SUMMARY - - update-and-sync-meilisearch-version: - runs-on: ubuntu-latest - env: - LOCAL_VERSION: ${{ needs.get-current-local-meilisearch-docker-version.outputs.local_version }} - LATEST_VERSION: ${{ needs.get-newest-meilisearch-docker-version.outputs.latest_version }} - needs: - - get-newest-meilisearch-docker-version - - get-current-local-meilisearch-docker-version - - steps: - - name: Create temporary GitHub App Token - id: app - uses: actions/create-github-app-token@v3 - with: - owner: ${{ github.repository_owner }} - app-id: ${{ vars.HOUSEKEEPING_BOT_APP_ID }} - private-key: ${{ secrets.HOUSEKEEPING_BOT_PRIVATE_KEY }} - - - name: Checkout and setup - uses: alchemaxinc/composite-toolbox/checkout-and-setup@v1 - with: - token: ${{ steps.app.outputs.token }} - - - name: Print and verify versions - id: print-versions - run: | - echo "Local version: $LOCAL_VERSION" - echo "Latest version: $LATEST_VERSION" - - if [ -z "$LOCAL_VERSION" ] || [ -z "$LATEST_VERSION" ]; then - echo "Error: LOCAL_VERSION or LATEST_VERSION is empty" >&2 - exit 1 - fi - - - name: Update local Meilisearch Docker version - id: update-version - run: | - shopt -s globstar - sed -i "s/FROM getmeili\/meilisearch:${LOCAL_VERSION}/FROM getmeili\/meilisearch:${LATEST_VERSION}/g" \ - Dockerfile \ - **/*.md - echo "updated_files=$(git diff --name-only | tr '\n' ' ')" >> "$GITHUB_OUTPUT" - - - name: Check for changes - id: check_changes - uses: alchemaxinc/composite-toolbox/check-changes@v1 - with: - files: ${{ steps.update-version.outputs.updated_files }} - - - name: Create Pull Request - id: open-pr - if: steps.check_changes.outputs.has_changes == 'true' - uses: alchemaxinc/composite-toolbox/create-pr@v1 - with: - token: ${{ steps.app.outputs.token }} - base-branch: "main" - branch-prefix: "fix/auto/meilisearch-docker-version" - files: ${{ steps.update-version.outputs.updated_files }} - # chore: so semantic-release doesn't create a new release for - # Dockerfile-only changes (which don't affect the wrapper binary) - commit-message: "chore: update Meilisearch Docker version to ${{ needs.get-newest-meilisearch-docker-version.outputs.latest_version }}" - pr-title: "chore: automatic Meilisearch Docker version upgrade" - pr-body: | - This PR updates the Meilisearch Docker version in the Dockerfile to the latest available version. - - - Previous version: `${{ needs.get-current-local-meilisearch-docker-version.outputs.local_version }}` - - New version: `${{ needs.get-newest-meilisearch-docker-version.outputs.latest_version }}` - - - name: Auto-merge Pull Request - if: steps.open-pr.outcome == 'success' - uses: alchemaxinc/composite-toolbox/merge-pr@v1 - with: - token: ${{ steps.app.outputs.token }} - merge-method: "merge" - pull-request-number: ${{ steps.open-pr.outputs.pr_number }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 991e818..27494c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,10 +77,6 @@ jobs: manifest: wrapper/Cargo.toml target: wrapper/target lockfile: wrapper/Cargo.lock - - name: sync-versions - manifest: infrastructure/sync_versions/Cargo.toml - target: infrastructure/sync_versions/target - lockfile: infrastructure/sync_versions/Cargo.lock steps: - *checkout-code @@ -137,7 +133,6 @@ jobs: path: | ~/.cargo/registry ~/.cargo/git - infrastructure/sync_versions/target wrapper/target key: v1-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }} restore-keys: | @@ -171,6 +166,8 @@ jobs: context: . load: true tags: meilisearch-lambda-wrapper-api:test + build-args: | + RUST_VERSION=${{ steps.rust-version.outputs.version }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/update-deps-docker.yml b/.github/workflows/update-deps-docker.yml new file mode 100644 index 0000000..274ba21 --- /dev/null +++ b/.github/workflows/update-deps-docker.yml @@ -0,0 +1,33 @@ +name: Update Docker Images +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * 0" # Run every Sunday at midnight + +jobs: + update-docker: + runs-on: ubuntu-latest + steps: + - name: Create temporary GitHub App Token + id: app + uses: actions/create-github-app-token@v3 + with: + owner: ${{ github.repository_owner }} + app-id: ${{ vars.HOUSEKEEPING_BOT_APP_ID }} + private-key: ${{ secrets.HOUSEKEEPING_BOT_PRIVATE_KEY }} + + - name: Update Docker Images + uses: alchemaxinc/update-deps/docker@v2 + with: + token: ${{ steps.app.outputs.token }} + auto-merge: "true" + base-branch: "main" + branch-prefix: "fix/auto/update-docker-images" + # chore: so semantic-release doesn't create a new release for + # Dockerfile-only changes (which don't affect the wrapper binary) + pr-title: "chore: automatic Docker image upgrades" + commit-message: "chore: update docker images" + markdown-glob: "**/*.md" + # Rust builder image is driven by rust-toolchain.toml via a + # --build-arg in the Dockerfile; the cargo updater bumps it. + excluded-images: "rust" diff --git a/Dockerfile b/Dockerfile index 568ddbb..eac392f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ # Build stage: compile the Rust wrapper binary -FROM rust:1.94-alpine AS builder +# RUST_VERSION is sourced from rust-toolchain.toml so the cargo updater +# (which bumps the channel field) is the single source of truth. +ARG RUST_VERSION=1.95.0 +FROM rust:${RUST_VERSION}-alpine AS builder RUN apk add --no-cache musl-dev diff --git a/Makefile b/Makefile index 1ec1b28..32ad716 100644 --- a/Makefile +++ b/Makefile @@ -7,9 +7,13 @@ SERVICE_NAME=meilisearch-lambda-wrapper DOCKER_IMAGE_NAME=$(SERVICE_NAME)-api DOCKER_IMAGE_TAG?=abc123def +# Sourced from rust-toolchain.toml so the cargo updater drives the Docker +# builder image version too (see .github/workflows/update-deps-docker.yml, +# which excludes `rust` from the docker-image bumper). +RUST_VERSION := $(shell sed -nE 's/^channel[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' rust-toolchain.toml) + # Rust crate manifest paths WRAPPER_MANIFEST=wrapper/Cargo.toml -SYNC_VERSIONS_MANIFEST=infrastructure/sync_versions/Cargo.toml STRESS_TEST_SCRIPT=infrastructure/stress_tests/stress-test.js @@ -20,6 +24,7 @@ define docker_build docker buildx build \ --provenance=false \ --platform linux/$(1) \ + --build-arg RUST_VERSION=$(RUST_VERSION) \ $(2) \ -t $(3) \ -f Dockerfile . @@ -36,17 +41,10 @@ clean: ## Clean up built files .PHONY: lint lint: ## Run linter - cargo clippy \ - --manifest-path $(SYNC_VERSIONS_MANIFEST) \ - --all-targets \ - -- -D warnings cargo clippy \ --manifest-path $(WRAPPER_MANIFEST) \ --all-targets \ -- -D warnings - cargo +nightly fmt \ - --manifest-path $(SYNC_VERSIONS_MANIFEST) \ - -- --check cargo +nightly fmt \ --manifest-path $(WRAPPER_MANIFEST) \ -- --check @@ -54,39 +52,28 @@ lint: ## Run linter .PHONY: format format: ## Format files - cargo clippy \ - --manifest-path $(SYNC_VERSIONS_MANIFEST) \ - --all-targets \ - --fix --allow-dirty cargo clippy \ --manifest-path $(WRAPPER_MANIFEST) \ --all-targets \ --fix --allow-dirty - cargo +nightly fmt \ - --manifest-path $(SYNC_VERSIONS_MANIFEST) cargo +nightly fmt \ --manifest-path $(WRAPPER_MANIFEST) npx prettier --write . .PHONY: build build: ## Build all Rust crates - cargo build \ - --manifest-path $(SYNC_VERSIONS_MANIFEST) \ - --release cargo build \ --manifest-path $(WRAPPER_MANIFEST) \ --release .PHONY: test-unit test-unit: ## Run unit tests - cargo test \ - --manifest-path $(SYNC_VERSIONS_MANIFEST) cargo test \ --manifest-path $(WRAPPER_MANIFEST) .PHONY: test-integration test-integration: ## Run integration tests - docker build -t $(DOCKER_IMAGE_NAME):test . + docker build --build-arg RUST_VERSION=$(RUST_VERSION) -t $(DOCKER_IMAGE_NAME):test . docker compose -f $(INTEGRATION_COMPOSE) up -d --wait MEILI_MASTER_KEY=test-master-key-12345 cargo test \ --manifest-path $(WRAPPER_MANIFEST) \ @@ -98,7 +85,7 @@ test-integration: ## Run integration tests .PHONY: test-stress test-stress: ## Run k6 stress tests (requires k6: https://grafana.com/docs/k6/latest/set-up/install-k6/) - docker build -t $(DOCKER_IMAGE_NAME):test . + docker build --build-arg RUST_VERSION=$(RUST_VERSION) -t $(DOCKER_IMAGE_NAME):test . docker compose -f $(INTEGRATION_COMPOSE) up -d --wait k6 run $(STRESS_TEST_SCRIPT); \ exit_code=$$?; \ diff --git a/infrastructure/sync_versions/Cargo.toml b/infrastructure/sync_versions/Cargo.toml deleted file mode 100644 index 8fe6722..0000000 --- a/infrastructure/sync_versions/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "sync_versions" -version = "0.1.0" -edition = "2024" -publish = false - -[lints.clippy] -implicit_return = "deny" -needless_return = "allow" - -[dependencies] -env_logger = "0.11.10" -log = "0.4.29" -reqwest = { version = "0.13.3", features = ["blocking", "json"] } -serde = { version = "1.0.228", features = ["derive"] } diff --git a/infrastructure/sync_versions/src/handlers/get_latest_meilisearch_version.rs b/infrastructure/sync_versions/src/handlers/get_latest_meilisearch_version.rs deleted file mode 100644 index 5ac52ba..0000000 --- a/infrastructure/sync_versions/src/handlers/get_latest_meilisearch_version.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::error; - -use log::debug; -use log::error; -use log::info; -use reqwest::blocking; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -struct Tag { - name: String, -} - -const TAGS_URL: &str = "https://api.github.com/repos/meilisearch/meilisearch/tags"; -const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); - -/// Fetch the latest Meilisearch release tag from the GitHub API. -pub fn handle() -> Result> { - let client = blocking::Client::new(); - - debug!("Fetching tags from {}", TAGS_URL); - let response: Vec = client.get(TAGS_URL).header("User-Agent", USER_AGENT).send()?.json()?; - - debug!("Fetched {} tags", response.len()); - if response.is_empty() { - error!("No tags found at {}", TAGS_URL); - return Err(format!("Tag not found: {}", TAGS_URL).into()); - } - - let latest_tag = response[0].name.clone(); - info!("Latest tag: {latest_tag}"); - return Ok(latest_tag); -} diff --git a/infrastructure/sync_versions/src/handlers/mod.rs b/infrastructure/sync_versions/src/handlers/mod.rs deleted file mode 100644 index a0dd16d..0000000 --- a/infrastructure/sync_versions/src/handlers/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::error; - -mod get_latest_meilisearch_version; - -type Handler = fn() -> Result>; - -pub const HANDLERS: &[(&str, Handler)] = &[("get-latest-meilisearch-version", get_latest_meilisearch_version::handle)]; - -#[cfg(test)] -mod tests { - use std::collections; - - use super::*; - - #[test] - fn test_all_handlers_have_unique_names() { - let names: Vec<&str> = HANDLERS.iter().map(|(name, _)| return *name).collect(); - let unique: collections::HashSet<&&str> = names.iter().collect(); - assert_eq!(names.len(), unique.len()); - } -} diff --git a/infrastructure/sync_versions/src/main.rs b/infrastructure/sync_versions/src/main.rs deleted file mode 100644 index c883714..0000000 --- a/infrastructure/sync_versions/src/main.rs +++ /dev/null @@ -1,47 +0,0 @@ -mod handlers; - -use std::env; -use std::process; - -fn main() { - env_logger::init(); - - let verb = parse_verb(); - - let Some((_, handler)) = handlers::HANDLERS.iter().find(|(name, _)| return *name == verb) else { - eprintln!("Unknown verb: {verb}"); - eprintln!("Available verbs:"); - for (name, _) in handlers::HANDLERS { - eprintln!("{}", name); - } - process::exit(2); - }; - - match handler() { - Ok(output) => { - println!("{output}"); - process::exit(0); - } - Err(e) => { - eprintln!("Error: {e}"); - process::exit(1); - } - } -} - -fn parse_verb() -> String { - // Skip the first item, that's the program name itself. - let mut args = env::args().skip(1); - - let Some(verb) = args.next() else { - eprintln!("Usage: sync_versions "); - process::exit(2); - }; - - if args.next().is_some() { - eprintln!("Too many arguments provided. Only one verb is expected."); - process::exit(2); - } - - return verb; -} diff --git a/infrastructure/sync_versions/tests/get_latest_meilisearch_version.rs b/infrastructure/sync_versions/tests/get_latest_meilisearch_version.rs deleted file mode 100644 index ae14895..0000000 --- a/infrastructure/sync_versions/tests/get_latest_meilisearch_version.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::process::Command; - -#[test] -fn test_get_latest_meilisearch_version_returns_tag() { - let output = Command::new(env!("CARGO_BIN_EXE_sync_versions")) - .arg("get-latest-meilisearch-version") - .output() - .expect("failed to execute sync_versions binary"); - - assert!(output.status.success(), "process exited with error"); - - let stdout = String::from_utf8(output.stdout).expect("invalid UTF-8"); - let tag = stdout.trim(); - - assert!(tag.starts_with("v"), "expected tag starting with 'v', got: {tag}"); -}