Skip to content

Commit b0c53e2

Browse files
Merge branch 'master' into ci/update-gradle-dependencies-20260316
2 parents 873b293 + 0a45dd7 commit b0c53e2

16 files changed

Lines changed: 490 additions & 35 deletions

File tree

.github/workflows/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ _Recovery:_ Manually verify the guideline compliance.
3030

3131
### check-pull-request-labels [🔗](check-pull-request-labels.yaml)
3232

33-
_Trigger:_ When creating or updating a pull request.
33+
_Trigger:_ When creating or updating a pull request, or when new commits are pushed to it.
34+
35+
_Actions:_
3436

35-
_Action:_ Check the pull request did not introduce unexpected label.
37+
* Detect AI-generated pull requests then apply the `tag: ai generated` label.
38+
* Check the pull request did not introduce unexpected labels.
3639

3740
_Recovery:_ Update the pull request or add a comment to trigger the action again.
3841

.github/workflows/check-pull-request-labels.yaml

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Validate PR Label Format
22
on:
33
pull_request:
4-
types: [opened, edited, ready_for_review, labeled]
4+
types: [opened, edited, ready_for_review, labeled, synchronize]
55

66
concurrency:
77
group: ${{ github.workflow }}-${{ github.ref }}
@@ -15,8 +15,114 @@ jobs:
1515
pull-requests: write
1616
runs-on: ubuntu-latest
1717
steps:
18+
- name: Flag AI-generated pull requests
19+
id: flag_ai_generated
20+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0
21+
with:
22+
github-token: ${{ secrets.GITHUB_TOKEN }}
23+
script: |
24+
// Skip draft pull requests
25+
if (context.payload.pull_request.draft) {
26+
return
27+
}
28+
const prNumber = context.payload.pull_request.number
29+
const owner = context.repo.owner
30+
const repo = context.repo.repo
31+
const aiGeneratedLabel = 'tag: ai generated'
32+
let isAiGenerated = false
33+
let labelsStale = false
34+
35+
/*
36+
* Check for 'Bits AI' label and remove it.
37+
*/
38+
const bitsAiLabel = 'Bits AI'
39+
const prLabels = context.payload.pull_request.labels.map(l => l.name)
40+
if (prLabels.includes(bitsAiLabel)) {
41+
isAiGenerated = true
42+
// Remove label from the PR
43+
try {
44+
await github.rest.issues.removeLabel({
45+
owner, repo,
46+
issue_number: prNumber,
47+
name: bitsAiLabel
48+
})
49+
} catch (e) {
50+
core.warning(`Could not remove '${bitsAiLabel}' label from PR: ${e.message}`)
51+
}
52+
labelsStale = true
53+
// Delete label from the repository
54+
try {
55+
await github.rest.issues.deleteLabel({ owner, repo, name: bitsAiLabel })
56+
} catch (e) {
57+
core.warning(`Could not delete '${bitsAiLabel}' label from repo: ${e.message}`)
58+
}
59+
}
60+
61+
/*
62+
* Inspect commits for AI authorship signals.
63+
*/
64+
if (context.payload.pull_request.labels.some(l => l.name === aiGeneratedLabel)) {
65+
core.info(`PR #${prNumber} is already labeled as AI-generated, skipping commit scan.`)
66+
core.setOutput('labels_stale', String(labelsStale))
67+
return
68+
}
69+
const aiRegex = /\b(anthropic|chatgpt|codex|copilot|cursor|openai)\b/i
70+
const commits = await github.paginate(github.rest.pulls.listCommits, {
71+
owner, repo,
72+
pull_number: prNumber,
73+
per_page: 100
74+
})
75+
for (const { commit } of commits) {
76+
const authorName = commit.author?.name ?? ''
77+
const authorEmail = commit.author?.email ?? ''
78+
const committerName = commit.committer?.name ?? ''
79+
const committerEmail = commit.committer?.email ?? ''
80+
// Extract Co-authored-by trailer lines from commit message
81+
const coAuthors = (commit.message ?? '').split('\n')
82+
.filter(line => /^co-authored-by:/i.test(line.trim()))
83+
const fieldsToCheck = [authorName, authorEmail]
84+
// Skip GitHub's generic noreply for committer
85+
if (committerEmail !== 'noreply@github.com') {
86+
fieldsToCheck.push(committerName, committerEmail)
87+
}
88+
fieldsToCheck.push(...coAuthors)
89+
if (fieldsToCheck.some(field => aiRegex.test(field))) {
90+
isAiGenerated = true
91+
break
92+
}
93+
}
94+
95+
/*
96+
* Add 'tag: ai generated' label if AI-generated.
97+
*/
98+
if (isAiGenerated) {
99+
// Re-fetch labels only if they were modified above (Bits AI removal)
100+
let currentLabels
101+
if (labelsStale) {
102+
const { data: currentPr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber })
103+
currentLabels = currentPr.labels.map(l => l.name)
104+
} else {
105+
currentLabels = context.payload.pull_request.labels.map(l => l.name)
106+
}
107+
if (!currentLabels.includes(aiGeneratedLabel)) {
108+
try {
109+
await github.rest.issues.addLabels({
110+
owner, repo,
111+
issue_number: prNumber,
112+
labels: [aiGeneratedLabel]
113+
})
114+
core.info(`Added '${aiGeneratedLabel}' label to PR #${prNumber}`)
115+
} catch (e) {
116+
core.setFailed(`Could not add '${aiGeneratedLabel}' label to PR #${prNumber}: ${e.message}`)
117+
}
118+
}
119+
}
120+
core.setOutput('labels_stale', String(labelsStale))
121+
18122
- name: Check pull request labels
19123
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0
124+
env:
125+
LABELS_STALE: ${{ steps.flag_ai_generated.outputs.labels_stale }}
20126
with:
21127
github-token: ${{ secrets.GITHUB_TOKEN }}
22128
script: |
@@ -35,8 +141,20 @@ jobs:
35141
'performance:', // To refactor to 'ci: ' in the future
36142
'run-tests:' // Unused since GitLab migration
37143
]
144+
// Re-fetch labels only if the previous step modified them (ex: "Bits AI" removal)
145+
let prLabels
146+
if (process.env.LABELS_STALE === 'true') {
147+
const { data: currentPr } = await github.rest.pulls.get({
148+
owner: context.repo.owner,
149+
repo: context.repo.repo,
150+
pull_number: context.payload.pull_request.number
151+
})
152+
prLabels = currentPr.labels
153+
} else {
154+
prLabels = context.payload.pull_request.labels
155+
}
38156
// Look for invalid labels
39-
const invalidLabels = context.payload.pull_request.labels
157+
const invalidLabels = prLabels
40158
.map(label => label.name)
41159
.filter(label => validCategories.every(prefix => !label.startsWith(prefix)))
42160
const hasInvalidLabels = invalidLabels.length > 0

