selective-ruby-core 0.2.8-aarch64-linux → 0.2.10-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 449df88a9c1971a298416d1a8572c35d9bd6ead7a76df9c7cdec29005f2bdb2f
4
- data.tar.gz: e7052df2bbff8f5927bf93a255d9a7b54cabe6bbd38e0ede590d3482fdf26e61
3
+ metadata.gz: 52b12017e465ee18c005f6661d79d12ea3e23821111ebe8ce13999b88dbc93b3
4
+ data.tar.gz: b1c4bbae8c2c5eb6dbc22376d8c1b6fbd49441af39de0632489e49b448692821
5
5
  SHA512:
6
- metadata.gz: 97cf8dc0e85104e3d6183f666e9ff6ec0ae1d54051f299fd8551097435b0cfa660cba823ad02067ba42a137d1760550c2743300dc42ed81ba450a9d82d33c47c
7
- data.tar.gz: 54c27979b7cbfa15ef74dce92a3ae5c880327a02325ee0bb7d0494d4c304137e7518be749c7da6def9047d8290148a64f3500bef631df3f3a11749c846bbd3af
6
+ metadata.gz: '028d9d8ddbebf24ba288ea081d6cc2d06f8cca626258c514d120e0f8140578e9e92b54c4dcbb281c2734844ec7a2fb622346eef84ab8e5867602efa022266d5a'
7
+ data.tar.gz: 7edf81122e5c5978f16866b5b1848bfe7514762e7831bc7592b3311c1524727b4df29ba49c74f2f8e6e073b77b30464fae5ccd36a7a9f9afc5ade85b04dc340b
data/LICENSE CHANGED
File without changes
data/Rakefile CHANGED
@@ -5,6 +5,26 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
+ # Builds the optional test map tracer into lib/ for development and CI.
9
+ # Installed gems build it through `spec.extensions` instead.
10
+ desc "Compile the native test map tracer"
11
+ task :compile do
12
+ require "fileutils"
13
+ require "rbconfig"
14
+ ext_dir = File.expand_path("ext/selective_tracer", __dir__)
15
+ build_dir = File.expand_path("tmp/selective_tracer", __dir__)
16
+ FileUtils.mkdir_p(build_dir)
17
+ Dir.chdir(build_dir) do
18
+ # extconf must not inherit this process's bundle; it only needs mkmf.
19
+ Bundler.with_unbundled_env do
20
+ sh RbConfig.ruby, File.join(ext_dir, "extconf.rb")
21
+ sh "make"
22
+ end
23
+ built = Dir["selective_tracer.{#{RbConfig::CONFIG["DLEXT"]},bundle,so}"].first
24
+ FileUtils.cp(built, File.expand_path("lib", __dir__)) if built
25
+ end
26
+ end
27
+
8
28
  require "standard/rake"
9
29
 
