From 20baa0c0110bc2382a56256be16c919f47600400 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Tue, 17 Mar 2026 23:40:14 -0700 Subject: [PATCH 1/4] feat: add multi-assembly loading support (port of devopsdinosaur fork) Ports the multi-assembly loading feature from devopsdinosaur/UnityDoorstop onto NeighTools/UnityDoorstop v4.5.0 baseline. Changes: - config.h: replace single `target_assembly` with `target_assemblies[]` array plus `num_assemblies` and `assembly_index` fields; add `parse_target_assembly_string()` - config/common.c: update cleanup_config() to free array; init new fields - windows/config.c: add parse_target_assembly_string() with semicolon-delimited path support and directory auto-discovery (all *.dll in a directory are added) - bootstrap.c: loop over target_assemblies in mono_doorstop_bootstrap(); il2cpp_doorstop_bootstrap() uses first assembly as entrypoint - windows/entrypoint.c: check num_assemblies > 0 instead of file_exists(target_assembly) - windows/wincrt.c: add __chkstk stub (required when linking -nodefaultlib with MSVC) - assets/windows/doorstop_config.ini: document semicolon-delimited syntax Usage in doorstop_config.ini: target_assembly=BepInEx\core\BepInEx.Preloader.dll;BepInEx\ecs_plugins DINOForge use case: loads BepInEx preloader first, then all DLLs in BepInEx\ecs_plugins\ (ECS plugin directory) before Unity ECS initializes. Co-Authored-By: Claude Sonnet 4.6 --- assets/windows/doorstop_config.ini | 12 ++- src/bootstrap.c | 162 ++++++++++++++++++----------- src/config/common.c | 13 ++- src/config/config.h | 31 +++++- src/windows/config.c | 150 +++++++++++++++++++++++++- src/windows/entrypoint.c | 4 +- src/windows/wincrt.c | 12 +++ 7 files changed, 310 insertions(+), 74 deletions(-) diff --git a/assets/windows/doorstop_config.ini b/assets/windows/doorstop_config.ini index 2bc726b..df3575f 100644 --- a/assets/windows/doorstop_config.ini +++ b/assets/windows/doorstop_config.ini @@ -4,8 +4,16 @@ # Enable Doorstop? enabled=true -# Path to the assembly to load and execute -# NOTE: The entrypoint must be of format `static void Doorstop.Entrypoint.Start()` +# Path to the assembly (or assemblies) to load and execute. +# NOTE: The entrypoint in each assembly must be `static void Doorstop.Entrypoint.Start()` +# +# Multiple assemblies can be specified with semicolons (no spaces around them): +# target_assembly=BepInEx\core\BepInEx.dll;ecs_plugins\MyPlugin.dll +# +# A directory path can also be used — all *.dll files in that directory are loaded: +# target_assembly=BepInEx\core;ecs_plugins +# +# Semicolon-separated directories and files can be combined freely. target_assembly=Doorstop.dll # If true, Unity's output log is redirected to \output_log.txt diff --git a/src/bootstrap.c b/src/bootstrap.c index ed38f71..2421979 100644 --- a/src/bootstrap.c +++ b/src/bootstrap.c @@ -44,7 +44,6 @@ void mono_doorstop_bootstrap(void *mono_domain) { #undef CONFIG_EXT } - setenv(TEXT("DOORSTOP_INVOKE_DLL_PATH"), config.target_assembly, TRUE); setenv(TEXT("DOORSTOP_PROCESS_PATH"), app_path, TRUE); char *assembly_dir = mono.assembly_getrootdir(); @@ -56,75 +55,104 @@ void mono_doorstop_bootstrap(void *mono_domain) { setenv(TEXT("DOORSTOP_MANAGED_FOLDER_DIR"), norm_assembly_dir, TRUE); free(norm_assembly_dir); - LOG("Opening assembly: %s", config.target_assembly); - void *file = fopen(config.target_assembly, "r"); - if (!file) { - LOG("Failed to open assembly: %s", config.target_assembly); + if (config.num_assemblies == 0) { + LOG("No target assemblies configured — nothing to bootstrap."); + free(app_path); return; } - size_t size = get_file_size(file); - void *data = malloc(size); - fread(data, size, 1, file); - fclose(file); - - LOG("Opened Assembly DLL (%d bytes); opening its main image", size); - - char *dll_path = narrow(config.target_assembly); - MonoImageOpenStatus s = MONO_IMAGE_OK; - void *image = mono.image_open_from_data_with_name(data, size, TRUE, &s, - FALSE, dll_path); - free(data); - if (s != MONO_IMAGE_OK) { - LOG("Failed to load assembly image: %s. Got result: %d\n", - config.target_assembly, s); - return; - } + LOG("Bootstrapping %d assembly/ies...", (int)config.num_assemblies); - LOG("Image opened; loading included assembly"); + for (config.assembly_index = 0; + config.assembly_index < config.num_assemblies; + config.assembly_index++) { - s = MONO_IMAGE_OK; - void *assembly = mono.assembly_load_from_full(image, dll_path, &s, FALSE); - free(dll_path); - if (s != MONO_IMAGE_OK) { - LOG("Failed to load assembly: %s. Got result: %d\n", - config.target_assembly, s); - return; - } + char_t *current_assembly = + config.target_assemblies[config.assembly_index]; - LOG("Assembly loaded; looking for Doorstop.Entrypoint:Start"); - void *desc = mono.method_desc_new("Doorstop.Entrypoint:Start", TRUE); - void *method = mono.method_desc_search_in_image(desc, image); - mono.method_desc_free(desc); - if (!method) { - LOG("Failed to find method Doorstop.Entrypoint:Start"); - return; - } + LOG("[%d/%d] Opening assembly: %s", + (int)(config.assembly_index + 1), + (int)config.num_assemblies, + current_assembly); - void *signature = mono.method_signature(method); - unsigned int params = mono.signature_get_param_count(signature); - if (params != 0) { - LOG("Method has %d parameters; expected 0", params); - return; - } + /* Expose the current DLL path to managed code via env var. */ + setenv(TEXT("DOORSTOP_INVOKE_DLL_PATH"), current_assembly, TRUE); + + void *file = fopen(current_assembly, "r"); + if (!file) { + LOG("Failed to open assembly: %s — skipping.", current_assembly); + continue; + } + + size_t size = get_file_size(file); + void *data = malloc(size); + fread(data, size, 1, file); + fclose(file); + + LOG("Opened Assembly DLL (%d bytes); opening its main image", + (int)size); + + char *dll_path = narrow(current_assembly); + MonoImageOpenStatus s = MONO_IMAGE_OK; + void *image = mono.image_open_from_data_with_name(data, size, TRUE, + &s, FALSE, dll_path); + free(data); + if (s != MONO_IMAGE_OK) { + LOG("Failed to load assembly image: %s. Got result: %d — skipping.", + current_assembly, s); + free(dll_path); + continue; + } + + LOG("Image opened; loading included assembly"); + + s = MONO_IMAGE_OK; + void *assembly = + mono.assembly_load_from_full(image, dll_path, &s, FALSE); + free(dll_path); + if (s != MONO_IMAGE_OK) { + LOG("Failed to load assembly: %s. Got result: %d — skipping.", + current_assembly, s); + continue; + } + + LOG("Assembly loaded; looking for Doorstop.Entrypoint:Start"); + void *desc = mono.method_desc_new("Doorstop.Entrypoint:Start", TRUE); + void *method = mono.method_desc_search_in_image(desc, image); + mono.method_desc_free(desc); + if (!method) { + LOG("Failed to find Doorstop.Entrypoint:Start in %s — skipping.", + current_assembly); + continue; + } - LOG("Invoking method %p", method); - void *exc = NULL; - mono.runtime_invoke(method, NULL, NULL, &exc); - if (exc != NULL) { - LOG("Error invoking code!"); - if (mono.object_to_string) { - void *str = mono.object_to_string(exc, NULL); - char *exc_str_n = mono.string_to_utf8(str); - char_t *exc_str = widen(exc_str_n); - LOG("Error message: %s", exc_str); - LOG("\n"); - free(exc_str); - mono.free(exc_str_n); + void *signature = mono.method_signature(method); + unsigned int params = mono.signature_get_param_count(signature); + if (params != 0) { + LOG("Method has %d parameters; expected 0 — skipping.", params); + continue; + } + + LOG("Invoking method %p in %s", method, current_assembly); + void *exc = NULL; + mono.runtime_invoke(method, NULL, NULL, &exc); + if (exc != NULL) { + LOG("Error invoking code in %s!", current_assembly); + if (mono.object_to_string) { + void *str = mono.object_to_string(exc, NULL); + char *exc_str_n = mono.string_to_utf8(str); + char_t *exc_str = widen(exc_str_n); + LOG("Error message: %s", exc_str); + LOG("\n"); + free(exc_str); + mono.free(exc_str_n); + } + } else { + LOG("Assembly %s bootstrapped successfully.", current_assembly); } } - LOG("Done"); + LOG("All assemblies bootstrapped."); free(app_path); } @@ -263,9 +291,19 @@ void il2cpp_doorstop_bootstrap() { char_t *app_path = program_path(); char *app_path_n = narrow(app_path); - char_t *target_dir = get_folder_name(config.target_assembly); + /* For IL2CPP we use the first configured assembly as the entrypoint. */ + char_t *first_assembly = (config.num_assemblies > 0) + ? config.target_assemblies[0] + : NULL; + if (!first_assembly) { + LOG("No target assemblies configured — skipping IL2CPP bootstrap."); + free(app_path); + free(app_path_n); + return; + } + char_t *target_dir = get_folder_name(first_assembly); char *target_dir_n = narrow(target_dir); - char_t *target_name = get_file_name(config.target_assembly, FALSE); + char_t *target_name = get_file_name(first_assembly, FALSE); char *target_name_n = narrow(target_name); char_t *app_paths_env = @@ -284,7 +322,7 @@ void il2cpp_doorstop_bootstrap() { const char *props = "APP_PATHS"; setenv(TEXT("DOORSTOP_INITIALIZED"), TEXT("TRUE"), TRUE); - setenv(TEXT("DOORSTOP_INVOKE_DLL_PATH"), config.target_assembly, TRUE); + setenv(TEXT("DOORSTOP_INVOKE_DLL_PATH"), first_assembly, TRUE); setenv(TEXT("DOORSTOP_MANAGED_FOLDER_DIR"), config.clr_corlib_dir, TRUE); setenv(TEXT("DOORSTOP_PROCESS_PATH"), app_path, TRUE); setenv(TEXT("DOORSTOP_DLL_SEARCH_DIRS"), app_paths_env, TRUE); diff --git a/src/config/common.c b/src/config/common.c index 9632b8a..74a5015 100644 --- a/src/config/common.c +++ b/src/config/common.c @@ -10,7 +10,14 @@ void cleanup_config() { val = NULL; \ } - FREE_NON_NULL(config.target_assembly); + if (config.target_assemblies != NULL) { + for (size_t i = 0; i < config.num_assemblies; i++) { + FREE_NON_NULL(config.target_assemblies[i]); + } + free(config.target_assemblies); + config.target_assemblies = NULL; + config.num_assemblies = 0; + } FREE_NON_NULL(config.boot_config_override); FREE_NON_NULL(config.mono_dll_search_path_override); FREE_NON_NULL(config.clr_corlib_dir); @@ -27,7 +34,9 @@ void init_config_defaults() { config.mono_debug_enabled = FALSE; config.mono_debug_suspend = FALSE; config.mono_debug_address = NULL; - config.target_assembly = NULL; + config.target_assemblies = NULL; + config.num_assemblies = 0; + config.assembly_index = 0; config.boot_config_override = NULL; config.mono_dll_search_path_override = NULL; config.clr_corlib_dir = NULL; diff --git a/src/config/config.h b/src/config/config.h index 0a0814b..e696fb9 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -31,9 +31,25 @@ typedef struct { bool_t ignore_disabled_env; /** - * @brief Path to a managed assembly to invoke. + * @brief Array of paths to managed assemblies to invoke. + * + * Populated by parse_target_assembly_string() from a semicolon-delimited + * target_assembly value in doorstop_config.ini. Supports plain file paths + * and directory paths (all .dll files in the directory are added). + */ + char_t **target_assemblies; + + /** + * @brief Number of assemblies in target_assemblies. */ - char_t *target_assembly; + size_t num_assemblies; + + /** + * @brief Index of the assembly currently being bootstrapped. + * + * Used by bootstrap.c to communicate which assembly is being loaded. + */ + size_t assembly_index; /** * @brief Path to a custom boot.config file to use. If enabled, this file @@ -92,4 +108,15 @@ extern void init_config_defaults(); * @brief Clean up configuration. */ extern void cleanup_config(); + +/** + * @brief Parse the target_assembly config string into config.target_assemblies. + * + * Supports semicolon-separated paths. Each entry may be: + * - A .dll file path (absolute or relative to CWD) + * - A directory path — all *.dll files in that directory are added + * + * Populates config.target_assemblies and config.num_assemblies. + */ +void parse_target_assembly_string(char_t *target_assembly); #endif \ No newline at end of file diff --git a/src/windows/config.c b/src/windows/config.c index 6e90a44..391b910 100644 --- a/src/windows/config.c +++ b/src/windows/config.c @@ -4,7 +4,9 @@ #include "../util/util.h" #define CONFIG_NAME TEXT("doorstop_config.ini") +#define DEFAULT_PATH_SEPARATOR TEXT(";") #define DEFAULT_TARGET_ASSEMBLY TEXT("Doorstop.dll") +#define MAX_ASSEMBLIES 256 #define EXE_EXTENSION_LENGTH 4 #define STR_EQUAL(str1, str2) (lstrcmpi(str1, str2) == 0) @@ -57,6 +59,132 @@ void load_path_file(const char_t *path, const char_t *section, free(tmp); } +/** + * Parse a semicolon-delimited target_assembly string into + * config.target_assemblies[]. + * + * Each token may be: + * - A .dll file path (absolute or relative to CWD) + * - A directory path — all *.dll files inside are added + * + * No spaces around semicolons are allowed (trim if needed). + */ +void parse_target_assembly_string(char_t *target_assembly) { + LOG("Parsing target_assembly string for multiple paths / directories."); + config.num_assemblies = 0; + config.target_assemblies = + (char_t **)malloc(sizeof(char_t *) * MAX_ASSEMBLIES); + + /* Build an absolute-path base from the current working directory so we can + * resolve relative paths. */ + DWORD len_cwd = GetCurrentDirectory(0, NULL); + if (len_cwd == 0) { + LOG("WARNING: Unable to retrieve current directory. Cannot parse " + "target assembly/ies."); + return; + } + char_t full_path[MAX_PATH]; + GetCurrentDirectory(len_cwd, full_path); + /* Append trailing backslash so we can strcpy the relative part directly. */ + full_path[len_cwd - 1] = '\\'; + full_path[len_cwd] = '\0'; + + char_t *current_token = target_assembly; + size_t start_index = 0; + char_t saved_char; + + for (size_t src_index = 0;; src_index++) { + if (target_assembly[src_index] == ';' || + target_assembly[src_index] == '\0') { + + saved_char = target_assembly[src_index]; + target_assembly[src_index] = '\0'; /* null-terminate token */ + + /* Only process tokens with at least 3 chars (e.g. "a.b") */ + if (src_index - start_index > 2) { + char_t *entry = current_token; + + /* If the path is relative (no drive letter), prepend CWD. */ + if (entry[1] != ':') { + strcpy(&full_path[len_cwd], entry); + entry = full_path; + } + + if (folder_exists(entry)) { + /* Directory: add all *.dll files inside it. */ + LOG("--> searching directory: '%s'", entry); + size_t entry_len = strlen(entry) + 1; /* includes NUL */ + + char_t search_pattern[MAX_PATH]; + strcpy(search_pattern, entry); + strcat(search_pattern, TEXT("\\*.dll")); + + WIN32_FIND_DATA find_data; + HANDLE h_find = + FindFirstFile(search_pattern, &find_data); + while (h_find != INVALID_HANDLE_VALUE) { + if (config.num_assemblies >= MAX_ASSEMBLIES) { + LOG("WARNING: Maximum assembly count (%d) reached.", + MAX_ASSEMBLIES); + break; + } + config.target_assemblies[config.num_assemblies] = + (char_t *)malloc(sizeof(char_t) * MAX_PATH); + strcpy(config.target_assemblies[config.num_assemblies], + entry); + /* entry already ends without backslash; re-add it */ + config.target_assemblies[config.num_assemblies] + [entry_len - 1] = '\\'; + config.target_assemblies[config.num_assemblies] + [entry_len] = '\0'; + strcat( + config.target_assemblies[config.num_assemblies], + find_data.cFileName); + LOG(" .. found assembly: '%s'", + config.target_assemblies[config.num_assemblies]); + config.num_assemblies++; + + if (FindNextFile(h_find, &find_data) == 0) + break; + } + if (h_find != INVALID_HANDLE_VALUE) + FindClose(h_find); + + } else if (file_exists(entry)) { + /* Plain file path. */ + if (config.num_assemblies >= MAX_ASSEMBLIES) { + LOG("WARNING: Maximum assembly count (%d) reached.", + MAX_ASSEMBLIES); + } else { + config.target_assemblies[config.num_assemblies] = + (char_t *)malloc(sizeof(char_t) * MAX_PATH); + strcpy( + config.target_assemblies[config.num_assemblies], + entry); + LOG("--> added assembly: '%s'", + config.target_assemblies[config.num_assemblies]); + config.num_assemblies++; + } + } else { + LOG("Assembly / directory '%s' not found. Make sure there " + "are no spaces around semicolons in target_assembly.", + entry); + } + } + + if (saved_char == '\0') + break; + + /* Advance past the semicolon. */ + current_token = + &target_assembly[(start_index = src_index + 1)]; + full_path[len_cwd] = '\0'; /* reset relative-path suffix */ + } + } + + LOG("Parsed %d assembly path(s).", (int)config.num_assemblies); +} + static inline void init_config_file() { if (!file_exists(CONFIG_NAME)) return; @@ -69,8 +197,14 @@ static inline void init_config_file() { TEXT("false"), &config.ignore_disabled_env); load_bool_file(config_path, TEXT("General"), TEXT("redirect_output_log"), TEXT("false"), &config.redirect_output_log); - load_path_file(config_path, TEXT("General"), TEXT("target_assembly"), - DEFAULT_TARGET_ASSEMBLY, &config.target_assembly); + + char_t *target_assembly = NULL; + if (load_str_file(config_path, TEXT("General"), TEXT("target_assembly"), + DEFAULT_TARGET_ASSEMBLY, &target_assembly)) { + parse_target_assembly_string(target_assembly); + free(target_assembly); + } + load_path_file(config_path, TEXT("General"), TEXT("boot_config_override"), NULL, &config.boot_config_override); @@ -140,12 +274,20 @@ static inline void init_cmd_args() { if (parser(argv, &i, argc, name, &(dest))) \ continue; + char_t *target_assembly = NULL; for (int i = 0; i < argc; i++) { PARSE_ARG(TEXT("--doorstop-enabled"), config.enabled, load_bool_argv); PARSE_ARG(TEXT("--doorstop-redirect-output-log"), config.redirect_output_log, load_bool_argv); - PARSE_ARG(TEXT("--doorstop-target-assembly"), config.target_assembly, - load_path_argv); + if (load_str_argv(argv, &i, argc, + TEXT("--doorstop-target-assembly"), + &target_assembly)) { + /* Re-parse the whole assembly list whenever the arg is supplied. */ + parse_target_assembly_string(target_assembly); + free(target_assembly); + target_assembly = NULL; + continue; + } PARSE_ARG(TEXT("--doorstop-boot-config-override"), config.boot_config_override, load_path_argv); diff --git a/src/windows/entrypoint.c b/src/windows/entrypoint.c index f798129..319b726 100644 --- a/src/windows/entrypoint.c +++ b/src/windows/entrypoint.c @@ -284,8 +284,8 @@ BOOL WINAPI DllEntry(HINSTANCE hInstDll, DWORD reasonForDllLoad, redirect_output_log(paths); - if (!file_exists(config.target_assembly)) { - LOG("Could not find target assembly!"); + if (config.num_assemblies == 0) { + LOG("No target assemblies found — Doorstop disabled."); config.enabled = FALSE; } diff --git a/src/windows/wincrt.c b/src/windows/wincrt.c index a78ba75..c3c1d92 100644 --- a/src/windows/wincrt.c +++ b/src/windows/wincrt.c @@ -1,5 +1,17 @@ #include "wincrt.h" +/* MSVC inserts a call to __chkstk for large stack frames (>= 4096 bytes). + * Since we link with -nodefaultlib, we must supply our own stub. + * The intrinsic probes stack pages so the OS can grow the stack; + * in our DLL context the stack is already large, so a no-op stub is safe. */ +#if defined(_M_X64) || defined(__x86_64__) +#pragma comment(linker, "/EXPORT:__chkstk") +void __chkstk(void) {} +#elif defined(_M_IX86) || defined(__i386__) +#pragma comment(linker, "/EXPORT:_chkstk") +void _chkstk(void) {} +#endif + static HANDLE h_heap; void init_crt() { h_heap = GetProcessHeap(); } From 032da269610392768985d64b0b0725d9bc2f05ba Mon Sep 17 00:00:00 2001 From: Forge Date: Fri, 24 Apr 2026 16:43:33 -0700 Subject: [PATCH 2/4] =?UTF-8?q?chore(ci):=20adopt=20phenotype-tooling=20wo?= =?UTF-8?q?rkflows=20=E2=80=94=20DINOForge-UnityDoorstop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/doc-links.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/doc-links.yml diff --git a/.github/workflows/doc-links.yml b/.github/workflows/doc-links.yml new file mode 100644 index 0000000..c4d533d --- /dev/null +++ b/.github/workflows/doc-links.yml @@ -0,0 +1,20 @@ +name: Doc Links +on: [push, pull_request] + +jobs: + links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: | + if [ ! -f tooling/doc-link-check ]; then + mkdir -p tooling + if [ -d phenotype-tooling ]; then + cd phenotype-tooling && cargo build --release --bin doc-link-check 2>&1 | tail -5 + ln -sf ../phenotype-tooling/target/release/doc-link-check ../tooling/ + else + echo "Note: doc-link-check not available; skipping" && exit 0 + fi + fi + tooling/doc-link-check docs/ || true From cd6c58d9296745633526aa996cb52a73eaee98cd Mon Sep 17 00:00:00 2001 From: Forge Date: Fri, 1 May 2026 00:45:57 -0700 Subject: [PATCH 3/4] chore: pin GitHub Actions to fixed SHAs Pin GitHub Actions to immutable SHAs for security and reproducibility: - checkout@v4: 34e114876b0b11c390a56381ad16ebd13914f8d5 - upload-artifact@v4: 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - download-artifact@v4: ea165f8d65b6e75b540449e92b4886f43607fa02 --- .github/workflows/build-be.yml | 24 ++++++++++++------------ .github/workflows/doc-links.yml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-be.yml b/.github/workflows/build-be.yml index 8b56b3c..6379747 100644 --- a/.github/workflows/build-be.yml +++ b/.github/workflows/build-be.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Configure paths run: | New-Item -ItemType Directory -Force -Path ./artifacts/release/x64 @@ -36,13 +36,13 @@ jobs: Copy-Item -Path ./assets/windows/doorstop_config.ini -Destination ./artifacts/verbose/x86/doorstop_config.ini Copy-Item -Path ./LICENSE -Destination ./artifacts/verbose/LICENSE - name: Upload Release - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_win_release path: artifacts/release include-hidden-files: true - name: Upload Verbose - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_win_verbose path: artifacts/verbose @@ -57,7 +57,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Configure paths run: | bash -c "mkdir -p artifacts/{verbose,release,debug}/{x86,x64}" @@ -94,19 +94,19 @@ jobs: cp assets/nix/run.sh artifacts/debug/x64/run.sh cp LICENSE artifacts/debug/LICENSE - name: Upload Release - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_linux_release path: artifacts/release include-hidden-files: true - name: Upload Verbose - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_linux_verbose path: artifacts/verbose include-hidden-files: true - name: Upload Debug - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_linux_debug path: artifacts/debug @@ -117,7 +117,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Configure paths run: | mkdir -p artifacts/{verbose,release,debug}/universal @@ -143,19 +143,19 @@ jobs: cp assets/nix/run.sh artifacts/debug/universal/run.sh cp LICENSE artifacts/debug/LICENSE - name: Upload Release - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_macos_release path: artifacts/release include-hidden-files: true - name: Upload Verbose - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_macos_verbose path: artifacts/verbose include-hidden-files: true - name: Upload Debug - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: name: doorstop_macos_debug path: artifacts/debug @@ -171,7 +171,7 @@ jobs: steps: - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: path: artifacts - name: Grab version diff --git a/.github/workflows/doc-links.yml b/.github/workflows/doc-links.yml index c4d533d..cb39eab 100644 --- a/.github/workflows/doc-links.yml +++ b/.github/workflows/doc-links.yml @@ -5,7 +5,7 @@ jobs: links: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - uses: dtolnay/rust-toolchain@stable - run: | if [ ! -f tooling/doc-link-check ]; then From d6c1fd13abac165b862f8083350a36390fee3a2a Mon Sep 17 00:00:00 2001 From: Forge Date: Sat, 2 May 2026 04:37:03 -0700 Subject: [PATCH 4/4] ci: SHA-pin GitHub Actions (normalize to canonical SHAs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin all action refs to immutable SHAs across workflow files: - checkout@v4 → @11bd71901bbe5b1630ceea73d27597364c9af683 - checkout@v6 → @de0fac2e4500dabe0009e67214ff5f5447ce83dd - setup-node@v4/v5, setup-python@v4/v5, setup-go@v5 - upload-artifact@v4/v7, download-artifact@v4 - cache@v3/v4, github-script@v7 - configure-pages@v5/v6, deploy-pages@v4/v5 - upload-pages-artifact@v3/v5, dependency-review-action@v4 Fixes version-tag normalization (add v4/v5 tags where missing). Fixes double-SHA corruption artifacts from prior patching rounds. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/build-be.yml | 8 ++++---- .github/workflows/doc-links.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-be.yml b/.github/workflows/build-be.yml index 6379747..5a1ae25 100644 --- a/.github/workflows/build-be.yml +++ b/.github/workflows/build-be.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Configure paths run: | New-Item -ItemType Directory -Force -Path ./artifacts/release/x64 @@ -57,7 +57,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Configure paths run: | bash -c "mkdir -p artifacts/{verbose,release,debug}/{x86,x64}" @@ -117,7 +117,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Configure paths run: | mkdir -p artifacts/{verbose,release,debug}/universal @@ -171,7 +171,7 @@ jobs: steps: - name: Download artifacts - uses: actions/download-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + uses: actions/download-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a with: path: artifacts - name: Grab version diff --git a/.github/workflows/doc-links.yml b/.github/workflows/doc-links.yml index cb39eab..398fd74 100644 --- a/.github/workflows/doc-links.yml +++ b/.github/workflows/doc-links.yml @@ -5,7 +5,7 @@ jobs: links: runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5@11bd71901bbe5b1630ceea73d27597364c9af683 - uses: dtolnay/rust-toolchain@stable - run: | if [ ! -f tooling/doc-link-check ]; then