Feature/rdkemw 18408#3
Conversation
|
b'## Copyright scan failure |
There was a problem hiding this comment.
Pull request overview
Introduces a new standalone rdke_splash application that renders a JPEG/PNG splash image fullscreen via OpenGL ES 2.0 on top of Essos/Westeros, dismissed by the presence of a flag file. It ships with build, packaging and runtime integration files (CMake, systemd unit, dismiss script, README, Apache-2.0 LICENSE).
Changes:
- New C++17 splash binary (
src/main.cpp) with JPEG (libjpeg) and PNG (libpng) decoders, a minimal GL ES 2.0 pipeline drawing a textured full-screen quad, signal/Essos-driven shutdown, and a file-based dismiss mechanism. - New CMake build (linking
essos,westeros_gl,EGL,GLESv2,jpeg,png,z) and install rules for binary, dismiss script and systemd unit. - Operational glue:
rdke_splash.service(Type=simple + RemainAfterExit),dismiss_splash.sh, README documentation, and the Apache-2.0 LICENSE.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 16 comments.
Show a summary per file
| File | Description |
|---|---|
| rdke_splash_opengles/src/main.cpp | Implements the splash application: image decode, GL setup, render loop, Essos integration, CLI parsing. |
| rdke_splash_opengles/CMakeLists.txt | Build/install configuration for the new binary, dismiss script and service file. |
| rdke_splash_opengles/rdke_splash.service | Systemd unit launching the splash with LD_PRELOAD=libwesteros_gl.so.0.0.0. |
| rdke_splash_opengles/dismiss_splash.sh | Shell helper that touches the dismiss flag file. |
| rdke_splash_opengles/README.md | Usage, build and operational documentation. |
| rdke_splash_opengles/LICENSE | Apache-2.0 license text. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # The splash image is expected to be part of the firmware image. | ||
| # If no --image is provided, the app shows an internal fallback splash. | ||
| ExecStart=/usr/bin/rdke_splash | ||
| RemainAfterExit=yes |
| const int fallbackWidth = (display.width > 0 ? display.width : 1920); | ||
| const int fallbackHeight = (display.height > 0 ? display.height : 1080); | ||
|
|
||
| // Match legacy behavior: show a solid red splash if no image. | ||
| image = makeSolidColorRgbaImage(fallbackWidth, fallbackHeight, 0xFF, 0x00, 0x00, 0xFF); |
| static const GLfloat kVerts[4][2] = { | ||
| {-1.0f, -1.0f}, | ||
| {1.0f, -1.0f}, | ||
| {-1.0f, 1.0f}, | ||
| {1.0f, 1.0f}, | ||
| }; | ||
|
|
||
| static const GLfloat kUvs[4][2] = { | ||
| {0.f, 1.f}, | ||
| {1.f, 1.f}, | ||
| {0.f, 0.f}, | ||
| {1.f, 0.f}, | ||
| }; | ||
|
|
||
| glUseProgram(gl.program); | ||
| glDisable(GL_BLEND); | ||
|
|
||
| glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, kVerts); | ||
| glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, kUvs); | ||
| glEnableVertexAttribArray(0); | ||
| glEnableVertexAttribArray(1); | ||
|
|
||
| bool firstFrame = true; | ||
| while (!gShouldQuit) | ||
| { | ||
| if (fileExists(opt->dismissFile.c_str())) | ||
| break; | ||
|
|
||
| glViewport(0, 0, display.width, display.height); | ||
| glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); |
| GlProgram gl{}; | ||
| const uint64_t glStartMs = nowMs(); | ||
| if (ok) | ||
| { | ||
| auto prog = createProgramAndTexture(*image); | ||
| if (!prog) | ||
| { | ||
| ok = false; | ||
| } | ||
| else | ||
| { | ||
| gl = *prog; | ||
| } | ||
| } | ||
| const uint64_t glSetupMs = nowMs() - glStartMs; | ||
|
|
||
| if (!ok) | ||
| { | ||
| const char* detail = EssContextGetLastErrorDetail(ctx); | ||
| std::printf("Startup failed. Essos detail: %s\n", (detail ? detail : "(none)")); | ||
| destroyProgramAndTexture(gl); | ||
| EssContextDestroy(ctx); | ||
| return 4; | ||
| } |
| * If not stated otherwise in this file or this component's LICENSE file the | ||
| * following copyright and licenses apply: | ||
| * | ||
| * Copyright 2026 |
mhughesacn
left a comment
There was a problem hiding this comment.
Hi @Chirag-BN :
- I agree with Copilot. Please add proper RDK Management copyrights to main.cpp and CMakeLists.txt. You don't need a header on dismiss_splash.sh (trivial content) or the service file (configuration file).
- Also, please add the other standard license files as in https://github.com/rdkcentral/sample-licensing/tree/main/standard_files/other-apache (i.e. COPYING (a soft link to LICENSE), CONTRIBUTING.md and NOTICE. NOTICE should be customised to delete the last line as it doesn't apply and fix the copyright on line 1.) This assumes that this is an RDK component. If this is Comcast's, please use the Comcast templates instead, from the same repo.
- There is a code match on lines main.cpp:243-261 but it looks like very boilerplate code, meaning this is a normal use of png APIs which everyone would be expected to use. Please comment on where you got the code or if it is your own work.
Thank you
|
All contributors have signed the CLA ✍️ ✅ |
465b35b to
bcea3f8
Compare
| if (components == 3) | ||
| { | ||
| for (int x = 0; x < width; ++x) | ||
| { | ||
| const uint8_t r = row[static_cast<size_t>(x) * 3 + 0]; | ||
| const uint8_t g = row[static_cast<size_t>(x) * 3 + 1]; | ||
| const uint8_t b = row[static_cast<size_t>(x) * 3 + 2]; | ||
| dst[static_cast<size_t>(x) * 4 + 0] = r; | ||
| dst[static_cast<size_t>(x) * 4 + 1] = g; | ||
| dst[static_cast<size_t>(x) * 4 + 2] = b; | ||
| dst[static_cast<size_t>(x) * 4 + 3] = 0xFF; | ||
| } |
| if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) | ||
| png_set_gray_to_rgb(pngPtr); | ||
|
|
||
| // Ensure RGBA. | ||
| if (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_PALETTE) | ||
| png_set_filler(pngPtr, 0xFF, PNG_FILLER_AFTER); | ||
|
|
||
| png_read_update_info(pngPtr, infoPtr); | ||
|
|
| ### Exit codes | ||
|
|
||
| - `0`: normal exit (dismiss file detected) | ||
| - `1`: usage/config/runtime failure (e.g., GL init failure) |
| bool fileExists(const char* path) | ||
| { | ||
| struct stat st; | ||
| return (path != nullptr) && (lstat(path, &st) == 0); |
| bool ok = true; | ||
| DisplayState display; | ||
|
|
||
| if (!EssContextSetTerminateListener(ctx, nullptr, &kTermListener)) | ||
| ok = false; | ||
| if (!EssContextSetSettingsListener(ctx, &display, &kSettingsListener)) | ||
| ok = false; | ||
|
|
||
| if (ok && !EssContextInit(ctx)) | ||
| { | ||
| std::printf("EssContextInit failed\n"); | ||
| ok = false; | ||
| } | ||
|
|
||
| if (ok && !EssContextGetDisplaySize(ctx, &display.width, &display.height)) | ||
| { | ||
| std::printf("EssContextGetDisplaySize failed\n"); | ||
| ok = false; | ||
| } | ||
|
|
||
| if (ok && !EssContextSetInitialWindowSize(ctx, display.width, display.height)) | ||
| { | ||
| std::printf("EssContextSetInitialWindowSize failed\n"); | ||
| ok = false; | ||
| } | ||
|
|
||
| if (ok && !EssContextStart(ctx)) | ||
| { | ||
| std::printf("EssContextStart failed\n"); | ||
| ok = false; | ||
| } | ||
|
|
||
| std::optional<Image> image; | ||
| const uint64_t decodeStartMs = nowMs(); | ||
| if (!opt->imagePath.empty()) | ||
| { | ||
| image = decodeImageToRgba(opt->imagePath); | ||
| if (!image) | ||
| { | ||
| std::printf("Failed to decode image: %s (using fallback)\n", opt->imagePath.c_str()); | ||
| } | ||
| } | ||
|
|
||
| if (!image) | ||
| { | ||
| // Match legacy behavior: show a solid red splash if no image. | ||
| // A 1x1 solid texture renders identically on the full-screen quad while | ||
| // avoiding a large fallback allocation and texture upload. | ||
| image = makeSolidColorRgbaImage(1, 1, 0xFF, 0x00, 0x00, 0xFF); | ||
| } | ||
| const uint64_t decodeMs = nowMs() - decodeStartMs; | ||
|
|
||
| GlProgram gl{}; | ||
| const uint64_t glStartMs = nowMs(); | ||
| if (ok) | ||
| { | ||
| auto prog = createProgramAndTexture(*image); | ||
| if (!prog) | ||
| { | ||
| ok = false; | ||
| } | ||
| else | ||
| { | ||
| gl = *prog; | ||
| } | ||
| } | ||
| const uint64_t glSetupMs = nowMs() - glStartMs; | ||
|
|
||
| if (!ok) | ||
| { | ||
| const char* detail = EssContextGetLastErrorDetail(ctx); | ||
| std::printf("Startup failed. Essos detail: %s\n", (detail ? detail : "(none)")); | ||
| destroyProgramAndTexture(gl); | ||
| EssContextDestroy(ctx); | ||
| return 4; | ||
| } |
| FOSSID_HOST_USERNAME: ${{ secrets.FOSSID_HOST_USERNAME }} | ||
| FOSSID_HOST_TOKEN: ${{ secrets.FOSSID_HOST_TOKEN }} | ||
| run: | | ||
| fossid \ | ||
| diffscan \ | ||
| --fossid-host $FOSSID_HOST_USERNAME \ | ||
| --fossid-token $FOSSID_HOST_TOKEN \ |
Chirag-BN
left a comment
There was a problem hiding this comment.
The copilot auto fixed the LICENSING part since it was failing in fossid scan.
Please check and review it.
mhughesacn
left a comment
There was a problem hiding this comment.
Hi @Chirag-BN : Please see comments - this repo needs to conform to the RDK standard format. I also need your input on my earlier code match comment.
Thank you,
MartinH, RDK CMF Compliance Team
| @@ -0,0 +1,743 @@ | |||
| /* | |||
There was a problem hiding this comment.
Please use full apache header here as required by Comcast Legal.
| @@ -0,0 +1,18 @@ | |||
| # Contributing | |||
There was a problem hiding this comment.
Please ensure that the text from the sample licensing directory in my earlier comment is incorporated into your own file.
| @@ -0,0 +1,3 @@ | |||
| This project is licensed under the Apache License, Version 2.0. | |||
There was a problem hiding this comment.
Please use the RDK standard here (a soft link to LICENSE).
| @@ -0,0 +1,6 @@ | |||
| Copyright 2026 RDK Management | |||
There was a problem hiding this comment.
Please use the standard Apache-2 license text as in the sample licensing repo I mentioned in my earlier comment.
| @@ -0,0 +1,8 @@ | |||
| RDK Splash Screen (rdke_splash_opengles) | |||
There was a problem hiding this comment.
Please use the standard NOTICE file from the sample licensing directory (i.e. other apache). (With RDK Copyright)
| @@ -0,0 +1,19 @@ | |||
| # Copyright 2026 RDK Management | |||
| # SPDX-License-Identifier: Apache-2.0 | |||
There was a problem hiding this comment.
Comcast Legal do not allow us to use the short SPDX header form. You can omit the header on this as it is a config file, but if you use one, it must be full apache please.
…ation Reason for change: Initial commit of the new opensource splash screen application.
a827af6 to
e477770
Compare
mhughesacn
left a comment
There was a problem hiding this comment.
Some more comments please. The code match has gone away after main.cpp has been changed.
| @@ -0,0 +1 @@ | |||
| LICENSE | |||
There was a problem hiding this comment.
This needs to be a soft link to LICENSE, not a file containing the text "LICENSE".
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
|
|
||
|
|
There was a problem hiding this comment.
The MIT license is not required as there is no MIT-licensed content.
| conditions of these licenses. The LICENSE file contains the text of all the licenses which apply | ||
| within this component, except as indicated in the following paragraph. | ||
|
|
||
|
|
There was a problem hiding this comment.
Please delete from here to end of file. (Lines 11-45 are coming from cut-and-paste.)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 10 comments.
Comments suppressed due to low confidence (1)
rdke_splash_opengles/src/main.cpp:315
- The same longjmp-across-C++-objects issue exists here: after the setjmp at line 237 returns normally, this function constructs
Image out(withstd::vector<uint8_t> rgba) andstd::vector<png_bytep> rows. If libpng then errors out ofpng_read_image(orpng_read_end) the resulting longjmp will skip those vectors' destructors, leaking heap memory and triggering undefined behavior per the C++ standard. Consider keeping the libpng interaction in a scope where no C++ objects with non-trivial destructors are live, or do a second setjmp around the read so that the cleanup branch can destroy these objects explicitly.
if (setjmp(png_jmpbuf(pngPtr)))
{
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
std::fclose(fp);
std::printf("decodePng: decode failed '%s'\n", path.c_str());
return std::nullopt;
}
png_set_read_fn(pngPtr, fp, pngReadFn);
png_set_sig_bytes(pngPtr, 8);
png_read_info(pngPtr, infoPtr);
png_uint_32 width = 0;
png_uint_32 height = 0;
int bitDepth = 0;
int colorType = 0;
png_get_IHDR(pngPtr, infoPtr, &width, &height, &bitDepth, &colorType, nullptr, nullptr, nullptr);
const bool fileHasAlpha = (colorType & PNG_COLOR_MASK_ALPHA) != 0;
const bool fileHasTrns = png_get_valid(pngPtr, infoPtr, PNG_INFO_tRNS) != 0;
if (bitDepth == 16)
png_set_strip_16(pngPtr);
if (colorType == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(pngPtr);
if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8)
png_set_expand_gray_1_2_4_to_8(pngPtr);
if (fileHasTrns)
png_set_tRNS_to_alpha(pngPtr);
if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(pngPtr);
// Ensure RGBA output. After our transforms, the output will have an alpha
// channel iff the file already had one OR we expanded tRNS.
const bool outputHasAlpha = fileHasAlpha || fileHasTrns;
if (!outputHasAlpha)
png_set_filler(pngPtr, 0xFF, PNG_FILLER_AFTER);
png_read_update_info(pngPtr, infoPtr);
// Re-query post-transform state for clarity/robustness.
const int outColorType = png_get_color_type(pngPtr, infoPtr);
const int outBitDepth = png_get_bit_depth(pngPtr, infoPtr);
if (outColorType != PNG_COLOR_TYPE_RGBA || outBitDepth != 8)
{
std::printf("decodePng: unsupported output format after transforms: colorType=%d bitDepth=%d\n",
outColorType, outBitDepth);
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
std::fclose(fp);
return std::nullopt;
}
const png_size_t rowBytes = png_get_rowbytes(pngPtr, infoPtr);
if (rowBytes != width * 4)
{
std::printf("decodePng: unexpected rowBytes=%zu width=%u\n", static_cast<size_t>(rowBytes), width);
png_destroy_read_struct(&pngPtr, &infoPtr, nullptr);
std::fclose(fp);
return std::nullopt;
}
Image out;
out.width = static_cast<int>(width);
out.height = static_cast<int>(height);
out.rgba.resize(static_cast<size_t>(out.width) * static_cast<size_t>(out.height) * 4);
std::vector<png_bytep> rows(static_cast<size_t>(out.height));
for (int y = 0; y < out.height; ++y)
{
rows[static_cast<size_t>(y)] = out.rgba.data() + (static_cast<size_t>(y) * static_cast<size_t>(out.width) * 4);
}
png_read_image(pngPtr, rows.data());
png_read_end(pngPtr, nullptr);
| auto prog = createProgramAndTexture(*image); | ||
| if (!prog) | ||
| { | ||
| ok = false; | ||
| } | ||
| else | ||
| { | ||
| gl = *prog; |
| Image out; | ||
| out.width = width; | ||
| out.height = height; | ||
| out.rgba.resize(static_cast<size_t>(width) * static_cast<size_t>(height) * 4); | ||
|
|
||
| std::vector<uint8_t> row(rowStride); |
d59d42b to
32dbef4
Compare
32dbef4 to
e8eaf1d
Compare
| if (!EssContextSetTerminateListener(ctx, nullptr, &kTermListener)) | ||
| ok = false; | ||
| if (!EssContextSetSettingsListener(ctx, &display, &kSettingsListener)) |
|
|
||
| - `0`: normal exit (dismiss file detected) | ||
| - `1`: usage, configuration, or other generic runtime failure (including invalid/unknown CLI arguments) | ||
| - `2`: unused/reserved |
mhughesacn
left a comment
There was a problem hiding this comment.
I just noticed that the 4 license-related files (LICENSE/COPYING (should be a soft link)/CONTRIBUTING.md/NOTICE are a level down - they should be in the top directory of the component. Also please check that you have addressed every comment (including correct headers on all non-trivial (>10 line) files including makefiles.)
| @@ -0,0 +1,3 @@ | |||
| #!/bin/sh | |||
| bool fileExists(const char* path) | ||
| { | ||
| struct stat st; | ||
| return (path != nullptr) && (lstat(path, &st) == 0); |
| std::printf( | ||
| "Usage: rdke_splash [--image <path.jpg|path.png>] [--dismiss-file <path>]\n" | ||
| "\n" | ||
| "Runs a fullscreen OpenGL ES splash screen until the dismiss file exists.\n" | ||
| "If --image is omitted (or decode fails), a solid-color fallback splash is shown.\n" | ||
| "Defaults: --dismiss-file /tmp/.dismissSplash\n"); | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 13 comments.
Comments suppressed due to low confidence (1)
rdke_splash_opengles/src/main.cpp:500
glCreateProgram()andglCreateShader()return 0 on failure, but only the shader path checks (if (!vs || !fs)); aglCreateProgramfailure isn't detected before subsequent gl calls are issued against the zero handle, which silently fail and ultimately surface as a confusing "program link failed" error.
GLuint prog = glCreateProgram();
glAttachShader(prog, vs);
glAttachShader(prog, fs);
glBindAttribLocation(prog, 0, "a_pos");
glBindAttribLocation(prog, 1, "a_uv");
glLinkProgram(prog);
| // A 1x1 solid texture renders identically on the full-screen quad while | ||
| // avoiding a large fallback allocation and texture upload. | ||
| image = makeSolidColorRgbaImage(1, 1, 0xFF, 0x00, 0x00, 0xFF); |
| if (bitDepth == 16) | ||
| png_set_strip_16(pngPtr); | ||
|
|
||
| if (colorType == PNG_COLOR_TYPE_PALETTE) | ||
| png_set_palette_to_rgb(pngPtr); | ||
|
|
||
| if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) | ||
| png_set_expand_gray_1_2_4_to_8(pngPtr); | ||
|
|
||
| if (fileHasTrns) | ||
| png_set_tRNS_to_alpha(pngPtr); | ||
|
|
||
| if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) | ||
| png_set_gray_to_rgb(pngPtr); | ||
|
|
||
| // Ensure RGBA output. After our transforms, the output will have an alpha | ||
| // channel iff the file already had one OR we expanded tRNS. | ||
| const bool outputHasAlpha = fileHasAlpha || fileHasTrns; | ||
| if (!outputHasAlpha) | ||
| png_set_filler(pngPtr, 0xFF, PNG_FILLER_AFTER); | ||
|
|
| EssTerminateListener kTermListener = []() { | ||
| EssTerminateListener l{}; | ||
| l.terminated = onEssosTerminated; | ||
| return l; | ||
| }(); | ||
|
|
||
| EssSettingsListener kSettingsListener = []() { | ||
| EssSettingsListener l{}; | ||
| l.displaySize = onDisplaySize; | ||
| return l; | ||
| }(); |
| ### Exit codes | ||
|
|
||
| - `0`: normal exit (dismiss file detected) | ||
| - `1`: usage, configuration, or other generic runtime failure (including invalid/unknown CLI arguments) |
| MIT License | ||
| =========== | ||
| Permission is hereby granted, free of charge, to any person obtaining a | ||
| copy of this software and associated documentation files (the "Software"), | ||
| to deal in the Software without restriction, including without limitation | ||
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||
| and/or sell copies of the Software, and to permit persons to whom the | ||
| Software is furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice (including the next | ||
| paragraph) shall be included in all copies or substantial portions of the | ||
| Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | ||
| THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
| FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||
| DEALINGS IN THE SOFTWARE. |
|
|
||
|
|
||
| MIT License | ||
| =========== | ||
| Permission is hereby granted, free of charge, to any person obtaining a | ||
| copy of this software and associated documentation files (the "Software"), | ||
| to deal in the Software without restriction, including without limitation | ||
| the rights to use, copy, modify, merge, publish, distribute, sublicense, | ||
| and/or sell copies of the Software, and to permit persons to whom the | ||
| Software is furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice (including the next | ||
| paragraph) shall be included in all copies or substantial portions of the | ||
| Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL | ||
| THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
| FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER | ||
| DEALINGS IN THE SOFTWARE. |
| char log[1024]; | ||
| GLsizei len = 0; | ||
| glGetProgramInfoLog(prog, static_cast<GLsizei>(sizeof(log)), &len, log); | ||
| std::printf("program link failed: %.*s\n", static_cast<int>(len), log); |
1b9116e to
e06405c
Compare
Reason for change: Applying copilot changes. Priority: P2 Risks: Low
e06405c to
31c58ca
Compare
No description provided.