Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,21 +276,22 @@ also verified. Host builds that go through the common `make` / `cmake` /
before the build tool runs. The fingerprint includes loaded modules, selected
build environment variables, tool real paths, versions, and binary SHA-256
hashes. The app-owned build recipe portion of the build input hash is limited
to `programs/<code>/build.sh` and optional patch files under
`programs/<code>/patches/`; `run.sh`, `profile.sh`, `estimate.sh`, and app
documentation are not build cache inputs. Because of that boundary,
`profile.sh` and `estimate.sh` must not select build options or rebuild app
artifacts. Cache misses fall back to the normal `programs/<code>/build.sh` path
and store a fresh cache after a successful build. The generated child pipeline
to Git-tracked files under `programs/<code>/`. This is intentionally
conservative: a changed app-side file may cause an extra rebuild, but it should
not accidentally restore an old binary after app-local source or build recipe
changes. `profile.sh` and `estimate.sh` still must not select build options or
rebuild app artifacts. Cache misses fall back to the normal
`programs/<code>/build.sh` path and store a fresh cache after a successful build.
The generated child pipeline
does not declare a GitLab `cache:` stanza; the cache directory must be a
site-managed persistent path such as the custom runner's `CUSTOM_DIR`, not a
per-job cleanup directory.

Git source は記録済みの `ref_name` と `resolved_commit` を使って `git ls-remote` で再確認し、file/archive source は SHA-256 を再計算します。
container image hash が記録されている場合は image hash も確認します。
common の `make` / `cmake` / `ninja` wrapper を通る host build は、build tool 実行直前に収集した build environment fingerprint で照合します。この fingerprint には loaded modules、選択された build 環境変数、tool の real path、version、binary SHA-256 hash が含まれます。
build input hash の app 側 build recipe は `programs/<code>/build.sh` と、任意のpatch file置き場である `programs/<code>/patches/` に限定します。`run.sh`、`profile.sh`、`estimate.sh`、app 文書は build cache input ではありません。
この境界を守るため、`profile.sh` と `estimate.sh` では build option の選択や app artifact の再buildを行わないでください。
build input hash の app 側 build recipe は `programs/<code>/` 配下の Git 管理ファイルを保守的に対象にします。app 側ファイルの変更で余分な rebuild が起きることはありますが、app-local source や build recipe の変更後に古い binary を誤って restore しないことを優先します。
`profile.sh` と `estimate.sh` では build option の選択や app artifact の再buildを行わないでください。
cache miss の場合は通常の `programs/<code>/build.sh` 経路に戻り、成功後に新しい cache を保存します。
生成された child pipeline は GitLab の `cache:` stanza を出しません。cache directory は job ごとの cleanup 対象ではなく、custom runner の `CUSTOM_DIR` など site 側で管理する永続パスにしてください。

Expand Down
31 changes: 25 additions & 6 deletions result_server/tests/test_result_detail_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ def test_meta_info_section(self, app):
assert "Cached Binary Created At" in html
assert "2026-09-04T10:20:30Z" in html
assert "Host Environment Fingerprint" in html
assert "host build environment is the same" in html
assert "build recipe inputs are the same" in html
assert "source metadata is the same" in html
assert "Matched current host build environment" in html
assert "Git-tracked files under programs/&lt;code&gt;/" in html
assert "Matched current source metadata" in html
assert "Cached Artifacts Digest" in html
assert "restored build outputs are the same" in html
assert "Matched restored build outputs" in html
assert "Hit Basis" in html
assert "build inputs hash matched" in html
assert "rccs-cloud" in html
Expand Down Expand Up @@ -408,15 +408,25 @@ def test_build_cache_miss_shows_rejected_reason(self, app):
"reason": "stored cache after build",
"entry": {
"created_at": "2026-09-05T01:02:03Z",
"digests": {"build_inputs": "sha256:new"},
"host_environment_fingerprint": "sha256:host-new",
"digests": {
"build_inputs": "sha256:new",
"source_info": "sha256:source-new",
"artifacts": "sha256:artifacts-new",
},
},
"store_basis": ["build inputs hash recorded"],
"restore": {
"status": "miss",
"reason": "build inputs changed: cached old, current new",
"rejected_entry": {
"created_at": "2026-09-04T01:02:03Z",
"digests": {"build_inputs": "sha256:old"},
"host_environment_fingerprint": "sha256:host-old",
"digests": {
"build_inputs": "sha256:old",
"source_info": "sha256:source-old",
"artifacts": "sha256:artifacts-old",
},
},
},
},
Expand All @@ -431,6 +441,15 @@ def test_build_cache_miss_shows_rejected_reason(self, app):
assert "build inputs changed: cached old, current new" in html
assert "Rejected Cached Binary Created At" in html
assert "2026-09-04T01:02:03Z" in html
assert "Recorded the host build environment" in html
assert "Recorded build recipe inputs" in html
assert "Recorded source_info.env" in html
assert "Recorded cached artifacts" in html
assert "Rejected Host Environment Fingerprint" in html
assert "Rejected Build Inputs Hash" in html
assert "Rejected candidate build inputs" in html
assert "Rejected candidate source metadata" in html
assert "Rejected candidate artifact digest" in html