.gitlab/collect_results.sh

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,16 @@ do
7878
# Replace random port numbers by marker in testcase XML nodes to get stable test names
7979
sed -i '/<testcase/ s/localhost:[0-9]\{2,5\}/localhost:PORT/g' "$TARGET_DIR/$AGGREGATED_FILE_NAME"
8080

81-
# Add dd_tags[test.final_status] property to each testcase
82-
xsl_file="$(dirname "$0")/add_final_status.xsl"
83-
tmp_file="$(mktemp)"
84-
xsltproc --output "$tmp_file" "$xsl_file" "$TARGET_DIR/$AGGREGATED_FILE_NAME"
85-
mv "$tmp_file" "$TARGET_DIR/$AGGREGATED_FILE_NAME"
86-
8781
if cmp -s "$RESULT_XML_FILE" "$TARGET_DIR/$AGGREGATED_FILE_NAME"; then
8882
echo ""
8983
else
90-
echo -n " (non-stable test names detected)"
84+
echo " (non-stable test names detected)"
9185
fi
86+
87+
echo "Add dd_tags[test.final_status] property to each testcase on $TARGET_DIR/$AGGREGATED_FILE_NAME"
88+
xsl_file="$(dirname "$0")/add_final_status.xsl"
89+
tmp_file="$(mktemp)"
90+
xsltproc --huge --output "$tmp_file" "$xsl_file" "$TARGET_DIR/$AGGREGATED_FILE_NAME"
91+
mv "$tmp_file" "$TARGET_DIR/$AGGREGATED_FILE_NAME"
92+
9293
done < <(find "${TEST_RESULT_DIRS[@]}" -name \*.xml -print0)

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/ConfigurationUpdater.java

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@
2121
import datadog.trace.bootstrap.debugger.ProbeRateLimiter;
2222
import datadog.trace.relocate.api.RatelimitedLogger;
2323
import datadog.trace.util.TagsHelper;
24+
import java.lang.annotation.Annotation;
25+
import java.lang.annotation.ElementType;
26+
import java.lang.annotation.Target;
2427
import java.lang.instrument.Instrumentation;
28+
import java.lang.reflect.AnnotatedType;
29+
import java.lang.reflect.Array;
2530
import java.lang.reflect.Method;
31+
import java.lang.reflect.Modifier;
2632
import java.lang.reflect.Parameter;
2733
import java.util.ArrayList;
34+
import java.util.Arrays;
2835
import java.util.Collection;
2936
import java.util.Collections;
3037
import java.util.EnumMap;
@@ -44,8 +51,30 @@
4451
* re-transformation of required classes
4552
*/
4653
public class ConfigurationUpdater implements DebuggerContext.ProbeResolver, ConfigurationAcceptor {
47-
54+
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationUpdater.class);
55+
private static final int MINUTES_BETWEEN_ERROR_LOG = 5;
4856
private static final boolean JAVA_AT_LEAST_19 = JavaVirtualMachine.isJavaVersionAtLeast(19);
57+
private static final boolean JAVA_AT_LEAST_16 = JavaVirtualMachine.isJavaVersionAtLeast(16);
58+
private static final Method GET_RECORD_COMPONENTS_METHOD;
59+
private static final Method GET_ANNOTATED_TYPES_METHOD;
60+
61+
static {
62+
Method getRecordComponentsMethod = null;
63+
Method getAnnotatedTypesMethod = null;
64+
if (JAVA_AT_LEAST_16) {
65+
try {
66+
Class<?> recordClass = Class.forName("java.lang.Record", true, null);
67+
getRecordComponentsMethod = recordClass.getClass().getDeclaredMethod("getRecordComponents");
68+
Class<?> recordComponentClass =
69+
Class.forName("java.lang.reflect.RecordComponent", true, null);
70+
getAnnotatedTypesMethod = recordComponentClass.getDeclaredMethod("getAnnotatedType");
71+
} catch (Exception e) {
72+
LOGGER.debug("Exception initializing reflection constants", e);
73+
}
74+
}
75+
GET_RECORD_COMPONENTS_METHOD = getRecordComponentsMethod;
76+
GET_ANNOTATED_TYPES_METHOD = getAnnotatedTypesMethod;
77+
}
4978