10
30
  task default: %i[spec standard]
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The tracer is optional. A customer without a C toolchain (or on an
4
+ # unsupported Ruby) must still be able to install selective-ruby-core and run
5
+ # their suite exactly as before, so any failed check writes a no-op Makefile
6
+ # instead of failing `gem install`. That includes compiling the tracer itself
7
+ # here first: an error in `make` would fail the install, so a Ruby whose
8
+ # headers the source doesn't build against must be caught before it. At
9
+ # runtime the missing library simply means "this runner can't record a test
10
+ # map".
11
+
12
+ require "mkmf"
13
+
14
+ def write_noop_makefile(reason)
15
+ warn "selective_tracer: skipping native build (#{reason}); test map recording will be unavailable"
16
+ File.write("Makefile", <<~MAKE)
17
+ all:
18
+ \t@true
19
+ install:
20
+ \t@true
21
+ clean:
22
+ \t@true
23
+ MAKE
24
+ end
25
+
26
+ begin
27
+ if RUBY_ENGINE != "ruby"
28
+ write_noop_makefile("#{RUBY_ENGINE} is not supported")
29
+ elsif !have_header("ruby/debug.h") || !have_func("rb_profile_frames", "ruby/debug.h")
30
+ write_noop_makefile("required VM hook APIs are missing")
31
+ else
32
+ $CFLAGS << " -O2 -std=c99 -Wall -Wno-unused-parameter" # standard:disable Style/GlobalVars -- mkmf's interface
33
+ if try_compile(File.read(File.expand_path("selective_tracer.c", __dir__)))
34
+ create_makefile("selective_tracer")
35
+ else
36
+ write_noop_makefile("the tracer does not compile on Ruby #{RUBY_VERSION}; see mkmf.log")
37
+ end
38
+ end
39
+ rescue => e
40
+ write_noop_makefile(e.message)
41
+ end
@@ -0,0 +1,336 @@
1
+ // selective_tracer — records which source files a single test executes.
2
+ //
3
+ // A test map is built from one question asked of every test: "which files
4
+ // did you run?". This extension answers it with two raw VM event hooks that
5
+ // never call back into Ruby while the test runs:
6
+ //
7
+ // * RUBY_EVENT_LINE | CALL | B_CALL, registered globally so work done on
8
+ // *any* thread is
9
+ // attributed to the running test. Capybara drives a Puma server thread;
10
+ // a hook scoped to the test's own thread (what rotoscope does) would map
11
+ // every system test to nothing and let selection skip them wrongly.
12
+ // Selective runs one test at a time per process, so "everything that ran
13
+ // between start and stop" is the right attribution. Stray background
14
+ // threads can only add files, which makes selection more conservative,
15
+ // never less. CALL and B_CALL are not redundant with LINE: an endless
16
+ // method (`def total = price * qty`) and a one-line block executed from
17
+ // another file emit no line event of their own, so a line-only hook never
18
+ // ties a test to the file they live in.
19
+ //
20
+ // * RUBY_INTERNAL_EVENT_NEWOBJ (optional), which records the class of every
21
+ // plain object allocated. A class whose file only ran at boot — an
22
+ // ActiveRecord model with no methods of its own — executes no lines
23
+ // during a test, so line events alone would never tie the test to it.
24
+ // The classes are resolved to source files in Ruby after the test stops.
25
+ //
26
+ // Why not Ruby's Coverage module: it is process-global, so it cannot run next
27
+ // to SimpleCov, and while it is enabled Bootsnap cannot use its compile cache.
28
+ // Why not TracePoint from Ruby: every event re-enters the interpreter. The
29
+ // design follows what Datadog learned building datadog-ci-rb (line hook with
30
+ // pointer caching, allocation tracking for code-less classes), written for
31
+ // Selective's needs: several project roots, several ignored prefixes, and no
32
+ // Ruby API use inside the allocation hook.
33
+
34
+ #include <ruby.h>
35
+ #include <ruby/debug.h>
36
+ #include <ruby/st.h>
37
+
38
+ #include <stdbool.h>
39
+ #include <string.h>
40
+
41
+ #define PATH_CACHE_SIZE 1024 // power of two
42
+ #define KLASS_CACHE_SIZE 4096 // power of two
43
+ #define MAX_PREFIXES 32
44
+ #define EXEC_EVENTS (RUBY_EVENT_LINE | RUBY_EVENT_CALL | RUBY_EVENT_B_CALL)
45
+
46
+ struct prefix_list {
47
+ int count;
48
+ char *values[MAX_PREFIXES];
49
+ long lengths[MAX_PREFIXES];
50
+ };
51
+
52
+ struct tracer {
53
+ // { path(String) => true } for the test being recorded.
54
+ VALUE files;
55
+
56
+ // Absolute paths are kept only under one of `roots` and outside every
57
+ // `ignored` prefix (bundled gems, Selective's own gems, tmp dirs).
58
+ struct prefix_list roots;
59
+ struct prefix_list ignored;
60
+
61
+ // Consecutive line events almost always come from the same file, and the
62
+ // path pointer of an iseq is stable, so a pointer comparison settles most
63
+ // events. The strings are marked so a freed path's address can't be reused
64
+ // for a different file and wrongly treated as "already seen".
65
+ VALUE last_path;
66
+ VALUE path_cache[PATH_CACHE_SIZE];
67
+
68
+ bool allocations;
69
+ bool running;
70
+
71
+ // Classes allocated during the test. Keys are class VALUEs, pinned by mark.
72
+ st_table *klasses;
73
+ VALUE last_klass;
74
+ VALUE klass_cache[KLASS_CACHE_SIZE];
75
+ };
76
+
77
+ static int mark_klass_i(st_data_t key, st_data_t _value, st_data_t _arg) {
78
+ rb_gc_mark((VALUE)key);
79
+ return ST_CONTINUE;
80
+ }
81
+
82
+ static void tracer_mark(void *ptr) {
83
+ struct tracer *t = ptr;
84
+ rb_gc_mark(t->files);
85
+ rb_gc_mark(t->last_path);
86
+ for (int i = 0; i < PATH_CACHE_SIZE; i++) rb_gc_mark(t->path_cache[i]);
87
+ rb_gc_mark(t->last_klass);
88
+ for (int i = 0; i < KLASS_CACHE_SIZE; i++) rb_gc_mark(t->klass_cache[i]);
89
+ if (t->klasses) st_foreach(t->klasses, mark_klass_i, 0);
90
+ }
91
+
92
+ static void free_prefixes(struct prefix_list *list) {
93
+ for (int i = 0; i < list->count; i++) xfree(list->values[i]);
94
+ list->count = 0;
95
+ }
96
+
97
+ static void tracer_free(void *ptr) {
98
+ struct tracer *t = ptr;
99
+ // No hook cleanup needed: the VM marks every registered hook's data, so a
100
+ // tracer that is still running is reachable and can't be freed.
101
+ free_prefixes(&t->roots);
102
+ free_prefixes(&t->ignored);
103
+ if (t->klasses) st_free_table(t->klasses);
104
+ xfree(t);
105
+ }
106
+
107
+ static size_t tracer_memsize(const void *ptr) {
108
+ return sizeof(struct tracer);
109
+ }
110
+
111
+ static const rb_data_type_t tracer_type = {
112
+ .wrap_struct_name = "selective_tracer",
113
+ .function = {.dmark = tracer_mark, .dfree = tracer_free, .dsize = tracer_memsize},
114
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY};
115
+
116
+ static void reset_caches(struct tracer *t) {
117
+ t->last_path = Qnil;
118
+ for (int i = 0; i < PATH_CACHE_SIZE; i++) t->path_cache[i] = Qnil;
119
+ t->last_klass = Qnil;
120
+ for (int i = 0; i < KLASS_CACHE_SIZE; i++) t->klass_cache[i] = Qnil;
121
+ }
122
+
123
+ static VALUE tracer_alloc(VALUE klass) {
124
+ struct tracer *t;
125
+ VALUE self = TypedData_Make_Struct(klass, struct tracer, &tracer_type, t);
126
+ t->files = Qnil;
127
+ t->roots.count = 0;
128
+ t->ignored.count = 0;
129
+ t->allocations = false;
130
+ t->running = false;
131
+ t->klasses = NULL;
132
+ reset_caches(t);
133
+ t->files = rb_hash_new();
134
+ t->klasses = st_init_numtable();
135
+ return self;
136
+ }
137
+
138
+ static struct tracer *get_tracer(VALUE self) {
139
+ struct tracer *t;
140
+ TypedData_Get_Struct(self, struct tracer, &tracer_type, t);
141
+ return t;
142
+ }
143
+
144
+ static bool has_dir_prefix(const char *path, long path_len, const char *prefix, long prefix_len) {
145
+ if (prefix_len > path_len || memcmp(prefix, path, prefix_len) != 0) return false;
146
+ // "/app" must not match "/application"; a prefix given with a trailing
147
+ // slash already carries the boundary.
148
+ return prefix_len == path_len || prefix[prefix_len - 1] == '/' || path[prefix_len] == '/';
149
+ }
150
+
151
+ static bool path_included(struct tracer *t, const char *path, long len) {
152
+ if (len == 0) return false;
153
+
154
+ if (path[0] != '/') {
155
+ // Relative paths ("./spec/x_spec.rb" from `load`) are resolved and
156
+ // re-checked in Ruby. Pseudo-paths like "(eval at ...)" and
157
+ // "<internal:kernel>" are never project files.
158
+ return path[0] != '(' && path[0] != '<';
159
+ }
160
+
161
+ bool in_root = false;
162
+ for (int i = 0; i < t->roots.count; i++) {
163
+ if (has_dir_prefix(path, len, t->roots.values[i], t->roots.lengths[i])) {
164
+ in_root = true;
165
+ break;
166
+ }
167
+ }
168
+ if (!in_root) return false;
169
+
170
+ for (int i = 0; i < t->ignored.count; i++) {
171
+ if (has_dir_prefix(path, len, t->ignored.values[i], t->ignored.lengths[i])) return false;
172
+ }
173
+ return true;
174
+ }
175
+
176
+ static inline size_t cache_slot(uintptr_t ptr, size_t size) {
177
+ return ((ptr >> 4) ^ (ptr >> 12)) & (size - 1);
178
+ }
179
+
180
+ static void on_exec(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass) {
181
+ struct tracer *t = RTYPEDDATA_DATA(data);
182
+
183
+ const char *current = rb_sourcefile();
184
+ if (current == NULL) return;
185
+
186
+ if (t->last_path != Qnil && RSTRING_PTR(t->last_path) == current) return;
187
+
188
+ size_t slot = cache_slot((uintptr_t)current, PATH_CACHE_SIZE);
189
+ VALUE cached = t->path_cache[slot];
190
+ if (cached != Qnil && RSTRING_PTR(cached) == current) {
191
+ t->last_path = cached;
192
+ return;
193
+ }
194
+
195
+ // First time this test sees this file: fetch the frame's path String so
196
+ // the pointer we cache is owned by a marked object.
197
+ VALUE frame;
198
+ if (rb_profile_frames(0, 1, &frame, NULL) != 1) return;
199
+
200
+ VALUE path = rb_profile_frame_path(frame);
201
+ if (NIL_P(path) || !RB_TYPE_P(path, T_STRING)) return;
202
+
203
+ t->last_path = path;
204
+ t->path_cache[slot] = path;
205
+
206
+ if (path_included(t, RSTRING_PTR(path), RSTRING_LEN(path))) {
207
+ rb_hash_aset(t->files, path, Qtrue);
208
+ }
209
+ }
210
+
211
+ // Runs inside object allocation: no Ruby method calls and no Ruby object
212
+ // allocation are allowed here. rb_obj_class, rb_mod_name and st_* do neither.
213
+ static void on_newobj(VALUE data, rb_trace_arg_t *targ) {
214
+ VALUE obj = rb_tracearg_object(targ);
215
+ enum ruby_value_type type = rb_type(obj);
216
+ if (type != RUBY_T_OBJECT && type != RUBY_T_STRUCT) return;
217
+
218
+ VALUE klass = rb_obj_class(obj);
219
+ if (klass == 0 || NIL_P(klass)) return;
220
+
221
+ struct tracer *t = RTYPEDDATA_DATA(data);
222
+ if (t->last_klass == klass) return;
223
+
224
+ size_t slot = cache_slot((uintptr_t)klass, KLASS_CACHE_SIZE);
225
+ if (t->klass_cache[slot] == klass) {
226
+ t->last_klass = klass;
227
+ return;
228
+ }
229
+
230
+ t->last_klass = klass;
231
+ t->klass_cache[slot] = klass;
232
+
233
+ if (st_is_member(t->klasses, (st_data_t)klass)) return;
234
+ if (NIL_P(rb_mod_name(klass))) return; // anonymous class: no file to resolve
235
+
236
+ st_insert(t->klasses, (st_data_t)klass, 1);
237
+ }
238
+
239
+ static void load_prefixes(struct prefix_list *list, VALUE ary, const char *name) {
240
+ if (NIL_P(ary)) return;
241
+ Check_Type(ary, T_ARRAY);
242
+ long n = RARRAY_LEN(ary);
243
+ if (n > MAX_PREFIXES) rb_raise(rb_eArgError, "at most %d %s are supported", MAX_PREFIXES, name);
244
+
245
+ for (long i = 0; i < n; i++) {
246
+ VALUE s = rb_ary_entry(ary, i);
247
+ Check_Type(s, T_STRING);
248
+ long len = RSTRING_LEN(s);
249
+ if (len == 0) continue;
250
+ char *copy = ALLOC_N(char, len + 1);
251
+ memcpy(copy, RSTRING_PTR(s), len);
252
+ copy[len] = '\0';
253
+ list->values[list->count] = copy;
254
+ list->lengths[list->count] = len;
255
+ list->count++;
256
+ }
257
+ }
258
+
259
+ // NativeTracer.new(roots: [...], ignored: [...], allocations: true/false)
260
+ static VALUE tracer_initialize(int argc, VALUE *argv, VALUE self) {
261
+ VALUE opts;
262
+ rb_scan_args(argc, argv, "1", &opts);
263
+ Check_Type(opts, T_HASH);
264
+
265
+ struct tracer *t = get_tracer(self);
266
+ free_prefixes(&t->roots);
267
+ free_prefixes(&t->ignored);
268
+
269
+ load_prefixes(&t->roots, rb_hash_aref(opts, ID2SYM(rb_intern("roots"))), "roots");
270
+ load_prefixes(&t->ignored, rb_hash_aref(opts, ID2SYM(rb_intern("ignored"))), "ignored prefixes");
271
+ if (t->roots.count == 0) rb_raise(rb_eArgError, "roots must contain at least one path");
272
+
273
+ t->allocations = RTEST(rb_hash_aref(opts, ID2SYM(rb_intern("allocations"))));
274
+ return self;
275
+ }
276
+
277
+ static VALUE tracer_start(VALUE self) {
278
+ struct tracer *t = get_tracer(self);
279
+ if (t->running) rb_raise(rb_eRuntimeError, "tracer is already running");
280
+
281
+ rb_hash_clear(t->files);
282
+ st_clear(t->klasses);
283
+ reset_caches(t);
284
+
285
+ rb_add_event_hook2((rb_event_hook_func_t)on_exec, EXEC_EVENTS, self, RUBY_EVENT_HOOK_FLAG_SAFE);
286
+ if (t->allocations) {
287
+ rb_add_event_hook2((rb_event_hook_func_t)on_newobj, RUBY_INTERNAL_EVENT_NEWOBJ, self,
288
+ RUBY_EVENT_HOOK_FLAG_SAFE | RUBY_EVENT_HOOK_FLAG_RAW_ARG);
289
+ }
290
+ t->running = true;
291
+ return self;
292
+ }
293
+
294
+ static int collect_klass_i(st_data_t key, st_data_t _value, st_data_t ary) {
295
+ rb_ary_push((VALUE)ary, (VALUE)key);
296
+ return ST_CONTINUE;
297
+ }
298
+
299
+ // Returns [files, classes]: the Hash of executed paths and the Array of named
300
+ // classes allocated during the test. Both are fresh objects owned by Ruby.
301
+ static VALUE tracer_stop(VALUE self) {
302
+ struct tracer *t = get_tracer(self);
303
+ if (!t->running) rb_raise(rb_eRuntimeError, "tracer is not running");
304
+
305
+ rb_remove_event_hook_with_data((rb_event_hook_func_t)on_exec, self);
306
+ if (t->allocations) rb_remove_event_hook_with_data((rb_event_hook_func_t)on_newobj, self);
307
+ t->running = false;
308
+
309
+ VALUE files = t->files;
310
+ t->files = rb_hash_new();
311
+
312
+ VALUE klasses = rb_ary_new_capa((long)t->klasses->num_entries);
313
+ st_foreach(t->klasses, collect_klass_i, (st_data_t)klasses);
314
+ st_clear(t->klasses);
315
+ reset_caches(t);
316
+
317
+ return rb_assoc_new(files, klasses);
318
+ }
319
+
320
+ static VALUE tracer_running_p(VALUE self) {
321
+ return get_tracer(self)->running ? Qtrue : Qfalse;
322
+ }
323
+
324
+ void Init_selective_tracer(void) {
325
+ VALUE mSelective = rb_define_module("Selective");
326
+ VALUE mRuby = rb_define_module_under(mSelective, "Ruby");
327
+ VALUE mCore = rb_define_module_under(mRuby, "Core");
328
+ VALUE mTestMap = rb_define_module_under(mCore, "TestMap");
329
+ VALUE cTracer = rb_define_class_under(mTestMap, "NativeTracer", rb_cObject);
330
+
331
+ rb_define_alloc_func(cTracer, tracer_alloc);
332
+ rb_define_method(cTracer, "initialize", tracer_initialize, -1);
333
+ rb_define_method(cTracer, "start", tracer_start, 0);
334
+ rb_define_method(cTracer, "stop", tracer_stop, 0);
335
+ rb_define_method(cTracer, "running?", tracer_running_p, 0);
336
+ }
data/lib/bin/build_env.sh CHANGED
@@ -1,6 +1,28 @@
1
1
  #!/bin/bash
2
2
 
3
- # Detect the platform (only GitHub Actions in this case)
3
+ # Parse an "owner/repo" slug out of a git remote URL (SSH or HTTPS) that points
4
+ # at github.com. Emits an empty string when the URL is not a github.com URL.
5
+ parse_github_slug() {
6
+ local url="$1"
7
+ case "$url" in
8
+ git@github.com:*|git://github.com/*|https://github.com/*|http://github.com/*|ssh://git@github.com/*)
9
+ echo "$url" | sed -E 's#^(git@|(git|https?|ssh)://(git@)?)github\.com[:/]##; s#\.git/?$##'
10
+ ;;
11
+ esac
12
+ }
13
+
14
+ # Given a git remote URL, classify the provider as github|gitlab|bitbucket or
15
+ # empty when unrecognized. Used for the git_provider hint.
16
+ detect_git_provider_from_url() {
17
+ local url="$1"
18
+ case "$url" in
19
+ *github.com*) echo "github" ;;
20
+ *gitlab.com*) echo "gitlab" ;;
21
+ *bitbucket.org*) echo "bitbucket" ;;
22
+ esac
23
+ }
24
+
25
+ # Detect the platform
4
26
  if [ -n "$GITHUB_ACTIONS" ]; then
