diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 473356f57..046d2ccaf 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/rust:1.60.0-bullseye +FROM docker.io/rust:1.63.0-bullseye ENV DEBIAN_FRONTEND=noninteractive RUN apt update && apt upgrade -y diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36ae35da2..c2d730d83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,29 +42,28 @@ jobs: - name: Build workspace run: cargo build - # Workspace tests - - name: Run workspace unit tests - run: cargo test --lib --all -j6 - - name: Run workspace doc tests - run: cargo test --doc --all -j6 - - name: Test examples - run: cargo test -p kube-examples --examples -j6 - - name: Compile check remaining examples - # No OS specific code in examples, run this on fastest executor + # Workspace unit tests with various feature sets + - name: Run workspace unit tests (no default features) + run: cargo test --workspace --lib --no-default-features -j6 if: matrix.os == 'ubuntu-latest' - run: cargo build -j4 -p kube-examples - - # Feature tests - - name: Test kube with features rustls-tls,ws,oauth - run: cargo test -p kube --lib --no-default-features --features=rustls-tls,ws,oauth + - name: Run workspace unit tests (default features) + run: cargo test --workspace --lib --exclude kube-examples --exclude e2e -j6 if: matrix.os == 'ubuntu-latest' - - name: Test kube with features openssl-tls,ws,oauth - run: cargo test -p kube --lib --no-default-features --features=openssl-tls,ws,oauth - if: matrix.os == 'ubuntu-latest' - # Feature tests in examples - - name: Test crd_derive_no_schema example - run: cargo test -p kube-examples --example crd_derive_no_schema --no-default-features --features=openssl-tls,latest + - name: Run workspace unit tests (all features) + run: cargo test --workspace --lib --all-features --exclude kube-examples --exclude e2e -j6 + # Workspace documentation (all features only) + - name: Run workspace doc tests + run: cargo test --workspace --doc --all-features --exclude kube-examples --exclude e2e -j6 + - name: Run ad-hoc doc test verification + run: | + if rg "\`\`\`ignored"; then + echo "ignored doctests are not allowed, use compile_fail or no_run" + exit 1 + fi if: matrix.os == 'ubuntu-latest' + # Examples + - name: Test examples + run: cargo test -p kube-examples --examples -j6 msrv: # Run `cargo check` on our minimum supported Rust version @@ -76,7 +75,7 @@ jobs: run: | MSRV=$(grep MSRV README.md | grep -oE "[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+") echo $MSRV - echo ::set-output name=msrv::${MSRV} + echo "msrv=${MSRV}" >> $GITHUB_OUTPUT - uses: actions-rs/toolchain@v1 with: toolchain: ${{ steps.msrv.outputs.msrv }} @@ -87,7 +86,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: check - args: --all + args: --workspace - name: Check rust-version keys matches MSRV consistently run: | @@ -148,18 +147,12 @@ jobs: run: cargo build # Run the equivalent of `just integration` - - name: Run all default features integration library tests - run: cargo test --lib --all -- --ignored - - name: Run all facade integration library tests with extra features - run: cargo test -p kube --lib --features=derive,runtime -- --ignored --nocapture + - name: Run all integration library tests + run: cargo test --lib --workspace --exclude e2e --all-features -j6 -- --ignored - name: Run crd example tests run: cargo run -p kube-examples --example crd_api - - name: Run all client integration library tests with rustls and ws - run: cargo test -p kube-client --lib --features=rustls-tls,ws -- --ignored - name: Run derive example tests run: cargo run -p kube-examples --example crd_derive - - name: Run crd example tests - run: cargo run -p kube-examples --example crd_api mk8sv: # comile check e2e tests against mk8sv @@ -172,8 +165,8 @@ jobs: run: | MK8SV=$(grep MK8SV README.md | grep -oE "[[:digit:]]+\.[[:digit:]]+" | head -n 1) echo $MK8SV - echo ::set-output name=mk8sv::${MK8SV} - echo ::set-output name=mk8svdash::v${MK8SV/\./_} + echo "mk8sv=${MK8SV}" >> $GITHUB_OUTPUT + echo "mk8svdash=v${MK8SV/\./_}" >> $GITHUB_OUTPUT - name: Check ci jobs run against advertised MK8SV run: | diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml index f54679026..72435d8e5 100644 --- a/.github/workflows/rustfmt.yml +++ b/.github/workflows/rustfmt.yml @@ -1,4 +1,4 @@ -# When pushed to main, run `cargo +nightly fmt --all` and open a PR. +# When pushed to main, run `cargo +nightly fmt` against all files and open a PR. name: rustfmt on: push: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 02a07083d..92ddf04a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,9 +45,9 @@ The easiest way set up a minimal Kubernetes cluster for these is with [`k3d`](ht ### Unit Tests & Documentation Tests -**Most** unit/doc tests are run from `cargo test --lib --doc --all`, but because of feature-sets, and examples, you will need a couple of extra invocations to replicate our CI. +Unit and doc tests are run against a particular crate with `cargo test -p KUBECRATE --lib --doc`, but because of feature-sets, you will need a couple of extra flags and invocations to replicate all our CI conditions. -For the complete variations, run the `just test` target in the `justfile`. +To run **all** unit tests, call: `just test` All public interfaces must be documented, and most should have minor documentation examples to show usage. @@ -57,7 +57,9 @@ Slower set of tests within the crates marked with an **`#[ignore]`** attribute. :warning: These **WILL** try to modify resources in your current cluster :warning: -Most integration tests are run with `cargo test --all --lib -- --ignored`, but because of feature-sets, you will need a few invocations of these to replicate our CI. See `just test-integration` +Integration tests are run against a crate with `cargo test -p KUBECRATE --lib -- --ignored`, but because of feature-sets, you will need a few invocations of these to replicate our CI. + +To run **all** integration tests, call: `just test-integration` ### End to End Tests @@ -75,7 +77,7 @@ All public interfaces should have doc tests with examples for [docs.rs](https:// When adding new non-trivial pieces of logic that results in a drop in coverage you should add a test. -Cross-reference with the coverage build [![coverage build](https://codecov.io/gh/kube-rs/kube/branch/main/graph/badge.svg?token=9FCqEcyDTZ)](https://codecov.io/gh/kube-rs/kube) and go to your branch. Coverage can also be run locally with [`cargo tarpaulin`](https://github.com/xd009642/tarpaulin) at project root. This will use our [tarpaulin.toml](https://github.com/kube-rs/kube/blob/main/tarpaulin.toml) config, and **will run both unit and integration** tests. +Cross-reference with the coverage build [![coverage build](https://codecov.io/gh/kube-rs/kube/branch/main/graph/badge.svg?token=9FCqEcyDTZ)](https://app.codecov.io/gh/kube-rs/kube/tree/main) and go to your branch. Coverage can also be run locally with [`cargo tarpaulin`](https://github.com/xd009642/tarpaulin) at project root. This will use our [tarpaulin.toml](https://github.com/kube-rs/kube/blob/main/tarpaulin.toml) config, and **will run both unit and integration** tests. #### What type of test diff --git a/README.md b/README.md index 352973884..fdd9c18de 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # kube-rs [![Crates.io](https://img.shields.io/crates/v/kube.svg)](https://crates.io/crates/kube) -[![Rust 1.60](https://img.shields.io/badge/MSRV-1.60-dea584.svg)](https://github.com/rust-lang/rust/releases/tag/1.60.0) +[![Rust 1.63](https://img.shields.io/badge/MSRV-1.63-dea584.svg)](https://github.com/rust-lang/rust/releases/tag/1.63.0) [![Tested against Kubernetes v1_21 and above](https://img.shields.io/badge/MK8SV-v1_21-326ce5.svg)](https://kube.rs/kubernetes-version) [![Best Practices](https://bestpractices.coreinfrastructure.org/projects/5413/badge)](https://bestpractices.coreinfrastructure.org/projects/5413) [![Discord chat](https://img.shields.io/discord/500028886025895936.svg?logo=discord&style=plastic)](https://discord.gg/tokio) diff --git a/justfile b/justfile index 75ec929fa..00fb61ad2 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,5 @@ VERSION := `git rev-parse HEAD` +open := if os() == "macos" { "open" } else { "xdg-open" } [private] default: @@ -22,25 +23,31 @@ deny: # Unit tests test: - cargo test --lib --all - cargo test --doc --all + #!/usr/bin/env bash + if rg "\`\`\`ignored"; then + echo "ignored doctests are not allowed, use compile_fail or no_run" + exit 1 + fi + # no default features + cargo test --workspace --lib --no-default-features + # default features + cargo test --workspace --lib --exclude kube-examples --exclude e2e + # all features + cargo test --workspace --lib --all-features --exclude kube-examples --exclude e2e + cargo test --workspace --doc --all-features --exclude kube-examples --exclude e2e cargo test -p kube-examples --examples - cargo test -p kube --lib --no-default-features --features=rustls-tls,ws,oauth - cargo test -p kube --lib --no-default-features --features=openssl-tls,ws,oauth - cargo test -p kube --lib --no-default-features # Integration tests (will modify your current context's cluster) test-integration: - kubectl delete pod -lapp=kube-rs-test - cargo test --lib --all -- --ignored # also run tests that fail on github actions - cargo test -p kube --lib --features=derive,runtime -- --ignored - cargo test -p kube-client --lib --features=rustls-tls,ws -- --ignored + kubectl delete pod -lapp=kube-rs-test > /dev/null + cargo test --lib --workspace --exclude e2e --all-features -- --ignored + # some examples are canonical tests cargo run -p kube-examples --example crd_derive cargo run -p kube-examples --example crd_api coverage: cargo tarpaulin --out=Html --output-dir=. - #xdg-open tarpaulin-report.html + {{open}} tarpaulin-report.html readme: rustdoc README.md --test --edition=2021 diff --git a/kube-client/Cargo.toml b/kube-client/Cargo.toml index 1c33fcb72..869c5c757 100644 --- a/kube-client/Cargo.toml +++ b/kube-client/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/kube-rs/kube" readme = "../README.md" keywords = ["kubernetes", "client",] categories = ["web-programming::http-client", "configuration", "network-programming", "api-bindings"] -rust-version = "1.60.0" +rust-version = "1.63.0" edition = "2021" [features] diff --git a/kube-client/src/api/remote_command.rs b/kube-client/src/api/remote_command.rs index 2c925df25..299dffff9 100644 --- a/kube-client/src/api/remote_command.rs +++ b/kube-client/src/api/remote_command.rs @@ -161,9 +161,15 @@ impl AttachedProcess { } /// Async writer to stdin. - /// ```ignore + /// ```no_run + /// # use kube_client::api::AttachedProcess; + /// # use tokio::io::{AsyncReadExt, AsyncWriteExt}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let attached: AttachedProcess = todo!(); /// let mut stdin_writer = attached.stdin().unwrap(); /// stdin_writer.write(b"foo\n").await?; + /// # Ok(()) + /// # } /// ``` /// Only available if [`AttachParams`](super::AttachParams) had `stdin`. pub fn stdin(&mut self) -> Option { @@ -174,9 +180,16 @@ impl AttachedProcess { } /// Async reader for stdout outputs. - /// ```ignore + /// ```no_run + /// # use kube_client::api::AttachedProcess; + /// # use tokio::io::{AsyncReadExt, AsyncWriteExt}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let attached: AttachedProcess = todo!(); /// let mut stdout_reader = attached.stdout().unwrap(); - /// let next_stdout = stdout_reader.read().await?; + /// let mut buf = [0u8; 4]; + /// stdout_reader.read_exact(&mut buf).await?; + /// # Ok(()) + /// # } /// ``` /// Only available if [`AttachParams`](super::AttachParams) had `stdout`. pub fn stdout(&mut self) -> Option { @@ -187,9 +200,16 @@ impl AttachedProcess { } /// Async reader for stderr outputs. - /// ```ignore + /// ```no_run + /// # use kube_client::api::AttachedProcess; + /// # use tokio::io::{AsyncReadExt, AsyncWriteExt}; + /// # async fn wrapper() -> Result<(), Box> { + /// # let attached: AttachedProcess = todo!(); /// let mut stderr_reader = attached.stderr().unwrap(); - /// let next_stderr = stderr_reader.read().await?; + /// let mut buf = [0u8; 4]; + /// stderr_reader.read_exact(&mut buf).await?; + /// # Ok(()) + /// # } /// ``` /// Only available if [`AttachParams`](super::AttachParams) had `stderr`. pub fn stderr(&mut self) -> Option { @@ -218,12 +238,19 @@ impl AttachedProcess { } /// Async writer to change the terminal size - /// ```ignore + /// ```no_run + /// # use kube_client::api::{AttachedProcess, TerminalSize}; + /// # use tokio::io::{AsyncReadExt, AsyncWriteExt}; + /// # use futures::SinkExt; + /// # async fn wrapper() -> Result<(), Box> { + /// # let attached: AttachedProcess = todo!(); /// let mut terminal_size_writer = attached.terminal_size().unwrap(); /// terminal_size_writer.send(TerminalSize{ /// height: 100, /// width: 200, /// }).await?; + /// # Ok(()) + /// # } /// ``` /// Only available if [`AttachParams`](super::AttachParams) had `tty`. pub fn terminal_size(&mut self) -> Option { diff --git a/kube-client/src/config/file_config.rs b/kube-client/src/config/file_config.rs index ff01857be..468121faa 100644 --- a/kube-client/src/config/file_config.rs +++ b/kube-client/src/config/file_config.rs @@ -25,14 +25,14 @@ pub struct Kubeconfig { #[serde(skip_serializing_if = "Option::is_none")] pub preferences: Option, /// Referencable names to cluster configs - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, deserialize_with = "deserialize_null_as_default")] pub clusters: Vec, /// Referencable names to user configs #[serde(rename = "users")] - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, deserialize_with = "deserialize_null_as_default")] pub auth_infos: Vec, /// Referencable names to context configs - #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[serde(default, deserialize_with = "deserialize_null_as_default")] pub contexts: Vec, /// The name of the context that you would like to use by default #[serde(rename = "current-context")] @@ -152,6 +152,15 @@ where } } +fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + let opt = Option::deserialize(deserializer)?; + Ok(opt.unwrap_or_default()) +} + /// AuthInfo stores information to tell cluster who you are. #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct AuthInfo { @@ -261,7 +270,7 @@ pub struct ExecConfig { /// Specifies which environment variables the host should avoid passing to the auth plugin. /// /// This does currently not exist upstream and cannot be specified on disk. - /// It has been suggested in client-go via https://github.com/kubernetes/client-go/issues/1177 + /// It has been suggested in client-go via #[serde(skip)] pub drop_env: Option>, diff --git a/kube-core/Cargo.toml b/kube-core/Cargo.toml index 112fc67c0..421d744e1 100644 --- a/kube-core/Cargo.toml +++ b/kube-core/Cargo.toml @@ -7,7 +7,7 @@ authors = [ "kazk ", ] edition = "2021" -rust-version = "1.60.0" +rust-version = "1.63.0" license = "Apache-2.0" keywords = ["kubernetes", "apimachinery"] categories = ["api-bindings", "encoding", "parser-implementations"] diff --git a/kube-core/src/admission.rs b/kube-core/src/admission.rs index 37e781cc9..94c8eb79c 100644 --- a/kube-core/src/admission.rs +++ b/kube-core/src/admission.rs @@ -76,11 +76,11 @@ impl TryInto> for AdmissionReview { /// /// In an admission controller scenario, this is extracted from an [`AdmissionReview`] via [`TryInto`] /// -/// ```ignore +/// ```no_run /// use kube::api::{admission::{AdmissionRequest, AdmissionReview}, DynamicObject}; /// /// // The incoming AdmissionReview received by the controller. -/// let body: AdmissionReview; +/// let body: AdmissionReview = todo!(); /// let req: AdmissionRequest<_> = body.try_into().unwrap(); /// ``` /// @@ -204,14 +204,14 @@ pub enum Operation { /// An outgoing [`AdmissionReview`] response. Constructed from the corresponding /// [`AdmissionRequest`]. -/// ```ignore +/// ```no_run /// use kube::api::{ /// admission::{AdmissionRequest, AdmissionResponse, AdmissionReview}, /// DynamicObject, /// }; /// /// // The incoming AdmissionReview received by the controller. -/// let body: AdmissionReview; +/// let body: AdmissionReview = todo!(); /// let req: AdmissionRequest<_> = body.try_into().unwrap(); /// /// // A normal response with no side effects. diff --git a/kube-derive/Cargo.toml b/kube-derive/Cargo.toml index 1a5c1baa4..5b6671db0 100644 --- a/kube-derive/Cargo.toml +++ b/kube-derive/Cargo.toml @@ -7,7 +7,7 @@ authors = [ "kazk ", ] edition = "2021" -rust-version = "1.60.0" +rust-version = "1.63.0" license = "Apache-2.0" repository = "https://github.com/kube-rs/kube" readme = "../README.md" @@ -28,7 +28,7 @@ proc-macro = true [dev-dependencies] serde = { version = "1.0.130", features = ["derive"] } serde_yaml = "0.8.21" -kube = { path = "../kube", default-features = false, version = "<1.0.0, >=0.61.0", features = ["derive"] } +kube = { path = "../kube", version = "<1.0.0, >=0.61.0", features = ["derive", "client"] } k8s-openapi = { version = "0.17.0", default-features = false, features = ["v1_26"] } schemars = { version = "0.8.6", features = ["chrono"] } validator = { version = "0.16.0", features = ["derive"] } diff --git a/kube-derive/src/lib.rs b/kube-derive/src/lib.rs index 5935e7ce8..ea0c74ff7 100644 --- a/kube-derive/src/lib.rs +++ b/kube-derive/src/lib.rs @@ -42,12 +42,22 @@ mod custom_resource; /// and optionally status. The **generated** type `Foo` can be used with the [`kube`] crate /// as an `Api` object (`FooSpec` can not be used with [`Api`][`kube::Api`]). /// -/// ```rust,ignore -/// let client = Client::try_default().await?; -/// let foos: Api = Api::namespaced(client.clone(), "default"); -/// +/// ```no_run +/// # use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition; +/// # use kube_derive::CustomResource; +/// # use kube::{api::{Api, Patch, PatchParams}, Client, CustomResourceExt}; +/// # use serde::{Deserialize, Serialize}; +/// # async fn wrapper() -> Result<(), Box> { +/// # #[derive(CustomResource, Clone, Debug, Deserialize, Serialize, schemars::JsonSchema)] +/// # #[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)] +/// # struct FooSpec {} +/// # let client: Client = todo!(); +/// let foos: Api = Api::default_namespaced(client.clone()); /// let crds: Api = Api::all(client.clone()); -/// crds.patch("foos.clux.dev", &ssapply, serde_yaml::to_vec(&Foo::crd())?).await +/// let crd_yaml = serde_yaml::to_vec(&Foo::crd())?; +/// crds.patch("foos.clux.dev", &PatchParams::apply("myapp"), &Patch::Apply(crd_yaml)).await; +/// # Ok(()) +/// # } /// ``` /// /// This example posts the generated `::crd` to the `CustomResourceDefinition` API. @@ -177,8 +187,8 @@ mod custom_resource; /// /// # Generated code /// -/// The example above will roughly generate: -/// ```ignore +/// The example above will **roughly** generate: +/// ```compile_fail /// #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, JsonSchema)] /// #[serde(rename_all = "camelCase")] /// pub struct FooCrd { @@ -188,11 +198,11 @@ mod custom_resource; /// spec: FooSpec, /// status: Option, /// } -/// impl kube::Resource for FooCrd {...} +/// impl kube::Resource for FooCrd { .. } /// /// impl FooCrd { -/// pub fn new(name: &str, spec: FooSpec) -> Self { ... } -/// pub fn crd() -> k8s_openapi::...::CustomResourceDefinition { ... } +/// pub fn new(name: &str, spec: FooSpec) -> Self { .. } +/// pub fn crd() -> CustomResourceDefinition { .. } /// } /// ``` /// diff --git a/kube-runtime/Cargo.toml b/kube-runtime/Cargo.toml index 109bc16af..6adac2fd9 100644 --- a/kube-runtime/Cargo.toml +++ b/kube-runtime/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/kube-rs/kube" readme = "../README.md" keywords = ["kubernetes", "runtime", "reflector", "watcher", "controller"] categories = ["web-programming::http-client", "caching", "network-programming"] -rust-version = "1.60.0" +rust-version = "1.63.0" edition = "2021" [features] diff --git a/kube/Cargo.toml b/kube/Cargo.toml index 7f22f2e7e..148152e8a 100644 --- a/kube/Cargo.toml +++ b/kube/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/kube-rs/kube" readme = "../README.md" keywords = ["kubernetes", "client", "runtime", "cncf"] categories = ["network-programming", "caching", "api-bindings", "configuration", "encoding"] -rust-version = "1.60.0" +rust-version = "1.63.0" edition = "2021" [features] diff --git a/release.toml b/release.toml index dfa006f51..21c7eef10 100644 --- a/release.toml +++ b/release.toml @@ -1,6 +1,6 @@ # Release process :: cargo-release >= 0.18.3 # -# Dependencies: cargo-release, cargo-tree, sd, ripgrep +# Dependencies: https://kube.rs/tools # # 0. (optional) cargo release minor ; verify readme + changelog bumped; then git reset --hard # 1. PUBLISH_GRACE_SLEEP=20 cargo release minor --execute diff --git a/scripts/release-post.sh b/scripts/release-post.sh index b04ba9a0a..c7804237f 100755 --- a/scripts/release-post.sh +++ b/scripts/release-post.sh @@ -3,7 +3,7 @@ set -euo pipefail main() { cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. # aka $WORKSPACE_ROOT - local -r CURRENT_VER="$(rg "kube =" README.md | head -n 1 | rg 'version = "(\S*)"' -or '$1')" + local -r CURRENT_VER="$(rg 'kube = \{ version = "(\S*)"' -or '$1' README.md | head -n1)" git tag -a "${CURRENT_VER}" -m "${CURRENT_VER}" git push git push --tags diff --git a/scripts/release-pre.sh b/scripts/release-pre.sh index 650cff0a6..ee6f5e200 100755 --- a/scripts/release-pre.sh +++ b/scripts/release-pre.sh @@ -16,7 +16,7 @@ replace-docs() { } sanity() { - CARGO_TREE_OPENAPI="$(cargo tree -i k8s-openapi | head -n 1 | choose 1)" + CARGO_TREE_OPENAPI="$(cargo tree -i k8s-openapi --depth=0 -e=normal | choose 1)" USED_K8S_OPENAPI="${CARGO_TREE_OPENAPI:1}" RECOMMENDED_K8S_OPENAPI="$(rg "k8s-openapi =" README.md | head -n 1)" # only check first instance if ! [[ $RECOMMENDED_K8S_OPENAPI =~ $USED_K8S_OPENAPI ]]; then @@ -29,7 +29,7 @@ sanity() { main() { # We only want this to run ONCE at workspace level cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. # aka $WORKSPACE_ROOT - local -r CURRENT_VER="$(rg "kube =" README.md | head -n 1 | rg 'version = "(\S*)"' -or '$1')" + local -r CURRENT_VER="$(rg 'kube = \{ version = "(\S*)"' -or '$1' README.md | head -n1)" # If the main README has been bumped, assume we are done: if [[ "${NEW_VERSION}" = "${CURRENT_VER}" ]]; then