5079
public interface TransformerSupplier {
5180
DebuggerTransformer supply(
@@ -56,9 +85,6 @@ DebuggerTransformer supply(
5685
DebuggerSink debuggerSink);
5786
}
5887

59-
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationUpdater.class);
60-
private static final int MINUTES_BETWEEN_ERROR_LOG = 5;
61-
6288
private final Instrumentation instrumentation;
6389
private final TransformerSupplier transformerSupplier;
6490
private final Lock configurationLock = new ReentrantLock();
@@ -185,6 +211,7 @@ private void handleProbesChanges(ConfigurationComparer changes, Configuration ne
185211
List<Class<?>> changedClasses =
186212
finder.getAllLoadedChangedClasses(instrumentation.getAllLoadedClasses(), changes);
187213
changedClasses = detectMethodParameters(changes, changedClasses);
214+
changedClasses = detectRecordWithTypeAnnotation(changes, changedClasses);
188215
retransformClasses(changedClasses);
189216
// ensures that we have at least re-transformed 1 class
190217
if (changedClasses.size() > 0) {
@@ -248,6 +275,65 @@ private List<Class<?>> detectMethodParameters(
248275
return result;
249276
}
250277

278+
private List<Class<?>> detectRecordWithTypeAnnotation(
279+
ConfigurationComparer changes, List<Class<?>> changedClasses) {
280+
if (!JAVA_AT_LEAST_16) {
281+
// records introduced in JDK 16 (final version)
282+
return changedClasses;
283+
}
284+
List<Class<?>> result = new ArrayList<>();
285+
for (Class<?> changedClass : changedClasses) {
286+
boolean addClass = true;
287+
try {
288+
if (changedClass.getSuperclass().getTypeName().equals("java.lang.Record")
289+
&& Modifier.isFinal(changedClass.getModifiers())) {
290+
if (hasTypeAnnotationOnRecordComponent(changedClass)) {
291+
LOGGER.debug(
292+
"Record with type annotation detected, instrumentation not supported for {}",
293+
changedClass.getTypeName());
294+
reportError(
295+
changes,
296+
"Record with type annotation detected, instrumentation not supported for "
297+
+ changedClass.getTypeName());
298+
addClass = false;
299+
}
300+
}
301+
} catch (Exception e) {
302+
LOGGER.debug("Exception detecting record with type annotation", e);
303+
}
304+
if (addClass) {
305+
result.add(changedClass);
306+
}
307+
}
308+
return result;
309+
}
310+
311+
private boolean hasTypeAnnotationOnRecordComponent(Class<?> recordClass) {
312+
if (GET_RECORD_COMPONENTS_METHOD == null || GET_ANNOTATED_TYPES_METHOD == null) {
313+
return false;
314+
}
315+
try {
316+
Object recordComponentsArray = GET_RECORD_COMPONENTS_METHOD.invoke(recordClass);
317+
int len = Array.getLength(recordComponentsArray);
318+
for (int i = 0; i < len; i++) {
319+
Object recordComponent = Array.get(recordComponentsArray, i);
320+
AnnotatedType annotatedType =
321+
(AnnotatedType) GET_ANNOTATED_TYPES_METHOD.invoke(recordComponent);
322+
for (Annotation annotation : annotatedType.getAnnotations()) {
323+
Target annotationTarget = annotation.annotationType().getAnnotation(Target.class);
324+
if (annotationTarget != null
325+
&& Arrays.stream(annotationTarget.value())
326+
.anyMatch(it -> it == ElementType.TYPE_USE)) {
327+
return true;
328+
}
329+
}
330+
}
331+
return false;
332+
} catch (Exception ex) {
333+
return false;
334+
}
335+
}
336+
251337
private void reportReceived(ConfigurationComparer changes) {
252338
for (ProbeDefinition def : changes.getAddedDefinitions()) {
253339
if (def instanceof ExceptionProbe) {

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerAgent.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,9 +371,15 @@ private static DebuggerSink createDebuggerSink(
371371
}
372372

373373
public static String getDefaultTagsMergedWithGlobalTags(Config config) {
374-
GitInfo gitInfo = GitInfoProvider.INSTANCE.getGitInfo();
375-
String gitSha = gitInfo.getCommit().getSha();
376-
String gitUrl = gitInfo.getRepositoryURL();
374+
String gitSha = null;
375+
String gitUrl = null;
376+
try {
377+
GitInfo gitInfo = GitInfoProvider.INSTANCE.getGitInfo();
378+
gitSha = gitInfo.getCommit().getSha();
379+
gitUrl = gitInfo.getRepositoryURL();
380+
} catch (Exception e) {
381+
LOGGER.error("Failed to retrieve git info: ", e);
382+
}
377383
String debuggerTags =
378384
TagsHelper.concatTags(
379385
"env:" + config.getEnv(),

dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import org.objectweb.asm.Type;
6868
import org.objectweb.asm.commons.JSRInlinerAdapter;
6969
import org.objectweb.asm.tree.ClassNode;
70+
import org.objectweb.asm.tree.FieldNode;
7071
import org.objectweb.asm.tree.MethodNode;
7172
import org.objectweb.asm.tree.analysis.Analyzer;
7273
import org.objectweb.asm.tree.analysis.AnalyzerException;
@@ -275,6 +276,9 @@ public byte[] transform(
275276
}
276277
ClassNode classNode = parseClassFile(classFilePath, classfileBuffer);
277278
checkMethodParameters(classNode);
279+
if (!checkRecordTypeAnnotation(classNode, definitions, fullyQualifiedClassName)) {
280+
return null;
281+
}
278282
boolean transformed =
279283
performInstrumentation(loader, fullyQualifiedClassName, definitions, classNode);
280284
if (transformed) {
@@ -333,6 +337,37 @@ private void checkMethodParameters(ClassNode classNode) {
333337
}
334338
}
335339

340+
/*
341+
* Because of this bug (https://bugs.openjdk.org/browse/JDK-8376185), when a record using a type
342+
* annotation is retransformed, the internal JVM representation of this record is corrupted
343+
* and lead to exception in best cases but in JVM crashes in worst cases.
344+
* Note: the bug happens only at retransform time and not instrumenting at load time. But the
345+
* fact we have already instrumented the record at load time, will prevent us to remove the
346+
* instrumentation because it needs a retransformation and will lead to corruption of the record
347+
*/
348+
private boolean checkRecordTypeAnnotation(
349+
ClassNode classNode, List<ProbeDefinition> definitions, String fullyQualifiedClassName) {
350+
if (!ASMHelper.isRecord(classNode)) {
351+
return true;
352+
}
353+
if (classNode.fields == null || classNode.fields.isEmpty()) {
354+
return true;
355+
}
356+
for (FieldNode field : classNode.fields) {
357+
if ((field.visibleTypeAnnotations != null && !field.visibleTypeAnnotations.isEmpty())
358+
|| (field.invisibleTypeAnnotations != null
359+
&& !field.invisibleTypeAnnotations.isEmpty())) {
360+
reportInstrumentationFails(
361+
definitions,
362+
fullyQualifiedClassName,
363+
"Instrumentation of a record with type annotation is not supported");
364+
return false;
365+
}
366+
}
367+
// no type annotation for components, not a problem
368+
return true;
369+
}
370+
336371
private boolean skipInstrumentation(ClassLoader loader, String classFilePath) {
337372
if (definitionMatcher.isEmpty()) {
338373
LOGGER.debug("No debugger definitions present.");

0 commit comments

Comments
 (0)