5
27
  platform=github_actions
6
28
  branch=${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}
@@ -13,6 +35,24 @@ if [ -n "$GITHUB_ACTIONS" ]; then
13
35
  commit_message=$(git log --format=%s -n 1 $sha)
14
36
  committer_name=$(git show -s --format='%an' -n 1 $sha)
15
37
  committer_email=$(git show -s --format='%ae' -n 1 $sha)
38
+ git_repo_full_name=$GITHUB_REPOSITORY
39
+ git_provider=github
40
+ # Extract base_sha from the webhook event payload GitHub drops at
41
+ # $GITHUB_EVENT_PATH. Prefer jq when available (handles both push and PR
42
+ # events); fall back to a grep-based parse for the top-level "before" field
43
+ # on push events when jq is missing.
44
+ if [ -f "$GITHUB_EVENT_PATH" ]; then
45
+ if command -v jq >/dev/null 2>&1; then
46
+ base_sha=$(jq -r '.pull_request.base.sha // .before // empty' < "$GITHUB_EVENT_PATH" 2>/dev/null)
47
+ else
48
+ base_sha=$(grep -o '"before"[[:space:]]*:[[:space:]]*"[^"]*"' "$GITHUB_EVENT_PATH" | head -1 | sed -E 's/.*"[^"]*"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/')
49
+ fi
50
+ # GitHub emits all-zero SHAs when the ref was just created and has no prior
51
+ # state. Treat that as "no base" so the server falls through to head-only.
52
+ if [ "$base_sha" = "0000000000000000000000000000000000000000" ]; then
53
+ base_sha=""
54
+ fi
55
+ fi
16
56
  elif [ -n "$CIRCLECI" ]; then
