diff --git a/.config/LocalizationValidationAllowlist.json b/.config/LocalizationValidationAllowlist.json
new file mode 100644
index 0000000000..0a3cbfea6b
--- /dev/null
+++ b/.config/LocalizationValidationAllowlist.json
@@ -0,0 +1,110 @@
+{
+ "_comment": [
+ "These culture/key pairs intentionally match the English source text.",
+ "Each pair was verified in the internal LCL source as localized (Stat=Loc, Orig=New). Remove an entry when its localized value changes."
+ ],
+ "AllowedEnglishValueMatches": {
+ "Strings.cs.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_Data",
+ "DataCategory_InfoMessage",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SqlMisc_NullString"
+ ],
+ "Strings.de.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_Pooling",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId"
+ ],
+ "Strings.es.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SQL_ExErrorNumberStateClass"
+ ],
+ "Strings.fr.resx": [
+ "DataCategory_InfoMessage",
+ "DataCategory_Notification",
+ "DataCategory_Source",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SqlMisc_NullString"
+ ],
+ "Strings.it.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_InfoMessage",
+ "DataCategory_Pooling",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SQL_ExErrorNumberStateClass",
+ "SqlMisc_NullString"
+ ],
+ "Strings.ja.resx": [
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId"
+ ],
+ "Strings.ko.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_InfoMessage",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SQL_ExErrorNumberStateClass",
+ "SqlMisc_NullString"
+ ],
+ "Strings.pl.resx": [
+ "DataCategory_InfoMessage",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SqlMisc_NullString"
+ ],
+ "Strings.pt-BR.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_InfoMessage",
+ "DataCategory_Pooling",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SQL_ExErrorNumberStateClass"
+ ],
+ "Strings.ru.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_InfoMessage",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SQL_ExOriginalClientConnectionId"
+ ],
+ "Strings.tr.resx": [
+ "ADP_InvalidMultipartName",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SqlMisc_NullString"
+ ],
+ "Strings.zh-Hans.resx": [
+ "DataCategory_InfoMessage",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SQL_ExErrorNumberStateClass",
+ "SqlMisc_NullString"
+ ],
+ "Strings.zh-Hant.resx": [
+ "DataCategory_InfoMessage",
+ "DataCategory_StatementCompleted",
+ "DataCategory_Xml",
+ "SQL_ExClientConnectionId",
+ "SQL_ExErrorNumberStateClass",
+ "SqlMisc_NullString"
+ ]
+ }
+}
diff --git a/.config/PolicheckExclusions.xml b/.config/PolicheckExclusions.xml
index a4269513d1..e1129013ba 100644
--- a/.config/PolicheckExclusions.xml
+++ b/.config/PolicheckExclusions.xml
@@ -1,5 +1,5 @@
SRC/MICROSOFT.DATA.SQLCLIENT/TESTS.YML|.MD|.SQL
- NOTICE.TXT|SQLDATAADAPTER.CS
+ NOTICE.TXT|SQLDATAADAPTER.CS|SQLDATAADAPTER.XML
\ No newline at end of file
diff --git a/.config/guardian/.gdnbaselines b/.config/guardian/.gdnbaselines
new file mode 100644
index 0000000000..e5c5ff89b2
--- /dev/null
+++ b/.config/guardian/.gdnbaselines
@@ -0,0 +1,787 @@
+{
+ "hydrated": false,
+ "properties": {
+ "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/baselines"
+ },
+ "version": "1.0.0",
+ "baselines": {
+ "default": {
+ "name": "default",
+ "createdDate": "2026-07-23 11:29:23Z",
+ "lastUpdatedDate": "2026-08-28 14:33:24Z"
+ }
+ },
+ "results": {
+ "40b23e076c5c65b58c7da0889f0bf58e271b1637f662060bbc7aadd5c62a7126": {
+ "signature": "40b23e076c5c65b58c7da0889f0bf58e271b1637f662060bbc7aadd5c62a7126",
+ "alternativeSignatures": [
+ "4113c2e383bc4d7206cc94ba3e16dcdaef105762c7e22060d8e7964407e048ab",
+ "41bc93496e4ace0362f89323cc166b46e7448108052b75f7dd3ae5f4d48b5ad8",
+ "357246d2ddf96dbab309e392e49d088dc5c15ec9343553876173296f02eef94c"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "6e0dddb2d631181c20dbbf2892aceea3fbb157b3c293a147b70f49fc4a17765b": {
+ "signature": "6e0dddb2d631181c20dbbf2892aceea3fbb157b3c293a147b70f49fc4a17765b",
+ "alternativeSignatures": [
+ "2a9d1465faa5347c1f77eb856eb21074dc1dfcca4963991fb918fa61c1c29b02",
+ "9f71983dfef73a0b5fa1ecd3d1f14b3af9067aed39c5eb8619ecd0a0f4ee33f6",
+ "bd1475a089ab85c698803c80d71e73148edfc9db706bafe3789509ea32f917f2"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "28f157377f5600a0ce77a1a2193653ca71c8de4cca5d24166e1363cfc1f53782": {
+ "signature": "28f157377f5600a0ce77a1a2193653ca71c8de4cca5d24166e1363cfc1f53782",
+ "alternativeSignatures": [
+ "ff91ec920d3ed908c7fdc8419f4e767216c312a3907419b65e8e11e611be0db6",
+ "cc5e7e0d3670c5b2738f2b9194d50de2b123bdae5e906729f3a9c7baa980477d",
+ "8641e063e4aff04c260660c01c71ee18d32bdb7788357eaecc420a64cb279b2d"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "253df4b6cde68fb64c0376d3fa5351a49ccd9136e987129a6686f003303902bb": {
+ "signature": "253df4b6cde68fb64c0376d3fa5351a49ccd9136e987129a6686f003303902bb",
+ "alternativeSignatures": [
+ "e98757960f13dfe3c2c4ef2acbbdbd782cf26a7635caae25b7fd9e9230f863f6",
+ "df606344d52fe66ab4fa386e5cc50b6ced2c8d711aa7bc5a14db61f1fad7b70f",
+ "61239d49bfbe4702066454a51c302633291f61542011ee755420f4f556ede2d6"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "40b7b29173191dd629e8921c99d8718cad7ebf4d6acd83d8719e2e15db3d89a3": {
+ "signature": "40b7b29173191dd629e8921c99d8718cad7ebf4d6acd83d8719e2e15db3d89a3",
+ "alternativeSignatures": [
+ "870b4398ab01cdfaa4be8605e03eaa7377a5079b79573d3290248f5d5fd6fd6c",
+ "6b690695e0e41aa7a7c79394fa5d9f6a08e6e06cffc326b7fae4a8883ff42ac6",
+ "514ed34ad14af5b3739b2804e791b1b63d88be42e658238794f467c4bf2ff140"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "fb0afe300269068323c4e3fab292104aa30201c545156ffee1ccf6109df3bd59": {
+ "signature": "fb0afe300269068323c4e3fab292104aa30201c545156ffee1ccf6109df3bd59",
+ "alternativeSignatures": [
+ "7bdac882cf7914eb42130101ca921f1e4c5654cc55916daf1c2b9bc51c439dfa",
+ "6e99b53e08dcbad4533fbd8a101b881fba112f31483c7e9b7b14aaa709f3cee8",
+ "fc771d2d8a5bd2dfa72695e42516ff6c504e18fd714a1462d911539ea69fe710"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "e3ec17e281386f24ee7650c502b034993dcfd9a5ad19089c9af3edde8321fbb9": {
+ "signature": "e3ec17e281386f24ee7650c502b034993dcfd9a5ad19089c9af3edde8321fbb9",
+ "alternativeSignatures": [
+ "0910ddd1f1c9f37b764457a39d0dd543f25fce6e5e8694f066d0092e99784b4d",
+ "ee166a838d6e6b4aa08218eb89b001689bc3ec8fb6343fdfee89834847a294d5",
+ "7beed57053ec779f46498ff84c41a9400761001ea494237fe32cfab1c829454a"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "0ef75f3e883bfae828f2b15b4a41963e6ea3a61306fc19767586044309bc739f": {
+ "signature": "0ef75f3e883bfae828f2b15b4a41963e6ea3a61306fc19767586044309bc739f",
+ "alternativeSignatures": [
+ "7e71c00bf08e29508821cc200dcc41556ecc570c205f8833a5a704466e80baa5",
+ "0d5b851e97bdb0eb8931e6f326718a657f9cd46092eb8a2a2d414be2147a797c",
+ "e6c0cd6ef2433a42c95a2939cce740019ecda3fcfde64a3a0f21661e6ff27f71"
+ ],
+ "target": "src/Microsoft.Data.SqlClient/tests/ManualTests/makepfxcert.ps1",
+ "line": 145,
+ "uriBaseId": "file:///D:/a/_work/1/s/",
+ "memberOf": [
+ "default"
+ ],
+ "tool": "psscriptanalyzer",
+ "ruleId": "PSAvoidUsingConvertToSecureStringWithPlainText",
+ "createdDate": "2026-08-28 12:58:19Z"
+ },
+ "27cf35f7df3f630fab489573ec19318f563e042424ca30625e9fe08407d74bdf": {
+ "signature": "27cf35f7df3f630fab489573ec19318f563e042424ca30625e9fe08407d74bdf",
+ "alternativeSignatures": [
+ "55e9029f79f883ee7e23a98ee6aad0c3713e02dd67cd7a7158875cdee4c86806",
+ "a443d3867a75710110ddf675eb639080559e694d4331fd89bc1589b1a60b8777",
+ "ff040e311fa90fc602c647b539f52148605321c178fc731f30395a8a9ef3f7a9"
+ ],
+ "memberOf": [
+ "default"
+ ],
+ "createdDate": "2026-07-23 11:29:23Z"
+ },
+ "1e0989a7cdd65afb10dd3787a2ed33e9f737e6dd049524edbf76ba1daadf6ef8": {
+ "signature": "1e0989a7cdd65afb10dd3787a2ed33e9f737e6dd049524edbf76ba1daadf6ef8",
+ "alternativeSignatures": [
+ "47067564034219f2cf40604fcd1beadb34ebe1b960cc75600796c8a0f4565604"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider/src/Utils.cs",
+ "line": 72,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 14:17:20Z"
+ },
+ "e5cd66384b36741191c98844947e6b857140c09b2c352b180664f8b4829c3e4c": {
+ "signature": "e5cd66384b36741191c98844947e6b857140c09b2c352b180664f8b4829c3e4c",
+ "alternativeSignatures": [
+ "07fc741e29b6f1d01d84d3290b8c53dc7f22036a8313d1f41acdca253aa3e254"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Resources/StringsHelper.cs",
+ "line": 90,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "281a106076dd3d70aefcd230a90cef542f52488fb3ce164c94b213ededa12da5": {
+ "signature": "281a106076dd3d70aefcd230a90cef542f52488fb3ce164c94b213ededa12da5",
+ "alternativeSignatures": [
+ "42cf7833cc5d63447162200f8926b57746ad3f6f2bd43318f0e606f022933c3e"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs",
+ "line": 215,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "fcda2db01b63d59f5a4aa73cb780405887f4616f26f9e23dfae52195cab4994e": {
+ "signature": "fcda2db01b63d59f5a4aa73cb780405887f4616f26f9e23dfae52195cab4994e",
+ "alternativeSignatures": [
+ "9134dead4de902cc02f5f2e7785d84b422f31847f237ba6bcbaeb823e228fd7d"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs",
+ "line": 170,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "4ff2eb551fd17b4d3239b89cf97bfb09507cdfb631cb4787adc3a4f195e8bac7": {
+ "signature": "4ff2eb551fd17b4d3239b89cf97bfb09507cdfb631cb4787adc3a4f195e8bac7",
+ "alternativeSignatures": [
+ "3b7985cbb5123ba5b91edc3e36a952d1f9d579943820f3c3e870095c7e264cc4"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/NoneAttestationEnclaveProvider.cs",
+ "line": 43,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "8f3e9755a800f590583d9e7ea2028c5d23ee2008450af7d55daea17d5fbecfa5": {
+ "signature": "8f3e9755a800f590583d9e7ea2028c5d23ee2008450af7d55daea17d5fbecfa5",
+ "alternativeSignatures": [
+ "4531f356921a98edacfca7c32eb389b459c02014ea3db2d2d899ce65a87d52ca"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAeadAes256CbcHmac256EncryptionKey.cs",
+ "line": 94,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "6ba87c464ec17dc52c0304f5cee224f8d3233507a79211916901a7b9d0d0908c": {
+ "signature": "6ba87c464ec17dc52c0304f5cee224f8d3233507a79211916901a7b9d0d0908c",
+ "alternativeSignatures": [
+ "fcd1d28e2fe4772861caa303138474dc79869cc77079f79b55eda1612a4c4693"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs",
+ "line": 353,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "ffe242f34f321ccf4d2e727c9812baaf006e4ad122d314e02dd287f388b5b176": {
+ "signature": "ffe242f34f321ccf4d2e727c9812baaf006e4ad122d314e02dd287f388b5b176",
+ "alternativeSignatures": [
+ "ca2f6e8136bafc65dc12eb6236123ee0877de7a1b48e810c36b7feae643b9654"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs",
+ "line": 1049,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "b73abd622849352bbe6c5188f106d4a2f9a1c4f650b7d2ba8f383653909a9479": {
+ "signature": "b73abd622849352bbe6c5188f106d4a2f9a1c4f650b7d2ba8f383653909a9479",
+ "alternativeSignatures": [
+ "636e8fa98391919cfbd7f27792cac5c7806dbd7ec3ab0506b27fbbe52ee9cd8b"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientPermission.netfx.cs",
+ "line": 144,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "cdbeec94358c58eb2960ea291f7f804e5d24e15807bc5b4a5ce259782727cd9a": {
+ "signature": "cdbeec94358c58eb2960ea291f7f804e5d24e15807bc5b4a5ce259782727cd9a",
+ "alternativeSignatures": [
+ "8fa2f809347c794dbb3659a25cb2ea3a15df3a64a86f5a78f72b39c63f6b8319"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs",
+ "line": 2336,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "01c8d80a7268d44c7d8e478f887e804d8819cc2382ad1d845a415d97ead39d2e": {
+ "signature": "01c8d80a7268d44c7d8e478f887e804d8819cc2382ad1d845a415d97ead39d2e",
+ "alternativeSignatures": [
+ "00aae68e847dfaed6240e67d9f0d1f5f64b9a0343c185c69f7dae148ebf2dc35"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionEncryptOption.cs",
+ "line": 57,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1304",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "5ef60ae6fdf13d9fcebe7631af27f7ea23d3f198215a19c8752bf5f8cdee8dc9": {
+ "signature": "5ef60ae6fdf13d9fcebe7631af27f7ea23d3f198215a19c8752bf5f8cdee8dc9",
+ "alternativeSignatures": [
+ "17d62daf6be556a78b7a342be79f5038d7f1831851722a69398b996cecd57bd8"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionEncryptOption.cs",
+ "line": 108,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "f99754da199aaa7a5e510552f0b4e851029c149dced1d5196eba374b03f0450d": {
+ "signature": "f99754da199aaa7a5e510552f0b4e851029c149dced1d5196eba374b03f0450d",
+ "alternativeSignatures": [
+ "2852f83af1a5eacc738153cae383e7e216179f3573e9082879dddb48e32a260c"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.cs",
+ "line": 1638,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "3dc0ef1e00dd1aed1bd9b6a2e9c06c4f8b24bf368419a2928feaab51252a3b47": {
+ "signature": "3dc0ef1e00dd1aed1bd9b6a2e9c06c4f8b24bf368419a2928feaab51252a3b47",
+ "alternativeSignatures": [
+ "6a46ad5f8328cb647c28327bb4260335dab6e381ee816f60e371f3f3c4f348b0"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.cs",
+ "line": 1640,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "d0e489f06819d4a2e78e99be8aedb5d683f8c787cfc07d5c25689bc4e4e3f5f6": {
+ "signature": "d0e489f06819d4a2e78e99be8aedb5d683f8c787cfc07d5c25689bc4e4e3f5f6",
+ "alternativeSignatures": [
+ "524203b79cc017dc30443712f932710574a9a39d5f7251a4c9354f3cdd21b36b"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.Debug.cs",
+ "line": 59,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "6d1f24e834de7bf17aa69abdd0fe79751a4afcc083253e84b577c813a19f46fb": {
+ "signature": "6d1f24e834de7bf17aa69abdd0fe79751a4afcc083253e84b577c813a19f46fb",
+ "alternativeSignatures": [
+ "9b805fc1d43486b21ec75d68dd2e4b19d5e7af7c954c546389307bd689a2930b"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnectionOptions.Debug.cs",
+ "line": 59,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1304",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "d245c0ad54d0229b6ea7a194bd42c40011b734ee5ef92dfce42c9a40096c906d": {
+ "signature": "d245c0ad54d0229b6ea7a194bd42c40011b734ee5ef92dfce42c9a40096c906d",
+ "alternativeSignatures": [
+ "472b266989720cacdc2666a97234830e954f84346ba13ee028d9f0dbac3d5d82"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDataReader.cs",
+ "line": 2801,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "68fc925a125fdb6381d42edaaae658cb5c23c5bbef262cbb569215d839e3a9b3": {
+ "signature": "68fc925a125fdb6381d42edaaae658cb5c23c5bbef262cbb569215d839e3a9b3",
+ "alternativeSignatures": [
+ "1a6475a1a8210fd0e0d4807a4c5d4e1017da257dbfa97b17c8e3382c1684a34a"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDependency.cs",
+ "line": 644,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA2219",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "df9958d713606f4b2d800a8f5b33e4839cde7d9e7514f732e0b999f4e7df0cb0": {
+ "signature": "df9958d713606f4b2d800a8f5b33e4839cde7d9e7514f732e0b999f4e7df0cb0",
+ "alternativeSignatures": [
+ "b44a5e32ae614b1bdda97a1a634dad5a4ab4ede29b463dd3196f309db10e1d15"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlDependency.cs",
+ "line": 1222,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "e318e84cf65f756c454457b437010a21fac2f27f2fa7378c3befc809b97369be": {
+ "signature": "e318e84cf65f756c454457b437010a21fac2f27f2fa7378c3befc809b97369be",
+ "alternativeSignatures": [
+ "d8118425a1409e87f5983a840ac22cc663a7ab494bb914b312f7b32adb84d5f5"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlEnums.cs",
+ "line": 1134,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "467568c2128c495889d899ef783ccca652f3b355aa8fe815d4da1b55b6deab95": {
+ "signature": "467568c2128c495889d899ef783ccca652f3b355aa8fe815d4da1b55b6deab95",
+ "alternativeSignatures": [
+ "8962958e2e5e468cc56038a04d02476e352bf6f1d48104f51c13990d364b0c64"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs",
+ "line": 164,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "727c936839845e9a68ff00fbc69abb5a132769506d3cfe87692612a2addac855": {
+ "signature": "727c936839845e9a68ff00fbc69abb5a132769506d3cfe87692612a2addac855",
+ "alternativeSignatures": [
+ "2450ed8367a7fac65b41524a145bf097f2dee5794752124fefa3a635057946bd"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs",
+ "line": 114,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "8c6b44ec1f44fbe00469a48beb3f9eee61c8e31e237c53ba372a1abfc5ea481f": {
+ "signature": "8c6b44ec1f44fbe00469a48beb3f9eee61c8e31e237c53ba372a1abfc5ea481f",
+ "alternativeSignatures": [
+ "7a3c05145c302720ca6feadabcacbb11303442b2289fb06796e21b713fd63717"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlMetaDataFactory.cs",
+ "line": 572,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "8a5592e024f45a9bdce3315e108cccfafe99b184dbcb4e50d59d783fa3db3942": {
+ "signature": "8a5592e024f45a9bdce3315e108cccfafe99b184dbcb4e50d59d783fa3db3942",
+ "alternativeSignatures": [
+ "929389afe5818dcc5113299f0e0263eca26ef989785742ac7e46b088d5cae598"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs",
+ "line": 2364,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "770e1d8ef5a06c9b4b552dc9c8bf000f572d633bf11ded7e749b3ac9c07d208b": {
+ "signature": "770e1d8ef5a06c9b4b552dc9c8bf000f572d633bf11ded7e749b3ac9c07d208b",
+ "alternativeSignatures": [
+ "4cd0e3d7087eaa0eaf05bec58d32fd71b660033f7bf3984392998d0b9946fb47"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs",
+ "line": 389,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "8502b61241cdbfac98f7c84b8c16d4d4ec0ea2185018d9fce710e2bac445e8b0": {
+ "signature": "8502b61241cdbfac98f7c84b8c16d4d4ec0ea2185018d9fce710e2bac445e8b0",
+ "alternativeSignatures": [
+ "afed061c02d7b602c516f09952197b58c6c1cdfa0e07ee3625af48287869b2c5"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs",
+ "line": 1749,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "a83b0d426733a42a34b4569643e60513b598035d6807fa8cda6f4a3501084929": {
+ "signature": "a83b0d426733a42a34b4569643e60513b598035d6807fa8cda6f4a3501084929",
+ "alternativeSignatures": [
+ "3614422f0f09ab4fff1a0195a4f40acd53bf55e4781153ced9d736a801fc6aa4"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs",
+ "line": 1875,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "d36be8dd5189d09c4b5db9176bda7628839f0ba608f31ba449f1203ca9c30c09": {
+ "signature": "d36be8dd5189d09c4b5db9176bda7628839f0ba608f31ba449f1203ca9c30c09",
+ "alternativeSignatures": [
+ "fcf7dc6efdfecd535087f2361d43cfa599cbe83b52717beb36c052dadbbc5a5d"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs",
+ "line": 2309,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "b19ca78715cc12a2051b38a495f41c060259e46d893be38e6dba447e8a87cc02": {
+ "signature": "b19ca78715cc12a2051b38a495f41c060259e46d893be38e6dba447e8a87cc02",
+ "alternativeSignatures": [
+ "efc4d3f86b80b0d8392e7fa74b15ca51411078ed8f249b0f30911036530bc0b4"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParser.cs",
+ "line": 3918,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1304",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "119b9514aeb84d5e34b5d7b1520378d4a2d017902d7d1bd572404f1b3186c59c": {
+ "signature": "119b9514aeb84d5e34b5d7b1520378d4a2d017902d7d1bd572404f1b3186c59c",
+ "alternativeSignatures": [
+ "57389b1211c68b1c8ed21fb3439d1dd9bd8a8e25677ed63b1db38ff7bd3b0c6b"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs",
+ "line": 4403,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "533d9dc25ef8183859aeed955b720d435e3035702b3b7c10d12204170815ec04": {
+ "signature": "533d9dc25ef8183859aeed955b720d435e3035702b3b7c10d12204170815ec04",
+ "alternativeSignatures": [
+ "55c532f39c15ec069c540c661f1ccb7371ee8c03f74fad4515d9cd94db8bcca1"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs",
+ "line": 100,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "90dd8eeec55b68eaf94f971d1af06723d22efc0f7cb5fd15870765e6e6602ff9": {
+ "signature": "90dd8eeec55b68eaf94f971d1af06723d22efc0f7cb5fd15870765e6e6602ff9",
+ "alternativeSignatures": [
+ "ac02e05713e85c6086fc30c51a7083a159329f0a222c9d0be513e6da9fda0300"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProvider.cs",
+ "line": 84,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "f1c711a9df14d19ac6682356d6648111a1e3c2ae051812e8f04a7466df045aa4": {
+ "signature": "f1c711a9df14d19ac6682356d6648111a1e3c2ae051812e8f04a7466df045aa4",
+ "alternativeSignatures": [
+ "1e752a5b4246f05f58da2adde778eb3eb46238ad51a6a2013d3fd9d80aaf5422"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs",
+ "line": 488,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "d159ab04bf10a650f1f43064602246acfa671e4f1f7ba07faf5620ab13043926": {
+ "signature": "d159ab04bf10a650f1f43064602246acfa671e4f1f7ba07faf5620ab13043926",
+ "alternativeSignatures": [
+ "74e1a9e9972040959e4176eb8dc981d8394545eaf86f30e059f9814f545bea67"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/ConnectionString/DbConnectionString.netfx.cs",
+ "line": 374,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "761ae24086cbfbeb9667e20ab45b174613bd156d0dc7bde56a37571db11cd779": {
+ "signature": "761ae24086cbfbeb9667e20ab45b174613bd156d0dc7bde56a37571db11cd779",
+ "alternativeSignatures": [
+ "1793d6a0e1d5ee495ced5ce18af4bb1c6f1635ce89d6dfe9d840b84673ccbe7a"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Connection/SqlConnectionInternal.cs",
+ "line": 4130,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "92759af0d8bc9a56339a09bfb5ae1e0adbe2c0aa7697911d0f14941ba65a1e67": {
+ "signature": "92759af0d8bc9a56339a09bfb5ae1e0adbe2c0aa7697911d0f14941ba65a1e67",
+ "alternativeSignatures": [
+ "f20cbad7a6ed702f47f5d26f6e56844ce9f1e5a7d03152361b1818d4aef03058"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ConnectionPool/DbConnectionPoolAuthenticationContextKey.cs",
+ "line": 83,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "8409f840dce0e042b55250ce12045afaabc1e2e48f443e43ec0e493517e3e38d": {
+ "signature": "8409f840dce0e042b55250ce12045afaabc1e2e48f443e43ec0e493517e3e38d",
+ "alternativeSignatures": [
+ "b607bb6c9aded6c9d9d1da95e9f49b9a17ec42880fe59156cfca1d2f088fec35"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniCommon.netcore.cs",
+ "line": 148,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "1152991aab5d089b6b086e389b075de228b319c82a577dae0afe850956202b12": {
+ "signature": "1152991aab5d089b6b086e389b075de228b319c82a577dae0afe850956202b12",
+ "alternativeSignatures": [
+ "95a2b83edbb49524b5834cba58e1c1670ad5397cca2bc8ed4912f02c7b885acd"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs",
+ "line": 150,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "66052c0cd9b0dade1d5d86703650fa9c8d46f7977b60148aea67f371393e32b3": {
+ "signature": "66052c0cd9b0dade1d5d86703650fa9c8d46f7977b60148aea67f371393e32b3",
+ "alternativeSignatures": [
+ "d67c06a92599b72202abeed718e385e4621947195fc3affc730a1066b55f5cb6"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniProxy.netcore.cs",
+ "line": 731,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "f236b2c674fc8574908a9b254e10d9a78f4df86278d9cfe52f5b4d072689829b": {
+ "signature": "f236b2c674fc8574908a9b254e10d9a78f4df86278d9cfe52f5b4d072689829b",
+ "alternativeSignatures": [
+ "fabdb2e276df6d043b029af9a40c831c8f9f7131dec81706dd664cd5d913f32e"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SniTcpHandle.netcore.cs",
+ "line": 658,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "424ec1510b6c1352e0731f19bd7a6c03f1da3bab869fa520046b0080cb7b5d70": {
+ "signature": "424ec1510b6c1352e0731f19bd7a6c03f1da3bab869fa520046b0080cb7b5d70",
+ "alternativeSignatures": [
+ "4dac74c5b9483f0a4b1f7e76f91002aa485fd1b83b19d5823169b0c15eaace8b"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ManagedSni/SsrpClient.netcore.cs",
+ "line": 80,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "76482116b8cae39bdad9b92782ab216ff958f6578405f93879733326a4283bfe": {
+ "signature": "76482116b8cae39bdad9b92782ab216ff958f6578405f93879733326a4283bfe",
+ "alternativeSignatures": [
+ "a815e32ff0ad176d42719f84cb015df61c89e9539fecbf58da169993e789b682"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Reliability/SqlConfigurableRetryLogicLoader.cs",
+ "line": 308,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:55:54Z"
+ },
+ "dd455bea13e4bd70696d1b0f4c890ca1ea42e18643bbe6b572989d84ac188f06": {
+ "signature": "dd455bea13e4bd70696d1b0f4c890ca1ea42e18643bbe6b572989d84ac188f06",
+ "alternativeSignatures": [
+ "0444dab1ebebce0b0e0bfb453d8513e9e8108aad22055bd038c873a8a8bc1f0e"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs",
+ "line": 629,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1309",
+ "createdDate": "2026-08-28 13:36:06Z"
+ },
+ "09526af76b9a6ad21a4841ea50bb51e72925789c5f0f733650ac946cc44060a9": {
+ "signature": "09526af76b9a6ad21a4841ea50bb51e72925789c5f0f733650ac946cc44060a9",
+ "alternativeSignatures": [
+ "2a3fb00242d533ee3253b91c42ba7882b9c35ada1635e3470c481c6b43baa0e6"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.SqlServer.Server/SqlUserDefinedAggregateAttribute.netstandard.cs",
+ "line": 61,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:10:07Z"
+ },
+ "6fec84f5f9e3087c484965319896c91b033557b8bcef7761b0ae74e1baf0ad7d": {
+ "signature": "6fec84f5f9e3087c484965319896c91b033557b8bcef7761b0ae74e1baf0ad7d",
+ "alternativeSignatures": [
+ "efa9c07656c41d1d2367a89a7146fda28f3b584e2654693b4954d40bfafecb8f"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.SqlServer.Server/SqlUserDefinedTypeAttribute.netstandard.cs",
+ "line": 73,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:10:07Z"
+ },
+ "1de5740b9e122c2d8bda4bfe66c94764a6192376f753a7234bac91e1fc28e5f6": {
+ "signature": "1de5740b9e122c2d8bda4bfe66c94764a6192376f753a7234bac91e1fc28e5f6",
+ "alternativeSignatures": [
+ "14c7177139597c2ab94cf632b76bc03c4f2c252691b30a7ba69957e047b400c4"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.SqlServer.Server/StringsHelper.netstandard.cs",
+ "line": 130,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:10:07Z"
+ },
+ "0ca59be802da38e82370950b24300deae99834aa9a3cee38e064d1f17c5942a9": {
+ "signature": "0ca59be802da38e82370950b24300deae99834aa9a3cee38e064d1f17c5942a9",
+ "alternativeSignatures": [
+ "26425ecc80bd3289865ce79ae6471c4ab21cde25acc1cf2067717fbf6d600a6a"
+ ],
+ "target": "file:///C:/__w/1/s/src/Microsoft.Data.SqlClient.Internal/Logging/src/SqlClientEventSource.cs",
+ "line": 1995,
+ "memberOf": [
+ "default"
+ ],
+ "tool": "roslynanalyzers",
+ "ruleId": "CA1305",
+ "createdDate": "2026-08-28 13:09:32Z"
+ }
+ }
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index c424e37834..db5f1c5da5 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -16,7 +16,7 @@
"ms-mssql.mssql"
],
"settings": {
- "dotnet.defaultSolution": "src/Microsoft.Data.SqlClient.sln"
+ "dotnet.defaultSolution": "src/Microsoft.Data.SqlClient.slnx"
}
}
},
diff --git a/.devcontainer/setup-sqlserver.sh b/.devcontainer/setup-sqlserver.sh
index 6b4ee480ed..fc962c3053 100755
--- a/.devcontainer/setup-sqlserver.sh
+++ b/.devcontainer/setup-sqlserver.sh
@@ -29,11 +29,11 @@ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# Write test config file so the test suite can find the connection string.
CONFIG_DIR="${REPO_ROOT}/src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities"
-CONFIG_DEFAULT_FILE="${CONFIG_DIR}/config.default.json"
-CONFIG_FILE="${CONFIG_DIR}/config.json"
+CONFIG_DEFAULT_FILE="${CONFIG_DIR}/config.default.jsonc"
+CONFIG_FILE="${CONFIG_DIR}/config.jsonc"
echo "Writing test config to ${CONFIG_FILE} (based on ${CONFIG_DEFAULT_FILE})..."
TCP_CONN_STR="Data Source=tcp:${SQL_HOST},${SQL_PORT};Database=Northwind;User Id=sa;Password=${SA_PASSWORD};Encrypt=false;TrustServerCertificate=true"
-# config.default.json contains JS-style comments (// ...) which are not valid JSON.
+# config.default.jsonc contains JS-style comments (// ...) which are not valid JSON.
# Strip single-line comments before feeding to jq.
sed 's|//.*||' "${CONFIG_DEFAULT_FILE}" \
| jq --arg cs "${TCP_CONN_STR}" \
diff --git a/.editorconfig b/.editorconfig
index 819a41fe1e..1864df162c 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -197,4 +197,4 @@ dotnet_diagnostic.CA1416.severity = silent
dotnet_code_quality.CA2100.excluded_type_names_with_derived_types = Microsoft.Data.SqlClient.ManualTesting.Tests.*
dotnet_diagnostic.xUnit1031.severity=none
-dotnet_diagnostic.xUnit1030.severity=none
+dotnet_diagnostic.xUnit1030.severity=none
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
index 1ff0c42304..ab4f136a23 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,63 +1,5 @@
-###############################################################################
-# Set default behavior to automatically normalize line endings.
-###############################################################################
+# Normalize line endings
* text=auto
-###############################################################################
-# Set default behavior for command prompt diff.
-#
-# This is need for earlier builds of msysgit that does not have it on by
-# default for csharp files.
-# Note: This is only used by command line
-###############################################################################
-#*.cs diff=csharp
-
-###############################################################################
-# Set the merge driver for project and solution files
-#
-# Merging from the command prompt will add diff markers to the files if there
-# are conflicts (Merging from VS is not affected by the settings below, in VS
-# the diff markers are never inserted). Diff markers may cause the following
-# file extensions to fail to load in VS. An alternative would be to treat
-# these files as binary and thus will always conflict and require user
-# intervention with every merge. To do so, just uncomment the entries below
-###############################################################################
-#*.sln merge=binary
-#*.csproj merge=binary
-#*.vbproj merge=binary
-#*.vcxproj merge=binary
-#*.vcproj merge=binary
-#*.dbproj merge=binary
-#*.fsproj merge=binary
-#*.lsproj merge=binary
-#*.wixproj merge=binary
-#*.modelproj merge=binary
-#*.sqlproj merge=binary
-#*.wwaproj merge=binary
-
-###############################################################################
-# behavior for image files
-#
-# image files are treated as binary by default.
-###############################################################################
-#*.jpg binary
-#*.png binary
-#*.gif binary
-
-###############################################################################
-# diff behavior for common document formats
-#
-# Convert binary document formats to text before diffing them. This feature
-# is only available from the command line. Turn it on by uncommenting the
-# entries below.
-###############################################################################
-#*.doc diff=astextplain
-#*.DOC diff=astextplain
-#*.docx diff=astextplain
-#*.DOCX diff=astextplain
-#*.dot diff=astextplain
-#*.DOT diff=astextplain
-#*.pdf diff=astextplain
-#*.PDF diff=astextplain
-#*.rtf diff=astextplain
-#*.RTF diff=astextplain
+# Treat workflow lock files as generated
+.github/workflows/*.lock.yml linguist-generated=true
diff --git a/.github/agents/agentic-workflows.md b/.github/agents/agentic-workflows.md
new file mode 100644
index 0000000000..b824e60580
--- /dev/null
+++ b/.github/agents/agentic-workflows.md
@@ -0,0 +1,226 @@
+---
+name: Agentic Workflows
+description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing.
+disable-model-invocation: true
+---
+
+# GitHub Agentic Workflows Agent
+
+This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files.
+
+## What This Agent Does
+
+This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task:
+
+- **Creating new workflows**: Routes to `create` prompt
+- **Updating existing workflows**: Routes to `update` prompt
+- **Debugging workflows**: Routes to `debug` prompt
+- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt
+- **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments
+- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt
+- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes
+- **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs
+- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions
+- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command
+- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments
+- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows
+
+Workflows may optionally include:
+
+- **Project tracking / monitoring** (GitHub Projects updates, status reporting)
+- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows)
+
+## Files This Applies To
+
+- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md`
+- Workflow lock files: `.github/workflows/*.lock.yml`
+- Shared components: `.github/workflows/shared/*.md`
+- Configuration: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md`
+
+## Problems This Solves
+
+- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions
+- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues
+- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes
+- **Component Design**: Create reusable shared workflow components that wrap MCP servers
+
+## How to Use
+
+When you interact with this agent, it will:
+
+1. **Understand your intent** - Determine what kind of task you're trying to accomplish
+2. **Route to the right prompt** - Load the specialized prompt file for your task
+3. **Execute the task** - Follow the detailed instructions in the loaded prompt
+
+## Available Prompts
+
+> **Note**: The prompt and reference files listed below are located in the [`github/gh-aw`](https://github.com/github/gh-aw) repository and are **not available locally** in this repository. Load them from their public URLs.
+
+### Create New Workflow
+**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-agentic-workflow.md`
+
+**Use cases**:
+- "Create a workflow that triages issues"
+- "I need a workflow to label pull requests"
+- "Design a weekly research automation"
+
+### Update Existing Workflow
+**Load when**: User wants to modify, improve, or refactor an existing workflow
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/update-agentic-workflow.md`
+
+**Use cases**:
+- "Add web-fetch tool to the issue-classifier workflow"
+- "Update the PR reviewer to use discussions instead of issues"
+- "Improve the prompt for the weekly-research workflow"
+
+### Debug Workflow
+**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/debug-agentic-workflow.md`
+
+**Use cases**:
+- "Why is this workflow failing?"
+- "Analyze the logs for workflow X"
+- "Investigate missing tool calls in run #12345"
+
+### Upgrade Agentic Workflows
+**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/upgrade-agentic-workflows.md`
+
+**Use cases**:
+- "Upgrade all workflows to the latest version"
+- "Fix deprecated fields in workflows"
+- "Apply breaking changes from the new release"
+
+### Create a Report-Generating Workflow
+**Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/report.md`
+
+**Use cases**:
+- "Create a weekly CI health report"
+- "Post a daily security audit to Discussions"
+- "Add a status update comment to open PRs"
+
+### Create Shared Agentic Workflow
+**Load when**: User wants to create a reusable workflow component or wrap an MCP server
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-shared-agentic-workflow.md`
+
+**Use cases**:
+- "Create a shared component for Notion integration"
+- "Wrap the Slack MCP server as a reusable component"
+- "Design a shared workflow for database queries"
+
+### Fix Dependabot PRs
+**Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`)
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/dependabot.md`
+
+**Use cases**:
+- "Fix the open Dependabot PRs for npm dependencies"
+- "Bundle and close the Dependabot PRs for workflow dependencies"
+- "Update @playwright/test to fix the Dependabot PR"
+
+### Analyze Test Coverage
+**Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy.
+
+**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/test-coverage.md`
+
+**Use cases**:
+- "Create a workflow that comments coverage on PRs"
+- "Analyze coverage trends over time"
+- "Add a coverage gate that blocks PRs below a threshold"
+
+### CLI Commands Reference
+**Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access.
+
+**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md`
+
+**Use cases**:
+- "How do I trigger workflow X on the main branch?"
+- "What's the MCP equivalent of `gh aw logs`?"
+- "I'm in Copilot Cloud — how do I compile a workflow?"
+- "Show me all available gh aw commands"
+
+### Token Consumption Optimization
+**Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes.
+
+**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/token-optimization.md`
+
+**Use cases**:
+- "How do I reduce the token cost of this workflow?"
+- "My workflow is too expensive — how do I optimize it?"
+- "How do I compare token usage between two runs?"
+- "Should I use gh-proxy or the MCP server?"
+- "How do I use sub-agents to reduce costs?"
+- "How do I measure the impact of a prompt change?"
+
+### Workflow Pattern Selection
+**Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows.
+
+**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/patterns.md`
+
+**Use cases**:
+- "Which pattern should I use for multi-repo rollout?"
+- "How should I structure this workflow architecture?"
+- "What pattern fits slash-command triage?"
+- "Should this be DispatchOps or DailyOps?"
+
+## Instructions
+
+When a user interacts with you:
+
+1. **Identify the task type** from the user's request
+2. **Load the appropriate prompt** from the URLs listed above
+3. **Follow the loaded prompt's instructions** exactly
+4. **If uncertain**, ask clarifying questions to determine the right prompt
+
+## Quick Reference
+
+```bash
+# Initialize repository for agentic workflows
+gh aw init
+
+# Generate the lock file for a workflow
+gh aw compile [workflow-name]
+
+# Trigger a workflow on demand (preferred over gh workflow run)
+gh aw run # interactive input collection
+gh aw run --ref main # run on a specific branch
+
+# Debug workflow runs
+gh aw logs [workflow-name]
+gh aw audit
+
+# Upgrade workflows
+gh aw fix --write
+gh aw compile --validate
+```
+
+## Key Features of gh-aw
+
+- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter
+- **AI Engine Support**: Copilot, Claude, Codex, or custom engines
+- **MCP Server Integration**: Connect to Model Context Protocol servers for tools
+- **Safe Outputs**: Structured communication between AI and GitHub API
+- **Strict Mode**: Security-first validation and sandboxing
+- **Shared Components**: Reusable workflow building blocks
+- **Repo Memory**: Persistent git-backed storage for agents
+- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default
+
+## Important Notes
+
+- Always reference the instructions file at `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` for complete documentation
+- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud
+- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions
+- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF
+- Follow security best practices: minimal permissions, explicit network access, no template injection
+- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/network.md` for the full list of valid ecosystem identifiers and domain patterns.
+- **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself.
+- **Triggering runs**: Always use `gh aw run ` to trigger a workflow on demand — not `gh workflow run .lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref ` to run on a specific branch.
+- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md`
diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json
new file mode 100644
index 0000000000..cdcb39f6a2
--- /dev/null
+++ b/.github/aw/actions-lock.json
@@ -0,0 +1,14 @@
+{
+ "entries": {
+ "actions/github-script@v9.0.0": {
+ "repo": "actions/github-script",
+ "version": "v9.0.0",
+ "sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
+ },
+ "github/gh-aw-actions/setup@v0.88.2": {
+ "repo": "github/gh-aw-actions/setup",
+ "version": "v0.88.2",
+ "sha": "9271a1804551c0dc4fb0085a97979950aa2f8489"
+ }
+ }
+}
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 272a0c2164..c0bbc25457 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -11,17 +11,18 @@
## 📚 Project Overview
This project is a .NET data provider for SQL Server, enabling .NET applications to interact with SQL Server databases. It supports various features like connection pooling, transaction management, and asynchronous operations.
-The project builds from a **single unified project** at `src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj` that multi-targets `net462`, `net8.0`, and `net9.0`. The legacy `netfx/` and `netcore/` directories are being phased out — only their `ref/` folders (which define the public API surface) remain active.
+The project builds from a **single unified project** at `src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj`. It targets `net8.0` and `net9.0` on all supported hosts, and adds `net462` only when building on Windows. The legacy `netfx/` and `netcore/` directories are being phased out — only their `ref/` folders (which define the public API surface) remain active.
The project includes:
- **Public APIs**: Defined in `netcore/ref/` and `netfx/ref/` directories.
- **Implementations**: All source code in `src/Microsoft.Data.SqlClient/src/`.
- **Tests**: Located in the `tests/` directory, covering unit and integration tests.
- - **Unit Tests**: Located in `tests/UnitTests/` directory, which includes tests for individual components and methods.
- - **Functional Tests**: Located in `tests/FunctionalTests/` directory, which includes tests for various features and functionalities that can be run without a SQL Server instance.
- - **Manual Tests**: Located in `tests/ManualTests/` directory, which includes tests that require a SQL Server instance to run.
+ - **Unit Tests**: Located in `src/Microsoft.Data.SqlClient/tests/UnitTests/`.
+ - **Functional Tests**: Located in `src/Microsoft.Data.SqlClient/tests/FunctionalTests/`.
+ - **Manual Tests**: Located in `src/Microsoft.Data.SqlClient/tests/ManualTests/`.
+ - **Performance/Stress Tests**: Located in `src/Microsoft.Data.SqlClient/tests/PerformanceTests/` and `src/Microsoft.Data.SqlClient/tests/StressTests/`.
- **Documentation**: Found in the `doc/` directory, including API documentation, usage examples.
- **Policies**: Contribution guidelines, coding standards, and review policies in the `policy/` directory.
-- **Building**: The project uses MSBuild for building and testing, with configurations and targets defined in the `build.proj` file, whereas instructions are provided in the `BUILDGUIDE.md` file.
+- **Building**: The repo uses `build.proj` for orchestrated build/test/pack workflows, `src/Microsoft.Data.SqlClient.slnx` for solution-centric development tooling, and Azure DevOps YAML under `eng/pipelines/` plus `eng/pipelines/onebranch/` for CI and official release flows. See `BUILDGUIDE.md` for local build details.
- **CI/CD**: ADO Pipelines for CI/CD and Pull request validation are defined in the `eng/` directory, ensuring code quality and automated testing.
## 📦 Products
@@ -33,7 +34,7 @@ This project includes several key products and libraries that facilitate SQL Ser
## 🛠️ Key Features
- **Connectivity to SQL Server**: Provides robust and secure connections to SQL Server databases, using various authentication methods, such as Windows Authentication, SQL Server Authentication, and Entra ID authentication, e.g. `ActiveDirectoryIntegrated`, `ActiveDirectoryPassword`, `ActiveDirectoryServicePrincipal`,`ActiveDirectoryInteractive`, `ActiveDirectoryDefault`, and `ActiveDirectoryManagedIdentity`.
- **Connection Resiliency**: Implements connection resiliency features to handle transient faults and network issues, ensuring reliable database connectivity.
-- **TLS Encryption**: Supports secure connections using TLS protocols to encrypt data in transit. Supports TLS 1.2 and higher, ensuring secure communication with SQL Server. Supported encryption modes are:
+- **TLS Encryption**: Supports secure connections using TLS protocols to encrypt data in transit. Supports TLS 1.2 and higher, ensuring secure communication with SQL Server. Supported encryption modes are:
- **Optional**: Encryption is used if available, but not required.
- **Mandatory**: Encryption is mandatory for the connection.
- **Strict**: Enforces strict TLS requirements, ensuring only secure connections are established.
@@ -49,6 +50,7 @@ This project includes several key products and libraries that facilitate SQL Ser
- **Data Encryption**: Supports data encryption for secure data transmission.
- **Logging and Diagnostics**: Provides event source tracing diagnostic capabilities for troubleshooting.
- **Failover Support**: Handles automatic failover scenarios for high availability.
+ - Compatibility switch: `Switch.Microsoft.Data.SqlClient.UseLegacyFailoverAlternationOnLoginSqlErrors` (default `false`) can restore legacy alternation behavior in `LoginWithFailover` for login-phase SQL errors.
- **Cross-Platform Support**: Compatible with both .NET Framework and .NET Core, allowing applications to run on Windows, Linux, and macOS.
- **Column Encryption AKV Provider**: Supports Azure Key Vault (AKV) provider for acquiring keys from Azure Key Vault to be used for encryption and decryption.
@@ -122,13 +124,17 @@ When a new issue is created, follow these steps:
- Ensure the PR passes all CI checks before merging.
### ✅ Closing Issues
-- Add a comment summarizing the fix and referencing the PR
+- Add a comment summarizing the fix and referencing the PR
### ⚙️ Automating Workflows
- Auto-label PRs based on folder paths (e.g., changes in `src/Microsoft.Data.SqlClient/src/` → `Area\SqlClient`, changes in `tests/` → `Area\Testing`) and whether they add new public APIs or introduce a breaking change.
- Suggest release note entries for fixes by updating files under `release-notes/` or by using the `release-notes` prompt (instead of editing `CHANGELOG.md` directly).
- Tag reviewers based on `CODEOWNERS` file
+## 🌿 Branch Naming
+- All branches created by AI agents **must** use the `dev/automation/` prefix (e.g. `dev/automation/fix-connection-timeout`).
+- Do **not** create branches directly under `main`, `dev/`, or any other top-level prefix.
+
## 🧠 Contextual Awareness
- All source code is in `src/Microsoft.Data.SqlClient/src/`. Do NOT add code to legacy `netfx/src/` or `netcore/src/` directories.
- Only `ref/` folders in `netcore/ref/` and `netfx/ref/` remain active for defining the public API surface.
@@ -145,6 +151,14 @@ When a new issue is created, follow these steps:
- Do not modify `CHANGELOG.md` unless executing a release workflow (see `release-notes` prompt).
- Do not close issues without a fix or without providing a clear reason.
+## Terminal Execution Safety
+- Treat any non-zero shell exit code as a failed step that requires correction before proceeding.
+- If a bash process exits, do not wait for more output from that process; rerun the command in a fresh terminal session.
+- Validate that expected command output was produced before using it as evidence for conclusions.
+- When terminal execution fails, surface the failure immediately and retry with a corrected command.
+- Avoid `set -e` in this automation workflow; use focused commands and verify each result explicitly so shell exits are observable and attributable.
+- Prefer short, single-purpose terminal commands over long chained scripts when debugging or gathering state.
+
## 📝 Notes
- Update policies and guidelines in the `policy/` directory as needed based on trending practices and team feedback.
- Regularly review and update the `doc/` directory to ensure it reflects the current state of the project.
diff --git a/.github/instructions/3rd-party-package-versions.instructions.md b/.github/instructions/3rd-party-package-versions.instructions.md
new file mode 100644
index 0000000000..1de7df26bf
--- /dev/null
+++ b/.github/instructions/3rd-party-package-versions.instructions.md
@@ -0,0 +1,208 @@
+---
+applyTo: "**/Directory.Packages.props,**/*.csproj,**/Directory.Build.props,**/*.nuspec"
+---
+# Choosing Third-Party Package Dependency Versions
+
+Guidance for choosing versions of **external (third-party) NuGet package dependencies** in multi-targeted projects (e.g. `net462;net8.0;net9.0`).
+
+> **Scope:** This document covers dependencies consumed from NuGet — packages the SqlClient repo does NOT own. For versioning of SqlClient's own inter-sibling packages (Logging, Abstractions, SqlClient, Azure, AKV Provider, SqlServer.Server), see `sqlclient-package-versions.instructions.md`.
+
+## Rule
+For runtime-aligned packages, **the package major must match the target runtime major**: 8.x on `net8.0`, 9.x on `net9.0`, 10.x on `net10.0`, and so on. TFMs that aren't tied to a specific runtime major (`net462`, `netstandard2.0`) get the major of the floor LTS. Other categories are versioned as described below.
+
+Split package references into three categories:
+
+### 1. Runtime-aligned packages — **version per TFM, matching the runtime band**
+
+Packages whose major version ships with (or is tightly coupled to) a specific .NET runtime:
+
+- `Microsoft.Extensions.*` (Logging, DependencyInjection, Configuration, Hosting, Options, Caching, Http, Primitives, ...)
+- `Microsoft.AspNetCore.*`
+- `Microsoft.EntityFrameworkCore.*`
+- `System.Text.Json`, `System.Memory`, `System.IO.Pipelines`, `System.Formats.Asn1`, `System.Security.Cryptography.Pkcs`
+- `Microsoft.Bcl.*`
+
+Use the major version that matches the TFM. For TFMs without a corresponding runtime major (`net462`, `netstandard2.0`, etc.), use the major of the **lowest supported modern TFM** — typically the floor LTS (e.g. `8.x` while net8 is supported). This keeps the legacy targets on a long-lived, well-patched band and avoids dragging in transitive deps from a newer major:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+When the floor LTS drops out of support, bump the default block to the new floor LTS major and drop any conditional block that becomes redundant.
+
+### 2. Independent packages — **single version across all TFMs**
+
+Packages whose versioning is decoupled from the .NET runtime:
+
+- `Newtonsoft.Json`, `Polly.*`, `Serilog.*`
+- `Azure.*`, `Microsoft.Identity.*`, `Microsoft.IdentityModel.*`
+- `Microsoft.Data.SqlClient`, `Dapper`, `StackExchange.Redis`
+- Most third-party packages
+
+Reference one (latest stable) version unconditionally:
+
+```xml
+
+
+```
+
+### 3. Polyfills — **conditional presence, single version**
+
+Packages that only exist (or are only needed) on older TFMs. The polyfill major doesn't have to match any runtime band (older TFMs have no in-box equivalent), so pick the latest stable available:
+
+```xml
+
+
+
+```
+
+Condition the *presence* of the reference, not the version.
+
+## How to categorize a package
+
+The named lists above aren't exhaustive. To classify a package you don't recognize, work through these steps in order:
+
+### 1. Read the nuget.org description
+
+Pure polyfills almost always say so explicitly. For example, `Microsoft.Bcl.TimeProvider`'s page reads: *"For apps targeting .NET 8 and newer versions, referencing this package is unnecessary, as the types it contains are already included in the .NET 8 and higher platform versions."*
+
+If the description says "for apps targeting .NET X and earlier" or "unnecessary on .NET X+", treat as polyfill candidate and continue to step 2 to confirm. If it makes no such claim and the package owner is Microsoft + a major number tracks the .NET release train, treat as runtime-aligned candidate.
+
+### 2. Inspect the package's `lib/` layout
+
+Look at the "Frameworks" tab on nuget.org or open the `.nupkg`:
+
+| `lib/` layout | Category |
+|---|---|
+| Only older TFM folders (`netstandard2.0`, `net462`) — no modern TFMs | Pure polyfill |
+| `lib/net8.0/_._`, `lib/net9.0/_._` placeholders + real DLL only on older TFMs | Pure polyfill (no-op on modern TFMs) |
+| Real DLLs in `lib/net8.0/`, `lib/net9.0/`, `lib/net10.0/`, differing per band | Runtime-aligned |
+| Real DLLs on every TFM including older ones, single major doesn't track .NET releases | Independent |
+
+### 3. Check the release cadence
+
+- Runtime-aligned: new major every November in lockstep with .NET (8.0, 9.0, 10.0, ...) plus monthly servicing patches.
+- Independent: releases on its own schedule, major doesn't correlate with .NET versions.
+- Pure polyfill: usually freezes at one major and rarely bumps; new majors only to ride the build train.
+
+### 4. Functional test — remove the reference and rebuild
+
+The decisive test for the polyfill-vs-runtime-aligned boundary: remove the `PackageReference` on a modern TFM (e.g. net8) and build.
+
+- Builds clean → package was acting as a polyfill on that TFM. Confirm category 3.
+- Fails with `CS0246`/`CS1061` (missing type or method) → the package contributes API the in-box BCL doesn't have. Treat as runtime-aligned (category 1), even if the description sounds polyfill-ish.
+
+### Beware hybrids
+
+Some packages look like polyfills but add API beyond the in-box BCL even on modern TFMs. Treat these like runtime-aligned packages.
+
+### When in doubt
+
+Treat as **runtime-aligned** (category 1) and reference on every TFM with per-TFM majors.
+
+- Cost of mis-classifying a true polyfill this way: a redundant `_._` asset at restore. Harmless.
+- Cost of mis-classifying a hybrid as a pure polyfill: a compile break on the TFMs where you dropped the reference.
+
+## Why
+
+### Why latest minor/patch always
+
+`PackageReference` versions are minimums (`[X, ∞)`), so writing a stale minor or patch buys nothing for downgrade-safety and only loses fixes:
+
+- **Security**: BCL and Extensions packages ship CVE patches in minor/patch bumps. Pinning to an older patch means a customer who doesn't transitively pull a newer version stays on the vulnerable floor.
+- **Bug fixes**: Same logic for non-security fixes. We have no reason to anchor customers to an older `8.0.0` when `8.0.5` is available.
+- **NU1605 risk is small and easy to fix**: NU1605 fires on *any* downgrade, including minor/patch within the same major (e.g. our `8.0.5` transitive vs. a customer's direct `8.0.0`). In practice this is rare and trivial to resolve — the customer bumps their direct reference to a current patch. The cost of *not* tracking latest (stale security/bug fixes for every consumer who doesn't override) is larger than the cost of an occasional one-line bump in a consumer project.
+- **Reduces noise from automated bumps**: Dependabot/Renovate PRs disappear if we already track latest.
+
+This rule applies to all three categories. The category decides the major; "latest" decides the minor and patch.
+
+### NuGet `PackageReference` versions are minimums, not pins
+
+`Version="X"` means `[X, ∞)`. The resolver picks the highest version requested across the graph, with one critical exception: a **direct** reference wins over a transitive one (nearest-wins).
+
+### NU1605 fires when the customer's direct version is *lower* than your transitive version
+
+If your library transitively requires `Microsoft.Extensions.Logging 10.0.0` and the consuming app has a direct ``, NuGet detects a downgrade and emits **NU1605**. In modern SDKs this is an **error**, not a warning — the customer's build fails.
+
+The reverse (customer's direct version higher than your transitive) resolves cleanly with no warning.
+
+### The asymmetry drives the rule
+
+| Your transitive version | Customer's direct version | Result |
+|---|---|---|
+| 10.x | 8.x | **NU1605 error** |
+| 10.x | 10.x or higher | Clean |
+| 8.x | 8.x or higher | Clean |
+
+Pinning runtime-aligned packages to the **band matching each TFM** means a net8 consumer transitively gets 8.x (no friction with their own 8.x reference), and a net10 consumer transitively gets 10.x.
+
+Pinning everything to the latest major (e.g. `10.x` unconditionally) forces every net8 customer to roll their direct references forward or hit NU1605.
+
+### Independent packages don't have this problem
+
+`Newtonsoft.Json 13.x`, `Azure.Identity 1.17.x`, etc. aren't tied to a runtime version. Customers don't have a "matching" version in mind, and the package's own multi-targeted assets handle TFM selection internally. One version is simpler and avoids needless conditional blocks.
+
+### Framework-provided assemblies win at runtime anyway
+
+On .NET 8+, packages like `System.Text.Json` and `System.Memory` are part of the shared framework. Even if you reference `System.Text.Json 9.0.0`, a net8 app uses the in-box net8 copy at runtime. Referencing 10.x on net8 just creates restore-graph noise with no runtime benefit — another reason to match the band.
+
+## Tradeoffs and alternatives considered
+
+### Per-TFM (the rule) vs. single lowest-LTS version
+
+A "pin everything to the lowest supported LTS major (e.g. 8.x everywhere) and bump only when that LTS drops" policy is also downgrade-safe and simpler in `Directory.Packages.props`. We rejected it because:
+
+- Customers on newer runtimes lose access to perf/feature work that landed in later package majors for packages that *aren't* fully overridden by the in-box shared framework (e.g. `Microsoft.Extensions.Caching.Memory`, `System.Configuration.ConfigurationManager`).
+- The simplification is small: one extra conditional `ItemGroup` per runtime-aligned package.
+- A coordinated bump when the floor LTS drops is a larger, riskier change than incremental per-TFM updates.
+
+Per-TFM keeps each runtime on its matching band and confines change to one `Update` line at a time.
+
+### Why no upper bounds
+
+Customers can transitively pull in a *higher* major than your reference. NuGet resolves nearest-wins with no warning, so a major that broke API can produce `MissingMethodException`/`TypeLoadException` at runtime rather than a restore-time failure.
+
+An upper bound (e.g. `Version="[8.0.0, 10.0.0)"`) would convert that runtime failure into a restore-time `NU1107` and is the only way to guarantee compatibility. We still avoid it because:
+
+- It blocks customers from rolling forward to fix CVEs or take perf wins in the newer major.
+- It propagates conflicts deep into customer dependency graphs that we can't see.
+- Microsoft's [library guidance](https://learn.microsoft.com/dotnet/standard/library-guidance/dependencies#nuget-dependency-version-ranges) explicitly says **AVOID upper bounds**, and the foundational Microsoft libraries (EF Core, ASP.NET Core, Aspire, Orleans, the BCL itself, Azure SDK) follow it.
+- Exact pins (`[X]`) in Microsoft repos are reserved for host-coupled scenarios: Roslyn analyzers (`Microsoft.CodeAnalysis.*`), MSBuild API consumers (`Microsoft.Build`), VS extensibility. None apply to runtime libraries like SqlClient.
+
+We accept the SemVer-break risk in exchange for not breaking customer rollforward. If a specific package is known to break compatibility at a future major, document it in code review and revisit — don't blanket-bound.
+
+### Downgrade direction matters
+
+| Your transitive | Customer's direct | Result |
+|---|---|---|
+| Higher | Lower | **NU1605 error** (we cause this) |
+| Lower | Higher | Clean restore, customer's wins |
+| Equal | Equal | Clean |
+
+The asymmetry is the entire reason per-TFM matching works: it makes us the *lower* or *equal* for any customer who has aligned their own references with their runtime.
+
+## Checklist before changing a package version
+
+1. Is the package in the runtime-aligned list above? → use per-TFM conditional `PackageVersion Update`, latest minor/patch in each band.
+2. Is it independent? → single `PackageVersion`, latest stable.
+3. Is it a polyfill? → conditional `PackageReference` only on TFMs that need it, latest stable.
+4. Always pick the latest available minor/patch within the chosen major. Don't carry forward a stale minor when bumping or adding.
+5. Prefer Central Package Management (`Directory.Packages.props`) over per-project versions.
+6. Never use exact-version (`[X]`) or upper-bound (`[X, Y)`) ranges on `PackageReference` unless you have a documented compatibility reason.
+
+## Sources
+
+- [NU1605 — Detected package downgrade](https://learn.microsoft.com/nuget/reference/errors-and-warnings/nu1605)
+- [NuGet Package versioning — version ranges](https://learn.microsoft.com/nuget/concepts/package-versioning#version-ranges)
+- [NuGet dependency resolution](https://learn.microsoft.com/nuget/concepts/dependency-resolution)
+- [Library guidance — Dependencies](https://learn.microsoft.com/dotnet/standard/library-guidance/dependencies)
+- [Library guidance — Cross-platform targeting](https://learn.microsoft.com/dotnet/standard/library-guidance/cross-platform-targeting)
diff --git a/.github/instructions/ado-pipelines.instructions.md b/.github/instructions/ado-pipelines.instructions.md
index 82b8046dee..688b57981a 100644
--- a/.github/instructions/ado-pipelines.instructions.md
+++ b/.github/instructions/ado-pipelines.instructions.md
@@ -1,195 +1,140 @@
---
applyTo: "eng/pipelines/**/*.yml"
---
-# Azure DevOps Pipelines Guide
-
-## Overview
-
-This repository uses Azure DevOps Pipelines for CI/CD. The pipeline configurations are located in `eng/pipelines/`.
-
-**ADO Organization**: sqlclientdrivers
-**ADO Project**: ADO.NET
-
-## Pipeline Structure
-
-```
-eng/pipelines/
-├── abstractions/ # Abstractions package pipelines
-├── azure/ # Azure package pipelines
-├── common/ # Shared templates
-│ └── templates/
-│ ├── jobs/ # Reusable job templates
-│ ├── stages/ # Reusable stage templates
-│ └── steps/ # Reusable step templates
-├── jobs/ # Top-level job definitions
-├── libraries/ # Shared variable definitions
-├── stages/ # Stage definitions
-├── steps/ # Step definitions
-├── variables/ # Variable templates
-├── akv-official-pipeline.yml # AKV provider official/signing build
-├── dotnet-sqlclient-ci-core.yml # Core CI pipeline (reusable)
-├── dotnet-sqlclient-ci-package-reference-pipeline.yml # CI with package references
-├── dotnet-sqlclient-ci-project-reference-pipeline.yml # CI with project references
-├── dotnet-sqlclient-signing-pipeline.yml # Package signing pipeline
-├── sqlclient-pr-package-ref-pipeline.yml # PR validation (package ref)
-├── sqlclient-pr-project-ref-pipeline.yml # PR validation (project ref)
-└── stress-tests-pipeline.yml # Stress testing
-```
-
-## Main Pipelines
-
-### CI Core Pipeline (`dotnet-sqlclient-ci-core.yml`)
-Reusable core CI pipeline consumed by both project-reference and package-reference CI pipelines. Configurable parameters:
-
-| Parameter | Description | Default |
-|-----------|-------------|---------|
-| `targetFrameworks` | Windows test frameworks | `[net462, net8.0, net9.0, net10.0]` |
-| `targetFrameworksUnix` | Unix test frameworks | `[net8.0, net9.0, net10.0]` |
-| `referenceType` | Project or Package reference | Required |
-| `buildConfiguration` | Debug or Release | Required |
-| `useManagedSNI` | Test with managed SNI | `[false, true]` |
-| `testJobTimeout` | Test job timeout (minutes) | Required |
-| `runAlwaysEncryptedTests` | Include AE tests | `true` |
-| `enableStressTests` | Include stress test stage | `false` |
-
-### CI Reference Pipelines
-- `dotnet-sqlclient-ci-project-reference-pipeline.yml` — Full CI using project references (builds from source)
-- `dotnet-sqlclient-ci-package-reference-pipeline.yml` — Full CI using package references (tests against published NuGet packages)
-
-### PR Validation Pipelines
-- `sqlclient-pr-project-ref-pipeline.yml` — PR validation with project references
-- `sqlclient-pr-package-ref-pipeline.yml` — PR validation with package references
-
-These pipelines trigger on pull requests and run a subset of the full CI matrix to provide fast feedback.
-
-### Official/Signing Pipeline (`dotnet-sqlclient-signing-pipeline.yml`)
-Signs and publishes NuGet packages. Used for official releases. Requires secure service connections and key vault access for code signing.
-
-### AKV Official Pipeline (`akv-official-pipeline.yml`)
-Builds and signs the `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` add-on package separately from the main driver. Uses 1ES pipeline templates for compliance.
-
-### Stress Tests Pipeline (`stress-tests-pipeline.yml`)
-Optional pipeline for long-running stress and endurance testing. Enabled via `enableStressTests` parameter in CI core.
-
-## Build Stages
-
-1. **build_abstractions_package_stage**: Build and pack abstractions
-2. **build_sqlclient_package_stage**: Build main driver and AKV packages
-3. **build_azure_package_stage**: Build Azure extensions package
-4. **stress_tests_stage**: Optional stress testing
-5. **run_tests_stage**: Execute all test suites
+# Azure DevOps CI/CD Pipeline Guidelines
+
+## Purpose
+
+Rules and conventions for editing the Azure DevOps CI/CD pipelines that build, test, and validate Microsoft.Data.SqlClient. These pipelines live under `eng/pipelines/` (excluding `onebranch/`, which is covered by separate instructions).
+
+**ADO Organization**: sqlclientdrivers | **ADO Project**: ADO.NET
+
+## Pipeline Layout
+
+Two categories of pipelines exist in this repository:
+
+- **CI/PR pipelines** (`eng/pipelines/`) — Build, test, and validate on every push/PR
+- **OneBranch pipelines** (`eng/pipelines/onebranch/`) — Official signing/release builds (separate instructions file)
+
+Top-level CI/PR pipeline files:
+- `dotnet-sqlclient-ci-core.yml` — Reusable core template; all CI and PR pipelines extend this
+- `dotnet-sqlclient-ci-package-reference-pipeline.yml` — CI with Package references (Release)
+- `dotnet-sqlclient-ci-project-reference-pipeline.yml` — CI with Project references (Release)
+- `sqlclient-pr-package-ref-pipeline.yml` — PR validation with Package references
+- `sqlclient-pr-project-ref-pipeline.yml` — PR validation with Project references
+- `ci/stress/sqlclient-ci-stress-pipeline.yml` — Stress test pipeline and templates
+
+Reusable templates are organized under:
+- `common/templates/jobs/` — Job templates (`ci-build-nugets-job`, `ci-code-coverage-job`, `ci-run-tests-job`)
+- `common/templates/stages/` — Stage templates (`ci-run-tests-stage`)
+- `common/templates/steps/` — Step templates (build, test, config, publish)
+- `jobs/` — Package-specific CI jobs (pack/test Abstractions, Azure, Logging, stress)
+- `stages/` — Package-specific CI stages (generate secrets, build SqlServer/Logging/Abstractions/SqlClient/Azure, verify packages)
+- `libraries/` — Shared variables (`ci-build-variables.yml`)
+- `steps/` — SDK install steps
+
+## CI Core Template
+
+`dotnet-sqlclient-ci-core.yml` is the central orchestrator. All CI and PR pipelines extend it with different parameters.
+
+Key parameters:
+- `referenceType` (required) — `Package` or `Project`; controls how sibling packages are referenced
+- `buildConfiguration` (required) — `Debug` or `Release`
+- `testJobTimeout` (required) — test job timeout in minutes
+- `targetFrameworks` — Windows test TFMs; default `[net462, net8.0, net9.0, net10.0]`
+- `targetFrameworksUnix` — Unix test TFMs; default `[net8.0, net9.0, net10.0]`
+- `netcoreVersionTestUtils` — default runtime for shared test utilities; default `net10.0`
+- `testSets` — test partitions; default `[1, 2, 3]`
+- `useManagedSNI` — SNI variants to test; default `[false, true]`
+- `runAlwaysEncryptedTests` — include AE test set; default `true`
+- `runLegacySqlTests` — include SQL Server 2016/2017 manual-test legs; default `true`
+- `debug` — enable debug output; default `false`
+- `dotnetVerbosity` — build verbosity; default `normal`
+
+## Build Stage Order
+
+Stages execute in dependency order (Package reference mode requires artifacts from prior stages):
+1. `generate_secrets` — Generate test secrets
+2. `build_sqlserver_package_stage` — Build `Microsoft.SqlServer.Server`
+3. `build_logging_package_stage` — Build Logging package
+4. `build_abstractions_package_stage` — Build Abstractions (package mode depends on Logging)
+5. `build_sqlclient_package_stage` — Build SqlClient and produce the AKV provider package
+6. `build_azure_package_stage` — Build Azure extensions
+7. `verify_nuget_packages_stage` — Verify NuGet package metadata
+8. `ci_run_tests_stage` — Run MDS and AKV test suites
+
+Stress testing is no longer a stage threaded through `dotnet-sqlclient-ci-core.yml`; it lives under `eng/pipelines/ci/stress/` as a separate pipeline flow.
+
+When adding a new build stage, respect the dependency graph and pass artifact names/versions to downstream stages.
+
+## PR vs CI Pipeline Differences
+
+PR pipelines:
+- Trigger on PRs targeting `release/7.1`; path filters vary by pipeline
+- Exception: the legacy `sqlclient-pr-package-ref-pipeline.yml` has an empty branch include list, so it has no PR trigger and is manual-queue only
+- Use reduced TFM matrix: `[net462, net8.0, net9.0]` (excludes net10.0)
+- Timeout: 90 minutes
+- Package-ref PR disables Always Encrypted tests in Debug config and also disables legacy SQL Server test legs to keep validation fast
+
+CI pipelines:
+- Trigger on push to `release/7.1` (GitHub) and `internal/release/7.1` (ADO) with `batch: true`
+- Exception: the legacy `dotnet-sqlclient-ci-package-reference-pipeline.yml` has its GitHub `release/7.1` push entry commented out, so only the ADO push trigger is active; its GitHub daily schedule still runs
+- Scheduled daily builds are staggered to avoid main and other release-branch schedules (see individual pipeline files for cron times)
+- Full TFM matrix including net10.0 test legs and legacy SQL Server manual-test coverage
## Test Configuration
-### Test Sets
-Tests are divided into sets for parallelization:
-- `TestSet=1` — First partition of tests
-- `TestSet=2` — Second partition
-- `TestSet=3` — Third partition
-- `TestSet=AE` — Always Encrypted tests
-
-### Test Filters
-Tests use category-based filtering. The default filter excludes both `failing` and `flaky` tests:
-```
-category!=failing&category!=flaky
-```
-
-Category values:
-- `nonnetfxtests` — Excluded on .NET Framework
-- `nonnetcoreapptests` — Excluded on .NET Core
-- `nonwindowstests` — Excluded on Windows
-- `nonlinuxtests` — Excluded on Linux
-- `failing` — Known permanent failures (excluded from all runs)
-- `flaky` — Intermittently failing tests (quarantined, run separately)
-
-### Flaky Test Quarantine in Pipelines
-Quarantined tests (`[Trait("Category", "flaky")]`) run in **separate pipeline steps** after the main test steps. This ensures:
-- Main test runs are **not blocked** by intermittent failures
-- Flaky tests are still **monitored** for regression or resolution
-- Code coverage is **not collected** for flaky test runs
-- Results appear in pipeline output for visibility
-
-The quarantine steps are configured in:
-- `eng/pipelines/common/templates/steps/build-and-run-tests-netcore-step.yml`
-- `eng/pipelines/common/templates/steps/build-and-run-tests-netfx-step.yml`
-- `eng/pipelines/common/templates/steps/run-all-tests-step.yml`
-
-### Test Timeout
-All test runs use `--blame-hang-timeout 10m` (configured in `build.proj`). Tests exceeding 10 minutes are killed and reported as failures.
-
-### SNI Testing
-The `useManagedSNI` parameter controls testing with:
-- Native SNI (`false`) - Windows native library
-- Managed SNI (`true`) - Cross-platform managed implementation
+Test partitioning:
+- Tests split into `TestSet=1`, `TestSet=2`, `TestSet=3` for parallelization
+- `TestSet=AE` — Always Encrypted tests (controlled by `runAlwaysEncryptedTests`)
-## Variables
-
-### Build Variables (`ci-build-variables.yml`)
-Common build configuration:
-- Package versions
-- Build paths
-- Signing configuration
-
-### Runtime Variables
-Set via pipeline parameters or UI:
-- `Configuration` - Debug/Release
-- `Platform` - AnyCPU/x86/x64
-- `TF` - Target framework
-
-## Creating Pipeline Changes
+Test filters — default excludes `failing` and `flaky` categories:
+- `failing` — known permanent failures, always excluded
+- `flaky` — intermittent failures, quarantined in separate pipeline steps
+- `nonnetfxtests` / `nonnetcoreapptests` — platform-specific exclusions
+- `nonwindowstests` / `nonlinuxtests` — OS-specific exclusions
-### Adding New Test Categories
-1. Add category attribute to tests: `[Category("newcategory")]`
-2. Update filter expressions in test job templates
-3. Document category purpose in test documentation
+Flaky test quarantine:
+- Quarantined tests (`[Trait("category", "flaky")]`) run in separate steps after main tests
+- Main test runs are not blocked by flaky failures
+- No code coverage collected for flaky runs
+- Configured in `common/templates/steps/build-and-run-tests-netcore-step.yml`, `build-and-run-tests-netfx-step.yml`, and `run-all-tests-step.yml`
-### Adding New Pipeline Parameters
-1. Define parameter in appropriate `.yml` file
-2. Add to parameter passing in calling templates
-3. Document in this file
+SNI testing — `useManagedSNI` controls testing with native SNI (`false`) or managed SNI (`true`)
-### Modifying Build Steps
-1. Changes should be made in template files for reusability
-2. Test changes locally when possible
-3. Submit as PR - validation will run
+Test timeout — `--blame-hang-timeout 10m` (configured in `build.proj` and threaded through CI test steps); tests exceeding 10 minutes are killed
-## Best Practices
-
-### Template Design
-- Use templates for reusable definitions
-- Pass parameters explicitly (avoid global variables)
-- Use descriptive stage/job/step names
-
-### Variable Management
-- Use template variables for shared values
-- Use pipeline parameters for per-run configuration
-- Avoid hardcoding versions (use Directory.Packages.props)
+## Variables
-### Test Infrastructure
-- Ensure tests are properly categorized
-- Handle test configuration files properly
-- Use test matrix for cross-platform coverage
+- All CI build variables centralized in `libraries/ci-build-variables.yml`
+- Package versions use `-ci` suffix (e.g., `7.0.0.$(Build.BuildNumber)-ci`)
+- `assemblyBuildNumber` derived from first segment of `Build.BuildNumber` (16-bit safe)
+- `localFeedPath` = `$(Build.SourcesDirectory)/packages` — local NuGet feed for inter-package deps
+- `packagePath` = `$(Build.SourcesDirectory)/output` — NuGet pack output
-## Troubleshooting
+## Variable Naming — Avoid `{COMMAND}ARGUMENTS` Names
-### Common Issues
-1. **Test failures due to missing config**: Ensure `config.json` exists
-2. **Platform-specific failures**: Check platform exclusion categories
-3. **Timeout issues**: Increase `testJobTimeout` parameter
+The dotnet CLI (via System.CommandLine) reads environment variables named `{COMMAND}ARGUMENTS` and silently injects their content into the parsed arguments for that subcommand. Because Azure DevOps automatically exposes all pipeline variables as uppercased environment variables, a pipeline variable named `runArguments` becomes `RUNARGUMENTS`, which `dotnet run` reads and injects into the application's `args[]` — bypassing the `--` separator.
-### Debugging Pipelines
-- Enable debug mode via `debug: true` parameter
-- Use `dotnetVerbosity: diagnostic` for detailed output
-- Check build logs in Azure DevOps
+**Forbidden variable names** (any casing):
+- `runArguments` — injected into `dotnet run`
+- `buildArguments` — injected into `dotnet build`
+- `testArguments` — injected into `dotnet test`
+- Any name matching `{dotnet-subcommand}Arguments`
-## Security Considerations
+**Use instead**: `dotnetBuildOpts`, `dotnetRunOpts`, `stressTestArgs`, or other names that do not match the `{COMMAND}ARGUMENTS` pattern.
-- Pipelines use service connections for artifact publishing
-- Signing uses secure key vault integration
-- Sensitive configuration should use pipeline secrets
-- Never commit credentials in pipeline files
+This affects ALL .NET SDK versions (8.0+). The injection is invisible in `[command]` log lines, making it extremely hard to diagnose. The only symptom is the application receiving unexpected arguments.
-## Related Documentation
+## Conventions When Editing Pipelines
-- [BUILDGUIDE.md](../../BUILDGUIDE.md) - Local build instructions
-- [Azure DevOps Documentation](https://learn.microsoft.com/azure/devops/pipelines/)
+- Always use templates for reusable logic — do not inline complex steps
+- Pass parameters explicitly; avoid relying on global variables
+- Use descriptive stage/job/step display names
+- When adding parameters, define them in the core template and thread through calling pipelines
+- When adding test categories, update filter expressions in test step templates
+- PR pipelines should run a minimal matrix for fast feedback
+- Test changes via PR pipeline first — validation runs automatically
+- Enable `debug: true` and `dotnetVerbosity: diagnostic` for troubleshooting
+- Never commit credentials or secrets in pipeline files
+- Signing and release are handled by OneBranch pipelines — not these CI/PR pipelines
diff --git a/.github/instructions/ado-work-items-markdown.instructions.md b/.github/instructions/ado-work-items-markdown.instructions.md
new file mode 100644
index 0000000000..2a70fd124a
--- /dev/null
+++ b/.github/instructions/ado-work-items-markdown.instructions.md
@@ -0,0 +1,139 @@
+---
+applyTo: "**"
+---
+# Azure DevOps Work Items: Markdown Description Rules
+
+Use this guide whenever creating or updating Azure DevOps work items that include rich text in `System.Description`.
+
+## Goals
+
+- Ensure descriptions render as Markdown (not HTML/plain text)
+- Preserve newline characters and list structure
+- Verify work items after every batch update
+
+## Required Behavior
+
+1. Always use `az rest` for description content-type changes.
+2. Use `application/json-patch+json` for PATCH requests.
+3. Set `multilineFieldsFormat.System.Description` to `markdown`.
+4. Preserve exact newlines in the Markdown body.
+5. Verify both format and newline integrity after updates.
+
+## Authentication and Resource
+
+Use Azure DevOps resource audience when calling `az rest`:
+
+- Resource: `499b84ac-1321-427f-aa17-267ca6975798`
+
+Example auth check:
+
+```bash
+az rest \
+ --method GET \
+ --resource 499b84ac-1321-427f-aa17-267ca6975798 \
+ --url "https://dev.azure.com//_apis/projects?api-version=7.1-preview.4"
+```
+
+Note: API versions can differ by endpoint. The examples below use `7.1-preview.3` for work item PATCH calls, while this auth check uses `7.1-preview.4`.
+
+## Safe Update Pattern (Prevents Type/Value Errors)
+
+Some work items reject a direct type switch unless a valid value is provided. Use this two-step process:
+
+### Step 1: Capture current description
+
+```bash
+az boards work-item show --id | jq -j '.fields["System.Description"] // ""' > ./desc-backup.txt
+```
+
+> **NOTE**: Writing to a file preserves all characters including trailing newlines.
+> Command substitution (`$(...)`) silently strips trailing newlines, which would
+> corrupt multi-line Markdown content.
+
+### Step 2: Force markdown type with temporary empty value
+
+```bash
+PATCH_STEP1_FILE="${PATCH_STEP1_FILE:-./patch-step1.json}"
+
+jq -n '[
+ {"op":"replace","path":"/fields/System.Description","value":""},
+ {"op":"replace","path":"/multilineFieldsFormat/System.Description","value":"markdown"}
+]' >"$PATCH_STEP1_FILE"
+
+az rest \
+ --method PATCH \
+ --resource 499b84ac-1321-427f-aa17-267ca6975798 \
+ --url "https://dev.azure.com///_apis/wit/workitems/?api-version=7.1-preview.3" \
+ --headers "Content-Type=application/json-patch+json" \
+ --body @"$PATCH_STEP1_FILE"
+```
+
+### Step 3: Restore exact Markdown text
+
+```bash
+PATCH_STEP2_FILE="${PATCH_STEP2_FILE:-./patch-step2.json}"
+
+jq -n --rawfile d ./desc-backup.txt '[
+ {"op":"replace","path":"/fields/System.Description","value":$d}
+]' >"$PATCH_STEP2_FILE"
+
+az rest \
+ --method PATCH \
+ --resource 499b84ac-1321-427f-aa17-267ca6975798 \
+ --url "https://dev.azure.com///_apis/wit/workitems/?api-version=7.1-preview.3" \
+ --headers "Content-Type=application/json-patch+json" \
+ --body @"$PATCH_STEP2_FILE"
+```
+
+## Newline Integrity Checks
+
+After updates, confirm newline characters are still present and structure was not flattened.
+
+### Check format and description sample
+
+```bash
+az boards work-item show --id | jq '.multilineFieldsFormat, .fields["System.Description"][0:200]'
+```
+
+Expected:
+
+- `multilineFieldsFormat.System.Description == "markdown"`
+- Description text contains `\n` where line breaks are expected
+
+### Check line count did not collapse
+
+```bash
+az boards work-item show --id \
+| jq -r '.fields["System.Description"]' \
+| awk 'END { print NR }'
+```
+
+If a multi-line description unexpectedly returns `1`, newline content was likely lost.
+
+## Batch Verification Script
+
+Use this after bulk updates:
+
+```bash
+python3 - <<'PY'
+import json, subprocess
+ids = [12345, 12346] # replace with your target IDs
+bad = []
+for i in ids:
+ out = subprocess.check_output(["az", "boards", "work-item", "show", "--id", str(i)], text=True)
+ j = json.loads(out)
+ fmt = (j.get("multilineFieldsFormat") or {}).get("System.Description")
+ desc = j.get("fields", {}).get("System.Description") or ""
+ if fmt != "markdown" or "\n" not in desc:
+ bad.append((i, fmt, "has_newlines" if "\n" in desc else "missing_newlines"))
+print("noncompliant:", len(bad))
+for row in bad:
+ print(row)
+PY
+```
+
+## Common Failure Modes
+
+- `401` or `TF400813`: wrong token audience or insufficient auth context
+- `Content-Type ... not supported`: must use `application/json-patch+json`
+- `type changed without a value`: use two-step pattern (empty + markdown type, then restore text)
diff --git a/.github/instructions/agentic-workflows.instructions.md b/.github/instructions/agentic-workflows.instructions.md
new file mode 100644
index 0000000000..6a42c0dce3
--- /dev/null
+++ b/.github/instructions/agentic-workflows.instructions.md
@@ -0,0 +1,49 @@
+---
+applyTo: ".github/workflows/**/*.md"
+description: Rules for editing gh-aw agentic workflow Markdown files.
+---
+
+# Agentic Workflow Edit Rules (`gh aw`)
+
+This repository authors GitHub Actions agentic workflows in Markdown using
+[`gh aw`](https://github.com/githubnext/gh-aw). Each workflow `.md` file under
+`.github/workflows/` compiles to a sibling `.lock.yml`, and **only the
+`.lock.yml` is executed by GitHub Actions at runtime.**
+
+## Mandatory rule
+
+Whenever you create, edit, rename, or delete a file matching
+`.github/workflows/**/*.md`, you **MUST**, in the **same commit / PR**:
+
+1. Run `gh aw compile` from the repository root.
+2. Stage and commit the regenerated sibling `.lock.yml`.
+3. If you deleted a workflow `.md`, also delete its `.lock.yml`.
+
+If the `.lock.yml` is stale or missing, the workflow fails at runtime
+(see PR #4279 for the exact failure mode). The
+`Verify gh aw lock files` CI check will block the PR in that case.
+
+## How to verify locally
+
+```bash
+gh aw compile
+git status # both the .md and .lock.yml should appear
+gh aw compile # second run must be a no-op (clean diff)
+```
+
+## Code-review checklist
+
+When reviewing a PR that touches `.github/workflows/**/*.md`:
+
+- [ ] A matching `.lock.yml` is updated in the same PR.
+- [ ] `gh aw compile` produces no further diff on top of the PR.
+- [ ] If new tools, network endpoints, or permissions are added in the `.md`,
+ they are present in the regenerated `.lock.yml`.
+
+## Out of scope
+
+- Do **not** hand-edit `.lock.yml` files. They are generated; edit the `.md`
+ source and recompile.
+- For deeper authoring guidance (creating, debugging, upgrading workflows),
+ invoke the `agentic-workflows` agent at
+ `.github/agents/agentic-workflows.agent.md`.
diff --git a/.github/instructions/architecture.instructions.md b/.github/instructions/architecture.instructions.md
index 3314babb7c..2fd30c9f55 100644
--- a/.github/instructions/architecture.instructions.md
+++ b/.github/instructions/architecture.instructions.md
@@ -34,10 +34,11 @@ src/
## Unified Project Model
### Architecture Goal
-The driver is transitioning away from separate `netfx/` and `netcore/` project files toward a **single unified project** at `src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj`. This project multi-targets all supported frameworks from one codebase:
+The driver is transitioning away from separate `netfx/` and `netcore/` project files toward a **single unified project** at `src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj`. This project targets the modern .NET TFMs on every host and conditionally adds .NET Framework on Windows:
```xml
-net462;net8.0;net9.0
+net8.0;net9.0
+$(TargetFrameworks);net462
```
**All new code MUST go into `src/Microsoft.Data.SqlClient/src/`**. Do NOT add files to the legacy `netcore/src/` or `netfx/src/` directories.
@@ -48,7 +49,7 @@ The `netcore/` and `netfx/` directories are legacy artifacts from the old dual-p
- `netcore/ref/` and `netfx/ref/` — **STILL ACTIVE**. Reference assemblies remain in these directories and define the public API surface for each target framework.
### OS Targeting with `TargetOs`
-The unified project uses a `TargetOs` MSBuild property to handle OS-specific compilation:
+The unified project uses a `TargetOs` build property to handle OS-specific compilation:
```xml
@@ -82,7 +83,7 @@ When writing code that differs by platform, use these preprocessor directives:
| `#if _UNIX` | Code for Unix/Linux/macOS OS (any framework) |
Guidelines:
-1. All code must compile for all target frameworks (`net462`, `net8.0`, `net9.0`)
+1. All code must compile for the TFMs supported by the current target OS: `net8.0`/`net9.0` everywhere, plus `net462` on Windows builds
2. Use `#if NETFRAMEWORK` or `#if NET` for framework-specific code paths
3. Use `#if _WINDOWS` or `#if _UNIX` for OS-specific code paths
4. Avoid APIs that don't exist on a target platform without conditional compilation
@@ -104,11 +105,15 @@ The `ref/` directories define the public API surface:
**IMPORTANT**: Any public API changes MUST update the corresponding reference assembly in the appropriate `ref/` directory.
### Build Output
-Build artifacts are organized by framework and OS:
+Build artifacts are organized by reference mode, configuration, OS, and framework:
```
-artifacts/Microsoft.Data.SqlClient/{Configuration}/{TargetOs}/{TargetFramework}/
+artifacts/Microsoft.Data.SqlClient/{ReferenceType}-{Configuration}/{NormalizedTargetOs}/{TargetFramework}/
```
+`ReferenceType` is a first-class build dimension in this branch. Local and CI builds may run in:
+- `Project` mode — sibling packages referenced as projects
+- `Package` mode — sibling packages restored from locally produced NuGet packages
+
## SNI (SQL Server Network Interface) Layer
Two implementations exist:
diff --git a/.github/instructions/connection-pooling.instructions.md b/.github/instructions/connection-pooling.instructions.md
index 6464fdedd0..95019b996d 100644
--- a/.github/instructions/connection-pooling.instructions.md
+++ b/.github/instructions/connection-pooling.instructions.md
@@ -124,13 +124,6 @@ private readonly ChannelWriter _idleConnectionWriter;
await _idleConnectionReader.WaitToReadAsync(token);
```
-### Sync Over Async Protection
-```csharp
-// Prevent thread pool starvation
-private static SemaphoreSlim _syncOverAsyncSemaphore =
- new(Math.Max(1, Environment.ProcessorCount / 2));
-```
-
## Best Practices
### Application Design
diff --git a/.github/instructions/features.instructions.md b/.github/instructions/features.instructions.md
index 3ecba2cc4e..34262b8db6 100644
--- a/.github/instructions/features.instructions.md
+++ b/.github/instructions/features.instructions.md
@@ -246,6 +246,7 @@ AppContext switches allow runtime behavior changes without modifying connection
| `Switch.Microsoft.Data.SqlClient.EnableMultiSubnetFailoverByDefault` | `false` | Sets `MultiSubnetFailover=true` as the default for all connections |
| `Switch.Microsoft.Data.SqlClient.EnableUserAgent` | varies | Controls sending user agent information to SQL Server |
| `Switch.Microsoft.Data.SqlClient.IgnoreServerProvidedFailoverPartner` | `false` | Ignores failover partner information sent by the server |
+| `Switch.Microsoft.Data.SqlClient.UseLegacyFailoverAlternationOnLoginSqlErrors` | `false` | Restores legacy `LoginWithFailover` alternation for login-phase SQL errors when parser state is not `Closed` |
| `Switch.Microsoft.Data.SqlClient.LegacyRowVersionNullBehavior` | `false` | Restores legacy null handling for rowversion columns |
| `Switch.Microsoft.Data.SqlClient.LegacyVarTimeZeroScaleBehaviour` | `false` | Restores legacy zero-scale behavior for time/datetime2/datetimeoffset |
| `Switch.Microsoft.Data.SqlClient.MakeReadAsyncBlocking` | `false` | Makes ReadAsync behave synchronously (legacy compat) |
diff --git a/.github/instructions/onebranch-pipeline-design.instructions.md b/.github/instructions/onebranch-pipeline-design.instructions.md
index 1c848e2493..02717a3a13 100644
--- a/.github/instructions/onebranch-pipeline-design.instructions.md
+++ b/.github/instructions/onebranch-pipeline-design.instructions.md
@@ -1,534 +1,179 @@
---
applyTo: "eng/pipelines/**/*.yml"
---
-# Multi-Product Azure DevOps Pipeline in dotnet/sqlclient — Design Specification
-
-## 1. Overview
-
-This document describes the design of the unified Azure DevOps YAML pipeline that builds, signs, packages, and optionally releases six NuGet packages with interdependencies. The pipeline uses **stages** and **jobs** to maximize parallelism while respecting dependency order. It comprises five stages: three build stages, a validation stage, and an on-demand release stage.
-
-Two pipeline variants exist from the same stage/job structure:
-
-| Pipeline | Template | Trigger | Purpose |
-|----------|----------|---------|---------|
-| `dotnet-sqlclient-official-pipeline.yml` | `OneBranch.Official.CrossPlat.yml` | CI + scheduled | Production-signed builds |
-| `dotnet-sqlclient-non-official-pipeline.yml` | `OneBranch.NonOfficial.CrossPlat.yml` | Manual only | Validation / test builds (release in dry-run mode) |
-
-Both pipelines use the **OneBranch (1ES) governed template** infrastructure and share identical stage definitions, job templates, and variable chains.
-
----
-
-## 2. Products and Dependencies
-
-| # | Package | Dependencies |
-|---|---------|-------------|
-| 1 | `Microsoft.SqlServer.Server` | — |
-| 2 | `Microsoft.Data.SqlClient.Internal.Logging` | — |
-| 3 | `Microsoft.Data.SqlClient.Extensions.Abstractions` | `Internal.Logging` |
-| 4 | `Microsoft.Data.SqlClient` | `Internal.Logging`, `Extensions.Abstractions` |
-| 5 | `Microsoft.Data.SqlClient.Extensions.Azure` | `Extensions.Abstractions`, `Internal.Logging` |
-| 6 | `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` | `SqlClient`, `Internal.Logging` |
-
----
-
-## 3. Pipeline Flow — Sequence Diagram
-
-```mermaid
-sequenceDiagram
- participant T as Trigger / User
- participant P as Pipeline Orchestrator
- participant B1a as Job: Build Internal.Logging
- participant B1c as Job: Build SqlServer.Server
- participant B1b as Job: Build Extensions.Abstractions
- participant B2a as Job: Build SqlClient
- participant B2b as Job: Build Extensions.Azure
- participant V as Job: Validate MDS Package
- participant B3 as Job: Build AKV Provider
- participant R as Stage: Release
-
- Note over T,R: ══════ BUILD & SIGN PHASE ══════
-
- T->>P: Pipeline triggered (CI / Scheduled / Manual)
-
- Note over P: Stage 1 — build_independent (parallel, no deps)
-
- par Stage 1 jobs (parallel)
- P->>B1a: Build DLLs → ESRP sign DLLs → Pack → ESRP sign NuGet (Logging)
- B1a-->>P: ✅ Signed .nupkg
- and
- P->>B1c: Build + ESRP sign + pack SqlServer.Server
- B1c-->>P: ✅ Signed .nupkg
- end
-
- Note over P: Stage 2 — build_abstractions (dependsOn: build_independent)
-
- P->>B1b: Build DLLs → ESRP sign DLLs → Pack → ESRP sign NuGet (Abstractions)
- Note right of B1b: Downloads: Internal.Logging artifact
- B1b-->>P: ✅ Signed .nupkg
-
- Note over P: Stage 3 — build_dependent (dependsOn: build_abstractions)
-
- par Stage 3 jobs (parallel)
- P->>B2a: Build + ESRP sign + pack SqlClient
- Note right of B2a: Downloads: Internal.Logging, Extensions.Abstractions artifacts
- B2a-->>P: ✅ Signed .nupkg + .snupkg
- and
- P->>B2b: Build DLLs → ESRP sign DLLs → Pack → ESRP sign NuGet (Azure)
- Note right of B2b: Downloads: Extensions.Abstractions, Internal.Logging artifacts
- B2b-->>P: ✅ Signed .nupkg
- end
-
- Note over P: Validation + Stage 4 (both dependsOn: build_dependent, run in parallel)
-
- par Validation and Stage 4 (parallel)
- P->>V: Validate signed MDS package
- V-->>P: ✅ Package validation passed
- and
- P->>B3: Build + ESRP sign + pack AKV Provider
- Note right of B3: Downloads: SqlClient, Internal.Logging artifacts
- B3-->>P: ✅ Signed .nupkg
- end
-
- Note over T,R: ══════ RELEASE PHASE (on-demand) ══════
-
- alt At least one release parameter is true
- P->>R: Stage: release (dependsOn: conditional on build stages)
- Note right of R: ADO Environment Approval (NuGet-Production environment)
- R-->>P: ✅ Approved
- Note right of R: Publish selected packages via NuGetCommand@2
- R-->>P: ✅ Published to NuGet
- else No release parameters set
- Note over P: Release stage skipped
- end
-
- Note over T,R: Pipeline complete 🎉
-```
-
----
-
-## 4. Stage Design
-
-### 4.1 Build Phase
-
-The build phase runs automatically on every CI trigger, scheduled run, or manual queue. It is divided into four build stages plus a validation stage, based on the dependency graph.
-
-#### Stage 1 — `build_independent`: Independent Packages (no dependencies)
-
-| Job Template | Package | Build Target | Condition |
-|--------------|---------|--------------|-----------|
-| `build-signed-csproj-package-job.yml` | `Microsoft.Data.SqlClient.Internal.Logging` | `BuildLogging` / `PackLogging` | `buildAKVProvider OR buildSqlClient` |
-| `build-signed-csproj-package-job.yml` | `Microsoft.SqlServer.Server` | `PackSqlServer` | `buildSqlServerServer` |
-
-- **`dependsOn`**: none
-- **Parallelism**: Jobs run in parallel (depending on which are enabled)
-- **Conditional builds**: Each job is wrapped with compile-time `${{ if }}` conditionals based on build parameters
-- csproj-based jobs (`build-signed-csproj-package-job.yml`) perform: **Build DLLs → ESRP DLL signing → NuGet pack (NoBuild=true) → ESRP NuGet signing** → publish artifact
-
-#### Stage 2 — `build_abstractions`: Abstractions Package (depends on Stage 1)
-
-| Job Template | Package | Build Target | Artifact Dependencies |
-|--------------|---------|--------------|----------------------|
-| `build-signed-csproj-package-job.yml` | `Microsoft.Data.SqlClient.Extensions.Abstractions` | `BuildAbstractions` / `PackAbstractions` | `Internal.Logging` |
-
-- **Stage condition**: `buildSqlClient = true` (entire stage is excluded when false)
-- **`dependsOn`**: `build_independent`
-- Downloads `Microsoft.Data.SqlClient.Internal.Logging.nupkg` (from Stage 1) pipeline artifact
-
-#### Stage 3 — `build_dependent`: Core Packages (depend on Stage 2)
-
-| Job Template | Package | Build Target | Artifact Dependencies |
-|--------------|---------|--------------|----------------------|
-| `build-signed-package-job.yml` | `Microsoft.Data.SqlClient` | *(nuspec-based)* | `Internal.Logging`, `Extensions.Abstractions` |
-| `build-signed-csproj-package-job.yml` | `Microsoft.Data.SqlClient.Extensions.Azure` | `BuildAzure` / `PackAzure` | `Extensions.Abstractions`, `Internal.Logging` |
-
-- **Stage condition**: `buildSqlClient = true` (entire stage is excluded when false)
-- **`dependsOn`**: `build_abstractions`
-- **Parallelism**: Both jobs run in parallel
-- The MDS (SqlClient) job also publishes symbol packages (`.snupkg`) when `publishSymbols` is true
-- All jobs configure APIScan with job-level `ob_sdl_apiscan_*` variables targeting package-specific folders
-
-#### Stage 4 — `build_addons`: Add-on Packages (depend on Stage 3)
-
-| Job Template | Package | Artifact Dependencies |
-|--------------|---------|----------------------|
-| `build-akv-official-job.yml` | `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` | `SqlClient`, `Internal.Logging` |
-
-- **Stage condition**: `buildAKVProvider AND buildSqlClient` (both must be true)
-- **`dependsOn`**: `build_dependent`
-- Downloads `Microsoft.Data.SqlClient.nupkg` (from Stage 3) and `Microsoft.Data.SqlClient.Internal.Logging.nupkg` (from Stage 1) pipeline artifacts
-- Uses separate ESRP signing credentials (`Signing`-prefixed variables from `esrp-variables-v2` group)
-
-### 4.2 Validation Stage — `mds_package_validation`
-
-Validates the signed MDS (SqlClient) package after Stage 3 completes.
-
-- **Stage condition**: `buildSqlClient = true`
-- **`dependsOn`**: `build_dependent`
-- Runs in parallel with Stage 4 (`build_addons`)
-- Uses `validate-signed-package-job.yml` template
-- Downloads the `drop_build_dependent_build_signed_package` artifact and validates against `CurrentNetFxVersion` (default: `net462`)
-
-### 4.3 Release Phase — `release`
-
-The release stage is gated and only executes on demand when at least one release parameter is set to `true` at queue time.
-
-- **`dependsOn`**: Conditional based on which build stages are enabled:
- - `build_independent` (when releasing SqlServer.Server or Logging)
- - `build_abstractions` (when releasing Abstractions)
- - `build_dependent`, `mds_package_validation` (when `buildSqlClient = true`)
- - `build_addons` (when `buildAKVProvider AND buildSqlClient`)
-- **Gate**: ADO Environment approvals (official pipeline only):
- - Official: `NuGet-Production` environment with configured approvals
- - Non-Official: `NuGet-DryRun` environment (no approvals, validation only)
-- **Package selection**: Controlled by 6 runtime boolean parameters (see Section 5.2)
-- **Stage condition**: The entire stage is skipped unless at least one release parameter is `true`:
- ```yaml
- - ${{ if or(parameters.releaseSqlServerServer, parameters.releaseLogging, ...) }}:
- - stage: release
- ```
-- **Publish jobs**: Each package has a conditional publish job that is included at compile time only when its parameter is `true`:
- ```yaml
- - ${{ if eq(parameters.releaseXxx, true) }}:
- - template: /eng/pipelines/onebranch/jobs/publish-nuget-package-job.yml@self
- ```
-- **Environment variables**: Stage sets `ob_release_usedeploymentjob: true` for OneBranch integration:
- - Official: `ob_release_environment: 'NuGet-Production'`
- - Non-Official: `ob_release_environment: 'NuGet-DryRun'`
-
-#### Artifact → Publish Job Mapping
-
-| Package | Artifact Name | Publish Job |
-|---------|---------------|-------------|
-| `Microsoft.SqlServer.Server` | `drop_build_independent_build_package_SqlServer` | `publish_SqlServer_Server` |
-| `Microsoft.Data.SqlClient.Internal.Logging` | `drop_build_independent_build_package_Logging` | `publish_Logging` |
-| `Microsoft.Data.SqlClient.Extensions.Abstractions` | `drop_build_abstractions_build_package_Abstractions` | `publish_Abstractions` |
-| `Microsoft.Data.SqlClient` | `drop_build_dependent_build_package_SqlClient` | `publish_SqlClient` |
-| `Microsoft.Data.SqlClient.Extensions.Azure` | `drop_build_dependent_build_package_Azure` | `publish_Extensions_Azure` |
-| `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` | `drop_build_addons_buildSignedAkvPackage` | `publish_AKVProvider` |
-
-Each publish job uses the reusable `publish-nuget-package-job.yml` template, which downloads the artifact and pushes `.nupkg`/`.snupkg` files via `NuGetCommand@2` with an external feed service connection.
-
-#### Dry-Run Mode
-
-Two ADO environments control release behavior:
-
-| Environment | Pipeline | Behavior |
-|------------|----------|----------|
-| `NuGet-DryRun` | Non-Official | Validation only — packages are never pushed |
-| `NuGet-Production` | Official | Real releases with approval gate |
-
-**Non-official pipeline**: Always runs in dry-run mode. There is no `releaseDryRun` parameter — `dryRun: true` is hardcoded in every publish job. This prevents accidental publication from validation builds.
-
-**Official pipeline**: Exposes a `releaseDryRun` parameter (default: `true` for safety). When enabled, the template downloads artifacts and lists the `.nupkg`/`.snupkg` files that *would* be published but skips the actual `NuGetCommand@2` push. Set `releaseDryRun: false` to perform real pushes after final validation.
-
----
-
-## 5. Runtime Parameters
-
-### 5.1 Build Parameters
-
-The pipeline exposes the following parameters at queue time:
-
-```yaml
-parameters:
- - name: debug
- displayName: 'Enable debug output'
- type: boolean
- default: false
-
- - name: publishSymbols
- displayName: 'Publish symbols'
- type: boolean
- default: false
-
- - name: CurrentNetFxVersion
- displayName: 'Lowest supported .NET Framework version (MDS validation)'
- type: string
- default: 'net462'
-
- - name: isPreview
- displayName: 'Is this a preview build?'
- type: boolean
- default: false
-
- - name: testJobTimeout
- displayName: 'Test job timeout (in minutes)'
- type: number
- default: 60
-
- # Build parameters — control which packages to build
- - name: buildSqlServerServer
- displayName: 'Build Microsoft.SqlServer.Server'
- type: boolean
- default: true
-
- - name: buildSqlClient
- displayName: 'Build Microsoft.Data.SqlClient and Extensions'
- type: boolean
- default: true
-
- - name: buildAKVProvider
- displayName: 'Build Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider'
- type: boolean
- default: true
-```
-
-The `isPreview` parameter controls version resolution — when `true`, each package uses its preview version (e.g., `loggingPackagePreviewVersion`) instead of the GA version (e.g., `loggingPackageVersion`). All versions are defined in the centralized `libraries/common-variables.yml`.
-
-The build parameters enable selective package building:
-- `buildSqlServerServer` — controls SqlServer.Server build job
-- `buildSqlClient` — controls MDS, Extensions.Azure, Abstractions, Logging (when AKV is disabled), and validation stages
-- `buildAKVProvider` — controls AKV Provider build (also requires `buildSqlClient=true`) and Logging (when SqlClient is disabled)
-
-When set to `false`, the respective jobs/stages are excluded at compile-time using `${{ if }}` conditionals. This allows faster pipeline runs when only certain packages need to be built.
-
-### 5.2 Release Parameters
-
-Six boolean parameters control selective package release. All default to `false` so the release stage is skipped on normal CI/scheduled builds:
-
-```yaml
-parameters:
- - name: releaseSqlServerServer
- displayName: 'Release Microsoft.SqlServer.Server'
- type: boolean
- default: false
-
- - name: releaseLogging
- displayName: 'Release Microsoft.Data.SqlClient.Internal.Logging'
- type: boolean
- default: false
-
- - name: releaseAbstractions
- displayName: 'Release Microsoft.Data.SqlClient.Extensions.Abstractions'
- type: boolean
- default: false
-
- - name: releaseSqlClient
- displayName: 'Release Microsoft.Data.SqlClient'
- type: boolean
- default: false
-
- - name: releaseAzure
- displayName: 'Release Microsoft.Data.SqlClient.Extensions.Azure'
- type: boolean
- default: false
-
- - name: releaseAKVProvider
- displayName: 'Release Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider'
- type: boolean
- default: false
-```
-
-#### Dry-Run Parameter (Official Pipeline Only)
-
-The **official pipeline** includes a `releaseDryRun` parameter that defaults to `true` for safety:
-
-```yaml
- - name: releaseDryRun
- displayName: 'Release Dry Run (do not push to NuGet)'
- type: boolean
- default: true # safety default — must explicitly disable for real releases
-```
-
-When `releaseDryRun: true`, publish jobs download artifacts and list packages but skip actual NuGet push. Set to `false` for production releases.
-
-> **Note**: The non-official pipeline does **not** expose this parameter — dry-run mode is hardcoded and cannot be disabled.
-
----
-
-## 6. Variable & Version Management
-
-### 6.1 Variable Chain
-
-Variables are defined in a layered template chain. All variable groups live inside the templates — none are declared inline at the pipeline level:
-
-```
-dotnet-sqlclient-official-pipeline.yml
- └─ libraries/variables.yml
- └─ libraries/build-variables.yml
- ├─ group: 'Release Variables'
- ├─ group: 'Symbols publishing' ← SymbolsPublishServer, SymbolsPublishTokenUri, etc.
- └─ libraries/common-variables.yml
- ├─ group: 'ESRP Federated Creds (AME)' ← ESRP signing credentials
- ├─ SymbolServer / SymbolTokenUri aliases ← mapped from Symbols publishing group
- └─ all package versions, paths, build variables
-```
-
-### 6.2 Package Version Variables
-
-All package versions are centralized in `libraries/common-variables.yml`:
-
-| Package | GA Version Var | Preview Version Var | Assembly Version Var |
-|---------|---------------|--------------------|--------------------|
-| Logging | `loggingPackageVersion` | `loggingPackagePreviewVersion` | `loggingAssemblyFileVersion` |
-| Abstractions | `abstractionsPackageVersion` | `abstractionsPackagePreviewVersion` | `abstractionsAssemblyFileVersion` |
-| SqlServer.Server | `sqlServerPackageVersion` | `sqlServerPackagePreviewVersion` | `sqlServerAssemblyFileVersion` |
-| SqlClient (MDS) | `mdsPackageVersion` | `mdsPackagePreviewVersion` | `mdsAssemblyFileVersion` |
-| Extensions.Azure | `azurePackageVersion` | `azurePackagePreviewVersion` | `azureAssemblyFileVersion` |
-| AKV Provider | `akvPackageVersion` | `akvPackagePreviewVersion` | `akvAssemblyFileVersion` |
-
-The pipeline resolves `effective*Version` variables at compile time based on the `isPreview` parameter.
-
-### 6.3 Release & Symbol Variables
-
-| Variable | Defined In | Purpose |
-|----------|-----------|---------|
-| `NuGetServiceConnection` | `libraries/common-variables.yml` | External NuGet service connection name for `NuGetCommand@2` push |
-| `SymbolServer` | `libraries/common-variables.yml` (alias) | Alias for `$(SymbolsPublishServer)` — used by MDS `publish-symbols-step.yml` |
-| `SymbolTokenUri` | `libraries/common-variables.yml` (alias) | Alias for `$(SymbolsPublishTokenUri)` — used by MDS `publish-symbols-step.yml` |
-
-### 6.4 Variable Groups
-
-| Group | Included In | Purpose |
-|-------|------------|---------|
-| `Release Variables` | `build-variables.yml` | Release-specific configuration |
-| `Symbols publishing` | `build-variables.yml` | Symbol publishing credentials (`SymbolsAzureSubscription`, `SymbolsPublishServer`, `SymbolsPublishTokenUri`, `SymbolsUploadAccount`, `SymbolsPublishProjectName`) |
-| `ESRP Federated Creds (AME)` | `common-variables.yml` | Federated identity for ESRP signing (`ESRPConnectedServiceName`, `ESRPClientId`, `AppRegistrationClientId`, `AppRegistrationTenantId`, `AuthAKVName`, `AuthSignCertName`) |
-
----
-
-## 7. Code Signing (ESRP)
-
-All packages are signed using **ESRP (Enterprise Security Release Pipeline)** with federated identity authentication.
-
-### Signing Flow (per job)
-
-#### csproj-based Extension Packages (Logging, Abstractions, Azure)
-1. **Build DLLs only** — `build.proj` target (e.g., `BuildLogging`) compiles assemblies without creating NuGet packages
-2. **ESRP DLL signing** — Assemblies are signed with Authenticode certificates via ESRP
-3. **NuGet pack** — `build.proj` pack target (e.g., `PackLogging`) creates `.nupkg` from signed DLLs using `NoBuild=true`
-4. **ESRP NuGet signing** — The `.nupkg` files are signed with NuGet certificates via ESRP
-
-This workflow ensures the NuGet package contains **signed DLLs** rather than signing the NuGet package around unsigned assemblies.
-
-#### nuspec-based Packages (SqlServer.Server, SqlClient, AKV Provider)
-1. **Build + pack** — MSBuild creates both assemblies and NuGet packages
-2. **ESRP DLL signing** — Assemblies are signed with Authenticode certificates via ESRP
-3. **ESRP NuGet signing** — The `.nupkg` files are signed with NuGet certificates via ESRP
-
-### Credential Model
-
-- Extension packages (Logging, Abstractions, Azure, SqlServer.Server, SqlClient) use the primary ESRP credentials from the `ESRP Federated Creds (AME)` variable group (loaded via `common-variables.yml`)
-- AKV Provider uses separate `Signing`-prefixed credential parameters that are passed explicitly to the `build-akv-official-job.yml` template
-- All credentials are sourced from Azure Key Vault and federated identity — no secrets stored in pipeline YAML
-
----
-
-## 8. SDL & Compliance (OneBranch)
-
-Both pipelines use **OneBranch governed templates** for 1ES compliance. The SDL configuration differs between Official and Non-Official:
-
-| SDL Tool | Official | Non-Official | Purpose |
-|----------|----------|--------------|---------|
-| **TSA** | ✅ `enabled: true` | ❌ `enabled: false` | Uploads SDL results to TSA for downstream analysis |
-| **ApiScan** | ✅ `enabled: true`, `break: true` | ✅ `enabled: true`, `break: true` | Scans APIs for compliance issues |
-| **CodeQL** | ✅ (non-preview) | ✅ (non-preview) | Static analysis for security vulnerabilities |
-| **SBOM** | ✅ (non-preview) | ✅ (non-preview) | Software Bill of Materials generation |
-| **Policheck** | ✅ `break: true` | ✅ `break: true` | Scans for policy-violating content |
-| **BinSkim** | ✅ (async, non-preview) | ✅ (async, non-preview) | Binary security analysis |
-| **CredScan** | ✅ (async, non-preview) | ✅ (async, non-preview) | Credential leak detection |
-| **Roslyn** | ✅ (async, non-preview) | ✅ (async, non-preview) | Roslyn-based security analyzers |
-| **Armory** | ✅ `break: true` | ✅ `break: true` | Additional security scanning |
-
-### APIScan Configuration
-
-APIScan is configured at **both pipeline level and job level**:
-
-**Pipeline-level** (`globalSdl:apiscan:`): Sets default configuration inherited by all jobs. This is configured for MDS (Microsoft.Data.SqlClient) as the primary product.
-
-**Job-level** (`ob_sdl_apiscan_*` variables): Each build job overrides the pipeline defaults with package-specific settings:
-
-| Variable | Purpose |
-|----------|---------|
-| `ob_sdl_apiscan_enabled` | Enable/disable APIScan for this job (`true`) |
-| `ob_sdl_apiscan_softwareFolder` | Path to signed DLLs for scanning |
-| `ob_sdl_apiscan_symbolsFolder` | Path to PDBs for scanning |
-| `ob_sdl_apiscan_softwarename` | Package name (e.g., `Microsoft.Data.SqlClient.Internal.Logging`) |
-| `ob_sdl_apiscan_versionNumber` | Assembly file version |
-
-Each job copies its signed DLLs and PDBs to a package-specific folder under `$(Build.SourcesDirectory)/apiScan//` after ESRP DLL signing, ensuring APIScan analyzes the correct signed binaries for each package.
-
-> **PRC Compliance**: The Official pipeline hardcodes `OneBranch.Official.CrossPlat.yml` (not parameterized) to satisfy Production Readiness Check static verification requirements.
-
----
-
-## 9. Artifact Strategy
-
-- Each build job publishes its output as a **pipeline artifact** managed by OneBranch's `ob_outputDirectory` convention.
-- Artifact names follow the OneBranch auto-generated pattern: `drop__` (e.g., `drop_build_dependent_build_package_SqlClient`).
-- Downstream stages use `DownloadPipelineArtifact@2` to pull required packages into a local directory.
-- A local NuGet source is configured at build time pointing to the downloaded artifacts directory so `dotnet restore` resolves internal dependencies.
-
----
-
-## 10. Trigger Configuration
-
-### Official Pipeline (`dotnet-sqlclient-official-pipeline.yml`)
-
-```yaml
-trigger:
- branches:
- include:
- - internal/main
- paths:
- include:
- - .azuredevops
- - .config
- - doc
- - eng/pipelines
- - src
- - tools
- - azurepipelines-coverage.yml
- - build.proj
- - NuGet.config
-
-schedules:
- - cron: '30 4 * * Mon' # Weekly Sunday 9:30 PM (UTC-7)
- branches: { include: [internal/main] }
- always: true
- - cron: '30 3 * * Mon-Fri' # Weekday 8:30 PM (UTC-7)
- branches: { include: [internal/main] }
-```
-
-- **CI trigger**: Runs on pushes to `internal/main` when relevant paths change
-- **Scheduled**: Weekly full build (Sundays) + weekday builds (Mon–Fri)
-- **No PR trigger**: Official pipeline should not run on PRs (separate PR pipelines exist)
-
-### Non-Official Pipeline (`dotnet-sqlclient-non-official-pipeline.yml`)
-
-```yaml
-trigger: none
-pr: none
-```
-
-- **Manual only**: Queued on-demand for validation/test builds
-
----
-
-## 11. Infrastructure
-
-| Concern | Implementation |
-|---------|---------------|
-| **Pipeline template** | OneBranch governed templates (`OneBranch.Pipelines/GovernedTemplates`) |
-| **Build agents** | OneBranch-managed Windows containers (`WindowsHostVersion: 1ESWindows2022`) |
-| **.NET SDK** | Pinned via `global.json` (with `useGlobalJson: true` in install steps) |
-| **Code signing** | ESRP v2 with federated identity (Azure Key Vault backed) |
-| **Symbol publishing** | Optional, controlled by `publishSymbols` parameter; uses `Symbols publishing` variable group (aliases `SymbolServer`/`SymbolTokenUri` defined in `common-variables.yml`) |
-
----
-
-## 12. Key Design Decisions
-
-1. **Single pipeline, multiple stages** — avoids managing 6 separate pipelines while keeping clear separation of concerns.
-2. **Official + Non-Official variants** — hardcoded OneBranch templates (no parameterized `oneBranchType`) for PRC compliance; Non-Official variant allows manual validation builds.
-3. **Parallel jobs within stages** — minimizes total wall-clock time; only waits where dependencies demand it.
-4. **Pipeline artifacts over Universal Packages** — faster, ephemeral, scoped to the run; appropriate for build-time dependency resolution.
-5. **ESRP-based code signing** — all DLLs and NuGet packages are signed in-pipeline using ESRP with federated identity; no secrets in YAML.
-6. **Centralized version management** — all 6 package versions (GA + preview) defined once in `libraries/common-variables.yml`; `isPreview` toggle selects the active set.
-7. **Dependency-aware stage ordering** — ensures packages are always built after their dependencies, guaranteeing consistent, reproducible builds.
-8. **Validation in parallel with Stage 3** — MDS package validation runs alongside AKV Provider build (both depend on Stage 2), reducing total pipeline duration.
-9. **Selective on-demand release** — 6 boolean parameters control which packages are published; the release stage is entirely skipped when none are selected, keeping normal CI builds unaffected.
-10. **ADO Environment approval gate** — two environments: `NuGet-Production` (official, with configured approvals) and `NuGet-DryRun` (non-official, validation only). Both use `ob_release_environment` for OneBranch integration.
-11. **Compile-time conditional publish jobs** — `${{ if eq(parameters.releaseXxx, true) }}` template expansion ensures unselected publish jobs are excluded entirely from the pipeline run (not just skipped at runtime).
-12. **Mandatory dry-run for non-official** — the non-official variant hardcodes `dryRun: true` (no parameter), preventing accidental publication. The official variant defaults `releaseDryRun: true` for safety but allows override for actual releases.
-13. **Selective build parameters** — `buildSqlClient`, `buildSqlServerServer`, and `buildAKVProvider` allow building subsets of packages, with dependency-aware conditionals ensuring Logging builds when either SqlClient or AKV is needed.
+# OneBranch Pipeline Guidelines
+
+## Purpose
+
+Rules and conventions for editing the OneBranch Azure DevOps YAML pipelines that build, sign, package, and release six NuGet packages with interdependencies.
+
+## Pipeline Variants
+
+- `sqlclient-official.yml` — Official pipeline; uses `OneBranch.Official.CrossPlat.yml`; runs on a daily schedule at 23:00 UTC on `internal/release/7.1`, with no activity-based trigger (`pr: none`, `trigger: none`)
+- `sqlclient-non-official.yml` — Non-Official pipeline; uses `OneBranch.NonOfficial.CrossPlat.yml`; manual only (`pr: none`, `trigger: none`)
+- Both live under `eng/pipelines/onebranch/` and extend OneBranch governed templates
+- Never parameterize the OneBranch template name — hardcode it per pipeline for PRC compliance
+- Official pipeline must never be run on PRs or dev branches.
+
+## Package Dependency Order
+
+Respect this graph when modifying build stages:
+
+1. `Microsoft.SqlServer.Server` — no dependencies
+2. `Microsoft.Data.SqlClient.Internal.Logging` — no dependencies
+3. `Microsoft.Data.SqlClient.Extensions.Abstractions` — depends on Logging
+4. `Microsoft.Data.SqlClient` — depends on Logging + Abstractions
+5. `Microsoft.Data.SqlClient.Extensions.Azure` — depends on Abstractions + Logging
+6. `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` — depends on SqlClient + Abstractions + Logging
+
+## Localization Validation
+
+The SqlClient build job runs `steps/validate-localization-step.yml` before building the driver. Validation always fails the build for missing or obsolete keys, empty localized values whose English value is non-empty, and untranslated resources. Approved identical translations are listed by culture and resource key in `.config/LocalizationValidationAllowlist.json`.
+
+## Build Stages
+
+Defined in `stages/build-stages.yml`. Four build stages plus package validation are ordered by dependency:
+
+- **`build_independent`** (Stage 1) — Logging and SqlServer.Server in parallel; no inter-package dependencies
+- **`build_abstractions`** (Stage 2) — Abstractions; `dependsOn: build_independent`; downloads Logging artifact
+- **`build_dependent`** (Stage 3) — SqlClient and Extensions.Azure in parallel; `dependsOn: build_abstractions`; downloads Abstractions + Logging artifacts
+- **`build_addons`** (Stage 4) — AKV Provider; `dependsOn: build_dependent`; downloads SqlClient + Abstractions + Logging artifacts
+- **`package_validation`** (Stage 5) — Validates every package produced by the run; `dependsOn` all four build stages plus `compute_versions`
+
+Each build job copies PDB files into `$(JOB_OUTPUT)/symbols/` so they are included in the auto-published pipeline artifact alongside the NuGet packages in `$(JOB_OUTPUT)/packages/`.
+
+Stage conditional rules:
+- The SqlClient family (Logging, Abstractions, SqlClient, Azure, AKV Provider) is **always built** — Stages 2, 3, and 4 and the Logging job in Stage 1 are unconditional. There is no `buildSqlClient`/`buildAKVProvider` toggle.
+- `buildSqlServer` is the only build toggle; it controls just the SqlServer.Server job in Stage 1.
+- When `buildSqlServer` is true, SqlClient/AKV depend on the freshly-built SqlServer artifact (downloaded into the local feed). When false, they depend on the most recently published SqlServer package — a version-only dependency (no artifact download) restored from NuGet.
+
+## Job Templates
+
+- **`build-buildproj-job.yml`** — Shared build.proj-driven package job used for all shipped packages. Flow: build via `build.proj` → optional ESRP DLL signing → pack via `build.proj` → optional ESRP NuGet signing → copy outputs for APIScan/artifacts
+- **`validate-packages-job.yml`** — Validates every package produced by the run. Downloads all package artifacts into one tree and validates them together, so `tools/PackageValidator` can apply its cross-package rules (the SqlClient family must share one version, and inter-package dependency ranges must agree); validating per package would silently skip those findings. Runs on Windows because Authenticode verification has no Linux equivalent
+- **`publish-nuget-package-job.yml`** — Reusable release job using OneBranch `templateContext.type: releaseJob` with `inputs` for artifact download; pushes via `NuGetCommand@2`
+- **`publish-symbols-job.yml`** — Reusable symbols job: downloads a build artifact, locates PDBs under `symbols/`, and invokes `publish-symbols-step.yml`
+
+When adding a new package to the OneBranch flow:
+- Extend `build-buildproj-job.yml` inputs with the new package metadata and dependency artifacts
+- Add or update the corresponding build/pack targets in `build.proj`
+- Add version variables to `variables/common-variables.yml`
+- Add artifact name variables to `variables/onebranch-variables.yml`
+
+## Package Validation Stage
+
+- Defined in `stages/build-stages.yml`; produces stage `package_validation`
+- Consumes the package and file versions published by `compute_versions` and asserts the produced packages carry exactly those values, so nothing is re-derived
+- All packages are validated together in one job so `tools/PackageValidator` can apply cross-package rules; the SqlServer artifact and its expectations are conditional on `buildSqlServer`
+- Expectations use the validator's `[id=]value` form: the SqlClient family version is applied as a wildcard (proving the family agrees, and catching the case where all packages are consistently wrong), with `Microsoft.SqlServer.Server` as a per-id override
+- When SqlServer is not built its expectations are **omitted entirely** rather than passed empty — the validator rejects an expectation with an empty value
+- Gate categories are derived from `isOfficial`: `error`, `missing-symbols`, `dependency-inconsistency`, `delay-signed`, and `unsigned` always, plus `package-unsigned` on official runs only. The `error` severity covers only error-severity findings, so each warning/info category must be named explicitly — `missing-symbols`, `dependency-inconsistency`, and `delay-signed` are warnings, and `unsigned` and `package-unsigned` are info. Strong-name signing is unconditional in `build-buildproj-step.yml`, so the two strong-name categories gate everywhere; NuGet package signing is ESRP and official-only, so `package-unsigned` would fire on every non-official run
+- The validator runs twice: once with `--json` and no gate so the report exists even for a failing run, then once human-readable with the gate so failures appear in the job log
+- Signature verification (`dotnet nuget verify --all`, Authenticode) runs on official builds only, and verifies that signatures are *trusted* — PackageValidator reports only their presence, from metadata
+- The release stage `dependsOn: package_validation`, so a package that fails validation is never published
+- Step and job logic lives in `scripts/validate-packages.ps1`, `scripts/verify-package-signatures.ps1`, and `scripts/verify-assembly-signatures.ps1`, each with Pester tests under `scripts/tests/`
+
+## Symbols Publishing Stage
+
+- Defined in `stages/publish-symbols-stage.yml`; produces stage `publish_symbols`
+- Entire stage excluded at compile time when `publishSymbols` is false
+- The SqlClient family symbols are always published; the SqlServer.Server symbols job is conditional on `buildSqlServer`
+- `dependsOn` covers all family build stages (always present), plus `build_independent` for SqlServer
+- One job per package (`publish-symbols-job.yml`), each downloading its build artifact and publishing PDBs from `symbols/`
+- Each package's PDBs are published separately with unique artifact names and version information
+- Build jobs copy PDBs into `$(JOB_OUTPUT)/symbols/` so they are included in the auto-published artifact
+- The `publish-symbols-step.yml` accepts a `symbolsFolder` parameter to point at the downloaded PDB location
+- The publish step calls an extracted `publish-symbols.ps1` script with structured error handling and diagnostic logging
+- Symbols publishing credentials come from the `Symbols Publishing` variable group
+- In the official pipeline, symbol server destination follows `releaseToProduction`: Production when true, PPE when false
+- Non-official pipeline always targets the PPE symbol server
+
+## Release Stage
+
+- Defined in `stages/release-stages.yml`; produces stage `release_production` (official) or `release_test` (non-official) via `stageNameSuffix` parameter
+- Entire stage excluded at compile time when no release parameters are true
+- `dependsOn` is conditional based on which release parameters are set
+- `releaseToProduction` parameter controls NuGet target feed:
+ - `true` → service connection `ADO Nuget Org Connection` (NuGet Production)
+ - `false` → service connection `ADO Nuget Org Test Connection` (NuGet Test)
+- Non-official pipeline always sets `releaseToProduction: false`
+- Environment gating:
+ - Official: `ob_release_environment: Production`, `ob_deploymentjob_environment: NuGet-Production`
+ - Non-official: `ob_release_environment: Test`, `ob_deploymentjob_environment: NuGet-DryRun`
+- Each publish job uses OneBranch deployment job syntax (`templateContext.type: releaseJob` with `inputs` for artifact download)
+
+## Parameters
+
+Build parameters:
+- `debug` — enable debug output (default `false`)
+- `isPreview` — use preview version numbers (default `false`)
+- `publishSymbols` — publish symbols to servers (default `false`)
+- `buildSqlServer` — build the Microsoft.SqlServer.Server package (default `true` in the non-official/nightly pipeline, `false` in the official pipeline). The SqlClient family is always built, so this is the only build toggle. It also drives the SqlServer dependency version the family uses (built/next vs published). Requesting `releaseSqlServer` without `buildSqlServer` fails template expansion.
+
+Release parameters (boolean, default `false`):
+- `releaseSqlClient` — release the entire SqlClient family together (Logging, Abstractions, SqlClient, Azure, AKV Provider) at the shared version
+- `releaseSqlServer` — release Microsoft.SqlServer.Server (versioned separately)
+
+Official-only parameter:
+- `releaseToProduction` — controls both NuGet target feed and symbol server destination (default `false`):
+ - `true` → NuGet Production feed + Production symbol server
+ - `false` → NuGet Test feed + PPE symbol server
+
+When `isPreview` is true, pipeline resolves `effective*Version` variables to preview versions; otherwise GA versions. All versions defined in `variables/common-variables.yml`.
+
+## Variables and Versions
+
+- Variable chain: pipeline YAML → `variables/onebranch-variables.yml` → `variables/common-variables.yml`
+- All package versions (GA, preview, assembly file) centralized in `variables/common-variables.yml`
+- The `compute_versions` stage reads canonical versions from MSBuild and publishes effective package,
+ file-build, and APIScan registration versions for downstream stages
+- Artifact name variables defined in `variables/onebranch-variables.yml` following `drop__` pattern
+- `assemblyBuildNumber` derived from first segment of `Build.BuildNumber` only (16-bit limit)
+- When adding a new package, add GA version, preview version, and assembly file version entries
+
+Variable groups:
+- `Symbols Publishing` — symbol publishing credentials (in `onebranch-variables.yml`)
+- `ESRP Federated Creds (AME)` — ESRP signing credentials (in `common-variables.yml`)
+
+## Code Signing (ESRP)
+
+- Uses ESRP v6 tasks (`EsrpMalwareScanning@6`, `EsrpCodeSigning@6`) with MSI/federated identity authentication
+- Signing only runs when `isOfficial: true` — non-official pipelines skip ESRP steps
+- The shared OneBranch job signs DLLs before packing and signs the resulting NuGet package afterward so the published package contains signed binaries
+- DLL signing uses keyCode `CP-230012` (Authenticode); NuGet signing uses keyCode `CP-401405`
+- All ESRP credentials come from variable groups — never hardcode secrets in YAML
+
+## SDL and Compliance
+
+- TSA: enabled only in official pipeline; disabled in non-official to avoid spurious alerts
+- ApiScan: enabled in both; `break` follows the `breakOnSdlError` parameter
+- Each package is registered with APIScan under its own name/version pair, so the `globalSdl.apiscan` blocks deliberately omit `softwareName`/`versionNumber`. `build-buildproj-job.yml` is the single place they are set, via `ob_sdl_apiscan_softwareName` (the package's `packageFullName`) and `ob_sdl_apiscan_versionNumber` (the `apiScanSoftwareVersion` parameter)
+- `compute-versions.ps1` derives APIScan registration versions as major.minor from the effective canonical package versions and publishes them as stage outputs. A package name/version pair must still be registered with APIScan before releasing a new major.minor. Consume these as runtime `$(...)` references so values such as `1.0` remain strings rather than being coerced to numbers by template expressions
+- Jobs that produce no assemblies (symbol publishing, signed-package validation, version computation) set `ob_sdl_apiscan_enabled: false` rather than reporting a name/version
+- Each build job also sets `ob_sdl_apiscan_softwareFolder` and `ob_sdl_apiscan_symbolsFolder` to its per-package `apiScan//dlls` and `apiScan//pdbs` paths
+- CodeQL, SBOM, Policheck (`break: true`): enabled in both pipelines
+- SBOM package name/version are resolvable **only** from the pipeline's `globalSdl.sbom` block — OneBranch's artifact-publishing path reads `globalSdl.sbom.packageName`/`packageVersion` directly and has no per-job equivalent (the `templateContext.sdl.sbom` override only applies to the native 1ES Stages entry point, which this repo does not use). Because the pipeline produces six differently-named and independently-versioned packages, `globalSdl.sbom` indirects through the `$(sbomPackageName)` / `$(sbomPackageVersion)` variables, which each build job sets to its own `packageFullName` and computed `packageVersion`. Jobs that publish no packages (version computation, symbol publishing) set `ob_sdl_sbom_enabled: false` alongside their existing APIScan/BinSkim opt-outs, so the variables never need pipeline-level defaults
+- asyncSdl `enabled: false` in both; individual sub-tools (CredScan, BinSkim, Armory, Roslyn) configured underneath
+- Policheck exclusions: `$(REPO_ROOT)\.config\PolicheckExclusions.xml`
+- CredScan suppressions: `$(REPO_ROOT)/.config/CredScanSuppressions.json`
+
+## Artifact Conventions
+
+- `ob_outputDirectory` set to `$(JOB_OUTPUT)` (= `$(REPO_ROOT)/output`) — OneBranch auto-publishes this directory
+- Each published artifact uses subdirectories to separate file types:
+ - `assemblies/` — DLL assemblies for APIScan (preserving TFM folder structure)
+ - `packages/` — NuGet packages (`.nupkg`, `.snupkg`)
+ - `symbols/` — PDB symbol files (preserving TFM folder structure, shared by APIScan and symbol publishing)
+- Artifact names follow `drop__` — defined in `variables/onebranch-variables.yml`
+- Downstream jobs download artifacts via `DownloadPipelineArtifact@2` into `$(Build.SourcesDirectory)/packages`
+- Downloaded packages serve as a local NuGet source for `dotnet restore`
+- If stage or job names change, update artifact name variables in `onebranch-variables.yml`
+
+## Common Pitfalls
+
+- Do not use `PublishPipelineArtifacts` task — OneBranch auto-publishes from `ob_outputDirectory`
+- Do not add `NuGetToolInstaller@1` in OneBranch containers — NuGet is pre-installed
+- Variable templates are under `variables/` not `libraries/`
+- Always test parameter changes in the non-official pipeline first
+- When modifying stage names, update all `dependsOn` references and artifact name variables
+- Release jobs must use `templateContext.type: releaseJob` with `inputs` for artifact download — deployment jobs do not auto-download artifacts
diff --git a/.github/instructions/secrets.instructions.md b/.github/instructions/secrets.instructions.md
new file mode 100644
index 0000000000..77af612bee
--- /dev/null
+++ b/.github/instructions/secrets.instructions.md
@@ -0,0 +1,112 @@
+---
+applyTo: "**"
+---
+# Secrets and Credential Handling
+
+This guide describes how to avoid committing secrets, how to write credential
+placeholders that do **not** trip secret scanners (e.g. GitHub Advanced Security
+/ 1ES push protection), and what to do when a push is blocked.
+
+## Golden Rules
+
+1. **Never commit a real secret** — passwords, connection strings with live
+ credentials, access keys, SAS tokens, client secrets, certificates/PFX
+ files, or bearer tokens. This applies to source, tests, docs, samples,
+ scripts, pipeline YAML, and config files.
+2. **Never route a secret through tooling or the model.** When a value is truly
+ secret, have the user type it directly into their terminal or set it as an
+ environment variable. Do not paste it into files, prompts, or chat.
+3. **Prefer indirection over literals.** Read credentials from environment
+ variables, a secret store (Azure Key Vault), user secrets, or CI secret
+ variables — not from committed text.
+4. **Assume public.** This repository mirrors to public GitHub via autosync.
+ Anything committed is effectively public and permanent in history.
+
+## Approved Placeholder Formats
+
+When you need to show the *shape* of a connection string or credential in code,
+docs, comments, or samples, use one of these placeholder styles. These are
+recognized as non-secrets by the scanner:
+
+| Style | Example | Use for |
+|-------|---------|---------|
+| Angle brackets | `User ID=;Password=` | Docs, READMEs, comments, samples |
+| Descriptive angle brackets | `Password=`, `User Id=` | Samples that name the value |
+| Masked | `Password=********` or `Password=***` | Illustrative output / redaction |
+| Env var expansion | `Password=${SA_PASSWORD}` (bash), `Password=$(SqlPwd)` (ADO) | Scripts and pipelines |
+| Named token (docs prose) | `Password=` | Narrative documentation |
+
+### Do NOT use these placeholder styles
+
+- **Ellipsis values** — a `Password` (or `Pwd`) key whose value is a literal
+ ellipsis, especially when paired with a `User ID` key in the same connection
+ string. The ellipsis is treated as a credential value and **will** trip
+ `SEC101/037 SqlLegacyCredentials`. Use `` instead.
+- **Realistic-looking fake secrets** — a `Password` key set to a value that
+ looks like a real password (mixed letters, digits, and symbols). Even fake
+ values that resemble real passwords can be flagged and set a bad example.
+- **Bare word secrets** — a `Password` key set to a plain dictionary word
+ inside a connection-string literal. Prefer ``.
+
+> **NOTE**: Ironically, this document cannot show verbatim examples of the
+> disallowed styles above — spelling out a `Password` key followed by an
+> ellipsis or realistic-looking value would itself trip `SEC101/037` and block
+> commits to this very file. That is exactly why the "don't" cases are described
+> in prose rather than shown literally.
+
+### Full connection-string placeholder examples
+
+```text
+Server=;Database=;User ID=;Password=;TrustServerCertificate=true
+Server=tcp:.database.windows.net;Database=;Authentication=Active Directory Service Principal;User Id=;Password=
+```
+
+```bash
+# Have the user set the value directly; never write the real value into a file:
+export SNICLOSE_CONNSTR="Server=;User ID=;Password="
+```
+
+## Reading Secrets at Runtime (preferred patterns)
+
+- **Environment variables**: read connection strings from an env var
+ (e.g. `SNICLOSE_CONNSTR`) so the password never lands in a committed file.
+- **SecureString / SqlCredential**: use `SqlCredential` and `SecureString`
+ rather than embedding a password in the connection string.
+- **Integrated auth**: prefer `Integrated Security=true` or an
+ `Authentication=ActiveDirectory*` mode where no password is stored.
+- **CI/CD**: reference pipeline secret variables (`$(mySecret)`), never inline
+ literals in YAML.
+
+## When a Push Is Blocked by Secret Scanning
+
+Error shape: `VS403654:BypassableBlock ... push was rejected because it contains
+one or more secrets` with a `SEC101/...` rule id and `commit`/`paths` details.
+
+1. **Locate it.** Inspect the exact committed blob:
+ `git show :` and go to the reported line/columns.
+2. **Determine real vs. false positive.**
+ - *Real secret*: rotate/revoke it immediately, then remove it from the file.
+ If it is in the tip commit only, amend; if it is deeper in **unshared**
+ history, rewrite that history. Never rewrite commits already public.
+ - *False positive* (an ellipsis or other non-credential placeholder value):
+ reword to an approved placeholder (`Password=`) so future commits
+ don't recur.
+3. **Already-public / mirrored commits.** If the flagged content lives in a
+ commit that is already on public GitHub (e.g. an autosync mirror replaying
+ `github/main`), you cannot scrub that specific commit without rewriting
+ shared/public history. For a confirmed false positive, **bypass** the block
+ via the 1ES/Advanced Security push-protection flow
+ (https://aka.ms/1esSecretScanning/PushProtectionBypassableBlock) with a clear
+ reason, or dismiss the alert as *False positive*. Then land the placeholder
+ reword going forward so new files don't trip the rule again.
+4. **Never** disable secret scanning or use `--no-verify`-style bypasses to work
+ around a *real* secret.
+
+## Common Scanner Rules to Watch
+
+| Rule | Triggers on |
+|------|-------------|
+| `SEC101/037 SqlLegacyCredentials` | Connection strings pairing a `User ID` key with a `Password`/`Pwd` value |
+| `SEC101/*` (general) | Cloud keys, SAS tokens, client secrets, PATs, bearer tokens |
+
+If in doubt, use an approved placeholder from the table above.
diff --git a/.github/instructions/sqlclient-package-versions.instructions.md b/.github/instructions/sqlclient-package-versions.instructions.md
new file mode 100644
index 0000000000..691367d9de
--- /dev/null
+++ b/.github/instructions/sqlclient-package-versions.instructions.md
@@ -0,0 +1,189 @@
+---
+applyTo: "**/Versions.props,build.proj,eng/pipelines/**/*.yml"
+---
+# SqlClient Package Version Resolution
+
+How package versions are determined across different build scenarios for the packages in this repository.
+
+## Package families
+
+The repository ships two independently-versioned units:
+
+- **The SqlClient family** — `Microsoft.Data.SqlClient` plus the packages that version in lockstep with
+ it: `Microsoft.Data.SqlClient.Internal.Logging`, `Microsoft.Data.SqlClient.Extensions.Abstractions`,
+ `Microsoft.Data.SqlClient.Extensions.Azure`, and
+ `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`. **All family packages use the
+ SqlClient version numbers** — the same NuGet package version, file version, and assembly version —
+ and are always built and released together.
+- **`Microsoft.SqlServer.Server`** — versioned and released on its own cadence.
+
+## Version Properties
+
+The SqlClient family version lives in `src/Microsoft.Data.SqlClient/Versions.props`, which is imported
+for every project by `src/Directory.Build.props` (so the `SqlClient*` version properties are always
+available). `Microsoft.SqlServer.Server` declares its own version in its own `Versions.props`.
+
+| Property | Applies to | Purpose | Example |
+|----------|-----------|---------|---------|
+| `SqlClientNextVersion` | SqlClient family | Version being developed; used for the next release | `7.1.0-preview1` |
+| `SqlServerNextVersion` | SqlServer | Version being developed for SqlServer | `1.1.0-preview1` |
+| `SqlServerPublishedVersion` | SqlServer | Last SqlServer version shipped to NuGet | `1.0.0` |
+
+The SqlClient family always ships its next version, so there is **no** family `PublishedVersion`. Only
+`Microsoft.SqlServer.Server` keeps a published version (used when it is built as a SqlClient dependency
+but not itself released).
+
+## Resolution Logic
+
+Each `Versions.props` uses a 3-tier `` block:
+
+| Priority | Condition | PackageVersion | FileVersion |
+|----------|-----------|----------------|-------------|
+| 1 | `PackageVersion` explicitly provided | Used as-is | Strip prerelease + append BuildNumber |
+| 2 | `BuildNumber` provided (non-zero) | `NextVersion[-BuildSuffix]`, then `.BuildNumber` appended if that carries a prerelease tag | `NextVersion.Split('-')[0].BuildNumber` |
+| 3 | Nothing provided | `NextVersion-dev` | `NextVersion.Split('-')[0].0` |
+
+For every family package, `` is `SqlClient` (e.g. `-p:SqlClientPackageVersion=...`); for
+Microsoft.SqlServer.Server it is `SqlServer`.
+
+> **Two equivalent names for the same value.** `SqlClientPackageVersion` / `SqlServerPackageVersion`
+> are the underlying project properties (read by `Versions.props`, `Directory.Packages.props`, the
+> csproj files, and the nuspec). When building through `build.proj` — which the CI and OneBranch
+> pipelines always do — pass the wrapper argument `-p:PackageVersionSqlClient=...` /
+> `-p:PackageVersionSqlServer=...`; `build.proj` forwards it to the underlying
+> `-p:SqlClientPackageVersion=...` / `-p:SqlServerPackageVersion=...`. Both set the same value —
+> pipeline logs show `PackageVersion`, while project builds and props show `PackageVersion`.
+
+### Developer (local `dotnet build`)
+
+**Mode:** Project (default `ReferenceType=Project`)
+
+- No `BuildNumber`, no `BuildSuffix`, no `PackageVersion*` passed.
+- Falls into Priority 3 (the `` branch).
+- **Result:** `7.1.0-preview1-dev` / FileVersion `7.1.0.0`
+- Dependencies are project references — no package versions needed for siblings.
+
+**Mode:** Package (`-p:ReferenceType=Package`)
+
+- Same version resolution for the package being built.
+- Sibling dependencies are restored from local `packages/` feed (previously packed with `-dev` suffix).
+- A developer would first `dotnet build build.proj -t:Pack` to produce local packages, then consume them.
+
+### PR Pipeline (non-official CI)
+
+**`buildSuffix`** set via core template parameter:
+- `buildSuffix: 'pr'` (passed explicitly from PR pipeline)
+- `BuildNumber` = `$(DayOfYear)$(Rev:rr)` (e.g. `15401` for day-of-year 154, run 01)
+
+**Mode:** Project (typical PR validation)
+
+- Versions computed in `compute-versions-ci-stage.yml` (runs `GetVersions*` targets with `-p:BuildSuffix=pr -p:BuildNumber=...`)
+- Falls into Priority 2 with BuildSuffix present.
+- **Result:** `7.1.0-preview1-pr.15401` / FileVersion `7.1.0.15401`
+- Dependencies are project references — all packages built together in-tree.
+
+**Mode:** Package (PR package-ref validation)
+
+- Same version computation via compute-versions stage.
+- Downstream stages define stage-level variables from compute-versions output using `$[ stageDependencies... ]`.
+- Each build step passes the family wrapper argument `-p:PackageVersionSqlClient=` (and `-p:PackageVersionSqlServer=` where needed) to `build.proj`, which forwards it to the underlying `-p:SqlClientPackageVersion=` / `-p:SqlServerPackageVersion=` project property, hitting Priority 1.
+- Sibling dependencies consumed from pipeline artifacts published by upstream stages.
+
+### CI Pipeline (non-official, triggered on merge)
+
+Same structure as PR but passes `buildSuffix: 'ci'` explicitly.
+
+- **Result:** `7.1.0-preview1-ci.15401` / FileVersion `7.1.0.15401`
+
+### OneBranch Pipeline (official)
+
+**Mode:** Always Package (`ReferenceType=Package`)
+
+Uses the full `compute-versions-stage.yml` machinery:
+
+#### Step A: Compute Versions (dedicated early stage)
+
+1. Runs the `GetVersionsSqlClient` and `GetVersionsSqlServer` MSBuild targets against `build.proj`.
+2. Each target calls `dotnet build -getProperty:PackageVersion` and `-getProperty:FileVersion` with `BuildNumber` but **no BuildSuffix**.
+3. Falls into Priority 2 without BuildSuffix. `NextVersion` already carries a prerelease tag on `main`, so the build number is appended (e.g. `7.1.0-preview1.26238.3`); on a release branch the stable `NextVersion` is used as-is (e.g. `7.1.0`).
+4. `GetVersionsSqlServer` also extracts `SqlServerPublishedVersion` (the SqlClient family has no published version).
+
+#### Step B: Resolve Effective Versions
+
+- The **SqlClient family** always uses `SqlClientNextVersion`.
+- **`Microsoft.SqlServer.Server`** uses its next or published version based on the
+ `buildSqlServer` boolean:
+
+| `buildSqlServer` | Effective SqlServer Version | Meaning |
+|--------------------------|-----------------------------|---------|
+| `True` | `SqlServerNextVersion` (e.g. `1.1.0-preview1`) | SqlServer is being released |
+| `False` | `SqlServerPublishedVersion` (e.g. `1.0.0`) | Only built as a SqlClient dependency; use last-shipped |
+
+> **The default differs by pipeline.** The non-official (nightly) pipeline defaults `buildSqlServer:
+> true`, exercising the "build SqlServer + local-feed dependency" flow; the official pipeline defaults
+> `buildSqlServer: false`, exercising the "depend on the published SqlServer package" flow that
+> matches actual release intent. Either can be overridden at queue time. Requesting `releaseSqlServer`
+> without `buildSqlServer` fails template expansion up front.
+
+These are published as ADO output variables: `versions.SqlClientPackageVersion`,
+`versions.SqlServerPackageVersion`, and their `*FileVersion` counterparts.
+
+#### Step C: Build Stages Consume Pre-computed Versions
+
+Each downstream build job receives:
+- `packageVersion` parameter → passed as `-p:SqlClientPackageVersion=` (or `-p:SqlServerPackageVersion=` for SqlServer)
+- Dependency versions → family dependencies use the shared `SqlClientPackageVersion`; the SqlServer dependency uses `SqlServerPackageVersion`
+
+Since an explicit `PackageVersion` is provided, Versions.props hits Priority 1 — uses the value verbatim.
+
+#### Package Version Shapes
+
+Both OneBranch pipelines use the human-readable run name `$(Year:YY)$(DayOfYear)$(Rev:.r)`, which the
+compute stage receives as `Build.BuildNumber`. That run name drives the single supported package
+version shape: it is appended after any prerelease suffix, reproducing the shape shipped by earlier
+previews.
+
+- `1.2.3` stays `1.2.3` — non-preview releases are never stamped with a build number
+- `1.2.3-preview1` becomes `1.2.3-preview1.`, e.g. `7.1.0-preview3.26238.3`
+- The matching file version is `1.2.3.`, e.g. `7.1.0.26238`
+
+Note the asymmetry: the *package* version omits the build number for non-preview releases, but the
+*file* version always carries one in its fourth component. This keeps every shipped assembly
+date-encoded, while preserving the released package version customers expect.
+
+The file version's fourth component is only the *date* segment of the run name, because a four-part
+file version cannot hold the full `.` value. Repeated runs on the same day therefore share
+a file version even though their package versions differ.
+
+Only packages built in the current run are stamped. When `buildSqlServer` is `false`, the effective
+SqlServer version remains `SqlServerPublishedVersion` so dependency restore continues to request the
+package that actually exists on NuGet.
+
+#### Summary
+
+| Package | Version Source | Example |
+|---------|----------------|---------|
+| SqlClient family (always released together) | `SqlClientNextVersion` | `7.1.0-preview1` |
+| SqlServer, being released | `SqlServerNextVersion` | `1.1.0-preview1` |
+| SqlServer, dependency only | `SqlServerPublishedVersion` | `1.0.0` |
+
+## Key Architectural Difference
+
+| Scenario | Who computes versions | How dependencies get versions |
+|----------|----------------------|-------------------------------|
+| Developer | Versions.props inline (Priority 3) | Project references (no version needed) |
+| PR/CI (Project) | `compute-versions-ci-stage` up-front | Project references (no version needed) |
+| PR/CI (Package) | `compute-versions-ci-stage` up-front | Stage variables via `$[ stageDependencies... ]` → `-p:SqlClientPackageVersion=` / `-p:SqlServerPackageVersion=` |
+| OneBranch | `compute-versions-stage` up-front | Explicit `-p:SqlClientPackageVersion=` / `-p:SqlServerPackageVersion=` from stage outputs |
+
+## Updating Versions
+
+After releasing the **SqlClient family**:
+1. Update `SqlClientNextVersion` in `src/Microsoft.Data.SqlClient/Versions.props` to the next planned
+ version. (There is no family published version to update.)
+
+After releasing **`Microsoft.SqlServer.Server`**:
+1. Update `SqlServerPublishedVersion` to the version just shipped.
+2. Update `SqlServerNextVersion` to the next planned version.
+
+The SqlServer properties live in `src/Microsoft.SqlServer.Server/Versions.props`.
diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md
index 4f651a7104..9f4032e070 100644
--- a/.github/instructions/testing.instructions.md
+++ b/.github/instructions/testing.instructions.md
@@ -9,11 +9,13 @@ applyTo: "**/tests/**,**/*Test*.cs"
src/Microsoft.Data.SqlClient/tests/
├── FunctionalTests/ # Tests without SQL Server dependency
├── ManualTests/ # Integration tests requiring SQL Server
+├── PerformanceTests/ # Benchmark-style perf validation
+├── StressTests/ # Long-running stress coverage
├── UnitTests/ # Unit tests with minimal dependencies
└── tools/
└── Microsoft.Data.SqlClient.TestUtilities/
- ├── config.default.json # Template configuration
- └── config.json # Local test configuration (git-ignored)
+ ├── config.default.jsonc # Template configuration
+ └── config.jsonc # Local test configuration (git-ignored)
```
## Test Categories
@@ -32,14 +34,14 @@ src/Microsoft.Data.SqlClient/tests/
### Manual Tests (`ManualTests/`)
- Full integration tests with SQL Server
-- Require `config.json` setup
+- Require `config.jsonc` setup
- Test real database operations
- Include Always Encrypted, Entra ID tests
## Test Configuration
-### Setting Up `config.json`
-Copy `config.default.json` to `config.json` and configure:
+### Setting Up `config.jsonc`
+Copy `config.default.jsonc` to `config.jsonc` and configure:
```json
{
@@ -67,7 +69,7 @@ Copy `config.default.json` to `config.json` and configure:
## Test Categories and Attributes
### Category Exclusions
-Use `[Trait("Category", "...")]` (xUnit, used in both ManualTests and UnitTests) to mark test categories and exclusions:
+Use `[Trait("category", "...")]` (xUnit, used in both ManualTests and UnitTests) to mark test categories and exclusions:
| Category | Excluded On | Description |
|----------|-------------|-------------|
@@ -80,7 +82,7 @@ Use `[Trait("Category", "...")]` (xUnit, used in both ManualTests and UnitTests)
| `flaky` | All platforms (quarantine) | Intermittently failing tests (see Quarantine Zone below) |
### Flaky Test Quarantine Zone
-Tests that intermittently fail are quarantined with `[Trait("Category", "flaky")]`. Quarantined tests:
+Tests that intermittently fail are quarantined with `[Trait("category", "flaky")]`. Quarantined tests:
- Are **excluded** from regular test runs by the default filter: `category!=failing&category!=flaky`
- Run in **separate quarantine pipeline steps** to track their status without blocking CI
- Do **not** collect code coverage
@@ -94,35 +96,35 @@ Tests that intermittently fail are quarantined with `[Trait("Category", "flaky")
**How to quarantine:**
```csharp
// For unit tests (xUnit Trait)
-[Trait("Category", "flaky")]
+[Trait("category", "flaky")]
public class FlakyConnectionTests { ... }
// For individual test methods
-[Trait("Category", "flaky")]
+[Trait("category", "flaky")]
[ConditionalFact(...)]
public async Task OpenAsync_TimingDependent_MayFail() { ... }
```
-**How to un-quarantine:** Remove the `[Trait("Category", "flaky")]` attribute once the root cause is fixed and the test passes consistently.
+**How to un-quarantine:** Remove the `[Trait("category", "flaky")]` attribute once the root cause is fixed and the test passes consistently.
### Test Timeout Enforcement
All test runs use `--blame-hang-timeout 10m` to kill tests that hang for more than 10 minutes. This is configured in `build.proj` and applied to all test targets. If a test is expected to run longer than 10 minutes, it must be restructured or split.
### Test Filter Configuration
-The default test filter is defined in `build.proj`:
+The default test filter is defined in `build.proj` via `TestFilters`:
```xml
-category!=failing&category!=flaky
+category!=failing&category!=flaky&category!=interactive
```
-This can be overridden via MSBuild property: `msbuild -p:FilterStatement="your_filter"`.
+This can be overridden via build property: `dotnet build build.proj -t:TestSqlClientUnit -p:TestFilters="your_filter"`.
### Test Attributes
```csharp
// Platform-specific exclusion
-[Trait("Category", "nonlinuxtests")]
+[Trait("category", "nonlinuxtests")]
public void TestWindowsSpecificFeature() { ... }
// Skip on .NET Framework
-[Trait("Category", "nonnetfxtests")]
+[Trait("category", "nonnetfxtests")]
public void TestNetCoreOnlyFeature() { ... }
// Conditional skip based on test configuration
@@ -130,26 +132,26 @@ public void TestNetCoreOnlyFeature() { ... }
public void TestRequiresDatabase() { ... }
// Quarantined flaky test
-[Trait("Category", "flaky")]
+[Trait("category", "flaky")]
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))]
public void TestIntermittentlyFails() { ... }
```
## Running Tests
-### Using MSBuild (Recommended)
+### Using `build.proj` targets (Recommended)
```bash
# Build and run all unit tests
-msbuild -t:RunUnitTests
+dotnet build build.proj -t:TestSqlClientUnit
# Run functional tests only
-msbuild -t:RunFunctionalTests
+dotnet build build.proj -t:TestSqlClientFunctional
# Run manual tests for specific framework
-msbuild -t:RunManualTests -p:TF=net8.0
+dotnet build build.proj -t:TestSqlClientManual -p:TestFramework=net8.0
# Run specific test set
-msbuild -t:RunManualTests -p:TestSet=1
+dotnet build build.proj -t:TestSqlClientManual -p:TestSet=1
```
### Using dotnet CLI
@@ -159,7 +161,7 @@ dotnet test "src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClie
-p:Configuration=Release
# Functional tests with filter (excludes failing, flaky, and interactive tests)
-dotnet test "src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj" \
+dotnet test "src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj" \
--filter "category!=failing&category!=flaky&category!=interactive"
# Run ONLY quarantined flaky tests (for investigation)
@@ -171,6 +173,45 @@ dotnet test ... --filter "FullyQualifiedName=Namespace.ClassName.MethodName"
## Writing Tests
+### Test Documentation Requirements
+
+To keep tests maintainable for contributors and AI agents, test intent must be documented at
+class and method level.
+
+#### Required XML Documentation
+- Add XML `` comments to every test class.
+- Add XML `` comments to every test method (`[Fact]`, `[Theory]`, conditional variants).
+- For helper methods used by tests, add XML `` comments and XML `` / ``
+ where applicable.
+- For fixture and collection types, add XML `` comments describing why the fixture exists
+ (for example, serialization of console-mutating tests).
+
+#### What the Comments Must Explain
+- The behavior/contract being tested (not just restating the method name).
+- Why the scenario matters (for example: regression guard, parsing contract, sync/async parity,
+ isolation requirement).
+- For helper methods, what side effects occur (for example console redirection, file system
+ copying, process execution) and why they are needed.
+
+#### Style Guidance
+- Keep comments concise and factual.
+- Prefer behavior-focused wording over implementation trivia.
+- Avoid comments that merely repeat obvious code.
+- Use inline comments inside test methods only for non-obvious setup/act/assert details.
+
+#### Example
+```csharp
+///
+/// Ensures malformed connection strings return a non-zero exit code and emit a parse error
+/// without verbose exception details.
+///
+[Fact]
+public void AppRunWithMalformedConnectionStringReturnsOneAndWritesParseError()
+{
+ // Arrange / Act / Assert
+}
+```
+
### Test Structure
```csharp
public class FeatureNameTests
@@ -319,11 +360,43 @@ Extended assertions for SqlClient:
AssertExtensions.ThrowsContains(() => action(), "expected message");
```
+### RAII Database Object Classes
+When writing manual integration tests that require transient database objects, use the RAII classes from `Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects` instead of manually writing `try/finally` blocks with DDL `DROP`/`CREATE` statements.
+
+**Available classes:**
+
+| Class | SQL generated | Example definition argument |
+|-------|--------------|----------------------------|
+| `Table` | `CREATE TABLE {Name} {definition}` | `"(Id INT, Value NVARCHAR(100))"` |
+| `StoredProcedure` | `CREATE PROCEDURE {Name} {definition}` | `"AS BEGIN SELECT 1 END"` |
+| `UserDefinedType` | `CREATE TYPE [dbo].{Name} AS {definition}` | `"TABLE (f1 INT)"` |
+
+Each class generates a unique object name from the given prefix (incorporating a timestamp-based GUID, username, and machine name), creates the object on construction (requiring the connection to already be open), and drops it when disposed. The generated name is available via the `.Name` property.
+
+**Pattern:**
+```csharp
+using SqlConnection conn = new(DataTestUtility.TCPConnectionString);
+conn.Open();
+
+using Table testTable = new(conn, "MyTable", "(Id INT, Name NVARCHAR(100))");
+using StoredProcedure proc = new(conn, "MyProc", $"AS BEGIN SELECT * FROM {testTable.Name} END");
+
+using SqlCommand cmd = conn.CreateCommand();
+cmd.CommandText = proc.Name;
+cmd.CommandType = CommandType.StoredProcedure;
+// ... objects are automatically dropped when the scope ends
+```
+
+**Rules:**
+- Open the connection **before** constructing any database object (the constructor executes DDL immediately)
+- When objects depend on each other (e.g., a stored procedure that references a table), declare the dependent object **last** so it is disposed first — `using` declarations are disposed in reverse order
+- Use the `.Name` property directly wherever you need to reference the object in SQL; for `UserDefinedType` this already includes the `[dbo].` schema prefix, making it suitable for use as a TVP `TypeName`
+
## Code Coverage
### Running with Coverage
```bash
-msbuild -t:RunTests -p:CollectCoverage=true
+dotnet build build.proj -t:TestSqlClientUnit -p:TestCodeCoverage=true
```
### Coverage Targets
@@ -333,16 +406,11 @@ msbuild -t:RunTests -p:CollectCoverage=true
## Debugging Tests
-### Visual Studio
+### IDE
1. Set breakpoints in test code
-2. Right-click test → Debug Test
+2. Right-click test → Debug Test (or use CodeLens "Debug Test" link)
3. Use Test Explorer for navigation
-### VS Code
-1. Configure C# extension
-2. Use CodeLens "Debug Test" link
-3. Attach to test process
-
### Command Line
```bash
# Enable verbose output
diff --git a/.github/plans/apicompat-ref-assembly-validation.md b/.github/plans/apicompat-ref-assembly-validation.md
index b968dbf678..3ae4f0a46d 100644
--- a/.github/plans/apicompat-ref-assembly-validation.md
+++ b/.github/plans/apicompat-ref-assembly-validation.md
@@ -9,7 +9,7 @@ The comparison uses `Microsoft.DotNet.ApiCompat.Tool` in **strict mode**, which
## Usage
```
-dotnet msbuild build.proj /t:CompareRefAssemblies /p:BaselinePackageVersion=6.1.4
+dotnet build build.proj /t:CompareMdsRefAssemblies /p:BaselinePackageVersion=6.1.4
```
- `BaselinePackageVersion` is **required** (no default). The user must specify which published package to compare against.
@@ -22,7 +22,7 @@ dotnet msbuild build.proj /t:CompareRefAssemblies /p:BaselinePackageVersion=6.1.
Add an entry for version `9.0.200` alongside the existing `dotnet-coverage` entry. This enables `dotnet apicompat` after `dotnet tool restore`.
-### 2. Create `tools/targets/CompareRefAssemblies.targets`
+### 2. Create `tools/targets/CompareMdsRefAssemblies.targets`
A single new file containing all properties, items, and targets (steps 3–11 below). Follows the naming convention of existing files like `GenerateMdsPackage.targets`.
@@ -85,7 +85,7 @@ Runs ``.
- Preceded by `` labelling each comparison
- Uses item metadata to map `net462` to `$(LegacyNetFxRefDir)` and others to `$(LegacyNetCoreRefDir)`
-### 11. `CompareRefAssemblies` target (public entry point)
+### 11. `CompareMdsRefAssemblies` target (public entry point)
- Declared with `DependsOnTargets="_RunRefApiCompat"`
- Emits a final `` summarizing completion
@@ -95,12 +95,12 @@ Runs ``.
Add one line after the existing `.targets` imports (after line 7):
```xml
-
+
```
## Design Decisions
-- **Single new file** at `tools/targets/CompareRefAssemblies.targets` — only one `` line added to `build.proj`.
+- **Single new file** at `tools/targets/CompareMdsRefAssemblies.targets` — only one `` line added to `build.proj`.
- **Internal targets prefixed with `_`** to signal they're not intended to be called directly.
- **Strict mode** ensures API additions are also flagged — important for detecting accidental public surface changes during file reorganization.
- **`ContinueOnError="ErrorAndContinue"`** on each apicompat `Exec` so all 8 comparisons run and all differences are reported together.
@@ -111,7 +111,7 @@ Add one line after the existing `.targets` imports (after line 7):
## Verification
```
-dotnet msbuild build.proj /t:CompareRefAssemblies /p:BaselinePackageVersion=6.1.4
+dotnet build build.proj /t:CompareMdsRefAssemblies /p:BaselinePackageVersion=6.1.4
```
- Downloads 6.1.4 nupkg, builds both ref project variants, runs 8 comparisons (4 TFMs × 2 variants).
diff --git a/.github/prompts/ado-work-item-agent.prompt.md b/.github/prompts/ado-work-item-agent.prompt.md
index 6eb6319ef6..e94d667bac 100644
--- a/.github/prompts/ado-work-item-agent.prompt.md
+++ b/.github/prompts/ado-work-item-agent.prompt.md
@@ -23,7 +23,7 @@ Perform the following steps to address the work item. Think step-by-step.
- Locate the relevant code in `src/` or `tests/`.
### 2. Planning and Branching
-- Propose a descriptive branch name following the pattern `dev/username/branch-name` (e.g., `dev/jdoe/fix-connection-pool`).
+- Propose a descriptive branch name following the repository rule `dev/automation/` (e.g., `dev/automation/fix-connection-pool`).
- Identify any dependencies or potential breaking changes.
### 3. Implementation
@@ -33,13 +33,13 @@ Perform the following steps to address the work item. Think step-by-step.
### 4. Testing and Verification
- **Mandatory**: All changes must be tested.
-- Create new unit tests in `tests/UnitTests` or functional tests in `tests/FunctionalTests` as appropriate.
+- Create new unit tests in `src/Microsoft.Data.SqlClient/tests/UnitTests` or functional tests in `src/Microsoft.Data.SqlClient/tests/FunctionalTests` as appropriate.
- Verify that the tests pass.
### 5. Documentation and Finalization
- If public APIs are modified, update the documentation in `doc/`.
- Provide a clear summary of changes for the Pull Request.
-- Suggest an entry for [CHANGELOG.md](CHANGELOG.md) if the change is significant.
+- Suggest a release-note entry under `release-notes/` or in the PR description if the change is significant; do not edit `CHANGELOG.md` directly.
## Input
**Work Item ID**: ${input:workItemId}
diff --git a/.github/prompts/audit-variable-groups.prompt.md b/.github/prompts/audit-variable-groups.prompt.md
new file mode 100644
index 0000000000..075e12d99a
--- /dev/null
+++ b/.github/prompts/audit-variable-groups.prompt.md
@@ -0,0 +1,164 @@
+---
+name: audit-variable-groups
+description: Audit Azure DevOps variable groups by searching repos/branches for usage and updating descriptions accordingly.
+argument-hint:
+tools: ['execute/runInTerminal', 'execute/getTerminalOutput', 'edit/createFile', 'read/readFile']
+---
+
+Audit Azure DevOps variable groups in the **sqlclientdrivers** organization, **ADO.Net** project. Use the `az` CLI where possible; fall back to direct REST API calls where `az` doesn't provide sufficient coverage (e.g., repo file scanning, AzureKeyVault group updates).
+
+## Safety constraints
+
+- **Read-only by default.** Steps 1–4 are purely read-only (listing, searching, summarizing). Do NOT issue any write operations (`PUT`, `PATCH`, `POST`, `az pipelines variable-group update`, or any command that modifies state) until the user has explicitly approved changes in step 5.
+- **No implicit approval.** Silence, ambiguous replies, or partial acknowledgements do NOT count as approval. You must receive an unambiguous "go" (or equivalent affirmative) from the user before proceeding to step 6.
+- **Scope lock.** Only update variable group descriptions. Never delete variable groups, modify variables within a group, or change any pipeline definitions.
+- **Dry-run first.** When presenting the summary in step 4, show the exact before/after description text for every group that would be modified so the user can verify the changes.
+- **Abort on doubt.** If any step produces unexpected errors, ambiguous results, or data that contradicts expectations, stop and ask the user for guidance rather than proceeding.
+
+## Inputs
+
+If the user provided arguments, parse `${input:scope}` for overrides — it may contain specific repos, branches, or variable group names to scope the audit. Apply any recognized values as overrides to the defaults below; ignore unrecognized tokens.
+
+The user may override any of the following defaults:
+
+- **Organization**: `https://sqlclientdrivers.visualstudio.com`
+- **Project**: `ADO.Net`
+- **Repos & branches to search**:
+ - `dotnet-sqlclient`: `internal/main`, `internal/release/7.0`, `internal/release/6.1`
+ - `Microsoft.Data.SqlClient`: `ConfigFuzz`
+ - `Microsoft.Data.SqlClient.Ctaip`: `certAuth`
+ - `Microsoft.Data.SqlClient.sni`: `master`, `release/6.0`
+- **Unused marker text**: `UNUSED - WILL BE DELETED SHORTLY`
+
+## Workflow
+
+### 1. List all variable groups
+
+```
+az pipelines variable-group list --org --project -o json
+```
+
+- Save the output.
+- Identify which groups are already marked with the unused marker text and which are active.
+- Use **fuzzy matching** when detecting the unused marker: check for the presence of both "unused" and "delete" (case-insensitive) in the description, since the actual marker text may vary (e.g. a person's name inserted before "WILL DELETE").
+- In later steps, ignore the current group description when determining usage to avoid biasing the search results.
+
+### 2. Search repos for variable group references
+
+For each repo/branch combination, use the Azure DevOps REST API (Items endpoint) to:
+
+1. List all files recursively in the repo at the given branch.
+2. Filter to `.yml` and `.yaml` files.
+3. Fetch the content of each YAML file.
+4. Search for each variable group name using **exact-match** patterns that prevent prefix false positives (e.g., searching for `Foo` must not match `FooBar`). Match against these forms, ensuring the name is delimited by quotes or end-of-value (whitespace/newline/comment):
+ - `group: ''` (single-quoted — name bounded by quotes)
+ - `group: ""` (double-quoted — name bounded by quotes)
+ - `group: ` followed by end-of-line, whitespace, or `#` (unquoted — no trailing alphanumeric characters)
+
+Use a Bearer token from `az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query accessToken -o tsv`.
+
+REST API endpoints:
+- **List items**: `{org}/{project}/_apis/git/repositories/{repo}/items?recursionLevel=Full&versionDescriptor.version={branch}&versionDescriptor.versionType=branch&api-version=7.1`
+- **Get file content**: Same endpoint with `path={URL-encoded filePath}&$format=text` (URL-encode the `path` value; use `$format=text` to retrieve raw file content instead of JSON metadata)
+
+Avoid cloning repos. Only use the REST API to fetch file listings and content.
+
+**Performance & rate-limiting guidance**:
+- Filter the file listing to paths likely to contain pipelines (e.g., `eng/`, `pipelines/`, or root-level YAML files) before fetching content, to reduce API calls.
+- Add a short delay (e.g., 200ms) between file-content fetches to avoid hitting Azure DevOps rate limits.
+- If a `429 Too Many Requests` or `503` response is received, back off exponentially (1s, 2s, 4s, …) and retry up to 3 times before logging a warning and moving on.
+
+### 3. Search Classic pipelines for variable group references
+
+Query all **enabled** Classic build and release pipeline definitions for variable group usage.
+
+#### Classic Build pipelines
+
+```
+GET {org}/{project}/_apis/build/definitions?api-version=7.1
+```
+
+- Page through all results (`$top` / `continuationToken` if needed).
+- Keep only definitions where `queueStatus` is **`enabled`**.
+- For each enabled definition, fetch its full JSON:
+ ```
+ GET {org}/{project}/_apis/build/definitions/{id}?api-version=7.1
+ ```
+- Inspect the `variableGroups` array; each element has an `id` that maps to a variable group ID.
+
+#### Classic Release pipelines
+
+The Release API lives on the `vsrm.` sub-domain, but direct REST calls to that sub-domain may fail with SSL certificate errors for `.visualstudio.com` organizations. Use `az devops invoke` instead:
+
+```
+az devops invoke --area release --resource definitions \
+ --org --route-parameters project= \
+ --query-parameters '$expand=environments' '$top=200' \
+ -o json
+```
+
+- Page through results using `continuation_token` if present in the response.
+- Exclude definitions where `isDeleted` is `true`.
+- Each definition can reference variable groups at two levels:
+ - **Definition level**: `variableGroups` array on the root object.
+ - **Stage/environment level**: each element in `environments` has its own `variableGroups` array.
+- Collect all referenced variable group IDs from both levels.
+
+#### Recording results
+
+For every variable group ID found, record:
+- The pipeline **name** and **type** (Build / Release).
+- The stage name (for release-environment-level references).
+
+Merge these results with the repo/branch search results from step 2 so the summary in the next step covers both YAML and Classic usage.
+
+### 4. Summarize findings
+
+Present a clear summary table to the user **before making any changes**. The summary must include:
+
+- **Used groups**: group name, ID, which repos/branches and/or Classic pipelines reference it, and the proposed new description.
+- **Unused groups**: group name, ID, current description, and confirmation it will be marked with the unused marker.
+- **Surprise findings**: any group already marked unused that is actually still referenced (these should be un-marked).
+- **No-change groups**: groups already marked unused and confirmed unused.
+
+### 5. Prompt for go/no-go
+
+**This is a mandatory gate — do NOT skip or auto-approve.**
+
+Ask the user to confirm before applying any changes and require an explicit selection from the options below (do not infer approval from ambiguous replies). Offer options:
+- **Apply all** — apply every proposed change from step 4
+- **Apply subset** — let the user specify which groups to update (by name or ID)
+- **Export script** — write all update commands to a shell script file for the user to inspect and run manually. Do NOT execute any updates. Save the script to a path the user specifies (default: `./audit-variable-group-updates.sh`). The script must include the full `az` CLI commands and REST API `curl` fallbacks (for AzureKeyVault-type groups) with comments identifying each group by name and ID. Mark the file executable.
+- **Abort** — make no changes at all
+
+Do NOT proceed to step 6 unless the user selects "Apply all" or "Apply subset" and, for the subset case, clearly identifies which groups to update. If the user selects "Export script", generate the file and stop — do NOT execute step 6. If the user says "abort" or does not respond, stop here.
+
+### 6. Apply description updates
+
+**Prerequisites**: Step 5 must have completed with explicit user approval. If you have not received approval, do NOT execute this step.
+
+For each group that needs updating, try:
+
+```
+az pipelines variable-group update --group-id --description "" --org --project --detect false
+```
+
+**Fallback for AzureKeyVault-type groups**: The `az pipelines variable-group update` command may fail with 500 errors on groups whose `type` is `AzureKeyVault` (it tries to refresh the vault connection). When this happens, fall back to the REST API:
+
+1. `GET {org}/{project}/_apis/distributedtask/variablegroups/{id}?api-version=7.1`
+2. Update **both** the top-level `description` field **and** every entry in `variableGroupProjectReferences[].description` in the JSON.
+3. `PUT` the modified JSON back to the same URL with `Content-Type: application/json`.
+
+**Description rules**:
+- **Used groups**: Prepend `[Used by: : , ; : ; Classic/: ] ` to the existing description (after stripping any previous `[Used by: ...]` prefix). `` is `Build` or `Release`.
+- **Unused groups**: Set description to the unused marker text.
+- **Incorrectly marked unused**: Replace the unused marker with `[Used by: ...]`.
+- **Already correct**: Skip groups whose description would not change.
+
+Report the outcome of each update (success/failure) and a final tally.
+
+## Error handling
+
+- If a repo or branch does not exist or returns an error, log a warning and continue with the remaining repos/branches.
+- If a variable group update fails, log the error and continue with the remaining updates.
+- At the end, report any failures so the user can address them manually.
diff --git a/.github/prompts/code-review.prompt.md b/.github/prompts/code-review.prompt.md
index 6196c32293..3a64abec0b 100644
--- a/.github/prompts/code-review.prompt.md
+++ b/.github/prompts/code-review.prompt.md
@@ -1,18 +1,20 @@
---
name: code-review
-description: AI-assisted code review for a pull request in Microsoft.Data.SqlClient.
+description: AI-assisted code review for a pull request or branch in Microsoft.Data.SqlClient.
argument-hint:
agent: agent
-tools: ['github/search_issues', 'read/readFile', 'codebase/search']
+tools: ['github/search_issues', 'github/pull_request_read', 'github/get_file_contents', 'github/run_secret_scanning', 'read/readFile', 'search']
---
-Review the pull request "${input:pr}" in `dotnet/SqlClient`.
+Review the changes in "${input:target}" for `dotnet/SqlClient`.
+
+The target may be either a **PR number** (e.g., `4106`) or a **branch name** (e.g., `dev/user/my-feature`). Determine which by checking whether the value is purely numeric.
Follow this structured review process:
## 1. Understand the Change
-- Fetch the PR details: title, description, linked issue(s), and diff.
- Read the PR description to understand the intent and scope of the change.
+- Check for linked issues referenced in the description (e.g., `Fixes #...`).
- Check which files are modified and categorize them:
- **Source code** (`src/Microsoft.Data.SqlClient/src/`) — the main review focus
- **Tests** (`tests/`) — verify coverage
diff --git a/.github/prompts/fix-bug.prompt.md b/.github/prompts/fix-bug.prompt.md
index 47b143f31f..ade780708d 100644
--- a/.github/prompts/fix-bug.prompt.md
+++ b/.github/prompts/fix-bug.prompt.md
@@ -25,9 +25,9 @@ Follow this workflow step-by-step:
## 3. Write a Failing Test
- Create a test that reproduces the bug BEFORE implementing the fix.
- Choose the correct test project:
- - `tests/UnitTests/` — for isolated logic tests (no SQL Server needed)
- - `tests/FunctionalTests/` — for API behavior tests (no SQL Server needed)
- - `tests/ManualTests/` — for integration tests (requires SQL Server)
+ - `src/Microsoft.Data.SqlClient/tests/UnitTests/` — for isolated logic tests (no SQL Server needed)
+ - `src/Microsoft.Data.SqlClient/tests/FunctionalTests/` — for API behavior tests (no SQL Server needed)
+ - `src/Microsoft.Data.SqlClient/tests/ManualTests/` — for integration tests (requires SQL Server)
- Follow existing naming conventions: `{ClassName}Tests.cs` with methods named `{MethodName}_{Scenario}_{ExpectedResult}`.
- If the bug is platform-specific, add appropriate `[ConditionalFact]` or `[ConditionalTheory]` attributes with `[PlatformSpecific]`.
- **Cover both sync and async code paths** if the affected API has both variants (e.g., `Open`/`OpenAsync`, `ExecuteReader`/`ExecuteReaderAsync`). Sync and async paths often have different internal implementations and a bug may manifest in only one.
diff --git a/.github/prompts/generate-doc-comments.prompt.md b/.github/prompts/generate-doc-comments.prompt.md
index 33ced936de..9361d3c7ad 100644
--- a/.github/prompts/generate-doc-comments.prompt.md
+++ b/.github/prompts/generate-doc-comments.prompt.md
@@ -1,5 +1,5 @@
---
-name: doc-comments
+name: generate-doc-comments
description: Generate XML documentation comments for C# code following .NET best practices.
argument-hint:
agent: agent
diff --git a/.github/prompts/generate-prompt.prompt.md b/.github/prompts/generate-prompt.prompt.md
index a4aa8cd40f..4db6c577c9 100644
--- a/.github/prompts/generate-prompt.prompt.md
+++ b/.github/prompts/generate-prompt.prompt.md
@@ -2,6 +2,7 @@
name: generate-prompt
description: Generates high-quality VS Code Copilot prompt files (.prompt.md) based on user descriptions, leveraging available skills.
argument-hint: Describe the prompt you want to create (e.g., "A prompt to generate unit tests for C#")
+tools: [read, edit, search, todo]
---
You are an expert AI prompt developer specialized in creating **Visual Studio Code Copilot Prompt Files (`.prompt.md`)**.
@@ -34,6 +35,7 @@ Before generating the prompt, review the available skills in the `.github/skills
* `name`: A concise, kebab-case name for the prompt.
* `description`: A clear, short description of what the prompt does.
* `argument-hint`: (Optional) A hint for what arguments the user can provide when using the prompt.
+ * `tools`: (Recommended) A list of tool identifiers the prompt is allowed to use. See the **Tool Scoping** section below.
* **Body Structure**:
* **Role**: Define the AI's persona (e.g., "You are an expert C# developer...").
* **Context**: Include specific context instructions or references.
@@ -50,12 +52,70 @@ Before generating the prompt, review the available skills in the `.github/skills
* Use `${input:variableName}` for user inputs (e.g., `${input:methodName}`).
* Use built-in variables like `${selection}`, `${file}`, or `${workspaceFolder}` where appropriate context is needed.
-6. **Best Practices**:
+6. **Scope Tools**: Restrict the tools available to each prompt using the `tools` frontmatter field. See the **Tool Scoping** section below for detailed guidance.
+
+7. **Best Practices**:
* Be specific and explicit.
* Encourage chain-of-thought reasoning if the task is complex.
* Reference workspace files using Markdown links `[path/to/file.cs](path/to/file.cs)` only if they are static and necessary for *all* invocations of this prompt.
* Prefer referencing skills over duplicating instructions that already exist in skills.
+## Tool Scoping
+
+Every generated prompt **should** include a `tools` list in its YAML frontmatter. Scoping tools keeps the model focused by limiting it to approved, known-effective tools for the task. Without tool scoping, the model may invoke irrelevant tools, waste context, or produce unpredictable results.
+
+### Why scope tools?
+- **Focus**: Fewer tools means the model spends less reasoning on tool selection and more on the task.
+- **Reliability**: Restricting to tested tools avoids unexpected side effects (e.g., a read-only review prompt shouldn't have edit tools).
+- **Safety**: Prevents prompts from accidentally running terminal commands or making file changes when they shouldn't.
+
+### How to choose tools
+Apply the **principle of least privilege** — include only the tools the prompt actually needs:
+
+| Prompt type | Recommended tools |
+|---|---|
+| **Read-only analysis** (review, triage, explain) | `read/readFile`, `search/codebase`, `search/textSearch` |
+| **Code editing** (bug fix, feature, refactor) | `edit/editFiles`, `edit/createFile`, `read/readFile`, `search/codebase` |
+| **Needs terminal** (build, test, scripts) | All of the above + `execute/runInTerminal`, `execute/getTerminalOutput` |
+| **Needs GitHub data** (triage, release notes) | All of the above + `github/search_issues` or other GitHub tools |
+| **Needs web content** (docs lookup) | `web/fetch` |
+
+### Available built-in tool identifiers
+
+You can specify individual tools or tool sets (which include all tools in that group).
+
+**Tool sets** (use these to include all tools in a category):
+- `edit` — File creation and editing tools
+- `read` — File and notebook reading tools
+- `search` — Codebase, text, and file search tools
+- `execute` — Terminal, task, and notebook execution tools
+- `web` — Web content fetching tools
+
+**Commonly used individual tools:**
+
+| Tool identifier | Purpose |
+|---|---|
+| `edit/editFiles` | Apply edits to existing files |
+| `edit/createFile` | Create a new file |
+| `read/readFile` | Read file contents |
+| `read/problems` | Get workspace problems/diagnostics |
+| `search/codebase` | Semantic code search |
+| `search/textSearch` | Text/regex search in files |
+| `search/fileSearch` | Search for files by glob pattern |
+| `search/listDirectory` | List directory contents |
+| `search/usages` | Find references and implementations |
+| `execute/runInTerminal` | Run a shell command |
+| `execute/getTerminalOutput` | Get terminal output |
+| `execute/testFailure` | Get test failure details |
+| `web/fetch` | Fetch a web page |
+
+**Extension / MCP tools** can also be included using their identifier (e.g., `github/search_issues`). Use `/*` to include all tools from an MCP server.
+
+### Frontmatter syntax
+```yaml
+tools: ['read/readFile', 'search/codebase', 'edit/editFiles']
+```
+
## Example Output Structure (with skill reference)
```markdown
@@ -63,6 +123,7 @@ Before generating the prompt, review the available skills in the `.github/skills
name: my-new-prompt
description: specialized task description
argument-hint: input parameter hint
+tools: ['edit/editFiles', 'read/readFile', 'search/codebase', 'execute/runInTerminal']
---
You are a specialized agent for...
@@ -89,6 +150,7 @@ Use ${input:param1} to...
name: my-new-prompt
description: specialized task description
argument-hint: input parameter hint
+tools: ['read/readFile', 'search/codebase']
---
You are a specialized agent for...
diff --git a/.github/prompts/generate-skill.prompt.md b/.github/prompts/generate-skill.prompt.md
index 484fe2debc..e258adedc6 100644
--- a/.github/prompts/generate-skill.prompt.md
+++ b/.github/prompts/generate-skill.prompt.md
@@ -2,6 +2,8 @@
name: generate-skill
description: Generate a GitHub Copilot Agent Skill (SKILL.md) following best practices and official documentation
argument-hint: Describe the skill you want to create (e.g., "debugging SQL connection issues")
+agent: agent
+tools: ['read/readFile', 'edit/createFile', 'search']
---
You are an expert developer specialized in creating **GitHub Copilot Agent Skills**.
diff --git a/.github/prompts/implement-feature.prompt.md b/.github/prompts/implement-feature.prompt.md
index 4beae539fe..4623cb235c 100644
--- a/.github/prompts/implement-feature.prompt.md
+++ b/.github/prompts/implement-feature.prompt.md
@@ -62,9 +62,9 @@ Before writing code, produce a brief implementation plan covering:
4. Test against multiple SQL Server versions.
## 5. Write Tests
-- **Unit tests** in `tests/UnitTests/` for isolated logic.
-- **Functional tests** in `tests/FunctionalTests/` for API behavior without SQL Server.
-- **Manual tests** in `tests/ManualTests/` for full integration with SQL Server.
+- **Unit tests** in `src/Microsoft.Data.SqlClient/tests/UnitTests/` for isolated logic.
+- **Functional tests** in `src/Microsoft.Data.SqlClient/tests/FunctionalTests/` for API behavior without SQL Server.
+- **Manual tests** in `src/Microsoft.Data.SqlClient/tests/ManualTests/` for full integration with SQL Server.
- Cover:
- Happy path and edge cases
- **Both sync and async code paths** where the feature exposes both variants
diff --git a/.github/prompts/refine-test-overlap.prompt.md b/.github/prompts/refine-test-overlap.prompt.md
index 824e18f145..f4d0339c27 100644
--- a/.github/prompts/refine-test-overlap.prompt.md
+++ b/.github/prompts/refine-test-overlap.prompt.md
@@ -1,7 +1,9 @@
---
-name: test-minimize-overlap
+name: refine-test-overlap
description: Run coverage overlap analysis and suggest test suite optimizations
argument-hint: Test filter (e.g. FullyQualifiedName~MyTests) or describe the tests you want to analyze
+agent: agent
+tools: ['edit/editFiles', 'read/readFile', 'search/codebase', 'execute/runInTerminal', 'execute/getTerminalOutput']
---
You are an expert .NET Test Engineer specialized in optimizing test coverage and reducing technical debt.
@@ -10,19 +12,19 @@ Your task is to analyze the user's test suite using the `AnalyzeTestOverlap.ps1`
## Skills
This prompt leverages the following skills for specific sub-tasks:
-- [generate-mstest-filter](../skills/generate-mstest-filter/SKILL.md) - For generating well-formed MSTest filter expressions
+- [generate-mstest-filter](.github/skills/generate-mstest-filter/SKILL.md) - For generating well-formed MSTest filter expressions
## Tools
-You have access to the analysis script at `[AnalyzeTestOverlap.ps1](./scripts/AnalyzeTestOverlap.ps1)`.
+You have access to the analysis script at [AnalyzeTestOverlap.ps1](.github/prompts/scripts/AnalyzeTestOverlap.ps1).
## Workflow
1. **Parse or Generate Test Filter**:
* If `${input:filter}` is a valid MSTest filter expression (e.g., `FullyQualifiedName~MyTests`), use it directly.
- * If `${input:filter}` is a loose description (e.g., "connection tests" or "SqlCommand class"), follow the instructions in the [generate-mstest-filter](../skills/generate-mstest-filter/SKILL.md) skill to generate a proper filter expression.
+ * If `${input:filter}` is a loose description (e.g., "connection tests" or "SqlCommand class"), follow the instructions in the [generate-mstest-filter](.github/skills/generate-mstest-filter/SKILL.md) skill to generate a proper filter expression.
* If `${input:filter}` is empty, ask the user for a test filter or description to target specific tests.
2. **Run Analysis**:
- * Run the script using the filter: `.\scripts\AnalyzeTestOverlap.ps1 -Filter ""`.
+ * Run the script from the workspace root: `.\.github\prompts\scripts\AnalyzeTestOverlap.ps1 -Filter ""`.
* *Note*: The script produces a console summary and a `test-coverage-analysis.json` file.
3. **Review Overlap**:
diff --git a/.github/prompts/release-notes.prompt.md b/.github/prompts/release-notes.prompt.md
index ca03933236..b800dfe245 100644
--- a/.github/prompts/release-notes.prompt.md
+++ b/.github/prompts/release-notes.prompt.md
@@ -1,19 +1,27 @@
---
name: release-notes
description: Generate release notes for a specific milestone, covering all packages in the repository that have changes.
-argument-hint:
+argument-hint:
agent: agent
-tools: ['edit/createFile', 'edit/editFiles', 'read/readFile']
+tools: ['edit/createFile', 'edit/editFiles', 'read/readFile', 'execute/runInTerminal']
---
-Generate release notes for the milestone "${input:milestone}".
+Generate release notes for the milestone "${input:milestone}" on the branch "${input:branch}".
This repository ships multiple packages. Only generate release notes for packages that have relevant PRs in the milestone. All packages use the same template: [release-notes/template/release-notes-template.md](release-notes/template/release-notes-template.md).
+## Branch Model
+
+The release notes content and the source code it describes live on different branches:
+
+- **Release notes files are maintained on `main`.** Every release's notes (for all branches/versions) are committed under `release-notes/` on `main`. Create and edit the release notes Markdown files on `main` (or a PR targeting `main`), not on the release branch.
+- **The source code for the release lives only on the target branch `${input:branch}`.** Version sources (`Versions.props`), project files (`*.csproj`), and dependency files (`Directory.Packages.props`) reflect the released bits *as they exist on `${input:branch}`*, which can differ from `main`. When you look up versions, dependencies, TFM/OS scope, or verify API names (Steps 2.1, 2.2, 4, and the Version and Dependency Lookup table), read those source files from `${input:branch}` — not from your current `main` checkout.
+- **Practical implication:** Do not assume a `...VersionDefault` or dependency version read from `main` matches what shipped on `${input:branch}`. Confirm against `${input:branch}` (e.g., `git show ${input:branch}:`), or against the milestone/release artifacts.
+
## Package Registry
| Package | Release Notes Directory | How to Identify PRs |
-|---------|------------------------|---------------------|
+| ------- | ----------------------- | ------------------- |
| `Microsoft.Data.SqlClient` | `release-notes//` | Default — PRs not assigned to another package |
| `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` | `release-notes/add-ons/AzureKeyVaultProvider//` | Labels containing `AKV`, or PR titles/bodies/files referencing `AzureKeyVaultProvider`, `add-ons/`, or `AlwaysEncrypted.AzureKeyVaultProvider` |
| `Microsoft.SqlServer.Server` | `release-notes/MSqlServerServer//` | PR titles/bodies/files referencing `Microsoft.SqlServer.Server` or `src/Microsoft.SqlServer.Server/` |
@@ -21,20 +29,28 @@ This repository ships multiple packages. Only generate release notes for package
| `Microsoft.Data.SqlClient.Extensions.Azure` | `release-notes/Extensions/Azure//` | PR titles/bodies/files referencing `Extensions.Azure` |
| `Microsoft.Data.SqlClient.Internal.Logging` | `release-notes/Internal/Logging//` | PR titles/bodies/files referencing `Internal.Logging` |
+> **Not all packages exist on every branch.** This table is the full, current package set. Older release branches ship a subset — for example, `release/6.1` and earlier have no extension packages (`Extensions.Abstractions`, `Extensions.Azure`) and no `Internal.Logging`; the companion-package set and even the `AzureKeyVaultProvider` source location vary by branch. Before generating notes for a package, confirm it actually exists on the target branch `${input:branch}` (e.g., `git ls-tree -r --name-only ${input:branch} | grep -i ""`). Skip any package that does not exist on `${input:branch}`, even if the table lists it.
+
## Version and Dependency Lookup
-Each package has its own versioning and dependency sources. Use these to determine package versions and dependency lists:
+Each package's version and dependency information comes from MSBuild props/project files **on the target branch `${input:branch}`** (see Branch Model). The exact file paths, file names, and property names that hold versions **differ by branch**, because the versioning layout was refactored over time. Do not assume the layout of your current checkout — discover the version source on `${input:branch}`.
+
+Two known layouts:
+
+| Layout | Branches | MDS version source | Companion/extension version sources |
+| ------ | -------- | ------------------ | ----------------------------------- |
+| **Centralized** | `release/7.0` (and earlier 7.0.x) | `tools/props/Versions.props` (`MdsVersionDefault`) | `tools/props/Versions.props` imports per-package props with the older names: `…/Extensions/Abstractions/src/AbstractionsVersions.props`, `…/Extensions/Azure/src/AzureVersions.props`, `…/Internal/Logging/src/LoggingVersions.props`, `…/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AkvProviderVersions.props` |
+| **Per-package** | `main`, `7.1+` | `src/Microsoft.Data.SqlClient/Versions.props` (`SqlClientVersionDefault`) | Each package has its own `Versions.props`: `…/Extensions/Abstractions/src/Versions.props` (`AbstractionsVersionDefault`), `…/Extensions/Azure/src/Versions.props` (`AzureVersionDefault`), `…/Internal/Logging/src/Versions.props` (`LoggingVersionDefault`), `…/AlwaysEncrypted.AzureKeyVaultProvider/src/Versions.props` (`AkvProviderVersionDefault`), `…/Microsoft.SqlServer.Server/Versions.props` (`SqlServerVersionDefault`) |
+
+Discovery approach (works regardless of layout):
-| Package | Version Source | Dependency Source |
-|---------|---------------|-------------------|
-| `Microsoft.Data.SqlClient` | [tools/props/Versions.props](tools/props/Versions.props) (`MdsVersionDefault`) | [Directory.Packages.props](Directory.Packages.props) and the [project file](src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj) |
-| `AzureKeyVaultProvider` | [tools/props/Versions.props](tools/props/Versions.props) (`AkvVersionDefault`) | [AKV project file](src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj) and [Directory.Packages.props](Directory.Packages.props) |
-| `Microsoft.SqlServer.Server` | [tools/props/Versions.props](tools/props/Versions.props) (`SqlServerPackageVersion`) | [SqlServer project file](src/Microsoft.SqlServer.Server/Microsoft.SqlServer.Server.csproj) |
-| `Extensions.Abstractions` | [AbstractionsVersions.props](src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/AbstractionsVersions.props) | [Abstractions.csproj](src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/Abstractions.csproj) |
-| `Extensions.Azure` | [AzureVersions.props](src/Microsoft.Data.SqlClient.Extensions/Azure/src/AzureVersions.props) | [Azure.csproj](src/Microsoft.Data.SqlClient.Extensions/Azure/src/Azure.csproj) |
-| `Internal.Logging` | [LoggingVersions.props](src/Microsoft.Data.SqlClient.Internal/Logging/src/LoggingVersions.props) | [Logging.csproj](src/Microsoft.Data.SqlClient.Internal/Logging/src/Logging.csproj) |
+1. List the version props on the target branch, e.g. `git ls-tree -r --name-only ${input:branch} | grep -i "Versions.props$"`.
+2. Read the relevant file from the target branch, e.g. `git show ${input:branch}:`, and find the package's default/`PackageVersion` property.
+3. Prefer the explicit shipped version: on a release branch the actual version may be supplied by the pipeline (`...PackageVersion`) rather than the `...VersionDefault` fallback, so confirm against the milestone/release artifacts rather than assuming the default.
-Concrete dependency versions (e.g., `Azure.Core 1.49.0`) are centrally managed in [Directory.Packages.props](Directory.Packages.props). Framework-conditional versions (e.g., `net9.0` vs everything else) are handled by `Condition` attributes in the same file.
+Dependency sources (read from `${input:branch}`): the per-package project file (`src/Microsoft.Data.SqlClient/src/Microsoft.Data.SqlClient.csproj`, the AKV `.csproj`, `Abstractions.csproj`, `Azure.csproj`, `Logging.csproj`, `Microsoft.SqlServer.Server.csproj`) plus the centrally-managed concrete versions in `Directory.Packages.props`. Framework-conditional versions (e.g., `net9.0` vs everything else) are handled by `Condition` attributes there.
+
+> **Companion package version alignment (7.0.2 and later):** Starting with 7.0.2, the companion packages (`AzureKeyVaultProvider`, `Extensions.Azure`, `Extensions.Abstractions`, `Internal.Logging`) ship version-aligned with the core `Microsoft.Data.SqlClient` driver. When generating notes for an aligned release, use the core MDS version (read from the target branch) for these companion packages — their per-package default version on `main` may point at a different next version and must not be assumed to be the shipped version. `Microsoft.SqlServer.Server` continues to version independently.
## Skills
@@ -46,7 +62,8 @@ This prompt uses the following skill:
### 1. Fetch Milestone Items
- Follow the instructions in the [fetch-milestone-prs](.github/skills/fetch-milestone-prs/SKILL.md) skill to fetch all merged PRs for the milestone "${input:milestone}".
-- The output will be saved to `.milestone-prs/${input:milestone}/` with individual JSON files per PR and an `_index.json` summary.
+- The output will be saved to `.milestone-prs/${input:milestone}/${input:branch}` with individual JSON files per PR and an `_index.json` summary.
+- Identify any milestone items that don't have corresponding commits on the release branch "${input:branch}", and vice versa.
### 2. Analyze and Categorize
@@ -57,6 +74,52 @@ This prompt uses the following skill:
- Identify the contributors for the "Contributors" section.
- **Assign each PR to one or more packages** using the identification rules in the Package Registry table. A PR may be relevant to multiple packages. PRs not matching any non-core package belong to `Microsoft.Data.SqlClient`.
+### 2.1. Determine Target Framework (TFM) Scope Per Change
+
+For each PR included in release notes, determine whether it applies to all supported TFMs for the package or only a subset.
+
+Use source-level evidence (not assumptions) to classify scope:
+
+- **TFM-specific files** indicate scoped impact (for example, `.netfx.cs`, `.netcore.cs`).
+- **Conditional compilation** indicates scoped impact (for example, `#if NETFRAMEWORK`, `#if NET`).
+- **Project or build conditions** indicate scoped impact (for example, `Condition` expressions on `TargetFramework` or `TargetFrameworks`).
+- **Tests-only TFM changes** should not be called out as customer-facing unless the behavior change is also present in product code.
+
+When writing notes:
+
+- If the change affects **all supported TFMs** for that package, do not add a TFM qualifier.
+- If the change affects **only some TFMs**, include an explicit qualifier in the relevant bullet or section title.
+- Use concise qualifiers like:
+ - `(net462 only)`
+ - `(net8.0/net9.0 only)`
+
+Do not infer TFM scope from labels alone; verify from changed files and code paths.
+
+### 2.2. Determine Operating System (OS) Scope Per Change
+
+For each PR included in release notes, determine whether it applies to all supported OS targets for the package or only a subset.
+
+Use source-level evidence (not assumptions) to classify scope:
+
+- **OS-specific files** indicate scoped impact (for example, `.windows.cs`, `.unix.cs`).
+- **OS preprocessor symbols** indicate scoped impact (for example, `#if _WINDOWS`, `#if _UNIX`).
+- **Project/build conditions** indicate scoped impact (for example, `TargetOs`, `NormalizedTargetOs`, or OS-conditional `ItemGroup`/`PropertyGroup` entries).
+- **SNI implementation or native dependency gates** can imply OS scope when behavior changes only apply to native Windows SNI vs managed cross-platform paths.
+- **Tests-only OS changes** should not be called out as customer-facing unless the behavior change is also present in product code.
+
+When writing notes:
+
+- If the change affects **all supported OS targets**, do not add an OS qualifier.
+- If the change affects **only some OS targets**, include an explicit qualifier in the relevant bullet or section title.
+- Use concise qualifiers like:
+ - `(Windows only)`
+ - `(Unix only)`
+ - `(Linux only)`
+ - `(macOS only)`
+- If both TFM and OS are scoped, combine them in one qualifier, for example: `(net8.0/net9.0 on Windows only)`.
+
+Do not infer OS scope from labels alone; verify from changed files and code paths.
+
### 3. Enrich Feature Sections with Issue Context
For significant features or bug fixes that reference a GitHub issue:
@@ -75,7 +138,7 @@ When release notes reference a public API (property, method, class):
### 5. Generate Release Notes for Each Package
-For each package that has relevant PRs in the milestone:
+For each package that ships in this milestone — i.e., it has relevant PRs, **or** it is a version-aligned companion package (7.0.2+) bumping to match the core `Microsoft.Data.SqlClient` release even without its own changes (see item 2):
1. **Determine the package version** using the Version Source from the lookup table above. Read the actual props/project file to find the version.
@@ -83,9 +146,15 @@ For each package that has relevant PRs in the milestone:
- Use the template from [release-notes/template/release-notes-template.md](release-notes/template/release-notes-template.md).
- Fill in the template following the instructions in each section.
- Only include sections (Added, Changed, Fixed, Removed) that have entries.
+ - For each Added/Changed/Fixed/Removed item, include TFM and OS scope qualifiers when Step 2.1 or Step 2.2 determines the change is not universal across the package's supported targets.
- Look up dependencies using the Dependency Sources from the lookup table above. Resolve concrete versions from [Directory.Packages.props](Directory.Packages.props).
- List dependencies per target framework. Use the project file's `` to determine which frameworks to list.
- Omit the Contributors section for packages with no public contributors.
+ - **GA releases (all packages):** When the release is a stable (non-preview) version, structure the notes with two sections:
+ 1. **"Changes Since [last preview]"** — only the delta since the most recent preview of this package.
+ 2. **"Cumulative Changes Since [last stable]"** — all changes since the last stable release of this package, synthesized from all preview release notes plus the GA milestone. This applies to every package (MDS, AKV, Extensions.Azure, Abstractions, Internal.Logging, etc.), not just the core driver. Apply the cross-referencing from Step 3 to eliminate items already shipped in prior stable patch releases.
+ - **Preview releases:** Only include the delta since the previous release (preview or stable). No cumulative section is needed.
+ - **Version-alignment-only releases (7.0.2+ companion packages):** Starting with 7.0.2, the companion packages (`AzureKeyVaultProvider`, `Extensions.Azure`, `Extensions.Abstractions`, `Internal.Logging`) ship a new version aligned with the core driver on every core release — **even when they have no functional or API changes**. In that case, still create the package's release notes file using a version-alignment-only style: state that there are no functional or API changes, note the version alignment with the core driver, link to the core `Microsoft.Data.SqlClient ` notes, and (for .NET Framework) call out any `AssemblyVersion` strong-name change. Use the shipped 7.0.2 companion notes as the reference pattern (e.g., [release-notes/Extensions/Abstractions/7.0/7.0.2.md](release-notes/Extensions/Abstractions/7.0/7.0.2.md), [release-notes/Internal/Logging/7.0/7.0.2.md](release-notes/Internal/Logging/7.0/7.0.2.md), [release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.2.md](release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.2.md)). For the `Internal.Logging` package, retain its internal-use note. `Microsoft.SqlServer.Server` is **not** version-aligned and follows the normal skip rule.
3. **Create or update the version README** at `/README.md`. Follow the existing format — see [release-notes/add-ons/AzureKeyVaultProvider/6.1/README.md](release-notes/add-ons/AzureKeyVaultProvider/6.1/README.md) for reference:
@@ -100,7 +169,9 @@ For each package that has relevant PRs in the milestone:
| | | [Release Notes](.md) |
```
-4. **Skip packages without changes.** If a package has no relevant PRs in the milestone, do not create release notes for it. Report which packages had changes and which did not.
+4. **Skip packages without changes (or that don't exist on the branch).** If a package has no relevant PRs in the milestone, or the package does not exist on the target branch `${input:branch}` (see the Package Registry note — older branches like `release/6.1` have no extension or `Internal.Logging` packages), do not create release notes for it. **Exception (7.0.2+):** version-aligned companion packages (`AzureKeyVaultProvider`, `Extensions.Azure`, `Extensions.Abstractions`, `Internal.Logging`) still get a release notes file when they bump to the aligned core version, even with no functional changes — use the version-alignment-only style from item 2. Report which packages had changes, which shipped alignment-only notes, which did not, and which are not present on the branch.
+
+5. **Cross-link companion packages from the core release notes.** When one or more companion packages (`AzureKeyVaultProvider`, `Extensions.Azure`, `Extensions.Abstractions`, `Internal.Logging`, `Microsoft.SqlServer.Server`) also ship in this milestone, add a `### Companion package release notes` section to the core `Microsoft.Data.SqlClient` release notes file that links to each companion package's release notes for the same version. This preserves context for the companion packages when the core release notes are used as the published GitHub release body. Use relative links (e.g., `../Extensions/Azure//.md`). Only list packages that actually shipped release notes in this milestone.
### 6. Update CHANGELOG.md
@@ -115,8 +186,17 @@ For each package that has relevant PRs in the milestone:
- If a section for the package doesn't yet exist, add one following the existing pattern (see the `AzureKeyVaultProvider` and `Microsoft.SqlServer.Server` sections for reference).
- If the section already exists, add the new version link to its Release Information list.
+### 8. Markdown for GitHub Release
+
+- Use the contents of the new release notes markdown file to produce markdown suitable for pasting into a GitHub UI Release textbox.
+ - GitHub renders newlines within paragraphs and lists as hard breaks, so remove those.
+ - Omit the main heading and first sub-heading.
+ - Update any relative links to use absolute URLs pointing to the file in the repository.
+ - Provide this new markdown in a code block that can easily be copied and pasted directly into the GitHub UI.
+
## Notes
-- Packages may ship as preview or stable independently. Use the actual version from the project/spec files.
-- The directory structure mirrors existing conventions: `add-ons/AzureKeyVaultProvider/` for AKV, `MSqlServerServer/` for SqlServer, and `Extensions//` for the new extension packages.
+- Release notes are maintained on `main` for all branches/releases, while the corresponding source code lives only on the target branch `${input:branch}` (see Branch Model above). Read version/dependency/source files from `${input:branch}`; write release notes files on `main`.
+- Packages may ship as preview or stable independently. Use the actual version from the project/spec files on the target branch.
+- The directory structure mirrors existing conventions: `add-ons/AzureKeyVaultProvider/` for AKV, `MSqlServerServer/` for SqlServer, `Extensions//` for the extension packages (e.g., `Extensions/Abstractions/`, `Extensions/Azure/`), and `Internal/Logging/` for the internal logging package.
- When referencing code samples, link to files in the `doc/samples/` directory if a relevant sample exists.
diff --git a/.github/prompts/review-pr-feedback.prompt.md b/.github/prompts/review-pr-feedback.prompt.md
new file mode 100644
index 0000000000..c14a7b96cb
--- /dev/null
+++ b/.github/prompts/review-pr-feedback.prompt.md
@@ -0,0 +1,123 @@
+---
+name: review-pr-feedback
+description: Uses gh CLI to collect unresolved PR review feedback, optionally includes discussion comments, applies fixes, and reports status.
+argument-hint: pr= repo= includeDiscussionComments= authorFilter= testScope=
+tools: ['edit/editFiles', 'edit/createFile', 'read/readFile', 'read/problems', 'search/codebase', 'search/textSearch', 'search/fileSearch', 'execute/runInTerminal', 'execute/getTerminalOutput']
+---
+You are an expert software maintenance agent focused on resolving pull request feedback quickly, safely, and with clear traceability.
+
+## Context
+- Workspace root: ${workspaceFolder}
+- Target PR: ${input:pr}
+- Optional repository override: ${input:repo}
+- Include non-review discussion comments: ${input:includeDiscussionComments}
+- Optional author filter: ${input:authorFilter}
+- Optional focused testing hint: ${input:testScope}
+- Optional selected context: ${selection}
+
+## Skills
+#skill:generate-mstest-filter
+
+Use this skill when building a dotnet test filter:
+- [generate-mstest-filter](.github/skills/generate-mstest-filter/SKILL.md)
+
+Follow the referenced skill instructions before producing any custom filter.
+
+## Task
+1. Validate prerequisites
+- Confirm gh CLI is installed and authenticated.
+- Resolve repository from ${input:repo}, or infer from git remote.
+- Resolve PR number from ${input:pr} (accept number or URL).
+- Discover the correct git remote name from the current repository and store it for later commands.
+- Use that discovered remote name for push and any other git operations that require a remote; do not assume `origin`.
+
+2. Gather actionable review feedback
+- Query PR review threads with gh api GraphQL.
+- Keep only unresolved threads where isResolved is false.
+- Extract thread id, file path, line/startLine, comment url, author login, and body.
+- If ${input:authorFilter} is provided, apply it case-insensitively.
+
+3. Optionally gather non-review discussion comments
+- If ${input:includeDiscussionComments} is true, fetch PR issue comments.
+- Mark these as Informational because they do not have open/resolved state.
+- Apply ${input:authorFilter} if provided.
+
+4. Build an implementation plan
+- Group unresolved review feedback by file and risk.
+- Determine minimal safe edits needed.
+- Identify comments that are non-actionable or ambiguous.
+- Ask the user to confirm the plan before proceeding, showing a concise summary of proposed changes and rationale.
+
+5. Implement and verify
+- Apply required code or test updates with smallest safe change set.
+- Run targeted checks first.
+- If ${input:testScope} is provided, generate and use a focused MSTest filter via the skill.
+- Collect diagnostics when tests cannot run.
+
+6. Classify each item
+- Fixed: change implemented and validated.
+- Needs Clarification: ambiguous, conflicting, or insufficiently specified.
+- Blocked: external dependency, permission, or missing context.
+- Informational: non-review discussion comment captured only.
+
+7. Produce a final report
+- Keep review-thread outcomes and discussion outcomes in separate sections.
+- Include evidence for each item: file location, change summary, validation result.
+- Draft a distinct reply for each comment item that addresses that exact comment's request, context, and outcome.
+
+8. Commit changes
+- If any changes were made, create a commit with a clear message referencing the PR and summarizing the resolution.
+- Prompt the user to review and confirm the commit message before finalizing.
+- When suggesting or performing a push, use the discovered git remote name.
+- Prompt the user to push the commit if they have permissions, or provide instructions if they do not.
+- Prompt the user to reply to each original PR comment with a comment-specific response and link to the relevant commit or code location, if appropriate.
+- Prompt the user to mark review threads as resolved in GitHub if they have permissions, or provide instructions if they do not.
+
+## Output Format
+1. PR Scope
+- Repo
+- PR number
+- Unresolved review threads found
+- Discussion comments found (if enabled)
+
+2. Unresolved Review Feedback (Actionable)
+- Item:
+- Location: :
+- Author:
+- Request summary:
+- Action taken:
+- Status: Fixed | Needs Clarification | Blocked
+- Evidence:
+- Suggested reply:
+
+3. Discussion Comments (Informational, optional)
+- Item:
+- Author:
+- Summary:
+- Notes:
+- Suggested reply:
+
+4. Validation
+- Commands run
+- Filters used
+- Pass/fail summary
+- Remaining warnings/errors
+
+5. Final Summary
+- Files changed
+- Number fixed
+- Number needing clarification
+- Number blocked
+- Number informational
+- Recommended next step
+
+## Rules
+- Do not invent comments; only act on data fetched from gh.
+- Review-thread resolution tracking is authoritative for unresolved state.
+- Keep behavior-compatible edits unless feedback explicitly requires change.
+- If no unresolved review threads exist, report that explicitly.
+- If auth or permission fails, report exact failure and minimum required user action.
+- Do not use `set -e` in bash commands or scripts.
+- After each terminal step, verify the bash session is still alive; if it died, report it immediately, start a new session, and continue from the last confirmed checkpoint.
+- Use the discovered git remote name consistently anywhere a remote is required.
+- Do not post generic batch replies; each reply must be tailored to the specific comment content and its exact resolution status.
diff --git a/.github/prompts/triage-pipeline-failures.prompt.md b/.github/prompts/triage-pipeline-failures.prompt.md
new file mode 100644
index 0000000000..98c223c3d5
--- /dev/null
+++ b/.github/prompts/triage-pipeline-failures.prompt.md
@@ -0,0 +1,185 @@
+---
+name: triage-pipeline-failures
+description: Find and classify failing tests in the CI/CD pipelines at or after a given commit, then fix or quarantine them.
+argument-hint: [optional scope, e.g. specific pipelines/branches]
+agent: agent
+# No `tools:` scoping on purpose: this prompt is access-agnostic and must be able
+# to call whatever Azure DevOps MCP server is connected (e.g. `ado/*`) in addition
+# to the built-in terminal/read/search/edit tools. Declaring a scoped `tools:` list
+# would strip out MCP/extension tools and break the preferred ADO MCP access path.
+---
+
+Triage failing tests in the CI/CD pipelines for commit
+`${input:commit}` and later. Treat only the **first whitespace-delimited token** of
+`${input:commit}` as the target commit SHA — that token is what every git ancestry
+check (`git merge-base --is-ancestor ...`) uses. Any remaining text is
+**optional scope** (e.g. a pipeline name or branch): honor it when present, otherwise
+use the defaults below.
+
+## Azure DevOps access is agnostic
+
+Every data-retrieval step below is described as an **operation**, not a command.
+Perform each operation with whatever Azure DevOps access is available, in this order
+of preference:
+
+1. An **Azure DevOps MCP server**, if one is connected (preferred — no shell needed).
+2. The **`az` CLI** (`az rest --resource ...`,
+ `az pipelines ...`, `az boards ...`).
+3. **Direct ADO REST** calls over HTTPS with a bearer token.
+
+Do not assume a specific mechanism. If the first choice is unavailable or errors,
+fall back to the next. Keep read operations read-only; only edits to test source
+files (quarantine/fix) modify state, and those happen in the repo, not in ADO.
+
+## Environment
+
+- **ADO organization**: ``.
+- **Projects**:
+ - `` — CI/PR pipelines target the upstream GitHub repo.
+ - `` — CI/OneBranch pipelines target the ADO mirror repo.
+- The ADO mirror repo preserves the **same commit SHAs** as GitHub, so git
+ ancestry against a GitHub SHA works. It **lags** GitHub because synchronization is
+ gated by PRs: a commit only appears in the mirror once its sync PR completes, so a
+ target commit may not be present in the mirror yet even though it is on GitHub.
+
+## Step 1 — Scope to the right pipelines
+
+**Operation:** list build definitions in both `public` and `ADO.Net`, with each
+definition's folder path, `queueStatus`, and `repository.type` / `repository.name`.
+
+Ignore any definition that is **not currently enabled** (`queueStatus != enabled`,
+i.e. disabled or paused) — also skip names flagged `[Disabled]`, `[Retired]`, or under
+`\Retired\` folders. Then keep only definitions whose repo is the upstream GitHub repo
+or the ADO mirror repo. Exclude legacy driver repos and native SNI repos unless the
+user asks for SNI.
+
+Because this triage targets **non-PR commit runs** (see Step 2), prefer CI/branch
+definitions over PR-validation ones. Prefer CI, package, stress, Kerberos,
+Managed-Instance, and OneBranch official/non-official definitions across both projects.
+PR-triggered definitions are in scope only for the CI/branch runs they may also host —
+their PR-ref runs are excluded in Step 2 unless the user asks to include PR runs.
+
+## Step 2 — Find runs at/after the target commit
+
+**Operation:** for each in-scope definition, list recent runs (filter to
+`failed`, `partiallySucceeded`, `canceled`) with their `sourceBranch`,
+`sourceVersion`, `result`, and `finishTime`.
+
+**Limit to non-PR commit runs.** Only consider runs triggered by real commits on
+tracked branches (e.g. `refs/heads/main`, `refs/heads/release/*`); **exclude PR
+validation runs**. A run is a PR run — and therefore out of scope — when any of these
+hold:
+
+- Its `sourceBranch` is an ephemeral merge ref such as `refs/pull/N/merge` or
+ `refs/pull/N/head`.
+- Its build `reason` is `pullRequest`.
+- It is a PR-triggered definition running against a PR ref.
+
+Keep only runs whose `sourceVersion` is a committed SHA on a tracked branch. If the
+user explicitly asks to include PR runs, honor that override.
+
+Resolve **"at or after `${input:commit}`" by commit graph, not timestamp**:
+
+- `git merge-base --is-ancestor ` → true means the run's
+ commit is the target or a descendant (in scope).
+- `sourceVersion == ` → the target itself (in scope).
+- Divergent `release/*` or `dev/*` commits do **not** descend from a `main` target —
+ exclude them.
+
+Mirror runs use the **same SHAs** as GitHub, so apply the same
+ancestry checks. Because mirror sync is PR-gated, the target commit may not have
+reached the mirror yet — in that window there simply are no in-scope mirror runs, so
+do not infer a run is out of scope from a SHA mismatch (there is none); it is only a
+timing lag.
+
+## Step 3 — Enumerate failing test runs per build
+
+**Operation:** for each in-scope build, list its test runs.
+
+Compute real failures as `totalTests - passedTests - notApplicableTests`. Do **not**
+treat `unanalyzedTests`/`notApplicableTests` as failures. Keep runs with failures > 0.
+
+## Step 4 — Get failing test names and errors
+
+**Operation:** for each failing test run, fetch the `Failed` results with their
+`automatedTestName`, `errorMessage`, and `stackTrace`.
+
+**You must capture the actual xUnit output and full stack trace for every failed
+test — do not classify a failure without it.** The one-line `errorMessage` is not
+enough; get the complete assertion text (e.g. `Assert.Equal() Failure: Values differ /
+Expected / Actual`) and the full stack frames (the test method and the failing product
+frames). If any source truncates it, cross-check another until you have the whole thing:
+
+- The result's `errorMessage` + `stackTrace` fields (expand sub-results — see below).
+- The test run's **attachments** (TRX / `*.trx`, console logs) when the API truncates
+ long stacks.
+- The **job log** for the test step (Step 5) — the raw `dotnet test` / xUnit output
+ always contains the assertion and stack, even when the results API does not.
+
+**CRITICAL — data-driven (Theory) results hide the error on a child:** xUnit
+`[Theory]`/`[ClassData]`/`[InlineData]` tests publish as a parent result with
+`resultGroupType == "dataDriven"` whose own `errorMessage`/`stackTrace` are **null**.
+The real assertion lives on the failing **sub-result**. When a `Failed` result has a
+null error, re-fetch that result **including sub-results** and read the child. A null
+parent error means "look at the children", not "the test aborted". Only treat it as an
+abort when the build log also shows no assertion and the process was terminated
+(e.g. a `--blame-hang` dump).
+
+## Step 5 — Locate each failure's job (for logs/links)
+
+**Operation:** for a failing run, read its `pipelineReference` (stage/phase/job), then
+read the build's timeline and walk Stage → Phase → Job by `parentId` to get the job
+record id. Build deep links:
+
+- Tests tab: `.../_build/results?buildId=&view=ms.vss-test-web.build-test-results-tab`
+- A specific result: append `&runId=&resultId=&paneView=debug`
+- Job logs: `.../_build/results?buildId=&view=logs&j=`
+
+## Step 6 — Classify every failure
+
+| Class | Signals | Action |
+|-------|---------|--------|
+| **True positive** (broken driver) | Deterministic; fails on every leg for the commit; assertion tied to changed code; absent on the parent commit | Fix the bug; keep/add a failing test |
+| **Test-isolation / concurrency** | Off-by-a-small-count on a process-global resource (e.g. pool `ConnectionCount` Expected 2 Actual 3); some data rows pass, others fail; depends on parallel tests | Isolate the resource (unique connection string, `[Collection]`); else quarantine |
+| **Flaky (timing/GC/load)** | Intermittent; only under CI load; GC-finalizer or retry/failover timing; "connection is broken" under contention | Deterministic fix (poll not sleep, set retry interval/timeouts); else quarantine |
+| **Environmental / infra** | Empty error AND no assertion in the log; host/agent crash; blame-hang dump; network/DTC outage; many unrelated tests fail at once | Re-run to confirm; report infra; don't quarantine on a single infra hit |
+
+Determine **regression vs pre-existing** by repeating Steps 3–4 on the
+immediately-preceding in-scope build (the parent commit). A failure present before
+`${input:commit}` was not introduced by it.
+
+## Step 7 — Check quarantine status before acting
+
+A test is already quarantined if it carries `[Trait("category", "flaky")]`; those run
+in a separate, non-blocking quarantine step (`TestFilters="category=flaky"`) while the
+regular step excludes `category!=failing&category!=flaky&category!=interactive`.
+
+- Already-quarantined failure = expected quarantine noise, not a blocker. Only escalate
+ with a real fix.
+- Non-quarantined failure in a regular step = a real blocker.
+
+## Step 8 — Present findings and STOP (checkpoint)
+
+Steps 1–7 are **read-only investigation**. Before changing anything, present your
+findings and wait for the user's explicit go-ahead. Do **not** edit any files or take
+any action until the user approves.
+
+Present a per-failure table: test name, in-scope build(s), full xUnit assertion +
+key stack frames, classification, whether it is a regression, current quarantine
+status, and the **proposed** action (fix / quarantine / already quarantined /
+re-run to confirm). Link each failure to its build/result and job. Then explicitly ask
+the user which items to act on.
+
+## Step 9 — Act (only after approval)
+
+For each item the user approves:
+
+1. Prefer a **deterministic fix** that removes the race/isolation/timing dependency.
+2. Otherwise **quarantine**: add `[Trait("category", "flaky")]` plus a comment holding
+ the observed failure signature (test name, assertion, key stack frames) and the
+ root-cause reasoning. Mirror the style of existing quarantine comments in the test suite.
+3. Cover both sync and async variants when the API has both.
+4. Un-quarantine once fixed and consistently green.
+
+Make only the source edits needed to fix or quarantine; do not modify pipeline YAML or
+ADO state. After editing, report what changed.
diff --git a/.github/prompts/update-build-pipelines.prompt.md b/.github/prompts/update-build-pipelines.prompt.md
index 617cf39901..a652c9f82f 100644
--- a/.github/prompts/update-build-pipelines.prompt.md
+++ b/.github/prompts/update-build-pipelines.prompt.md
@@ -3,7 +3,7 @@ name: update-build-pipelines
description: Guided workflow for updating Azure DevOps CI/CD pipelines for Microsoft.Data.SqlClient.
argument-hint:
agent: agent
-tools: ['edit/createFile', 'edit/editFiles', 'read/readFile', 'codebase/search']
+tools: ['edit/createFile', 'edit/editFiles', 'read/readFile', 'search']
---
Update the Azure DevOps build pipelines for: "${input:change}".
@@ -15,35 +15,40 @@ Follow this workflow step-by-step:
## 1. Understand the Pipeline Architecture
- Read the relevant pipeline file(s) in `eng/pipelines/`.
- Key pipelines:
- - `dotnet-sqlclient-ci-core.yml` — Core CI pipeline (reusable by reference pipelines)
+ - `dotnet-sqlclient-ci-core.yml` — Core CI pipeline template used by CI and PR definitions
- `dotnet-sqlclient-ci-project-reference-pipeline.yml` — CI with project references
- `dotnet-sqlclient-ci-package-reference-pipeline.yml` — CI with package references
- `sqlclient-pr-project-ref-pipeline.yml` — PR validation (project references)
- `sqlclient-pr-package-ref-pipeline.yml` — PR validation (package references)
- - `dotnet-sqlclient-signing-pipeline.yml` — Package signing
- - `akv-official-pipeline.yml` — AKV provider official build (1ES/OneBranch)
- - `stress-tests-pipeline.yml` — Stress tests
+ - `onebranch/sqlclient-official.yml` — official OneBranch build/release pipeline
+ - `onebranch/sqlclient-non-official.yml` — non-official OneBranch build/release pipeline
+ - `ci/stress/sqlclient-ci-stress-pipeline.yml` — stress test pipeline
- Shared templates live in `eng/pipelines/common/templates/` (jobs/, stages/, steps/).
-- Variables are defined in `eng/pipelines/variables/` and `eng/pipelines/libraries/`.
+- CI variables are defined in `eng/pipelines/libraries/`; OneBranch variables are defined in `eng/pipelines/onebranch/variables/`.
## 2. Identify What Needs to Change
- Determine which pipeline files are affected.
- Check if the change impacts shared templates that are reused across multiple pipelines.
- Identify if new parameters, variables, or stages need to be added.
- Review existing parameters to understand the current configuration surface:
- - `targetFrameworks` / `targetFrameworksUnix` — test target frameworks
+ - `targetFrameworks` / `targetFrameworksUnix` — Windows and Unix test TFMs
+ - `netcoreVersionTestUtils` — runtime used by shared test utilities
- `referenceType` — Project or Package reference
- `buildConfiguration` — Debug/Release
- `useManagedSNI` — Managed vs Native SNI testing
+ - `runLegacySqlTests` — whether to include SQL Server 2016/2017 legs
## 3. Implement the Change
- Modify YAML files following the existing patterns and indentation style.
- When adding new stages, follow the existing stage ordering:
- 1. `build_abstractions_package_stage`
- 2. `build_sqlclient_package_stage`
- 3. `build_azure_package_stage`
- 4. `stress_tests_stage` (optional)
- 5. `run_tests_stage`
+ 1. `generate_secrets`
+ 2. `build_sqlserver_package_stage`
+ 3. `build_logging_package_stage`
+ 4. `build_abstractions_package_stage`
+ 5. `build_sqlclient_package_stage`
+ 6. `build_azure_package_stage`
+ 7. `verify_nuget_packages_stage`
+ 8. `ci_run_tests_stage`
- When adding new test parameters, ensure they are wired through to test execution steps.
- When modifying shared templates, verify all consuming pipelines still work.
@@ -52,6 +57,7 @@ Follow this workflow step-by-step:
- Test filters by platform: `nonnetfxtests`, `nonnetcoreapptests`, `nonwindowstests`, `nonlinuxtests`.
- SNI testing matrix: both Native (`useManagedSNI=false`) and Managed (`useManagedSNI=true`).
- Always Encrypted tests controlled by `runAlwaysEncryptedTests` parameter.
+- Stress coverage is maintained under `eng/pipelines/ci/stress/`, not as a stage inside `dotnet-sqlclient-ci-core.yml`.
## 5. Validate
- Verify YAML syntax is valid.
diff --git a/.github/scripts/auto-assign-pr.js b/.github/scripts/auto-assign-pr.js
new file mode 100644
index 0000000000..0f2e77d3c1
--- /dev/null
+++ b/.github/scripts/auto-assign-pr.js
@@ -0,0 +1,133 @@
+// Auto-assign PR load balancer.
+//
+// Selects up to 2 assignees for a qualifying PR from a configurable pool,
+// balancing by current open-PR assignment count. Invoked by the
+// `auto-assign-pr.yml` workflow via `actions/github-script`.
+module.exports = async ({ github, context, core }) => {
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const prNumber = context.issue.number;
+ const author = context.payload.pull_request.user.login;
+ const normalizeLogin = login => login.toLowerCase();
+ const parseCsvLogins = value => (value ?? '')
+ .split(',')
+ .map(entry => entry.trim())
+ .filter(entry => entry.length > 0);
+
+ // Fallback pool keeps behavior unchanged when no repo variable is configured.
+ const defaultPool = ['cheenamalhotra', 'paulmedynski', 'priyankatiwari08', 'benrr101', 'mdaigle', 'apoorvdeshmukh'];
+ const configuredPool = parseCsvLogins(process.env.PR_REVIEWER_POOL);
+ const rawPool = configuredPool.length > 0 ? configuredPool : defaultPool;
+ const seenPoolUsers = new Set();
+ const pool = [];
+ for (const user of rawPool) {
+ const normalized = normalizeLogin(user);
+ if (!seenPoolUsers.has(normalized)) {
+ seenPoolUsers.add(normalized);
+ pool.push(user);
+ }
+ }
+
+ let latestPr;
+ try {
+ const response = await github.rest.pulls.get({
+ owner,
+ repo,
+ pull_number: prNumber
+ });
+ latestPr = response.data;
+ } catch (error) {
+ throw new Error(`Failed to fetch latest PR details: ${error.message}`);
+ }
+
+ if (latestPr.state !== 'open' || latestPr.draft || !latestPr.milestone) {
+ console.log('PR is no longer assignment-eligible (not open, draft, or missing milestone).');
+ return;
+ }
+
+ const currentAssignees = (latestPr.assignees ?? []).map(a => a.login);
+
+ console.log(`PR Author: ${author}`);
+ console.log(`Event Name: ${context.eventName}; Is Fork PR: ${context.payload.pull_request.head.repo.fork === true}`);
+ console.log(`Current Assignees: ${currentAssignees.join(', ')}`);
+
+ if (currentAssignees.length >= 2) {
+ console.log('PR already has 2 or more assignees. No action needed.');
+ return;
+ }
+
+ const neededAssigneesCount = 2 - currentAssignees.length;
+
+ const candidates = pool.filter(user =>
+ normalizeLogin(user) !== normalizeLogin(author) &&
+ !currentAssignees.some(a => normalizeLogin(a) === normalizeLogin(user))
+ );
+
+ if (candidates.length === 0) {
+ console.log('No valid candidates left in the pool.');
+ return;
+ }
+
+ const workloads = {};
+ const canonicalCandidateByNormalized = {};
+ candidates.forEach(user => {
+ const normalized = normalizeLogin(user);
+ workloads[normalized] = 0;
+ canonicalCandidateByNormalized[normalized] = user;
+ });
+
+ try {
+ // Rank candidates by current assignment count across all open PRs.
+ const iterator = github.paginate.iterator(github.rest.pulls.list, {
+ owner,
+ repo,
+ state: 'open',
+ per_page: 100
+ });
+
+ for await (const response of iterator) {
+ for (const pr of response.data) {
+ if (pr.draft) continue;
+ if (pr.assignees) {
+ for (const assignee of pr.assignees) {
+ const login = normalizeLogin(assignee.login);
+ if (workloads[login] !== undefined) {
+ workloads[login]++;
+ }
+ }
+ }
+ }
+ }
+ } catch (error) {
+ throw new Error(`Failed to fetch open PRs for auto-assignment: ${error.message}`);
+ }
+
+ const workloadArray = candidates.map(user => {
+ const normalized = normalizeLogin(user);
+ return { user: canonicalCandidateByNormalized[normalized], count: workloads[normalized] };
+ });
+ console.log('Current Workloads:', workloadArray);
+
+ // Shuffle before sorting so ties are broken fairly instead of favoring pool order.
+ for (let i = workloadArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [workloadArray[i], workloadArray[j]] = [workloadArray[j], workloadArray[i]];
+ }
+
+ workloadArray.sort((a, b) => a.count - b.count);
+
+ const selectedAssignees = workloadArray.slice(0, neededAssigneesCount).map(w => w.user);
+ console.log(`Selected candidates: ${selectedAssignees.join(', ')}`);
+
+ if (selectedAssignees.length === 0) {
+ console.log('No assignees selected. No action needed.');
+ return;
+ }
+
+ await github.rest.issues.addAssignees({
+ owner,
+ repo,
+ issue_number: prNumber,
+ assignees: selectedAssignees
+ });
+};
diff --git a/.github/scripts/check-milestone-branch.sh b/.github/scripts/check-milestone-branch.sh
new file mode 100755
index 0000000000..3be77eec59
--- /dev/null
+++ b/.github/scripts/check-milestone-branch.sh
@@ -0,0 +1,210 @@
+#!/usr/bin/env bash
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# check-milestone-branch.sh
+#
+# Validates that a pull request's milestone is consistent with the branch the
+# pull request targets.
+#
+# OVERVIEW
+# --------
+# Milestones in this repository are named "..", optionally
+# with a pre-release suffix (e.g. "7.0.3", "8.0.0-preview1"). Every milestone
+# therefore maps to a candidate release branch:
+#
+# .. -> release/.
+#
+# Release branches and configured milestones determine where the work belongs:
+#
+# * The branch EXISTS -> that version has already forked off the default
+# branch and is in servicing. Changes for it go to release/..
+#
+# * The branch DOES NOT exist, and this is the earliest configured milestone
+# series without a release branch -> that version is in development on the
+# default branch. Changes for it go to the default branch.
+#
+# * Any other series -> the default branch carries exactly one development
+# line, so a later series is not active yet and an earlier series is no
+# longer in development. Neither may target the default branch.
+#
+# This rule is self-maintaining: no hard-coded version list needs updating when
+# a new release branch is cut.
+#
+# VALIDATION MATRIX
+# -----------------
+# Target branch Release branch exists? Result
+# ----------------------- ----------------------- ----------------------
+# release/. n/a (it is the target) pass
+# another release/* n/a fail (mismatch)
+# default branch no, active line pass
+# default branch no, later configured line fail (not active yet)
+# default branch no, earlier configured line fail (no longer in development)
+# default branch no active line configured fail (milestone missing)
+# default branch yes fail (needs servicing branch)
+# anything else n/a skipped (integration branch)
+#
+# Pull requests into long-lived integration branches (e.g. "dev/paul/foo") are
+# skipped, because the milestone is enforced when that branch is merged into
+# the default branch or a release branch.
+#
+# Milestones that don't parse as ".." are skipped with a
+# notice rather than failing the build.
+#
+# REQUIRED ENVIRONMENT VARIABLES
+# ------------------------------
+# MILESTONE_TITLE The PR's milestone title (e.g. "7.0.3").
+# BASE_REF The branch the PR targets (e.g. "main", "release/7.0").
+# DEFAULT_BRANCH The repository's default branch (e.g. "main").
+# GITHUB_REPOSITORY Owner/repo (e.g. "dotnet/SqlClient"). Set automatically by Actions.
+# GH_TOKEN GitHub token for API calls (gh CLI auth).
+#
+# OUTPUTS
+# -------
+# Emits ::notice:: on success/skip and ::error:: on failure.
+# Exits 0 when the milestone and target branch agree (or the check is
+# skipped), and 1 when they conflict.
+#
+# USAGE
+# Called from the check-milestone.yml workflow. Can also be run locally:
+#
+# export MILESTONE_TITLE="7.0.3"
+# export BASE_REF="main"
+# export DEFAULT_BRANCH="main"
+# export GITHUB_REPOSITORY="dotnet/SqlClient"
+# bash .github/scripts/check-milestone-branch.sh
+#
+#################################################################################
+set -euo pipefail
+
+# -- Runtime help -------------------------------------------------------------
+if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
+ # Print the header comment block (between the license banner and the
+ # closing banner), stripping the leading '# ' prefix.
+ awk '/^#{2,}$/ { n++; next } n == 2 { sub(/^# ?/, ""); print }' "$0"
+ exit 0
+fi
+
+# -- Input validation ---------------------------------------------------------
+: "${MILESTONE_TITLE:?MILESTONE_TITLE environment variable is required}"
+: "${BASE_REF:?BASE_REF environment variable is required}"
+: "${DEFAULT_BRANCH:?DEFAULT_BRANCH environment variable is required}"
+: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY environment variable is required}"
+
+# -- Derive the candidate release branch from the milestone -------------------
+# Accepts "X.Y.Z" with an optional pre-release/build suffix, e.g. "8.0.0-preview1".
+if [[ "${MILESTONE_TITLE}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)([-+].*)?$ ]]; then
+ MAJOR="${BASH_REMATCH[1]}"
+ MINOR="${BASH_REMATCH[2]}"
+ PATCH="${BASH_REMATCH[3]}"
+else
+ echo "::notice::Milestone '${MILESTONE_TITLE}' is not in 'major.minor.patch' form; skipping the target branch check."
+ exit 0
+fi
+
+RELEASE_BRANCH="release/${MAJOR}.${MINOR}"
+
+# -- Skip integration branches ------------------------------------------------
+# Only the default branch and release branches carry milestone semantics.
+if [[ "${BASE_REF}" != "${DEFAULT_BRANCH}" && "${BASE_REF}" != release/* ]]; then
+ echo "::notice::PR targets integration branch '${BASE_REF}'; skipping the milestone/branch check."
+ exit 0
+fi
+
+# -- Validate a release branch target -----------------------------------------
+# The target branch itself proves which version is being serviced, so no branch
+# listing is needed here.
+if [[ "${BASE_REF}" == release/* ]]; then
+ if [[ "${BASE_REF}" != "${RELEASE_BRANCH}" ]]; then
+ echo "::error::Milestone '${MILESTONE_TITLE}' belongs to '${RELEASE_BRANCH}', but this PR targets '${BASE_REF}'. Retarget the PR or assign the milestone that matches '${BASE_REF}'."
+ exit 1
+ fi
+
+ echo "::notice::Milestone '${MILESTONE_TITLE}' matches target branch '${BASE_REF}'."
+ exit 0
+fi
+
+# -- Validate a default branch target -----------------------------------------
+# 'matching-refs' returns only refs under the given prefix, so this is a single
+# cheap call regardless of how many topic branches the repository has.
+if ! RELEASE_REFS=$(gh api "repos/${GITHUB_REPOSITORY}/git/matching-refs/heads/release/" \
+ --jq '.[].ref' 2>&1); then
+ echo "::error::Unable to list release branches for '${GITHUB_REPOSITORY}': ${RELEASE_REFS}"
+ exit 1
+fi
+
+if grep -qxF "refs/heads/${RELEASE_BRANCH}" <<< "${RELEASE_REFS}"; then
+ echo "::error::Milestone '${MILESTONE_TITLE}' is a servicing release owned by '${RELEASE_BRANCH}', but this PR targets '${DEFAULT_BRANCH}'. Either retarget the PR to '${RELEASE_BRANCH}', or assign an in-development milestone and add the 'Hotfix ${MAJOR}.${MINOR}.${PATCH}' label so the change is cherry-picked after merge."
+ exit 1
+fi
+
+if ! MILESTONES=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/milestones?state=all&per_page=100" \
+ --jq '.[].title' 2>&1); then
+ echo "::error::Unable to list milestones for '${GITHUB_REPOSITORY}': ${MILESTONES}"
+ exit 1
+fi
+
+ACTIVE_MAJOR=""
+ACTIVE_MINOR=""
+LATEST_RELEASE_MAJOR=""
+LATEST_RELEASE_MINOR=""
+while IFS= read -r release_ref; do
+ if [[ ! "${release_ref}" =~ ^refs/heads/release/([0-9]+)\.([0-9]+)$ ]]; then
+ continue
+ fi
+
+ release_major="${BASH_REMATCH[1]}"
+ release_minor="${BASH_REMATCH[2]}"
+ if [[ -z "${LATEST_RELEASE_MAJOR}" ]] ||
+ (( 10#${release_major} > 10#${LATEST_RELEASE_MAJOR} )) ||
+ (( 10#${release_major} == 10#${LATEST_RELEASE_MAJOR} && 10#${release_minor} > 10#${LATEST_RELEASE_MINOR} )); then
+ LATEST_RELEASE_MAJOR="${release_major}"
+ LATEST_RELEASE_MINOR="${release_minor}"
+ fi
+done <<< "${RELEASE_REFS}"
+
+while IFS= read -r milestone; do
+ if [[ ! "${milestone}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)([-+].*)?$ ]]; then
+ continue
+ fi
+
+ candidate_major="${BASH_REMATCH[1]}"
+ candidate_minor="${BASH_REMATCH[2]}"
+ candidate_branch="release/${candidate_major}.${candidate_minor}"
+ if grep -qxF "refs/heads/${candidate_branch}" <<< "${RELEASE_REFS}"; then
+ continue
+ fi
+
+ if [[ -n "${LATEST_RELEASE_MAJOR}" ]] &&
+ { (( 10#${candidate_major} < 10#${LATEST_RELEASE_MAJOR} )) ||
+ (( 10#${candidate_major} == 10#${LATEST_RELEASE_MAJOR} && 10#${candidate_minor} <= 10#${LATEST_RELEASE_MINOR} )); }; then
+ continue
+ fi
+
+ if [[ -z "${ACTIVE_MAJOR}" ]] ||
+ (( 10#${candidate_major} < 10#${ACTIVE_MAJOR} )) ||
+ (( 10#${candidate_major} == 10#${ACTIVE_MAJOR} && 10#${candidate_minor} < 10#${ACTIVE_MINOR} )); then
+ ACTIVE_MAJOR="${candidate_major}"
+ ACTIVE_MINOR="${candidate_minor}"
+ fi
+done <<< "${MILESTONES}"
+
+if [[ -z "${ACTIVE_MAJOR}" ]]; then
+ echo "::error::No configured milestone series is newer than the newest release branch, so no development line is active on '${DEFAULT_BRANCH}'. Create the milestone for the next version before targeting '${DEFAULT_BRANCH}' with '${MILESTONE_TITLE}'."
+ exit 1
+fi
+
+if (( 10#${MAJOR} != 10#${ACTIVE_MAJOR} || 10#${MINOR} != 10#${ACTIVE_MINOR} )); then
+ if (( 10#${MAJOR} > 10#${ACTIVE_MAJOR} ||
+ (10#${MAJOR} == 10#${ACTIVE_MAJOR} && 10#${MINOR} > 10#${ACTIVE_MINOR}) )); then
+ echo "::error::Milestone '${MILESTONE_TITLE}' is for a later development line, but the ${ACTIVE_MAJOR}.${ACTIVE_MINOR} milestone series remains active on '${DEFAULT_BRANCH}' until 'release/${ACTIVE_MAJOR}.${ACTIVE_MINOR}' is cut. Assign a milestone from the active ${ACTIVE_MAJOR}.${ACTIVE_MINOR} line."
+ else
+ echo "::error::Milestone '${MILESTONE_TITLE}' is for the ${MAJOR}.${MINOR} line, which is no longer in development on '${DEFAULT_BRANCH}'; the active line is ${ACTIVE_MAJOR}.${ACTIVE_MINOR}. Assign a milestone from the active ${ACTIVE_MAJOR}.${ACTIVE_MINOR} line."
+ fi
+ exit 1
+fi
+
+echo "::notice::Milestone '${MILESTONE_TITLE}' is still in development (no '${RELEASE_BRANCH}' branch); targeting '${DEFAULT_BRANCH}' is correct."
diff --git a/.github/scripts/cherry-pick-to-release.sh b/.github/scripts/cherry-pick-to-release.sh
new file mode 100755
index 0000000000..69a655d110
--- /dev/null
+++ b/.github/scripts/cherry-pick-to-release.sh
@@ -0,0 +1,221 @@
+#!/usr/bin/env bash
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# cherry-pick-to-release.sh
+#
+# Cherry-picks a merge commit from the default branch onto a release branch
+# and opens a pull request for the result. If the cherry-pick conflicts, an
+# empty-commit placeholder PR is created with manual resolution instructions.
+#
+# OVERVIEW
+# --------
+# This script performs the following steps:
+#
+# 1. Derive the target release branch from the version's major.minor
+# (e.g. "7.0.1" → release/7.0).
+#
+# 2. Check whether the commit's patch is already present on the target
+# branch (via 'git cherry'). If so, exit cleanly — nothing to do.
+#
+# 3. Detect whether the merge commit is a true merge (2+ parents) or a
+# squash-merge (1 parent). True merges require '--mainline 1'.
+#
+# 4. Attempt the cherry-pick:
+# - On success: push the branch, look up the milestone, create a PR.
+# - On conflict: abort, push an empty-commit placeholder, create a
+# "CONFLICTS" PR with manual resolution instructions.
+#
+# 5. Milestone lookup is best-effort. If the milestone doesn't exist yet
+# the PR is created without one and a warning note is added to the body.
+#
+# REQUIRED ENVIRONMENT VARIABLES
+# ------------------------------
+# VERSION Full hotfix version, e.g. "7.0.1".
+# MERGE_COMMIT_SHA SHA of the merge commit on the default branch.
+# PR_NUMBER Number of the original PR that was merged.
+# PR_TITLE Title of the original PR (used in cherry-pick PR title).
+# GH_TOKEN GitHub token for 'gh' CLI authentication.
+# GITHUB_REPOSITORY Owner/repo (e.g. "dotnet/SqlClient"). Set by Actions.
+#
+# OUTPUTS
+# -------
+# On success or conflict, a new PR is created on GitHub.
+# On already-applied, the script exits 0 with a notice.
+#
+# USAGE
+# Typically called from the cherry-pick-hotfix.yml workflow.
+# The git working directory must have full history (fetch-depth: 0) and
+# user.name / user.email must be configured before calling this script.
+#
+# Local testing example (dry-run — comment out 'gh pr create' calls):
+#
+# export VERSION="7.0.1"
+# export MERGE_COMMIT_SHA="abc123"
+# export PR_NUMBER=42
+# export PR_TITLE="Fix connection timeout"
+# export GH_TOKEN="ghp_..."
+# export GITHUB_REPOSITORY="dotnet/SqlClient"
+# bash .github/scripts/cherry-pick-to-release.sh
+#
+#################################################################################
+set -euo pipefail
+
+# -- Runtime help -------------------------------------------------------------
+if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
+ # Print the header comment block (between the license banner and the
+ # closing banner), stripping the leading '# ' prefix.
+ awk '/^#{2,}$/ { n++; next } n == 2 { sub(/^# ?/, ""); print }' "$0"
+ exit 0
+fi
+
+# -- Input validation ---------------------------------------------------------
+: "${VERSION:?VERSION environment variable is required}"
+: "${MERGE_COMMIT_SHA:?MERGE_COMMIT_SHA environment variable is required}"
+: "${PR_NUMBER:?PR_NUMBER environment variable is required}"
+: "${PR_TITLE:?PR_TITLE environment variable is required}"
+: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY environment variable is required}"
+
+# -- Step 1: Derive target branch from major.minor ---------------------------
+# "7.0.1" → "7.0", so target branch is "release/7.0".
+# Use a bash regex for portability (grep -P is not available on macOS BSD grep).
+if [[ "${VERSION}" =~ ^([0-9]+)\.([0-9]+) ]]; then
+ BRANCH_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
+else
+ BRANCH_VERSION=""
+fi
+if [[ -z "${BRANCH_VERSION}" ]]; then
+ echo "::error::Could not parse major.minor from version '${VERSION}'."
+ exit 1
+fi
+
+TARGET_BRANCH="release/${BRANCH_VERSION}"
+CHERRY_PICK_BRANCH="dev/automation/pr-${PR_NUMBER}-to-${VERSION}"
+
+echo "Version: ${VERSION}"
+echo "Target branch: ${TARGET_BRANCH}"
+echo "Cherry-pick branch: ${CHERRY_PICK_BRANCH}"
+echo "Merge commit: ${MERGE_COMMIT_SHA}"
+
+# Ensure the target branch ref is available locally.
+git fetch origin "${TARGET_BRANCH}"
+
+# -- Step 2: Check if the patch is already applied ----------------------------
+# 'git cherry ' lists commits in .. and
+# marks each with '-' (patch already on ) or '+' (not yet applied).
+# By passing =MERGE_COMMIT_SHA and =MERGE_COMMIT_SHA^ we scope
+# the check to exactly one commit — the PR being cherry-picked.
+if git cherry "origin/${TARGET_BRANCH}" "${MERGE_COMMIT_SHA}" "${MERGE_COMMIT_SHA}^" \
+ | grep -q '^-'; then
+ echo "::notice::Commit ${MERGE_COMMIT_SHA} is already applied on" \
+ "${TARGET_BRANCH}. Skipping cherry-pick."
+ exit 0
+fi
+
+# Create the cherry-pick working branch from the target release branch.
+git checkout -b "${CHERRY_PICK_BRANCH}" "origin/${TARGET_BRANCH}"
+
+# -- Step 3: Detect merge commit type -----------------------------------------
+# True merge commits have 2+ parents and require '--mainline 1' to tell git
+# which parent's tree to diff against (the first parent = the target branch).
+# Squash-merge commits have exactly 1 parent and must NOT use --mainline.
+#
+# 'git rev-list --parents -n1 ' outputs: [ ...]
+# awk counts fields and subtracts 1 (the SHA itself) to get the parent count.
+PARENT_COUNT=$(git rev-list --parents -n1 "${MERGE_COMMIT_SHA}" \
+ | awk '{print NF - 1}')
+MAINLINE_FLAG=""
+if [[ "${PARENT_COUNT}" -gt 1 ]]; then
+ MAINLINE_FLAG="--mainline 1"
+ echo "Merge commit has ${PARENT_COUNT} parents — using --mainline 1."
+else
+ echo "Squash-merge commit (single parent) — no --mainline flag needed."
+fi
+
+# -- Helper: look up milestone ------------------------------------------------
+# Milestone assignment is best-effort. If the milestone doesn't exist yet, the
+# PR is created without one and a note is appended to the PR body.
+lookup_milestone() {
+ local version="$1"
+ MILESTONE_ARG=""
+ MILESTONE_NOTE=""
+
+ if gh api "repos/${GITHUB_REPOSITORY}/milestones" --method GET --paginate \
+ --field state=open --jq '.[].title' | grep -qx "${version}"; then
+ MILESTONE_ARG="--milestone ${version}"
+ echo "Milestone '${version}' found."
+ else
+ echo "::warning::Milestone '${version}' does not exist." \
+ "PR will be created without a milestone."
+ MILESTONE_NOTE=$'\n\n> **Note:** Milestone `'"${version}"'` does not exist yet. Please create it and assign this PR manually.'
+ fi
+}
+
+# -- Step 4: Attempt the cherry-pick ------------------------------------------
+# Options (--mainline) must precede the commit operand.
+if git cherry-pick ${MAINLINE_FLAG} "${MERGE_COMMIT_SHA}"; then
+ # --- Success path ---
+ echo "Cherry-pick succeeded. Pushing branch and creating PR."
+ git push origin "${CHERRY_PICK_BRANCH}"
+
+ lookup_milestone "${VERSION}"
+
+ gh pr create \
+ --base "${TARGET_BRANCH}" \
+ --head "${CHERRY_PICK_BRANCH}" \
+ --title "[${VERSION} Cherry-pick] ${PR_TITLE}" \
+ --body "Cherry-pick of #${PR_NUMBER} (${MERGE_COMMIT_SHA}) into \`${TARGET_BRANCH}\`.${MILESTONE_NOTE}" \
+ ${MILESTONE_ARG}
+else
+ # --- Conflict path ---
+ echo "::error::Cherry-pick of ${MERGE_COMMIT_SHA} failed due to conflicts."
+ git cherry-pick --abort
+
+ # Build the cherry-pick command for inclusion in the conflict-resolution
+ # instructions. Options must precede the commit SHA.
+ CHERRY_PICK_CMD="git cherry-pick"
+ if [[ -n "${MAINLINE_FLAG}" ]]; then
+ CHERRY_PICK_CMD="${CHERRY_PICK_CMD} ${MAINLINE_FLAG}"
+ fi
+ CHERRY_PICK_CMD="${CHERRY_PICK_CMD} ${MERGE_COMMIT_SHA}"
+
+ # Create a branch with an empty commit so a PR can be opened. The PR body
+ # contains step-by-step instructions for manual conflict resolution.
+ git checkout "origin/${TARGET_BRANCH}"
+ git checkout -B "${CHERRY_PICK_BRANCH}"
+ git commit --allow-empty \
+ -m "Cherry-pick of #${PR_NUMBER} requires manual resolution" \
+ -m "To resolve, run: ${CHERRY_PICK_CMD}"
+ git push origin "${CHERRY_PICK_BRANCH}"
+
+ lookup_milestone "${VERSION}"
+
+ # Build the PR body using printf to avoid quoting pitfalls with embedded
+ # newlines (mixed $'...' and '...' quoting can leave literal \n in output).
+ CONFLICT_BODY="$(printf '%s' \
+ "Cherry-pick of #${PR_NUMBER} (${MERGE_COMMIT_SHA}) into " \
+ "\`${TARGET_BRANCH}\` **failed due to merge conflicts**." \
+ "${MILESTONE_NOTE}" \
+ $'\n\nPlease resolve manually:\n```bash\n' \
+ "git fetch origin" \
+ $'\n' \
+ "git checkout ${CHERRY_PICK_BRANCH}" \
+ $'\n' \
+ "${CHERRY_PICK_CMD}" \
+ $'\n' \
+ "# resolve conflicts" \
+ $'\n' \
+ "git push origin ${CHERRY_PICK_BRANCH} --force" \
+ $'\n```')"
+
+ gh pr create \
+ --draft \
+ --base "${TARGET_BRANCH}" \
+ --head "${CHERRY_PICK_BRANCH}" \
+ --title "[${VERSION} Cherry-pick - CONFLICTS] ${PR_TITLE}" \
+ ${MILESTONE_ARG} \
+ --body "${CONFLICT_BODY}"
+fi
diff --git a/.github/scripts/extract-hotfix-versions.sh b/.github/scripts/extract-hotfix-versions.sh
new file mode 100755
index 0000000000..b7c702a8f8
--- /dev/null
+++ b/.github/scripts/extract-hotfix-versions.sh
@@ -0,0 +1,136 @@
+#!/usr/bin/env bash
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# extract-hotfix-versions.sh
+#
+# Parses "Hotfix X.Y.Z" labels from a merged GitHub PR and emits a JSON array
+# of version strings suitable for use as a GitHub Actions matrix dimension.
+#
+# OVERVIEW
+# --------
+# This script handles two distinct trigger scenarios:
+#
+# 1. 'closed' event — The PR was just merged. ALL "Hotfix X.Y.Z" labels on
+# the PR are processed, emitting one version per valid label.
+#
+# 2. 'labeled' event — A label was added to an already-merged PR. Only the
+# NEWLY ADDED label is considered, and only if a cherry-pick for that
+# version hasn't already been created (branch or PR exists).
+#
+# Label names must match the exact pattern "Hotfix .."
+# (e.g. "Hotfix 7.0.1"). All other labels are silently ignored.
+#
+# REQUIRED ENVIRONMENT VARIABLES
+# ------------------------------
+# LABELS Comma-separated list of all label names on the PR.
+# EVENT_ACTION The GitHub event action: "closed" or "labeled".
+# EVENT_LABEL For 'labeled' events, the name of the label that was added.
+# Empty or unset for 'closed' events.
+# PR_NUMBER The pull request number (used to derive cherry-pick branch names).
+# GH_TOKEN GitHub token for API calls (gh CLI auth).
+# GITHUB_REPOSITORY Owner/repo (e.g. "dotnet/SqlClient"). Set automatically by Actions.
+#
+# OUTPUTS
+# -------
+# Writes to $GITHUB_OUTPUT:
+# versions= e.g. versions=["7.0.1","8.0.0"]
+#
+# An empty array (versions=[]) means no work is needed.
+# The script exits with code 1 if the 'closed' event has no valid labels.
+#
+# USAGE
+# Called from the cherry-pick-hotfix.yml workflow. Can also be run locally
+# for testing by setting the required environment variables and providing a
+# writable GITHUB_OUTPUT file:
+#
+# export LABELS="Hotfix 7.0.1,bug"
+# export EVENT_ACTION="closed"
+# export PR_NUMBER=42
+# export GITHUB_OUTPUT=$(mktemp)
+# bash .github/scripts/extract-hotfix-versions.sh
+# cat "$GITHUB_OUTPUT"
+#
+#################################################################################
+set -euo pipefail
+
+# -- Runtime help -------------------------------------------------------------
+if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
+ # Print the header comment block (between the license banner and the
+ # closing banner), stripping the leading '# ' prefix.
+ awk '/^#{2,}$/ { n++; next } n == 2 { sub(/^# ?/, ""); print }' "$0"
+ exit 0
+fi
+
+# -- Input validation ---------------------------------------------------------
+: "${LABELS:?LABELS environment variable is required}"
+: "${EVENT_ACTION:?EVENT_ACTION environment variable is required}"
+: "${PR_NUMBER:?PR_NUMBER environment variable is required}"
+: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY environment variable is required}"
+
+# -- 'labeled' event: process only the newly added label ----------------------
+if [[ "${EVENT_ACTION}" == "labeled" ]]; then
+ # Extract version from the new label. If it doesn't match "Hotfix X.Y.Z",
+ # this is a non-hotfix label — emit empty matrix and exit cleanly.
+ if [[ "${EVENT_LABEL:-}" =~ ^Hotfix\ ([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
+ CANDIDATE="${BASH_REMATCH[1]}"
+ else
+ CANDIDATE=""
+ fi
+
+ if [[ -z "${CANDIDATE}" ]]; then
+ echo "Label '${EVENT_LABEL:-}' is not a valid 'Hotfix X.Y.Z' label. Skipping."
+ echo "versions=[]" >> "${GITHUB_OUTPUT}"
+ exit 0
+ fi
+
+ # Guard against duplicate cherry-picks. If the cherry-pick branch already
+ # exists on the remote, or a PR (open, closed, or merged) was already created
+ # from it, there is nothing left to do.
+ #
+ # NOTE: We use the GitHub API rather than 'git ls-remote' because the
+ # detect-versions job does not check out the repository (no .git directory).
+ CHERRY_PICK_BRANCH="dev/automation/pr-${PR_NUMBER}-to-${CANDIDATE}"
+
+ if gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${CHERRY_PICK_BRANCH}" \
+ --silent 2>/dev/null; then
+ echo "Cherry-pick branch '${CHERRY_PICK_BRANCH}' already exists. Skipping."
+ echo "versions=[]" >> "${GITHUB_OUTPUT}"
+ exit 0
+ fi
+
+ EXISTING_PR=$(gh pr list --repo "${GITHUB_REPOSITORY}" --head "${CHERRY_PICK_BRANCH}" --state all \
+ --json number --jq 'length')
+ if [[ "${EXISTING_PR}" -gt 0 ]]; then
+ echo "A cherry-pick PR from '${CHERRY_PICK_BRANCH}' already exists. Skipping."
+ echo "versions=[]" >> "${GITHUB_OUTPUT}"
+ exit 0
+ fi
+
+ VERSIONS="${CANDIDATE}"
+else
+ # -- 'closed' event: process all hotfix labels on the PR --------------------
+ # Split by comma, keep only labels matching "Hotfix X.Y.Z", extract the version.
+ # Use sed -E for portable extended regex (works on both GNU and BSD sed).
+ VERSIONS=$(echo "${LABELS}" | tr ',' '\n' \
+ | sed -nE 's/^Hotfix ([0-9]+\.[0-9]+\.[0-9]+)$/\1/p')
+fi
+
+# -- Validate that at least one version was found ----------------------------
+if [[ -z "${VERSIONS}" ]]; then
+ echo "::error::No valid 'Hotfix X.Y.Z' label found. " \
+ "Labels must match 'Hotfix ..'."
+ exit 1
+fi
+
+# -- Emit JSON array for the matrix strategy ----------------------------------
+# Convert the newline-separated version list into a compact JSON array.
+# e.g. "7.0.1\n8.0.0" → ["7.0.1","8.0.0"]
+JSON=$(echo "${VERSIONS}" \
+ | jq -R -s -c 'split("\n") | map(select(length > 0))')
+
+echo "versions=${JSON}" >> "${GITHUB_OUTPUT}"
+echo "Detected hotfix versions: ${JSON}"
diff --git a/.github/scripts/recheck-milestones-for-release-branch.sh b/.github/scripts/recheck-milestones-for-release-branch.sh
new file mode 100755
index 0000000000..b48ceb4ad6
--- /dev/null
+++ b/.github/scripts/recheck-milestones-for-release-branch.sh
@@ -0,0 +1,150 @@
+#!/usr/bin/env bash
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# recheck-milestones-for-release-branch.sh
+#
+# Re-runs the milestone check for open pull requests whose result can change
+# when a release branch is created.
+#
+# OVERVIEW
+# --------
+# check-milestone-branch.sh decides where a milestone belongs by asking whether
+# release/. exists and which configured milestone series is the
+# earliest one without a release branch. Cutting a branch therefore moves its
+# X.Y.* milestones into servicing and can make the next series active.
+#
+# Creating a branch emits no pull request activity, so an already-open PR keeps
+# whatever result it last recorded. This script closes that gap by re-running
+# every semantic-version-milestoned PR targeting the default branch. That covers
+# both the newly serviced series and any later series whose eligibility changes.
+#
+# Re-running is enough because check-milestone-branch.sh queries the live list
+# of release branches. The replayed event payload still carries the correct
+# milestone and base branch, since the check re-runs on every 'milestoned' and
+# 'edited' activity, so the newest run always reflects the current PR state.
+#
+# LIMITATION
+# ----------
+# A re-run replays the original run's commit, which means it executes the
+# workflow definition and script as they existed then. Only the release branch
+# lookup is evaluated live.
+#
+# So a pull request whose most recent milestone check predates the arrival of
+# the target-branch rule will replay the older check, pass it, and keep its
+# stale result. This is transitional: any pull request with activity after the
+# rule shipped has a run that contains it. It is not detected here, because
+# distinguishing a stale replay costs an extra API call per pull request and
+# the window closes on its own. After the first release branch is cut following
+# a change to the check itself, review the affected pull requests by hand.
+#
+# REQUIRED ENVIRONMENT VARIABLES
+# ------------------------------
+# RELEASE_BRANCH The branch that was just created (e.g. "release/7.1").
+# DEFAULT_BRANCH The repository's default branch (e.g. "main").
+# WORKFLOW_FILE Workflow file name to re-run (e.g. "check-milestone.yml").
+# GITHUB_REPOSITORY Owner/repo (e.g. "dotnet/SqlClient"). Set automatically by Actions.
+# GH_TOKEN GitHub token for API calls. Needs 'actions: write'.
+#
+# OUTPUTS
+# -------
+# Emits ::notice:: per PR re-run and ::warning:: for any PR that could not be
+# re-run. Exits 1 if at least one re-run failed, so the failure is visible in
+# the Actions UI; a maintainer can then re-run those checks by hand.
+#
+# USAGE
+# Called from the recheck-milestones.yml workflow. Can also be run locally:
+#
+# export RELEASE_BRANCH="release/7.1"
+# export DEFAULT_BRANCH="main"
+# export WORKFLOW_FILE="check-milestone.yml"
+# export GITHUB_REPOSITORY="dotnet/SqlClient"
+# bash .github/scripts/recheck-milestones-for-release-branch.sh
+#
+#################################################################################
+# 'set -e' is deliberately omitted: one PR failing to re-run must not abandon
+# the rest. Failures are tracked explicitly and reported at the end.
+set -uo pipefail
+
+# -- Runtime help -------------------------------------------------------------
+if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
+ # Print the header comment block (between the license banner and the
+ # closing banner), stripping the leading '# ' prefix.
+ awk '/^#{2,}$/ { n++; next } n == 2 { sub(/^# ?/, ""); print }' "$0"
+ exit 0
+fi
+
+# -- Input validation ---------------------------------------------------------
+: "${RELEASE_BRANCH:?RELEASE_BRANCH environment variable is required}"
+: "${DEFAULT_BRANCH:?DEFAULT_BRANCH environment variable is required}"
+: "${WORKFLOW_FILE:?WORKFLOW_FILE environment variable is required}"
+: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY environment variable is required}"
+
+if [[ ! "${RELEASE_BRANCH}" =~ ^release/[0-9]+\.[0-9]+$ ]]; then
+ echo "::notice::'${RELEASE_BRANCH}' is not a 'release/.' branch; nothing to reconcile."
+ exit 0
+fi
+
+# -- Find open PRs whose result can change -------------------------------------
+if ! OPEN_PRS=$(gh pr list --repo "${GITHUB_REPOSITORY}" --base "${DEFAULT_BRANCH}" \
+ --state open --limit 500 --json number,headRefOid,milestone \
+ --jq '.[] | select(.milestone != null) | "\(.number) \(.headRefOid) \(.milestone.title)"' 2>&1); then
+ echo "::error::Unable to list open pull requests for '${GITHUB_REPOSITORY}': ${OPEN_PRS}"
+ exit 1
+fi
+
+FAILED=0
+MATCHED=0
+
+while read -r NUMBER HEAD_SHA MILESTONE_TITLE; do
+ [[ -n "${NUMBER}" ]] || continue
+
+ # Same milestone grammar as check-milestone-branch.sh.
+ [[ "${MILESTONE_TITLE}" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+].*)?$ ]] || continue
+
+ MATCHED=$((MATCHED + 1))
+
+ RUNS=$(gh api \
+ "repos/${GITHUB_REPOSITORY}/actions/workflows/${WORKFLOW_FILE}/runs?head_sha=${HEAD_SHA}&per_page=100" \
+ 2>/dev/null)
+
+ # One head commit can back PRs against several bases, so prefer the run that
+ # names this PR rather than just the newest run for the SHA.
+ RUN_ID=$(jq -r --argjson pr "${NUMBER}" \
+ '([.workflow_runs[] | select(any(.pull_requests[]?; .number == $pr))] | first | .id) // empty' \
+ <<< "${RUNS}" 2>/dev/null)
+
+ # 'pull_requests' is empty for runs from forked repositories, so fall back to
+ # the newest run for the SHA when the association is unavailable.
+ if [[ -z "${RUN_ID}" ]]; then
+ RUN_ID=$(jq -r '.workflow_runs[0].id // empty' <<< "${RUNS}" 2>/dev/null)
+ fi
+
+ if [[ -z "${RUN_ID}" ]]; then
+ echo "::warning::PR #${NUMBER} (milestone '${MILESTONE_TITLE}') has no milestone check run to re-run; re-check it manually."
+ FAILED=$((FAILED + 1))
+ continue
+ fi
+
+ if gh run rerun "${RUN_ID}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
+ echo "::notice::Re-ran the milestone check for PR #${NUMBER} (milestone '${MILESTONE_TITLE}', run ${RUN_ID})."
+ else
+ echo "::warning::Could not re-run the milestone check for PR #${NUMBER} (run ${RUN_ID}); re-check it manually."
+ FAILED=$((FAILED + 1))
+ fi
+done <<< "${OPEN_PRS}"
+
+if [[ "${MATCHED}" -eq 0 ]]; then
+ echo "::notice::No open PR targeting '${DEFAULT_BRANCH}' carries a semantic-version milestone."
+ exit 0
+fi
+
+if [[ "${FAILED}" -gt 0 ]]; then
+ echo "::error::${FAILED} of ${MATCHED} affected pull requests could not be re-checked automatically."
+ exit 1
+fi
+
+echo "::notice::Re-checked ${MATCHED} pull request(s) affected by '${RELEASE_BRANCH}'."
diff --git a/.github/scripts/tests/README.md b/.github/scripts/tests/README.md
new file mode 100644
index 0000000000..9a4ea68906
--- /dev/null
+++ b/.github/scripts/tests/README.md
@@ -0,0 +1,182 @@
+# GitHub Actions Script Tests
+
+This directory contains tests for the shell scripts used by the
+[cherry-pick-hotfix](./../../../.github/workflows/cherry-pick-hotfix.yml),
+[check-milestone](./../../../.github/workflows/check-milestone.yml) and
+[recheck-milestones](./../../../.github/workflows/recheck-milestones.yml) GitHub Actions workflows.
+These tests are intended to be run manually by developers when they are changing the associated
+scripts, and not as part of any CI runs.
+
+## What is Bats?
+
+**[Bats](https://github.com/bats-core/bats-core)** (Bash Automated Testing System) is a
+TAP-compliant testing framework for Bash scripts. Each `.bats` file contains one or more `@test`
+blocks that run shell commands and assert outcomes using the built-in `run` helper. A test passes
+when every command exits with code 0; it fails on the first non-zero exit.
+
+Key concepts:
+
+| Concept | Description |
+| ------- | ----------- |
+| `@test "name" { ... }` | Defines a single test case |
+| `run ` | Captures stdout, stderr, and exit code into `$output` and `$status` |
+| `setup()` | Runs before every `@test` — used to create temp files and set env vars |
+| `teardown()` | Runs after every `@test` — used to clean up temp files |
+| `[[ "$status" -eq 0 ]]` | Assert exit code |
+| `[[ "$output" == *"text"* ]]` | Assert output contains a string |
+
+## Installing Bats
+
+### Linux (apt)
+
+```bash
+sudo apt-get update && sudo apt-get install -y bats
+```
+
+### macOS (Homebrew)
+
+```bash
+brew install bats-core
+```
+
+### From source (any platform)
+
+```bash
+git clone https://github.com/bats-core/bats-core.git
+cd bats-core
+sudo ./install.sh /usr/local
+```
+
+## Additional Prerequisites
+
+The scripts under test also require:
+
+- **jq** — used to build JSON matrix output
+- **gh** (GitHub CLI) — used for API calls and PR creation (mocked during tests, but must be on
+ `$PATH` for the test stubs to shadow it)
+
+Most CI runners and development machines have these pre-installed. If not:
+
+```bash
+# Linux (apt)
+sudo apt-get install -y jq gh
+
+# macOS (Homebrew)
+brew install jq gh
+```
+
+### Verify installation
+
+```bash
+bats --version
+# Expected output: Bats 1.x.x
+```
+
+## Running the Tests
+
+All commands assume you are at the **repository root**.
+
+### Run all tests
+
+```bash
+bats .github/scripts/tests/
+```
+
+### Run a single test file
+
+```bash
+bats .github/scripts/tests/extract-hotfix-versions.bats
+bats .github/scripts/tests/cherry-pick-to-release.bats
+bats .github/scripts/tests/check-milestone-branch.bats
+bats .github/scripts/tests/recheck-milestones-for-release-branch.bats
+```
+
+### Run a specific test by name
+
+```bash
+bats .github/scripts/tests/extract-hotfix-versions.bats \
+ --filter "single Hotfix label"
+```
+
+### Verbose output (show each test name)
+
+```bash
+bats --tap .github/scripts/tests/
+```
+
+### Pretty output (requires bats-core 1.5+)
+
+```bash
+bats --formatter pretty .github/scripts/tests/
+```
+
+## Test Files
+
+| File | Tests | Covers |
+| ---- | ----- | ------ |
+| `extract-hotfix-versions.bats` | 18 | Label parsing, version extraction, matrix JSON output, edge cases (malformed labels, duplicates, `labeled` vs `closed` events) |
+| `cherry-pick-to-release.bats` | 15 | Branch derivation, already-applied detection, clean cherry-pick, conflict handling, milestone lookup, PR creation, duplicate skip logic |
+| `check-milestone-branch.bats` | 26 | Milestone version parsing, state-independent active development-line selection, rejection of earlier and later series on the default branch, fail-closed handling when no series is active, release-branch derivation, default-branch vs release-branch validation, integration-branch and non-semver skips, API invocation assertions, API failure handling |
+| `recheck-milestones-for-release-branch.bats` | 17 | Release-branch name parsing, matching open PRs by milestone, run lookup by head SHA and PR association, fork fallback, re-run invocation, and failure reporting |
+
+## How the Tests Work
+
+All test files use the same general approach:
+
+1. **`setup()`** creates a temporary directory and populates it with mock `git` and `gh` executables
+ — simple shell scripts that echo predetermined responses. Environment variables (`VERSION`,
+ `MERGE_COMMIT_SHA`, etc.) are set to known values.
+
+2. **`@test` blocks** call `run bash "$SCRIPT"` to execute the script under test in a subshell. The
+ mocks intercept all `git` and `gh` invocations, so no real repository or GitHub API access is
+ needed.
+
+3. **Assertions** check `$status` (exit code) and `$output` (combined stdout/stderr) for expected
+ values, error messages, GitHub Actions output file writes via `$GITHUB_OUTPUT`, or other
+ workflow commands such as `::error::` and `::notice::`.
+
+4. **`teardown()`** removes the temporary directory and mock binaries.
+
+### Example mock
+
+```bash
+# Mock git that reports 2 parents (a merge commit)
+cat > "${STUB_DIR}/git" <<'MOCK'
+#!/usr/bin/env bash
+case "$*" in
+ "rev-list --parents -n1 "*) echo "abc123 parent1 parent2" ;;
+ "cherry "*) echo "+ abc123" ;;
+ *) echo "git mock: $*" ;;
+esac
+MOCK
+chmod +x "${STUB_DIR}/git"
+```
+
+The mock sits earlier on `$PATH` than the real `git`, so the script under test calls the mock
+transparently.
+
+## Troubleshooting
+
+### `bats: command not found`
+
+Bats is not installed. See [Installing Bats](#installing-bats) above.
+
+### Tests fail with `permission denied`
+
+The scripts under `.github/scripts/` must be executable:
+
+```bash
+chmod +x .github/scripts/*.sh
+```
+
+### A test fails unexpectedly
+
+Run with `set -x` tracing to see each command:
+
+```bash
+bats --tap .github/scripts/tests/cherry-pick-to-release.bats \
+ --filter "name of failing test" 2>&1
+```
+
+Or add `echo "DEBUG: $variable" >&3` inside a test to print to the terminal (file descriptor 3 is
+bats's "direct to terminal" channel).
diff --git a/.github/scripts/tests/check-milestone-branch.bats b/.github/scripts/tests/check-milestone-branch.bats
new file mode 100644
index 0000000000..813605a012
--- /dev/null
+++ b/.github/scripts/tests/check-milestone-branch.bats
@@ -0,0 +1,317 @@
+#!/usr/bin/env bats
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# Tests for check-milestone-branch.sh
+#
+# Run with: bats .github/scripts/tests/check-milestone-branch.bats
+#
+# Dependencies: bats-core (https://github.com/bats-core/bats-core)
+#
+#################################################################################
+
+# Path to the script under test (relative to repo root).
+SCRIPT=".github/scripts/check-milestone-branch.sh"
+
+# ── Helpers ──────────────────────────────────────────────────────────────────
+
+setup() {
+ STUB_DIR="$(mktemp -d)"
+ export PATH="${STUB_DIR}:${PATH}"
+
+ # Defaults — individual tests override as needed.
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="main"
+ export DEFAULT_BRANCH="main"
+ export GITHUB_REPOSITORY="dotnet/SqlClient"
+ export GH_TOKEN="fake-token"
+ export MOCK_MILESTONES=$'1.0.0\n2.0.1\n7.1.0\n8.0.0-preview1\n8.0.0-preview2\n8.0.0'
+
+ mock_release_branches "release/6.1" "release/7.0"
+}
+
+teardown() {
+ rm -rf "${STUB_DIR}"
+}
+
+# Install a 'gh' mock that reports the given release branches.
+mock_release_branches() {
+ local refs=""
+ local branch
+ for branch in "$@"; do
+ refs+="refs/heads/${branch}"$'\n'
+ done
+
+ cat > "${STUB_DIR}/gh" <> "${STUB_DIR}/gh.log"
+if [[ "\$*" == *"/milestones"* ]]; then
+ printf '%s' "\${MOCK_MILESTONES}"
+else
+ printf '%s' '${refs}'
+fi
+MOCK
+ chmod +x "${STUB_DIR}/gh"
+}
+
+# Install a 'gh' mock that fails, simulating an API error.
+mock_gh_failure() {
+ cat > "${STUB_DIR}/gh" <> "${STUB_DIR}/gh.log"
+echo "HTTP 403: rate limit exceeded" >&2
+exit 1
+MOCK
+ chmod +x "${STUB_DIR}/gh"
+}
+
+# Install a 'gh' mock that lists branches but fails when milestones are queried.
+mock_milestone_failure() {
+ cat > "${STUB_DIR}/gh" <> "${STUB_DIR}/gh.log"
+if [[ "\$*" == *"/milestones"* ]]; then
+ echo "HTTP 403: resource not accessible by integration" >&2
+ exit 1
+fi
+printf '%s' 'refs/heads/release/6.1
+refs/heads/release/7.0
+'
+MOCK
+ chmod +x "${STUB_DIR}/gh"
+}
+
+# ── --help flag ──────────────────────────────────────────────────────────────
+
+@test "prints help text with --help" {
+ run bash "${SCRIPT}" --help
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"VALIDATION MATRIX"* ]]
+ [[ "$output" == *"REQUIRED ENVIRONMENT VARIABLES"* ]]
+}
+
+@test "prints help text with -h" {
+ run bash "${SCRIPT}" -h
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"VALIDATION MATRIX"* ]]
+}
+
+# ── Input validation ─────────────────────────────────────────────────────────
+
+@test "fails when MILESTONE_TITLE is unset" {
+ unset MILESTONE_TITLE
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"MILESTONE_TITLE"* ]]
+}
+
+@test "fails when BASE_REF is unset" {
+ unset BASE_REF
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"BASE_REF"* ]]
+}
+
+@test "fails when DEFAULT_BRANCH is unset" {
+ unset DEFAULT_BRANCH
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"DEFAULT_BRANCH"* ]]
+}
+
+@test "fails when GITHUB_REPOSITORY is unset" {
+ unset GITHUB_REPOSITORY
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"GITHUB_REPOSITORY"* ]]
+}
+
+# ── Default branch targets ───────────────────────────────────────────────────
+
+@test "passes when in-development milestone targets the default branch" {
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"::notice::"* ]]
+ [[ "$output" == *"still in development"* ]]
+}
+
+@test "fails when a later milestone targets the default branch before the active release branch is cut" {
+ export MILESTONE_TITLE="8.0.0-preview1"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"7.1 milestone series"* ]]
+ [[ "$output" == *"release/7.1"* ]]
+}
+
+@test "closed milestone state does not change the active development line" {
+ export MILESTONE_TITLE="8.0.0-preview1"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"7.1 milestone series"* ]]
+ [[ "$output" != *"1.0.0"* ]]
+ grep -qF "milestones?state=all" "${STUB_DIR}/gh.log"
+}
+
+@test "fails when an earlier milestone series targets the default branch" {
+ export MILESTONE_TITLE="1.0.0"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"no longer in development"* ]]
+ [[ "$output" == *"active line is 7.1"* ]]
+}
+
+@test "fails when no configured milestone series is active" {
+ mock_release_branches "release/7.0"
+ export MOCK_MILESTONES=$'1.0.0'
+ export MILESTONE_TITLE="1.0.0"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"no development line is active"* ]]
+}
+
+@test "passes when a later milestone targets the default branch after the active release branch is cut" {
+ mock_release_branches "release/6.1" "release/7.0" "release/7.1"
+ export MILESTONE_TITLE="8.0.0-preview1"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"still in development"* ]]
+}
+
+@test "fails when a serviced milestone targets the default branch" {
+ export MILESTONE_TITLE="7.0.3"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"::error::"* ]]
+ [[ "$output" == *"release/7.0"* ]]
+ [[ "$output" == *"Hotfix 7.0.3"* ]]
+}
+
+@test "honours a non-'main' default branch" {
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="master"
+ export DEFAULT_BRANCH="master"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"targeting 'master' is correct"* ]]
+}
+
+# ── API invocation ───────────────────────────────────────────────────────────
+
+@test "queries the release refs endpoint with the expected arguments" {
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ grep -qF "GH: api repos/dotnet/SqlClient/git/matching-refs/heads/release/ --jq .[].ref" "${STUB_DIR}/gh.log"
+ grep -qF "GH: api --paginate repos/dotnet/SqlClient/milestones?state=all&per_page=100 --jq .[].title" "${STUB_DIR}/gh.log"
+}
+
+@test "does not call the API when the PR targets a release branch" {
+ export MILESTONE_TITLE="7.0.3"
+ export BASE_REF="release/7.0"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [ ! -f "${STUB_DIR}/gh.log" ]
+}
+
+# ── Release branch targets ───────────────────────────────────────────────────
+
+@test "passes when the milestone matches the target release branch" {
+ export MILESTONE_TITLE="7.0.3"
+ export BASE_REF="release/7.0"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"matches target branch 'release/7.0'"* ]]
+}
+
+@test "fails when the milestone belongs to a different release branch" {
+ export MILESTONE_TITLE="7.0.3"
+ export BASE_REF="release/6.1"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"::error::"* ]]
+ [[ "$output" == *"belongs to 'release/7.0'"* ]]
+}
+
+@test "fails when an in-development milestone targets a release branch" {
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="release/7.0"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"belongs to 'release/7.1'"* ]]
+}
+
+@test "passes when a newly cut release branch matches the milestone" {
+ mock_release_branches "release/6.1" "release/7.0" "release/7.1"
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="release/7.1"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"matches target branch 'release/7.1'"* ]]
+}
+
+@test "fails on the default branch once the release branch is cut" {
+ mock_release_branches "release/6.1" "release/7.0" "release/7.1"
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"release/7.1"* ]]
+}
+
+# ── Skipped cases ────────────────────────────────────────────────────────────
+
+@test "skips integration branch targets" {
+ export MILESTONE_TITLE="7.0.3"
+ export BASE_REF="dev/paul/some-feature"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"integration branch"* ]]
+}
+
+@test "skips milestones that are not major.minor.patch" {
+ export MILESTONE_TITLE="vNext"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"not in 'major.minor.patch' form"* ]]
+}
+
+@test "skips two-part milestone titles" {
+ export MILESTONE_TITLE="1.0 Hotfix 2"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"not in 'major.minor.patch' form"* ]]
+}
+
+# ── API failures ─────────────────────────────────────────────────────────────
+
+@test "fails when the branch listing API call fails" {
+ mock_gh_failure
+ export MILESTONE_TITLE="7.0.3"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Unable to list release branches"* ]]
+}
+
+@test "fails when the milestone listing API call fails" {
+ mock_milestone_failure
+ export MILESTONE_TITLE="7.1.0"
+ export BASE_REF="main"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Unable to list milestones"* ]]
+}
diff --git a/.github/scripts/tests/cherry-pick-to-release.bats b/.github/scripts/tests/cherry-pick-to-release.bats
new file mode 100644
index 0000000000..3484b48ea9
--- /dev/null
+++ b/.github/scripts/tests/cherry-pick-to-release.bats
@@ -0,0 +1,347 @@
+#!/usr/bin/env bats
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# Tests for cherry-pick-to-release.sh
+#
+# Run with: bats .github/scripts/tests/cherry-pick-to-release.bats
+#
+# NOTE: These tests mock git and gh commands to validate the script's logic
+# without requiring a real repository or GitHub API access.
+#
+# Dependencies: bats-core (https://github.com/bats-core/bats-core)
+#
+#################################################################################
+
+# Path to the script under test (relative to repo root).
+SCRIPT=".github/scripts/cherry-pick-to-release.sh"
+
+# ── Helpers ──────────────────────────────────────────────────────────────────
+
+setup() {
+ # Create a directory for mock binaries that override real git/gh.
+ STUB_DIR="$(mktemp -d)"
+ export PATH="${STUB_DIR}:${PATH}"
+
+ # Defaults — individual tests override as needed.
+ export VERSION="7.0.1"
+ export MERGE_COMMIT_SHA="abc123def456"
+ export PR_NUMBER="42"
+ export PR_TITLE="Fix connection timeout"
+ export GH_TOKEN="fake-token"
+ export GITHUB_REPOSITORY="dotnet/SqlClient"
+ export GITHUB_OUTPUT="$(mktemp)"
+}
+
+teardown() {
+ rm -rf "${STUB_DIR}"
+ rm -f "${GITHUB_OUTPUT}"
+}
+
+# Write a mock 'git' script. Each call to the mock appends a log line so
+# tests can verify which git subcommands were executed and with what args.
+write_git_mock() {
+ local body="$1"
+ cat > "${STUB_DIR}/git" <> "${STUB_DIR}/git.log"
+${body}
+STUB
+ chmod +x "${STUB_DIR}/git"
+}
+
+# Write a mock 'gh' script.
+write_gh_mock() {
+ local body="$1"
+ cat > "${STUB_DIR}/gh" <> "${STUB_DIR}/gh.log"
+${body}
+STUB
+ chmod +x "${STUB_DIR}/gh"
+}
+
+# ── --help flag ──────────────────────────────────────────────────────────────
+
+@test "prints help text with --help" {
+ run bash "${SCRIPT}" --help
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"Cherry-picks a merge commit"* ]]
+ [[ "$output" == *"REQUIRED ENVIRONMENT VARIABLES"* ]]
+}
+
+@test "prints help text with -h" {
+ run bash "${SCRIPT}" -h
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"Cherry-picks"* ]]
+}
+
+# ── Input validation ─────────────────────────────────────────────────────────
+
+@test "fails when VERSION is unset" {
+ unset VERSION
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"VERSION"* ]]
+}
+
+@test "fails when MERGE_COMMIT_SHA is unset" {
+ unset MERGE_COMMIT_SHA
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"MERGE_COMMIT_SHA"* ]]
+}
+
+@test "fails when PR_NUMBER is unset" {
+ unset PR_NUMBER
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"PR_NUMBER"* ]]
+}
+
+@test "fails when PR_TITLE is unset" {
+ unset PR_TITLE
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"PR_TITLE"* ]]
+}
+
+@test "fails when GITHUB_REPOSITORY is unset" {
+ unset GITHUB_REPOSITORY
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"GITHUB_REPOSITORY"* ]]
+}
+
+# ── Version parsing ─────────────────────────────────────────────────────────
+
+@test "derives release/7.0 from version 7.0.1" {
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi
+ if [[ "$1" == "checkout" ]]; then exit 0; fi
+ if [[ "$1" == "rev-list" ]]; then echo "abc123 parent1"; exit 0; fi
+ if [[ "$1" == "cherry-pick" ]]; then exit 0; fi
+ if [[ "$1" == "push" ]]; then exit 0; fi
+ exit 0
+ '
+ write_gh_mock '
+ if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi
+ if [[ "$1" == "pr" ]]; then exit 0; fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ # Verify the git fetch targeted release/7.0.
+ grep -q "GIT: fetch origin release/7.0" "${STUB_DIR}/git.log"
+ # Milestone lookup must use GET to avoid accidentally POSTing to the create endpoint.
+ grep "GH: api repos/dotnet/SqlClient/milestones" "${STUB_DIR}/gh.log" | grep -q "\-\-method GET"
+}
+
+@test "derives release/8.0 from version 8.0.0" {
+ export VERSION="8.0.0"
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi
+ if [[ "$1" == "checkout" ]]; then exit 0; fi
+ if [[ "$1" == "rev-list" ]]; then echo "abc123 parent1"; exit 0; fi
+ if [[ "$1" == "cherry-pick" ]]; then exit 0; fi
+ if [[ "$1" == "push" ]]; then exit 0; fi
+ exit 0
+ '
+ write_gh_mock '
+ if [[ "$1" == "api" ]]; then echo "8.0.0"; exit 0; fi
+ if [[ "$1" == "pr" ]]; then exit 0; fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ grep -q "GIT: fetch origin release/8.0" "${STUB_DIR}/git.log"
+}
+
+@test "fails on unparseable version" {
+ export VERSION="bad"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Could not parse"* ]]
+}
+
+# ── Already-applied detection ───────────────────────────────────────────────
+
+@test "exits cleanly when commit is already applied" {
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ # git cherry: "-" prefix means patch is already applied.
+ if [[ "$1" == "cherry" ]]; then echo "- abc123def456"; exit 0; fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"already applied"* ]]
+ # Should NOT have attempted a cherry-pick.
+ ! grep -q "GIT: cherry-pick" "${STUB_DIR}/git.log"
+}
+
+# ── Squash-merge detection (single parent) ──────────────────────────────────
+
+@test "does not use --mainline for squash merges" {
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi
+ if [[ "$1" == "checkout" ]]; then exit 0; fi
+ # Single parent: rev-list outputs "sha parent1" (2 fields → 1 parent).
+ if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi
+ if [[ "$1" == "cherry-pick" ]]; then exit 0; fi
+ if [[ "$1" == "push" ]]; then exit 0; fi
+ exit 0
+ '
+ write_gh_mock '
+ if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi
+ if [[ "$1" == "pr" ]]; then exit 0; fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"single parent"* ]]
+ # cherry-pick should NOT include --mainline.
+ grep "GIT: cherry-pick" "${STUB_DIR}/git.log" | grep -qv "\-\-mainline"
+}
+
+# ── True merge detection (multiple parents) ─────────────────────────────────
+
+@test "uses --mainline 1 for true merge commits" {
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi
+ if [[ "$1" == "checkout" ]]; then exit 0; fi
+ # Two parents: rev-list outputs "sha parent1 parent2" (3 fields → 2 parents).
+ if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1 parent2"; exit 0; fi
+ if [[ "$1" == "cherry-pick" ]]; then exit 0; fi
+ if [[ "$1" == "push" ]]; then exit 0; fi
+ exit 0
+ '
+ write_gh_mock '
+ if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi
+ if [[ "$1" == "pr" ]]; then exit 0; fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"--mainline 1"* ]]
+}
+
+# ── Milestone lookup ────────────────────────────────────────────────────────
+
+@test "warns when milestone does not exist" {
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi
+ if [[ "$1" == "checkout" ]]; then exit 0; fi
+ if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi
+ if [[ "$1" == "cherry-pick" ]]; then exit 0; fi
+ if [[ "$1" == "push" ]]; then exit 0; fi
+ if [[ "$1" == "config" ]]; then exit 0; fi
+ exit 0
+ '
+ # gh api returns a milestone that does NOT match VERSION (7.0.1).
+ write_gh_mock '
+ if [[ "$1" == "api" ]]; then echo "6.0.0"; exit 0; fi
+ if [[ "$1" == "pr" ]]; then exit 0; fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"does not exist"* ]]
+ # Milestone lookup must use GET to avoid accidentally POSTing to the create endpoint.
+ grep "GH: api repos/dotnet/SqlClient/milestones" "${STUB_DIR}/gh.log" | grep -q "\-\-method GET"
+}
+
+# ── Conflict handling ───────────────────────────────────────────────────────
+
+@test "creates CONFLICTS PR when cherry-pick fails" {
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi
+ if [[ "$1" == "checkout" ]]; then exit 0; fi
+ if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi
+ # cherry-pick fails with conflicts.
+ if [[ "$1" == "cherry-pick" ]]; then
+ if [[ "$2" == "--abort" ]]; then exit 0; fi
+ exit 1
+ fi
+ if [[ "$1" == "commit" ]]; then exit 0; fi
+ if [[ "$1" == "push" ]]; then exit 0; fi
+ exit 0
+ '
+ write_gh_mock '
+ if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi
+ if [[ "$1" == "pr" ]]; then exit 0; fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"failed due to conflicts"* ]]
+ # Should have called cherry-pick --abort.
+ grep -q "GIT: cherry-pick --abort" "${STUB_DIR}/git.log"
+ # Should have created an empty commit.
+ grep -q "GIT: commit --allow-empty" "${STUB_DIR}/git.log"
+}
+
+@test "conflict PR body contains real newlines, not literal backslash-n" {
+ write_git_mock '
+ if [[ "$1" == "fetch" ]]; then exit 0; fi
+ if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi
+ if [[ "$1" == "checkout" ]]; then exit 0; fi
+ if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi
+ if [[ "$1" == "cherry-pick" ]]; then
+ if [[ "$2" == "--abort" ]]; then exit 0; fi
+ exit 1
+ fi
+ if [[ "$1" == "commit" ]]; then exit 0; fi
+ if [[ "$1" == "push" ]]; then exit 0; fi
+ exit 0
+ '
+ # Capture the full --body argument to a file for inspection.
+ write_gh_mock '
+ if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi
+ if [[ "$1" == "pr" && "$2" == "create" ]]; then
+ while [[ $# -gt 0 ]]; do
+ if [[ "$1" == "--body" ]]; then
+ printf "%s" "$2" > "'"${STUB_DIR}"'/pr-body.txt"
+ break
+ fi
+ shift
+ done
+ exit 0
+ fi
+ exit 0
+ '
+
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+
+ # The body file must exist (gh pr create was called with --body).
+ [ -f "${STUB_DIR}/pr-body.txt" ]
+
+ local body
+ body="$(cat "${STUB_DIR}/pr-body.txt")"
+
+ # Must NOT contain literal two-character sequence '\n'.
+ [[ "$body" != *'\\n'* ]]
+ # Each command in the code block must be on its own line.
+ [[ "$body" == *$'\ngit fetch origin\n'* ]]
+ [[ "$body" == *$'\ngit checkout dev/automation/pr-42-to-7.0.1\n'* ]]
+ [[ "$body" == *$'\ngit cherry-pick abc123def456\n'* ]]
+ [[ "$body" == *$'\n# resolve conflicts\n'* ]]
+ [[ "$body" == *$'\ngit push origin dev/automation/pr-42-to-7.0.1 --force\n'* ]]
+}
diff --git a/.github/scripts/tests/extract-hotfix-versions.bats b/.github/scripts/tests/extract-hotfix-versions.bats
new file mode 100644
index 0000000000..e0767ffd04
--- /dev/null
+++ b/.github/scripts/tests/extract-hotfix-versions.bats
@@ -0,0 +1,244 @@
+#!/usr/bin/env bats
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# Tests for extract-hotfix-versions.sh
+#
+# Run with: bats .github/scripts/tests/extract-hotfix-versions.bats
+#
+# Dependencies: bats-core (https://github.com/bats-core/bats-core)
+#
+#################################################################################
+
+# Path to the script under test (relative to repo root).
+SCRIPT=".github/scripts/extract-hotfix-versions.sh"
+
+# ── Helpers ──────────────────────────────────────────────────────────────────
+
+setup() {
+ # Create a temporary GITHUB_OUTPUT file for each test.
+ export GITHUB_OUTPUT
+ GITHUB_OUTPUT="$(mktemp)"
+
+ # Defaults — individual tests override as needed.
+ export EVENT_ACTION="closed"
+ export EVENT_LABEL=""
+ export PR_NUMBER="42"
+ export GH_TOKEN="fake-token"
+ export GITHUB_REPOSITORY="dotnet/SqlClient"
+}
+
+teardown() {
+ rm -f "${GITHUB_OUTPUT}"
+}
+
+# Read the 'versions' output written to GITHUB_OUTPUT.
+get_versions() {
+ grep '^versions=' "${GITHUB_OUTPUT}" | head -1 | cut -d= -f2-
+}
+
+# ── --help flag ──────────────────────────────────────────────────────────────
+
+@test "prints help text with --help" {
+ run bash "${SCRIPT}" --help
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"Parses"* ]]
+ [[ "$output" == *"REQUIRED ENVIRONMENT VARIABLES"* ]]
+}
+
+@test "prints help text with -h" {
+ run bash "${SCRIPT}" -h
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"Parses"* ]]
+}
+
+# ── Input validation ─────────────────────────────────────────────────────────
+
+@test "fails when LABELS is unset" {
+ unset LABELS
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"LABELS"* ]]
+}
+
+@test "fails when EVENT_ACTION is unset" {
+ export LABELS="Hotfix 7.0.1"
+ unset EVENT_ACTION
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"EVENT_ACTION"* ]]
+}
+
+@test "fails when PR_NUMBER is unset" {
+ export LABELS="Hotfix 7.0.1"
+ unset PR_NUMBER
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"PR_NUMBER"* ]]
+}
+
+# ── Closed event: single label ──────────────────────────────────────────────
+
+@test "closed event: extracts single Hotfix label" {
+ export LABELS="Hotfix 7.0.1"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '["7.0.1"]' ]
+}
+
+@test "closed event: ignores non-hotfix labels" {
+ export LABELS="bug,Hotfix 7.0.1,enhancement"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '["7.0.1"]' ]
+}
+
+# ── Closed event: multiple labels ───────────────────────────────────────────
+
+@test "closed event: extracts multiple Hotfix labels" {
+ export LABELS="Hotfix 7.0.1,Hotfix 8.0.0"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '["7.0.1","8.0.0"]' ]
+}
+
+@test "closed event: extracts hotfix labels mixed with other labels" {
+ export LABELS="bug,Hotfix 7.0.1,enhancement,Hotfix 8.0.0,docs"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '["7.0.1","8.0.0"]' ]
+}
+
+# ── Closed event: malformed labels ──────────────────────────────────────────
+
+@test "closed event: rejects malformed Hotfix labels (no patch)" {
+ export LABELS="Hotfix 7.0"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"No valid"* ]]
+}
+
+@test "closed event: rejects Hotfix label with text suffix" {
+ export LABELS="Hotfix 7.0.1-beta"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"No valid"* ]]
+}
+
+@test "closed event: rejects Hotfix label with non-numeric version" {
+ export LABELS="Hotfix abc"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"No valid"* ]]
+}
+
+@test "closed event: fails when no labels present" {
+ export LABELS=""
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+}
+
+# ── Labeled event: basic behavior ───────────────────────────────────────────
+
+@test "labeled event: processes valid newly added label" {
+ export EVENT_ACTION="labeled"
+ export EVENT_LABEL="Hotfix 7.0.1"
+ export LABELS="Hotfix 7.0.1,Hotfix 8.0.0"
+
+ # Mock gh to report no existing branch or PR.
+ # The script now uses 'gh api' for branch checks (no git checkout in this job).
+ local stub_dir
+ stub_dir="$(mktemp -d)"
+ cat > "${stub_dir}/gh" <<'STUB'
+#!/usr/bin/env bash
+# gh api repos/.../git/ref/heads/...: exit 1 (branch not found)
+# gh pr list: return "0" PRs
+if [[ "$1" == "api" && "$2" == repos/*/git/ref/heads/* ]]; then
+ exit 1
+fi
+echo "0"
+STUB
+ chmod +x "${stub_dir}/gh"
+
+ export PATH="${stub_dir}:${PATH}"
+ run bash "${SCRIPT}"
+ rm -rf "${stub_dir}"
+
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '["7.0.1"]' ]
+}
+
+@test "labeled event: skips non-hotfix label" {
+ export EVENT_ACTION="labeled"
+ export EVENT_LABEL="bug"
+ export LABELS="bug,Hotfix 7.0.1"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '[]' ]
+}
+
+@test "labeled event: skips malformed hotfix label" {
+ export EVENT_ACTION="labeled"
+ export EVENT_LABEL="Hotfix 7.0"
+ export LABELS="Hotfix 7.0"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '[]' ]
+}
+
+# ── Labeled event: duplicate detection ──────────────────────────────────────
+
+@test "labeled event: skips when cherry-pick branch already exists" {
+ export EVENT_ACTION="labeled"
+ export EVENT_LABEL="Hotfix 7.0.1"
+ export LABELS="Hotfix 7.0.1"
+
+ local stub_dir
+ stub_dir="$(mktemp -d)"
+ # gh api returns success — branch exists on the remote.
+ cat > "${stub_dir}/gh" <<'STUB'
+#!/usr/bin/env bash
+if [[ "$1" == "api" && "$2" == repos/*/git/ref/heads/* ]]; then
+ exit 0
+fi
+echo "0"
+STUB
+ chmod +x "${stub_dir}/gh"
+ export PATH="${stub_dir}:${PATH}"
+
+ run bash "${SCRIPT}"
+ rm -rf "${stub_dir}"
+
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '[]' ]
+ [[ "$output" == *"already exists"* ]]
+}
+
+@test "labeled event: skips when cherry-pick PR already exists" {
+ export EVENT_ACTION="labeled"
+ export EVENT_LABEL="Hotfix 7.0.1"
+ export LABELS="Hotfix 7.0.1"
+
+ local stub_dir
+ stub_dir="$(mktemp -d)"
+ # gh api for branch check returns 1 (not found), but pr list returns 1 PR.
+ cat > "${stub_dir}/gh" <<'STUB'
+#!/usr/bin/env bash
+if [[ "$1" == "api" && "$2" == repos/*/git/ref/heads/* ]]; then
+ exit 1
+fi
+echo "1"
+STUB
+ chmod +x "${stub_dir}/gh"
+ export PATH="${stub_dir}:${PATH}"
+
+ run bash "${SCRIPT}"
+ rm -rf "${stub_dir}"
+
+ [ "$status" -eq 0 ]
+ [ "$(get_versions)" = '[]' ]
+ [[ "$output" == *"already exists"* ]]
+}
diff --git a/.github/scripts/tests/recheck-milestones-for-release-branch.bats b/.github/scripts/tests/recheck-milestones-for-release-branch.bats
new file mode 100644
index 0000000000..0d48388f33
--- /dev/null
+++ b/.github/scripts/tests/recheck-milestones-for-release-branch.bats
@@ -0,0 +1,228 @@
+#!/usr/bin/env bats
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# Tests for recheck-milestones-for-release-branch.sh
+#
+# Run with: bats .github/scripts/tests/recheck-milestones-for-release-branch.bats
+#
+# Dependencies: bats-core (https://github.com/bats-core/bats-core)
+#
+#################################################################################
+
+# Path to the script under test (relative to repo root).
+SCRIPT=".github/scripts/recheck-milestones-for-release-branch.sh"
+
+# ── Helpers ──────────────────────────────────────────────────────────────────
+
+setup() {
+ STUB_DIR="$(mktemp -d)"
+ export PATH="${STUB_DIR}:${PATH}"
+
+ # Defaults — individual tests override as needed.
+ export RELEASE_BRANCH="release/7.1"
+ export DEFAULT_BRANCH="main"
+ export WORKFLOW_FILE="check-milestone.yml"
+ export GITHUB_REPOSITORY="dotnet/SqlClient"
+ export GH_TOKEN="fake-token"
+
+ # Open PRs as "", one per line.
+ mock_gh "100 aaa111 7.1.0
+101 bbb222 8.0.0-preview1
+102 ccc333 7.1.0-preview3"
+
+ # Default: one run per head SHA, correctly associated with its PR.
+ set_runs aaa111 '{"workflow_runs":[{"id":"run-aaa111","pull_requests":[{"number":100}]}]}'
+ set_runs bbb222 '{"workflow_runs":[{"id":"run-bbb222","pull_requests":[{"number":101}]}]}'
+ set_runs ccc333 '{"workflow_runs":[{"id":"run-ccc333","pull_requests":[{"number":102}]}]}'
+}
+
+teardown() {
+ rm -rf "${STUB_DIR}"
+}
+
+# Register the workflow-runs response for a given head SHA.
+set_runs() {
+ printf '%s' "$2" > "${STUB_DIR}/runs-$1.json"
+}
+
+# Install a 'gh' mock. $1 is the 'pr list' output; 'api' serves the JSON
+# registered by set_runs, and 'run rerun' succeeds unless RERUN_FAILS is set.
+mock_gh() {
+ cat > "${STUB_DIR}/gh" <> "${STUB_DIR}/gh.log"
+case "\$1" in
+ pr)
+ printf '%s\n' '${1}'
+ ;;
+ api)
+ sha="\$(sed -n 's/.*head_sha=\([^&]*\).*/\1/p' <<< "\$2")"
+ if [[ -n "\${NO_RUN_FOUND:-}" || ! -f "${STUB_DIR}/runs-\${sha}.json" ]]; then
+ echo '{"workflow_runs":[]}'
+ else
+ cat "${STUB_DIR}/runs-\${sha}.json"
+ fi
+ ;;
+ run)
+ [[ -z "\${RERUN_FAILS:-}" ]] || exit 1
+ ;;
+esac
+MOCK
+ chmod +x "${STUB_DIR}/gh"
+}
+
+# Install a 'gh' mock whose 'pr list' call fails.
+mock_pr_list_failure() {
+ cat > "${STUB_DIR}/gh" <<'MOCK'
+#!/usr/bin/env bash
+echo "HTTP 403: rate limit exceeded" >&2
+exit 1
+MOCK
+ chmod +x "${STUB_DIR}/gh"
+}
+
+# ── --help flag ──────────────────────────────────────────────────────────────
+
+@test "prints help text with --help" {
+ run bash "${SCRIPT}" --help
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"REQUIRED ENVIRONMENT VARIABLES"* ]]
+}
+
+# ── Input validation ─────────────────────────────────────────────────────────
+
+@test "fails when RELEASE_BRANCH is unset" {
+ unset RELEASE_BRANCH
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"RELEASE_BRANCH"* ]]
+}
+
+@test "fails when DEFAULT_BRANCH is unset" {
+ unset DEFAULT_BRANCH
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"DEFAULT_BRANCH"* ]]
+}
+
+@test "fails when WORKFLOW_FILE is unset" {
+ unset WORKFLOW_FILE
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"WORKFLOW_FILE"* ]]
+}
+
+@test "fails when GITHUB_REPOSITORY is unset" {
+ unset GITHUB_REPOSITORY
+ run bash "${SCRIPT}"
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"GITHUB_REPOSITORY"* ]]
+}
+
+# ── Branch name parsing ──────────────────────────────────────────────────────
+
+@test "skips branches that are not release/." {
+ export RELEASE_BRANCH="dev/paul/some-feature"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"nothing to reconcile"* ]]
+ [ ! -f "${STUB_DIR}/gh.log" ]
+}
+
+@test "skips a release branch with a patch component" {
+ export RELEASE_BRANCH="release/7.1.0"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"nothing to reconcile"* ]]
+}
+
+# ── Matching and re-running ──────────────────────────────────────────────────
+
+@test "re-runs all semver-milestoned PRs affected by active-line transitions" {
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"PR #100"* ]]
+ [[ "$output" == *"PR #102"* ]]
+ [[ "$output" == *"PR #101"* ]]
+ [[ "$output" == *"Re-checked 3 pull request(s)"* ]]
+}
+
+@test "queries open PRs against the default branch" {
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ grep -qF "GH: pr list --repo dotnet/SqlClient --base main --state open" "${STUB_DIR}/gh.log"
+}
+
+@test "looks up the run by the PR head sha and re-runs it" {
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ grep -qF "head_sha=aaa111" "${STUB_DIR}/gh.log"
+ grep -qF "GH: run rerun run-aaa111 --repo dotnet/SqlClient" "${STUB_DIR}/gh.log"
+}
+
+@test "picks the run belonging to this PR when a head sha backs several PRs" {
+ # The newest run for the SHA belongs to a different PR against another base.
+ set_runs aaa111 '{"workflow_runs":[
+ {"id":"run-other","pull_requests":[{"number":999}]},
+ {"id":"run-aaa111","pull_requests":[{"number":100}]}
+ ]}'
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ grep -qF "GH: run rerun run-aaa111 --repo dotnet/SqlClient" "${STUB_DIR}/gh.log"
+ ! grep -qF "run rerun run-other" "${STUB_DIR}/gh.log"
+}
+
+@test "falls back to the newest run when the PR association is missing" {
+ # Runs from forked repositories carry an empty 'pull_requests' array.
+ set_runs aaa111 '{"workflow_runs":[
+ {"id":"run-newest","pull_requests":[]},
+ {"id":"run-older","pull_requests":[]}
+ ]}'
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"PR #100"* ]]
+ grep -qF "GH: run rerun run-newest --repo dotnet/SqlClient" "${STUB_DIR}/gh.log"
+}
+
+@test "reports when no open PR carries a matching milestone" {
+ export RELEASE_BRANCH="release/6.1"
+ mock_gh "200 ddd444 vNext"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"No open PR targeting 'main' carries a semantic-version milestone"* ]]
+}
+
+@test "ignores PRs whose milestone is not major.minor.patch" {
+ mock_gh "200 ddd444 vNext"
+ run bash "${SCRIPT}"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"No open PR"* ]]
+}
+
+# ── Failure handling ─────────────────────────────────────────────────────────
+
+@test "warns and fails when a PR has no run to re-run" {
+ export NO_RUN_FOUND=1
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"has no milestone check run to re-run"* ]]
+ [[ "$output" == *"3 of 3 affected pull requests"* ]]
+}
+
+@test "warns and fails when a re-run cannot be started" {
+ export RERUN_FAILS=1
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Could not re-run the milestone check for PR #100"* ]]
+}
+
+@test "fails when the PR listing API call fails" {
+ mock_pr_list_failure
+ run bash "${SCRIPT}"
+ [ "$status" -eq 1 ]
+ [[ "$output" == *"Unable to list open pull requests"* ]]
+}
diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md
new file mode 100644
index 0000000000..ee714d339d
--- /dev/null
+++ b/.github/skills/agentic-workflows/SKILL.md
@@ -0,0 +1,80 @@
+---
+name: agentic-workflows
+description: Route gh-aw workflow design/create/debug/upgrade requests to the right prompts.
+---
+
+# Agentic Workflows Router
+
+Use this skill when a user asks to design, create, update, debug, or upgrade GitHub Agentic Workflows in this repository.
+
+This skill is a dispatcher: identify the task type, load the matching workflow prompt/skill file, and follow it directly. Keep responses concise and ask a clarifying question if the correct prompt is unclear.
+
+Read only the files you need:
+Load these files from `github/gh-aw` (they are not available locally).
+- `.github/aw/agentic-chat.md`
+- `.github/aw/agentic-workflows-mcp.md`
+- `.github/aw/asciicharts.md`
+- `.github/aw/campaign.md`
+- `.github/aw/charts-trending.md`
+- `.github/aw/charts.md`
+- `.github/aw/cli-commands.md`
+- `.github/aw/context.md`
+- `.github/aw/create-agentic-workflow.md`
+- `.github/aw/create-shared-agentic-workflow.md`
+- `.github/aw/debug-agentic-workflow.md`
+- `.github/aw/dependabot.md`
+- `.github/aw/deployment-status.md`
+- `.github/aw/experiments.md`
+- `.github/aw/github-agentic-workflows.md`
+- `.github/aw/github-mcp-server.md`
+- `.github/aw/llms.md`
+- `.github/aw/mcp-clis.md`
+- `.github/aw/memory.md`
+- `.github/aw/messages.md`
+- `.github/aw/network.md`
+- `.github/aw/optimize-agentic-workflow.md`
+- `.github/aw/patterns.md`
+- `.github/aw/pr-reviewer.md`
+- `.github/aw/report.md`
+- `.github/aw/reuse.md`
+- `.github/aw/safe-outputs-automation.md`
+- `.github/aw/safe-outputs-content.md`
+- `.github/aw/safe-outputs-management.md`
+- `.github/aw/safe-outputs-runtime.md`
+- `.github/aw/safe-outputs.md`
+- `.github/aw/serena-tool.md`
+- `.github/aw/shared-safe-jobs.md`
+- `.github/aw/skills.md`
+- `.github/aw/subagents.md`
+- `.github/aw/syntax-agentic.md`
+- `.github/aw/syntax-core.md`
+- `.github/aw/syntax-tools-imports.md`
+- `.github/aw/syntax.md`
+- `.github/aw/test-coverage.md`
+- `.github/aw/test-expression.md`
+- `.github/aw/token-optimization.md`
+- `.github/aw/triggers.md`
+- `.github/aw/update-agentic-workflow.md`
+- `.github/aw/upgrade-agentic-workflows.md`
+- `.github/aw/visual-regression.md`
+- `.github/aw/workflow-constraints.md`
+- `.github/aw/workflow-editing.md`
+- `.github/aw/workflow-patterns.md`
+
+- `.github/skills/agentic-workflow-designer/SKILL.md`
+After loading the matching workflow prompt or skill, follow it directly:
+- Design workflows from scratch via interview: `skills/agentic-workflow-designer/SKILL.md`
+- Create new workflows: `.github/aw/create-agentic-workflow.md`
+- Update existing workflows: `.github/aw/update-agentic-workflow.md`
+- Debug, audit, or investigate workflows: `.github/aw/debug-agentic-workflow.md`
+- Upgrade workflows and fix deprecations: `.github/aw/upgrade-agentic-workflows.md`
+- Create shared components or MCP wrappers: `.github/aw/create-shared-agentic-workflow.md`
+- Create report-generating workflows: `.github/aw/report.md`
+- Fix Dependabot manifest PRs: `.github/aw/dependabot.md`
+- Analyze coverage workflows: `.github/aw/test-coverage.md`
+- Render compact markdown charts: `.github/aw/asciicharts.md`
+- Map CLI commands to MCP usage: `.github/aw/cli-commands.md`
+- Choose workflow architecture and patterns: `.github/aw/patterns.md`
+- Optimize token usage and cost: `.github/aw/token-optimization.md`
+
+When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt or skill.
diff --git a/.github/skills/generate-mstest-filter/SKILL.md b/.github/skills/generate-mstest-filter/SKILL.md
index 8441823347..3d2039259b 100644
--- a/.github/skills/generate-mstest-filter/SKILL.md
+++ b/.github/skills/generate-mstest-filter/SKILL.md
@@ -187,7 +187,7 @@ dotnet test --list-tests --filter "" --framework <
```bash
# Generate filter for "ChannelDbConnectionPoolTest" class
-dotnet test tests/UnitTests/UnitTests.csproj --list-tests --filter "FullyQualifiedName~ChannelDbConnectionPoolTest" --framework net9.0
+dotnet test src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj --list-tests --filter "FullyQualifiedName~ChannelDbConnectionPoolTest" --framework net9.0
# Expected output shows matching tests:
# The following Tests are available:
diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml
new file mode 100644
index 0000000000..aab463f2e2
--- /dev/null
+++ b/.github/workflows/auto-assign-pr.yml
@@ -0,0 +1,38 @@
+name: Auto Assign PR Load Balancer
+
+on:
+ pull_request_target:
+ types: [opened, reopened, ready_for_review, milestoned]
+
+concurrency:
+ group: auto-assign-pr-${{ github.event.pull_request.number }}
+ cancel-in-progress: true
+
+jobs:
+ load-balance-assignees:
+ # Only run for assignment-eligible PRs in the upstream repository. Use
+ # pull_request_target so the workflow only runs once per PR event.
+ # Exclude PRs from forks as GitHub Actions does not have permissions to assign users on PRs from forks.
+ if: github.repository == 'dotnet/SqlClient' && github.event.pull_request.state == 'open' && github.event.pull_request.draft == false && github.event.pull_request.milestone != null && github.event.pull_request.head.repo.fork == false
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+ env:
+ PR_REVIEWER_POOL: ${{ vars.PR_REVIEWER_POOL }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ sparse-checkout: |
+ .github/scripts/auto-assign-pr.js
+ sparse-checkout-cone-mode: false
+ persist-credentials: false
+
+ - name: Calculate Workload and Apply
+ uses: actions/github-script@v9
+ with:
+ script: |
+ const script = require('./.github/scripts/auto-assign-pr.js');
+ await script({ github, context, core });
diff --git a/.github/workflows/check-milestone.yml b/.github/workflows/check-milestone.yml
new file mode 100644
index 0000000000..8d64ae5f13
--- /dev/null
+++ b/.github/workflows/check-milestone.yml
@@ -0,0 +1,73 @@
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# Check Milestone
+#
+# Validates that every pull request has an open milestone assigned, and that
+# the milestone is consistent with the branch the PR targets.
+#
+# Milestones map to release branches by major.minor:
+#
+# * "7.0.3" -> release/7.0 exists -> the PR must target release/7.0.
+# * "7.1.0" -> release/7.1 does not exist and it is the earliest configured
+# unbranched series -> the PR must target main.
+# * "8.0.0-preview1" -> release/8.0 does not exist, but 7.1 is still the
+# active unbranched series -> the PR cannot target main yet.
+#
+# See .github/scripts/check-milestone-branch.sh for the full rule set.
+#
+# Cutting release/X.Y flips the expected target for X.Y.* milestones but emits
+# no pull request activity. recheck-milestones.yml reconciles the already open
+# PRs that the new branch invalidates.
+#
+#################################################################################
+
+name: Check Milestone
+
+on:
+ pull_request:
+ # The 'edited' type covers base branch changes, so retargeting a PR (manually,
+ # or automatically when a stacked PR's parent merges) re-runs this check.
+ types: [opened, reopened, edited, synchronize, milestoned, demilestoned]
+
+jobs:
+ check-milestone:
+ name: Validate milestone
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+ steps:
+ - name: Check milestone is set
+ if: github.event.pull_request.milestone == null
+ run: |
+ echo "::error::This PR does not have a milestone set. Please assign a milestone before merging."
+ exit 1
+
+ - name: Check milestone is open
+ if: github.event.pull_request.milestone != null && github.event.pull_request.milestone.state != 'open'
+ run: |
+ echo "::error::Milestone '${{ github.event.pull_request.milestone.title }}' is ${{ github.event.pull_request.milestone.state }}. Please assign an open milestone."
+ exit 1
+
+ - name: Checkout scripts
+ if: github.event.pull_request.milestone != null
+ uses: actions/checkout@v6
+ with:
+ # Only the scripts directory is needed; skip full history.
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+
+ - name: Check milestone matches target branch
+ if: github.event.pull_request.milestone != null
+ env:
+ # Pass PR data via env to avoid script injection from milestone text.
+ MILESTONE_TITLE: ${{ github.event.pull_request.milestone.title }}
+ BASE_REF: ${{ github.event.pull_request.base.ref }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: bash "${GITHUB_WORKSPACE}/.github/scripts/check-milestone-branch.sh"
diff --git a/.github/workflows/cherry-pick-hotfix.yml b/.github/workflows/cherry-pick-hotfix.yml
new file mode 100644
index 0000000000..678a6b20b3
--- /dev/null
+++ b/.github/workflows/cherry-pick-hotfix.yml
@@ -0,0 +1,113 @@
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# Cherry-pick Hotfix to Release Branch
+#
+# Automatically cherry-picks merged PRs into release branches when a
+# "Hotfix " label is present. Supports multiple hotfix labels on
+# a single PR — each one produces an independent cherry-pick PR targeting
+# the corresponding release/ branch.
+#
+# Usage:
+# 1. Merge a PR to the default branch.
+# 2. Add a "Hotfix " label (e.g. "Hotfix 7.0.1") either before
+# or after merging.
+# 3. The workflow derives the release branch from the label's major.minor
+# version (e.g. "Hotfix 7.0.1" → release/7.0) and creates a cherry-pick
+# PR prefixed with "[ Cherry-pick]".
+# 4. If the cherry-pick has conflicts, a placeholder PR is opened with
+# "[ Cherry-pick - CONFLICTS]" and manual resolution steps.
+#
+#################################################################################
+
+name: Cherry-pick Hotfix to release branch
+
+# Triggers:
+# - 'closed': fires at merge time — if a "Hotfix " label is already present,
+# the cherry-pick runs immediately.
+# - 'labeled': fires when a label is added after merge — allows retroactive cherry-picks
+# by adding the label to an already-merged PR.
+on:
+ pull_request_target:
+ types: [closed, labeled]
+
+# 'contents: write' is needed to push the cherry-pick branch.
+# 'pull-requests: write' is needed to create the new PR via the GitHub CLI.
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ # First job: extract all hotfix versions from the PR labels and emit them as
+ # a JSON array so the matrix strategy can fan out one job per version.
+ detect-versions:
+ runs-on: ubuntu-latest
+ # Only fire for merged PRs targeting the default branch that have at least
+ # one "Hotfix *" label. The default-branch guard prevents recursive
+ # cherry-picks when a cherry-pick PR is merged into a release branch.
+ if: >-
+ github.event.pull_request.merged == true &&
+ github.event.pull_request.base.ref == github.event.repository.default_branch &&
+ join(github.event.pull_request.labels.*.name, ' ') != '' &&
+ contains(join(github.event.pull_request.labels.*.name, ','), 'Hotfix ')
+ outputs:
+ versions: ${{ steps.extract.outputs.versions }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ # Only the scripts directory is needed; skip full history.
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+
+ - name: Extract hotfix versions from labels
+ id: extract
+ env:
+ # Pass label names via env to avoid script injection from label text.
+ LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
+ # For the 'labeled' event, this is the single label that was just added.
+ # For the 'closed' event this is empty, meaning all labels are processed.
+ EVENT_LABEL: ${{ github.event.label.name || '' }}
+ EVENT_ACTION: ${{ github.event.action }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: bash "${GITHUB_WORKSPACE}/.github/scripts/extract-hotfix-versions.sh"
+
+ # Second job: runs once per detected version, cherry-picking the merge commit
+ # into each target release branch.
+ cherry-pick:
+ needs: detect-versions
+ if: needs.detect-versions.outputs.versions != '[]'
+ runs-on: ubuntu-latest
+ strategy:
+ # Don't cancel other cherry-picks if one version fails.
+ fail-fast: false
+ matrix:
+ version: ${{ fromJson(needs.detect-versions.outputs.versions) }}
+ name: Cherry-pick to release branch (${{ matrix.version }})
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ # Full history is required so the merge commit and target branch are available
+ # for the cherry-pick operation.
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Configure git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+ - name: Cherry-pick and create PR
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ VERSION: ${{ matrix.version }}
+ run: bash "${GITHUB_WORKSPACE}/.github/scripts/cherry-pick-to-release.sh"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 64584746df..dc237459ee 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -23,6 +23,7 @@ on:
- main
- feat/**
- dev/**
+ - release/**
# Scan weekly on Saturdays at 23:33 UTC
schedule:
@@ -65,12 +66,16 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup .NET Core SDK
- uses: actions/setup-dotnet@v5.0.1
+ uses: actions/setup-dotnet@v5.2.0
with:
global-json-file: global.json
+
+ - name: Restore dotnet tools
+ shell: bash
+ run: dotnet tool restore
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
@@ -94,7 +99,7 @@ jobs:
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
- run: dotnet build src/Microsoft.Data.SqlClient.sln
+ run: dotnet build build.proj -t:BuildAll
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
new file mode 100644
index 0000000000..90ca026d37
--- /dev/null
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -0,0 +1,26 @@
+name: "Copilot Setup Steps"
+
+# This workflow configures the environment for GitHub Copilot Agent with gh-aw MCP server
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - .github/workflows/copilot-setup-steps.yml
+
+jobs:
+ # The job MUST be called 'copilot-setup-steps' to be recognized by GitHub Copilot Agent
+ copilot-setup-steps:
+ runs-on: ubuntu-latest
+
+ # Set minimal permissions for setup steps
+ # Copilot Agent receives its own token with appropriate permissions
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ - name: Install gh-aw extension
+ uses: github/gh-aw-actions/setup-cli@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9
+ with:
+ version: v0.80.9
diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml
new file mode 100644
index 0000000000..d1ef3b2e67
--- /dev/null
+++ b/.github/workflows/issue-triage.lock.yml
@@ -0,0 +1,1851 @@
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"8b81a4840372af279438b0250d6ae7168fa2e0a2f82ae8cd52d714f18a83d425","body_hash":"99b21e9ba167d3bfbcfaeb3b04c515fc42b56679f0ee175c119bc5843f034d31","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","agent_model":"auto","engine_versions":{"copilot":"1.0.80"}}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop","remove_labels"]}]}
+# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
+#
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+#
+# To update this file, edit the corresponding .md file and run:
+# gh aw compile
+# Not all edits will cause changes to this file.
+#
+# For more information: https://github.github.com/gh-aw/introduction/overview/
+#
+#
+# Secrets used:
+# - COPILOT_GITHUB_TOKEN
+# - GH_AW_DEFAULT_OTLP_HEADERS
+# - GH_AW_GITHUB_MCP_SERVER_TOKEN
+# - GH_AW_GITHUB_TOKEN
+# - GITHUB_TOKEN
+#
+# Custom actions used:
+# - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+# - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
+# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2
+#
+# Container images used:
+# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202
+# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32
+# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f
+# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e
+# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e
+# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699
+
+name: "SqlClient Issue Auto-Triage"
+on:
+ issue_comment:
+ types:
+ - created
+ issues:
+ types:
+ - opened
+# roles: all # Roles processed as role check in pre-activation job
+
+permissions: {}
+
+concurrency:
+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}"
+ queue: max
+
+run-name: "SqlClient Issue Auto-Triage"
+
+env:
+ OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}
+ OTEL_SERVICE_NAME: gh-aw.issue-triage
+ OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=SqlClient%20Issue%20Auto-Triage,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot'
+ OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}
+ GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]'
+ GH_AW_OTLP_IF_MISSING: ignore
+
+jobs:
+ activation:
+ if: >
+ github.event_name == 'issues' ||
+ (github.event_name == 'issue_comment'
+ && github.event.issue.pull_request == null
+ && !endsWith(github.event.comment.user.login, '[bot]')
+ && (
+ ((github.event.comment.body == '/triage' || startsWith(github.event.comment.body, '/triage '))
+ && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
+ ||
+ (github.event.comment.body != '/triage'
+ && !startsWith(github.event.comment.body, '/triage ')
+ && github.event.comment.user.login == github.event.issue.user.login
+ && contains(github.event.issue.labels.*.name, 'Auto-Triage: Waiting for Author'))
+ ))
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: read
+ env:
+ GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }}
+ GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}
+ outputs:
+ body: ${{ steps.sanitized.outputs.body }}
+ comment_id: ""
+ comment_repo: ""
+ daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }}
+ daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }}
+ daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }}
+ daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }}
+ engine_id: ${{ steps.generate_aw_info.outputs.engine_id }}
+ lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }}
+ setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }}
+ setup-span-id: ${{ steps.setup.outputs.span-id }}
+ setup-trace-id: ${{ steps.setup.outputs.trace-id }}
+ stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }}
+ text: ${{ steps.sanitized.outputs.text }}
+ title: ${{ steps.sanitized.outputs.title }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.80"
+ GH_AW_INFO_AWF_VERSION: "v0.28.12"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Mask OTLP telemetry headers
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh"
+ - name: Generate agentic run info
+ id: generate_aw_info
+ env:
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
+ GH_AW_INFO_MODEL: "auto"
+ GH_AW_INFO_VERSION: "1.0.80"
+ GH_AW_INFO_AGENT_VERSION: "1.0.80"
+ GH_AW_INFO_CLI_VERSION: "v0.88.2"
+ GH_AW_INFO_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.28.12"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ GH_AW_INFO_AGENT_RUNTIME: ""
+ GH_AW_COMPILED_STRICT: "true"
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs'));
+ await main(core, context);
+ - name: Restore daily AIC usage cache
+ id: restore-daily-aic-cache
+ if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }}
+ continue-on-error: true
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ key: agentic-workflow-usage-issuetriage-${{ github.run_id }}
+ restore-keys: agentic-workflow-usage-issuetriage-
+ path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl
+ - name: Restore daily AIC usage cache (artifact fallback)
+ id: restore-daily-aic-cache-fallback
+ if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }}
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }}
+ GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }}
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs'));
+ await main();
+ - name: Check daily workflow token guardrail
+ id: daily-effective-workflow-guardrail
+ if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }}
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_ID: "issue-triage"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }}
+ GH_AW_HAS_SLASH_COMMAND: "false"
+ GH_AW_HAS_LABEL_COMMAND: "false"
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }}
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs'));
+ await main();
+ - name: Check for OAuth tokens
+ id: check-oauth-tokens
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh"
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ - name: Checkout .github and .agents folders
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ sparse-checkout: |
+ .github
+ .agents
+ .claude
+ .codex
+ .gemini
+ .pi
+ sparse-checkout-cone-mode: true
+ fetch-depth: 1
+ - name: Save agent config folders for base branch restoration
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .github"
+ GH_AW_AGENT_FILES: "AGENTS.md"
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh"
+ - name: Check workflow lock file
+ id: check-lock-file
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_WORKFLOW_FILE: "issue-triage.lock.yml"
+ GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}"
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs'));
+ await main();
+ - name: Check compile-agentic version
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_COMPILED_VERSION: "v0.88.2"
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'check_version_updates.cjs'));
+ await main();
+ - name: Compute current body text
+ id: sanitized
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'compute_text.cjs'));
+ await main();
+ - name: Log runtime features
+ if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }}
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh"
+ - name: Create prompt with built-in context
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions
+ GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
+ GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"file\":\"pr_context_prompt.md\",\"condition_env\":\"GH_AW_INCLUDE_PR_CONTEXT\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}"
+ GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }}
+ GH_AW_PROMPT_CONTENT_0000: "\n"
+ GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, remove_labels, missing_tool, missing_data, noop\n"
+ GH_AW_PROMPT_CONTENT_0002: "\n"
+ GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n"
+ GH_AW_PROMPT_CONTENT_0004: "\n"
+ GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/issue-triage.md}}\n"
+ with:
+ script: |
+ const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs');
+ await main(core);
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt
+ GH_AW_ENGINE_ID: "copilot"
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'interpolate_prompt.cjs'));
+ await main();
+ - name: Substitute placeholders
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt
+ GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }}
+ GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools"
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+
+ const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs'));
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A,
+ GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A,
+ GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A,
+ GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE,
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
+ GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
+ GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
+ GH_AW_INCLUDE_PR_CONTEXT: process.env.GH_AW_INCLUDE_PR_CONTEXT,
+ GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST
+ }
+ });
+ - name: Validate prompt placeholders
+ env:
+ GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh"
+ - name: Print prompt
+ env:
+ GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh"
+ - name: Stage prompt files for artifact upload
+ run: |
+ mkdir -p /tmp/gh-aw/aw-prompts
+ cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/
+ - name: Upload activation artifact
+ if: success() || failure()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: activation
+ include-hidden-files: true
+ path: |
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/models.json
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/aw-prompts/prompt-template.txt
+ /tmp/gh-aw/aw-prompts/prompt-import-tree.json
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/base
+ /tmp/gh-aw/.github/agents
+ /tmp/gh-aw/.github/skills
+ if-no-files-found: ignore
+ retention-days: 1
+
+ agent:
+ needs: activation
+ if: needs.activation.outputs.daily_ai_credits_exceeded != 'true'
+ runs-on: ubuntu-latest
+ environment: issue-triage
+ permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+ timeout-minutes: 60
+ env:
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ GH_AW_ASSETS_ALLOWED_EXTS: ""
+ GH_AW_ASSETS_BRANCH: ""
+ GH_AW_ASSETS_MAX_SIZE_KB: 0
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ GH_AW_PR_HEAD_BASE_BRANCH: ""
+ GH_AW_PR_HEAD_BASE_PR_NUMBER: ""
+ GH_AW_PR_HEAD_BASE_REF: ""
+ GH_AW_PR_HEAD_BASE_REPO: ""
+ GH_AW_PR_HEAD_BASE_SHA: ""
+ GH_AW_PR_HEAD_REPO: ""
+ GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}
+ GH_AW_WORKFLOW_ID_SANITIZED: issuetriage
+ outputs:
+ agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }}
+ ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }}
+ aic: ${{ steps.parse-mcp-gateway.outputs.aic }}
+ ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }}
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }}
+ has_patch: ${{ steps.collect_output.outputs.has_patch }}
+ http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }}
+ inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }}
+ invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }}
+ max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }}
+ mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }}
+ missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }}
+ missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }}
+ model: ${{ needs.activation.outputs.model }}
+ model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }}
+ output: ${{ steps.collect_output.outputs.output }}
+ output_types: ${{ steps.collect_output.outputs.output_types }}
+ setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }}
+ setup-span-id: ${{ steps.setup.outputs.span-id }}
+ setup-trace-id: ${{ steps.setup.outputs.trace-id }}
+ shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }}
+ unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.80"
+ GH_AW_INFO_AWF_VERSION: "v0.28.12"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Set runtime paths
+ id: set-runtime-paths
+ env:
+ GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }}
+ run: |
+ if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then
+ echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV"
+ fi
+ {
+ echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl"
+ echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json"
+ echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
+ } >> "$GITHUB_OUTPUT"
+ - name: Mask OTLP telemetry headers
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh"
+ - name: Check OTLP telemetry configuration
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh"
+ - name: Checkout repository
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh"
+ - name: Configure gh CLI for GitHub Enterprise
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh"
+ env:
+ GH_TOKEN: ${{ github.token }}
+ - name: Download activation artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: activation
+ path: /tmp/gh-aw
+ - name: Configure Git credentials
+ env:
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request'
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs'));
+ await main();
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh"
+ env:
+ GH_HOST: github.com
+ GH_AW_COMPILED_VERSION: v0.88.2
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless
+ - name: Determine automatic lockdown mode for GitHub MCP Server
+ id: determine-automatic-lockdown
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
+ env:
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ GH_AW_GITHUB_MIN_INTEGRITY: 'none'
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs'));
+ await determineAutomaticLockdown(github, context, core);
+ - name: Parse integrity filter lists
+ id: parse-guard-vars
+ env:
+ GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }}
+ GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }}
+ GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }}
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh"
+ - name: Restore agent config folders from base branch
+ if: steps.checkout-pr.outcome == 'success'
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .github"
+ GH_AW_AGENT_FILES: "AGENTS.md"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh"
+ - name: Restore inline sub-agents from activation artifact
+ env:
+ GH_AW_SUB_AGENT_DIR: ".github/agents"
+ GH_AW_SUB_AGENT_EXT: ".agent.md"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh"
+ - name: Restore inline skills from activation artifact
+ env:
+ GH_AW_SKILL_DIR: ".github/skills"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
+ - name: Download container images
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699
+ - name: Prepare Safe Outputs Directories
+ run: |
+ mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
+ mkdir -p /tmp/gh-aw/safeoutputs
+ mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
+ - name: Generate Safe Outputs Config
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw"
+ GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}"
+ GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"Auto-Triage: Waiting for Author\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"Auto-Triage: Waiting for Author\"],\"max\":1},\"report_incomplete\":{}}"
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'create_files.cjs'));
+ await main();
+ - name: Generate Safe Outputs Tools
+ env:
+ GH_AW_TOOLS_META_JSON: |
+ {
+ "description_suffixes": {
+ "add_comment": " CONSTRAINTS: Maximum 1 comment(s) can be added. Supports reply_to_id for discussion threading.",
+ "add_labels": " CONSTRAINTS: Maximum 1 label(s) can be added. Only these labels are allowed: [\"Auto-Triage: Waiting for Author\"].",
+ "remove_labels": " CONSTRAINTS: Maximum 1 label(s) can be removed. Only these labels can be removed: [Auto-Triage: Waiting for Author]."
+ },
+ "repo_params": {},
+ "dynamic_tools": []
+ }
+ GH_AW_VALIDATION_JSON: |
+ {
+ "add_comment": {
+ "defaultMax": 1,
+ "fields": {
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "comment_id": {
+ "optionalPositiveInteger": true
+ },
+ "item_number": {
+ "issueOrPRNumber": true
+ },
+ "pr": {
+ "issueOrPRNumber": true
+ },
+ "pr_number": {
+ "issueOrPRNumber": true
+ },
+ "reply_to_id": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "target": {
+ "type": "string",
+ "enum": [
+ "status"
+ ]
+ },
+ "temporary_id": {
+ "type": "string",
+ "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$"
+ }
+ }
+ },
+ "add_labels": {
+ "defaultMax": 5,
+ "fields": {
+ "item_number": {
+ "issueNumberOrTemporaryId": true
+ },
+ "labels": {
+ "required": true,
+ "type": "array"
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_data": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "context": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "data_type": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "reason": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_tool": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 512
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "tool": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "noop": {
+ "defaultMax": 1,
+ "fields": {
+ "message": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ }
+ }
+ },
+ "remove_labels": {
+ "defaultMax": 5,
+ "fields": {
+ "item_number": {
+ "issueNumberOrTemporaryId": true
+ },
+ "labels": {
+ "required": true,
+ "type": "array"
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ }
+ }
+ },
+ "report_incomplete": {
+ "defaultMax": 5,
+ "fields": {
+ "details": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 1024
+ }
+ }
+ }
+ }
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs'));
+ await main();
+ - name: Start MCP Gateway
+ id: start-mcp-gateway
+ env:
+ GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }}
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }}
+ GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }}
+ GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ set -eo pipefail
+ mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config"
+ if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then
+ GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json"
+ cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}"
+ export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}"
+ fi
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="8080"
+ export MCP_GATEWAY_DOMAIN="awmg-mcpg"
+ export MCP_GATEWAY_HOST_DOMAIN="localhost"
+ MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${MCP_GATEWAY_AGENT_ID}"
+ export MCP_GATEWAY_AGENT_ID
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288"
+ export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro"
+ export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}"
+ export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}"
+ export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}"
+ export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}"
+ export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}"
+ export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}"
+ export DEBUG="*"
+
+ export GH_AW_ENGINE="copilot"
+ MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0')
+ MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0')
+ source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh"
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_AGENT_ID -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15'
+
+ mkdir -p "$HOME/.copilot"
+ GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
+ cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
+ {
+ "mcpServers": {
+ "github": {
+ "type": "stdio",
+ "container": "ghcr.io/github/github-mcp-server:v1.11.0",
+ "env": {
+ "GITHUB_FEATURES": "fields_param",
+ "GITHUB_HOST": "${GITHUB_SERVER_URL}",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
+ },
+ "guard-policies": {
+ "allow-only": {
+ "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }},
+ "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }},
+ "min-integrity": "none",
+ "repos": "all",
+ "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }}
+ }
+ }
+ },
+ "safeoutputs": {
+ "type": "stdio",
+ "container": "ghcr.io/github/gh-aw-node",
+ "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"],
+ "args": ["-w", "\${GITHUB_WORKSPACE}"],
+ "entrypoint": "sh",
+ "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"],
+ "env": {
+ "DEBUG": "*",
+ "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}",
+ "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}",
+ "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}",
+ "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}",
+ "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}",
+ "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}",
+ "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}",
+ "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}",
+ "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}",
+ "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}",
+ "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}",
+ "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}",
+ "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}",
+ "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}",
+ "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}",
+ "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}",
+ "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}",
+ "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}",
+ "GITHUB_SHA": "\${GITHUB_SHA}",
+ "GITHUB_TOKEN": "\${GITHUB_TOKEN}",
+ "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}",
+ "RUNNER_TEMP": "\${RUNNER_TEMP}"
+ },
+ "guard-policies": {
+ "write-sink": {
+ "accept": [
+ "*"
+ ],
+ "sink-visibility": "${GH_AW_SINK_VISIBILITY}"
+ }
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "agentId": "${MCP_GATEWAY_AGENT_ID}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}",
+ "startupTimeout": 120,
+ "opentelemetry": {
+ "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}",
+ "traceId": "${GITHUB_AW_OTEL_TRACE_ID}",
+ "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}"
+ }
+ }
+ }
+ GH_AW_MCP_CONFIG_137b94c134aafdc9_EOF
+ - name: Mount MCP servers as CLIs
+ id: mount-mcp-clis
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }}
+ MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }}
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs'));
+ await main();
+ - name: Clean credentials
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh"
+ - name: Audit pre-agent workspace
+ id: pre_agent_audit
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh"
+ - name: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ # --allow-tool github
+ # --allow-tool safeoutputs
+ # --allow-tool shell(cat)
+ # --allow-tool shell(date)
+ # --allow-tool shell(echo)
+ # --allow-tool shell(find)
+ # --allow-tool shell(github:*)
+ # --allow-tool shell(grep)
+ # --allow-tool shell(head)
+ # --allow-tool shell(ls)
+ # --allow-tool shell(printf)
+ # --allow-tool shell(pwd)
+ # --allow-tool shell(safeoutputs:*)
+ # --allow-tool shell(sort)
+ # --allow-tool shell(tail)
+ # --allow-tool shell(uniq)
+ # --allow-tool shell(wc)
+ # --allow-tool shell(yq)
+ # --allow-tool write
+ timeout-minutes: ${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }}
+ run: |
+ set -o pipefail
+ printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt
+ trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT
+ mkdir -p "$HOME/.copilot"
+ printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json"
+ export XDG_CONFIG_HOME="$HOME"
+ export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json"
+ GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)"
+ if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then
+ echo "GitHub Copilot CLI executable not found on PATH after installation" >&2
+ exit 127
+ fi
+ GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot"
+ mkdir -p "${RUNNER_TEMP}/gh-aw/bin"
+ if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then
+ cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN"
+ fi
+ chmod 755 "$GH_AW_COPILOT_BIN"
+
+ touch /tmp/gh-aw/agent-step-summary.md
+ GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true)
+ export GH_AW_NODE_BIN
+ export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK"
+ (umask 177 && touch /tmp/gh-aw/agent-stdio.log)
+ GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}"
+ if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then
+ GH_AW_MAX_AI_CREDITS="1000"
+ fi
+ printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json"
+ GH_AW_DOCKER_HOST=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST="${DOCKER_HOST}"
+ fi
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"
+ if [ -d "$GH_AW_TOOL_CACHE" ]; then
+ if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then
+ GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro"
+ fi
+ fi
+ # shellcheck disable=SC1003,SC2016,SC2086
+ GH_AW_AWF_ENGINE_NAME=copilot \
+ GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \
+ GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \
+ GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \
+ bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \
+ awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \
+ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt'
+ env:
+ AWF_REFLECT_ENABLED: 1
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: auto
+ GH_AW_LLM_PROVIDER: github
+ GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}
+ GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }}
+ GH_AW_PHASE: agent
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_TIMEOUT_MINUTES: ${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }}
+ GH_AW_VERSION: v0.88.2
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }}
+ - name: Detect agent errors
+ if: always()
+ id: detect-agent-errors
+ continue-on-error: true
+ env:
+ GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }}
+ GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: ${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }}
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs'));
+ await main();
+ - name: Configure Git credentials
+ env:
+ GITHUB_REPOSITORY: ${{ github.repository }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh"
+ - name: Stop MCP Gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'redact_secrets.cjs'));
+ await main();
+ env:
+ GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Append agent step summary
+ if: always()
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh"
+ - name: Copy Safe Outputs
+ if: always()
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ run: |
+ mkdir -p /tmp/gh-aw
+ cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
+ - name: Ingest agent output
+ id: collect_output
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs'));
+ await main();
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs'));
+ await main();
+ - name: Parse MCP Gateway logs for step summary
+ if: always()
+ id: parse-mcp-gateway
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs'));
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless
+ - name: Parse token usage for step summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs'));
+ await main();
+ - name: Print AWF reflect summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs'));
+ await main();
+ - name: Generate observability summary
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs'));
+ await main(core);
+ - name: Write agent output placeholder if missing
+ if: always()
+ run: |
+ if [ ! -f /tmp/gh-aw/agent_output.json ]; then
+ echo '{"items":[]}' > /tmp/gh-aw/agent_output.json
+ fi
+ # Small dedicated copy of the agent output so safe-output processing
+ # survives a failed or timed-out upload of the larger agent artifact
+ - name: Upload agent output fallback artifact
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: agent-output-fallback
+ path: |
+ /tmp/gh-aw/agent_output.json
+ /tmp/gh-aw/safeoutputs.jsonl
+ if-no-files-found: ignore
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: agent
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/proxy-logs/
+ !/tmp/gh-aw/proxy-logs/proxy-tls/
+ /tmp/gh-aw/agent_usage.json
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/pre-agent-audit.txt
+ /tmp/gh-aw/agent/
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/otel.jsonl
+ /tmp/gh-aw/otlp-export-errors.jsonl
+ /tmp/gh-aw/safeoutputs.jsonl
+ /tmp/gh-aw/agent_output.json
+ /tmp/gh-aw/aw-*.patch
+ /tmp/gh-aw/aw-*.bundle
+ /tmp/gh-aw/awf-config.json
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/sandbox/firewall/audit/
+ /tmp/gh-aw/sandbox/firewall/awf-reflect.json
+ if-no-files-found: ignore
+
+ conclusion:
+ needs:
+ - activation
+ - agent
+ - detection
+ - safe_outputs
+ if: >
+ always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
+ needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' ||
+ needs.activation.outputs.daily_ai_credits_exceeded == 'true')
+ runs-on: ubuntu-slim
+ environment: issue-triage
+ permissions:
+ actions: read
+ issues: write
+ pull-requests: write
+ concurrency:
+ group: "gh-aw-conclusion-issue-triage"
+ cancel-in-progress: false
+ queue: max
+ env:
+ GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}
+ outputs:
+ incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }}
+ noop_message: ${{ steps.noop.outputs.noop_message }}
+ tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
+ total_count: ${{ steps.missing_tool.outputs.total_count }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.80"
+ GH_AW_INFO_AWF_VERSION: "v0.28.12"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: "{agent,agent-output-fallback}"
+ merge-multiple: true
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ if [ -f "/tmp/gh-aw/agent_output.json" ]; then
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
+ fi
+ - name: Download detection artifact
+ id: download-detection-artifact
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: detection
+ path: /tmp/gh-aw/threat-detection/
+ - name: Download Safe Outputs Items Manifest
+ id: download-safe-outputs-manifest
+ if: always()
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: safe-outputs-items
+ merge-multiple: true
+ path: /tmp/gh-aw/
+ - name: Collect usage artifact files
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh"
+ - name: Upload usage artifact
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: usage
+ path: |
+ /tmp/gh-aw/usage/aw_info.json
+ /tmp/gh-aw/usage/aw-info.jsonl
+ /tmp/gh-aw/usage/agent_usage.json
+ /tmp/gh-aw/usage/agent_usage.jsonl
+ /tmp/gh-aw/usage/detection_usage.jsonl
+ /tmp/gh-aw/usage/evals.jsonl
+ /tmp/gh-aw/usage/graders/grader_manifest.json
+ /tmp/gh-aw/usage/graders/grader_results.json
+ /tmp/gh-aw/usage/github_rate_limits.jsonl
+ /tmp/gh-aw/usage/agent/token_usage.jsonl
+ /tmp/gh-aw/usage/detection/token_usage.jsonl
+ /tmp/gh-aw/usage/activity/summary.json
+ if-no-files-found: ignore
+ - name: Restore daily AIC usage cache
+ id: restore-daily-aic-cache-conclusion
+ if: always()
+ continue-on-error: true
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ key: agentic-workflow-usage-issuetriage-${{ github.run_id }}
+ restore-keys: agentic-workflow-usage-issuetriage-
+ path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl
+ - name: Write daily AIC usage cache entry
+ id: write-daily-aic-cache
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ github-token: ${{ github.token }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context);
+ const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs'));
+ await main();
+ - name: Save daily AIC usage cache
+ id: save-daily-aic-cache
+ if: always()
+ continue-on-error: true
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ key: agentic-workflow-usage-issuetriage-${{ github.run_id }}
+ path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl
+ - name: Upload daily AIC usage cache artifact
+ id: upload-daily-aic-cache
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: aic-usage-cache
+ path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl
+ if-no-files-found: ignore
+ retention-days: 7
+ - name: Process no-op messages
+ id: noop
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_NOOP_MAX: "1"
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_NOOP_REPORT_AS_ISSUE: "false"
+ GH_AW_AIC: ${{ needs.agent.outputs.aic }}
+ GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }}
+ GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }}
+ GH_AW_WORKFLOW_ID: "issue-triage"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs'));
+ await main();
+ - name: Log detection run
+ id: detection_runs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
+ GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs'));
+ await main();
+ - name: Record missing tool
+ id: missing_tool
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_MISSING_TOOL_CREATE_ISSUE: "true"
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'missing_tool.cjs'));
+ await main();
+ - name: Record incomplete
+ id: report_incomplete
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true"
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs'));
+ await main();
+ - name: Handle agent failure
+ id: handle_agent_failure
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_WORKFLOW_ID: "issue-triage"
+ GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0"
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }}
+ GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }}
+ GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }}
+ GH_AW_AIC: ${{ needs.agent.outputs.aic }}
+ GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }}
+ GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}
+ GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
+ GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }}
+ GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }}
+ GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }}
+ GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }}
+ GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }}
+ GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }}
+ GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }}
+ GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }}
+ GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com"
+ GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }}
+ GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }}
+ GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }}
+ GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }}
+ GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }}
+ GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }}
+ GH_AW_GROUP_REPORTS: "false"
+ GH_AW_FAILURE_REPORT_AS_ISSUE: "true"
+ GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true"
+ GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true"
+ GH_AW_TIMEOUT_MINUTES: "${{ fromJSON(vars.GH_AW_DEFAULT_TIMEOUT_MINUTES || '20') }}"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs'));
+ await main();
+ - name: Report failed jobs
+ id: report_failed_jobs
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_REPORT_FAILED_JOBS: "true"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs'));
+ await main();
+
+ detection:
+ needs:
+ - activation
+ - agent
+ if: always() && needs.agent.result != 'skipped'
+ runs-on: ubuntu-latest
+ environment: issue-triage
+ permissions:
+ contents: read
+ timeout-minutes: 10
+ env:
+ GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}
+ outputs:
+ aic: ${{ steps.parse_detection_token_usage.outputs.aic }}
+ detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
+ detection_reason: ${{ steps.detection_conclusion.outputs.reason }}
+ detection_success: ${{ steps.detection_conclusion.outputs.success }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.80"
+ GH_AW_INFO_AWF_VERSION: "v0.28.12"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download activation artifact
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: activation
+ path: /tmp/gh-aw
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: "{agent,agent-output-fallback}"
+ merge-multiple: true
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ if [ -f "/tmp/gh-aw/agent_output.json" ]; then
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
+ fi
+ - name: Checkout repository for patch context
+ if: needs.agent.outputs.has_patch == 'true'
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
+ # --- Threat Detection ---
+ - name: Clean stale firewall files from agent artifact
+ run: |
+ rm -rf /tmp/gh-aw/sandbox/firewall/logs
+ rm -rf /tmp/gh-aw/sandbox/firewall/audit
+ - name: Download container images
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f
+ - name: Check if detection needed
+ id: detection_guard
+ if: always()
+ env:
+ OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }}
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ run: |
+ if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
+ echo "run_detection=true" >> "$GITHUB_OUTPUT"
+ echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH"
+ else
+ echo "run_detection=false" >> "$GITHUB_OUTPUT"
+ echo "Detection skipped: no agent outputs or patches to analyze"
+ fi
+ - name: Clear MCP Config for detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json"
+ rm -f "$HOME/.copilot/mcp-config.json"
+ rm -f "$GITHUB_WORKSPACE/.gemini/settings.json"
+ - name: Prepare threat detection files
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh"
+ - name: Setup threat detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ WORKFLOW_DESCRIPTION: "No description provided"
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ GH_AW_DETECTION_CONTINUE_ON_ERROR: "true"
+ GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true"
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'setup_threat_detection.cjs'));
+ await main();
+ - name: Ensure threat-detection directory and log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection
+ touch /tmp/gh-aw/threat-detection/detection.log
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh"
+ env:
+ GH_HOST: github.com
+ GH_AW_COMPILED_VERSION: v0.88.2
+ - name: Install threat-detect binary
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ continue-on-error: true
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1
+ - name: Execute threat detection with AWF
+ id: detection_agentic_execution
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ continue-on-error: true
+ timeout-minutes: 10
+ env:
+ AWF_REFLECT_ENABLED: 1
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: auto
+ GH_AW_HARNESS_MAX_RETRIES: 0
+ GH_AW_LLM_PROVIDER: github
+ GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}
+ GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }}
+ GH_AW_PHASE: detection
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_TIMEOUT_MINUTES: 10
+ GH_AW_VERSION: v0.88.2
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }}
+ WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ WORKFLOW_DESCRIPTION: "No description provided"
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ GH_AW_DETECTION_CONTINUE_ON_ERROR: "true"
+ run: |
+ set -o pipefail
+ printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt
+ GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)"
+ if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then
+ echo "GitHub Copilot CLI executable not found on PATH after installation" >&2
+ exit 127
+ fi
+ GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot"
+ mkdir -p "${RUNNER_TEMP}/gh-aw/bin"
+ if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then
+ cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN"
+ fi
+ chmod 755 "$GH_AW_COPILOT_BIN"
+
+ (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log)
+ GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}"
+ if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then
+ GH_AW_MAX_AI_CREDITS="400"
+ fi
+ printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json"
+ GH_AW_DOCKER_HOST=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST="${DOCKER_HOST}"
+ fi
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; }
+ printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"
+ if [ -d "$GH_AW_TOOL_CACHE" ]; then
+ if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then
+ GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro"
+ fi
+ fi
+ # shellcheck disable=SC1003,SC2016,SC2086
+ awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \
+ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
+ - name: Render detection log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'render_detection_log.cjs'));
+ await main();
+ - name: Copy detection firewall logs
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ continue-on-error: true
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall
+ if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi
+ if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi
+ - name: Upload threat detection artifact
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: detection
+ path: |
+ /tmp/gh-aw/threat-detection/detection_result.json
+ /tmp/gh-aw/threat-detection/sandbox/firewall/logs/
+ /tmp/gh-aw/threat-detection/sandbox/firewall/audit/
+ if-no-files-found: ignore
+ - name: Parse threat detection token usage for step summary
+ id: parse_detection_token_usage
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage
+ with:
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs'));
+ await main();
+ - name: Conclude threat detection
+ id: detection_conclusion
+ if: always()
+ continue-on-error: true
+ env:
+ RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }}
+ DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }}
+ GH_AW_DETECTION_CONTINUE_ON_ERROR: "true"
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json
+
+ safe_outputs:
+ needs:
+ - activation
+ - agent
+ - detection
+ if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
+ runs-on: ubuntu-slim
+ environment: issue-triage
+ permissions:
+ issues: write
+ pull-requests: write
+ timeout-minutes: 45
+ env:
+ GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }}
+ GH_AW_AIC: ${{ needs.agent.outputs.aic }}
+ GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }}
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/issue-triage"
+ GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
+ GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }}
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_ENGINE_MODEL: "auto"
+ GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }}
+ GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }}
+ GH_AW_WORKFLOW_ID: "issue-triage"
+ GH_AW_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md"
+ outputs:
+ code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }}
+ code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }}
+ comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }}
+ comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }}
+ create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }}
+ create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
+ process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }}
+ process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }}
+ process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }}
+ process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }}
+ process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }}
+ process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }}
+ process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }}
+ process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }}
+ process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }}
+ process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
+ steps:
+ - name: Setup Scripts
+ id: setup
+ uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "SqlClient Issue Auto-Triage"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.80"
+ GH_AW_INFO_AWF_VERSION: "v0.28.12"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Mask OTLP telemetry headers
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ pattern: "{agent,agent-output-fallback}"
+ merge-multiple: true
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ if [ -f "/tmp/gh-aw/agent_output.json" ]; then
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
+ fi
+ - name: Configure GH_HOST for enterprise compatibility
+ id: ghes-host-config
+ shell: bash
+ run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
+ # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
+ # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
+ GH_HOST="${GITHUB_SERVER_URL#https://}"
+ GH_HOST="${GH_HOST#http://}"
+ echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
+ - name: Process Safe Outputs
+ id: process_safe_outputs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }}
+ GH_AW_ALLOWED_DOMAINS: "api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"add_labels\":{\"allowed\":[\"Auto-Triage: Waiting for Author\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"remove_labels\":{\"allowed\":[\"Auto-Triage: Waiting for Author\"],\"max\":1},\"report_incomplete\":{}}"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const path = require('path');
+ const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions');
+ const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs'));
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs'));
+ await main();
+ - name: Upload Safe Outputs Items
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: safe-outputs-items
+ path: |
+ /tmp/gh-aw/safe-output-items.jsonl
+ /tmp/gh-aw/temporary-id-map.json
+ /tmp/gh-aw/safe-output-errors.json
+ if-no-files-found: ignore
diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md
new file mode 100644
index 0000000000..100d8baf54
--- /dev/null
+++ b/.github/workflows/issue-triage.md
@@ -0,0 +1,286 @@
+---
+on:
+ issues:
+ types: [opened]
+ issue_comment:
+ types: [created]
+ roles: all
+
+# Cheap gate evaluated BEFORE the agent boots. The activation job is skipped
+# (zero compute, $0) if this is false. Only events matching one of the
+# following three conditions cause the workflow to run:
+#
+# 1. issues.opened
+# -> Initial triage. Always runs.
+#
+# 2. issue_comment.created from the issue's original author, on an issue
+# (not a PR), not a bot, AND the issue currently has the label
+# "Auto-Triage: Waiting for Author".
+# -> Follow-up triage. The label is applied by the initial triage
+# only when environment fields were missing, and removed by the
+# follow-up triage once the author supplies them. Without the
+# label, author comments do NOT boot the agent.
+#
+# 3. issue_comment.created whose body starts with "/triage", from a repo
+# OWNER, MEMBER, or COLLABORATOR (maintainer-only on-demand override).
+# -> On-demand triage. Bypasses the follow-up gate; produces a fresh
+# triage summary regardless of label or prior summaries.
+if: |
+ github.event_name == 'issues' ||
+ (github.event_name == 'issue_comment'
+ && github.event.issue.pull_request == null
+ && !endsWith(github.event.comment.user.login, '[bot]')
+ && (
+ ((github.event.comment.body == '/triage' || startsWith(github.event.comment.body, '/triage '))
+ && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
+ ||
+ (github.event.comment.body != '/triage'
+ && !startsWith(github.event.comment.body, '/triage ')
+ && github.event.comment.user.login == github.event.issue.user.login
+ && contains(github.event.issue.labels.*.name, 'Auto-Triage: Waiting for Author'))
+ ))
+
+engine: copilot
+model: auto
+
+environment: issue-triage
+
+permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+
+tools:
+ bash: [cat, find, grep]
+ github:
+ min-integrity: none
+
+safe-outputs:
+ # One triage summary per run. `hide-older-comments` collapses previous
+ # summaries so only the latest is visible.
+ add-comment:
+ max: 1
+ hide-older-comments: true
+ # Allow the workflow to apply/remove ONLY this one internal-state label.
+ # The label is the YAML-level flag that lets the cheap `if:` gate above
+ # decide whether an author comment should boot the agent at all.
+ add-labels:
+ allowed: ["Auto-Triage: Waiting for Author"]
+ max: 1
+ remove-labels:
+ allowed: ["Auto-Triage: Waiting for Author"]
+ max: 1
+ # Silently skip noop runs (e.g. random author comment with no new env info).
+ # Without this, gh-aw auto-creates a tracking issue "[aw] No-Op Runs" and
+ # appends a comment to it for every noop.
+ noop:
+ report-as-issue: false
+---
+
+# SqlClient Issue Auto-Triage
+
+You are a triage specialist for **Microsoft.Data.SqlClient**.
+Your job is to post **at most one** triage summary comment per workflow run
+using `add_comment`.
+
+This workflow runs in three situations. Identify which one **before** doing
+any work, then follow the matching flow:
+
+1. **Initial triage** — `event_name == "issues"`. A new issue was just opened.
+ Always proceed to the triage instructions below.
+2. **Follow-up triage** — `event_name == "issue_comment"` and the comment body
+ does NOT start with `/triage`. The workflow-level `if:` has already verified
+ the issue currently carries the label `Auto-Triage: Waiting for Author`,
+ so a prior triage flagged missing env info and the author has now responded.
+ Treat later author comments as part of the issue body and re-validate the
+ environment. There are three sub-cases routed by "Follow-up routing" under
+ Instructions:
+ - **No progress** (comment supplied no new env field, e.g. "will share
+ soon") → silent `noop`, label stays, no comment posted.
+ - **Partial progress** (comment supplied at least one new env field but
+ others are still missing) → post a fresh summary acknowledging what
+ was provided and re-asking for the rest, KEEP the label.
+ - **Complete** (all required env fields are now present) → post a fresh
+ summary, REMOVE the label.
+3. **On-demand triage** — `event_name == "issue_comment"` and the comment body
+ starts with `/triage`. A maintainer is explicitly requesting a fresh triage.
+ Ignore label state and prior summary counts; proceed to the triage
+ instructions and produce a new summary. Do NOT change the label as part of
+ `/triage` runs (leave it as-is).
+
+Do NOT call `add_comment` more than once per run.
+Do NOT call `add_labels` or `remove_labels` for any label other than
+`Auto-Triage: Waiting for Author` — that single label is the only one this
+workflow is permitted to manage.
+Do NOT post intermediate findings. Do NOT post separate comments for
+area detection, duplicate checking, or environment validation.
+Everything goes into the single triage summary at the end.
+
+---
+
+## Label-managed state
+
+The workflow uses one internal-state label to decide cheaply (at the YAML
+`if:` level) whether an author comment should boot the agent at all:
+
+- **`Auto-Triage: Waiting for Author`** — present iff the most recent
+ triage summary flagged `⚠️ Missing:` or `⚠️ Partial:` environment fields
+ and we are waiting for the issue author to supply them.
+
+The agent (you) is responsible for keeping this label accurate — see the
+"Actions" section below for exactly when to call `add_labels` /
+`remove_labels`.
+
+Only if the workflow-level `if:` gate above evaluated to true, proceed to the
+triage instructions below and produce a fresh summary. Treat the prior summary
+as **invalidated** — the new one supersedes it (the older one will be collapsed
+automatically by `hide-older-comments`).
+
+---
+
+## Required Context
+
+Before analyzing the issue, you MUST read all project knowledge base files
+from the checked-out repository. Recursively list the `.github/` directory
+and read every markdown file (`.md`) found under it, excluding the `workflows/`
+subdirectory. This includes but is not limited to instructions, prompts,
+issue templates, skills, plans, and any other documentation files present.
+
+Use these files to inform your area classification, duplicate detection,
+environment validation, and analysis. Do not skip this step.
+
+---
+
+## Instructions
+
+Read the issue body **and, for follow-up / on-demand runs, every subsequent
+comment**. Then do ALL of the following analysis silently (using read tools
+and search only — no comments, no outputs):
+
+### Follow-up routing (scenario 2 only)
+
+Before running the full analysis on a follow-up run, decide which of three
+sub-cases this comment falls into. Use the rules in step **B** below to
+determine which environment fields each source supplies.
+
+Compute two snapshots:
+
+- **BEFORE** = env fields supplied by the issue body + every author comment
+ EXCEPT the triggering comment.
+- **AFTER** = env fields supplied by the issue body + every author comment
+ INCLUDING the triggering comment.
+
+Then route as follows:
+
+1. **No progress** — `AFTER == BEFORE` (the new comment did not supply any
+ new env field; e.g. "okay, will share details soon", a question, an
+ unrelated remark). → Call `noop` with a short reason like `"Author
+ commented but supplied no new env info"` and STOP. Do NOT call
+ `add_comment`. Do NOT change the label. The label stays so the next
+ author comment can re-trigger this workflow.
+
+2. **Partial progress** — `AFTER` adds at least one new env field but is
+ still incomplete (some required fields are still missing). → Proceed
+ to the full analysis below and post a fresh triage summary. The
+ `Environment` row MUST acknowledge what was just provided and list
+ only the fields that are STILL missing, e.g.
+ `⚠️ Partial: received SqlClient version and OS; still missing: .NET TFM, SQL Server version`.
+ Keep the label `Auto-Triage: Waiting for Author` on the issue (i.e. call
+ `add_labels` with that label — it is a no-op if already present).
+
+3. **Complete** — `AFTER` contains every required env field. → Proceed to
+ the full analysis below and post a fresh triage summary. The
+ `Environment` row says `All required environment details provided for
+ investigation`. Call `remove_labels` with the label.
+
+This routing does NOT apply to initial triage (scenario 1) or on-demand
+`/triage` (scenario 3) — those always produce a fresh summary using the
+standard label-management rules in the Actions section below.
+
+### Full analysis
+
+**A. Classify issue type**: Bug (reports unexpected behavior, crash, regression, or incorrect results), Feature (has proposal), Question, or Task.
+
+**B. Validate environment** (bugs only): Check for these required fields:
+SqlClient version, .NET target framework, SQL Server version, OS,
+repro steps, expected vs actual behavior.
+If any are missing, list them explicitly in the triage summary (e.g. "Missing: SQL Server version, OS").
+For follow-up runs, treat information supplied in any later comment by the
+issue author as if it were part of the original issue body.
+Proceed with all remaining triage steps regardless of missing environment details.
+
+**C. Classify area**: Based on the issue content, pick the single best matching area label from this list:
+
+| Label | Scope |
+|-------|-------|
+| `Area\Connection Pooling` | Pool behavior, timeouts, pool size, pool exhaustion |
+| `Area\AKV Provider` | Always Encrypted Azure Key Vault provider |
+| `Area\Json` | JSON data type support |
+| `Area\Managed SNI` | Managed SNI / network layer |
+| `Area\Native SNI` | Native SNI / network layer |
+| `Area\Sql Bulk Copy` | SqlBulkCopy operations |
+| `Area\Netcore` | .NET runtime / netcore specific |
+| `Area\Netfx` | .NET Framework specific |
+| `Area\Tests` | Test code / test projects |
+| `Area\Documentation` | Docs and samples |
+| `Area\Azure Connectivity` | Azure connectivity |
+| `Area\Engineering` | Build, CI/CD, infrastructure |
+| `Area\Vector` | Vector feature |
+| `Area\Async` | Async operations |
+
+**D. Search for duplicates**: Search `repo:dotnet/SqlClient ` for similar issues.
+
+**E. Check for regression**: If the reporter mentions a previously working version, note the version boundary.
+
+---
+
+## Actions
+
+Call `add_comment` exactly **once** with this markdown. For follow-up runs
+add "(updated after author response)" to the heading; for on-demand `/triage`
+runs add "(on-demand re-triage)" to the heading:
+
+```
+## 🔍 Triage Summary
+
+| Check | Result |
+|-------|--------|
+| Issue type | |
+| Environment | ; still missing: / ⚠️ Missing: list specific fields> |
+| Area | |
+| Duplicates | |
+| Regression | |
+
+### Analysis
+
+<2-4 sentences: what the issue is about, which component is likely affected,
+and severity assessment (P0-P3)>
+
+### Next Steps
+
+ handling logic present in the synchronous path.")
+- If duplicates were found: recommend reviewing the linked issues before proceeding.
+- If regression: note the version boundary and state that bisection is recommended.>
+
+> **Note**: This triage summary is auto-generated by an AI agent. The analysis and suggestions above have not been verified by a human maintainer. Please treat as preliminary guidance only.
+```
+
+**Then manage the label**:
+
+- For **on-demand `/triage` runs**, do NOT touch the label — preserve
+ whatever state existed before.
+- For **initial triage** and **follow-up triage**, base the decision on the
+ `Environment` row of the summary you just posted:
+ - If it contains `⚠️ Missing:` or `⚠️ Partial:` → call `add_labels` with
+ `["Auto-Triage: Waiting for Author"]` (no-op if already present).
+ - Otherwise (env complete) → call `remove_labels` with
+ `["Auto-Triage: Waiting for Author"]` (safe to call even if absent).
+
+If the issue is spam or no action is needed, call the `noop` tool instead.
\ No newline at end of file
diff --git a/.github/workflows/notify-author-attention.yml b/.github/workflows/notify-author-attention.yml
new file mode 100644
index 0000000000..4799e9efd1
--- /dev/null
+++ b/.github/workflows/notify-author-attention.yml
@@ -0,0 +1,35 @@
+name: Notify Author attention needed
+
+on:
+ pull_request_target:
+ types: [labeled]
+
+jobs:
+ notify-author:
+ # Only run when 'Author attention needed' label is added to a PR
+ if: >-
+ github.repository == 'dotnet/SqlClient' &&
+ github.event.label.name == 'Author attention needed'
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Post comment to PR author
+ uses: actions/github-script@v9
+ with:
+ script: |
+ const issue_number = context.payload.pull_request.number;
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+ const author = context.payload.pull_request.user.login;
+
+ const body = `@${author} This pull request has been marked as **Author attention needed**.\n\nWhen you have addressed the reviewer feedback and are ready for another review, please post a comment with \`/ready\` to remove the label and re-engage reviewers.`;
+
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number,
+ body,
+ });
+ core.info(`Posted notification comment on PR #${issue_number}`);
diff --git a/.github/workflows/recheck-milestones.yml b/.github/workflows/recheck-milestones.yml
new file mode 100644
index 0000000000..679b76f144
--- /dev/null
+++ b/.github/workflows/recheck-milestones.yml
@@ -0,0 +1,65 @@
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+#
+# Recheck Milestones
+#
+# Reconciles open pull requests when a release branch is cut.
+#
+# check-milestone.yml decides where a milestone belongs by asking whether
+# release/. exists and which milestone series is active on the
+# default branch. Creating a release branch can invalidate X.Y.* pull requests
+# and make the next series eligible, but emits no pull request activity, so
+# already open pull requests would otherwise keep their previous verdicts.
+#
+# This lives in its own workflow so that check-milestone.yml stays purely pull
+# request scoped, and so the elevated 'actions: write' permission needed to
+# re-run checks is isolated from the PR gate.
+#
+# A re-run replays the original run's commit, so a pull request whose last
+# milestone check predates a change to the check itself replays the older
+# version and keeps its stale result. See the LIMITATION section in
+# .github/scripts/recheck-milestones-for-release-branch.sh.
+#
+# See .github/scripts/recheck-milestones-for-release-branch.sh for the details.
+#
+#################################################################################
+
+name: Recheck Milestones
+
+# 'create' has no branch filter, so every branch creation starts a run of this
+# workflow. The guard below skips the job for anything but release/*, which is
+# why this is kept out of check-milestone.yml.
+on: [create]
+
+jobs:
+ recheck-open-prs:
+ name: Re-check open PRs after a release branch is cut
+ if: github.event.ref_type == 'branch' && startsWith(github.event.ref, 'release/')
+ runs-on: ubuntu-latest
+ permissions:
+ # 'actions: write' is needed to re-run the affected milestone checks.
+ actions: write
+ contents: read
+ pull-requests: read
+ steps:
+ - name: Checkout scripts
+ uses: actions/checkout@v6
+ with:
+ # A 'create' run defaults to the new branch; pin the default branch so
+ # the script is read from a known-good copy.
+ ref: ${{ github.event.repository.default_branch }}
+ # Only the scripts directory is needed; skip full history.
+ sparse-checkout: .github/scripts
+ sparse-checkout-cone-mode: false
+
+ - name: Re-check affected pull requests
+ env:
+ # Pass the ref via env to avoid script injection from branch names.
+ RELEASE_BRANCH: ${{ github.event.ref }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ WORKFLOW_FILE: check-milestone.yml
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: bash "${GITHUB_WORKSPACE}/.github/scripts/recheck-milestones-for-release-branch.sh"
diff --git a/.github/workflows/remove-author-attention-label.yml b/.github/workflows/remove-author-attention-label.yml
new file mode 100644
index 0000000000..808de9b9c2
--- /dev/null
+++ b/.github/workflows/remove-author-attention-label.yml
@@ -0,0 +1,83 @@
+name: Remove Author attention needed Label
+
+on:
+ issue_comment:
+ types: [created]
+
+jobs:
+ remove-label:
+ # Only run on PR comments with '/ready' from the PR author
+ if: >-
+ github.repository == 'dotnet/SqlClient' &&
+ github.event.issue.pull_request != null &&
+ github.event.comment.user.login == github.event.issue.user.login &&
+ startsWith(github.event.comment.body, '/ready')
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Remove 'Author attention needed' label
+ uses: actions/github-script@v9
+ with:
+ script: |
+ const labelName = 'Author attention needed';
+ const issue_number = context.issue.number;
+ const owner = context.repo.owner;
+ const repo = context.repo.repo;
+
+ // Check if the label exists on the PR
+ const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
+ owner,
+ repo,
+ issue_number,
+ per_page: 100,
+ });
+
+ const hasLabel = labels.some(label => label.name === labelName);
+
+ if (!hasLabel) {
+ core.info(`PR #${issue_number} does not have the '${labelName}' label. No action taken.`);
+ return;
+ }
+
+ await github.rest.issues.removeLabel({
+ owner,
+ repo,
+ issue_number,
+ name: labelName,
+ });
+ core.info(`Removed '${labelName}' label from PR #${issue_number}`);
+
+ // Re-request reviews from existing reviewers (paginate to include all reviews)
+ const reviews = await github.paginate(github.rest.pulls.listReviews, {
+ owner,
+ repo,
+ pull_number: issue_number,
+ per_page: 100,
+ });
+
+ // Collect unique reviewers (exclude the PR author)
+ const author = context.payload.issue.user.login;
+ const reviewers = [...new Set(
+ reviews
+ .filter(r => r.user?.type === 'User')
+ .map(r => r.user?.login)
+ .filter(login => login && login !== author)
+ )];
+
+ if (reviewers.length > 0) {
+ try {
+ await github.rest.pulls.requestReviewers({
+ owner,
+ repo,
+ pull_number: issue_number,
+ reviewers: reviewers.slice(0, 15),
+ });
+ core.info(`Re-requested reviews from: ${reviewers.join(', ')}`);
+ } catch (err) {
+ core.warning(`Failed to re-request reviewers: ${err.message}`);
+ }
+ } else {
+ core.info('No previous reviewers to re-request.');
+ }
diff --git a/.github/workflows/verify-aw-lock.yml b/.github/workflows/verify-aw-lock.yml
new file mode 100644
index 0000000000..eb3cc2e608
--- /dev/null
+++ b/.github/workflows/verify-aw-lock.yml
@@ -0,0 +1,32 @@
+name: Verify gh aw lock files
+
+on:
+ pull_request:
+ paths:
+ - '.github/workflows/**/*.md'
+ - '.github/workflows/**/*.lock.yml'
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install gh-aw extension
+ uses: github/gh-aw-actions/setup-cli@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2
+ with:
+ version: v0.88.2
+
+ - name: Recompile agentic workflows
+ run: gh aw compile
+
+ - name: Fail if any .lock.yml is out of date
+ run: |
+ if ! git diff --exit-code -- '.github/workflows/**/*.lock.yml'; then
+ echo "::error::One or more .github/workflows/**/*.lock.yml files are out of date relative to their .md source (running 'gh aw compile' produced a diff)."
+ echo "::error::Run 'gh aw compile' locally and commit the regenerated .lock.yml files in this PR."
+ exit 1
+ fi
diff --git a/.gitignore b/.gitignore
index 0d8dfed6d5..9289d0ab5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,4 @@
-## Ignore Visual Studio temporary files, build results, and
-## files generated by popular Visual Studio add-ons.
-##
-## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+## .gitignore for Microsoft.Data.SqlClient
# User-specific files
*.rsuser
@@ -10,12 +7,6 @@
*.userosscache
*.sln.docstates
-# User-specific files (MonoDevelop/Xamarin Studio)
-*.userprefs
-
-# Mono auto generated files
-mono_crash.*
-
# Build results
[Dd]ebug/
[Dd]ebugPublic/
@@ -29,14 +20,11 @@ bld/
[Bb]in/
[Oo]bj/
[Ll]og/
-.nuget/
-# Visual Studio 2015/2017 cache/options directory
+# Visual Studio cache/options directory
.vs/
-# Uncomment if you have tasks that create the project's static files in wwwroot
-#wwwroot/
-# Visual Studio 2017 auto generated files
+# Visual Studio auto generated files
Generated\ Files/
**/.AssemblyAttributes
@@ -44,34 +32,21 @@ Generated\ Files/
.vscode/*
!.vscode/mcp.json
-# MSTest test Results
+# MSTest test results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
-# NUnit
-*.VisualState.xml
-TestResult.xml
-nunit-*.xml
-
# TRX format test results
**/*.trx
-# Build Results of an ATL Project
-[Dd]ebugPS/
-[Rr]eleasePS/
-dlldata.c
-
-# Benchmark Results
+# Benchmark results
BenchmarkDotNet.Artifacts/
-# .NET Core
+# .NET project lock files and build artifacts
project.lock.json
project.fragment.lock.json
artifacts/
-# StyleCop
-StyleCopReport.xml
-
# Files built by Visual Studio
*_i.c
*_p.c
@@ -101,139 +76,36 @@ StyleCopReport.xml
*.svclog
*.scc
-# Chutzpah Test files
-_Chutzpah*
-
-# Visual C++ cache files
-ipch/
-*.aps
-*.ncb
-*.opendb
-*.opensdf
-*.sdf
-*.cachefile
-*.VC.db
-*.VC.VC.opendb
-
-# Visual Studio profiler
-*.psess
-*.vsp
-*.vspx
-*.sap
-
-# Visual Studio Trace Files
-*.e2e
-
-# TFS 2012 Local Workspace
-$tf/
-
-# Guidance Automation Toolkit
-*.gpState
-
-# ReSharper is a .NET coding add-in
+# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
-# JustCode is a .NET coding add-in
-.JustCode
-
-# TeamCity is a build add-in
-_TeamCity*
-
-# DotCover is a Code Coverage Tool
-*.dotCover
-
-# AxoCover is a Code Coverage Tool
-.axoCover/*
-!.axoCover/settings.json
-
# Visual Studio code coverage results
*.coverage
*.coveragexml
-# NCrunch
-_NCrunch_*
-.*crunch*.local.xml
-nCrunchTemp_*
-
-# MightyMoose
-*.mm.*
-AutoTest.Net/
-
-# Web workbench (sass)
-.sass-cache/
-
-# Installshield output folder
-[Ee]xpress/
-
-# DocProject is a documentation generator add-in
-DocProject/buildhelp/
-DocProject/Help/*.HxT
-DocProject/Help/*.HxC
-DocProject/Help/*.hhc
-DocProject/Help/*.hhk
-DocProject/Help/*.hhp
-DocProject/Help/Html2
-DocProject/Help/html
-
-# Click-Once directory
-publish/
-
-# Publish Web Output
-*.[Pp]ublish.xml
-*.azurePubxml
-# Note: Comment the next line if you want to checkin your web deploy settings,
-# but database connection strings (with potential passwords) will be unencrypted
-*.pubxml
-*.publishproj
-
-# Microsoft Azure Web App publish settings. Comment the next line if you want to
-# checkin your Azure Web App publish settings, but sensitive information contained
-# in these scripts will be unencrypted
-PublishScripts/
-
-# NuGet Packages
+# NuGet packages
*.nupkg
-# NuGet Symbol Packages
*.snupkg
-# Most of the packages folder can be ignored.
**/[Pp]ackages/*
!**/[Pp]ackages/.gitkeep
+.nuget/
-# Most of the output folder can be ignored.
+# Output folder
**/[Oo]utput/*
!**/[Oo]utput/.gitkeep
-# NuGet v3's project.json files produces more ignorable files
+# NuGet v3 project.json auxiliary files
*.nuget.props
*.nuget.targets
-# Microsoft Azure Build Output
-csx/
-*.build.csdef
-
-# Microsoft Azure Emulator
-ecf/
-rcf/
-
-# Windows Store app package directories and files
-AppPackages/
-BundleArtifacts/
-Package.StoreAssociation.xml
-_pkginfo.txt
-*.appx
-*.appxbundle
-*.appxupload
-
# Visual Studio cache files
-# files ending in .cache can be ignored
*.[Cc]ache
-# but keep track of directories ending in .cache
+# but keep directories ending in .cache
!?*.[Cc]ache/
-# Others
-ClientBin/
+# Temp and backup files
~$*
*~
*.dbmdl
@@ -241,138 +113,40 @@ ClientBin/
*.jfm
*.pfx
*.publishsettings
-orleans.codegen.cs
-
-# Including strong name files can present a security risk
-# (https://github.com/github/gitignore/pull/2483#issue-259490424)
-#*.snk
-
-# Since there are multiple workflows, uncomment next line to ignore bower_components
-# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
-#bower_components/
-# RIA/Silverlight projects
-Generated_Code/
-
-# Backup & report files from converting an old project file
-# to a newer Visual Studio version. Backup files are not needed,
-# because we have git ;-)
-_UpgradeReport_Files/
-Backup*/
-UpgradeLog*.XML
-UpgradeLog*.htm
-ServiceFabricBackup/
-*.rptproj.bak
-
-# SQL Server files
+# SQL Server data files
*.mdf
*.ldf
*.ndf
-# Business Intelligence projects
-*.rdl.data
-*.bim.layout
-*.bim_*.settings
-*.rptproj.rsuser
-*- [Bb]ackup.rdl
-*- [Bb]ackup ([0-9]).rdl
-*- [Bb]ackup ([0-9][0-9]).rdl
-
# Microsoft Fakes
FakesAssemblies/
-# GhostDoc plugin setting file
-*.GhostDoc.xml
-
-# Node.js Tools for Visual Studio
-.ntvs_analysis.dat
+# Node modules
node_modules/
-# Visual Studio 6 build log
-*.plg
-
-# Visual Studio 6 workspace options file
-*.opt
-
-# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
-*.vbw
-
-# Visual Studio LightSwitch build output
-**/*.HTMLClient/GeneratedArtifacts
-**/*.DesktopClient/GeneratedArtifacts
-**/*.DesktopClient/ModelManifest.xml
-**/*.Server/GeneratedArtifacts
-**/*.Server/ModelManifest.xml
-_Pvt_Extensions
-
-# Paket dependency manager
-.paket/paket.exe
-paket-files/
-
-# FAKE - F# Make
-.fake/
-
-# CodeRush personal settings
-.cr/personal
-
-# Python Tools for Visual Studio (PTVS)
-__pycache__/
-*.pyc
-
-# Cake - Uncomment if you are using it
-# tools/**
-# !tools/packages.config
-
-# Tabs Studio
-*.tss
-
-# Telerik's JustMock configuration file
-*.jmconfig
-
-# BizTalk build output
-*.btp.cs
-*.btm.cs
-*.odx.cs
-*.xsd.cs
-
-# OpenCover UI analysis results
-OpenCover/
-
-# Azure Stream Analytics local run output
-ASALocalRun/
-
-# MSBuild Binary and Structured Log
+# MSBuild binary and structured log
*.binlog
-# NVidia Nsight GPU debugger configuration file
-*.nvuser
-
-# MFractors (Xamarin productivity tool) working folder
-.mfractor/
-
# Local History for Visual Studio
.localhistory/
-# BeatPulse healthcheck temp database
-healthchecksdb
-
-# Backup folder for Package Reference Convert tool in Visual Studio 2017
-MigrationBackup/
-
-# JetBrains Rider (cross platform .NET IDE) working folder
+# JetBrains Rider
.idea/
-# Ionide (cross platform F# VS Code tools) working folder
-.ionide/
-
-# Nuget package files
-.nuget/
-
# Config Json file
**/config.json
+**/config.jsonc
# Generated Milestone PR metadata files
.milestone-prs/
# MDS "Not Supported" GenAPI code
**/notsupported/*.cs
+
+# C# language server cache
+*.lscache
+
+# Python bytecode caches
+__pycache__/
+*.py[cod]
diff --git a/.vscode/mcp.json b/.vscode/mcp.json
deleted file mode 100644
index a3f5378943..0000000000
--- a/.vscode/mcp.json
+++ /dev/null
@@ -1,75 +0,0 @@
-{
- "servers": {
- "ado": {
- "args": [
- "-y",
- "@azure-devops/mcp",
- "SqlClientDrivers"
- ],
- "command": "npx",
- "type": "stdio"
- },
- "bluebird_ctaip": {
- "headers": {
- "x-mcp-ec-branch": "certAuth",
- "x-mcp-ec-organization": "sqlclientdrivers",
- "x-mcp-ec-project": "ADO.NET",
- "x-mcp-ec-repository": "Microsoft.Data.SqlClient.Ctaip"
- },
- "type": "http",
- "url": "https://mcp.bluebird-ai.net/"
- },
- "bluebird_onebranchwiki": {
- "headers": {
- "x-mcp-ec-branch": "main",
- "x-mcp-ec-organization": "onebranch",
- "x-mcp-ec-project": "OneBranch Customer Wiki",
- "x-mcp-ec-repository": "OneBranch-Customer-Wiki.v2"
- },
- "type": "http",
- "url": "https://mcp.bluebird-ai.net/"
- },
- "bluebird-mcp-1es-docs": {
- "headers": {
- "x-mcp-ec-branch": "main",
- "x-mcp-ec-organization": "mseng",
- "x-mcp-ec-project": "1ES",
- "x-mcp-ec-repository": "1ES-on-EngHub"
- },
- "type": "http",
- "url": "https://mcp.bluebird-ai.net/"
- },
- "bluebird-mcp-sni": {
- "headers": {
- "x-mcp-ec-branch": "master",
- "x-mcp-ec-organization": "SqlClientDrivers",
- "x-mcp-ec-project": "ADO.NET",
- "x-mcp-ec-repository": "Microsoft.Data.SqlClient.SNI"
- },
- "type": "http",
- "url": "https://mcp.bluebird-ai.net/"
- },
- "bluebird-mcp-sqlclient": {
- "headers": {
- "x-mcp-ec-branch": "internal/main",
- "x-mcp-ec-organization": "SqlClientDrivers",
- "x-mcp-ec-project": "ADO.NET",
- "x-mcp-ec-repository": "dotnet-sqlclient"
- },
- "type": "http",
- "url": "https://mcp.bluebird-ai.net/"
- },
- "github": {
- "type": "http",
- "url": "https://api.githubcopilot.com/mcp/"
- },
- "icm": {
- "type": "http",
- "url": "https://icm-mcp-prod.azure-api.net/v1/"
- },
- "microsoft-learn": {
- "type": "http",
- "url": "https://learn.microsoft.com/api/mcp"
- }
- }
-}
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
index 56d7555c99..c461eec189 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,6 +5,7 @@ This document provides guidance for AI coding agents working with the Microsoft.
## Quick Start
### Essential Context Files
+
Before making changes, agents should be aware of:
| File | Purpose |
@@ -15,6 +16,7 @@ Before making changes, agents should be aware of:
| [.github/copilot-instructions.md](.github/copilot-instructions.md) | Copilot-specific instructions |
### Detailed Technical Instructions
+
The `.github/instructions/` directory contains comprehensive guides:
| Guide | Coverage |
@@ -29,6 +31,7 @@ The `.github/instructions/` directory contains comprehensive guides:
| [features.instructions.md](.github/instructions/features.instructions.md) | Feature reference, keywords |
| [documentation.instructions.md](.github/instructions/documentation.instructions.md) | Documentation and samples |
| [external-resources.instructions.md](.github/instructions/external-resources.instructions.md) | Docs links, version matrix, external references |
+| [ado-work-items-markdown.instructions.md](.github/instructions/ado-work-items-markdown.instructions.md) | Ensure Azure DevOps work item descriptions are Markdown and preserve newlines |
## Workflow Prompts
@@ -54,17 +57,40 @@ This repository provides reusable prompts in `.github/prompts/` for common maint
6. **Performance Optimization**: Use pooling, async, efficient allocations
7. **Observability**: EventSource tracing, meaningful errors
+## Terminal Reliability Rules
+
+When using shell/terminal tools, follow these rules strictly:
+
+1. Treat non-zero terminal exit codes as immediate failures to investigate; do not continue as if the command succeeded.
+2. If a bash session exits, assume it is dead and start a new command/session; do not wait for additional output from that session.
+3. After any command expected to gather data, verify output was actually returned before proceeding.
+4. If command execution failed, report the failure clearly and retry with a corrected command instead of waiting.
+5. Avoid `set -e` in this automation context; prefer single-purpose commands with explicit follow-up checks so failures are visible without killing the shell unexpectedly.
+6. Prefer shorter command batches over long chained scripts when collecting evidence; this makes bash exits easier to detect and recover from.
+
+## Branch Naming
+
+All branches created by AI agents **must** live under the `dev/automation/` prefix. Use a descriptive suffix, for example:
+
+- `dev/automation/fix-connection-timeout`
+- `dev/automation/add-json-type-tests`
+
+Do **not** create branches directly under `main`, `dev/`, or any other top-level prefix.
+
## Common Tasks
### Bug Fix Workflow
+
1. Understand the issue from the bug report
2. Locate relevant code in `src/Microsoft.Data.SqlClient/src/` (do NOT modify legacy `netcore/src/` or `netfx/src/`)
-3. Write a failing test that reproduces the issue
-4. Implement the fix
-5. Ensure all tests pass
-6. Update documentation if behavior changes
+3. Check `.github/instructions/features.instructions.md` for existing AppContext switches (including failover compatibility switches) before introducing behavior changes
+4. Write a failing test that reproduces the issue
+5. Implement the fix
+6. Ensure all tests pass
+7. Update documentation if behavior changes
### Feature Implementation
+
1. Review the feature specification
2. Plan the implementation (see `implement-feature` prompt)
3. Update reference assemblies if adding public APIs
@@ -73,6 +99,7 @@ This repository provides reusable prompts in `.github/prompts/` for common maint
6. Do not edit `CHANGELOG.md` directly; instead, add a suggested release-note entry (per `.github/copilot-instructions.md`) in the PR description or via the release-notes workflow/prompt.
### Adding Connection String Keywords
+
1. Add to `SqlConnectionStringBuilder`
2. Update connection string parser
3. Default to backward-compatible value
@@ -80,6 +107,7 @@ This repository provides reusable prompts in `.github/prompts/` for common maint
5. Document in feature reference
### Protocol Changes
+
1. Reference MS-TDS specification
2. Update `TdsEnums.cs` for new constants
3. Implement in `TdsParser.cs` and related files
@@ -87,6 +115,7 @@ This repository provides reusable prompts in `.github/prompts/` for common maint
5. Consider backward compatibility
### Performance Optimization
+
1. Profile the issue using benchmarks or traces
2. Identify allocation hotspots (see `perf-optimization` prompt)
3. Apply patterns: `ArrayPool`, `Span`, static/cached instances, source generation
@@ -96,6 +125,7 @@ This repository provides reusable prompts in `.github/prompts/` for common maint
### Key Documentation Links
+
- [Microsoft.Data.SqlClient on Microsoft Learn](https://learn.microsoft.com/sql/connect/ado-net/introduction-microsoft-data-sqlclient-namespace)
- [MS-TDS Protocol Specification](https://learn.microsoft.com/openspecs/windows_protocols/ms-tds)
- [SQL Server Documentation](https://learn.microsoft.com/sql/sql-server/)
@@ -103,6 +133,7 @@ This repository provides reusable prompts in `.github/prompts/` for common maint
## Repository Policies
See the `policy/` directory for:
+
- [coding-best-practices.md](policy/coding-best-practices.md) - Programming standards
- [coding-style.md](policy/coding-style.md) - Code formatting guidelines
- [review-process.md](policy/review-process.md) - PR review requirements
diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md
index 8f13730f68..72daa88ea8 100644
--- a/BUILDGUIDE.md
+++ b/BUILDGUIDE.md
@@ -1,224 +1,378 @@
-# Guidelines for Building Microsoft.Data.SqlClient
+
-This document provides all the necessary details to build the driver and run tests present in the repository.
+# Build Guide for Microsoft.Data.SqlClient and Related Packages
+
+This document provides details on how to build the Microsoft.Data.SqlClient package and the other related packages
+contained within this repository.
## Prerequisites
### .NET SDK
-The projects in this repo require the .NET 10.0 SDK to build. Please ensure you
-have the latest version of that SDK installed.
-
-Tests and tools may require different .NET Runtimes that may be installed
-independently. For example, tests targeting .NET 8.0 will need that runtime
-installed.
-
-### Visual Studio
-
-This project should be built with Visual Studio 2019+ for the best compatibility. The required set of components are provided in the below file:
-
-- **Visual Studio 2019** with imported components: [VS19Components](/tools/vsconfig/VS19Components.vsconfig)
-
-- **Powershell**: To build SqlClient on Linux, powershell is needed as well. Follow the distro specific instructions at [Install Powershell on Linux](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux?view=powershell-7.4)
-
-Once the environment is setup properly, execute the desired set of commands below from the _root_ folder to perform the respective operations:
-
-### Manual Test Prerequisites
-
-Manual Tests require the below setup to run:
-
-- SQL Server with enabled Shared Memory, TCP and Named Pipes Protocols and access to the Client OS.
-- Databases "NORTHWIND" and "UdtTestDb" present in SQL Server, created using SQL scripts [createNorthwindDb.sql](tools/testsql/createNorthwindDb.sql) and [createUdtTestDb.sql](tools/testsql/createUdtTestDb.sql). To setup an Azure Database with "NORTHWIND" tables, use SQL Script: [createNorthwindAzureDb.sql](tools/testsql/createNorthwindAzureDb.sql).
-- Make a copy of the configuration file [config.default.json](src/Microsoft.Data.SqlClient/tests/tools/Microsoft.Data.SqlClient.TestUtilities/config.default.json) and rename it to `config.json`. Update the values in `config.json`:
-
- |Property|Description|Value|
- |------|--------|-------------------|
- |TCPConnectionString | Connection String for a TCP enabled SQL Server instance. | `Server={servername};Database={Database_Name};Trusted_Connection=True;` OR `Data Source={servername};Initial Catalog={Database_Name};Integrated Security=True;`|
- |NPConnectionString | Connection String for a Named Pipes enabled SQL Server instance.| `Server=\\{servername}\pipe\sql\query;Database={Database_Name};Trusted_Connection=True;` OR `Data Source=np:{servername};Initial Catalog={Database_Name};Integrated Security=True;`|
- |TCPConnectionStringHGSVBS | (Optional) Connection String for a TCP enabled SQL Server with Host Guardian Service (HGS) attestation protocol configuration. | `Server=tcp:{servername}; Database={Database_Name}; UID={UID}; PWD={PWD}; Attestation Protocol = HGS; Enclave Attestation Url = {AttestationURL};`|
- |TCPConnectionStringNoneVBS | (Optional) Connection String for a TCP enabled SQL Server with a VBS Enclave and using None Attestation protocol configuration. | `Server=tcp:{servername}; Database={Database_Name}; UID={UID}; PWD={PWD}; Attestation Protocol = NONE;`|
- |TCPConnectionStringAASSGX | (Optional) Connection String for a TCP enabled SQL Server with a SGX Enclave and using Microsoft Azure Attestation (AAS) attestation protocol configuration. | `Server=tcp:{servername}; Database={Database_Name}; UID={UID}; PWD={PWD}; Attestation Protocol = AAS; Enclave Attestation Url = {AttestationURL};`|
- |EnclaveEnabled | Enables tests requiring an enclave-configured server.|
- |TracingEnabled | Enables EventSource related tests |
- |AADAuthorityURL | (Optional) Identifies the OAuth2 authority resource for `Server` specified in `AADPasswordConnectionString` | `https://login.windows.net/`, where `` is the tenant ID of the Entra ID (Azure AD) tenant |
- |AADPasswordConnectionString | (Optional) Connection String for testing Entra ID Password Authentication. | `Data Source={server.database.windows.net}; Initial Catalog={Azure_DB_Name};Authentication=Active Directory Password; User ID={AAD_User}; Password={AAD_User_Password};`|
- |AADSecurePrincipalId | (Optional) The Application Id of a registered application which has been granted permission to the database defined in the AADPasswordConnectionString. | {Application ID} |
- |AADSecurePrincipalSecret | (Optional) A Secret defined for a registered application which has been granted permission to the database defined in the AADPasswordConnectionString. | {Secret} |
- |AzureKeyVaultURL | (Optional) Azure Key Vault Identifier URL | `https://{keyvaultname}.vault.azure.net/` |
- |AzureKeyVaultTenantId | (Optional) The Entra ID tenant (directory) Id of the service principal. | _{Tenant ID of Active Directory}_ |
- |SupportsIntegratedSecurity | (Optional) Whether or not the USER running tests has integrated security access to the target SQL Server.| `true` OR `false`|
- |LocalDbAppName | (Optional) If Local Db Testing is supported, this property configures the name of Local DB App instance available in client environment. Empty string value disables Local Db testing. | Name of Local Db App to connect to.|
- |LocalDbSharedInstanceName | (Optional) If LocalDB testing is supported and the instance is shared, this property configures the name of the shared instance of LocalDB to connect to. | Name of shared instance of LocalDB. |
- |FileStreamDirectory | (Optional) If File Stream is enabled on SQL Server, pass local directory path to be used for setting up File Stream enabled database. | `D:\\escaped\\absolute\\path\\to\\directory\\` |
- |UseManagedSNIOnWindows | (Optional) Enables testing with Managed SNI on Windows| `true` OR `false`|
- |DNSCachingConnString | Connection string for a server that supports DNS Caching|
- |EnclaveAzureDatabaseConnString | (Optional) Connection string for Azure database with enclaves |
- |ManagedIdentitySupported | (Optional) When set to `false` **Managed Identity** related tests won't run. The default value is `true`. |
- |IsManagedInstance | (Optional) When set to `true` **TVP** related tests will use non-Azure bsl files to compare test results. This is needed when testing against Azure Managed Instances; otherwise TVP Tests will fail on TestSet 3. The default value is `false`. |
- |PowerShellPath | The full path to PowerShell.exe. This is not required if the path is present in the PATH environment variable. | `D:\\escaped\\absolute\\path\\to\\PowerShell.exe` |
-
-## MSBuild Reference
-
-### Targets
-
-The following build targets are defined in `build.proj`:
-
-|Target|Description|
-|-|-|
-|`BuildAbstractions`|Restore and build the Abstractions package.|
-|`BuildAkvProvider`|Builds the Azure Key Vault Provider package for all supported platforms.|
-|`BuildAllConfigurations`|Default target. Builds the .NET Framework and .NET drivers for all target frameworks and operating systems.|
-|`BuildAzure`|Restore and build the Azure package.|
-|`BuildLogging`|Restore and build the Logging package.|
-|`BuildNetCore`|Builds the .NET driver for all target frameworks.|
-|`BuildNetCoreAllOS`|Builds the .NET driver for all target frameworks and operating systems.|
-|`BuildNetFx`|Builds the .NET Framework driver for all target frameworks.|
-|`BuildSqlClient`|Build the driver for all target frameworks.|
-|`Clean`|Cleans all generated files.|
-|`PackAbstractions`|Pack the Abstractions NuGet package into `packages/`. Requires `BuildAbstractions` first.|
-|`PackAkvProvider`|Pack the Azure Key Vault Provider NuGet package (requires a prior build).|
-|`PackAzure`|Pack the Azure NuGet package into `packages/`. Requires `BuildAzure` first.|
-|`PackLogging`|Pack the Logging NuGet package into `packages/`. Requires `BuildLogging` first.|
-|`Restore`|Restores NuGet packages.|
-|`RunTests`|Runs the unit, functional, and manual tests for the .NET Framework and .NET drivers|
-|`RunUnitTests`|Runs just the unit tests for the .NET Framework and .NET drivers|
-|`RunFunctionalTests`|Runs just the functional tests for the .NET Framework and .NET drivers|
-|`RunManualTests`|Runs just the manual tests for the .NET Framework and .NET drivers|
-
-### Parameters
-
-The following parameters may be defined as MSBuild properties to configure the
-build:
-
-|Name|Supported Values|Default|Description|
-|-|-|-|-|
-|`Configuration`|`Debug`, `Release`|`Debug`|Sets the release configuration.|
-|`OSGroup`|`Unix`, `Windows_NT`, `AnyOS`|typically defaults to the client system's OS, unless using `BuildAllConfigurations` or an `AnyOS` specific target|The operating system to target.|
-|`Platform`|`AnyCPU`, `x86`, `x64`, `ARM`, `ARM64`|`AnyCPU`|May only be set when using package reference type or running tests.|
-|`TestSet`|`1`, `2`, `3`, `AE`, or any combination thereof|`''`|Build or run a subset of the manual tests. Omit (default) to run all tests.|
-|`DotnetPath`|Absolute file path to an installed `dotnet` version.|The system default specified by the path variable|Set to run tests using a specific dotnet version (e.g. C:\net6-win-x86\)|
-|`TF`|`net8.0`, `net462`, `net47`, `net471`, `net472`, `net48`, `net481`|`net9.0` in netcore, `net462` in netfx|Sets the target framework when building or running tests. Not applicable when building the drivers.|
-|`ResultsDirectory`|An absolute file path|./TestResults relative to current directory|Specifies where to write test results.|
-
-## Example Commands to Run Tests Using MSBuild (Recommended)
-
-Using the default configuration and running all tests:
+Projects in this repository require the .NET SDK to be installed in order to build. For the exact version required for
+building the current version, see [global.json](global.json). Downloads for .NET SDK can be found at
+[.NET Downloads](https://dotnet.microsoft.com/en-us/download/dotnet).
+
+The .NET SDK contains support for building for previous versions of .NET, including support for building .NET Framework
+on operating systems that do not support .NET Framework. As such, it is not necessary to install any version of the
+.NET SDK aside from the version specified in [global.json](global.json).
+
+### Miscellaneous
+
+**PowerShell** is included as a .NET local tool in this repository. Running `dotnet tool restore`
+(see below) will make it available via `dotnet tool run pwsh -- `. Note that `pwsh` is not
+added to PATH — it must be invoked through `dotnet tool run`. Build targets handle this
+automatically; manual invocation is only needed for ad-hoc scripting.
+
+The **NuGet** binary is optional for inspection and feed-management workflows, but build and packaging flows in this
+repository are run through `dotnet build` against `build.proj`.
+
+### .NET Tools
+
+This repository uses .NET local tools (e.g. PowerShell) that must be restored before building. Run the following from the repository root:
```bash
-msbuild -t:RunTests
+dotnet tool restore
```
-Using the Release configuration:
+## Developer Workflow
+
+Once you've cloned the repository and made your changes to the codebase, it is time to build, test, and optionally
+package the project. The `build.proj` file provides convenient targets to accomplish these tasks.
+
+> [!NOTE]
+> Although every effort has been made to make building and testing work in your IDE of choice, some quirks in behavior
+> may be noticed, possibly severe. All official build and test infrastructure uses the `build.proj` entrypoint, and it
+> is recommended that `build.proj` is used for local development, as well.
+
+
+
+> [!TIP]
+> This section is not exhaustive of all targets or parameters to `build.proj`. Complete documentation is available in
+> [`build.proj`](build.proj).
+
+### Building Projects
+
+From the root of your repository, run `dotnet build` against `build.proj` with a build target, following this pattern:
```bash
-msbuild -t:RunTests -p:Configuration=Release
+dotnet build build.proj -t: [optional_parameters]
```
-Running only the unit tests:
+Since `build.proj` is the only project file in the repo root, it can be omitted when building from
+the root:
```bash
-msbuild -t:RunUnitTests
+dotnet build -t: [optional_parameters]
```
-Using a specific .NET runtime to run tests:
+The command-line examples below will assume that `build.proj` is selected by default and will omit
+it from the `dotnet build` command.
+
+If no target is specified, `build.proj` runs the `BuildAll` target by default, which builds all
+projects, tests, samples, and tools for all supported OS combinations. To build only the driver
+projects, specify `-t:BuildDriver` explicitly.
+
+The following build targets can be used to build the following projects. All targets will implicitly build any other
+projects they depend on.
+
+| `` | Description |
+|-------------------------------|---------------------------------------------------------------------------------|
+| `BuildAbstractions` | Builds Microsoft.Data.SqlClient.Extensions.Abstractions |
+| `BuildAkvProvider` | Builds Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider |
+| `BuildAll` | Builds all projects, tests, samples, and tools for all supported OS combinations (default target) |
+| `BuildAzure` | Builds Microsoft.Data.SqlClient.Extensions.Azure |
+| `BuildDriver` | Builds all driver projects for all platforms |
+| `BuildLogging` | Builds Microsoft.Data.SqlClient.Internal.Logging |
+| `BuildSamples` | Builds the sample projects under `doc/samples/` |
+| `BuildSqlClient` | Builds all variants of Microsoft.Data.SqlClient, for all platforms |
+| `BuildSqlClientNotSupported` | Builds the "unsupported platform" assemblies for Microsoft.Data.SqlClient |
+| `BuildSqlClientRef` | Builds the reference assemblies for Microsoft.Data.SqlClient |
+| `BuildSqlClientUnix` | Builds the Unix-specific implementation binaries of Microsoft.Data.SqlClient |
+| `BuildSqlClientWindows` | Builds the Windows-specific implementation binaries of Microsoft.Data.SqlClient |
+| `BuildSqlServer` | Builds Microsoft.SqlServer.Server |
+| `BuildTests` | Builds all test projects for all supported OS combinations |
+| `BuildTools` | Builds auxiliary tool/app projects and their test projects |
+| `Clean` | Removes build and test output directories |
+
+A selection of parameters for build targets in `build.proj` can be found below:
+
+
+
+| `[optional_parameter]` | Allowed Values | Default | Description |
+|-----------------------------------|----------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| `-p:Configuration=` | `Debug`, `Release` | `Debug` | Build configuration |
+| `-p:PackageVersionSqlClient=` | `major.minor.patch[-prerelease]` | `[blank]` | Version to assign to the SqlClient family (`Microsoft.Data.SqlClient`, `Internal.Logging`, `Extensions.Abstractions`, `Extensions.Azure`, and the AKV Provider all share it). Assembly and file versions are derived from this, if it is provided. See Versioning for more details |
+| `-p:PackageVersionSqlServer=` | `major.minor.patch[-prerelease]` | `[blank]` | Version to assign to `Microsoft.SqlServer.Server`, which is versioned separately from the SqlClient family. |
+
+
+
+For most projects, build output is placed in `artifacts//Project-/`. ``
+is the full name of the package, `` is the build configuration, and `` is the target framework
+moniker. SqlClient deviates slightly from this convention, since it consists of multiple projects and the
+implementation project is OS-specific. Implementation project output is placed in
+`artifacts/Microsoft.Data.SqlClient/Project-//`. The unsupported platform assemblies are placed
+in `artifacts/Microsoft.Data.SqlClient.unsupported/Project-/`, and the reference assemblies are
+placed in `artifacts/Microsoft.Data.SqlClient.ref/Project-/`.
+
+#### Examples
+
+Build everything (all projects, tests, samples, and tools) using the default target:
```bash
-msbuild -t:RunTests -p:DotnetPath=C:\net8-win-x86\
+dotnet build
```
-To run tests against a specific version of .NET/.NET Framework, set the `-p:TF` parameter.
+Build only the driver projects:
```bash
-msbuild -t:RunTests -p:TF=net8.0
-msbuild -t:RunTests -p:TF=net462
+dotnet build -t:BuildDriver
```
-## Example Commands to Run Tests using `dotnet`
+Build Microsoft.Data.SqlClient in Release configuration:
+
+```bash
+dotnet build -t:BuildSqlClient -p:Configuration=Release
+```
-Under the hood, the MSBuild commands to run tests use `dotnet` commands. But, if you wish to run
-them without the overhead of wrapping/unwrapping in MSBuild, you can run them directly.
+Build a specific version of Microsoft.Data.SqlClient.Extensions.Abstractions (Abstractions is part of the
+SqlClient family, so its version is set via the family parameter `PackageVersionSqlClient`):
-To change the processor architecture that runs the test (where possible, ie, x86 on x64), use the
-appropriate `dotnet` executable.
+```bash
+dotnet build -t:BuildAbstractions -p:PackageVersionSqlClient=7.1.0
+```
-By default, the tests will be executed on all supported .NET/.NET framework versions. To run on a
-specific version, pass the `-f` parameter with the desired version (eg `net9.0`).
+### Testing Projects
-The `--filter` parameter is used to select which tests run. The default `category!=failing&
-category!=flaky&category!=interactive` prevents tests that are known to be failing or flaky from
-running. To run a specific test, use `FullyQualifiedName=[fully qualified name of the test method]`
-as the filter parameter. To run all possible tests, even known failing and flaky ones, simply omit
-the filter parameter. Please note, however, that this will still omit tests that cannot run on the
-current platform or with the current test configuration (eg, Windows tests on Linux, or SQL DB tests
-when Azure Synapse is configured).
+This section provides a summary and brief example of how to execute tests for projects in this repository. **For more
+information about test procedures, including config file setup, see [TESTGUIDE.md](TESTGUIDE.md).**
-### Run Functional Tests
+From the root of your repository, run `dotnet build` against `build.proj` with a test target, following this pattern:
```bash
-dotnet test "src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj" \
- -p:Configuration=Release \
- --filter "category!=failing&category!=flaky&category!=interactive"
+dotnet build -t: [optional_parameters]
+```
+
+| `` | Description |
+|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
+| `Test` | Runs all tests in the repository for all platforms supported by the host OS. _This will take a considerable amount of time and is not recommended_. |
+| `TestAbstractions` | Runs all tests for Microsoft.Data.SqlClient.Extensions.Abstractions |
+| `TestAkvProvider` | Runs the unit test project for Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider. |
+| `TestAzure` | Runs all tests for Microsoft.Data.SqlClient.Extensions.Azure |
+| `TestSqlClient` | Runs all tests for Microsoft.Data.SqlClient. |
+| `TestSqlClientFunctional` | Runs the "functional" test project for Microsoft.Data.SqlClient. These are a mix of unit and integration tests against live servers. |
+| `TestSqlClientManual` | Runs the "manual" test project for Microsoft.Data.SqlClient. These are generally integration tests against live servers. |
+| `TestSqlClientUnit` | Runs the unit test project for Microsoft.Data.SqlClient. These are a mix of unit tests and integration tests against simulated servers. |
+
+> [!TIP]
+> Test targets will automatically build the projects they depend on. Therefore, it is not necessary to explicitly build
+> (eg) SqlClient before executing the (eg) functional tests target.
+
+A selection of parameters for test targets in `build.proj` relevant to common developer workflows can be found below:
+
+
+| `[optional_parameter]` | Default Value | Description |
+|------------------------|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `-p:Configuration=` | `Debug` | Build configuration. Can be `Debug` or `Release`. |
+| `-p:DotnetPath=` | `[blank]` | Path to `dotnet` binary to run the test project. This is useful for running tests against x86 platform on a x86_64 machine. Path must end with `\` or `/`. |
+| `-p:TestBlameTimeout=` | `10m` | How long to wait on a test before timing it out. Use `0` to disable hang timeouts. |
+| `-p:TestFilters=` | `category!=failing&category!=flaky&category!=interactive` | Filters to use to select the xUnit tests to execute. Use `none` to run all possible tests. |
+| `-p:TestFramework=` | `[blank]` | Target framework moniker for the version of .NET to use to execute tests. |
+| `-p:TestSet=` | `[blank]` | The `TestSqlClientManual` project is very large and is split into multiple sets that can be executed individually. This parameter allows selecting between test sets: `1`, `2`, `3`, and `AE`. |
+
+
+
+#### Examples
+
+Run Microsoft.Data.SqlClient unit tests:
+
+```bash
+dotnet build -t:TestSqlClientUnit
```
-### Run Manual Tests
+Run Microsoft.Data.SqlClient manual test set 2:
```bash
-dotnet test "src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTests.csproj" \
- -p:Configuration=Release \
- --filter "category!=failing&category!=flaky&category!=interactive"
+dotnet build -t:TestSqlClientManual -p:TestSet=2
```
-### Run Unit Tests
+Run Microsoft.Data.SqlClient functional tests against x86 dotnet:
+
```bash
-dotnet test "src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft.Data.SqlClient.UnitTests.csproj" \
- -p:Configuration=Release \
- --filter "category!=failing&category!=flaky&category!=interactive"
+dotnet build -t:TestSqlClientFunctional -p:DotnetPath='C:\path\to\dotnet\x86\'
```
-## Testing with Package References
+Run all Microsoft.Data.SqlClient.Extensions.Azure unit tests, including interactive, but excluding failing tests:
-The MDS driver consists of several components, each of which produces its own
-NuGet package. During development, components reference each other via
-`` properties by default. This means that building
-and testing one component will implicitly build its project referenced
-dependencies.
+```bash
+dotnet build -t:TestAzure -p:TestFilters=category!=failing
+```
+
+Run Microsoft.Data.SqlClient functional tests against net8.0 runtime:
+
+```bash
+dotnet build -t:TestSqlClientFunctional -p:TestFramework=net8.0
+```
-Alternatively, the `ReferenceType` build property may be specified with a value
-of `Package`. This will change inter-component dependencies to use
-`` dependencies, and require that dependent components be
-built and packaged before building the depending component. This will generate NuGet
-packages in the root packages/ directory, and will be automatically searched by NuGet
-(see our root `NuGet.config`).
+### Packaging Projects
-Then, you can specify `Package` references be used, for example:
+Just like building and testing the various projects in this repository, packaging the projects into NuGet packages is
+also handled by `build.proj`. From the root of your repository, run `dotnet build` against `build.proj` with a pack target,
+following this pattern:
```bash
-dotnet build -t:BuildLogging,PackLogging
-dotnet build -t:BuildSqlServer,PackSqlServer
-dotnet build -t:BuildAbstractions,PackAbstractions -p:ReferenceType=Package
-dotnet build -t:BuildAzure,PackAzure -p:ReferenceType=Package
-dotnet build -t:BuildSqlClient -p:ReferenceType=Package
-dotnet build -t:GenerateMdsPackage
-dotnet build -t:BuildAKVNetCore -p:ReferenceType=Package
-dotnet build -t:GenerateAkvPackage
+dotnet build -t: [optional_parameters]
```
-The above will build the MDS and AKV components, place their NuGet packages into
-the `packages/` directory.
+| `` | Description |
+|--------------------|-------------------------------------------------------------------------------------|
+| `Pack` | Packages all projects in the repository. |
+| `PackAbstractions` | Packages the Microsoft.Data.SqlClient.Extensions.Abstractions package |
+| `PackAkvProvider` | Packages the Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider package |
+| `PackAzure` | Packages the Microsoft.Data.SqlClient.Extensions.Azure package |
+| `PackLogging` | Packages the Microsoft.Data.SqlClient.Internal.Logging package |
+| `PackSqlClient` | Packages the Microsoft.Data.SqlClient package |
+| `PackSqlServer` | Packages the Microsoft.SqlServer.Server package |
+
+> [!TIP]
+> For convenience, the Pack targets will automatically build the target project and any dependencies.
+
+A selection of parameters for pack targets in `build.proj` relevant to common developer workflows can be found below:
+
+
+
+| `[optional_parameter]` | Default Value | Allowed Values | Description |
+|------------------------------------|---------------|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `-p:Configuration=` | `Debug` | `Debug`, `Release` | Build configuration. Only applies if project and dependencies are being built. |
+| `-p:PackBuild=` | `true` | `true`, `false` | Whether or not to build the project before packing. If `false`, project must be built using the same parameters. |
+| `-p:PackageVersionSqlClient=` | `[blank]` | eg. `7.1.0-dev123` | Version to assign to the entire SqlClient family (`Microsoft.Data.SqlClient`, `Internal.Logging`, `Extensions.Abstractions`, `Extensions.Azure`, and the AKV Provider — they all share the SqlClient version). If `PackBuild` is `true`, the assembly and file versions are derived from this version. See Versioning for more details. |
+| `-p:PackageVersionSqlServer=` | `[blank]` | eg. `1.1.0-dev123` | Version to assign to `Microsoft.SqlServer.Server`, which is versioned separately from the SqlClient family. |
+
+
-A non-AnyCPU platform reference can only be used with package reference type.
-Otherwise, the specified platform will be replaced with AnyCPU in the build
-process.
+For `PackSqlClient`, the SqlClient nuspec pins its family dependencies (Abstractions and Logging) to the same `SqlClientPackageVersion` value, so a single `-p:PackageVersionSqlClient=` controls both the SqlClient package version and those dependency ranges. `Microsoft.SqlServer.Server` is pinned separately via `-p:PackageVersionSqlServer=`.
-### Running Tests with Reference Type
+If omitted, `PackSqlClient` computes these versions from `Versions.props` using the current `BuildNumber` and `BuildSuffix` context.
+
+#### Examples
+
+Package Microsoft.Data.SqlClient.Internal.Logging into a NuGet package:
+
+```bash
+dotnet build -t:PackLogging
+```
-Provide property to `dotnet test` commands for testing desired reference type.
+Package Microsoft.Data.SqlClient:
```bash
-dotnet test -p:ReferenceType=Project ...
+dotnet build -t:PackSqlClient
```
+Package a specific version of Microsoft.Data.SqlClient.Extensions.Abstractions (set via the family parameter
+`PackageVersionSqlClient`):
+
+```bash
+dotnet build -t:PackAbstractions -p:PackageVersionSqlClient=7.1.0
+```
+
+Package Microsoft.Data.SqlClient.Extensions.Azure without building it beforehand:
+
+```bash
+dotnet build -t:PackAzure -p:PackBuild=false
+```
+
+## Versioning
+
+Versioning can be accomplished by using a mix of different parameters to the `build.proj` targets:
+`PackageVersionSqlClient` (or `PackageVersionSqlServer`), `BuildNumber`, and `BuildSuffix`. Using these in different
+combinations can generate appropriate package, assembly, and file versions for different scenarios. For most developer
+workflows, it is not necessary to specify any of these parameters - appropriate versions based on the latest release
+will be generated automatically. This section primarily exists to document the various parameters, their effects, and
+the scenarios they can be useful for.
+
+All packages in the **SqlClient family** (`Microsoft.Data.SqlClient`, `Internal.Logging`, `Extensions.Abstractions`,
+`Extensions.Azure`, and the AKV Provider) share a single version, set via `-p:PackageVersionSqlClient`.
+`Microsoft.SqlServer.Server` is versioned separately via `-p:PackageVersionSqlServer`.
+
+The SqlClient family version is defined in `src/Microsoft.Data.SqlClient/Versions.props` (and SqlServer's in its own
+`Versions.props`), which declares a "default" version — the next version to release. For the table below, we assume this
+is "1.2.3".
+
+| `PackageVersion` | `BuildNumber` | `BuildSuffix` | Package Version | Assembly Version | File Version | Scenario |
+|------------------|---------------|---------------|------------------|------------------|---------------|------------------------------------------------------------|
+| N/A | N/A | N/A | `1.2.3-dev` | `1.0.0` | `1.2.3.0` | Standard developer scenario |
+| `9.8.7` | N/A | N/A | `9.8.7` | `9.0.0` | `9.8.7.0` | Developer is building a specific version of the package |
+| `9.8.7-preview1` | N/A | N/A | `9.8.7-preview1` | `9.0.0` | `9.8.7.0` | Developer is building a pre-release version of the package |
+| N/A | `1234` | N/A | `1.2.3` | `1.0.0` | `1.2.3.1234` | Automated pipelines building GA releases |
+| N/A | `1234` | `ci` | `1.2.3-ci1234` | `1.0.0` | `1.2.3.1234` | Automated pipelines building non-prod releases |
+
+---
+
+## Package Mode Builds
+
+The above documentation is the default mode of operation, and is the recommended mode for most developers. However,
+`build.proj` supports "package mode" builds. In this mode, instead of projects depending on other projects, they
+depend on NuGet packages. This mode is useful for verifying that packages work with each other, especially in automated
+build scenarios. For completeness, and debugging of automated builds, this section documents behavior of "package mode".
+
+To switch to "package mode", set the `ReferenceType` parameter in `build.proj` to `Package`. And, optionally, include
+one or both of the following parameters:
+
+- `PackageVersionSqlClient` — the version for the entire SqlClient family.
+- `PackageVersionSqlServer` — the version for `Microsoft.SqlServer.Server`.
+
+These parameters pull double duty. In targets where a package is being built, the parameter sets the version of the
+package. In targets where a package is being referenced, the parameter sets the version of the referenced package.
+Because the SqlClient family shares one version, `PackageVersionSqlClient` covers every family package, whether it is
+being built or referenced.
+
+If these parameters are not specified, the latest version, as defined in the `Versions.props` file, will be used.
+
+The `nuget.config` for this repository defines a local feed that points to the `packages` directory. This allows
+developers that need to test against development packages to drop their development packages into this directory, and
+run subsequent `build.proj` targets against them.
+
+### Examples
+
+Build Microsoft.Data.SqlClient version 7.1.1 in package mode. Because all SqlClient family packages share the same
+version, a single `-p:PackageVersionSqlClient=7.1.1` applies to SqlClient and its family dependencies (Abstractions and
+Logging).
+
+Build v7.1.1 of Logging and copy to packages:
+
+```bash
+dotnet build -t:PackLogging -p:ReferenceType=Package -p:PackageVersionSqlClient=7.1.1
+cp artifacts/Microsoft.Data.SqlClient.Internal.Logging/Debug/*.*pkg packages/
+```
+
+Build v7.1.1 of Abstractions (which depends on v7.1.1 of Logging):
+
+```bash
+dotnet build -t:PackAbstractions \
+ -p:ReferenceType=Package \
+ -p:PackageVersionSqlClient=7.1.1
+cp artifacts/Microsoft.Data.SqlClient.Extensions.Abstractions/Package-Debug/*.*pkg packages/
+```
+
+Build SqlClient:
+
+```bash
+dotnet build -t:PackSqlClient \
+ -p:ReferenceType=Package \
+ -p:PackageVersionSqlClient=7.1.1
+cp artifacts/Microsoft.Data.SqlClient/Package-Debug/*.*pkg packages/
+```
+
+Run Microsoft.Data.SqlClient functional tests against the versions built above:
+
+```bash
+dotnet build -t:TestSqlClientFunctional \
+ -p:ReferenceType=Package \
+ -p:PackageVersionSqlClient=7.1.1
+```
+
+Manual test prerequisites and configuration are covered in [TESTGUIDE.md](TESTGUIDE.md#manual-test-prerequisites).
## Using Managed SNI on Windows
@@ -274,10 +428,14 @@ PowerShell:
Bash:
+
+
```bash
$ cd src/Microsoft.Data.SqlClient/tests/PerformanceTests
```
+
+
### Create Database
Create an empty database for the benchmarks to use. This example assumes
@@ -290,13 +448,14 @@ $ sqlcmd -S localhost -U sa -P password
1> quit
```
-The default `runnerconfig.json` expects a database named `sqlclient-perf-db`,
-but you may change the config to use any existing database. All tables in
-the database will be dropped when running the benchmarks.
+The default `runnerconfig.jsonc` expects a database named `sqlclient-perf-db`,
+but you may change the config to use any existing database. The benchmarks
+create and drop their own tables (typically prefixed with `perf_`) in this
+database; other existing tables are left untouched.
### Configure Runner
-Configure the benchmarks by editing the `runnerconfig.json` file directly in the
+Configure the benchmarks by editing the `runnerconfig.jsonc` file directly in the
`PerformanceTests` directory with an appropriate connection string and benchmark
settings:
@@ -304,6 +463,9 @@ settings:
{
"ConnectionString": "Server=tcp:localhost; Integrated Security=true; Initial Catalog=sqlclient-perf-db;",
"UseManagedSniOnWindows": false,
+ "UseOptimizedAsyncBehaviour": true,
+ "WaitForProfiler": false,
+ "UseNativeMemoryAndETWProfiler": false,
"Benchmarks":
{
"SqlConnectionRunnerConfig":
@@ -323,36 +485,51 @@ settings:
Individual benchmarks may be enabled or disabled, and each has several
benchmarking options for fine tuning.
-After making edits to `runnerconfig.json` you must perform a build which will
+The top-level flags control global runner behavior:
+
+| Flag | Description |
+| --- | --- |
+| `UseManagedSniOnWindows` | Enables the managed SNI implementation on Windows instead of native SNI. |
+| `UseOptimizedAsyncBehaviour` | Enables packet multiplexing and other async optimizations in SqlClient. |
+| `WaitForProfiler` | Pauses at startup and prints the process ID so you can attach an external profiler (e.g. `dotnet-trace`) before benchmarks run. |
+| `UseNativeMemoryAndETWProfiler` | Attaches the `NativeMemoryProfiler` and `EtwProfiler` BenchmarkDotNet diagnosers. Windows only; has no effect on other OSes. |
+
+Some benchmarks (e.g. `DataTypeReaderRunner`) also
+read per-type test values from `datatypes.json` in the `PerformanceTests`
+directory. Like `runnerconfig.jsonc`, this file's location can be overridden
+with the `DATATYPES_CONFIG` environment variable.
+
+After making edits to `runnerconfig.jsonc` you must perform a build which will
copy the file into the `artifacts` directory alongside the benchmark DLL. By
-default, the benchmarks look for `runnerconfig.json` in the same directory as
+default, the benchmarks look for `runnerconfig.jsonc` in the same directory as
the DLL.
-Optionally, to avoid polluting your git workspace and requring a build after
-each config change, copy `runnerconfig.json` to a new file, make your edits
+Optionally, to avoid polluting your git workspace and requiring a build after
+each config change, copy `runnerconfig.jsonc` to a new file, make your edits
there, and then specify the new file with the RUNNER_CONFIG environment
-variable.
+variable. The same approach works for `datatypes.json` via the
+`DATATYPES_CONFIG` environment variable.
PowerShell:
```pwsh
-> copy runnerconfig.json $HOME\.configs\runnerconfig.json
+> copy runnerconfig.jsonc $HOME\.configs\runnerconfig.jsonc
-# Make edits to $HOME\.configs\runnerconfig.json
+# Make edits to $HOME\.configs\runnerconfig.jsonc
# You must set the RUNNER_CONFIG environment variable for the current shell.
-> $env:RUNNER_CONFIG="${HOME}\.configs\runnerconfig.json"
+> $env:RUNNER_CONFIG="${HOME}\.configs\runnerconfig.jsonc"
```
Bash:
```bash
-$ cp runnerconfig.json ~/.configs/runnerconfig.json
+$ cp runnerconfig.jsonc ~/.configs/runnerconfig.jsonc
-# Make edits to ~/.configs/runnerconfig.json
+# Make edits to ~/.configs/runnerconfig.jsonc
# Optionally export RUNNER_CONFIG.
-$ export RUNNER_CONFIG=~/.configs/runnerconfig.json
+$ export RUNNER_CONFIG=~/.configs/runnerconfig.jsonc
```
### Run Benchmarks
@@ -372,5 +549,5 @@ Bash:
# copy prepared by the build.
$ dotnet run -c Release -f net9.0
-$ RUNNER_CONFIG=~/.configs/runnerconfig.json dotnet run -c Release -f net9.0
+$ RUNNER_CONFIG=~/.configs/runnerconfig.jsonc dotnet run -c Release -f net9.0
```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc51c7d2bc..3901428151 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,597 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
-
> **Note:** Releases are sorted in reverse chronological order (newest first).
+## [Stable Release 7.0.3] - 2026-09-10
+
+### Changed
+
+- Updated the `Microsoft.Data.SqlClient.SNI` and `Microsoft.Data.SqlClient.SNI.runtime` dependencies to 6.0.3 (was 6.0.2).
+ ([#4599](https://github.com/dotnet/SqlClient/pull/4599))
+
+### Fixed
+
+- Fixed a `SqlBulkCopy` regression in environments where the application login cannot read `sys.all_columns`. Bulk copy now falls back to the earlier column-discovery behavior when that permission is unavailable. Support for hidden columns and SQL Graph column aliases still requires access to the metadata view.
+ ([#4370](https://github.com/dotnet/SqlClient/issues/4370), [#4306](https://github.com/dotnet/SqlClient/pull/4306), [#4402](https://github.com/dotnet/SqlClient/pull/4402))
+
+- Fixed a memory-allocation regression in connection and command operations caused by formatting diagnostic strings even when tracing was disabled. Also corrected trace messages that reported an incorrect object ID or could throw `FormatException` when traced values contained braces.
+ ([#4528](https://github.com/dotnet/SqlClient/pull/4528), [#4533](https://github.com/dotnet/SqlClient/pull/4533))
+
+- Fixed `ServerCertificate` validation on the managed SNI path so the configured certificate is compared against the server certificate even when the server certificate passes chain and host-name validation. When certificate validation is enabled, a missing, unreadable, or invalid certificate file, a certificate mismatch, or a missing server certificate now causes the TLS handshake to fail instead of bypassing the configured certificate check. (net8.0/net9.0 only)
+ ([#4445](https://github.com/dotnet/SqlClient/pull/4445), [#4583](https://github.com/dotnet/SqlClient/pull/4583))
+
+- Fixed Always Encrypted VSM/HGS enclave attestation to verify that the enclave public key used to establish a session matches the key committed to by the signed attestation report. Missing, malformed, or mismatched key-binding data now causes attestation to fail before the session secret is derived.
+ ([#4532](https://github.com/dotnet/SqlClient/pull/4532), [#4553](https://github.com/dotnet/SqlClient/pull/4553))
+
+- Fixed `SqlConnection.AccessTokenCallback` not disabling Transparent Network IP Resolution by default, making it consistent with `SqlConnection.AccessToken`. An explicitly configured `TransparentNetworkIPResolution` connection-string value still takes precedence. (net462 only)
+ ([#4520](https://github.com/dotnet/SqlClient/pull/4520), [#4561](https://github.com/dotnet/SqlClient/pull/4561))
+
+- Fixed authentication state handling so clearing `SqlConnection.AccessToken`, `AccessTokenCallback`, or `SspiContextProvider` preserves the other authentication values in the connection pool key. Cloning a connection or updating its credential also preserves its `SspiContextProvider`. Combining a non-null `SspiContextProvider` with `AccessToken` or `AccessTokenCallback` now throws `InvalidOperationException` instead of silently discarding authentication state; applications must use one authentication mechanism at a time.
+ ([#4520](https://github.com/dotnet/SqlClient/pull/4520), [#4561](https://github.com/dotnet/SqlClient/pull/4561), [#4644](https://github.com/dotnet/SqlClient/pull/4644))
+
+- Fixed configurable retry logic installing a permanent, process-wide assembly-resolution handler that could interfere with unrelated assembly loading. The handler is now active only while an explicitly configured custom retry provider is resolved and constructed, and probes `AppContext.BaseDirectory` instead of the current working directory. Place custom retry assemblies in the application base directory; dependencies loaded after provider construction must be resolvable through normal application dependency resolution or an application-provided handler. (net8.0/net9.0 only)
+ ([#2214](https://github.com/dotnet/SqlClient/issues/2214), [#4547](https://github.com/dotnet/SqlClient/pull/4547), [#4663](https://github.com/dotnet/SqlClient/pull/4663))
+
+### Companion packages
+
+- Released `Microsoft.Data.SqlClient.Extensions.Azure` 7.0.3 with the Entra ID authority parsing fix for Dataverse/Dynamics 365 connections. See [release notes](release-notes/Extensions/Azure/7.0/7.0.3.md).
+- Released version-aligned `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`, `Microsoft.Data.SqlClient.Extensions.Abstractions`, and `Microsoft.Data.SqlClient.Internal.Logging` 7.0.3 with no functional or API changes. See the [Azure Key Vault provider](release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.3.md), [Abstractions](release-notes/Extensions/Abstractions/7.0/7.0.3.md), and [Logging](release-notes/Internal/Logging/7.0/7.0.3.md) release notes.
+
+## [Stable Release 6.1.7] - 2026-09-10
+
+### Changed
+
+- Updated the `Microsoft.Data.SqlClient.SNI` and `Microsoft.Data.SqlClient.SNI.runtime` dependencies to 6.0.3 (was 6.0.2).
+ ([#4598](https://github.com/dotnet/SqlClient/pull/4598))
+
+### Fixed
+
+- Fixed `ServerCertificate` validation on the managed SNI path so the configured certificate is compared against the server certificate even when the server certificate passes chain and host-name validation. When certificate validation is enabled, a missing, unreadable, or invalid certificate file, a certificate mismatch, or a missing server certificate now causes the TLS handshake to fail instead of bypassing the configured certificate check. (net8.0/net9.0 only)
+ ([#4445](https://github.com/dotnet/SqlClient/pull/4445), [#4584](https://github.com/dotnet/SqlClient/pull/4584))
+
+- Fixed Always Encrypted VSM/HGS enclave attestation to verify that the enclave public key used to establish a session matches the key committed to by the signed attestation report. Missing, malformed, or mismatched key-binding data now causes attestation to fail before the session secret is derived.
+ ([#4532](https://github.com/dotnet/SqlClient/pull/4532), [#4552](https://github.com/dotnet/SqlClient/pull/4552))
+
+- Fixed `SqlConnection.AccessTokenCallback` not disabling Transparent Network IP Resolution by default, making it consistent with `SqlConnection.AccessToken`. An explicitly configured `TransparentNetworkIPResolution` connection-string value still takes precedence. (net462 only)
+ ([#4520](https://github.com/dotnet/SqlClient/pull/4520), [#4560](https://github.com/dotnet/SqlClient/pull/4560))
+
+- Fixed token authentication state handling so clearing `SqlConnection.AccessToken` preserves an existing `AccessTokenCallback` in the connection pool key, and clearing `AccessTokenCallback` preserves an existing `AccessToken`. Callback-based authentication now also follows the same prelogin server-certificate validation rules as an explicitly supplied access token.
+ ([#4520](https://github.com/dotnet/SqlClient/pull/4520), [#4560](https://github.com/dotnet/SqlClient/pull/4560))
+
+- Fixed configurable retry logic installing a permanent, process-wide assembly-resolution handler that could interfere with unrelated assembly loading. The handler is now active only while an explicitly configured custom retry provider is resolved and constructed, and probes `AppContext.BaseDirectory` instead of the current working directory. Place custom retry assemblies in the application base directory; dependencies loaded after provider construction must be resolvable through normal application dependency resolution or an application-provided handler. (net8.0/net9.0 only)
+ ([#2214](https://github.com/dotnet/SqlClient/issues/2214), [#4547](https://github.com/dotnet/SqlClient/pull/4547), [#4664](https://github.com/dotnet/SqlClient/pull/4664))
+
+## [Preview Release 7.1.0-preview3] - 2026-08-26
+
+This update brings the following changes since the [7.1.0-preview2](release-notes/7.1/7.1.0-preview2.md) release.
+See the [full release notes](release-notes/7.1/7.1.0-preview3.md) for detailed descriptions.
+
+> **Important — package version alignment:** Starting with the [7.0.2](release-notes/7.0/7.0.2.md) release, the `Microsoft.Data.SqlClient` driver and its companion packages share a single aligned version. Preview 3 of the `7.1` line continues this alignment; the following packages now ship together as `7.1.0-preview3`:
+>
+> - `Microsoft.Data.SqlClient`
+> - `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`
+> - `Microsoft.Data.SqlClient.Extensions.Azure`
+> - `Microsoft.Data.SqlClient.Extensions.Abstractions`
+> - `Microsoft.Data.SqlClient.Internal.Logging`
+>
+> (`Microsoft.SqlServer.Server` continues to version independently and remains at `1.0.0`.)
+>
+> Applications that reference `Microsoft.Data.SqlClient.Extensions.Azure` must upgrade it to `7.1.0-preview3` when upgrading `Microsoft.Data.SqlClient` to `7.1.0-preview3`.
+>
+> **Compatibility guarantee:** All aligned assemblies ship with `FileVersion 7.1.0.x` and `AssemblyVersion 7.0.0.0`. The `AssemblyVersion` is unchanged from [7.0.2](release-notes/7.0/7.0.2.md), so upgrading from `7.0.2` to `7.1.0-preview3` does **not** require any new .NET Framework strong-name binding redirects.
+
+### Added
+
+- Added four `virtual` asynchronous counterparts to the synchronous methods on `SqlColumnEncryptionKeyStoreProvider` — `DecryptColumnEncryptionKeyAsync`, `EncryptColumnEncryptionKeyAsync`, `SignColumnMasterKeyMetadataAsync`, and `VerifyColumnMasterKeyMetadataAsync` — each accepting an optional `CancellationToken`. The default implementations delegate to the existing synchronous methods, so existing custom providers are unaffected. These APIs are introduced for provider authors but are not consumed by the driver yet; a future release will enable their use from the driver's own asynchronous APIs.
+ ([#3673](https://github.com/dotnet/SqlClient/pull/3673))
+
+- Implemented those four asynchronous APIs in `SqlColumnEncryptionAzureKeyVaultProvider`, calling the Azure SDK's own asynchronous methods and flowing the supplied `CancellationToken`. Concurrent cache misses for the same key are gated so a burst of callers issues a single Key Vault request. See the [AKV release notes](release-notes/add-ons/AzureKeyVaultProvider/7.1/7.1.0-preview3.md) for the `VerifyColumnMasterKeyMetadata` signature-validation behavior change and the 7.1 runtime requirement.
+ ([#4540](https://github.com/dotnet/SqlClient/pull/4540))
+
+- Substantially expanded `ChannelDbConnectionPool` (the opt-in pool behind `Switch.Microsoft.Data.SqlClient.UseConnectionPoolV2`) to parity with the default pool: transaction support, broken-connection replacement, leaked-connection reclamation, background warmup and replenishment to `Min Pool Size`, idle pruning driven by `Connection Idle Timeout`, optional connection-creation rate limiting, and metrics/tracing parity. Default pooling behavior is unchanged.
+ ([#4395](https://github.com/dotnet/SqlClient/pull/4395),
+ [#4396](https://github.com/dotnet/SqlClient/pull/4396),
+ [#4429](https://github.com/dotnet/SqlClient/pull/4429),
+ [#4452](https://github.com/dotnet/SqlClient/pull/4452),
+ [#4463](https://github.com/dotnet/SqlClient/pull/4463),
+ [#4487](https://github.com/dotnet/SqlClient/pull/4487),
+ [#4504](https://github.com/dotnet/SqlClient/pull/4504),
+ [#4529](https://github.com/dotnet/SqlClient/pull/4529))
+
+### Changed
+
+- The driver now builds a single cross-platform assembly. Package structure and contents are unchanged, and Windows-only native SNI types now trim cleanly on Linux and macOS.
+ ([#4207](https://github.com/dotnet/SqlClient/pull/4207),
+ [#4465](https://github.com/dotnet/SqlClient/pull/4465),
+ [#4474](https://github.com/dotnet/SqlClient/pull/4474))
+
+- Reduced managed allocations in the async read path by restoring reuse of `PacketData` nodes via a bounded free list on `StateSnapshot`. This applies to the default async read path and is not gated behind any AppContext switch.
+ ([#4536](https://github.com/dotnet/SqlClient/pull/4536))
+
+- Operations no longer allocate a formatted trace string when `SqlClientEventSource` tracing is disabled, recovering a memory regression against the 6.1.6 baseline. Trace output with tracing enabled is unchanged.
+ ([#4528](https://github.com/dotnet/SqlClient/pull/4528))
+
+- Updated centrally managed dependency versions for the `net9.0` target framework to `9.0.18`, and added `System.Threading.RateLimiting` to the packaged dependency metadata. Non-`net9.0` targets keep their existing `8.0.x` pins.
+ ([#4507](https://github.com/dotnet/SqlClient/pull/4507))
+
+- Updated the `Microsoft.Data.SqlClient.SNI` and `Microsoft.Data.SqlClient.SNI.runtime` dependencies to `7.1.0-preview3.26226.3`.
+ ([#4564](https://github.com/dotnet/SqlClient/pull/4564))
+
+- Bypassed SQL Graph column alias mapping in `SqlBulkCopy` when neither the source nor destination table contains graph pseudo-columns, recovering a bulk copy performance regression. Graph table bulk copy behavior is unchanged.
+ ([#4535](https://github.com/dotnet/SqlClient/pull/4535))
+
+- Re-shipped `Microsoft.Data.SqlClient.Extensions.Azure` as `7.1.0-preview3`, fixing Entra ID tenant parsing for multi-segment authorities. See [release notes](release-notes/Extensions/Azure/7.1/7.1.0-preview3.md).
+
+- Re-shipped `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` as `7.1.0-preview3` with new asynchronous key store provider APIs, and `Microsoft.Data.SqlClient.Extensions.Abstractions` and `Microsoft.Data.SqlClient.Internal.Logging` as `7.1.0-preview3` (version alignment only, no functional changes). See release notes for [AKV](release-notes/add-ons/AzureKeyVaultProvider/7.1/7.1.0-preview3.md), [Abstractions](release-notes/Extensions/Abstractions/7.1/7.1.0-preview3.md), and [Logging](release-notes/Internal/Logging/7.1/7.1.0-preview3.md).
+
+### Fixed
+
+- Fixed Always Encrypted VSM/HGS enclave attestation not verifying that the enclave public key used to establish the session matches the key committed to by the signed attestation report.
+ ([#4532](https://github.com/dotnet/SqlClient/pull/4532))
+
+- Fixed a `SqlConnectionFactory` timer that woke the process every 30 seconds for the lifetime of the application even when no connection pools existed, including with `Pooling=False` and after `ClearAllPools()`.
+ ([#4479](https://github.com/dotnet/SqlClient/pull/4479))
+
+- Fixed connection pool performance counter defects affecting the default pool as well as pool V2. `active-soft-connects` and `number-of-active-connections` could go negative after a failed connection activation, and several gauges drifted upward permanently after a broken connection was replaced.
+ ([#4504](https://github.com/dotnet/SqlClient/pull/4504))
+
+- Fixed `OverflowException` when sending large `decimal` values as a parameter with explicit `Precision` and `Scale`, which primarily affected Always Encrypted scenarios.
+ ([#4443](https://github.com/dotnet/SqlClient/pull/4443))
+
+- Fixed a TDS stream error when passing a `DateOnly` value as a parameter with `SqlDbType.Variant`.
+ ([#4294](https://github.com/dotnet/SqlClient/pull/4294))
+
+- Fixed `DateOnly` values written to a `sql_variant` column of a table-valued parameter being sent as `datetime` instead of `date`, which also caused an overflow for values out of `datetime` range.
+ ([#4439](https://github.com/dotnet/SqlClient/pull/4439))
+
+- Fixed the `ServerCertificate` connection-string keyword not being honored when the platform reported no TLS policy errors, and made an unloadable certificate file fail closed instead of silently falling back to host-name validation.
+ ([#4445](https://github.com/dotnet/SqlClient/pull/4445))
+
+- Fixed `SqlConnection.AccessTokenCallback` not disabling Transparent Network IP Resolution by default, unlike `SqlConnection.AccessToken`, along with a related connection pool key defect.
+ ([#4520](https://github.com/dotnet/SqlClient/pull/4520))
+
+- Fixed several async entry points in `SqlBulkCopy`, `SqlDataReader`, and `SqlCommand` that captured fatal exceptions such as `OutOfMemoryException` into faulted `Task`s instead of letting them propagate.
+ ([#4437](https://github.com/dotnet/SqlClient/pull/4437))
+
+- Fixed `Authentication=Active Directory Service Principal` (and the other Entra ID flows) failing against endpoints that return a multi-segment authority such as the Dataverse / Dynamics 365 TDS endpoint; the tenant id is now taken from the first path segment of the STSURL authority instead of the last. Ships in `Microsoft.Data.SqlClient.Extensions.Azure`.
+ ([#4521](https://github.com/dotnet/SqlClient/pull/4521))
+
+## [Preview Release 7.1.0-preview2] - 2026-07-09
+
+This update brings the following changes since the [7.1.0-preview1](release-notes/7.1/7.1.0-preview1.md) release.
+See the [full release notes](release-notes/7.1/7.1.0-preview2.md) for detailed descriptions.
+
+> **Important — package version alignment:** Starting with the [7.0.2](release-notes/7.0/7.0.2.md) release, the `Microsoft.Data.SqlClient` driver and its companion packages share a single aligned version. Preview 2 of the `7.1` line continues this alignment; the following packages now ship together as `7.1.0-preview2`:
+>
+> - `Microsoft.Data.SqlClient`
+> - `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`
+> - `Microsoft.Data.SqlClient.Extensions.Azure`
+> - `Microsoft.Data.SqlClient.Extensions.Abstractions`
+> - `Microsoft.Data.SqlClient.Internal.Logging`
+>
+> (`Microsoft.SqlServer.Server` continues to version independently and remains at `1.0.0`.)
+>
+> Applications that reference `Microsoft.Data.SqlClient.Extensions.Azure` must upgrade it to `7.1.0-preview2` when upgrading `Microsoft.Data.SqlClient` to `7.1.0-preview2`.
+>
+> **Compatibility guarantee:** All aligned assemblies ship with `FileVersion 7.1.0.x` and `AssemblyVersion 7.0.0.0`. The `AssemblyVersion` is unchanged from [7.0.2](release-notes/7.0/7.0.2.md), so upgrading from `7.0.2` to `7.1.0-preview2` does **not** require any new .NET Framework strong-name binding redirects. See the 7.0.2 release notes for the original `AssemblyVersion` alignment (the one-time breaking change that raised `AssemblyVersion` from `1.0.0.0` to `7.0.0.0` for `Extensions.Azure`, `Extensions.Abstractions`, and `Internal.Logging`; `AzureKeyVaultProvider` was already on `7.x`).
+
+### Added
+
+- Added `SqlConnection.GetSchemaAsync` overloads mirroring the existing synchronous shapes.
+ ([#3005](https://github.com/dotnet/SqlClient/pull/3005))
+
+- Added SQL Graph column-alias support (`$node_id`, `$edge_id`, `$from_id`, `$to_id`) as destination columns in `SqlBulkCopy`.
+ ([#3677](https://github.com/dotnet/SqlClient/pull/3677))
+
+- `SqlBatchCommand.CommandBehavior` (a driver-specific property that existed since batching was introduced but was previously ignored) is now honored during `SqlBatch` execution, and `SqlBatch.ExecuteReader` now respects the `CommandBehavior` value passed to it.
+ ([#4125](https://github.com/dotnet/SqlClient/pull/4125))
+
+- Added a `Connection Idle Timeout` connection-string keyword and matching `SqlConnectionStringBuilder.IdleTimeout` property to evict idle pooled connections (default `300` seconds; `0` disables). Enforcement is opt-in via `Switch.Microsoft.Data.SqlClient.UseLegacyIdleTimeoutBehavior=false`; the default preserves the historical pooling behavior. When enabled, idle-timeout enforcement applies to the existing connection pool as well.
+ ([#4295](https://github.com/dotnet/SqlClient/pull/4295))
+
+### Changed
+
+- The `Connect Timeout` budget can now be propagated through pool acquisition via a shared `TimeoutTimer`, so time spent waiting in the pool is deducted from the overall timeout. Enforcement is opt-in via `Switch.Microsoft.Data.SqlClient.UseOverallConnectTimeoutForPoolWait=true`; the default (`false`) preserves the historical behavior where pool waits do not count against `Connect Timeout`. Adds a dependency on `Microsoft.Bcl.TimeProvider`.
+ ([#4270](https://github.com/dotnet/SqlClient/pull/4270))
+
+- Hardened `SqlConnection` internal state transitions with `Interlocked.CompareExchange` guards.
+ ([#4267](https://github.com/dotnet/SqlClient/pull/4267))
+
+- Removed legacy connection-options inheritance from internal APIs and simplified `TryOpenConnection` / `TryReplaceConnection` / pool interfaces.
+ ([#4235](https://github.com/dotnet/SqlClient/pull/4235),
+ [#4237](https://github.com/dotnet/SqlClient/pull/4237),
+ [#4261](https://github.com/dotnet/SqlClient/pull/4261),
+ [#4415](https://github.com/dotnet/SqlClient/pull/4415))
+
+- Added the SQL Server 2025 `json` data type to the `DataTypes` schema table returned by `SqlConnection.GetSchema`.
+ ([#3858](https://github.com/dotnet/SqlClient/pull/3858))
+
+- Added async generic helpers to reduce sync/async duplication.
+ ([#4334](https://github.com/dotnet/SqlClient/pull/4334))
+
+- Use hardcoded LCID mappings when decoding strings.
+ ([#4212](https://github.com/dotnet/SqlClient/pull/4212))
+
+- Reduced allocations by skipping lock acquisition on `SqlErrorCollection` counters when no errors exist, and by avoiding stack-trace materialization on expected `null`-return paths.
+ ([#4099](https://github.com/dotnet/SqlClient/pull/4099),
+ [#4102](https://github.com/dotnet/SqlClient/pull/4102),
+ [#4157](https://github.com/dotnet/SqlClient/pull/4157))
+
+- Improved accuracy of `EnclaveDiffieHellmanInfo.Size`.
+ ([#4346](https://github.com/dotnet/SqlClient/pull/4346))
+
+- `SqlVector` now serializes and deserializes multibyte values as little-endian explicitly.
+ ([#3861](https://github.com/dotnet/SqlClient/pull/3861))
+
+- Updated the bundled .NET 10 SDK to `10.0.300`.
+ ([#4287](https://github.com/dotnet/SqlClient/pull/4287))
+
+- Re-shipped `Microsoft.Data.SqlClient.Extensions.Azure` as `7.1.0-preview2`, adding Windows Account Manager (WAM) broker support for Entra ID authentication on Windows. See [release notes](release-notes/Extensions/Azure/7.1/7.1.0-preview2.md).
+
+- Re-shipped `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`, `Microsoft.Data.SqlClient.Extensions.Abstractions`, and `Microsoft.Data.SqlClient.Internal.Logging` as `7.1.0-preview2` (version alignment only, no functional changes). See release notes for [AKV](release-notes/add-ons/AzureKeyVaultProvider/7.1/7.1.0-preview2.md), [Abstractions](release-notes/Extensions/Abstractions/7.1/7.1.0-preview2.md), and [Logging](release-notes/Internal/Logging/7.1/7.1.0-preview2.md).
+
+### Fixed
+
+- Fixed a `NullReferenceException` in `SqlCommand.Cancel()` when the active connection has already been torn down.
+ ([#4372](https://github.com/dotnet/SqlClient/pull/4372))
+
+- Fixed Always Encrypted column master key signature verification incorrectly reusing cached results after a prior verification failure.
+ ([#4339](https://github.com/dotnet/SqlClient/pull/4339))
+
+- Fixed missing bounds checks on TDS token and feature-extension-acknowledgment data lengths that could allow a spoofing server to trigger unbounded allocations.
+ ([#4340](https://github.com/dotnet/SqlClient/pull/4340))
+
+- Fixed `SqlBulkCopy` failing in least-privilege environments.
+ ([#4306](https://github.com/dotnet/SqlClient/pull/4306))
+
+- Fixed Always Encrypted reads of `CekMdVersion` and `EkValueCount` to align with the TDS specification.
+ ([#4240](https://github.com/dotnet/SqlClient/pull/4240))
+
+- Fixed `LoginWithFailover` to validate parser state before continuing.
+ ([#4140](https://github.com/dotnet/SqlClient/pull/4140))
+
+- Fixed the SPN used during login to use the resolved port instead of the instance name when `Protocol=None` or `Protocol=Admin`.
+ ([#4180](https://github.com/dotnet/SqlClient/pull/4180))
+
+- Fixed a race in `SqlConnection.TryOpenInner` that could surface as `InvalidCastException`; the same race now returns a deterministic `InvalidOperationException`.
+ ([#4179](https://github.com/dotnet/SqlClient/pull/4179))
+
+- Fixed several `CancellationTokenSource` leaks across `SqlDataReader`, `SqlConnection` reconnect, `SqlCommand` reconnect timeout, and sequential-stream helpers.
+ ([#4009](https://github.com/dotnet/SqlClient/pull/4009))
+
+- Fixed server certificate documentation.
+ ([#4408](https://github.com/dotnet/SqlClient/pull/4408))
+
+### Removed
+
+- **Breaking:** Removed SQL Server 7.0 and SQL Server 2000 support, along with the `TypeSystem.SQLServer2000` enum value and the `Type System Version=SQL Server 2000` connection-string value. Connection strings that specify this value now throw `ArgumentException` when the connection is opened. Supported values are `Latest`, `SQL Server 2005`, `SQL Server 2008`, and `SQL Server 2012`.
+ ([#4015](https://github.com/dotnet/SqlClient/pull/4015))
+
+## [Stable Release 7.0.2] - 2026-06-24
+
+This update brings the following changes since the [7.0.1](release-notes/7.0/7.0.1.md) release.
+See the [full release notes](release-notes/7.0/7.0.2.md) for detailed descriptions.
+
+> **Important — package version alignment:** Starting with 7.0.2, the `Microsoft.Data.SqlClient` driver and its companion packages share a single aligned version. The following packages now ship together as `7.0.2`:
+>
+> - `Microsoft.Data.SqlClient`
+> - `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`
+> - `Microsoft.Data.SqlClient.Extensions.Azure`
+> - `Microsoft.Data.SqlClient.Extensions.Abstractions`
+> - `Microsoft.Data.SqlClient.Internal.Logging`
+>
+> (`Microsoft.SqlServer.Server` continues to version independently and remains at `1.0.0`.)
+>
+> Applications must reference the same versions of `Microsoft.Data.SqlClient` and its extensions for best compatibility. In particular, applications that reference `Microsoft.Data.SqlClient.Extensions.Azure` must upgrade it to `7.0.2` when upgrading `Microsoft.Data.SqlClient` to `7.0.2`.
+
+> **Breaking change (.NET Framework only):** As part of this alignment, the `AssemblyVersion` of `Microsoft.Data.SqlClient.Extensions.Azure`, `Microsoft.Data.SqlClient.Extensions.Abstractions`, and `Microsoft.Data.SqlClient.Internal.Logging` changed from `1.0.0.0` to `7.0.0.0`. On .NET Framework, `AssemblyVersion` is part of the strong-name identity, so applications that drop these assemblies into an existing deployment without rebuilding must rebuild against the 7.0.2 packages (or add binding redirects). Applications on .NET / .NET Core are not affected. `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` already used a `7.x` assembly version and is unaffected.
+
+### Fixed
+
+- Fixed a `NullReferenceException` in `SqlCommand.Cancel()` when the active connection has already been torn down.
+ ([#4372](https://github.com/dotnet/SqlClient/pull/4372),
+ [#4373](https://github.com/dotnet/SqlClient/pull/4373))
+
+- Fixed a `NullReferenceException` in `SqlDataReader.GetBytes`/`GetChars` when called with a `null` destination buffer.
+ ([#4159](https://github.com/dotnet/SqlClient/pull/4159),
+ [#4206](https://github.com/dotnet/SqlClient/pull/4206))
+
+- Fixed Always Encrypted column master key signature verification incorrectly reusing cached results.
+ ([#4339](https://github.com/dotnet/SqlClient/pull/4339),
+ [#4343](https://github.com/dotnet/SqlClient/pull/4343))
+
+### Changed
+
+- Hardened TDS token parsing by adding data-length bounds checks for token and feature-extension-acknowledgment data.
+ ([#4340](https://github.com/dotnet/SqlClient/pull/4340),
+ [#4358](https://github.com/dotnet/SqlClient/pull/4358))
+
+- Released `Microsoft.Data.SqlClient.Extensions.Azure 7.0.2`, adding WAM broker support for Entra ID authentication modes on Windows. See [release notes](release-notes/Extensions/Azure/7.0/7.0.2.md).
+ ([#4288](https://github.com/dotnet/SqlClient/pull/4288),
+ [#4388](https://github.com/dotnet/SqlClient/pull/4388))
+
+- Re-shipped `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider`, `Microsoft.Data.SqlClient.Extensions.Abstractions`, and `Microsoft.Data.SqlClient.Internal.Logging` as `7.0.2` (version alignment only, no functional changes). See release notes for [AKV](release-notes/add-ons/AzureKeyVaultProvider/7.0/7.0.2.md), [Abstractions](release-notes/Extensions/Abstractions/7.0/7.0.2.md), and [Logging](release-notes/Internal/Logging/7.0/7.0.2.md).
+
+## [Stable Release 6.1.6] - 2026-06-24
+
+This update brings the following changes since the [6.1.5](release-notes/6.1/6.1.5.md) release.
+See the [full release notes](release-notes/6.1/6.1.6.md) for detailed descriptions.
+
+### Added
+
+- Added Web Account Manager (WAM) broker support for the supported Entra ID authentication modes (Windows only), including the new `ActiveDirectoryAuthenticationProviderOptions` type with a `UseWamBroker` property, an `ActiveDirectoryAuthenticationProvider(ActiveDirectoryAuthenticationProviderOptions options)` constructor overload, and a cross-platform `SetParentActivityOrWindowFunc(Func