From 5ac7077dd031108a9a133bba8ff44be7cef9140c Mon Sep 17 00:00:00 2001 From: catbot Date: Wed, 27 May 2026 01:55:29 +0000 Subject: [PATCH 1/2] fix external cache race on concurrent builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two crafter-build invocations resolving to the same external dep both cloned into /-.tmp, corrupting each other's pack tempfiles. Use a random per-invocation tmp suffix and treat a failed rename whose destination now exists as the loser-of-the-race case — discard the local clone and reuse the winner's. --- implementations/Crafter.Build-External.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/implementations/Crafter.Build-External.cpp b/implementations/Crafter.Build-External.cpp index e9d1689..fc6c1ec 100644 --- a/implementations/Crafter.Build-External.cpp +++ b/implementations/Crafter.Build-External.cpp @@ -103,9 +103,13 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) { } } + // Random per-invocation tmp suffix so concurrent crafter-build processes + // resolving to the same cache key don't share an in-flight `.tmp` + // directory — git's pack tempfiles would get yanked when one process + // removes or renames the dir out from under the other. + std::random_device rd; fs::path tmpDir = cloneDir; - tmpDir += ".tmp"; - if (fs::exists(tmpDir)) fs::remove_all(tmpDir); + tmpDir += std::format(".tmp.{:08x}{:08x}", rd(), rd()); if (auto err = runGit(std::format("git clone --recursive {} {}", ShellQuote(source.url), ShellQuote(tmpDir.string())))) { if (fs::exists(tmpDir)) fs::remove_all(tmpDir); @@ -128,6 +132,10 @@ std::string FetchGit(const GitSource& source, const fs::path& cloneDir) { fs::rename(tmpDir, cloneDir, ec); if (ec) { fs::remove_all(tmpDir); + // If a concurrent build won the race, cloneDir now exists with the + // same source revision (cache key includes url+branch+commit), so + // discard our clone and reuse theirs. + if (fs::exists(cloneDir)) return ""; return std::format("rename {} -> {} failed: {}", tmpDir.string(), cloneDir.string(), ec.message()); } return ""; From 779de55e588b10488217c6f79c308f6f8f45ff1c Mon Sep 17 00:00:00 2001 From: catbot Date: Wed, 27 May 2026 02:05:39 +0000 Subject: [PATCH 2/2] ci: skip variant builds and artifact uploads on pull requests PRs now stop after bootstrap + tests. The per-march variant builds, packaging, and upload-artifact steps reuse the same guard the rolling 'latest' tag/release steps already had, so artifacts are only produced on push to master (or manual workflow_dispatch). --- .forgejo/workflows/ci.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.forgejo/workflows/ci.yaml b/.forgejo/workflows/ci.yaml index feb74e6..fa695af 100644 --- a/.forgejo/workflows/ci.yaml +++ b/.forgejo/workflows/ci.yaml @@ -66,25 +66,36 @@ jobs: - name: Run tests run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build test + # Everything below produces release artifacts and only runs on push to + # master. PRs stop after the test step above — the bootstrap binary is + # enough to validate the change; the per-march variant builds and the + # archive uploads are wasted work on a PR. - name: Build Linux x86-64-v2 (matches bootstrap) + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build - name: Build Linux x86-64-v3 + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v3 ./bin/crafter-build - name: Build Linux x86-64-v4 + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v4 ./bin/crafter-build - name: Build Windows x86-64-v2 (mingw) + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build ./bin/crafter-build --target=x86_64-w64-mingw32 - name: Build Windows x86-64-v3 (mingw) + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v3 ./bin/crafter-build --target=x86_64-w64-mingw32 - name: Build Windows x86-64-v4 (mingw) + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' run: CRAFTER_BUILD_HOME=$PWD/share/crafter-build CRAFTER_BUILD_MARCH=x86-64-v4 ./bin/crafter-build --target=x86_64-w64-mingw32 - name: Package artifacts + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' run: | set -eux mkdir -p dist @@ -120,21 +131,27 @@ jobs: # so users browsing the PR / run page get six small, individually-named # downloads instead of one wrapper zip containing the lot. - name: Upload Linux v2 artifact + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' uses: actions/upload-artifact@v3 with: { name: crafter-build-linux-x86_64-v2, path: dist/crafter-build-linux-x86_64-v2.tar.gz, if-no-files-found: error } - name: Upload Linux v3 artifact + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' uses: actions/upload-artifact@v3 with: { name: crafter-build-linux-x86_64-v3, path: dist/crafter-build-linux-x86_64-v3.tar.gz, if-no-files-found: error } - name: Upload Linux v4 artifact + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' uses: actions/upload-artifact@v3 with: { name: crafter-build-linux-x86_64-v4, path: dist/crafter-build-linux-x86_64-v4.tar.gz, if-no-files-found: error } - name: Upload Windows v2 artifact + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' uses: actions/upload-artifact@v3 with: { name: crafter-build-windows-x86_64-v2, path: dist/crafter-build-windows-x86_64-v2.zip, if-no-files-found: error } - name: Upload Windows v3 artifact + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' uses: actions/upload-artifact@v3 with: { name: crafter-build-windows-x86_64-v3, path: dist/crafter-build-windows-x86_64-v3.zip, if-no-files-found: error } - name: Upload Windows v4 artifact + if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/master' uses: actions/upload-artifact@v3 with: { name: crafter-build-windows-x86_64-v4, path: dist/crafter-build-windows-x86_64-v4.zip, if-no-files-found: error }