17
57
  platform=circleci
18
58
  branch=$CIRCLE_BRANCH
@@ -23,6 +63,19 @@ elif [ -n "$CIRCLECI" ]; then
23
63
  commit_message=$(git log --format=%s -n 1 $sha)
24
64
  committer_name=$(git show -s --format='%an' -n 1 $sha)
25
65
  committer_email=$(git show -s --format='%ae' -n 1 $sha)
66
+ # CircleCI has no single "slug" env var. Compose from username + reponame.
67
+ # These work for both GitHub OAuth and GitHub App pipelines. For non-GitHub
68
+ # pipelines (GitLab, Bitbucket) the same vars exist but we gate on the repo
69
+ # URL to avoid emitting a non-GitHub slug.
70
+ if [ -n "$CIRCLE_PROJECT_USERNAME" ] && [ -n "$CIRCLE_PROJECT_REPONAME" ]; then
71
+ git_provider=$(detect_git_provider_from_url "$CIRCLE_REPOSITORY_URL")
72
+ if [ "$git_provider" = "github" ] || [ -z "$CIRCLE_REPOSITORY_URL" ]; then
73
+ git_repo_full_name="${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}"
74
+ # If we fell through the "no repo URL" path, default provider to github
75
+ # since the slug composition path is specific to GitHub-style owner/repo.
76
+ [ -z "$git_provider" ] && git_provider=github
77
+ fi
78
+ fi
26
79
  elif [ -n "$SEMAPHORE" ]; then
