Skip to content

Commit 7e97aa6

Browse files
schneemsedmorley
andauthored
Add bash based metrics collection (heroku#1635)
* Introduce bash based metrics * Fix/test duration metrics test * Standardize on Ruby yaml format * Shellcheck metrics file * Re-use bash YAML file in Ruby * Capture problems with build report file If the env var is missing or the file does not exist, print that information. * Add docs to bin/report * Stricter bash * Capture manually installing JVM * Capture known stack failures * Add failure reasons for compile_buildpack_v2 * Capture if `which java` is detected * Move metrics into main bash functions file Simplifies calling metrics functions inside of bash functions, and it's relatively small. * Namespace env var * Capture boostrap failure * Fix Shellcheck ``` Run shellcheck bin/support/bash_functions.sh bin/support/download_ruby -x && In bin/support/bash_functions.sh line 234: local timestamp=$(date +%s%3N 2>/dev/null) ^-------^ SC2155 (warning): Declare and assign separately to avoid masking return values. In bin/support/bash_functions.sh line 243: local seconds=$(date +%s) ^-----^ SC2155 (warning): Declare and assign separately to avoid masking return values. In bin/support/bash_functions.sh line 244: local nanoseconds=$(date +%N 2>/dev/null || echo "000000000") ^---------^ SC2155 (warning): Declare and assign separately to avoid masking return values. ``` * Changelog * Fix missing export ``` +remote: Updated 2 paths from 60b426a +remote: Compressing source files... done. +remote: Building source: +remote: +remote: -----> Building on the Heroku-24 stack +remote: -----> Using buildpack: https://github.com/heroku/heroku-buildpack-ruby#schneems/bash-metrics-initial +remote: -----> Ruby app detected +remote: /tmp/codon/tmp/buildpacks/bc6628ed5814e6d8925d4527655dd8e7dda421f4/bin/support/ruby_compile:14:in `fetch': key not found: "HEROKU_RUBY_BUILD_REPORT_FILE" (KeyError) +remote: from /tmp/codon/tmp/buildpacks/bc6628ed5814e6d8925d4527655dd8e7dda421f4/bin/support/ruby_compile:14:in `<main>' +remote: ! Push rejected, failed to compile Ruby app. +remote: +remote: ! Push failed +remote: Verifying deploy... +remote: +remote: ! Push rejected to hatchet-t-90251d64ed. +remote: +To https://git.heroku.com/hatchet-t-90251d64ed.git + ! [remote rejected] HEAD -> main (pre-receive hook declined) +error: failed to push some refs to 'https://git.heroku.com/hatchet-t-90251d64ed.git' # ./vendor/bundle/ruby/3.3.0/gems/rspec-support-3.13.4/lib/rspec/support.rb:110:in `block in <module:Support>' ``` * Fix shellcheck ``` In bin/compile line 12: BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd) ^-----------^ SC2034 (warning): BUILDPACK_DIR appears unused. Verify use (or export if used externally). In bin/test line 9: BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd) ^-----------^ SC2034 (warning): BUILDPACK_DIR appears unused. Verify use (or export if used externally). In bin/test line 15: metrics::init "${CACHE_DIR}" ^----------^ SC2153 (info): Possible misspelling: CACHE_DIR may not be assigned. Did you mean cache_dir? In bin/test-compile line 12: BUILDPACK_DIR=$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd) ^-----------^ SC2034 (warning): BUILDPACK_DIR appears unused. Verify use (or export if used externally). For more information: https://www.shellcheck.net/wiki/SC2034 -- BUILDPACK_DIR appears unused. Ver... https://www.shellcheck.net/wiki/SC2153 -- Possible misspelling: CACHE_DIR m... ``` * Add and fix shellcheck for bin/report * Fix docs * Fix spec The file now relies on importing bash functions which weren't being copied. This fixes the error. * bin/test has no cache dir ``` $ bin/test BUILD_DIR ENV_DIR ``` https://devcenter.heroku.com/articles/testpack-api * Use JSON for bash metrics The python buildpack was modified to use JSON instead of YAML as it's easier to use `jq` to format and handle issues such as escaping. This preserves the same API, but changes the format and adopts the strategy from heroku/heroku-buildpack-python#1878. * Update Ruby build report to use json output Previously the bash build output helpers were adjusted to emit json. This wires up the Ruby metrics code to be compatible with that change. * Apply suggestions from code review Co-authored-by: Ed Morley <501702+edmorley@users.noreply.github.com> Signed-off-by: Richard Schneeman <richard.schneeman+no-recruiters@gmail.com> * Use EPOCHREALTIME Inspired by heroku/heroku-buildpack-python#1881. This works locally and on Heroku. It works in bash, but not zsh by default (have to `zmodload zsh/datetime` to get it to work). We're explicitly using bash in these scripts. * metrics:: prefix to build_data:: prefix Metrics implies numeric data. While we derive numeric data by counting the outputs, it contains other values and has other uses. `build_data` is an accurate representation of the domain. * Update bash docs Switched from emitting YMAL to emitting JSON. * Change metric name Address feedback from https://github.com/heroku/heroku-buildpack-ruby/pull/1635/files#r2293428233 > (optional) If I were looking through a Ruby event in Honeycomb (eg if I were paged in the night) and saw a field named which_java I might struggle to understand what it meant. My first guess is that the values for such a key would be the path to the Java being used or similar, rather than a bool. Is there another name that might be clearer here and less coupled to the Bash function names? Switching away from boolean and to a string field with two values either `ruby_buildpack` or `previously_installed` * Delete removed function * Remove error handling from printing Comment https://github.com/heroku/heroku-buildpack-ruby/pull/1635/files#r2293616228 ``` I'm not too sure about this error handling implementation. If the env var isn't set, it's a buildpack bug, and with the :-'(unset) fallback removed that bug would correctly be caught by bash error on undefined env vars (which which file enables). As-is, this fallback masks the issue and causes bin/report to exit zero when it actually hit an internal error case. Also, if the report file is missing, that's also an internal buildpack bug (and not a user facing error case). Currently cytokine logs an error if a buildpack has a bin/report and it exits 1 (having set a minimal build report), which I think might be a better way to catch these cases? If we did still want to customise the report in this scenario, implementing it in cytokine (rather than each buildpack) would allow standardising across buildpacks more easily. ``` Adds in a check for when the exported env var HEROKU_RUBY_BUILD_REPORT_FILE is mutated to error. * print -> print_bin_report_json Align API with python https://github.com/heroku/heroku-buildpack-python/blob/30086f2e8da2bf50d1b669b2e9ca82d479bf0892/lib/build_data.sh * Update docs * Use /dev/null in unsupported bin/ executables The bin/report script does not work on bin/test-compile or bin/test. We don't want to hint that data can be collected about usage in those cases. The shared code still calls `build_data::kv` functions so those need to not error. Setting to `/dev/null` allows these calls to work without error even if no data is being collected. * Update docs * Update shellcheck CI call So we don't forget any files by mistake heroku#1635 (comment) * Fix test We have another test for when the variables differ, this test is supposed to show using a bad file raises an error. Fixes this failure 1) Bash functions metrics prints error when report env var is set to a non-existent file Failure/Error: expect(out).to include("No such file or directory") expected "Error: HEROKU_RUBY_BUILD_REPORT_FILE does not match BUILD_DATA_FILE\nHEROKU_RUBY_BUILD_REPORT_FILE: /dev/null\nBUILD_DATA_FILE: /var/folders/yr/yytf3z3n3q336f1tj2b2j0gw0000gn/T/d20250822-87942-cz2rfj/does-not-exist\n" to include "No such file or directory" Diff: @@ -1 +1,3 @@ -No such file or directory +Error: HEROKU_RUBY_BUILD_REPORT_FILE does not match BUILD_DATA_FILE +HEROKU_RUBY_BUILD_REPORT_FILE: /dev/null +BUILD_DATA_FILE: /var/folders/yr/yytf3z3n3q336f1tj2b2j0gw0000gn/T/d20250822-87942-cz2rfj/does-not-exist # /Users/rschneeman/.gem/ruby/3.3.9/gems/rspec-support-3.13.4/lib/rspec/support.rb:110:in `block in <module:Support>' # /Users/rschneeman/.gem/ruby/3.3.9/gems/rspec-support-3.13.4/lib/rspec/support.rb:119:in `notify_failure' * Prefer bash if/else Mentioned in heroku#1635 (comment) the `|| {}` style of bash error handling was not behaving as expected. * Fix missing error output Before ``` -----> Downloading Buildpack: heroku/jvm ``` After ``` -----> Downloading Buildpack: heroku/jvm curl: (22) The requested URL returned error: 403 Failed to download https://buildpack-registry.s3.us-east-1.amazonaws.com/buildpacks/heroku/oops.tgz ``` * Prefer quiet commands to routing to dev/null - tar: Remove verbose `v` - git clone: Add --quiet * Track loading bad json results in metric When deserializing a prior json input fails, capture that status. This doesn't technically * Apply suggestions from code review Co-authored-by: Ed Morley <501702+edmorley@users.noreply.github.com> Signed-off-by: Richard Schneeman <richard.schneeman+no-recruiters@gmail.com> * Add .rb suffix to ruby executables This provides a more robust grep for the shellcheck exclude. heroku#1635 (comment) * Fix Error Calling `build_data::init "/dev/null" does not actually allow `build_data::kv_*` calls as it behaves differently than the test. It sets the data file to `/dev/null/build-data/ruby.json` which doesn't exist so those calls fail. The default of that file is already `/dev/null` we can use that instead. heroku#1635 (comment) * Use null termination for xargs inputs The shellcheck command now uses git ls-files -z to output null-separated file paths, which requires xargs -0 to properly handle filenames containing spaces or special characters. Without the -0 flag, xargs treats newlines as separators and may incorrectly parse complex file paths, leading to unreliable behavior when processing the file list. Added a link to docs that mention the exclude syntax > After a path matches any non-exclude pathspec, it will be run through all exclude pathspecs (magic signature: ! or its synonym ^). If it matches, the path is ignored. When there is no non-exclude pathspec, the exclusion is applied to the result set as if invoked without any pathspec. * Use verbose `git ls-files` exclude syntax > "In the long form, the leading colon : is followed by an open parenthesis (, a comma-separated list of zero or more "magic words", and a close parentheses ), and the remainder is the pattern to match against the path." It's not clear looking at the docs but the header itself qualifies as a "magic word" ``` exclude After a path matches any non-exclude pathspec, it will be run through all exclude pathspecs (magic signature: ! or its synonym ^). If it matches, the path is ignored. When there is no non-exclude pathspec, the exclusion is applied to the result set as if invoked without any pathspec. ``` This allows us to use `(exclude)` as syntax here. heroku#1635 (comment) --------- Signed-off-by: Richard Schneeman <richard.schneeman+no-recruiters@gmail.com> Co-authored-by: Ed Morley <501702+edmorley@users.noreply.github.com>
1 parent c8ab729 commit 7e97aa6

17 files changed

Lines changed: 501 additions & 78 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ jobs:
1818
uses: actions/checkout@v4
1919
- name: Run ShellCheck bin top level
2020
run: |
21-
shellcheck bin/support/bash_functions.sh bin/support/download_ruby -x &&
22-
shellcheck bin/compile bin/detect bin/release bin/test bin/test-compile -x
21+
# All bash files that don't end in '.rb'
22+
# https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec
23+
git ls-files -z --cached --others --exclude-standard ':(exclude)*.rb' 'bin/*' '*/bin/*' '*.sh' | \
24+
xargs -0 shellcheck --check-sourced --color=always
2325
2426
integration-test:
2527
runs-on: ubuntu-24.04

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Add bash based failure metrics reporting for `bin/report`. This allows the buildpack to distinguish beteween types of failures that occur before Ruby code in the buildpack has executed (such as when bootstrapping). (https://github.com/heroku/heroku-buildpack-ruby/pull/1635)
56

67
## [v318] - 2025-08-11
78

bin/compile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ BIN_DIR=$(cd "$(dirname "$0")" || exit; pwd) # absolute path
1212
# shellcheck source=bin/support/bash_functions.sh
1313
source "$BIN_DIR/support/bash_functions.sh"
1414

15+
# Initialize metrics for bin/report
16+
build_data::init "${CACHE_DIR}"
17+
build_data::clear
18+
1519
checks::ensure_supported_stack "${STACK:?Required env var STACK is not set}"
1620

1721
bootstrap_ruby_dir=$(mktemp -d)
18-
"$BIN_DIR"/support/download_ruby "$BIN_DIR" "$bootstrap_ruby_dir"
22+
if "$BIN_DIR"/support/download_ruby "$BIN_DIR" "$bootstrap_ruby_dir"; then
23+
:
24+
else
25+
build_data::kv_string "failure_reason" "bootstrap_ruby_fail"
26+
exit 1
27+
fi
1928
trap 'rm -rf "$bootstrap_ruby_dir"' EXIT
2029

2130
export PATH="$bootstrap_ruby_dir/bin/:$PATH"
@@ -35,7 +44,8 @@ if detect_needs_java "$BUILD_DIR"; then
3544
3645
EOM
3746

47+
build_data::kv_string "java_origin" "ruby_buildpack"
3848
compile_buildpack_v2 "$BUILD_DIR" "$CACHE_DIR" "$ENV_DIR" "https://buildpack-registry.s3.us-east-1.amazonaws.com/buildpacks/heroku/jvm.tgz" "heroku/jvm"
3949
fi
4050

41-
"$bootstrap_ruby_dir"/bin/ruby "$BIN_DIR/support/ruby_compile" "$@"
51+
"$bootstrap_ruby_dir"/bin/ruby "$BIN_DIR/support/ruby_compile.rb" "$@"

bin/report

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
11
#!/usr/bin/env bash
2+
# Usage: bin/report <build-dir> <cache-dir> <env-dir>
3+
4+
# Produces a build report containing metadata about the build, that's consumed by the build system.
5+
# This script is run for both successful and failing builds, so it should not assume the build ran
6+
# to completion (e.g. Python or other tools may not even have been installed).
7+
#
8+
# Metrics are collected from the part of the buildpack written in Ruby and the
9+
# part written in Bash (that is used to bootstrap Ruby). They both write to the
10+
# same JSON file on disk.
11+
#
12+
# Metadata must be emitted to stdout as valid un-nested json (values must not be objects or arrays).
13+
#
14+
# Example valid stdout:
15+
# {
16+
# "ruby_version": "X.Y.Z",
17+
# "ruby_install_duration": 1.234
18+
# }
19+
#
20+
# Failures in this script don't cause the overall build to fail (and won't appear in user
21+
# facing build logs) to avoid breaking builds unnecessarily / causing confusion. To debug
22+
# issues check the internal build system logs for `buildpack.report.failed` events, or
23+
# when developing run `make run` in this repo locally, which runs `bin/report` too.
224

325
set -euo pipefail
426

527
CACHE_DIR="${2}"
6-
REPORT_FILE="${CACHE_DIR}/.heroku/ruby/build_report.yml"
7-
8-
# Whilst the release file is always written by the buildpack, some apps use
9-
# third-party slug cleaner buildpacks to remove this and other files, so we
10-
# cannot assume it still exists by the time the release step runs.
11-
if [[ -f "${REPORT_FILE}" ]]; then
12-
cat "${REPORT_FILE}"
13-
fi
28+
# The absolute path to the root of the buildpack.
29+
BIN_DIR=$(cd "$(dirname "$0")" || exit; pwd) # absolute path
30+
31+
# Initialize metrics for bin/report
32+
# shellcheck source=bin/support/bash_functions.sh
33+
source "$BIN_DIR/support/bash_functions.sh"
34+
build_data::init "${CACHE_DIR}"
35+
build_data::print_bin_report_json

bin/support/bash_functions.sh

Lines changed: 227 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#!/usr/bin/env bash
22

3+
set -euo pipefail
4+
35
curl_retry_on_18() {
46
local ec=18;
57
local attempts=0;
@@ -25,6 +27,7 @@ detect_needs_java()
2527
local skip_java_install=1
2628

2729
if which_java; then
30+
build_data::kv_string "java_origin" "previously_installed"
2831
return $skip_java_install
2932
fi
3033

@@ -64,45 +67,77 @@ compile_buildpack_v2()
6467

6568
if [[ "$url" =~ \.tgz$ ]] || [[ "$url" =~ \.tgz\? ]]; then
6669
mkdir -p "$dir"
67-
curl_retry_on_18 -s --fail --retry 3 --retry-connrefused --connect-timeout "${CURL_CONNECT_TIMEOUT:-3}" "$url" | tar xvz -C "$dir" >/dev/null 2>&1
70+
if curl_retry_on_18 -s --fail --show-error --retry 3 --retry-connrefused --connect-timeout "${CURL_CONNECT_TIMEOUT:-3}" "$url" | tar xz -C "$dir"; then
71+
:
72+
else
73+
echo "Failed to download $url"
74+
build_data::kv_string "failure_reason" "compile_buildpack_v2_download_fail"
75+
build_data::kv_string "failure_detail" "url: $url"
76+
exit 1
77+
fi
6878
else
69-
git clone "$url" "$dir" >/dev/null 2>&1
79+
if git clone --quiet "$url" "$dir"; then
80+
:
81+
else
82+
echo "Failed to clone $url"
83+
build_data::kv_string "failure_reason" "compile_buildpack_v2_download_fail"
84+
build_data::kv_string "failure_detail" "url: $url"
85+
exit 1
86+
fi
7087
fi
7188
cd "$dir" || return
7289

7390
if [ "$branch" != "" ]; then
74-
git checkout "$branch" >/dev/null 2>&1
91+
if git checkout "$branch" >/dev/null 2>&1; then
92+
:
93+
else
94+
echo "Failed to checkout branch $branch"
95+
build_data::kv_string "failure_reason" "compile_buildpack_v2_checkout_fail"
96+
build_data::kv_string "failure_detail" "buildpack: $buildpack, branch: $branch"
97+
exit 1
98+
fi
7599
fi
76100

77101
# we'll get errors later if these are needed and don't exist
78102
chmod -f +x "$dir/bin/{detect,compile,release}" || true
79103

80-
framework=$("$dir"/bin/detect "$build_dir")
104+
if framework=$("$dir"/bin/detect "$build_dir"); then
105+
:
106+
else
107+
echo "Couldn't detect any framework for this buildpack. Exiting."
108+
build_data::kv_string "failure_reason" "compile_buildpack_v2_detect_fail"
109+
build_data::kv_string "failure_detail" "buildpack: $buildpack"
81110

82-
# shellcheck disable=SC2181
83-
if [ $? == 0 ]; then
84-
echo "-----> Detected Framework: $framework"
85-
"$dir"/bin/compile "$build_dir" "$cache_dir" "$env_dir"
111+
exit 1
112+
fi
86113

87-
# shellcheck disable=SC2181
88-
if [ $? != 0 ]; then
89-
exit 1
90-
fi
114+
echo "-----> Detected Framework: $framework"
115+
if "$dir"/bin/compile "$build_dir" "$cache_dir" "$env_dir"; then
116+
:
117+
else
118+
echo "Failed to compile with $buildpack"
119+
build_data::kv_string "failure_reason" "compile_buildpack_v2_compile_fail"
120+
build_data::kv_string "failure_detail" "buildpack: $buildpack"
121+
exit 1
122+
fi
91123

92-
# check if the buildpack left behind an environment for subsequent ones
93-
if [ -e "$dir/export" ]; then
94-
set +u # http://redsymbol.net/articles/unofficial-bash-strict-mode/#sourcing-nonconforming-document
95-
# shellcheck disable=SC1091
96-
source "$dir/export"
97-
set -u # http://redsymbol.net/articles/unofficial-bash-strict-mode/#sourcing-nonconforming-document
98-
fi
124+
# check if the buildpack left behind an environment for subsequent ones
125+
if [ -e "$dir/export" ]; then
126+
set +u # http://redsymbol.net/articles/unofficial-bash-strict-mode/#sourcing-nonconforming-document
127+
# shellcheck disable=SC1091
128+
source "$dir/export"
129+
set -u # http://redsymbol.net/articles/unofficial-bash-strict-mode/#sourcing-nonconforming-document
130+
fi
99131

100-
if [ -x "$dir/bin/release" ]; then
101-
"$dir"/bin/release "$build_dir" > "$1"/last_pack_release.out
132+
if [ -x "$dir/bin/release" ]; then
133+
if "$dir"/bin/release "$build_dir" > "$1"/last_pack_release.out; then
134+
:
135+
else
136+
echo "Failed bin/release with $buildpack"
137+
build_data::kv_string "failure_reason" "compile_buildpack_v2_release_fail"
138+
build_data::kv_string "failure_detail" "buildpack: $buildpack"
139+
exit 1
102140
fi
103-
else
104-
echo "Couldn't detect any framework for this buildpack. Exiting."
105-
exit 1
106141
fi
107142
fi
108143
}
@@ -119,6 +154,8 @@ function checks::ensure_supported_stack() {
119154
return 0
120155
;;
121156
heroku-18 | heroku-20)
157+
build_data::kv_string "failure_reason" "stack_eol"
158+
build_data::kv_string "failure_detail" "${stack} stack"
122159
# This error will only ever be seen on non-Heroku environments, since the
123160
# Heroku build system rejects builds using EOL stacks.
124161
cat <<-EOF
@@ -133,6 +170,8 @@ function checks::ensure_supported_stack() {
133170
exit 1
134171
;;
135172
*)
173+
build_data::kv_string "failure_reason" "stack_unknown"
174+
build_data::kv_string "failure_detail" "${stack} stack"
136175
cat <<-EOF
137176
Error: The '${stack}' stack isn't recognised.
138177
@@ -147,3 +186,167 @@ function checks::ensure_supported_stack() {
147186
;;
148187
esac
149188
}
189+
190+
## ==============================
191+
# Start of build_data section
192+
## ==============================
193+
194+
# Contains functions for storing build data from the buildpack in bash.
195+
#
196+
# The format of the report file is JSON.
197+
#
198+
# Example:
199+
# {
200+
# "ruby_version": "3.3.3",
201+
# "ruby_install_duration": 1.234
202+
# }
203+
#
204+
# All keys get `ruby.` prepended to them in the backend automatically.
205+
206+
# Variables shared by this whole module
207+
BUILD_DATA_FILE="/dev/null"
208+
# Exported for use by Ruby code
209+
HEROKU_RUBY_BUILD_REPORT_FILE="/dev/null"
210+
211+
# Must be called before you can use any other methods
212+
#
213+
# Usage:
214+
# ```
215+
# build_data::init "${CACHE_DIR}"
216+
# ```
217+
build_data::init() {
218+
local cache_dir="${1}"
219+
BUILD_DATA_FILE="${cache_dir}/build-data/ruby.json"
220+
HEROKU_RUBY_BUILD_REPORT_FILE="${BUILD_DATA_FILE}"
221+
222+
# Used later in the `HerokuBuildReport.set_global` call in `bin/support/ruby_compile`
223+
export HEROKU_RUBY_BUILD_REPORT_FILE
224+
}
225+
226+
# Clears any prior build data since it persists between builds
227+
#
228+
# Usage:
229+
# ```
230+
# build_data::init "${CACHE_DIR}"
231+
# build_data::clear
232+
# ```
233+
build_data::clear() {
234+
mkdir -p "$(dirname "${BUILD_DATA_FILE}")"
235+
echo "{}" >"${BUILD_DATA_FILE}"
236+
}
237+
238+
# Adds a key-value pair to the report file without any attempt to quote or escape the value.
239+
#
240+
# Usage:
241+
# ```
242+
# build_data::kv_raw "ruby_version_major" "3"
243+
# build_data::kv_raw "ruby_version_default" "true"
244+
# ```
245+
build_data::kv_raw() {
246+
local key="${1}"
247+
local value="${2}"
248+
build_report::_set "${key}" "${value}" "false"
249+
}
250+
251+
# Adds a key-value pair to the report file, quoting the value.
252+
#
253+
# Usage:
254+
# ```
255+
# build_data::kv_string "ruby_version" "3.3.3"
256+
# ```
257+
build_data::kv_string() {
258+
local key="${1}"
259+
local value="${2}"
260+
build_report::_set "${key}" "${value}" "true"
261+
}
262+
263+
# Internal helper to write a key/value pair to the build data store. The buildpack shouldn't call this directly.
264+
# Takes a key, value, and a boolean flag indicating whether the value needs to be quoted.
265+
#
266+
# Usage:
267+
# ```
268+
# build_report::_set "foo_string" "quote me" "true"
269+
# build_report::_set "bar_number" "99" "false"
270+
# ```
271+
function build_report::_set() {
272+
local key="${1}"
273+
# Truncate the value to an arbitrary 200 characters since it will sometimes contain user-provided
274+
# inputs which may be unbounded in size. Ideally individual call sites will perform more aggressive
275+
# truncation themselves based on the expected value size, however this is here as a fallback.
276+
# (Honeycomb supports string fields up to 64KB in size, however, it's not worth filling up the
277+
# build data store or bloating the payload passed back to Vacuole/submitted to Honeycomb given the
278+
# extra content in those cases is not normally useful.)
279+
local value="${2:0:200}"
280+
local needs_quoting="${3}"
281+
282+
if [[ "${needs_quoting}" == "true" ]]; then
283+
# Values passed using `--arg` are treated as strings, and so have double quotes added and any JSON
284+
# special characters (such as newlines, carriage returns, double quotes, backslashes) are escaped.
285+
local jq_args=(--arg value "${value}")
286+
else
287+
# Values passed using `--argjson` are treated as raw JSON values, and so aren't escaped or quoted.
288+
local jq_args=(--argjson value "${value}")
289+
fi
290+
291+
local new_data_file_contents
292+
new_data_file_contents="$(jq --arg key "${key}" "${jq_args[@]}" '. + { ($key): ($value) }' "${BUILD_DATA_FILE}")"
293+
echo "${new_data_file_contents}" >"${BUILD_DATA_FILE}"
294+
}
295+
296+
# Returns the current time since the UNIX Epoch, as a float with microseconds precision.
297+
#
298+
# Usage (2025-08-22 11:15 UTC):
299+
# ```
300+
# build_data::current_unix_realtime
301+
# # => 1755879324.771610
302+
# ```
303+
build_data::current_unix_realtime() {
304+
# We use a subshell with `LC_ALL=C` to ensure the output format isn't affected by system locale.
305+
(
306+
LC_ALL=C
307+
echo "${EPOCHREALTIME}"
308+
)
309+
}
310+
311+
# Adds a key=duration to the report file.
312+
#
313+
# Usage:
314+
# ```
315+
# start_time=$(build_data::current_unix_realtime)
316+
# sleep 1
317+
# build_data::kv_duration_since "ruby_install" "${start_time}"
318+
#
319+
# build_data::print_bin_report_json
320+
# # => { "ruby_install": 1.234 }
321+
# ```
322+
build_data::kv_duration_since() {
323+
local key="${1}"
324+
local start_time="${2}"
325+
local end_time duration
326+
end_time="$(build_data::current_unix_realtime)"
327+
duration="$(awk -v start="${start_time}" -v end="${end_time}" 'BEGIN { printf "%f", (end - start) }')"
328+
329+
build_data::kv_raw "${key}" "${duration}"
330+
}
331+
332+
# Prints the build data in JSON format.
333+
#
334+
# Usage:
335+
# ```
336+
# build_data::print_bin_report_json
337+
# # => { "ruby_install": 1.234 }
338+
# ```
339+
build_data::print_bin_report_json() {
340+
if [ "${HEROKU_RUBY_BUILD_REPORT_FILE}" != "${BUILD_DATA_FILE}" ]; then
341+
echo "Error: HEROKU_RUBY_BUILD_REPORT_FILE does not match BUILD_DATA_FILE"
342+
echo "HEROKU_RUBY_BUILD_REPORT_FILE: ${HEROKU_RUBY_BUILD_REPORT_FILE}"
343+
echo "BUILD_DATA_FILE: ${BUILD_DATA_FILE}"
344+
exit 1
345+
fi
346+
347+
jq --sort-keys '.' "${BUILD_DATA_FILE}"
348+
}
349+
350+
## ==============================
351+
# End of build data section
352+
## ==============================

0 commit comments

Comments
 (0)