def test_quality_section(self, app):
with app.test_request_context():
Expand Down
24 changes: 18 additions & 6 deletions result_server/utils/evidence_packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from urllib.parse import urlsplit

from utils.result_detail_view import (
BUILD_CACHE_DIGEST_HELP,
HOST_ENVIRONMENT_FINGERPRINT_HELP,
build_cache_digest_help,
build_cache_host_environment_help,
)
from utils.result_records import format_numeric_value, summarize_input_info
from utils.trigger_display import summarize_execution_trigger
Expand Down Expand Up @@ -324,14 +324,17 @@ def _build_cache_rows(build_cache: Any) -> list[tuple[str, Any]]:
entry = build_cache.get("entry")
entry = entry if isinstance(entry, dict) else {}
if entry:
context = (
"matched" if str(build_cache.get("status") or "") == "hit" else "recorded"
)
rows.extend([
("Cached binary created at", entry.get("created_at")),
("Source", _format_cache_source(entry.get("source"))),
(
"Host environment fingerprint",
_digest_with_help(
entry.get("host_environment_fingerprint"),
HOST_ENVIRONMENT_FINGERPRINT_HELP,
build_cache_host_environment_help(context),
),
),
])
Expand All @@ -340,15 +343,24 @@ def _build_cache_rows(build_cache: Any) -> list[tuple[str, Any]]:
rows.extend([
(
"Build inputs hash",
_digest_with_help(digests.get("build_inputs"), BUILD_CACHE_DIGEST_HELP["build_inputs"]),
_digest_with_help(
digests.get("build_inputs"),
build_cache_digest_help("build_inputs", context),
),
),
(
"Source info digest",
_digest_with_help(digests.get("source_info"), BUILD_CACHE_DIGEST_HELP["source_info"]),
_digest_with_help(
digests.get("source_info"),
build_cache_digest_help("source_info", context),
),
),
(
"Cached artifacts digest",
_digest_with_help(digests.get("artifacts"), BUILD_CACHE_DIGEST_HELP["artifacts"]),
_digest_with_help(
digests.get("artifacts"),
build_cache_digest_help("artifacts", context),
),
),
])
if build_cache.get("hit_basis"):
Expand Down
107 changes: 84 additions & 23 deletions result_server/utils/result_detail_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,91 @@
from utils.trigger_display import summarize_execution_trigger