27
80
  platform=semaphore
28
81
  branch=${SEMAPHORE_GIT_PR_BRANCH:-$SEMAPHORE_GIT_BRANCH}
@@ -38,6 +91,35 @@ elif [ -n "$SEMAPHORE" ]; then
38
91
  commit_message=$(git log --format=%s -n 1 $sha)
39
92
  committer_name=$(git show -s --format='%an' -n 1 $sha)
40
93
  committer_email=$(git show -s --format='%ae' -n 1 $sha)
94
+ git_provider=$SEMAPHORE_GIT_PROVIDER
95
+ if [ "$git_provider" = "github" ]; then
96
+ git_repo_full_name=$SEMAPHORE_GIT_REPO_SLUG
97
+ fi
98
+ # SEMAPHORE_GIT_COMMIT_RANGE is "<base>...<head>" on push/PR events; take the
99
+ # left side. It is unset when the build isn't commit-scoped.
100
+ if [ -n "$SEMAPHORE_GIT_COMMIT_RANGE" ]; then
101
+ base_sha=$(echo "$SEMAPHORE_GIT_COMMIT_RANGE" | sed -E 's/\.\.\..*//')
102
+ fi
103
+ elif [ -n "$BUILDKITE" ]; then
104
+ platform=buildkite
105
+ branch=$BUILDKITE_BRANCH
106
+ target_branch=$BUILDKITE_PULL_REQUEST_BASE_BRANCH
107
+ actor=$BUILDKITE_BUILD_AUTHOR
108
+ sha=$BUILDKITE_COMMIT
109
+ run_id=$BUILDKITE_BUILD_ID
110
+ run_attempt=$BUILDKITE_RETRY_COUNT
111
+ runner_id=$BUILDKITE_PARALLEL_JOB
112
+ pr_title=$BUILDKITE_PULL_REQUEST_TITLE
113
+ commit_message=$BUILDKITE_MESSAGE
114
+ committer_name=$BUILDKITE_BUILD_AUTHOR
115
+ committer_email=$BUILDKITE_BUILD_AUTHOR_EMAIL
116
+ # Buildkite tells us the provider explicitly, and BUILDKITE_REPO is the URL.
117
+ if [ "$BUILDKITE_PIPELINE_PROVIDER" = "github" ]; then
118
+ git_provider=github
119
+ git_repo_full_name=$(parse_github_slug "$BUILDKITE_REPO")
120
+ else
121
+ git_provider=$BUILDKITE_PIPELINE_PROVIDER
122
+ fi
41
123
  elif [ -n "$RWX" ]; then
