feat: Support COURSIER_SHA256 environment variable (#1527)

Allows users to specify a different Coursier version by overriding
the SHA256 checksum, complementing the existing COURSIER_URL env var.

Closes bazel-contrib/rules_jvm_external#1526
diff --git a/README.md b/README.md
index 4feea48..e100b11 100644
--- a/README.md
+++ b/README.md
@@ -476,6 +476,17 @@
 
 Please note it still requires the SHA to match.
 
+To use a different Coursier version entirely, you can override both the URL and SHA256:
+
+```shell
+$ bazel build @maven//... \
+  --repo_env=COURSIER_URL='https://github.com/coursier/coursier/releases/download/v2.1.25-M23/coursier.jar' \
+  --repo_env=COURSIER_SHA256='<sha256-of-the-jar>'
+```
+
+Note: rules_jvm_external is tested against a specific Coursier version. Using a
+different version is not officially supported and compatibility is not guaranteed.
+
 ### `artifact` helper macro
 
 The `artifact` macro translates the artifact's `group:artifact` coordinates to
diff --git a/private/rules/coursier.bzl b/private/rules/coursier.bzl
index f534d61..16f29de 100644
--- a/private/rules/coursier.bzl
+++ b/private/rules/coursier.bzl
@@ -927,6 +927,9 @@
         else:
             print("\n".join(msg_parts))
 
+def get_coursier_sha256(environ, default_sha256):
+    return environ.get("COURSIER_SHA256", default_sha256)
+
 # Get the path to the cache directory containing Coursier-downloaded artifacts.
 #
 # This method is public for testing.
@@ -1198,7 +1201,8 @@
     if coursier_url_from_env != None:
         coursier_download_urls.insert(0, coursier_url_from_env)
 
-    repository_ctx.download(coursier_download_urls, "coursier", sha256 = COURSIER_CLI_SHA256, executable = True)
+    coursier_sha256 = get_coursier_sha256(repository_ctx.os.environ, COURSIER_CLI_SHA256)
+    repository_ctx.download(coursier_download_urls, "coursier", sha256 = coursier_sha256, executable = True)
 
     # Try running coursier once
     cmd = _generate_java_jar_command_for_coursier(repository_ctx, repository_ctx.path("coursier"))
@@ -1721,6 +1725,7 @@
         "NO_PROXY",
         "COURSIER_CACHE",
         "COURSIER_OPTS",
+        "COURSIER_SHA256",
         "COURSIER_URL",
         "RJE_VERBOSE",
         "XDG_CACHE_HOME",
diff --git a/tests/unit/coursier_test.bzl b/tests/unit/coursier_test.bzl
index 954af9a..7ea0dfc 100644
--- a/tests/unit/coursier_test.bzl
+++ b/tests/unit/coursier_test.bzl
@@ -4,6 +4,7 @@
     "//private/rules:coursier.bzl",
     "compute_dependency_inputs_signature",
     "get_coursier_cache_or_default",
+    "get_coursier_sha256",
     "get_direct_dependencies",
     "get_netrc_lines_from_entries",
     infer = "infer_artifact_path_from_primary_and_repos",
@@ -446,6 +447,28 @@
 
 get_coursier_cache_or_default_enabled_with_custom_location_test = add_test(_get_coursier_cache_or_default_enabled_with_custom_location_test)
 
+def _get_coursier_sha256_default_test_impl(ctx):
+    env = unittest.begin(ctx)
+    asserts.equals(
+        env,
+        "default_sha256_value",
+        get_coursier_sha256({}, "default_sha256_value"),
+    )
+    return unittest.end(env)
+
+get_coursier_sha256_default_test = add_test(_get_coursier_sha256_default_test_impl)
+
+def _get_coursier_sha256_from_env_test_impl(ctx):
+    env = unittest.begin(ctx)
+    asserts.equals(
+        env,
+        "custom_sha256_from_env",
+        get_coursier_sha256({"COURSIER_SHA256": "custom_sha256_from_env"}, "default_sha256_value"),
+    )
+    return unittest.end(env)
+
+get_coursier_sha256_from_env_test = add_test(_get_coursier_sha256_from_env_test_impl)
+
 def _mock_which_true(path):
     return True