HOST_ENVIRONMENT_FINGERPRINT_HELP = (
"Checks whether the host build environment is the same. Covers code, "
"system, loaded modules, selected build environment, and build tool real "
"paths, versions, and binary hashes."
HOST_ENVIRONMENT_FINGERPRINT_COVERAGE = (
"Covers code, system, loaded modules, selected build environment, and build "
"tool real paths, versions, and binary hashes."
)

BUILD_CACHE_DIGEST_HELP = {
"build_inputs": (
"Checks whether the build recipe inputs are the same. Covers app "
"build.sh, app patches, build-cache wrapper, build-tool wrappers, "
"environment snapshot helper, matrix generator, and any declared extra "
"cache inputs."
HOST_ENVIRONMENT_FINGERPRINT_HELP = {
"matched": (
"Matched current host build environment against the restored cache "
f"entry. {HOST_ENVIRONMENT_FINGERPRINT_COVERAGE}"
),
"source_info": (
"Checks whether the source metadata is the same. Uses source_info.env "
"saved with the cache entry, including source type and resolved source "
"identity."
"recorded": (
"Recorded the host build environment for the newly stored cache entry. "
f"{HOST_ENVIRONMENT_FINGERPRINT_COVERAGE}"
),
"artifacts": (
"Checks whether the restored build outputs are the same as the saved "
"cache entry. Covers artifacts/ relative paths, entry types, file "
"modes, file contents, and symlink targets."
"rejected": (
"Recorded host build environment from a rejected cache candidate, shown "
f"for diagnosis. {HOST_ENVIRONMENT_FINGERPRINT_COVERAGE}"
),
}

BUILD_CACHE_BUILD_INPUTS_COVERAGE = (
"Covers Git-tracked files under programs/<code>/, the build-cache wrapper, "
"build-tool wrappers, environment snapshot helper, matrix generator, and "
"any site-provided extra cache inputs."
)

BUILD_CACHE_DIGEST_HELP = {
"matched": {
"build_inputs": (
"Matched current build recipe inputs against the restored cache "
f"entry. {BUILD_CACHE_BUILD_INPUTS_COVERAGE}"
),
"source_info": (
"Matched current source metadata against source_info.env saved with "
"the cache entry, including source type and resolved source identity."
),
"artifacts": (
"Matched restored build outputs against the saved cache entry before "
"and after restore. Covers artifacts/ relative paths, entry types, "
"file modes, file contents, and symlink targets."
),
},
"recorded": {
"build_inputs": (
"Recorded build recipe inputs for the newly stored cache entry. "
f"{BUILD_CACHE_BUILD_INPUTS_COVERAGE}"
),
"source_info": (
"Recorded source_info.env for the newly stored cache entry, including "
"source type and resolved source identity."
),
"artifacts": (
"Recorded cached artifacts for the newly stored cache entry. Covers "
"artifacts/ relative paths, entry types, file modes, file contents, "
"and symlink targets."
),
},
"rejected": {
"build_inputs": (
"Rejected candidate build inputs. The candidate was not restored; "
"compare this value with the rejection reason."
),
"source_info": (
"Rejected candidate source metadata. The candidate was not restored; "
"compare this value with the rejection reason."
),
"artifacts": (
"Rejected candidate artifact digest. The candidate was not restored; "
"this value describes the skipped cache entry."
),
},
}


def build_cache_host_environment_help(context="matched"):
return HOST_ENVIRONMENT_FINGERPRINT_HELP.get(
context, HOST_ENVIRONMENT_FINGERPRINT_HELP["matched"]
)


def build_cache_digest_help(key, context="matched"):
return BUILD_CACHE_DIGEST_HELP.get(context, BUILD_CACHE_DIGEST_HELP["matched"]).get(
key, ""
)


def build_result_detail_context(
result,
Expand Down Expand Up @@ -335,9 +395,10 @@ def _build_build_cache_rows(build_cache):
if reason:
rows.append({"label": "Reason", "value": reason})

entry_context = "matched" if status == "hit" else "recorded"
entry = build_cache.get("entry")
entry = entry if isinstance(entry, dict) else {}
_append_cache_entry_rows(rows, entry, prefix="")
_append_cache_entry_rows(rows, entry, prefix="", context=entry_context)

if status == "hit":
hit_basis = build_cache.get("hit_basis") or []
Expand All @@ -355,12 +416,12 @@ def _build_build_cache_rows(build_cache):
rows.append({"label": "Rejected Cache Reason", "value": restore_reason})
rejected_entry = restore.get("rejected_entry")
rejected_entry = rejected_entry if isinstance(rejected_entry, dict) else {}
_append_cache_entry_rows(rows, rejected_entry, prefix="Rejected ")
_append_cache_entry_rows(rows, rejected_entry, prefix="Rejected ", context="rejected")

return rows


def _append_cache_entry_rows(rows, entry, *, prefix):
def _append_cache_entry_rows(rows, entry, *, prefix, context):
if not entry:
return

Expand All @@ -383,7 +444,7 @@ def _append_cache_entry_rows(rows, entry, *, prefix):
rows.append({
"label": f"{prefix}Host Environment Fingerprint",
"value": host_fingerprint,
"help": HOST_ENVIRONMENT_FINGERPRINT_HELP,
"help": build_cache_host_environment_help(context),
})
elif entry.get("env_key_present") is True:
rows.append({"label": f"{prefix}Host Environment", "value": "environment key matched"})
Expand All @@ -401,7 +462,7 @@ def _append_cache_entry_rows(rows, entry, *, prefix):
rows.append({
"label": f"{prefix}{label}",
"value": value,
"help": BUILD_CACHE_DIGEST_HELP[key],
"help": build_cache_digest_help(key, context),
})


Expand Down
8 changes: 3 additions & 5 deletions scripts/build_with_cache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,7 @@ build_inputs_hash() {
hash_list=$(mktemp)
if git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$repo_root" ls-files \
"programs/${code}/build.sh" \
"programs/${code}/patches" \
"programs/${code}" \
"scripts/bk_functions.sh" \
"scripts/build_tool_wrappers" \
"scripts/build_with_cache.sh" \
Expand All @@ -261,12 +260,11 @@ build_inputs_hash() {
else
find "${repo_root}/scripts/build_tool_wrappers" \
-type f -print > "$file_list" 2>/dev/null || true
if [ -d "${repo_root}/programs/${code}/patches" ]; then
find "${repo_root}/programs/${code}/patches" \
if [ -d "${repo_root}/programs/${code}" ]; then
find "${repo_root}/programs/${code}" \
-type f -print >> "$file_list" 2>/dev/null || true
fi
printf '%s\n' \
"programs/${code}/build.sh" \
"scripts/bk_functions.sh" \
"scripts/build_with_cache.sh" \
"scripts/collect_environment_snapshot.sh" \
Expand Down
18 changes: 14 additions & 4 deletions scripts/tests/test_build_cache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,9 @@ printf '#!/bin/bash\n' > "${TMP_DIR}/git-project/programs/app/run.sh"
printf '#!/bin/bash\n' > "${TMP_DIR}/git-project/programs/app/profile.sh"
printf '# test app\n' > "${TMP_DIR}/git-project/programs/app/README.md"
mkdir -p "${TMP_DIR}/git-project/programs/app/patches"
mkdir -p "${TMP_DIR}/git-project/programs/app/src"
printf 'patch-v1\n' > "${TMP_DIR}/git-project/programs/app/patches/build.patch"
printf 'local-source-v1\n' > "${TMP_DIR}/git-project/programs/app/src/local-source.txt"
pushd "${TMP_DIR}/git-project" >/dev/null
git init --initial-branch=main >/dev/null
git config user.email "benchkit@example.invalid"
Expand All @@ -392,18 +394,26 @@ run_git_project_build_with_cache
test "$(cat "${TMP_DIR}/git-build-count")" = "1"
grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env"

printf 'local-source-v2\n' >> "${TMP_DIR}/git-project/programs/app/src/local-source.txt"
rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc"
run_git_project_build_with_cache
test "$(cat "${TMP_DIR}/git-build-count")" = "2"
grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env"
grep -q '^BK_BUILD_CACHE_RESTORE_STATUS=miss$' "${TMP_DIR}/git-project/results/build_cache.env"

printf '# run-only change\n' >> "${TMP_DIR}/git-project/programs/app/run.sh"
printf '# profile-only change\n' >> "${TMP_DIR}/git-project/programs/app/profile.sh"
printf '# docs-only change\n' >> "${TMP_DIR}/git-project/programs/app/README.md"
rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc"
run_git_project_build_with_cache
test "$(cat "${TMP_DIR}/git-build-count")" = "1"
grep -q '^BK_BUILD_CACHE_STATUS=hit$' "${TMP_DIR}/git-project/results/build_cache.env"
test "$(cat "${TMP_DIR}/git-build-count")" = "3"
grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env"
grep -q '^BK_BUILD_CACHE_RESTORE_STATUS=miss$' "${TMP_DIR}/git-project/results/build_cache.env"

printf 'patch-v2\n' >> "${TMP_DIR}/git-project/programs/app/patches/build.patch"
rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc"
run_git_project_build_with_cache
test "$(cat "${TMP_DIR}/git-build-count")" = "2"
test "$(cat "${TMP_DIR}/git-build-count")" = "4"
grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env"
grep -q '^BK_BUILD_CACHE_RESTORE_STATUS=miss$' "${TMP_DIR}/git-project/results/build_cache.env"
grep -q '^BK_BUILD_CACHE_REJECTED_CREATED_AT=' "${TMP_DIR}/git-project/results/build_cache.env"
Expand All @@ -421,7 +431,7 @@ esac
printf '\n# git build input change\n' >> "${TMP_DIR}/git-project/programs/app/build.sh"
rm -rf "${TMP_DIR}/git-project/artifacts" "${TMP_DIR}/git-project/results" "${TMP_DIR}/git-project/appsrc"
run_git_project_build_with_cache
test "$(cat "${TMP_DIR}/git-build-count")" = "3"
test "$(cat "${TMP_DIR}/git-build-count")" = "5"
grep -q '^BK_BUILD_CACHE_STORED=true$' "${TMP_DIR}/git-project/results/build_cache.env"

rm -rf "${TMP_DIR}/project/artifacts" "${TMP_DIR}/project/results" "${TMP_DIR}/project/tagsrc"
Expand Down
Loading