42
124
  platform=rwx
43
125
  branch="${RWX_GIT_REF_NAME}"
@@ -52,6 +134,13 @@ elif [ -n "$RWX" ]; then
52
134
  commit_message="${RWX_GIT_COMMIT_SUMMARY}"
53
135
  committer_name="${RWX_GIT_COMMITTER_NAME}"
54
136
  committer_email="${RWX_GIT_COMMITTER_EMAIL}"
137
+ # RWX_GIT_REPOSITORY_NAME is extracted by the git/clone package from whatever
138
+ # URL was cloned. For non-GitHub hosts this still looks like "owner/repo" but
139
+ # points elsewhere, so cross-check the URL host before trusting it.
140
+ git_provider=$(detect_git_provider_from_url "$RWX_GIT_REPOSITORY_URL")
141
+ if [ "$git_provider" = "github" ]; then
142
+ git_repo_full_name="${RWX_GIT_REPOSITORY_NAME}"
143
+ fi
55
144
  elif [ -n "$MINT" ]; then
56
145
  platform=rwx
57
146
  branch="${MINT_GIT_REF_NAME}"
@@ -66,6 +155,27 @@ elif [ -n "$MINT" ]; then
66
155
  commit_message="${MINT_GIT_COMMIT_SUMMARY}"
67
156
  committer_name="${MINT_GIT_COMMITTER_NAME}"
68
157
  committer_email="${MINT_GIT_COMMITTER_EMAIL}"
158
+ git_provider=$(detect_git_provider_from_url "$MINT_GIT_REPOSITORY_URL")
159
+ if [ "$git_provider" = "github" ]; then
160
+ git_repo_full_name="${MINT_GIT_REPOSITORY_NAME}"
161
+ fi
162
+ fi
163
+
164
+ # Final fallbacks when we haven't resolved the GitHub slug from a known CI
165
+ # platform. Order:
166
+ # 1. Explicit SELECTIVE_GIT_REPO override (always wins; see below).
167
+ # 2. git config --get remote.origin.url on a local checkout.
168
+ if [ -z "$git_repo_full_name" ] && command -v git >/dev/null 2>&1; then
169
+ origin_url=$(git config --get remote.origin.url 2>/dev/null || true)
170
+ if [ -n "$origin_url" ]; then
171
+ parsed_slug=$(parse_github_slug "$origin_url")
172
+ if [ -n "$parsed_slug" ]; then
173
+ git_repo_full_name="$parsed_slug"
174
+ [ -z "$git_provider" ] && git_provider=github
175
+ elif [ -z "$git_provider" ]; then
176
+ git_provider=$(detect_git_provider_from_url "$origin_url")
177
+ fi
178
+ fi
69
179
  fi
70
180
 
71
181
  function escape() {
@@ -83,11 +193,14 @@ cat <<EOF
83
193
  "target_branch": "$(escape "${SELECTIVE_TARGET_BRANCH:-$target_branch}")",
84
194
  "actor": "$(escape "${SELECTIVE_ACTOR:-$actor}")",
85
195
  "sha": "$(escape "${SELECTIVE_SHA:-$sha}")",
196
+ "base_sha": "$(escape "${SELECTIVE_BASE_SHA:-$base_sha}")",
86
197
  "run_id": "$(escape "${SELECTIVE_RUN_ID:-$run_id}")",
87
198
  "run_attempt": "$(escape "${SELECTIVE_RUN_ATTEMPT:-$run_attempt}")",
88
199
  "runner_id": "$(escape "${SELECTIVE_RUNNER_ID:-$runner_id}")",
89
200
  "commit_message": "$(escape "${SELECTIVE_COMMIT_MESSAGE:-$commit_message}")",
90
201
  "committer_name": "$(escape "${SELECTIVE_COMMITTER_NAME:-$committer_name}")",
91
- "committer_email": "$(escape "${SELECTIVE_COMMITTER_EMAIL:-$committer_email}")"
202
+ "committer_email": "$(escape "${SELECTIVE_COMMITTER_EMAIL:-$committer_email}")",
203
+ "git_repo_full_name": "$(escape "${SELECTIVE_GIT_REPO:-$git_repo_full_name}")",
204
+ "git_provider": "$(escape "${SELECTIVE_GIT_PROVIDER:-$git_provider}")"
92
205
  }
93
206
  EOF