dunstan 0.1.15 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -7
- package/dist/dunstan.mjs +1676 -430
- package/package.json +1 -1
package/dist/dunstan.mjs
CHANGED
|
@@ -101,7 +101,7 @@ var require_code = __commonJS({
|
|
|
101
101
|
}
|
|
102
102
|
exports._ = _;
|
|
103
103
|
var plus = new _Code("+");
|
|
104
|
-
function
|
|
104
|
+
function str4(strs, ...args) {
|
|
105
105
|
const expr = [safeStringify(strs[0])];
|
|
106
106
|
let i = 0;
|
|
107
107
|
while (i < args.length) {
|
|
@@ -112,7 +112,7 @@ var require_code = __commonJS({
|
|
|
112
112
|
optimize(expr);
|
|
113
113
|
return new _Code(expr);
|
|
114
114
|
}
|
|
115
|
-
exports.str =
|
|
115
|
+
exports.str = str4;
|
|
116
116
|
function addCodeArg(code, arg) {
|
|
117
117
|
if (arg instanceof _Code)
|
|
118
118
|
code.push(...arg._items);
|
|
@@ -155,7 +155,7 @@ var require_code = __commonJS({
|
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
function strConcat(c1, c2) {
|
|
158
|
-
return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 :
|
|
158
|
+
return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str4`${c1}${c2}`;
|
|
159
159
|
}
|
|
160
160
|
exports.strConcat = strConcat;
|
|
161
161
|
function interpolate(x) {
|
|
@@ -859,9 +859,9 @@ var require_codegen = __commonJS({
|
|
|
859
859
|
forOf(nameOrPrefix, iterable, forBody, varKind = scope_1.varKinds.const) {
|
|
860
860
|
const name = this._scope.toName(nameOrPrefix);
|
|
861
861
|
if (this.opts.es5) {
|
|
862
|
-
const
|
|
863
|
-
return this.forRange("_i", 0, (0, code_1._)`${
|
|
864
|
-
this.var(name, (0, code_1._)`${
|
|
862
|
+
const arr3 = iterable instanceof code_1.Name ? iterable : this.var("_arr", iterable);
|
|
863
|
+
return this.forRange("_i", 0, (0, code_1._)`${arr3}.length`, (i) => {
|
|
864
|
+
this.var(name, (0, code_1._)`${arr3}[${i}]`);
|
|
865
865
|
forBody(name);
|
|
866
866
|
});
|
|
867
867
|
}
|
|
@@ -869,12 +869,12 @@ var require_codegen = __commonJS({
|
|
|
869
869
|
}
|
|
870
870
|
// `for-in` statement.
|
|
871
871
|
// With option `ownProperties` replaced with a `for-of` loop for object keys
|
|
872
|
-
forIn(nameOrPrefix,
|
|
872
|
+
forIn(nameOrPrefix, obj3, forBody, varKind = this.opts.es5 ? scope_1.varKinds.var : scope_1.varKinds.const) {
|
|
873
873
|
if (this.opts.ownProperties) {
|
|
874
|
-
return this.forOf(nameOrPrefix, (0, code_1._)`Object.keys(${
|
|
874
|
+
return this.forOf(nameOrPrefix, (0, code_1._)`Object.keys(${obj3})`, forBody);
|
|
875
875
|
}
|
|
876
876
|
const name = this._scope.toName(nameOrPrefix);
|
|
877
|
-
return this._for(new ForIter("in", varKind, name,
|
|
877
|
+
return this._for(new ForIter("in", varKind, name, obj3), () => forBody(name));
|
|
878
878
|
}
|
|
879
879
|
// end `for` loop
|
|
880
880
|
endFor() {
|
|
@@ -1060,9 +1060,9 @@ var require_util = __commonJS({
|
|
|
1060
1060
|
exports.checkStrictMode = exports.getErrorPath = exports.Type = exports.useFunc = exports.setEvaluated = exports.evaluatedPropsToName = exports.mergeEvaluated = exports.eachItem = exports.unescapeJsonPointer = exports.escapeJsonPointer = exports.escapeFragment = exports.unescapeFragment = exports.schemaRefOrVal = exports.schemaHasRulesButRef = exports.schemaHasRules = exports.checkUnknownRules = exports.alwaysValidSchema = exports.toHash = void 0;
|
|
1061
1061
|
var codegen_1 = require_codegen();
|
|
1062
1062
|
var code_1 = require_code();
|
|
1063
|
-
function toHash(
|
|
1063
|
+
function toHash(arr3) {
|
|
1064
1064
|
const hash2 = {};
|
|
1065
|
-
for (const item of
|
|
1065
|
+
for (const item of arr3)
|
|
1066
1066
|
hash2[item] = true;
|
|
1067
1067
|
return hash2;
|
|
1068
1068
|
}
|
|
@@ -1117,22 +1117,22 @@ var require_util = __commonJS({
|
|
|
1117
1117
|
return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
|
|
1118
1118
|
}
|
|
1119
1119
|
exports.schemaRefOrVal = schemaRefOrVal;
|
|
1120
|
-
function unescapeFragment(
|
|
1121
|
-
return unescapeJsonPointer(decodeURIComponent(
|
|
1120
|
+
function unescapeFragment(str4) {
|
|
1121
|
+
return unescapeJsonPointer(decodeURIComponent(str4));
|
|
1122
1122
|
}
|
|
1123
1123
|
exports.unescapeFragment = unescapeFragment;
|
|
1124
|
-
function escapeFragment(
|
|
1125
|
-
return encodeURIComponent(escapeJsonPointer(
|
|
1124
|
+
function escapeFragment(str4) {
|
|
1125
|
+
return encodeURIComponent(escapeJsonPointer(str4));
|
|
1126
1126
|
}
|
|
1127
1127
|
exports.escapeFragment = escapeFragment;
|
|
1128
|
-
function escapeJsonPointer(
|
|
1129
|
-
if (typeof
|
|
1130
|
-
return `${
|
|
1131
|
-
return
|
|
1128
|
+
function escapeJsonPointer(str4) {
|
|
1129
|
+
if (typeof str4 == "number")
|
|
1130
|
+
return `${str4}`;
|
|
1131
|
+
return str4.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
1132
1132
|
}
|
|
1133
1133
|
exports.escapeJsonPointer = escapeJsonPointer;
|
|
1134
|
-
function unescapeJsonPointer(
|
|
1135
|
-
return
|
|
1134
|
+
function unescapeJsonPointer(str4) {
|
|
1135
|
+
return str4.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
1136
1136
|
}
|
|
1137
1137
|
exports.unescapeJsonPointer = unescapeJsonPointer;
|
|
1138
1138
|
function eachItem(xs, f) {
|
|
@@ -2157,8 +2157,8 @@ var require_json_schema_traverse = __commonJS({
|
|
|
2157
2157
|
post(schema, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
|
|
2158
2158
|
}
|
|
2159
2159
|
}
|
|
2160
|
-
function escapeJsonPtr(
|
|
2161
|
-
return
|
|
2160
|
+
function escapeJsonPtr(str4) {
|
|
2161
|
+
return str4.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2162
2162
|
}
|
|
2163
2163
|
}
|
|
2164
2164
|
});
|
|
@@ -2694,11 +2694,11 @@ var require_validate = __commonJS({
|
|
|
2694
2694
|
if (!this.allErrors)
|
|
2695
2695
|
this.gen.if(cond);
|
|
2696
2696
|
}
|
|
2697
|
-
setParams(
|
|
2697
|
+
setParams(obj3, assign) {
|
|
2698
2698
|
if (assign)
|
|
2699
|
-
Object.assign(this.params,
|
|
2699
|
+
Object.assign(this.params, obj3);
|
|
2700
2700
|
else
|
|
2701
|
-
this.params =
|
|
2701
|
+
this.params = obj3;
|
|
2702
2702
|
}
|
|
2703
2703
|
block$data(valid, codeBlock, $dataValid = codegen_1.nil) {
|
|
2704
2704
|
this.gen.block(() => {
|
|
@@ -3255,10 +3255,10 @@ var require_utils = __commonJS({
|
|
|
3255
3255
|
isIPV6: true
|
|
3256
3256
|
};
|
|
3257
3257
|
}
|
|
3258
|
-
function findToken(
|
|
3258
|
+
function findToken(str4, token) {
|
|
3259
3259
|
let ind = 0;
|
|
3260
|
-
for (let i = 0; i <
|
|
3261
|
-
if (
|
|
3260
|
+
for (let i = 0; i < str4.length; i++) {
|
|
3261
|
+
if (str4[i] === token) ind++;
|
|
3262
3262
|
}
|
|
3263
3263
|
return ind;
|
|
3264
3264
|
}
|
|
@@ -4272,7 +4272,7 @@ var require_core = __commonJS({
|
|
|
4272
4272
|
var util_1 = require_util();
|
|
4273
4273
|
var $dataRefSchema = require_data();
|
|
4274
4274
|
var uri_1 = require_uri();
|
|
4275
|
-
var defaultRegExp = (
|
|
4275
|
+
var defaultRegExp = (str4, flags) => new RegExp(str4, flags);
|
|
4276
4276
|
defaultRegExp.code = "new RegExp";
|
|
4277
4277
|
var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
|
|
4278
4278
|
var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -5067,16 +5067,16 @@ var require_ucs2length = __commonJS({
|
|
|
5067
5067
|
"node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/runtime/ucs2length.js"(exports) {
|
|
5068
5068
|
"use strict";
|
|
5069
5069
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5070
|
-
function ucs2length(
|
|
5071
|
-
const len =
|
|
5070
|
+
function ucs2length(str4) {
|
|
5071
|
+
const len = str4.length;
|
|
5072
5072
|
let length = 0;
|
|
5073
5073
|
let pos = 0;
|
|
5074
5074
|
let value;
|
|
5075
5075
|
while (pos < len) {
|
|
5076
5076
|
length++;
|
|
5077
|
-
value =
|
|
5077
|
+
value = str4.charCodeAt(pos++);
|
|
5078
5078
|
if (value >= 55296 && value <= 56319 && pos < len) {
|
|
5079
|
-
value =
|
|
5079
|
+
value = str4.charCodeAt(pos);
|
|
5080
5080
|
if ((value & 64512) === 56320)
|
|
5081
5081
|
pos++;
|
|
5082
5082
|
}
|
|
@@ -7534,8 +7534,8 @@ var require_formats = __commonJS({
|
|
|
7534
7534
|
}
|
|
7535
7535
|
var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
|
|
7536
7536
|
var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
7537
|
-
function date5(
|
|
7538
|
-
const matches = DATE.exec(
|
|
7537
|
+
function date5(str4) {
|
|
7538
|
+
const matches = DATE.exec(str4);
|
|
7539
7539
|
if (!matches)
|
|
7540
7540
|
return false;
|
|
7541
7541
|
const year = +matches[1];
|
|
@@ -7554,8 +7554,8 @@ var require_formats = __commonJS({
|
|
|
7554
7554
|
}
|
|
7555
7555
|
var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
|
|
7556
7556
|
function getTime(strictTimeZone) {
|
|
7557
|
-
return function time3(
|
|
7558
|
-
const matches = TIME.exec(
|
|
7557
|
+
return function time3(str4) {
|
|
7558
|
+
const matches = TIME.exec(str4);
|
|
7559
7559
|
if (!matches)
|
|
7560
7560
|
return false;
|
|
7561
7561
|
const hr = +matches[1];
|
|
@@ -7601,8 +7601,8 @@ var require_formats = __commonJS({
|
|
|
7601
7601
|
var DATE_TIME_SEPARATOR = /t|\s/i;
|
|
7602
7602
|
function getDateTime(strictTimeZone) {
|
|
7603
7603
|
const time3 = getTime(strictTimeZone);
|
|
7604
|
-
return function date_time(
|
|
7605
|
-
const dateTime =
|
|
7604
|
+
return function date_time(str4) {
|
|
7605
|
+
const dateTime = str4.split(DATE_TIME_SEPARATOR);
|
|
7606
7606
|
return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
|
|
7607
7607
|
};
|
|
7608
7608
|
}
|
|
@@ -7627,13 +7627,13 @@ var require_formats = __commonJS({
|
|
|
7627
7627
|
}
|
|
7628
7628
|
var NOT_URI_FRAGMENT = /\/|:/;
|
|
7629
7629
|
var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
|
7630
|
-
function uri(
|
|
7631
|
-
return NOT_URI_FRAGMENT.test(
|
|
7630
|
+
function uri(str4) {
|
|
7631
|
+
return NOT_URI_FRAGMENT.test(str4) && URI.test(str4);
|
|
7632
7632
|
}
|
|
7633
7633
|
var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
|
|
7634
|
-
function byte(
|
|
7634
|
+
function byte(str4) {
|
|
7635
7635
|
BYTE.lastIndex = 0;
|
|
7636
|
-
return BYTE.test(
|
|
7636
|
+
return BYTE.test(str4);
|
|
7637
7637
|
}
|
|
7638
7638
|
var MIN_INT32 = -(2 ** 31);
|
|
7639
7639
|
var MAX_INT32 = 2 ** 31 - 1;
|
|
@@ -7647,11 +7647,11 @@ var require_formats = __commonJS({
|
|
|
7647
7647
|
return true;
|
|
7648
7648
|
}
|
|
7649
7649
|
var Z_ANCHOR = /[^\\]\\Z/;
|
|
7650
|
-
function regex(
|
|
7651
|
-
if (Z_ANCHOR.test(
|
|
7650
|
+
function regex(str4) {
|
|
7651
|
+
if (Z_ANCHOR.test(str4))
|
|
7652
7652
|
return false;
|
|
7653
7653
|
try {
|
|
7654
|
-
new RegExp(
|
|
7654
|
+
new RegExp(str4);
|
|
7655
7655
|
return true;
|
|
7656
7656
|
} catch (e) {
|
|
7657
7657
|
return false;
|
|
@@ -8030,7 +8030,7 @@ import { readFileSync as readFileSync8 } from "node:fs";
|
|
|
8030
8030
|
import { createHash } from "node:crypto";
|
|
8031
8031
|
|
|
8032
8032
|
// src/spec/constants.ts
|
|
8033
|
-
var SPEC_VERSION = "0.1.
|
|
8033
|
+
var SPEC_VERSION = "0.1.11";
|
|
8034
8034
|
var BLOCK_VERSION = "0.1";
|
|
8035
8035
|
var SUPPORTED_BLOCK_VERSIONS = [BLOCK_VERSION];
|
|
8036
8036
|
var BLOCK_INFO_STRING = "dunstan-handback";
|
|
@@ -8038,6 +8038,8 @@ var STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
|
8038
8038
|
var PREDICATE_TYPE = "https://barglabs.ai/dunstan/record/v0.1";
|
|
8039
8039
|
var DRAFT_SPEC_VERSION = "0.2.0-draft";
|
|
8040
8040
|
var DRAFT_PREDICATE_TYPE = "https://barglabs.ai/dunstan/record/v0.2-draft";
|
|
8041
|
+
var GITLAB_DRAFT_SPEC_VERSION = `${SPEC_VERSION}-gitlab-0.1-draft`;
|
|
8042
|
+
var GITLAB_DRAFT_PREDICATE_TYPE = "https://barglabs.ai/dunstan/record/v0.1-gitlab-draft";
|
|
8041
8043
|
var BLOCK_SUBJECT_NAME = "handback-block";
|
|
8042
8044
|
var SIGNATURE_NAMESPACE = "dunstan-record";
|
|
8043
8045
|
var MAX_BLOCK_DEPTH = 32;
|
|
@@ -8344,11 +8346,11 @@ function unreadReason(unread2) {
|
|
|
8344
8346
|
function absentSection(section) {
|
|
8345
8347
|
return `evidence_field_unpopulated:${section}`;
|
|
8346
8348
|
}
|
|
8347
|
-
function toSecond(
|
|
8348
|
-
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?Z$/.test(
|
|
8349
|
-
throw new Error(`not an RFC 3339 UTC timestamp: ${
|
|
8349
|
+
function toSecond(timestamp3) {
|
|
8350
|
+
if (!/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?Z$/.test(timestamp3)) {
|
|
8351
|
+
throw new Error(`not an RFC 3339 UTC timestamp: ${timestamp3}`);
|
|
8350
8352
|
}
|
|
8351
|
-
return
|
|
8353
|
+
return timestamp3.slice(0, 19);
|
|
8352
8354
|
}
|
|
8353
8355
|
function compareCodeUnits(a, b) {
|
|
8354
8356
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -8533,9 +8535,6 @@ function checkReferences(block, evidence, repository) {
|
|
|
8533
8535
|
}
|
|
8534
8536
|
const observed = { exists: entry.exists, reachableFromHead: entry.reachableFromHead };
|
|
8535
8537
|
if (!entry.exists) return fail("reference", field, reference, observed, "not_found");
|
|
8536
|
-
if (!entry.reachableFromHead) {
|
|
8537
|
-
return fail("reference", field, reference, observed, "not_reachable");
|
|
8538
|
-
}
|
|
8539
8538
|
return pass("reference", field, reference, observed);
|
|
8540
8539
|
});
|
|
8541
8540
|
}
|
|
@@ -8553,6 +8552,15 @@ function countRow(field, declared, evidence, pick2) {
|
|
|
8553
8552
|
const observed = pick2(evidence.runs[0]);
|
|
8554
8553
|
return observed === declared ? pass("count", field, declared, observed) : fail("count", field, declared, observed, "count_mismatch");
|
|
8555
8554
|
}
|
|
8555
|
+
function producerReason(record2, evidence) {
|
|
8556
|
+
const files = evidence.files;
|
|
8557
|
+
if (files === void 0) return absentSection("files");
|
|
8558
|
+
if (files.status !== "ok") return unreadReason(files);
|
|
8559
|
+
if (files.entries.some((e) => e.path === record2.workflow || e.previousPath === record2.workflow)) {
|
|
8560
|
+
return "record_producer_changed_by_pr";
|
|
8561
|
+
}
|
|
8562
|
+
return files.complete ? void 0 : "file_list_truncated";
|
|
8563
|
+
}
|
|
8556
8564
|
function checkTests(block, evidence) {
|
|
8557
8565
|
const rows = [];
|
|
8558
8566
|
(block.tests ?? []).forEach((test, i) => {
|
|
@@ -8567,6 +8575,14 @@ function checkTests(block, evidence) {
|
|
|
8567
8575
|
return;
|
|
8568
8576
|
}
|
|
8569
8577
|
const record2 = test.record;
|
|
8578
|
+
const producer = producerReason(record2, evidence);
|
|
8579
|
+
if (producer !== void 0) {
|
|
8580
|
+
rows.push(unverifiable("count", countField, test.count, producer));
|
|
8581
|
+
if (test.failures !== void 0) {
|
|
8582
|
+
rows.push(unverifiable("count", failuresField, test.failures, producer));
|
|
8583
|
+
}
|
|
8584
|
+
return;
|
|
8585
|
+
}
|
|
8570
8586
|
const entry = evidence.testRecords?.find((r) => sameRecord(r.record, record2));
|
|
8571
8587
|
rows.push(countRow(countField, test.count, entry, (run) => run.executed));
|
|
8572
8588
|
if (test.failures !== void 0) {
|
|
@@ -9881,7 +9897,7 @@ async function reportFromComment(client, repository, number4, login) {
|
|
|
9881
9897
|
// src/record/checker.ts
|
|
9882
9898
|
import { readFileSync } from "node:fs";
|
|
9883
9899
|
var CHECKER_NAME = "dunstan";
|
|
9884
|
-
var CHECKER_VERSION = "0.1.
|
|
9900
|
+
var CHECKER_VERSION = "0.1.18";
|
|
9885
9901
|
function checkerIdentity(artifact) {
|
|
9886
9902
|
return {
|
|
9887
9903
|
name: CHECKER_NAME,
|
|
@@ -10077,7 +10093,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10077
10093
|
}
|
|
10078
10094
|
}
|
|
10079
10095
|
}
|
|
10080
|
-
`, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|none_failed_mismatch|checks_incomplete|no_check_runs|no_succeeded_check_runs|check_runs_not_at_head|no_comparable_record_field|local_run_not_in_record|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
|
|
10096
|
+
`, "record-0.1.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/record-0.1.schema.json",\n "title": "Dunstan record, v0.1",\n "description": "An in-toto Statement v1 whose predicate records one check of a handback block against the record. See spec/claim-format.md.",\n "type": "object",\n "required": ["_type", "subject", "predicateType", "predicate"],\n "additionalProperties": false,\n "properties": {\n "_type": { "const": "https://in-toto.io/Statement/v1" },\n "subject": {\n "type": "array",\n "minItems": 1,\n "maxItems": 2,\n "prefixItems": [{ "$ref": "#/$defs/commitSubject" }, { "$ref": "#/$defs/blockSubject" }],\n "items": false\n },\n "predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1" },\n "predicate": { "$ref": "#/$defs/predicate" }\n },\n "allOf": [\n {\n "if": {\n "properties": {\n "predicate": {\n "properties": { "block": { "properties": { "status": { "const": "found" } } } }\n }\n }\n },\n "then": { "properties": { "subject": { "minItems": 2 } } },\n "else": { "properties": { "subject": { "maxItems": 1 } } }\n }\n ],\n "$defs": {\n "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },\n "commitSha": { "type": "string", "pattern": "^[0-9a-f]{40}$" },\n "repository": {\n "description": "owner/repo",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}$"\n },\n "issueRef": {\n "description": "owner/repo#N, always fully qualified in evidence.",\n "type": "string",\n "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}#[1-9][0-9]{0,9}$"\n },\n "timestamp": {\n "type": "string",\n "format": "date-time",\n "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\\\.[0-9]+)?Z$"\n },\n "sourceKind": {\n "enum": [\n "pull_request",\n "pull_request_files",\n "pull_request_commits",\n "closing_references",\n "repository",\n "issue",\n "commit",\n "compare",\n "check_runs",\n "workflow_runs",\n "artifact",\n "deployments"\n ]\n },\n "commitSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": {\n "type": "string",\n "pattern": "^git\\\\+https://github\\\\.com/[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})/[A-Za-z0-9._-]{1,100}@[0-9a-f]{40}$"\n },\n "digest": {\n "type": "object",\n "required": ["gitCommit"],\n "additionalProperties": false,\n "properties": { "gitCommit": { "$ref": "#/$defs/commitSha" } }\n }\n }\n },\n "blockSubject": {\n "type": "object",\n "required": ["name", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "const": "handback-block" },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "predicate": {\n "type": "object",\n "required": [\n "spec",\n "checker",\n "report",\n "block",\n "subject",\n "evidence",\n "claims",\n "verdict",\n "digests",\n "rerun",\n "assurance"\n ],\n "additionalProperties": false,\n "properties": {\n "spec": {\n "description": "The spec version the checker implements, semver.",\n "type": "string",\n "pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)$"\n },\n "checker": {\n "type": "object",\n "required": ["name", "version", "digest"],\n "additionalProperties": false,\n "properties": {\n "name": { "type": "string", "minLength": 1, "maxLength": 214 },\n "version": { "type": "string", "minLength": 1, "maxLength": 100 },\n "digest": {\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "#/$defs/sha256" } }\n }\n }\n },\n "report": {\n "description": "The report\'s digest and where it was read. Never its text.",\n "type": "object",\n "required": ["sha256", "source"],\n "additionalProperties": false,\n "properties": {\n "sha256": { "$ref": "#/$defs/sha256" },\n "source": {\n "type": "object",\n "required": ["kind", "locator"],\n "additionalProperties": false,\n "properties": {\n "kind": { "enum": ["pr-body", "pr-comment", "file", "stdin", "api"] },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n }\n }\n },\n "block": { "$ref": "#/$defs/block" },\n "subject": {\n "type": "object",\n "required": ["repository", "pullRequest", "headSha", "mergeSha"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "#/$defs/repository" },\n "pullRequest": { "type": "integer", "minimum": 1 },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": {\n "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }]\n }\n }\n },\n "evidence": { "$ref": "#/$defs/evidence" },\n "claims": {\n "type": "array",\n "items": { "$ref": "#/$defs/claim" }\n },\n "verdict": { "$ref": "#/$defs/verdict" },\n "digests": {\n "type": "object",\n "required": ["claims", "evidence"],\n "additionalProperties": false,\n "properties": {\n "claims": { "$ref": "#/$defs/sha256" },\n "evidence": { "$ref": "#/$defs/sha256" }\n }\n },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "online": { "type": "string", "minLength": 1, "maxLength": 2000 }\n }\n },\n "assurance": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issuer"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "unsigned" },\n "issuer": { "const": "self-generated" }\n }\n },\n {\n "type": "object",\n "required": ["status", "issuer", "keyFingerprint"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "signed" },\n "issuer": { "type": "string", "minLength": 1, "maxLength": 320 },\n "keyFingerprint": { "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]{43}$" }\n }\n }\n ]\n }\n }\n },\n "verdict": { "enum": ["pass", "fail", "unverifiable"] },\n "blockError": {\n "type": "object",\n "required": ["code"],\n "additionalProperties": false,\n "properties": {\n "code": {\n "enum": [\n "unterminated_fence",\n "invalid_json",\n "duplicate_member",\n "unsupported_version",\n "schema_violation"\n ]\n },\n "pointer": { "type": "string", "pattern": "^(?:/.*)?$" },\n "keyword": { "type": "string", "minLength": 1 }\n }\n },\n "block": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "found" },\n "sha256": { "$ref": "#/$defs/sha256" },\n "value": { "$ref": "handback-block-0.1.schema.json" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "missing" },\n "reason": { "const": "block_missing" },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "count", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ambiguous" },\n "reason": { "const": "block_ambiguous" },\n "count": { "type": "integer", "minimum": 2 },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n },\n {\n "type": "object",\n "required": ["status", "reason", "errors", "sha256", "value"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "invalid" },\n "reason": { "const": "block_invalid" },\n "errors": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/blockError" } },\n "sha256": { "type": "null" },\n "value": { "type": "null" }\n }\n }\n ]\n },\n "claim": {\n "type": "object",\n "required": ["id", "check", "field", "declared", "observed", "verdict"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "string", "minLength": 1 },\n "check": { "enum": ["head", "scope", "reference", "count", "time"] },\n "field": {\n "description": "A JSON Pointer (RFC 6901) into the block.",\n "type": "string",\n "pattern": "^/.+$"\n },\n "declared": true,\n "observed": true,\n "verdict": { "$ref": "#/$defs/verdict" },\n "reason": { "$ref": "#/$defs/claimReason" }\n },\n "if": { "properties": { "verdict": { "const": "pass" } } },\n "then": { "not": { "required": ["reason"] } },\n "else": { "required": ["reason"] }\n },\n "claimReason": {\n "type": "string",\n "pattern": "^(?:head_mismatch|declared_not_changed|undeclared_file|file_list_truncated|not_closing|closing_link_unsettled|closing_base_not_default|closing_event_unsettled|not_found|not_reachable|count_mismatch|all_succeeded_mismatch|none_failed_mismatch|checks_incomplete|no_check_runs|no_succeeded_check_runs|check_runs_not_at_head|no_comparable_record_field|local_run_not_in_record|record_producer_changed_by_pr|record_not_found|record_ambiguous|not_merged|premature|no_deployment|evidence_field_unpopulated:[A-Za-z0-9_.]+|source_unreadable:(?:pull_request|pull_request_files|pull_request_commits|closing_references|repository|issue|commit|compare|check_runs|workflow_runs|artifact|deployments))$"\n },\n "unread": {\n "description": "A section the checker could not use: its source errored, or the source answered without the field.",\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "source"],\n "properties": {\n "status": { "const": "unreadable" },\n "source": { "$ref": "#/$defs/sourceKind" }\n }\n },\n {\n "type": "object",\n "required": ["status", "field"],\n "properties": {\n "status": { "const": "unpopulated" },\n "field": { "type": "string", "pattern": "^[A-Za-z0-9_.]+$" }\n }\n }\n ]\n },\n "evidence": {\n "type": "object",\n "required": ["pullRequest", "sources"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "description": "Always read: a checker that cannot read the pull request writes no record.",\n "type": "object",\n "required": [\n "status",\n "number",\n "state",\n "merged",\n "mergedAt",\n "headSha",\n "mergeSha",\n "changedFiles"\n ],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "number": { "type": "integer", "minimum": 1 },\n "state": { "enum": ["open", "closed"] },\n "merged": { "type": "boolean" },\n "mergedAt": { "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }] },\n "headSha": { "$ref": "#/$defs/commitSha" },\n "mergeSha": { "oneOf": [{ "$ref": "#/$defs/commitSha" }, { "type": "null" }] },\n "changedFiles": { "type": "integer", "minimum": 0 }\n }\n },\n "files": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "complete", "entries"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "complete": { "type": "boolean" },\n "entries": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["path", "status"],\n "additionalProperties": false,\n "properties": {\n "path": { "type": "string", "minLength": 1 },\n "status": {\n "enum": [\n "added",\n "removed",\n "modified",\n "renamed",\n "copied",\n "changed",\n "unchanged"\n ]\n },\n "previousPath": { "type": "string", "minLength": 1 }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "closingReferences": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "issues"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "references": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "issue" },\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "resolvedAs": {\n "description": "Spec 0.1.3: the owner/repo#N GitHub answered the read of ref with, when it is another issue (an issue transferred to another repository).",\n "$ref": "#/$defs/issueRef"\n }\n },\n "if": { "required": ["resolvedAs"] },\n "then": { "properties": { "exists": { "const": true } } }\n },\n {\n "type": "object",\n "required": ["kind", "ref", "status", "exists", "reachableFromHead"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "ref": { "$ref": "#/$defs/commitSha" },\n "status": { "const": "ok" },\n "exists": { "type": "boolean" },\n "reachableFromHead": { "type": "boolean" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "properties": {\n "kind": { "enum": ["issue", "commit"] },\n "ref": { "type": "string", "minLength": 1 }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "closeEvents": {\n "description": "Spec 0.1.4: who closed each closes target that exists and is not among the closing references, read under the identity it resolves to; and the pull request\'s base, its repository\'s default branch and its commit ids.",\n "type": "object",\n "required": ["pullRequest", "targets"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "baseRef", "defaultBranch", "commits"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "baseRef": { "type": "string", "minLength": 1 },\n "defaultBranch": {\n "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]\n },\n "commits": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/commitSha" }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "targets": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["ref", "status", "type", "state", "events"],\n "additionalProperties": false,\n "properties": {\n "ref": { "$ref": "#/$defs/issueRef" },\n "status": { "const": "ok" },\n "type": { "enum": ["issue", "pull_request"] },\n "state": { "enum": ["open", "closed", "merged"] },\n "events": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["at", "closer"],\n "additionalProperties": false,\n "properties": {\n "at": { "$ref": "#/$defs/timestamp" },\n "closer": {\n "oneOf": [\n { "type": "null" },\n {\n "type": "object",\n "required": ["kind", "ref"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pull_request" },\n "ref": { "$ref": "#/$defs/issueRef" }\n }\n },\n {\n "type": "object",\n "required": ["kind", "oid"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "commit" },\n "oid": { "$ref": "#/$defs/commitSha" }\n }\n },\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "other" } }\n }\n ]\n }\n }\n }\n }\n },\n "if": { "properties": { "type": { "const": "issue" } } },\n "then": { "properties": { "state": { "enum": ["open", "closed"] } } }\n },\n {\n "type": "object",\n "required": ["ref"],\n "properties": { "ref": { "$ref": "#/$defs/issueRef" } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n }\n }\n },\n "checkRuns": {\n "oneOf": [\n {\n "type": "object",\n "required": ["status", "commit", "excludedIds", "runs"],\n "additionalProperties": false,\n "properties": {\n "status": { "const": "ok" },\n "commit": { "$ref": "#/$defs/commitSha" },\n "excludedIds": {\n "description": "Check runs left out of the count: the run the checker itself executes in.",\n "type": "array",\n "items": { "type": "integer", "minimum": 1 }\n },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "name", "status", "conclusion"],\n "additionalProperties": false,\n "dependentRequired": {\n "workflowPath": ["checkSuiteId", "workflowRunId", "runAttempt"],\n "workflowName": ["workflowPath"],\n "workflowRunId": ["workflowPath"],\n "runAttempt": ["workflowPath"]\n },\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "name": { "type": "string" },\n "status": { "type": "string", "minLength": 1 },\n "conclusion": { "type": ["string", "null"] },\n "checkSuiteId": {\n "description": "The run\'s check suite (`check_suite.id`). A run without `workflowPath` is counted as the latest per check suite and name; a run without either is keyed by its name alone. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "integer",\n "minimum": 1\n },\n "appSlug": {\n "description": "The slug of the app that created the run (`app.slug`). Recorded, not read by any check. Added in spec 0.1.4, amended 2026-10-12.",\n "type": "string",\n "minLength": 1\n },\n "workflowPath": {\n "description": "The `path` of the workflow run whose `check_suite_id` is the run\'s check suite, less any `@<ref>`, from the workflow runs at the head. Runs that have it are counted as the latest per workflow path and name. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "string",\n "minLength": 1\n },\n "workflowName": {\n "description": "The `name` of that workflow run, as the listing gives it; absent where the listing gives none, null or the empty string. No 0.1 check reads it. Added in spec 0.1.5.",\n "type": "string",\n "minLength": 1\n },\n "workflowRunId": {\n "description": "The `id` of that workflow run. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n },\n "runAttempt": {\n "description": "That workflow run\'s `run_attempt` as the listing gives it, its latest attempt at the read. Added in spec 0.1.4, amended 2026-10-13.",\n "type": "integer",\n "minimum": 1\n }\n }\n }\n }\n }\n },\n { "$ref": "#/$defs/unreadOnly" }\n ]\n },\n "testRecords": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["record", "status", "runs"],\n "additionalProperties": false,\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" },\n "status": { "const": "ok" },\n "runs": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["runId", "runAttempt", "executed", "failed"],\n "additionalProperties": false,\n "properties": {\n "runId": { "type": "integer", "minimum": 1 },\n "runAttempt": { "type": "integer", "minimum": 1 },\n "executed": { "type": "integer", "minimum": 0 },\n "failed": { "type": "integer", "minimum": 0 }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["record"],\n "properties": {\n "record": { "$ref": "handback-block-0.1.schema.json#/$defs/junitRecord" }\n },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "deployments": {\n "type": "array",\n "items": {\n "oneOf": [\n {\n "type": "object",\n "required": ["environment", "status", "deployments"],\n "additionalProperties": false,\n "properties": {\n "environment": { "type": "string", "minLength": 1 },\n "status": { "const": "ok" },\n "deployments": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["id", "sha", "relation", "successAt"],\n "additionalProperties": false,\n "properties": {\n "id": { "type": "integer", "minimum": 1 },\n "sha": { "$ref": "#/$defs/commitSha" },\n "relation": {\n "enum": ["head", "merge", "descendant", "unrelated", "unknown"]\n },\n "successAt": {\n "oneOf": [{ "$ref": "#/$defs/timestamp" }, { "type": "null" }]\n }\n }\n }\n }\n }\n },\n {\n "type": "object",\n "required": ["environment"],\n "properties": { "environment": { "type": "string", "minLength": 1 } },\n "allOf": [{ "$ref": "#/$defs/unread" }]\n }\n ]\n }\n },\n "sources": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["kind", "locator", "sha256", "readAt"],\n "additionalProperties": false,\n "properties": {\n "kind": { "$ref": "#/$defs/sourceKind" },\n "locator": { "type": "string", "minLength": 1, "maxLength": 2000 },\n "sha256": { "$ref": "#/$defs/sha256" },\n "etag": { "type": "string", "minLength": 1 },\n "readAt": { "$ref": "#/$defs/timestamp" },\n "error": {\n "description": "Present when the read failed: http_<status>, network, or parse.",\n "type": "string",\n "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$"\n }\n }\n }\n }\n }\n },\n "unreadOnly": {\n "allOf": [{ "$ref": "#/$defs/unread" }],\n "unevaluatedProperties": false\n }\n }\n}\n', "record-0.2-draft.schema.json": `{
|
|
10081
10097
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
10082
10098
|
"$id": "https://barglabs.ai/dunstan/spec/schema/record-0.2-draft.schema.json",
|
|
10083
10099
|
"title": "Dunstan record, DRAFT 0.2.0",
|
|
@@ -10408,7 +10424,7 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10408
10424
|
"additionalProperties": false,
|
|
10409
10425
|
"properties": {
|
|
10410
10426
|
"comparison": {
|
|
10411
|
-
"description": "The version of the rules that turn a proposed claim and the evidence into its note, separate from the extractor's. 0.1.0 compared a file claim by exact path only; 0.2.0 also matches it by name; 0.3.0 also asks whether a path not among the changed files exists at the head (D.11.4); 0.4.0 compares a closes claim by who closed the target (spec 0.1.4).",
|
|
10427
|
+
"description": "The version of the rules that turn a proposed claim and the evidence into its note, separate from the extractor's. 0.1.0 compared a file claim by exact path only; 0.2.0 also matches it by name; 0.3.0 also asks whether a path not among the changed files exists at the head (D.11.4); 0.4.0 compares a closes claim by who closed the target (spec 0.1.4); 0.5.0 compares a cited commit by whether it exists only (spec 0.1.11).",
|
|
10412
10428
|
"type": "object",
|
|
10413
10429
|
"required": ["version"],
|
|
10414
10430
|
"additionalProperties": false,
|
|
@@ -10567,6 +10583,253 @@ var FILES = { "handback-block-0.1.schema.json": `{
|
|
|
10567
10583
|
}
|
|
10568
10584
|
}
|
|
10569
10585
|
}
|
|
10586
|
+
`, "record-gitlab-0.1-draft.schema.json": `{
|
|
10587
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
10588
|
+
"$id": "https://barglabs.ai/dunstan/spec/schema/record-gitlab-0.1-draft.schema.json",
|
|
10589
|
+
"title": "Dunstan record, DRAFT GitLab profile 0.1",
|
|
10590
|
+
"description": "DRAFT, not normative, for the operator to check (spec/claim-format.md, \\"DRAFT GitLab profile 0.1\\"). A 0.1 record of a GitLab merge request: the predicate's subject carries forge {kind: gitlab, host}, and the head commit's subject entry names that host. A project and an issue reference are named by the project's full path, group/subgroup/.../project, which admits every path the 0.1 repository pattern admits; the evidence sections that hold issue references are restated here with that pattern. Every other member is a reference into record-0.1.schema.json, which this file does not change.",
|
|
10591
|
+
"type": "object",
|
|
10592
|
+
"required": ["_type", "subject", "predicateType", "predicate"],
|
|
10593
|
+
"additionalProperties": false,
|
|
10594
|
+
"properties": {
|
|
10595
|
+
"_type": { "$ref": "record-0.1.schema.json#/properties/_type" },
|
|
10596
|
+
"subject": {
|
|
10597
|
+
"type": "array",
|
|
10598
|
+
"minItems": 1,
|
|
10599
|
+
"maxItems": 2,
|
|
10600
|
+
"prefixItems": [
|
|
10601
|
+
{ "$ref": "#/$defs/commitSubject" },
|
|
10602
|
+
{ "$ref": "record-0.1.schema.json#/$defs/blockSubject" }
|
|
10603
|
+
],
|
|
10604
|
+
"items": false
|
|
10605
|
+
},
|
|
10606
|
+
"predicateType": { "const": "https://barglabs.ai/dunstan/record/v0.1-gitlab-draft" },
|
|
10607
|
+
"predicate": { "$ref": "#/$defs/predicate" }
|
|
10608
|
+
},
|
|
10609
|
+
"allOf": [{ "$ref": "record-0.1.schema.json#/allOf/0" }],
|
|
10610
|
+
"$defs": {
|
|
10611
|
+
"repository": {
|
|
10612
|
+
"description": "A GitLab project's full path: up to 20 levels of groups, then the project. Every path the 0.1 repository pattern admits is one.",
|
|
10613
|
+
"type": "string",
|
|
10614
|
+
"maxLength": 5375,
|
|
10615
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9._-]{0,254}(?:/[A-Za-z0-9_][A-Za-z0-9._-]{0,254}){0,19}/[A-Za-z0-9._-]{1,255}$"
|
|
10616
|
+
},
|
|
10617
|
+
"issueRef": {
|
|
10618
|
+
"description": "An issue of a GitLab project, <full path>#<iid>.",
|
|
10619
|
+
"type": "string",
|
|
10620
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9._-]{0,254}(?:/[A-Za-z0-9_][A-Za-z0-9._-]{0,254}){0,19}/[A-Za-z0-9._-]{1,255}#[1-9][0-9]{0,9}$"
|
|
10621
|
+
},
|
|
10622
|
+
"evidence": {
|
|
10623
|
+
"description": "0.1's evidence, with the sections that hold issue references (closingReferences, references, closeEvents) restated to name an issue by #/$defs/issueRef. Every other section is 0.1's.",
|
|
10624
|
+
"type": "object",
|
|
10625
|
+
"required": ["pullRequest", "sources"],
|
|
10626
|
+
"additionalProperties": false,
|
|
10627
|
+
"properties": {
|
|
10628
|
+
"pullRequest": {
|
|
10629
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/pullRequest"
|
|
10630
|
+
},
|
|
10631
|
+
"files": { "$ref": "record-0.1.schema.json#/$defs/evidence/properties/files" },
|
|
10632
|
+
"closingReferences": {
|
|
10633
|
+
"oneOf": [
|
|
10634
|
+
{
|
|
10635
|
+
"type": "object",
|
|
10636
|
+
"required": ["status", "issues"],
|
|
10637
|
+
"additionalProperties": false,
|
|
10638
|
+
"properties": {
|
|
10639
|
+
"status": { "const": "ok" },
|
|
10640
|
+
"issues": { "type": "array", "items": { "$ref": "#/$defs/issueRef" } }
|
|
10641
|
+
}
|
|
10642
|
+
},
|
|
10643
|
+
{ "$ref": "record-0.1.schema.json#/$defs/unreadOnly" }
|
|
10644
|
+
]
|
|
10645
|
+
},
|
|
10646
|
+
"references": {
|
|
10647
|
+
"type": "array",
|
|
10648
|
+
"items": {
|
|
10649
|
+
"oneOf": [
|
|
10650
|
+
{
|
|
10651
|
+
"type": "object",
|
|
10652
|
+
"required": ["kind", "ref", "status", "exists"],
|
|
10653
|
+
"additionalProperties": false,
|
|
10654
|
+
"properties": {
|
|
10655
|
+
"kind": { "const": "issue" },
|
|
10656
|
+
"ref": { "$ref": "#/$defs/issueRef" },
|
|
10657
|
+
"status": { "const": "ok" },
|
|
10658
|
+
"exists": { "type": "boolean" },
|
|
10659
|
+
"resolvedAs": { "$ref": "#/$defs/issueRef" }
|
|
10660
|
+
},
|
|
10661
|
+
"if": { "required": ["resolvedAs"] },
|
|
10662
|
+
"then": { "properties": { "exists": { "const": true } } }
|
|
10663
|
+
},
|
|
10664
|
+
{
|
|
10665
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/references/items/oneOf/1"
|
|
10666
|
+
},
|
|
10667
|
+
{
|
|
10668
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/references/items/oneOf/2"
|
|
10669
|
+
}
|
|
10670
|
+
]
|
|
10671
|
+
}
|
|
10672
|
+
},
|
|
10673
|
+
"closeEvents": {
|
|
10674
|
+
"type": "object",
|
|
10675
|
+
"required": ["pullRequest", "targets"],
|
|
10676
|
+
"additionalProperties": false,
|
|
10677
|
+
"properties": {
|
|
10678
|
+
"pullRequest": {
|
|
10679
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/closeEvents/properties/pullRequest"
|
|
10680
|
+
},
|
|
10681
|
+
"targets": {
|
|
10682
|
+
"type": "array",
|
|
10683
|
+
"items": {
|
|
10684
|
+
"oneOf": [
|
|
10685
|
+
{
|
|
10686
|
+
"type": "object",
|
|
10687
|
+
"required": ["ref", "status", "type", "state", "events"],
|
|
10688
|
+
"additionalProperties": false,
|
|
10689
|
+
"properties": {
|
|
10690
|
+
"ref": { "$ref": "#/$defs/issueRef" },
|
|
10691
|
+
"status": { "const": "ok" },
|
|
10692
|
+
"type": { "enum": ["issue", "pull_request"] },
|
|
10693
|
+
"state": { "enum": ["open", "closed", "merged"] },
|
|
10694
|
+
"events": {
|
|
10695
|
+
"type": "array",
|
|
10696
|
+
"items": {
|
|
10697
|
+
"type": "object",
|
|
10698
|
+
"required": ["at", "closer"],
|
|
10699
|
+
"additionalProperties": false,
|
|
10700
|
+
"properties": {
|
|
10701
|
+
"at": { "$ref": "record-0.1.schema.json#/$defs/timestamp" },
|
|
10702
|
+
"closer": {
|
|
10703
|
+
"oneOf": [
|
|
10704
|
+
{ "type": "null" },
|
|
10705
|
+
{
|
|
10706
|
+
"type": "object",
|
|
10707
|
+
"required": ["kind", "ref"],
|
|
10708
|
+
"additionalProperties": false,
|
|
10709
|
+
"properties": {
|
|
10710
|
+
"kind": { "const": "pull_request" },
|
|
10711
|
+
"ref": { "$ref": "#/$defs/issueRef" }
|
|
10712
|
+
}
|
|
10713
|
+
},
|
|
10714
|
+
{
|
|
10715
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/closeEvents/properties/targets/items/oneOf/0/properties/events/items/properties/closer/oneOf/2"
|
|
10716
|
+
},
|
|
10717
|
+
{
|
|
10718
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/closeEvents/properties/targets/items/oneOf/0/properties/events/items/properties/closer/oneOf/3"
|
|
10719
|
+
}
|
|
10720
|
+
]
|
|
10721
|
+
}
|
|
10722
|
+
}
|
|
10723
|
+
}
|
|
10724
|
+
}
|
|
10725
|
+
},
|
|
10726
|
+
"if": { "properties": { "type": { "const": "issue" } } },
|
|
10727
|
+
"then": { "properties": { "state": { "enum": ["open", "closed"] } } }
|
|
10728
|
+
},
|
|
10729
|
+
{
|
|
10730
|
+
"type": "object",
|
|
10731
|
+
"required": ["ref"],
|
|
10732
|
+
"properties": { "ref": { "$ref": "#/$defs/issueRef" } },
|
|
10733
|
+
"allOf": [{ "$ref": "record-0.1.schema.json#/$defs/unread" }]
|
|
10734
|
+
}
|
|
10735
|
+
]
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
}
|
|
10739
|
+
},
|
|
10740
|
+
"checkRuns": { "$ref": "record-0.1.schema.json#/$defs/evidence/properties/checkRuns" },
|
|
10741
|
+
"testRecords": {
|
|
10742
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/testRecords"
|
|
10743
|
+
},
|
|
10744
|
+
"deployments": {
|
|
10745
|
+
"$ref": "record-0.1.schema.json#/$defs/evidence/properties/deployments"
|
|
10746
|
+
},
|
|
10747
|
+
"sources": { "$ref": "record-0.1.schema.json#/$defs/evidence/properties/sources" }
|
|
10748
|
+
}
|
|
10749
|
+
},
|
|
10750
|
+
"host": {
|
|
10751
|
+
"description": "The GitLab instance's lowercase DNS name: gitlab.com, or a self-managed host.",
|
|
10752
|
+
"type": "string",
|
|
10753
|
+
"maxLength": 253,
|
|
10754
|
+
"pattern": "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$"
|
|
10755
|
+
},
|
|
10756
|
+
"commitSubject": {
|
|
10757
|
+
"type": "object",
|
|
10758
|
+
"required": ["name", "digest"],
|
|
10759
|
+
"additionalProperties": false,
|
|
10760
|
+
"properties": {
|
|
10761
|
+
"name": {
|
|
10762
|
+
"type": "string",
|
|
10763
|
+
"pattern": "^git\\\\+https://[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*/[A-Za-z0-9_][A-Za-z0-9._-]{0,254}(?:/[A-Za-z0-9_][A-Za-z0-9._-]{0,254}){0,19}/[A-Za-z0-9._-]{1,255}@[0-9a-f]{40}$"
|
|
10764
|
+
},
|
|
10765
|
+
"digest": { "$ref": "record-0.1.schema.json#/$defs/commitSubject/properties/digest" }
|
|
10766
|
+
}
|
|
10767
|
+
},
|
|
10768
|
+
"predicate": {
|
|
10769
|
+
"type": "object",
|
|
10770
|
+
"required": [
|
|
10771
|
+
"spec",
|
|
10772
|
+
"checker",
|
|
10773
|
+
"report",
|
|
10774
|
+
"block",
|
|
10775
|
+
"subject",
|
|
10776
|
+
"evidence",
|
|
10777
|
+
"claims",
|
|
10778
|
+
"verdict",
|
|
10779
|
+
"digests",
|
|
10780
|
+
"rerun",
|
|
10781
|
+
"assurance"
|
|
10782
|
+
],
|
|
10783
|
+
"additionalProperties": false,
|
|
10784
|
+
"properties": {
|
|
10785
|
+
"spec": {
|
|
10786
|
+
"description": "The 0.1 version the checker implements, with the profile: 0.1.11-gitlab-0.1-draft.",
|
|
10787
|
+
"type": "string",
|
|
10788
|
+
"pattern": "^0\\\\.1\\\\.(?:0|[1-9][0-9]*)-gitlab-0\\\\.1-draft$"
|
|
10789
|
+
},
|
|
10790
|
+
"checker": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/checker" },
|
|
10791
|
+
"report": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/report" },
|
|
10792
|
+
"block": { "$ref": "record-0.1.schema.json#/$defs/block" },
|
|
10793
|
+
"subject": {
|
|
10794
|
+
"type": "object",
|
|
10795
|
+
"required": ["repository", "pullRequest", "headSha", "mergeSha", "forge"],
|
|
10796
|
+
"additionalProperties": false,
|
|
10797
|
+
"properties": {
|
|
10798
|
+
"repository": {
|
|
10799
|
+
"description": "The project's full path, group/subgroup/.../project, as the merge request read names it.",
|
|
10800
|
+
"$ref": "#/$defs/repository"
|
|
10801
|
+
},
|
|
10802
|
+
"pullRequest": {
|
|
10803
|
+
"description": "The merge request's iid.",
|
|
10804
|
+
"type": "integer",
|
|
10805
|
+
"minimum": 1
|
|
10806
|
+
},
|
|
10807
|
+
"headSha": { "$ref": "record-0.1.schema.json#/$defs/commitSha" },
|
|
10808
|
+
"mergeSha": {
|
|
10809
|
+
"oneOf": [{ "$ref": "record-0.1.schema.json#/$defs/commitSha" }, { "type": "null" }]
|
|
10810
|
+
},
|
|
10811
|
+
"forge": {
|
|
10812
|
+
"description": "The forge the evidence was read from. A record without it is a GitHub record.",
|
|
10813
|
+
"type": "object",
|
|
10814
|
+
"required": ["kind", "host"],
|
|
10815
|
+
"additionalProperties": false,
|
|
10816
|
+
"properties": {
|
|
10817
|
+
"kind": { "const": "gitlab" },
|
|
10818
|
+
"host": { "$ref": "#/$defs/host" }
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
},
|
|
10823
|
+
"evidence": { "$ref": "#/$defs/evidence" },
|
|
10824
|
+
"claims": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/claims" },
|
|
10825
|
+
"verdict": { "$ref": "record-0.1.schema.json#/$defs/verdict" },
|
|
10826
|
+
"digests": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/digests" },
|
|
10827
|
+
"rerun": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/rerun" },
|
|
10828
|
+
"assurance": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/assurance" }
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
}
|
|
10832
|
+
}
|
|
10570
10833
|
`, "survey-0.1-draft.schema.json": '{\n "$schema": "https://json-schema.org/draft/2020-12/schema",\n "$id": "https://barglabs.ai/dunstan/spec/schema/survey-0.1-draft.schema.json",\n "title": "Dunstan survey record, DRAFT 0.1",\n "description": "DRAFT, informative, for the operator to check (spec/claim-format.md, \\"DRAFT survey 0.1\\"). A read-only survey of a repository\'s recent pull requests: the selection, one entry per selected pull request with the record a check wrote for it or the reason there is none, and an aggregate that is a pure function of the entries. Each embedded record is a 0.1 or DRAFT 0.2.0 record and is validated against its own schema when the survey is verified. It changes neither record schema.",\n "type": "object",\n "required": ["format", "checker", "selection", "entries", "aggregate", "rerun", "digest"],\n "additionalProperties": false,\n "properties": {\n "format": { "const": "dunstan-survey-0.1-draft" },\n "checker": { "$ref": "record-0.1.schema.json#/$defs/predicate/properties/checker" },\n "selection": {\n "type": "object",\n "required": ["parameters", "reader", "listing", "selected"],\n "additionalProperties": false,\n "properties": {\n "parameters": { "$ref": "#/$defs/parameters" },\n "reader": {\n "description": "Whether the survey read GitHub with a token (GITHUB_TOKEN was set). The aggregate\'s remedy lines are worded for it. The token is never recorded.",\n "type": "object",\n "required": ["authenticated"],\n "additionalProperties": false,\n "properties": { "authenticated": { "type": "boolean" } }\n },\n "listing": { "$ref": "#/$defs/listing" },\n "selected": {\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/number" }\n }\n }\n },\n "entries": {\n "description": "One per selected pull request, in selection order. Verify checks that their pull request numbers are exactly `selection.selected`, which holds no number twice.",\n "type": "array",\n "uniqueItems": true,\n "items": { "$ref": "#/$defs/entry" }\n },\n "aggregate": { "$ref": "#/$defs/aggregate" },\n "rerun": {\n "type": "object",\n "required": ["offline", "online"],\n "additionalProperties": false,\n "properties": {\n "offline": { "type": "string", "minLength": 1 },\n "online": { "type": "string", "minLength": 1 }\n }\n },\n "digest": {\n "description": "SHA-256 of the JCS bytes of the survey record without this member.",\n "type": "object",\n "required": ["sha256"],\n "additionalProperties": false,\n "properties": { "sha256": { "$ref": "record-0.1.schema.json#/$defs/sha256" } }\n }\n },\n "$defs": {\n "number": { "type": "integer", "minimum": 1 },\n "count": { "type": "integer", "minimum": 0 },\n "login": { "type": "string", "pattern": "^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})(?:\\\\[bot\\\\])?$" },\n "parameters": {\n "type": "object",\n "required": ["repository", "last", "since", "authors", "report", "advisory"],\n "additionalProperties": false,\n "properties": {\n "repository": { "$ref": "record-0.1.schema.json#/$defs/repository" },\n "last": { "type": "integer", "minimum": 1, "maximum": 200 },\n "since": {\n "oneOf": [\n { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },\n { "type": "null" }\n ]\n },\n "authors": { "type": "array", "items": { "$ref": "#/$defs/login" } },\n "report": {\n "oneOf": [\n {\n "type": "object",\n "required": ["kind"],\n "additionalProperties": false,\n "properties": { "kind": { "const": "pr-body" } }\n },\n {\n "type": "object",\n "required": ["kind", "author"],\n "additionalProperties": false,\n "properties": {\n "kind": { "const": "pr-comment" },\n "author": { "$ref": "#/$defs/login" }\n }\n }\n ]\n },\n "advisory": { "type": "boolean" }\n }\n },\n "listingPage": {\n "type": "object",\n "required": ["page", "locator", "sha256", "readAt", "items", "duplicates"],\n "additionalProperties": false,\n "properties": {\n "page": { "$ref": "#/$defs/number" },\n "locator": { "type": "string", "minLength": 1 },\n "sha256": { "$ref": "record-0.1.schema.json#/$defs/sha256" },\n "readAt": { "$ref": "record-0.1.schema.json#/$defs/timestamp" },\n "items": { "$ref": "#/$defs/count" },\n "duplicates": {\n "description": "How many of this page\'s items repeat a pull request number an earlier item of the same read already held. In the read used, they are not in `pullRequests`.",\n "$ref": "#/$defs/count"\n },\n "error": { "type": "string", "pattern": "^(?:http_[1-5][0-9]{2}|network|parse)$" }\n }\n },\n "listing": {\n "description": "The read of the listing the selection is from, the last one, and the reads abandoned before it. A read is abandoned at its first page that repeats an earlier item, unless it is the third read, which is never abandoned.",\n "type": "object",\n "required": ["pageBound", "pages", "abandoned", "pullRequests", "duplicates", "end"],\n "additionalProperties": false,\n "properties": {\n "pageBound": { "type": "integer", "minimum": 1 },\n "pages": {\n "type": "array",\n "minItems": 1,\n "items": { "$ref": "#/$defs/listingPage" }\n },\n "abandoned": {\n "description": "One item per abandoned read, in order: its pages, the last of them the page that repeated an earlier item. No pull request list; abandoned reads contribute nothing to the selection. Empty when the first read was used.",\n "type": "array",\n "maxItems": 2,\n "items": {\n "type": "object",\n "required": ["pages"],\n "additionalProperties": false,\n "properties": {\n "pages": {\n "type": "array",\n "minItems": 1,\n "items": { "$ref": "#/$defs/listingPage" }\n }\n }\n }\n },\n "duplicates": {\n "description": "How many listed items repeated an earlier one: the sum of the pages\' duplicates. The pages\' items are the length of `pullRequests` plus this.",\n "$ref": "#/$defs/count"\n },\n "pullRequests": {\n "description": "Each listed pull request once, at its first occurrence, in listing order. Verify refuses a number listed twice.",\n "type": "array",\n "uniqueItems": true,\n "items": {\n "type": "object",\n "required": ["number", "createdAt", "author", "merged"],\n "additionalProperties": false,\n "properties": {\n "number": { "$ref": "#/$defs/number" },\n "createdAt": { "$ref": "record-0.1.schema.json#/$defs/timestamp" },\n "author": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] },\n "merged": { "type": "boolean" }\n }\n }\n },\n "end": {\n "enum": [\n "selection_full",\n "since_reached",\n "listing_ended",\n "listing_unreadable",\n "page_bound"\n ]\n }\n }\n },\n "noRecordReason": {\n "enum": [\n "pull_request_unreadable",\n "pull_request_moved",\n "rate_limited",\n "report_unavailable"\n ]\n },\n "entry": {\n "oneOf": [\n {\n "type": "object",\n "required": ["pullRequest", "record"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": { "$ref": "#/$defs/number" },\n "record": {\n "description": "The record `dunstan check` writes for this pull request, as written: a 0.1 record, or a DRAFT 0.2.0 record when the survey ran the advisory reader. Verify validates it against its own schema.",\n "type": "object"\n }\n }\n },\n {\n "type": "object",\n "required": ["pullRequest", "record", "reason"],\n "additionalProperties": false,\n "properties": {\n "pullRequest": { "$ref": "#/$defs/number" },\n "record": { "type": "null" },\n "reason": { "$ref": "#/$defs/noRecordReason" }\n }\n }\n ]\n },\n "columns": {\n "type": "object",\n "required": ["supported", "unverifiable", "notRecordCheckable", "contradicted"],\n "properties": {\n "supported": { "$ref": "#/$defs/count" },\n "unverifiable": { "$ref": "#/$defs/count" },\n "notRecordCheckable": { "$ref": "#/$defs/count" },\n "contradicted": { "$ref": "#/$defs/count" }\n }\n },\n "fixed": { "type": "string", "pattern": "^[01]\\\\.[0-9]{4}$" },\n "rate": {\n "type": "object",\n "required": ["numerator", "denominator", "value", "wilson95"],\n "additionalProperties": false,\n "properties": {\n "numerator": { "$ref": "#/$defs/count" },\n "denominator": { "$ref": "#/$defs/count" },\n "value": { "oneOf": [{ "$ref": "#/$defs/fixed" }, { "type": "null" }] },\n "wilson95": {\n "oneOf": [\n {\n "type": "object",\n "required": ["low", "high"],\n "additionalProperties": false,\n "properties": {\n "low": { "$ref": "#/$defs/fixed" },\n "high": { "$ref": "#/$defs/fixed" }\n }\n },\n { "type": "null" }\n ]\n }\n }\n },\n "aggregate": {\n "type": "object",\n "required": [\n "mapping",\n "pullRequests",\n "claims",\n "byType",\n "cannotSettle",\n "contradictedOfSettled",\n "unsettled"\n ],\n "additionalProperties": false,\n "properties": {\n "mapping": {\n "type": "object",\n "required": ["version"],\n "additionalProperties": false,\n "properties": { "version": { "type": "string", "minLength": 1 } }\n },\n "pullRequests": {\n "type": "object",\n "required": [\n "listed",\n "selected",\n "withRecord",\n "blockFound",\n "blockMissing",\n "blockInvalid",\n "blockAmbiguous",\n "noRecord"\n ],\n "additionalProperties": false,\n "properties": {\n "listed": { "$ref": "#/$defs/count" },\n "selected": { "$ref": "#/$defs/count" },\n "withRecord": { "$ref": "#/$defs/count" },\n "blockFound": { "$ref": "#/$defs/count" },\n "blockMissing": { "$ref": "#/$defs/count" },\n "blockInvalid": { "$ref": "#/$defs/count" },\n "blockAmbiguous": { "$ref": "#/$defs/count" },\n "noRecord": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["reason", "count"],\n "additionalProperties": false,\n "properties": {\n "reason": { "$ref": "#/$defs/noRecordReason" },\n "count": { "$ref": "#/$defs/count" }\n }\n }\n }\n }\n },\n "claims": {\n "allOf": [{ "$ref": "#/$defs/columns" }],\n "required": ["total"],\n "properties": { "total": { "$ref": "#/$defs/count" } },\n "unevaluatedProperties": false\n },\n "byType": {\n "type": "array",\n "items": {\n "allOf": [{ "$ref": "#/$defs/columns" }],\n "required": ["type"],\n "properties": {\n "type": { "enum": ["T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "R"] }\n },\n "unevaluatedProperties": false\n }\n },\n "cannotSettle": { "$ref": "#/$defs/rate" },\n "contradictedOfSettled": { "$ref": "#/$defs/rate" },\n "unsettled": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["reason", "column", "count", "remedy"],\n "additionalProperties": false,\n "properties": {\n "reason": { "$ref": "record-0.1.schema.json#/$defs/claimReason" },\n "column": { "enum": ["unverifiable", "notRecordCheckable"] },\n "count": { "type": "integer", "minimum": 1 },\n "remedy": { "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }\n }\n }\n },\n "advisory": {\n "type": "object",\n "required": ["pullRequests", "notes"],\n "additionalProperties": false,\n "properties": {\n "pullRequests": { "$ref": "#/$defs/count" },\n "notes": {\n "type": "array",\n "items": {\n "type": "object",\n "required": ["note", "count"],\n "additionalProperties": false,\n "properties": {\n "note": { "type": "string", "minLength": 1 },\n "count": { "type": "integer", "minimum": 1 }\n }\n }\n }\n }\n }\n }\n }\n }\n}\n' };
|
|
10571
10834
|
function schemaText(file2) {
|
|
10572
10835
|
const text2 = FILES[file2];
|
|
@@ -10640,6 +10903,16 @@ function validateDraftRecord(value) {
|
|
|
10640
10903
|
}
|
|
10641
10904
|
return draftRecord(value) ? [] : toSchemaErrors(draftRecord.errors);
|
|
10642
10905
|
}
|
|
10906
|
+
var GITLAB_DRAFT_RECORD_SCHEMA_FILE = "record-gitlab-0.1-draft.schema.json";
|
|
10907
|
+
var gitlabDraftRecord;
|
|
10908
|
+
function validateGitlabDraftRecord(value) {
|
|
10909
|
+
if (gitlabDraftRecord === void 0) {
|
|
10910
|
+
const ajv = createAjv();
|
|
10911
|
+
ajv.addSchema(readSchema(GITLAB_DRAFT_RECORD_SCHEMA_FILE));
|
|
10912
|
+
gitlabDraftRecord = ajv.getSchema(schemaId(GITLAB_DRAFT_RECORD_SCHEMA_FILE));
|
|
10913
|
+
}
|
|
10914
|
+
return gitlabDraftRecord(value) ? [] : toSchemaErrors(gitlabDraftRecord.errors);
|
|
10915
|
+
}
|
|
10643
10916
|
var SURVEY_SCHEMA_FILE = "survey-0.1-draft.schema.json";
|
|
10644
10917
|
var surveyRecord;
|
|
10645
10918
|
function validateSurveyRecord(value) {
|
|
@@ -10743,6 +11016,64 @@ function extractHandbackBlock(reportText) {
|
|
|
10743
11016
|
return readBlockContent(block.content);
|
|
10744
11017
|
}
|
|
10745
11018
|
|
|
11019
|
+
// src/template/wording.ts
|
|
11020
|
+
var PHRASES = {
|
|
11021
|
+
// The notes of a template.
|
|
11022
|
+
again: {
|
|
11023
|
+
cli: "run dunstan template again",
|
|
11024
|
+
mcp: "call dunstan_template_handback again"
|
|
11025
|
+
},
|
|
11026
|
+
againCi: {
|
|
11027
|
+
cli: "run dunstan template --tests-ci again",
|
|
11028
|
+
mcp: 'call dunstan_template_handback again with tests.from "ci"'
|
|
11029
|
+
},
|
|
11030
|
+
againCiSentence: {
|
|
11031
|
+
cli: "Run dunstan template --tests-ci again",
|
|
11032
|
+
mcp: 'Call dunstan_template_handback again with tests.from "ci"'
|
|
11033
|
+
},
|
|
11034
|
+
ciOnceFinished: {
|
|
11035
|
+
cli: "run dunstan template --tests-ci once CI has finished",
|
|
11036
|
+
mcp: 'call dunstan_template_handback with tests.from "ci" once CI has finished'
|
|
11037
|
+
},
|
|
11038
|
+
noTests: {
|
|
11039
|
+
cli: "tests are not declared. To declare CI's counts, run dunstan template again with --tests-ci --tests-command <cmd> --tests-workflow <path> --tests-job <name> once CI has finished on your last push; it reads the JUnit record CI published (docs/ci-test-records.md). For a run made only locally, --tests-local <JUnit XML file> --tests-command <cmd> declares it as one. --tests-junit <file> with --tests-command, --tests-workflow, --tests-job, --tests-artifact and --tests-artifact-path declares a local file's counts against a CI record.",
|
|
11040
|
+
mcp: `tests are not declared. To declare CI's counts, call dunstan_template_handback again with tests { from: "ci", command, workflow, job } once CI has finished on your last push; it reads the JUnit record CI published (docs/ci-test-records.md). For a run made only locally, tests { from: "local", junit, command } declares it as one.`
|
|
11041
|
+
},
|
|
11042
|
+
// The errors of a template.
|
|
11043
|
+
base: {
|
|
11044
|
+
cli: "pass --base <ref>",
|
|
11045
|
+
mcp: "give base"
|
|
11046
|
+
},
|
|
11047
|
+
baseName: {
|
|
11048
|
+
cli: "--base",
|
|
11049
|
+
mcp: "base"
|
|
11050
|
+
},
|
|
11051
|
+
repo: {
|
|
11052
|
+
cli: "pass --repo <owner/name>",
|
|
11053
|
+
mcp: "give tests.repository (owner/name)"
|
|
11054
|
+
},
|
|
11055
|
+
localFile: {
|
|
11056
|
+
cli: "--tests-local",
|
|
11057
|
+
mcp: "tests.junit"
|
|
11058
|
+
},
|
|
11059
|
+
// The remedy lines (src/remedy/remedies.ts).
|
|
11060
|
+
templateWrites: {
|
|
11061
|
+
cli: "dunstan template writes it",
|
|
11062
|
+
mcp: "dunstan_template_handback writes it"
|
|
11063
|
+
},
|
|
11064
|
+
templateCiFills: {
|
|
11065
|
+
cli: "dunstan template --tests-ci fills them",
|
|
11066
|
+
mcp: 'dunstan_template_handback with tests.from "ci" fills them'
|
|
11067
|
+
},
|
|
11068
|
+
templateCiAfter: {
|
|
11069
|
+
cli: "run dunstan template --tests-ci after CI has finished on your last push",
|
|
11070
|
+
mcp: 'call dunstan_template_handback with tests.from "ci" after CI has finished on your last push'
|
|
11071
|
+
}
|
|
11072
|
+
};
|
|
11073
|
+
function phrase(caller, key) {
|
|
11074
|
+
return PHRASES[key][caller];
|
|
11075
|
+
}
|
|
11076
|
+
|
|
10746
11077
|
// src/template/template.ts
|
|
10747
11078
|
var TemplateError = class extends Error {
|
|
10748
11079
|
constructor(message) {
|
|
@@ -10772,12 +11103,12 @@ function commitOf(cwd, ref) {
|
|
|
10772
11103
|
const sha = r.stdout.trim();
|
|
10773
11104
|
return r.ok && SHA.test(sha) ? sha : void 0;
|
|
10774
11105
|
}
|
|
10775
|
-
function defaultBase(cwd) {
|
|
11106
|
+
function defaultBase(cwd, caller) {
|
|
10776
11107
|
const r = git(cwd, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"]);
|
|
10777
11108
|
const ref = r.stdout.trim();
|
|
10778
11109
|
if (!r.ok || !ref.startsWith("refs/remotes/")) {
|
|
10779
11110
|
throw new TemplateError(
|
|
10780
|
-
|
|
11111
|
+
`the remote's default branch cannot be resolved (refs/remotes/origin/HEAD is not set): ${phrase(caller, "base")}, or run git remote set-head origin --auto`
|
|
10781
11112
|
);
|
|
10782
11113
|
}
|
|
10783
11114
|
return ref.slice("refs/remotes/".length);
|
|
@@ -10789,12 +11120,12 @@ function localCounts(cwd, flag, file2) {
|
|
|
10789
11120
|
throw new TemplateError(`${flag} ${file2}: ${e.message}`);
|
|
10790
11121
|
}
|
|
10791
11122
|
}
|
|
10792
|
-
function testsFrom(cwd, tests) {
|
|
11123
|
+
function testsFrom(cwd, tests, caller) {
|
|
10793
11124
|
if (tests.kind === "ci") {
|
|
10794
11125
|
return tests.read.status === "settled" ? { run: tests.read.run, notes: tests.read.notes } : { notes: tests.read.notes };
|
|
10795
11126
|
}
|
|
10796
11127
|
if (tests.kind === "local") {
|
|
10797
|
-
const counts2 = localCounts(cwd, "
|
|
11128
|
+
const counts2 = localCounts(cwd, phrase(caller, "localFile"), tests.junit);
|
|
10798
11129
|
return {
|
|
10799
11130
|
run: {
|
|
10800
11131
|
command: tests.command,
|
|
@@ -10803,7 +11134,7 @@ function testsFrom(cwd, tests) {
|
|
|
10803
11134
|
record: { kind: "local" }
|
|
10804
11135
|
},
|
|
10805
11136
|
notes: [
|
|
10806
|
-
`tests: ${counts2.executed} executed and ${counts2.failed} failed in ${tests.junit}, declared as a local run (record {"kind": "local"}). The gate reports them unverifiable (local_run_not_in_record) until CI publishes a record: publish one (docs/ci-test-records.md), then
|
|
11137
|
+
`tests: ${counts2.executed} executed and ${counts2.failed} failed in ${tests.junit}, declared as a local run (record {"kind": "local"}). The gate reports them unverifiable (local_run_not_in_record) until CI publishes a record: publish one (docs/ci-test-records.md), then ${phrase(caller, "ciOnceFinished")}. Until then, say in the report that CI publishes no test record, and state no count in prose that the block does not declare.`
|
|
10807
11138
|
]
|
|
10808
11139
|
};
|
|
10809
11140
|
}
|
|
@@ -10821,18 +11152,18 @@ function testsFrom(cwd, tests) {
|
|
|
10821
11152
|
]
|
|
10822
11153
|
};
|
|
10823
11154
|
}
|
|
10824
|
-
function originRepository(cwd) {
|
|
11155
|
+
function originRepository(cwd, caller) {
|
|
10825
11156
|
const r = git(cwd, ["remote", "get-url", "origin"]);
|
|
10826
11157
|
const url2 = r.stdout.trim();
|
|
10827
11158
|
if (!r.ok || url2 === "") {
|
|
10828
|
-
throw new TemplateError(
|
|
11159
|
+
throw new TemplateError(`there is no origin remote: ${phrase(caller, "repo")}`);
|
|
10829
11160
|
}
|
|
10830
11161
|
const m = /^(?:https:\/\/(?:[^@/]+@)?|ssh:\/\/git@|git@)github\.com[:/]([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?$/.exec(
|
|
10831
11162
|
url2
|
|
10832
11163
|
);
|
|
10833
11164
|
if (m === null) {
|
|
10834
11165
|
throw new TemplateError(
|
|
10835
|
-
`origin ${url2} is not a github.com repository:
|
|
11166
|
+
`origin ${url2} is not a github.com repository: ${phrase(caller, "repo")}`
|
|
10836
11167
|
);
|
|
10837
11168
|
}
|
|
10838
11169
|
return `${m[1]}/${m[2]}`;
|
|
@@ -10857,7 +11188,7 @@ function headCommit(cwd) {
|
|
|
10857
11188
|
return head;
|
|
10858
11189
|
}
|
|
10859
11190
|
function templateBlock(options) {
|
|
10860
|
-
const { cwd } = options;
|
|
11191
|
+
const { cwd, caller } = options;
|
|
10861
11192
|
const head = headCommit(cwd);
|
|
10862
11193
|
const symbolic = git(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
10863
11194
|
if (!symbolic.ok) {
|
|
@@ -10866,8 +11197,10 @@ function templateBlock(options) {
|
|
|
10866
11197
|
);
|
|
10867
11198
|
}
|
|
10868
11199
|
const branch = symbolic.stdout.trim().replace(/^refs\/heads\//, "");
|
|
10869
|
-
if (options.base?.startsWith("-"))
|
|
10870
|
-
|
|
11200
|
+
if (options.base?.startsWith("-")) {
|
|
11201
|
+
throw new TemplateError(`${phrase(caller, "baseName")} ${options.base} is not a ref`);
|
|
11202
|
+
}
|
|
11203
|
+
const base = options.base ?? defaultBase(cwd, caller);
|
|
10871
11204
|
const baseCommit = commitOf(cwd, base);
|
|
10872
11205
|
if (baseCommit === void 0) throw new TemplateError(`the base ${base} cannot be resolved`);
|
|
10873
11206
|
const mb = git(cwd, ["merge-base", baseCommit, head]);
|
|
@@ -10890,7 +11223,7 @@ function templateBlock(options) {
|
|
|
10890
11223
|
compareCodeUnits
|
|
10891
11224
|
);
|
|
10892
11225
|
const block = { dunstan: BLOCK_VERSION, headCommit: head, filesChanged };
|
|
10893
|
-
const tests = options.tests === void 0 ? void 0 : testsFrom(cwd, options.tests);
|
|
11226
|
+
const tests = options.tests === void 0 ? void 0 : testsFrom(cwd, options.tests, caller);
|
|
10894
11227
|
if (tests?.run !== void 0) block.tests = [tests.run];
|
|
10895
11228
|
const read = readBlockContent(JSON.stringify(block));
|
|
10896
11229
|
if (read.status !== "found") {
|
|
@@ -10900,7 +11233,7 @@ function templateBlock(options) {
|
|
|
10900
11233
|
);
|
|
10901
11234
|
}
|
|
10902
11235
|
const notes = [
|
|
10903
|
-
`this block describes the local HEAD ${head} on ${branch}, against ${base} (merge base ${mergeBase}). Pushing more commits makes it stale, and the gate then reports head_mismatch:
|
|
11236
|
+
`this block describes the local HEAD ${head} on ${branch}, against ${base} (merge base ${mergeBase}). Pushing more commits makes it stale, and the gate then reports head_mismatch: ${phrase(caller, "again")} after your last push.`
|
|
10904
11237
|
];
|
|
10905
11238
|
if (filesChanged.length === 0) notes.push("HEAD changes no file against the merge base.");
|
|
10906
11239
|
const dirty = git(cwd, ["status", "--porcelain", "-z"]);
|
|
@@ -10908,9 +11241,7 @@ function templateBlock(options) {
|
|
|
10908
11241
|
notes.push("the working tree has changes that are not committed; they are not in this block.");
|
|
10909
11242
|
}
|
|
10910
11243
|
if (tests === void 0) {
|
|
10911
|
-
notes.push(
|
|
10912
|
-
"tests are not declared. To declare CI's counts, run dunstan template again with --tests-ci --tests-command <cmd> --tests-workflow <path> --tests-job <name> once CI has finished on your last push; it reads the JUnit record CI published (docs/ci-test-records.md). For a run made only locally, --tests-local <JUnit XML file> --tests-command <cmd> declares it as one. --tests-junit <file> with --tests-command, --tests-workflow, --tests-job, --tests-artifact and --tests-artifact-path declares a local file's counts against a CI record."
|
|
10913
|
-
);
|
|
11244
|
+
notes.push(phrase(caller, "noTests"));
|
|
10914
11245
|
} else {
|
|
10915
11246
|
notes.push(...tests.notes);
|
|
10916
11247
|
}
|
|
@@ -10932,27 +11263,642 @@ import { readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
|
10932
11263
|
import { basename } from "node:path";
|
|
10933
11264
|
import { parseArgs } from "node:util";
|
|
10934
11265
|
|
|
11266
|
+
// src/evidence/gitlab.ts
|
|
11267
|
+
var NO_HEAD_PIPELINE = "head_pipeline";
|
|
11268
|
+
var HEAD_PIPELINE_ELSEWHERE = "head_pipeline_not_at_head_commit";
|
|
11269
|
+
var CLOSE_EVENT_PAGES2 = 10;
|
|
11270
|
+
var SEGMENT = "[A-Za-z0-9_][A-Za-z0-9._-]{0,254}";
|
|
11271
|
+
var FULL_PATH = `${SEGMENT}(?:/${SEGMENT}){0,19}/[A-Za-z0-9._-]{1,255}`;
|
|
11272
|
+
var GITLAB_PROJECT = new RegExp(`^${FULL_PATH}$`);
|
|
11273
|
+
var ISSUE_REF2 = new RegExp(`^${FULL_PATH}#[1-9][0-9]{0,9}$`);
|
|
11274
|
+
var COMMIT = /^[0-9a-f]{40}$/;
|
|
11275
|
+
var TIMESTAMP = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?Z$/;
|
|
11276
|
+
var MERGE_REQUEST_URL = /^https?:\/\/[^/]+\/(.+)\/-\/merge_requests\/([1-9][0-9]{0,9})$/;
|
|
11277
|
+
var ShapeError2 = class extends Error {
|
|
11278
|
+
};
|
|
11279
|
+
function obj2(value) {
|
|
11280
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new ShapeError2();
|
|
11281
|
+
return value;
|
|
11282
|
+
}
|
|
11283
|
+
function arr2(value) {
|
|
11284
|
+
if (!Array.isArray(value)) throw new ShapeError2();
|
|
11285
|
+
return value;
|
|
11286
|
+
}
|
|
11287
|
+
function str2(value) {
|
|
11288
|
+
if (typeof value !== "string") throw new ShapeError2();
|
|
11289
|
+
return value;
|
|
11290
|
+
}
|
|
11291
|
+
function int2(value) {
|
|
11292
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) throw new ShapeError2();
|
|
11293
|
+
return value;
|
|
11294
|
+
}
|
|
11295
|
+
function bool(value) {
|
|
11296
|
+
if (typeof value !== "boolean") throw new ShapeError2();
|
|
11297
|
+
return value;
|
|
11298
|
+
}
|
|
11299
|
+
function strOrNull2(value) {
|
|
11300
|
+
return value === null || value === void 0 ? null : str2(value);
|
|
11301
|
+
}
|
|
11302
|
+
function timestamp2(value) {
|
|
11303
|
+
const text2 = str2(value);
|
|
11304
|
+
if (!TIMESTAMP.test(text2)) throw new ShapeError2();
|
|
11305
|
+
return text2;
|
|
11306
|
+
}
|
|
11307
|
+
function commitOid2(value) {
|
|
11308
|
+
const text2 = str2(value);
|
|
11309
|
+
if (!COMMIT.test(text2)) throw new ShapeError2();
|
|
11310
|
+
return text2;
|
|
11311
|
+
}
|
|
11312
|
+
function unreadable2(source) {
|
|
11313
|
+
return { status: "unreadable", source };
|
|
11314
|
+
}
|
|
11315
|
+
function byCodeUnits(a, b) {
|
|
11316
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
11317
|
+
}
|
|
11318
|
+
function projectPath(project) {
|
|
11319
|
+
return `/api/v4/projects/${encodeURIComponent(project)}`;
|
|
11320
|
+
}
|
|
11321
|
+
function mergeRequestPath(project, iid) {
|
|
11322
|
+
return `${projectPath(project)}/merge_requests/${iid}`;
|
|
11323
|
+
}
|
|
11324
|
+
async function readPages(client, kind, pathFor, map2, options = {}) {
|
|
11325
|
+
const out = [];
|
|
11326
|
+
for (let page = 1; page <= (options.maxPages ?? MAX_PAGES); page++) {
|
|
11327
|
+
const path = pathFor(page);
|
|
11328
|
+
const read = await client.read(kind, path, options.record === false ? { record: false } : {});
|
|
11329
|
+
if (!read.ok) return null;
|
|
11330
|
+
let items;
|
|
11331
|
+
try {
|
|
11332
|
+
items = arr2(read.json);
|
|
11333
|
+
out.push(...items.map((item) => map2(item, `GET ${path}`)));
|
|
11334
|
+
} catch (e) {
|
|
11335
|
+
if (!(e instanceof ShapeError2)) throw e;
|
|
11336
|
+
client.markParseError(`GET ${path}`);
|
|
11337
|
+
return null;
|
|
11338
|
+
}
|
|
11339
|
+
if (items.length < PER_PAGE) return out;
|
|
11340
|
+
}
|
|
11341
|
+
return null;
|
|
11342
|
+
}
|
|
11343
|
+
var pages = (path) => (page) => `${path}?per_page=${PER_PAGE}&page=${page}`;
|
|
11344
|
+
function headPipeline(value) {
|
|
11345
|
+
if (value === null || value === void 0) return null;
|
|
11346
|
+
const p = obj2(value);
|
|
11347
|
+
const id = int2(p.id);
|
|
11348
|
+
if (id < 1) throw new ShapeError2();
|
|
11349
|
+
return { id, sha: commitOid2(p.sha) };
|
|
11350
|
+
}
|
|
11351
|
+
function diffEntry(item) {
|
|
11352
|
+
const f = obj2(item);
|
|
11353
|
+
const path = str2(f.new_path);
|
|
11354
|
+
const previous = str2(f.old_path);
|
|
11355
|
+
const added = bool(f.new_file);
|
|
11356
|
+
const removed = bool(f.deleted_file);
|
|
11357
|
+
const renamed = bool(f.renamed_file);
|
|
11358
|
+
if ([added, removed, renamed].filter(Boolean).length > 1) throw new ShapeError2();
|
|
11359
|
+
if (added) return { path, status: "added" };
|
|
11360
|
+
if (removed) return { path, status: "removed" };
|
|
11361
|
+
if (renamed) return { path, status: "renamed", previousPath: previous };
|
|
11362
|
+
return { path, status: "modified" };
|
|
11363
|
+
}
|
|
11364
|
+
function mergeCommit(mr) {
|
|
11365
|
+
if (mr.merge_commit_sha !== null && mr.merge_commit_sha !== void 0) {
|
|
11366
|
+
return commitOid2(mr.merge_commit_sha);
|
|
11367
|
+
}
|
|
11368
|
+
if (mr.squash_commit_sha !== null && mr.squash_commit_sha !== void 0) {
|
|
11369
|
+
return commitOid2(mr.squash_commit_sha);
|
|
11370
|
+
}
|
|
11371
|
+
return null;
|
|
11372
|
+
}
|
|
11373
|
+
var STATES = {
|
|
11374
|
+
opened: { state: "open", merged: false },
|
|
11375
|
+
closed: { state: "closed", merged: false },
|
|
11376
|
+
merged: { state: "closed", merged: true }
|
|
11377
|
+
};
|
|
11378
|
+
async function readMergeRequest(client, project, iid, options = {}) {
|
|
11379
|
+
const path = mergeRequestPath(project, iid);
|
|
11380
|
+
const name = `merge request ${project}!${iid}`;
|
|
11381
|
+
const record2 = options.record === false ? { record: false } : {};
|
|
11382
|
+
const read = await client.read("pull_request", path, record2);
|
|
11383
|
+
if (!read.ok) {
|
|
11384
|
+
throw new PullRequestUnreadable(`cannot read ${name} (${read.error}); no record is written`);
|
|
11385
|
+
}
|
|
11386
|
+
let head;
|
|
11387
|
+
try {
|
|
11388
|
+
const mr = obj2(read.json);
|
|
11389
|
+
const mapped = STATES[str2(mr.state)];
|
|
11390
|
+
if (mapped === void 0) throw new ShapeError2();
|
|
11391
|
+
const number4 = int2(mr.iid);
|
|
11392
|
+
const full = str2(obj2(mr.references).full);
|
|
11393
|
+
const suffix = `!${number4}`;
|
|
11394
|
+
if (!full.endsWith(suffix)) throw new ShapeError2();
|
|
11395
|
+
const repository = full.slice(0, -suffix.length);
|
|
11396
|
+
if (!GITLAB_PROJECT.test(repository)) throw new ShapeError2();
|
|
11397
|
+
if (repository.toLowerCase() !== project.toLowerCase()) throw new ShapeError2();
|
|
11398
|
+
const changesCount = mr.changes_count;
|
|
11399
|
+
head = {
|
|
11400
|
+
evidence: {
|
|
11401
|
+
status: "ok",
|
|
11402
|
+
number: number4,
|
|
11403
|
+
state: mapped.state,
|
|
11404
|
+
merged: mapped.merged,
|
|
11405
|
+
mergedAt: mr.merged_at === null || mr.merged_at === void 0 ? null : timestamp2(mr.merged_at),
|
|
11406
|
+
headSha: commitOid2(mr.sha),
|
|
11407
|
+
mergeSha: mapped.merged ? mergeCommit(mr) : null
|
|
11408
|
+
},
|
|
11409
|
+
repository,
|
|
11410
|
+
baseRef: str2(mr.target_branch),
|
|
11411
|
+
body: strOrNull2(mr.description),
|
|
11412
|
+
changesCount: strOrNull2(changesCount),
|
|
11413
|
+
headPipeline: headPipeline(mr.head_pipeline)
|
|
11414
|
+
};
|
|
11415
|
+
} catch (e) {
|
|
11416
|
+
if (e instanceof ShapeError2) {
|
|
11417
|
+
throw new PullRequestUnreadable(
|
|
11418
|
+
`${name} answered in an unexpected shape; no record is written`
|
|
11419
|
+
);
|
|
11420
|
+
}
|
|
11421
|
+
throw e;
|
|
11422
|
+
}
|
|
11423
|
+
const listed2 = await readPages(client, "pull_request_files", pages(`${path}/diffs`), diffEntry, {
|
|
11424
|
+
record: options.record !== false
|
|
11425
|
+
});
|
|
11426
|
+
if (listed2 === null) {
|
|
11427
|
+
throw new PullRequestUnreadable(`cannot read the diffs of ${name}; no record is written`);
|
|
11428
|
+
}
|
|
11429
|
+
const unique = new Map(listed2.map((e) => [e.path, e]));
|
|
11430
|
+
const entries = [...unique.values()].sort((a, b) => byCodeUnits(a.path, b.path));
|
|
11431
|
+
const count = head.changesCount;
|
|
11432
|
+
const complete = count !== null && /^[0-9]+$/.test(count) && Number(count) === entries.length;
|
|
11433
|
+
return {
|
|
11434
|
+
evidence: { ...head.evidence, changedFiles: entries.length },
|
|
11435
|
+
repository: head.repository,
|
|
11436
|
+
baseRef: head.baseRef,
|
|
11437
|
+
body: head.body,
|
|
11438
|
+
files: { status: "ok", complete, entries },
|
|
11439
|
+
headPipeline: head.headPipeline
|
|
11440
|
+
};
|
|
11441
|
+
}
|
|
11442
|
+
function reportFromMergeRequestDescription(pr) {
|
|
11443
|
+
return {
|
|
11444
|
+
bytes: new TextEncoder().encode(pr.body ?? ""),
|
|
11445
|
+
source: {
|
|
11446
|
+
kind: "pr-body",
|
|
11447
|
+
locator: `GET ${mergeRequestPath(pr.repository, pr.evidence.number)}#description`
|
|
11448
|
+
}
|
|
11449
|
+
};
|
|
11450
|
+
}
|
|
11451
|
+
async function readClosingReferences2(client, project, iid) {
|
|
11452
|
+
const issues = await readPages(
|
|
11453
|
+
client,
|
|
11454
|
+
"closing_references",
|
|
11455
|
+
pages(`${mergeRequestPath(project, iid)}/closes_issues`),
|
|
11456
|
+
(item) => {
|
|
11457
|
+
const ref = str2(obj2(obj2(item).references).full);
|
|
11458
|
+
if (!ISSUE_REF2.test(ref)) throw new ShapeError2();
|
|
11459
|
+
return ref;
|
|
11460
|
+
}
|
|
11461
|
+
);
|
|
11462
|
+
if (issues === null) return unreadable2("closing_references");
|
|
11463
|
+
return { status: "ok", issues: [...new Set(issues)].sort(byCodeUnits) };
|
|
11464
|
+
}
|
|
11465
|
+
function answeredIssue2(json2, ref) {
|
|
11466
|
+
const full = str2(obj2(obj2(json2).references).full);
|
|
11467
|
+
if (!sameIssue(full, ref)) throw new ShapeError2();
|
|
11468
|
+
}
|
|
11469
|
+
function issueState(json2) {
|
|
11470
|
+
const state = str2(obj2(json2).state);
|
|
11471
|
+
if (state === "opened") return "open";
|
|
11472
|
+
if (state === "closed") return "closed";
|
|
11473
|
+
throw new ShapeError2();
|
|
11474
|
+
}
|
|
11475
|
+
async function isAncestor2(client, project, commit, head) {
|
|
11476
|
+
const path = `${projectPath(project)}/repository/merge_base?refs[]=${commit}&refs[]=${head}`;
|
|
11477
|
+
const read = await client.read("compare", path);
|
|
11478
|
+
if (!read.ok) return null;
|
|
11479
|
+
try {
|
|
11480
|
+
return commitOid2(obj2(read.json).id) === commit;
|
|
11481
|
+
} catch (e) {
|
|
11482
|
+
if (!(e instanceof ShapeError2)) throw e;
|
|
11483
|
+
client.markParseError(`GET ${path}`);
|
|
11484
|
+
return null;
|
|
11485
|
+
}
|
|
11486
|
+
}
|
|
11487
|
+
async function readIssue(client, ref) {
|
|
11488
|
+
const [repo, num] = ref.split("#");
|
|
11489
|
+
const path = `${projectPath(repo)}/issues/${num}`;
|
|
11490
|
+
return { read: await client.read("issue", path), path };
|
|
11491
|
+
}
|
|
11492
|
+
async function readReferences2(client, block, project, headSha, closing) {
|
|
11493
|
+
const toRead = (block.references ?? []).filter((r) => {
|
|
11494
|
+
if (r.relation === "cites") return true;
|
|
11495
|
+
if (!("issue" in r) || closing?.status !== "ok") return false;
|
|
11496
|
+
const target = qualifyIssue(r.issue, project);
|
|
11497
|
+
return !closing.issues.some((issue2) => sameIssue(issue2, target));
|
|
11498
|
+
});
|
|
11499
|
+
if (toRead.length === 0) return void 0;
|
|
11500
|
+
const out = [];
|
|
11501
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11502
|
+
const projectReadable = /* @__PURE__ */ new Map([[project.toLowerCase(), true]]);
|
|
11503
|
+
for (const reference of toRead) {
|
|
11504
|
+
if ("issue" in reference) {
|
|
11505
|
+
const ref2 = qualifyIssue(reference.issue, project);
|
|
11506
|
+
const key2 = `issue:${ref2.toLowerCase()}`;
|
|
11507
|
+
if (seen.has(key2)) continue;
|
|
11508
|
+
seen.add(key2);
|
|
11509
|
+
const repo = ref2.split("#")[0];
|
|
11510
|
+
let readable = projectReadable.get(repo.toLowerCase());
|
|
11511
|
+
if (readable === void 0) {
|
|
11512
|
+
readable = (await client.read("repository", projectPath(repo))).ok;
|
|
11513
|
+
projectReadable.set(repo.toLowerCase(), readable);
|
|
11514
|
+
}
|
|
11515
|
+
if (!readable) {
|
|
11516
|
+
out.push({ kind: "issue", ref: ref2, ...unreadable2("repository") });
|
|
11517
|
+
continue;
|
|
11518
|
+
}
|
|
11519
|
+
const { read: read2, path } = await readIssue(client, ref2);
|
|
11520
|
+
if (!read2.ok) {
|
|
11521
|
+
out.push({ kind: "issue", ref: ref2, ...unreadable2("issue") });
|
|
11522
|
+
continue;
|
|
11523
|
+
}
|
|
11524
|
+
try {
|
|
11525
|
+
answeredIssue2(read2.json, ref2);
|
|
11526
|
+
} catch (e) {
|
|
11527
|
+
if (!(e instanceof ShapeError2)) throw e;
|
|
11528
|
+
client.markParseError(`GET ${path}`);
|
|
11529
|
+
out.push({ kind: "issue", ref: ref2, ...unreadable2("issue") });
|
|
11530
|
+
continue;
|
|
11531
|
+
}
|
|
11532
|
+
out.push({ kind: "issue", ref: ref2, status: "ok", exists: true });
|
|
11533
|
+
continue;
|
|
11534
|
+
}
|
|
11535
|
+
const ref = reference.commit;
|
|
11536
|
+
const key = `commit:${ref}`;
|
|
11537
|
+
if (seen.has(key)) continue;
|
|
11538
|
+
seen.add(key);
|
|
11539
|
+
const read = await client.read("commit", `${projectPath(project)}/repository/commits/${ref}`);
|
|
11540
|
+
if (!read.ok && (read.status === 404 || read.status === 410)) {
|
|
11541
|
+
out.push({ kind: "commit", ref, status: "ok", exists: false, reachableFromHead: false });
|
|
11542
|
+
continue;
|
|
11543
|
+
}
|
|
11544
|
+
if (!read.ok) {
|
|
11545
|
+
out.push({ kind: "commit", ref, ...unreadable2("commit") });
|
|
11546
|
+
continue;
|
|
11547
|
+
}
|
|
11548
|
+
const reachable = ref === headSha ? true : await isAncestor2(client, project, ref, headSha);
|
|
11549
|
+
if (reachable === null) out.push({ kind: "commit", ref, ...unreadable2("compare") });
|
|
11550
|
+
else
|
|
11551
|
+
out.push({ kind: "commit", ref, status: "ok", exists: true, reachableFromHead: reachable });
|
|
11552
|
+
}
|
|
11553
|
+
return out;
|
|
11554
|
+
}
|
|
11555
|
+
async function readCloseBase2(client, project, mr) {
|
|
11556
|
+
const path = projectPath(project);
|
|
11557
|
+
const read = await client.read("repository", path);
|
|
11558
|
+
if (!read.ok) return unreadable2("repository");
|
|
11559
|
+
let defaultBranch;
|
|
11560
|
+
try {
|
|
11561
|
+
defaultBranch = strOrNull2(obj2(read.json).default_branch);
|
|
11562
|
+
} catch (e) {
|
|
11563
|
+
if (!(e instanceof ShapeError2)) throw e;
|
|
11564
|
+
client.markParseError(`GET ${path}`);
|
|
11565
|
+
return unreadable2("repository");
|
|
11566
|
+
}
|
|
11567
|
+
const commits = await readPages(
|
|
11568
|
+
client,
|
|
11569
|
+
"pull_request_commits",
|
|
11570
|
+
pages(`${mergeRequestPath(project, mr.evidence.number)}/commits`),
|
|
11571
|
+
(item) => commitOid2(obj2(item).id)
|
|
11572
|
+
);
|
|
11573
|
+
if (commits === null) return unreadable2("pull_request_commits");
|
|
11574
|
+
return {
|
|
11575
|
+
status: "ok",
|
|
11576
|
+
baseRef: mr.baseRef,
|
|
11577
|
+
defaultBranch,
|
|
11578
|
+
commits: [...new Set(commits)].sort(byCodeUnits)
|
|
11579
|
+
};
|
|
11580
|
+
}
|
|
11581
|
+
function closeEvent2(item) {
|
|
11582
|
+
const e = obj2(item);
|
|
11583
|
+
if (str2(e.state) !== "closed") return null;
|
|
11584
|
+
const at2 = timestamp2(e.created_at);
|
|
11585
|
+
const source = e.source_merge_request;
|
|
11586
|
+
if (source !== null && source !== void 0) {
|
|
11587
|
+
const m = obj2(source);
|
|
11588
|
+
const iid = int2(m.iid);
|
|
11589
|
+
const url2 = MERGE_REQUEST_URL.exec(str2(m.web_url));
|
|
11590
|
+
if (url2 === null || Number(url2[2]) !== iid) throw new ShapeError2();
|
|
11591
|
+
const ref = `${url2[1]}#${iid}`;
|
|
11592
|
+
if (!ISSUE_REF2.test(ref)) throw new ShapeError2();
|
|
11593
|
+
return { at: at2, closer: { kind: "pull_request", ref } };
|
|
11594
|
+
}
|
|
11595
|
+
if (e.source_commit !== null && e.source_commit !== void 0) {
|
|
11596
|
+
return { at: at2, closer: { kind: "commit", oid: commitOid2(e.source_commit) } };
|
|
11597
|
+
}
|
|
11598
|
+
return { at: at2, closer: null };
|
|
11599
|
+
}
|
|
11600
|
+
async function readCloseTarget2(client, ref) {
|
|
11601
|
+
const { read, path } = await readIssue(client, ref);
|
|
11602
|
+
if (!read.ok) return { ref, ...unreadable2("issue") };
|
|
11603
|
+
let state;
|
|
11604
|
+
try {
|
|
11605
|
+
answeredIssue2(read.json, ref);
|
|
11606
|
+
state = issueState(read.json);
|
|
11607
|
+
} catch (e) {
|
|
11608
|
+
if (!(e instanceof ShapeError2)) throw e;
|
|
11609
|
+
client.markParseError(`GET ${path}`);
|
|
11610
|
+
return { ref, ...unreadable2("issue") };
|
|
11611
|
+
}
|
|
11612
|
+
const events = await readPages(
|
|
11613
|
+
client,
|
|
11614
|
+
"issue",
|
|
11615
|
+
pages(`${path}/resource_state_events`),
|
|
11616
|
+
closeEvent2,
|
|
11617
|
+
{
|
|
11618
|
+
maxPages: CLOSE_EVENT_PAGES2
|
|
11619
|
+
}
|
|
11620
|
+
);
|
|
11621
|
+
if (events === null) return { ref, ...unreadable2("issue") };
|
|
11622
|
+
return {
|
|
11623
|
+
ref,
|
|
11624
|
+
status: "ok",
|
|
11625
|
+
type: "issue",
|
|
11626
|
+
state,
|
|
11627
|
+
events: events.filter((e) => e !== null)
|
|
11628
|
+
};
|
|
11629
|
+
}
|
|
11630
|
+
async function readCloseEvents2(client, project, mr, targets) {
|
|
11631
|
+
const pullRequest = await readCloseBase2(client, project, mr);
|
|
11632
|
+
const read = [];
|
|
11633
|
+
for (const target of targets) read.push(await readCloseTarget2(client, target));
|
|
11634
|
+
return { pullRequest, targets: read };
|
|
11635
|
+
}
|
|
11636
|
+
function pipelineAt(mr, headCommit2) {
|
|
11637
|
+
if (mr.headPipeline === null) return { status: "unpopulated", field: NO_HEAD_PIPELINE };
|
|
11638
|
+
if (mr.headPipeline.sha !== headCommit2) {
|
|
11639
|
+
return { status: "unpopulated", field: HEAD_PIPELINE_ELSEWHERE };
|
|
11640
|
+
}
|
|
11641
|
+
return mr.headPipeline;
|
|
11642
|
+
}
|
|
11643
|
+
function listedJob(item, locator) {
|
|
11644
|
+
const j = obj2(item);
|
|
11645
|
+
const id = int2(j.id);
|
|
11646
|
+
if (id < 1) throw new ShapeError2();
|
|
11647
|
+
const files = j.artifacts === null || j.artifacts === void 0 ? [] : arr2(j.artifacts);
|
|
11648
|
+
const archive = files.some((f) => str2(obj2(f).file_type) === "archive");
|
|
11649
|
+
const expires = j.artifacts_expire_at;
|
|
11650
|
+
return {
|
|
11651
|
+
id,
|
|
11652
|
+
name: str2(j.name),
|
|
11653
|
+
status: str2(j.status),
|
|
11654
|
+
archive,
|
|
11655
|
+
expireAt: expires === null || expires === void 0 ? null : timestamp2(expires),
|
|
11656
|
+
locator
|
|
11657
|
+
};
|
|
11658
|
+
}
|
|
11659
|
+
var GITLAB_JOB_STATUSES = {
|
|
11660
|
+
success: { status: "completed", conclusion: "success" },
|
|
11661
|
+
failed: { status: "completed", conclusion: "failure" },
|
|
11662
|
+
canceled: { status: "completed", conclusion: "cancelled" },
|
|
11663
|
+
skipped: { status: "completed", conclusion: "skipped" },
|
|
11664
|
+
manual: { status: "completed", conclusion: "skipped" },
|
|
11665
|
+
created: { status: "queued", conclusion: null },
|
|
11666
|
+
pending: { status: "queued", conclusion: null },
|
|
11667
|
+
preparing: { status: "queued", conclusion: null },
|
|
11668
|
+
waiting_for_resource: { status: "queued", conclusion: null },
|
|
11669
|
+
scheduled: { status: "queued", conclusion: null },
|
|
11670
|
+
running: { status: "in_progress", conclusion: null }
|
|
11671
|
+
};
|
|
11672
|
+
function checkRunOf(job) {
|
|
11673
|
+
if (!Object.hasOwn(GITLAB_JOB_STATUSES, job.status)) return null;
|
|
11674
|
+
const mapped = GITLAB_JOB_STATUSES[job.status];
|
|
11675
|
+
return { id: job.id, name: job.name, ...mapped };
|
|
11676
|
+
}
|
|
11677
|
+
function jobsReader(client, project, pipeline) {
|
|
11678
|
+
let read;
|
|
11679
|
+
return () => {
|
|
11680
|
+
read ??= readPages(
|
|
11681
|
+
client,
|
|
11682
|
+
"check_runs",
|
|
11683
|
+
(page) => `${projectPath(project)}/pipelines/${pipeline}/jobs?include_retried=false&per_page=${PER_PAGE}&page=${page}`,
|
|
11684
|
+
listedJob
|
|
11685
|
+
);
|
|
11686
|
+
return read;
|
|
11687
|
+
};
|
|
11688
|
+
}
|
|
11689
|
+
async function readCheckRuns2(client, project, pipeline, jobs, excludedIds) {
|
|
11690
|
+
const listed2 = await jobs();
|
|
11691
|
+
if (listed2 === null) return unreadable2("check_runs");
|
|
11692
|
+
const bridgesPath = `${projectPath(project)}/pipelines/${pipeline.id}/bridges`;
|
|
11693
|
+
const bridges = await readPages(client, "check_runs", pages(bridgesPath), listedJob);
|
|
11694
|
+
if (bridges === null) return unreadable2("check_runs");
|
|
11695
|
+
const runs = [];
|
|
11696
|
+
for (const job of [...listed2, ...bridges]) {
|
|
11697
|
+
const run = checkRunOf(job);
|
|
11698
|
+
if (run === null) {
|
|
11699
|
+
client.markParseError(job.locator);
|
|
11700
|
+
return unreadable2("check_runs");
|
|
11701
|
+
}
|
|
11702
|
+
runs.push(run);
|
|
11703
|
+
}
|
|
11704
|
+
return {
|
|
11705
|
+
status: "ok",
|
|
11706
|
+
commit: pipeline.sha,
|
|
11707
|
+
excludedIds: [...new Set(excludedIds)].sort((a, b) => a - b),
|
|
11708
|
+
runs: [...new Map(runs.map((r) => [r.id, r])).values()].sort((a, b) => a.id - b.id)
|
|
11709
|
+
};
|
|
11710
|
+
}
|
|
11711
|
+
async function readCiConfigPath(client, project) {
|
|
11712
|
+
const path = projectPath(project);
|
|
11713
|
+
const read = await client.read("repository", path);
|
|
11714
|
+
if (!read.ok) return null;
|
|
11715
|
+
try {
|
|
11716
|
+
const p = obj2(read.json);
|
|
11717
|
+
if (!("ci_config_path" in p)) throw new ShapeError2();
|
|
11718
|
+
const config2 = strOrNull2(p.ci_config_path);
|
|
11719
|
+
return config2 === null || config2 === "" ? ".gitlab-ci.yml" : config2;
|
|
11720
|
+
} catch (e) {
|
|
11721
|
+
if (!(e instanceof ShapeError2)) throw e;
|
|
11722
|
+
client.markParseError(`GET ${path}`);
|
|
11723
|
+
return null;
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
async function readTestRecord2(client, project, record2, jobs, ciConfig) {
|
|
11727
|
+
const config2 = await ciConfig();
|
|
11728
|
+
if (config2 === null) return { record: record2, ...unreadable2("repository") };
|
|
11729
|
+
if (config2 !== record2.workflow) return { record: record2, status: "ok", runs: [] };
|
|
11730
|
+
const listed2 = await jobs();
|
|
11731
|
+
if (listed2 === null) return { record: record2, ...unreadable2("check_runs") };
|
|
11732
|
+
const runs = [];
|
|
11733
|
+
const named = listed2.filter((j) => j.name === record2.job).sort((a, b) => a.id - b.id);
|
|
11734
|
+
for (const job of named) {
|
|
11735
|
+
if (record2.artifact !== "archive") continue;
|
|
11736
|
+
if (!job.archive) {
|
|
11737
|
+
if (job.expireAt !== null) return { record: record2, ...unreadable2("artifact") };
|
|
11738
|
+
continue;
|
|
11739
|
+
}
|
|
11740
|
+
const zipPath = `${projectPath(project)}/jobs/${job.id}/artifacts`;
|
|
11741
|
+
const download = await client.read("artifact", zipPath, {
|
|
11742
|
+
json: false,
|
|
11743
|
+
locatorSuffix: `#${record2.path}`
|
|
11744
|
+
});
|
|
11745
|
+
if (!download.ok) return { record: record2, ...unreadable2("artifact") };
|
|
11746
|
+
try {
|
|
11747
|
+
const file2 = readZipEntry(download.body, record2.path, client.maxEntryBytes);
|
|
11748
|
+
if (file2 === null) continue;
|
|
11749
|
+
const counts = countJunit(new TextDecoder("utf-8").decode(file2));
|
|
11750
|
+
runs.push({ runId: job.id, runAttempt: 1, ...counts });
|
|
11751
|
+
} catch (e) {
|
|
11752
|
+
if (!(e instanceof ZipError || e instanceof JunitError)) throw e;
|
|
11753
|
+
client.markParseError(`GET ${zipPath}#${record2.path}`);
|
|
11754
|
+
return { record: record2, ...unreadable2("artifact") };
|
|
11755
|
+
}
|
|
11756
|
+
}
|
|
11757
|
+
return { record: record2, status: "ok", runs };
|
|
11758
|
+
}
|
|
11759
|
+
var GITLAB_DEPLOYMENT_STATUSES = {
|
|
11760
|
+
success: true,
|
|
11761
|
+
created: false,
|
|
11762
|
+
running: false,
|
|
11763
|
+
failed: false,
|
|
11764
|
+
canceled: false,
|
|
11765
|
+
skipped: false,
|
|
11766
|
+
blocked: false
|
|
11767
|
+
};
|
|
11768
|
+
function second2(t) {
|
|
11769
|
+
return t.slice(0, 19);
|
|
11770
|
+
}
|
|
11771
|
+
function listedDeployment(environment) {
|
|
11772
|
+
return (item) => {
|
|
11773
|
+
const d = obj2(item);
|
|
11774
|
+
const id = int2(d.id);
|
|
11775
|
+
if (id < 1) throw new ShapeError2();
|
|
11776
|
+
if (str2(obj2(d.environment).name) !== environment) throw new ShapeError2();
|
|
11777
|
+
const status = str2(d.status);
|
|
11778
|
+
if (!Object.hasOwn(GITLAB_DEPLOYMENT_STATUSES, status)) throw new ShapeError2();
|
|
11779
|
+
const updatedAt = timestamp2(d.updated_at);
|
|
11780
|
+
return {
|
|
11781
|
+
id,
|
|
11782
|
+
sha: commitOid2(d.sha),
|
|
11783
|
+
createdAt: timestamp2(d.created_at),
|
|
11784
|
+
successAt: GITLAB_DEPLOYMENT_STATUSES[status] === true ? updatedAt : null
|
|
11785
|
+
};
|
|
11786
|
+
};
|
|
11787
|
+
}
|
|
11788
|
+
async function readDeployments2(client, project, pr, deployedAt) {
|
|
11789
|
+
const environment = deployedAt.environment;
|
|
11790
|
+
const listed2 = await readPages(
|
|
11791
|
+
client,
|
|
11792
|
+
"deployments",
|
|
11793
|
+
(page) => `${projectPath(project)}/deployments?environment=${encodeURIComponent(environment)}&order_by=id&sort=desc&per_page=${PER_PAGE}&page=${page}`,
|
|
11794
|
+
listedDeployment(environment)
|
|
11795
|
+
);
|
|
11796
|
+
if (listed2 === null) return { environment, ...unreadable2("deployments") };
|
|
11797
|
+
const window = pr.merged && pr.mergedAt !== null && pr.mergeSha !== null ? { from: second2(pr.mergedAt), to: second2(deployedAt.at) } : null;
|
|
11798
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
11799
|
+
for (const d of listed2) {
|
|
11800
|
+
const created = second2(d.createdAt);
|
|
11801
|
+
const inWindow = window !== null && created >= window.from && created <= window.to;
|
|
11802
|
+
if (d.sha === pr.headSha || inWindow) candidates.set(d.id, d);
|
|
11803
|
+
}
|
|
11804
|
+
const deployments = [];
|
|
11805
|
+
const related = /* @__PURE__ */ new Map();
|
|
11806
|
+
for (const d of [...candidates.values()].sort((a, b) => a.id - b.id)) {
|
|
11807
|
+
let relation;
|
|
11808
|
+
if (d.sha === pr.headSha) relation = "head";
|
|
11809
|
+
else if (d.sha === pr.mergeSha) relation = "merge";
|
|
11810
|
+
else if (!pr.merged || pr.mergeSha === null) relation = "unrelated";
|
|
11811
|
+
else {
|
|
11812
|
+
let known = related.get(d.sha);
|
|
11813
|
+
if (known === void 0) {
|
|
11814
|
+
const ancestor = await isAncestor2(client, project, pr.mergeSha, d.sha);
|
|
11815
|
+
known = ancestor === null ? "unknown" : ancestor ? "descendant" : "unrelated";
|
|
11816
|
+
related.set(d.sha, known);
|
|
11817
|
+
}
|
|
11818
|
+
relation = known;
|
|
11819
|
+
}
|
|
11820
|
+
deployments.push({ id: d.id, sha: d.sha, relation, successAt: d.successAt });
|
|
11821
|
+
}
|
|
11822
|
+
return { environment, status: "ok", deployments };
|
|
11823
|
+
}
|
|
11824
|
+
async function readGitLabEvidence(client, input2) {
|
|
11825
|
+
const { block, repository } = input2;
|
|
11826
|
+
const mr = input2.pullRequest;
|
|
11827
|
+
if (mr.files === void 0) throw new Error("the GitLab reader reads a merge request it read");
|
|
11828
|
+
const pr = mr.evidence;
|
|
11829
|
+
const evidence = { pullRequest: pr, sources: [] };
|
|
11830
|
+
if (block !== null) {
|
|
11831
|
+
evidence.files = mr.files;
|
|
11832
|
+
if ((block.references ?? []).some((r) => r.relation === "closes")) {
|
|
11833
|
+
evidence.closingReferences = await readClosingReferences2(client, repository, pr.number);
|
|
11834
|
+
}
|
|
11835
|
+
const references = await readReferences2(
|
|
11836
|
+
client,
|
|
11837
|
+
block,
|
|
11838
|
+
repository,
|
|
11839
|
+
pr.headSha,
|
|
11840
|
+
evidence.closingReferences
|
|
11841
|
+
);
|
|
11842
|
+
if (references !== void 0) evidence.references = references;
|
|
11843
|
+
const targets = closeTargets(block, repository, evidence.closingReferences, references);
|
|
11844
|
+
if (targets.length > 0) {
|
|
11845
|
+
evidence.closeEvents = await readCloseEvents2(client, repository, mr, targets);
|
|
11846
|
+
}
|
|
11847
|
+
const pipeline = pipelineAt(mr, block.headCommit);
|
|
11848
|
+
const jobs = "id" in pipeline ? jobsReader(client, repository, pipeline.id) : null;
|
|
11849
|
+
if (block.checks !== void 0) {
|
|
11850
|
+
evidence.checkRuns = "id" in pipeline && jobs !== null ? await readCheckRuns2(client, repository, pipeline, jobs, input2.excludedCheckRunIds ?? []) : pipeline;
|
|
11851
|
+
}
|
|
11852
|
+
const records = /* @__PURE__ */ new Map();
|
|
11853
|
+
for (const test of block.tests ?? []) {
|
|
11854
|
+
if (test.record.kind !== "junit") continue;
|
|
11855
|
+
const record2 = test.record;
|
|
11856
|
+
records.set(canonicalize(record2), record2);
|
|
11857
|
+
}
|
|
11858
|
+
if (records.size > 0) {
|
|
11859
|
+
evidence.testRecords = [];
|
|
11860
|
+
let config2;
|
|
11861
|
+
const ciConfig = () => {
|
|
11862
|
+
config2 ??= readCiConfigPath(client, repository);
|
|
11863
|
+
return config2;
|
|
11864
|
+
};
|
|
11865
|
+
for (const record2 of records.values()) {
|
|
11866
|
+
evidence.testRecords.push(
|
|
11867
|
+
"id" in pipeline && jobs !== null ? await readTestRecord2(client, repository, record2, jobs, ciConfig) : { record: record2, ...pipeline }
|
|
11868
|
+
);
|
|
11869
|
+
}
|
|
11870
|
+
}
|
|
11871
|
+
if (block.deployedAt !== void 0) {
|
|
11872
|
+
evidence.deployments = [await readDeployments2(client, repository, pr, block.deployedAt)];
|
|
11873
|
+
}
|
|
11874
|
+
}
|
|
11875
|
+
evidence.sources = client.sortedSources();
|
|
11876
|
+
return evidence;
|
|
11877
|
+
}
|
|
11878
|
+
|
|
10935
11879
|
// src/evidence/http.ts
|
|
10936
11880
|
var DEFAULT_API = "https://api.github.com";
|
|
10937
11881
|
var MAX_ARCHIVE_BYTES = MAX_ENTRY_BYTES + 8 * 1024 * 1024;
|
|
10938
11882
|
var MAX_BODY_BYTES = 48 * 1024 * 1024;
|
|
10939
11883
|
var EMPTY = new Uint8Array();
|
|
10940
|
-
var
|
|
11884
|
+
var ForgeClient = class {
|
|
10941
11885
|
sources = /* @__PURE__ */ new Map();
|
|
10942
11886
|
maxArchiveBytes;
|
|
10943
11887
|
maxEntryBytes;
|
|
10944
11888
|
#allocate;
|
|
10945
11889
|
#token;
|
|
10946
11890
|
#baseUrl;
|
|
11891
|
+
#profileHeaders;
|
|
10947
11892
|
#fetch;
|
|
10948
11893
|
#now;
|
|
10949
11894
|
#sleep;
|
|
10950
11895
|
#warn;
|
|
10951
11896
|
#maxAttempts;
|
|
10952
11897
|
#maxWaitMs;
|
|
10953
|
-
constructor(options
|
|
11898
|
+
constructor(options, profile) {
|
|
10954
11899
|
this.#token = options.token;
|
|
10955
|
-
this.#baseUrl = options.baseUrl ??
|
|
11900
|
+
this.#baseUrl = options.baseUrl ?? profile.baseUrl;
|
|
11901
|
+
this.#profileHeaders = profile.headers;
|
|
10956
11902
|
this.#fetch = options.fetch ?? fetch;
|
|
10957
11903
|
this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
10958
11904
|
this.#sleep = options.sleep ?? ((ms) => new Promise((resolve5) => setTimeout(resolve5, ms)));
|
|
@@ -11029,8 +11975,7 @@ var GitHubClient = class {
|
|
|
11029
11975
|
}
|
|
11030
11976
|
async #fetchWithRetry(method, path, body, locator, budget) {
|
|
11031
11977
|
const headers = {
|
|
11032
|
-
|
|
11033
|
-
"x-github-api-version": "2022-11-28",
|
|
11978
|
+
...this.#profileHeaders,
|
|
11034
11979
|
"user-agent": `dunstan/${CHECKER_VERSION}`
|
|
11035
11980
|
};
|
|
11036
11981
|
if (this.hasToken) headers.authorization = `Bearer ${this.#token}`;
|
|
@@ -11073,6 +12018,25 @@ var GitHubClient = class {
|
|
|
11073
12018
|
}
|
|
11074
12019
|
}
|
|
11075
12020
|
};
|
|
12021
|
+
var GitHubClient = class extends ForgeClient {
|
|
12022
|
+
constructor(options = {}) {
|
|
12023
|
+
super(options, {
|
|
12024
|
+
baseUrl: DEFAULT_API,
|
|
12025
|
+
headers: { accept: "application/vnd.github+json", "x-github-api-version": "2022-11-28" }
|
|
12026
|
+
});
|
|
12027
|
+
}
|
|
12028
|
+
};
|
|
12029
|
+
var DEFAULT_GITLAB_HOST = "gitlab.com";
|
|
12030
|
+
var GITLAB_HOST = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
|
|
12031
|
+
var GitLabClient = class extends ForgeClient {
|
|
12032
|
+
host;
|
|
12033
|
+
constructor(options = {}) {
|
|
12034
|
+
const host = options.host ?? DEFAULT_GITLAB_HOST;
|
|
12035
|
+
if (!GITLAB_HOST.test(host)) throw new Error("a GitLab host is a lowercase DNS name");
|
|
12036
|
+
super(options, { baseUrl: `https://${host}`, headers: { accept: "application/json" } });
|
|
12037
|
+
this.host = host;
|
|
12038
|
+
}
|
|
12039
|
+
};
|
|
11076
12040
|
async function cancel(body) {
|
|
11077
12041
|
await body?.cancel().catch(() => {
|
|
11078
12042
|
});
|
|
@@ -11684,7 +12648,7 @@ function precisionText(precision) {
|
|
|
11684
12648
|
}
|
|
11685
12649
|
|
|
11686
12650
|
// src/advisory/advise.ts
|
|
11687
|
-
var COMPARISON_VERSION = "0.
|
|
12651
|
+
var COMPARISON_VERSION = "0.5.0";
|
|
11688
12652
|
var COMPARISON = { version: COMPARISON_VERSION };
|
|
11689
12653
|
var HEX = /^[0-9a-f]{7,40}$/;
|
|
11690
12654
|
function blockFor(kind, value, headSha) {
|
|
@@ -11845,7 +12809,7 @@ function advisoryDigest(section) {
|
|
|
11845
12809
|
|
|
11846
12810
|
// src/advisory/extract.ts
|
|
11847
12811
|
var words = (list) => new Set(list);
|
|
11848
|
-
var phrases = (list) => list.map((
|
|
12812
|
+
var phrases = (list) => list.map((phrase2) => phrase2.split(" "));
|
|
11849
12813
|
var V = GRAMMAR.verbs;
|
|
11850
12814
|
var FILE_VERBS = words(V.file);
|
|
11851
12815
|
var CLOSE_VERBS = words(V.close);
|
|
@@ -12100,7 +13064,7 @@ function clausesOf(prep) {
|
|
|
12100
13064
|
}
|
|
12101
13065
|
var ISSUE = /^(?:[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100})?#[1-9][0-9]{0,9}$/;
|
|
12102
13066
|
var ISSUE_URL = /^https?:\/\/github\.com\/([A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100})\/(?:issues|pull)\/([1-9][0-9]{0,9})\/?$/;
|
|
12103
|
-
var
|
|
13067
|
+
var TIMESTAMP2 = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?Z$/;
|
|
12104
13068
|
function repoPath(token, inCode) {
|
|
12105
13069
|
const path = token.replace(/^\.\//, "").replace(/(?::[0-9]+){1,2}$/, "").replace(/\([0-9]+(?:,[0-9]+)?\)?$/, "");
|
|
12106
13070
|
if (!/^\.?[A-Za-z0-9_][A-Za-z0-9_.+/-]*$/.test(path)) return void 0;
|
|
@@ -12128,7 +13092,7 @@ function classify(word) {
|
|
|
12128
13092
|
if (word.atom === "code" && /\s/.test(word.text)) return { cls: "word" };
|
|
12129
13093
|
const t = word.text;
|
|
12130
13094
|
if (ISSUE.test(t)) return { cls: "issue", value: t };
|
|
12131
|
-
if (
|
|
13095
|
+
if (TIMESTAMP2.test(t)) return { cls: "timestamp", value: t };
|
|
12132
13096
|
if (/^[0-9a-fA-F]{7,40}$/.test(t) && /[0-9]/.test(t) && /[a-fA-F]/.test(t)) {
|
|
12133
13097
|
return { cls: "sha", value: t.toLowerCase() };
|
|
12134
13098
|
}
|
|
@@ -12156,7 +13120,7 @@ var Clause = class {
|
|
|
12156
13120
|
}
|
|
12157
13121
|
// One of the phrases starts at k.
|
|
12158
13122
|
phraseAt(k, list) {
|
|
12159
|
-
return list.some((
|
|
13123
|
+
return list.some((phrase2) => phrase2.every((w, n) => this.lower(k + n) === w));
|
|
12160
13124
|
}
|
|
12161
13125
|
// One of the phrases starts at or after `from` and before `to`.
|
|
12162
13126
|
holds(list, from = 0, to = this.words.length) {
|
|
@@ -12193,7 +13157,7 @@ var Clause = class {
|
|
|
12193
13157
|
// An analogue phrase starts at k and ends before j.
|
|
12194
13158
|
analogue(k, j) {
|
|
12195
13159
|
return ANALOGUES.some(
|
|
12196
|
-
(
|
|
13160
|
+
(phrase2) => k + phrase2.length <= j && phrase2.every((w, n) => this.lower(k + n) === w)
|
|
12197
13161
|
);
|
|
12198
13162
|
}
|
|
12199
13163
|
// The nearest verb to the left of j, if no negation, modal or infinitive stands between, and it
|
|
@@ -13071,7 +14035,7 @@ function lookup(type, evidence) {
|
|
|
13071
14035
|
}
|
|
13072
14036
|
return { entries, duplicates };
|
|
13073
14037
|
}
|
|
13074
|
-
var
|
|
14038
|
+
var ISSUE_REF3 = /^(?:[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100})?#[1-9][0-9]{0,9}$/;
|
|
13075
14039
|
function combinedCandidates(typed, floor) {
|
|
13076
14040
|
const exact = typed.filter((c) => c.score === null);
|
|
13077
14041
|
return exact.length > 0 ? exact : typed.filter((c) => c.score !== null && c.score >= floor);
|
|
@@ -13145,7 +14109,7 @@ function checkReaderClaim(claim2, evidence, repository) {
|
|
|
13145
14109
|
return unverifiable2("no_comparable_record_field");
|
|
13146
14110
|
}
|
|
13147
14111
|
if (claim2.kind === "reference_closes") {
|
|
13148
|
-
if (!
|
|
14112
|
+
if (!ISSUE_REF3.test(declared)) return unverifiable2("no_comparable_record_field");
|
|
13149
14113
|
const head = evidence.pullRequest.headSha;
|
|
13150
14114
|
const row2 = checkReferences(closesBlock(declared, head), evidence, repository)[0];
|
|
13151
14115
|
if (row2 === void 0) return unverifiable2("no_comparable_record_field");
|
|
@@ -13188,7 +14152,7 @@ function checkReaderClaim(claim2, evidence, repository) {
|
|
|
13188
14152
|
case "timeline_event": {
|
|
13189
14153
|
const ref = declared;
|
|
13190
14154
|
const events = observed;
|
|
13191
|
-
const match =
|
|
14155
|
+
const match = ISSUE_REF3.test(ref) ? events.find((e) => e.ref !== void 0 && sameIssue(qualifyIssue(ref, repository), e.ref)) : void 0;
|
|
13192
14156
|
return match === void 0 ? unverifiable2("declared_item_not_among_candidates") : verdictOf("pass", { event: match.event, ref: match.ref ?? null });
|
|
13193
14157
|
}
|
|
13194
14158
|
case "check_run": {
|
|
@@ -13277,7 +14241,7 @@ function claimsFor(block, evidence, repository) {
|
|
|
13277
14241
|
function statementSubjects(block, subject) {
|
|
13278
14242
|
const subjects = [
|
|
13279
14243
|
{
|
|
13280
|
-
name: `git+https
|
|
14244
|
+
name: `git+https://${subject.forge?.host ?? "github.com"}/${subject.repository}@${subject.headSha}`,
|
|
13281
14245
|
digest: { gitCommit: subject.headSha }
|
|
13282
14246
|
}
|
|
13283
14247
|
];
|
|
@@ -13301,6 +14265,7 @@ function buildRecord(input2) {
|
|
|
13301
14265
|
headSha: pr.headSha,
|
|
13302
14266
|
mergeSha: pr.mergeSha
|
|
13303
14267
|
};
|
|
14268
|
+
if (input2.forge !== void 0) subject.forge = { kind: input2.forge.kind, host: input2.forge.host };
|
|
13304
14269
|
const claims = claimsFor(block, evidence, repository);
|
|
13305
14270
|
const readerClaims = input2.readerClaims?.map((c) => readReaderClaim(c, evidence, repository));
|
|
13306
14271
|
const advisory = input2.advisory === void 0 ? void 0 : advisorySection(input2.advisory, evidence, repository, input2.pathsAtHead ?? []);
|
|
@@ -13325,6 +14290,13 @@ function buildRecord(input2) {
|
|
|
13325
14290
|
assurance: input2.assurance ?? { status: "unsigned", issuer: "self-generated" }
|
|
13326
14291
|
}
|
|
13327
14292
|
};
|
|
14293
|
+
if (input2.forge !== void 0) {
|
|
14294
|
+
if (readerClaims !== void 0 || advisory !== void 0) {
|
|
14295
|
+
throw new Error("a GitLab record carries no reader claims or advisories in this draft");
|
|
14296
|
+
}
|
|
14297
|
+
record2.predicateType = GITLAB_DRAFT_PREDICATE_TYPE;
|
|
14298
|
+
record2.predicate.spec = GITLAB_DRAFT_SPEC_VERSION;
|
|
14299
|
+
}
|
|
13328
14300
|
if (readerClaims !== void 0 || advisory !== void 0) {
|
|
13329
14301
|
record2.predicateType = DRAFT_PREDICATE_TYPE;
|
|
13330
14302
|
record2.predicate.spec = DRAFT_SPEC_VERSION;
|
|
@@ -13353,6 +14325,16 @@ function githubReaders(client) {
|
|
|
13353
14325
|
restart: () => client.sources.clear()
|
|
13354
14326
|
};
|
|
13355
14327
|
}
|
|
14328
|
+
function gitlabReaders(client) {
|
|
14329
|
+
return {
|
|
14330
|
+
pullRequest: (project, iid, options) => readMergeRequest(client, project, iid, options),
|
|
14331
|
+
evidence: (input2) => readGitLabEvidence(client, input2),
|
|
14332
|
+
pathObjects: async () => {
|
|
14333
|
+
throw new Error("the advisory is not read from GitLab in this draft");
|
|
14334
|
+
},
|
|
14335
|
+
restart: () => client.sources.clear()
|
|
14336
|
+
};
|
|
14337
|
+
}
|
|
13356
14338
|
async function atOnePullRequest(readers, repository, number4, first, collect) {
|
|
13357
14339
|
let pr = first ?? await readers.pullRequest(repository, number4);
|
|
13358
14340
|
for (let run = 1; ; run++) {
|
|
@@ -13398,7 +14380,8 @@ async function checkAt(input2, pr) {
|
|
|
13398
14380
|
evidence,
|
|
13399
14381
|
rerun: rerunCommands(input2.recordFile),
|
|
13400
14382
|
...input2.assurance === void 0 ? {} : { assurance: input2.assurance },
|
|
13401
|
-
...proposed === void 0 ? {} : { advisory: proposed, pathsAtHead }
|
|
14383
|
+
...proposed === void 0 ? {} : { advisory: proposed, pathsAtHead },
|
|
14384
|
+
...input2.forge === void 0 ? {} : { forge: input2.forge }
|
|
13402
14385
|
});
|
|
13403
14386
|
}
|
|
13404
14387
|
|
|
@@ -13484,10 +14467,14 @@ function line(text2) {
|
|
|
13484
14467
|
return () => text2;
|
|
13485
14468
|
}
|
|
13486
14469
|
var decided = line(NO_REMEDY);
|
|
14470
|
+
function worded(key, text2) {
|
|
14471
|
+
return ({ caller }) => text2(phrase(caller, key));
|
|
14472
|
+
}
|
|
13487
14473
|
var REMEDIES = {
|
|
13488
14474
|
// The block (spec section 5). No claim row carries these; the survey counts them per pull request.
|
|
13489
|
-
block_missing:
|
|
13490
|
-
"
|
|
14475
|
+
block_missing: worded(
|
|
14476
|
+
"templateWrites",
|
|
14477
|
+
(writes) => `The report has no dunstan-handback block: declare the claims in one (${writes}).`
|
|
13491
14478
|
),
|
|
13492
14479
|
block_ambiguous: line("The report has more than one dunstan-handback block: keep exactly one."),
|
|
13493
14480
|
block_invalid: line(
|
|
@@ -13495,14 +14482,20 @@ var REMEDIES = {
|
|
|
13495
14482
|
),
|
|
13496
14483
|
// Unverifiable, a tests row: the record holds no count to compare, or could not read it. These are
|
|
13497
14484
|
// TESTS_REMEDY_REASONS, the lines the claims table prints under a tests row.
|
|
13498
|
-
record_not_found:
|
|
13499
|
-
|
|
14485
|
+
record_not_found: worded(
|
|
14486
|
+
"templateCiFills",
|
|
14487
|
+
(fills) => `no run of the named workflow at the head uploaded that artifact from that job. Publish the JUnit record from CI (${RECIPE}), then check that the record's workflow, job, artifact and path are the ones the run used: ${fills}.`
|
|
13500
14488
|
),
|
|
13501
14489
|
no_comparable_record_field: line(
|
|
13502
14490
|
`this record's kind has nothing Dunstan can compare. Name a junit record a CI run uploaded (${RECIPE}); for a run that left no record in CI, declare {"kind": "local"}.`
|
|
13503
14491
|
),
|
|
13504
|
-
local_run_not_in_record:
|
|
13505
|
-
|
|
14492
|
+
local_run_not_in_record: worded(
|
|
14493
|
+
"templateCiAfter",
|
|
14494
|
+
(after) => `these counts are from a local run. Publish the JUnit record from CI (${RECIPE}), and ${after}.`
|
|
14495
|
+
),
|
|
14496
|
+
// Spec 0.1.10: the pull request changed the workflow that wrote the record.
|
|
14497
|
+
record_producer_changed_by_pr: line(
|
|
14498
|
+
"This pull request changes the workflow that produced the test record, so the record is its own evidence. Land the workflow change separately, then declare the counts."
|
|
13506
14499
|
),
|
|
13507
14500
|
"source_unreadable:artifact": ({ hasToken }) => hasToken ? "the run uploaded the record, but it could not be read: it expired, was uploaded outside the latest attempt of the named job, the job had not completed, or the file is not well-formed JUnit. Re-run the job to upload it again, or check again once the job completes." : "GitHub answers an artifact download only with a token (401 without one, even on a public repository). Check again with GITHUB_TOKEN set to a token that can read the repository, with actions: read.",
|
|
13508
14501
|
// Unverifiable, any other row: the record could not settle the claim.
|
|
@@ -13558,6 +14551,7 @@ var TESTS_REMEDY_REASONS = [
|
|
|
13558
14551
|
"record_not_found",
|
|
13559
14552
|
"no_comparable_record_field",
|
|
13560
14553
|
"local_run_not_in_record",
|
|
14554
|
+
"record_producer_changed_by_pr",
|
|
13561
14555
|
"source_unreadable:artifact"
|
|
13562
14556
|
];
|
|
13563
14557
|
function remedyForReason(reason, context) {
|
|
@@ -13674,7 +14668,7 @@ async function signingAssurance(signKey, signer) {
|
|
|
13674
14668
|
return { status: "signed", issuer: signer, keyFingerprint: keyFingerprint(signKey) };
|
|
13675
14669
|
}
|
|
13676
14670
|
function defaultRecordFile(repository, number4) {
|
|
13677
|
-
return `dunstan-${repository.
|
|
14671
|
+
return `dunstan-${repository.replaceAll("/", "-")}-${number4}.json`;
|
|
13678
14672
|
}
|
|
13679
14673
|
async function check(args, io) {
|
|
13680
14674
|
const { values } = parseArgs({
|
|
@@ -13691,9 +14685,17 @@ async function check(args, io) {
|
|
|
13691
14685
|
"sign-key": { type: "string" },
|
|
13692
14686
|
signer: { type: "string" },
|
|
13693
14687
|
"exclude-check-run": { type: "string", multiple: true },
|
|
13694
|
-
|
|
14688
|
+
"exclude-gitlab-job": { type: "string", multiple: true },
|
|
14689
|
+
advisory: { type: "boolean" },
|
|
14690
|
+
"gitlab-project": { type: "string" },
|
|
14691
|
+
mr: { type: "string" },
|
|
14692
|
+
"gitlab-host": { type: "string" },
|
|
14693
|
+
"report-mr-description": { type: "boolean" }
|
|
13695
14694
|
}
|
|
13696
14695
|
});
|
|
14696
|
+
if (values["gitlab-project"] !== void 0 || values.mr !== void 0 || values["gitlab-host"] !== void 0 || values["report-mr-description"] === true || values["exclude-gitlab-job"] !== void 0) {
|
|
14697
|
+
return checkGitLab(values, io);
|
|
14698
|
+
}
|
|
13697
14699
|
if (values.repo === void 0 || !REPOSITORY.test(values.repo)) {
|
|
13698
14700
|
throw new UsageError("--repo <owner/name> is required");
|
|
13699
14701
|
}
|
|
@@ -13739,7 +14741,75 @@ async function check(args, io) {
|
|
|
13739
14741
|
advisory: values.advisory === true
|
|
13740
14742
|
});
|
|
13741
14743
|
writeFileSync(out, serializeRecord(record2));
|
|
13742
|
-
io.out(renderRecord(record2, { hasToken: client.hasToken }));
|
|
14744
|
+
io.out(renderRecord(record2, { hasToken: client.hasToken, caller: "cli" }));
|
|
14745
|
+
io.out(`record: ${out}
|
|
14746
|
+
`);
|
|
14747
|
+
if (assurance !== void 0) {
|
|
14748
|
+
io.out(`signature: ${signRecordFile(out, values["sign-key"])}
|
|
14749
|
+
`);
|
|
14750
|
+
}
|
|
14751
|
+
return exitFor(record2.predicate.verdict);
|
|
14752
|
+
}
|
|
14753
|
+
async function checkGitLab(values, io) {
|
|
14754
|
+
const refused = [
|
|
14755
|
+
["--repo", values.repo !== void 0],
|
|
14756
|
+
["--pr", values.pr !== void 0],
|
|
14757
|
+
["--report-pr-body (use --report-mr-description)", values["report-pr-body"] === true],
|
|
14758
|
+
["--report-comment-author", values["report-comment-author"] !== void 0],
|
|
14759
|
+
["--exclude-check-run", values["exclude-check-run"] !== void 0],
|
|
14760
|
+
["--advisory", values.advisory === true]
|
|
14761
|
+
];
|
|
14762
|
+
const named = refused.filter(([, given]) => given).map(([flag]) => flag);
|
|
14763
|
+
if (named.length > 0) {
|
|
14764
|
+
throw new UsageError(`a GitLab merge request is checked without ${named.join(", ")}`);
|
|
14765
|
+
}
|
|
14766
|
+
const project = values["gitlab-project"];
|
|
14767
|
+
if (project === void 0 || !GITLAB_PROJECT.test(project)) {
|
|
14768
|
+
throw new UsageError(
|
|
14769
|
+
"--gitlab-project <namespace/project> is required: the project's full path, group/subgroup/.../project"
|
|
14770
|
+
);
|
|
14771
|
+
}
|
|
14772
|
+
if (values.mr === void 0) throw new UsageError("--mr <iid> is required");
|
|
14773
|
+
const iid = parsePositiveInt(values.mr, "--mr");
|
|
14774
|
+
const host = values["gitlab-host"] ?? DEFAULT_GITLAB_HOST;
|
|
14775
|
+
if (!GITLAB_HOST.test(host)) {
|
|
14776
|
+
throw new UsageError("--gitlab-host is a lowercase host name, such as gitlab.example.com");
|
|
14777
|
+
}
|
|
14778
|
+
const file2 = values["report-file"];
|
|
14779
|
+
if (file2 !== void 0 === (values["report-mr-description"] === true)) {
|
|
14780
|
+
throw new UsageError(
|
|
14781
|
+
"name exactly one report: --report-file <path|-> or --report-mr-description"
|
|
14782
|
+
);
|
|
14783
|
+
}
|
|
14784
|
+
const excluded = (values["exclude-gitlab-job"] ?? []).map(
|
|
14785
|
+
(v) => parsePositiveInt(v, "--exclude-gitlab-job")
|
|
14786
|
+
);
|
|
14787
|
+
const assurance = await signingAssurance(values["sign-key"], values.signer);
|
|
14788
|
+
const client = new GitLabClient({
|
|
14789
|
+
host,
|
|
14790
|
+
token: io.env.GITLAB_TOKEN,
|
|
14791
|
+
...io.fetch === void 0 ? {} : { fetch: io.fetch },
|
|
14792
|
+
warn: (message) => io.err(`dunstan: ${message}
|
|
14793
|
+
`)
|
|
14794
|
+
});
|
|
14795
|
+
if (!client.hasToken) io.err("dunstan: GITLAB_TOKEN is not set; reading anonymously\n");
|
|
14796
|
+
const out = values.out ?? defaultRecordFile(project, iid);
|
|
14797
|
+
const record2 = await checkPullRequest({
|
|
14798
|
+
readers: gitlabReaders(client),
|
|
14799
|
+
repository: project,
|
|
14800
|
+
number: iid,
|
|
14801
|
+
report: async (pr) => {
|
|
14802
|
+
if (file2 === void 0) return reportFromMergeRequestDescription(pr);
|
|
14803
|
+
return file2 === "-" ? { bytes: io.readStdin(), source: { kind: "stdin", locator: "stdin" } } : { bytes: readFileSync3(file2), source: { kind: "file", locator: file2 } };
|
|
14804
|
+
},
|
|
14805
|
+
excludedCheckRunIds: excluded,
|
|
14806
|
+
checker: checkerIdentity(io.artifact),
|
|
14807
|
+
recordFile: basename(out),
|
|
14808
|
+
...assurance === void 0 ? {} : { assurance },
|
|
14809
|
+
forge: { kind: "gitlab", host }
|
|
14810
|
+
});
|
|
14811
|
+
writeFileSync(out, serializeRecord(record2));
|
|
14812
|
+
io.out(renderRecord(record2, { hasToken: client.hasToken, caller: "cli" }));
|
|
13743
14813
|
io.out(`record: ${out}
|
|
13744
14814
|
`);
|
|
13745
14815
|
if (assurance !== void 0) {
|
|
@@ -13991,7 +15061,7 @@ __export(external_exports, {
|
|
|
13991
15061
|
includes: () => _includes,
|
|
13992
15062
|
input: () => input,
|
|
13993
15063
|
instanceof: () => _instanceof,
|
|
13994
|
-
int: () =>
|
|
15064
|
+
int: () => int3,
|
|
13995
15065
|
int32: () => int32,
|
|
13996
15066
|
int64: () => int64,
|
|
13997
15067
|
intersection: () => intersection,
|
|
@@ -14585,8 +15655,8 @@ function defineLazy(object3, key, getter) {
|
|
|
14585
15655
|
configurable: true
|
|
14586
15656
|
});
|
|
14587
15657
|
}
|
|
14588
|
-
function objectClone(
|
|
14589
|
-
return Object.create(Object.getPrototypeOf(
|
|
15658
|
+
function objectClone(obj3) {
|
|
15659
|
+
return Object.create(Object.getPrototypeOf(obj3), Object.getOwnPropertyDescriptors(obj3));
|
|
14590
15660
|
}
|
|
14591
15661
|
function assignProp(target, prop, value) {
|
|
14592
15662
|
Object.defineProperty(target, prop, {
|
|
@@ -14657,10 +15727,10 @@ function mergeDefs(...defs) {
|
|
|
14657
15727
|
function cloneDef(schema) {
|
|
14658
15728
|
return mergeDefs(schema._zod.def);
|
|
14659
15729
|
}
|
|
14660
|
-
function getElementAtPath(
|
|
15730
|
+
function getElementAtPath(obj3, path) {
|
|
14661
15731
|
if (!path)
|
|
14662
|
-
return
|
|
14663
|
-
return path.reduce((acc, key) => acc?.[key],
|
|
15732
|
+
return obj3;
|
|
15733
|
+
return path.reduce((acc, key) => acc?.[key], obj3);
|
|
14664
15734
|
}
|
|
14665
15735
|
function promiseAllObject(promisesObj) {
|
|
14666
15736
|
const keys = Object.keys(promisesObj);
|
|
@@ -14675,14 +15745,14 @@ function promiseAllObject(promisesObj) {
|
|
|
14675
15745
|
}
|
|
14676
15746
|
function randomString(length = 10) {
|
|
14677
15747
|
const chars = "abcdefghijklmnopqrstuvwxyz";
|
|
14678
|
-
let
|
|
15748
|
+
let str4 = "";
|
|
14679
15749
|
for (let i = 0; i < length; i++) {
|
|
14680
|
-
|
|
15750
|
+
str4 += chars[Math.floor(Math.random() * chars.length)];
|
|
14681
15751
|
}
|
|
14682
|
-
return
|
|
15752
|
+
return str4;
|
|
14683
15753
|
}
|
|
14684
|
-
function esc(
|
|
14685
|
-
return JSON.stringify(
|
|
15754
|
+
function esc(str4) {
|
|
15755
|
+
return JSON.stringify(str4);
|
|
14686
15756
|
}
|
|
14687
15757
|
function slugify(input2) {
|
|
14688
15758
|
return input2.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -14796,8 +15866,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
|
|
|
14796
15866
|
"symbol",
|
|
14797
15867
|
"undefined"
|
|
14798
15868
|
]);
|
|
14799
|
-
function escapeRegex(
|
|
14800
|
-
return
|
|
15869
|
+
function escapeRegex(str4) {
|
|
15870
|
+
return str4.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14801
15871
|
}
|
|
14802
15872
|
function clone(inst, def, params) {
|
|
14803
15873
|
const cl = new inst._zod.constr(def ?? inst._zod.def);
|
|
@@ -15051,13 +16121,13 @@ function getSizableOrigin(input2) {
|
|
|
15051
16121
|
return "unknown";
|
|
15052
16122
|
}
|
|
15053
16123
|
var highSurrogate = /[\uD800-\uDBFF]/;
|
|
15054
|
-
function codePointLength(
|
|
15055
|
-
const units =
|
|
15056
|
-
if (!highSurrogate.test(
|
|
16124
|
+
function codePointLength(str4) {
|
|
16125
|
+
const units = str4.length;
|
|
16126
|
+
if (!highSurrogate.test(str4))
|
|
15057
16127
|
return units;
|
|
15058
16128
|
let count = units;
|
|
15059
16129
|
for (let i = 0; i < units - 1; i++) {
|
|
15060
|
-
if ((
|
|
16130
|
+
if ((str4.charCodeAt(i) & 64512) === 55296 && (str4.charCodeAt(i + 1) & 64512) === 56320) {
|
|
15061
16131
|
count--;
|
|
15062
16132
|
i++;
|
|
15063
16133
|
}
|
|
@@ -15084,9 +16154,9 @@ function parsedType(data) {
|
|
|
15084
16154
|
if (Array.isArray(data)) {
|
|
15085
16155
|
return "array";
|
|
15086
16156
|
}
|
|
15087
|
-
const
|
|
15088
|
-
if (
|
|
15089
|
-
return
|
|
16157
|
+
const obj3 = data;
|
|
16158
|
+
if (obj3 && Object.getPrototypeOf(obj3) !== Object.prototype && "constructor" in obj3 && obj3.constructor) {
|
|
16159
|
+
return obj3.constructor.name;
|
|
15090
16160
|
}
|
|
15091
16161
|
}
|
|
15092
16162
|
}
|
|
@@ -15104,8 +16174,8 @@ function issue(...args) {
|
|
|
15104
16174
|
}
|
|
15105
16175
|
return { ...iss };
|
|
15106
16176
|
}
|
|
15107
|
-
function cleanEnum(
|
|
15108
|
-
return Object.entries(
|
|
16177
|
+
function cleanEnum(obj3) {
|
|
16178
|
+
return Object.entries(obj3).filter(([k, _]) => {
|
|
15109
16179
|
return Number.isNaN(Number.parseInt(k, 10));
|
|
15110
16180
|
}).map((el) => el[1]);
|
|
15111
16181
|
}
|
|
@@ -15430,15 +16500,15 @@ var $ZodError = $constructor("$ZodError", initializer);
|
|
|
15430
16500
|
var $ZodRealError = $constructor("$ZodError", initializer, void 0, {
|
|
15431
16501
|
Parent: Error
|
|
15432
16502
|
});
|
|
15433
|
-
function node(
|
|
15434
|
-
if (!Object.prototype.hasOwnProperty.call(
|
|
16503
|
+
function node(obj3, key, make) {
|
|
16504
|
+
if (!Object.prototype.hasOwnProperty.call(obj3, key)) {
|
|
15435
16505
|
if (key === "__proto__") {
|
|
15436
|
-
Object.defineProperty(
|
|
16506
|
+
Object.defineProperty(obj3, key, { value: make(), writable: true, enumerable: true, configurable: true });
|
|
15437
16507
|
} else {
|
|
15438
|
-
|
|
16508
|
+
obj3[key] = make();
|
|
15439
16509
|
}
|
|
15440
16510
|
}
|
|
15441
|
-
return
|
|
16511
|
+
return obj3[key];
|
|
15442
16512
|
}
|
|
15443
16513
|
function flattenError(error62, mapper = (issue2) => issue2.message) {
|
|
15444
16514
|
const fieldErrors = {};
|
|
@@ -30879,7 +31949,7 @@ __export(schemas_exports2, {
|
|
|
30879
31949
|
httpUrl: () => httpUrl,
|
|
30880
31950
|
iban: () => iban2,
|
|
30881
31951
|
instanceof: () => _instanceof,
|
|
30882
|
-
int: () =>
|
|
31952
|
+
int: () => int3,
|
|
30883
31953
|
int32: () => int32,
|
|
30884
31954
|
int64: () => int64,
|
|
30885
31955
|
intersection: () => intersection,
|
|
@@ -31623,10 +32693,10 @@ var ZodNumber = /* @__PURE__ */ $constructor(
|
|
|
31623
32693
|
return this.check(_lte(value, params));
|
|
31624
32694
|
},
|
|
31625
32695
|
int(params) {
|
|
31626
|
-
return this.check(
|
|
32696
|
+
return this.check(int3(params));
|
|
31627
32697
|
},
|
|
31628
32698
|
safe(params) {
|
|
31629
|
-
return this.check(
|
|
32699
|
+
return this.check(int3(params));
|
|
31630
32700
|
},
|
|
31631
32701
|
positive(params) {
|
|
31632
32702
|
return this.check(_gt(0, params));
|
|
@@ -31658,7 +32728,7 @@ var ZodNumberFormat = /* @__PURE__ */ $constructor("ZodNumberFormat", (inst, def
|
|
|
31658
32728
|
$ZodNumberFormat.init(inst, def);
|
|
31659
32729
|
ZodNumber.init(inst, def);
|
|
31660
32730
|
});
|
|
31661
|
-
function
|
|
32731
|
+
function int3(params) {
|
|
31662
32732
|
return _int(ZodNumberFormat, params);
|
|
31663
32733
|
}
|
|
31664
32734
|
function float32(params) {
|
|
@@ -35146,7 +36216,7 @@ var StdioServerTransport = class {
|
|
|
35146
36216
|
|
|
35147
36217
|
// src/mcp/server.ts
|
|
35148
36218
|
import { existsSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
35149
|
-
import { basename as basename2, resolve as resolve3 } from "node:path";
|
|
36219
|
+
import { basename as basename2, isAbsolute, resolve as resolve3 } from "node:path";
|
|
35150
36220
|
|
|
35151
36221
|
// node_modules/.pnpm/zod@4.6.5/node_modules/zod/v3/helpers/util.js
|
|
35152
36222
|
var util;
|
|
@@ -35161,26 +36231,26 @@ var util;
|
|
|
35161
36231
|
}
|
|
35162
36232
|
util2.assertNever = assertNever2;
|
|
35163
36233
|
util2.arrayToEnum = (items) => {
|
|
35164
|
-
const
|
|
36234
|
+
const obj3 = {};
|
|
35165
36235
|
for (const item of items) {
|
|
35166
|
-
|
|
36236
|
+
obj3[item] = item;
|
|
35167
36237
|
}
|
|
35168
|
-
return
|
|
36238
|
+
return obj3;
|
|
35169
36239
|
};
|
|
35170
|
-
util2.getValidEnumValues = (
|
|
35171
|
-
const validKeys = util2.objectKeys(
|
|
36240
|
+
util2.getValidEnumValues = (obj3) => {
|
|
36241
|
+
const validKeys = util2.objectKeys(obj3).filter((k) => typeof obj3[obj3[k]] !== "number");
|
|
35172
36242
|
const filtered = {};
|
|
35173
36243
|
for (const k of validKeys) {
|
|
35174
|
-
filtered[k] =
|
|
36244
|
+
filtered[k] = obj3[k];
|
|
35175
36245
|
}
|
|
35176
36246
|
return util2.objectValues(filtered);
|
|
35177
36247
|
};
|
|
35178
|
-
util2.objectValues = (
|
|
35179
|
-
return util2.objectKeys(
|
|
35180
|
-
return
|
|
36248
|
+
util2.objectValues = (obj3) => {
|
|
36249
|
+
return util2.objectKeys(obj3).map(function(e) {
|
|
36250
|
+
return obj3[e];
|
|
35181
36251
|
});
|
|
35182
36252
|
};
|
|
35183
|
-
util2.objectKeys = typeof Object.keys === "function" ? (
|
|
36253
|
+
util2.objectKeys = typeof Object.keys === "function" ? (obj3) => Object.keys(obj3) : (object3) => {
|
|
35184
36254
|
const keys = [];
|
|
35185
36255
|
for (const key in object3) {
|
|
35186
36256
|
if (Object.prototype.hasOwnProperty.call(object3, key)) {
|
|
@@ -35189,8 +36259,8 @@ var util;
|
|
|
35189
36259
|
}
|
|
35190
36260
|
return keys;
|
|
35191
36261
|
};
|
|
35192
|
-
util2.find = (
|
|
35193
|
-
for (const item of
|
|
36262
|
+
util2.find = (arr3, checker) => {
|
|
36263
|
+
for (const item of arr3) {
|
|
35194
36264
|
if (checker(item))
|
|
35195
36265
|
return item;
|
|
35196
36266
|
}
|
|
@@ -35210,10 +36280,10 @@ var util;
|
|
|
35210
36280
|
})(util || (util = {}));
|
|
35211
36281
|
var objectUtil;
|
|
35212
36282
|
(function(objectUtil2) {
|
|
35213
|
-
objectUtil2.mergeShapes = (first,
|
|
36283
|
+
objectUtil2.mergeShapes = (first, second3) => {
|
|
35214
36284
|
return {
|
|
35215
36285
|
...first,
|
|
35216
|
-
...
|
|
36286
|
+
...second3
|
|
35217
36287
|
// second overwrites first
|
|
35218
36288
|
};
|
|
35219
36289
|
};
|
|
@@ -38145,11 +39215,11 @@ var ZodRecord2 = class _ZodRecord extends ZodType2 {
|
|
|
38145
39215
|
get element() {
|
|
38146
39216
|
return this._def.valueType;
|
|
38147
39217
|
}
|
|
38148
|
-
static create(first,
|
|
38149
|
-
if (
|
|
39218
|
+
static create(first, second3, third) {
|
|
39219
|
+
if (second3 instanceof ZodType2) {
|
|
38150
39220
|
return new _ZodRecord({
|
|
38151
39221
|
keyType: first,
|
|
38152
|
-
valueType:
|
|
39222
|
+
valueType: second3,
|
|
38153
39223
|
typeName: ZodFirstPartyTypeKind2.ZodRecord,
|
|
38154
39224
|
...processCreateParams(third)
|
|
38155
39225
|
});
|
|
@@ -38158,7 +39228,7 @@ var ZodRecord2 = class _ZodRecord extends ZodType2 {
|
|
|
38158
39228
|
keyType: ZodString2.create(),
|
|
38159
39229
|
valueType: first,
|
|
38160
39230
|
typeName: ZodFirstPartyTypeKind2.ZodRecord,
|
|
38161
|
-
...processCreateParams(
|
|
39231
|
+
...processCreateParams(second3)
|
|
38162
39232
|
});
|
|
38163
39233
|
}
|
|
38164
39234
|
};
|
|
@@ -42307,7 +43377,7 @@ function validateToolName(name) {
|
|
|
42307
43377
|
warnings.push("Tool name starts or ends with a dot, which may cause parsing issues in some contexts");
|
|
42308
43378
|
}
|
|
42309
43379
|
if (!TOOL_NAME_REGEX.test(name)) {
|
|
42310
|
-
const invalidChars = name.split("").filter((char) => !/[A-Za-z0-9._-]/.test(char)).filter((char, index,
|
|
43380
|
+
const invalidChars = name.split("").filter((char) => !/[A-Za-z0-9._-]/.test(char)).filter((char, index, arr3) => arr3.indexOf(char) === index);
|
|
42311
43381
|
warnings.push(`Tool name contains invalid characters: ${invalidChars.map((c) => `"${c}"`).join(", ")}`, "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)");
|
|
42312
43382
|
return {
|
|
42313
43383
|
isValid: false,
|
|
@@ -42448,8 +43518,8 @@ var McpServer = class {
|
|
|
42448
43518
|
title: tool.title,
|
|
42449
43519
|
description: tool.description,
|
|
42450
43520
|
inputSchema: (() => {
|
|
42451
|
-
const
|
|
42452
|
-
return
|
|
43521
|
+
const obj3 = normalizeObjectSchema(tool.inputSchema);
|
|
43522
|
+
return obj3 ? toJsonSchemaCompat(obj3, {
|
|
42453
43523
|
strictUnions: true,
|
|
42454
43524
|
pipeStrategy: "input"
|
|
42455
43525
|
}) : EMPTY_OBJECT_JSON_SCHEMA;
|
|
@@ -42459,9 +43529,9 @@ var McpServer = class {
|
|
|
42459
43529
|
_meta: tool._meta
|
|
42460
43530
|
};
|
|
42461
43531
|
if (tool.outputSchema) {
|
|
42462
|
-
const
|
|
42463
|
-
if (
|
|
42464
|
-
toolDefinition.outputSchema = toJsonSchemaCompat(
|
|
43532
|
+
const obj3 = normalizeObjectSchema(tool.outputSchema);
|
|
43533
|
+
if (obj3) {
|
|
43534
|
+
toolDefinition.outputSchema = toJsonSchemaCompat(obj3, {
|
|
42465
43535
|
strictUnions: true,
|
|
42466
43536
|
pipeStrategy: "output"
|
|
42467
43537
|
});
|
|
@@ -43114,20 +44184,20 @@ var EMPTY_OBJECT_JSON_SCHEMA = {
|
|
|
43114
44184
|
function isZodTypeLike(value) {
|
|
43115
44185
|
return value !== null && typeof value === "object" && "parse" in value && typeof value.parse === "function" && "safeParse" in value && typeof value.safeParse === "function";
|
|
43116
44186
|
}
|
|
43117
|
-
function isZodSchemaInstance(
|
|
43118
|
-
return "_def" in
|
|
44187
|
+
function isZodSchemaInstance(obj3) {
|
|
44188
|
+
return "_def" in obj3 || "_zod" in obj3 || isZodTypeLike(obj3);
|
|
43119
44189
|
}
|
|
43120
|
-
function isZodRawShapeCompat(
|
|
43121
|
-
if (typeof
|
|
44190
|
+
function isZodRawShapeCompat(obj3) {
|
|
44191
|
+
if (typeof obj3 !== "object" || obj3 === null) {
|
|
43122
44192
|
return false;
|
|
43123
44193
|
}
|
|
43124
|
-
if (isZodSchemaInstance(
|
|
44194
|
+
if (isZodSchemaInstance(obj3)) {
|
|
43125
44195
|
return false;
|
|
43126
44196
|
}
|
|
43127
|
-
if (Object.keys(
|
|
44197
|
+
if (Object.keys(obj3).length === 0) {
|
|
43128
44198
|
return true;
|
|
43129
44199
|
}
|
|
43130
|
-
return Object.values(
|
|
44200
|
+
return Object.values(obj3).some(isZodTypeLike);
|
|
43131
44201
|
}
|
|
43132
44202
|
function getZodSchemaObject(schema) {
|
|
43133
44203
|
if (!schema) {
|
|
@@ -43335,14 +44405,149 @@ function suggestText(report) {
|
|
|
43335
44405
|
return renderSuggestions(suggestDeclarations(report));
|
|
43336
44406
|
}
|
|
43337
44407
|
|
|
44408
|
+
// src/template/tests-ci.ts
|
|
44409
|
+
var plural2 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
44410
|
+
function unsettled(reason, why, remedy) {
|
|
44411
|
+
const notes = [`tests are not declared: ${why} (${reason}).`];
|
|
44412
|
+
if (remedy !== void 0) notes.push(`remedy: ${remedy}`);
|
|
44413
|
+
return { status: "unsettled", reason, notes };
|
|
44414
|
+
}
|
|
44415
|
+
async function testsFromCi(client, input2) {
|
|
44416
|
+
const { repository, headCommit: head, record: record2, caller } = input2;
|
|
44417
|
+
const againCi = phrase(caller, "againCi");
|
|
44418
|
+
if (!client.hasToken) {
|
|
44419
|
+
return unsettled(
|
|
44420
|
+
"no_token",
|
|
44421
|
+
"GITHUB_TOKEN is not set, and GitHub answers an artifact download only with a token, even on a public repository",
|
|
44422
|
+
`set GITHUB_TOKEN to a token that can read the repository, with actions: read, and ${againCi}.`
|
|
44423
|
+
);
|
|
44424
|
+
}
|
|
44425
|
+
if (!input2.pushed) {
|
|
44426
|
+
return unsettled(
|
|
44427
|
+
"not_pushed",
|
|
44428
|
+
`HEAD ${head} is not on any remote-tracking branch, so no CI run can have tested it`,
|
|
44429
|
+
`push it, wait for CI to finish, and ${againCi} (run git fetch first if it was pushed from elsewhere).`
|
|
44430
|
+
);
|
|
44431
|
+
}
|
|
44432
|
+
const observed = { runsNotCompleted: 0 };
|
|
44433
|
+
const evidence = await readTestRecord(client, repository, record2, head, observed);
|
|
44434
|
+
const named = `the artifact ${record2.artifact} holding ${record2.path} from job ${record2.job}`;
|
|
44435
|
+
const pending = observed.runsNotCompleted > 0 ? `CI has not finished: ${plural2(observed.runsNotCompleted, "run", "runs")} of ${record2.workflow} at ${head} ${observed.runsNotCompleted === 1 ? "is" : "are"} not completed` : void 0;
|
|
44436
|
+
const again = `${againCi} once CI has finished on this commit.`;
|
|
44437
|
+
if (evidence.status !== "ok") {
|
|
44438
|
+
const reason = evidence.status === "unreadable" ? `source_unreadable:${evidence.source}` : `evidence_field_unpopulated:${evidence.field}`;
|
|
44439
|
+
if (pending !== void 0) return unsettled(reason, pending, again);
|
|
44440
|
+
const why = reason === "source_unreadable:artifact" ? `a run of ${record2.workflow} at ${head} uploaded ${record2.artifact}, and it could not be read` : `the workflow runs of ${record2.workflow} at ${head} could not be read`;
|
|
44441
|
+
return unsettled(
|
|
44442
|
+
reason,
|
|
44443
|
+
why,
|
|
44444
|
+
remedyForReason(reason, { hasToken: true, caller }) ?? `${againCi}.`
|
|
44445
|
+
);
|
|
44446
|
+
}
|
|
44447
|
+
if (evidence.runs.length === 0) {
|
|
44448
|
+
if (pending !== void 0) {
|
|
44449
|
+
return unsettled(
|
|
44450
|
+
"record_not_found",
|
|
44451
|
+
`${pending}, and no completed run uploaded ${named}`,
|
|
44452
|
+
again
|
|
44453
|
+
);
|
|
44454
|
+
}
|
|
44455
|
+
return unsettled(
|
|
44456
|
+
"record_not_found",
|
|
44457
|
+
`no run of ${record2.workflow} at ${head} uploaded ${named}`,
|
|
44458
|
+
remedyForReason("record_not_found", { hasToken: true, caller })
|
|
44459
|
+
);
|
|
44460
|
+
}
|
|
44461
|
+
const executed = new Set(evidence.runs.map((r) => r.executed));
|
|
44462
|
+
const failed = new Set(evidence.runs.map((r) => r.failed));
|
|
44463
|
+
if (executed.size > 1 || failed.size > 1) {
|
|
44464
|
+
const counts = evidence.runs.map((r) => `run ${r.runId}: ${r.executed} executed, ${r.failed} failed`).join("; ");
|
|
44465
|
+
return unsettled(
|
|
44466
|
+
"record_ambiguous",
|
|
44467
|
+
`the runs of ${record2.workflow} at ${head} that uploaded ${record2.artifact} disagree (${counts}), and the gate never picks one`,
|
|
44468
|
+
"make the runs agree (re-run the one that differs), or name a job only one of them runs."
|
|
44469
|
+
);
|
|
44470
|
+
}
|
|
44471
|
+
const run = evidence.runs[0];
|
|
44472
|
+
const runs = evidence.runs.map((r) => r.runId).join(", ");
|
|
44473
|
+
const notes = [
|
|
44474
|
+
`tests: ${run.executed} executed and ${run.failed} failed in ${record2.path} in the artifact ${record2.artifact} of job ${record2.job} in ${record2.workflow} at ${head}, read from CI as the gate reads it (${evidence.runs.length === 1 ? "run" : "runs"} ${runs}).`
|
|
44475
|
+
];
|
|
44476
|
+
if (pending !== void 0) {
|
|
44477
|
+
notes.push(
|
|
44478
|
+
`${pending}; a run that finishes later with other counts makes the gate's read ambiguous. ${phrase(caller, "againCiSentence")} once CI has finished.`
|
|
44479
|
+
);
|
|
44480
|
+
}
|
|
44481
|
+
return {
|
|
44482
|
+
status: "settled",
|
|
44483
|
+
run: { command: input2.command, count: run.executed, failures: run.failed, record: record2 },
|
|
44484
|
+
notes
|
|
44485
|
+
};
|
|
44486
|
+
}
|
|
44487
|
+
|
|
44488
|
+
// src/template/run.ts
|
|
44489
|
+
var DEFAULT_ARTIFACT = "dunstan-junit";
|
|
44490
|
+
var DEFAULT_ARTIFACT_PATH = "junit.xml";
|
|
44491
|
+
async function testsSource(request, context) {
|
|
44492
|
+
const { cwd, tests } = request;
|
|
44493
|
+
if (tests === void 0) return void 0;
|
|
44494
|
+
if (tests.from === "local") return { kind: "local", junit: tests.junit, command: tests.command };
|
|
44495
|
+
if (tests.from === "junit") {
|
|
44496
|
+
return {
|
|
44497
|
+
kind: "junit",
|
|
44498
|
+
junit: tests.junit,
|
|
44499
|
+
command: tests.command,
|
|
44500
|
+
record: {
|
|
44501
|
+
workflow: tests.workflow,
|
|
44502
|
+
job: tests.job,
|
|
44503
|
+
artifact: tests.artifact,
|
|
44504
|
+
path: tests.artifactPath
|
|
44505
|
+
}
|
|
44506
|
+
};
|
|
44507
|
+
}
|
|
44508
|
+
const head = headCommit(cwd);
|
|
44509
|
+
const repository = tests.repository ?? originRepository(cwd, request.caller);
|
|
44510
|
+
const client = new GitHubClient({
|
|
44511
|
+
token: context.env.GITHUB_TOKEN,
|
|
44512
|
+
...context.fetch === void 0 ? {} : { fetch: context.fetch },
|
|
44513
|
+
warn: context.warn
|
|
44514
|
+
});
|
|
44515
|
+
const read = await testsFromCi(client, {
|
|
44516
|
+
repository,
|
|
44517
|
+
headCommit: head,
|
|
44518
|
+
command: tests.command,
|
|
44519
|
+
pushed: isPushed(cwd, head),
|
|
44520
|
+
caller: request.caller,
|
|
44521
|
+
record: {
|
|
44522
|
+
kind: "junit",
|
|
44523
|
+
workflow: tests.workflow,
|
|
44524
|
+
job: tests.job,
|
|
44525
|
+
artifact: tests.artifact ?? DEFAULT_ARTIFACT,
|
|
44526
|
+
path: tests.artifactPath ?? DEFAULT_ARTIFACT_PATH
|
|
44527
|
+
}
|
|
44528
|
+
});
|
|
44529
|
+
return { kind: "ci", read };
|
|
44530
|
+
}
|
|
44531
|
+
async function runTemplate(request, context) {
|
|
44532
|
+
const tests = await testsSource(request, context);
|
|
44533
|
+
const t = templateBlock({ cwd: request.cwd, base: request.base, tests, caller: request.caller });
|
|
44534
|
+
return {
|
|
44535
|
+
fence: renderBlock(t.block),
|
|
44536
|
+
notes: t.notes,
|
|
44537
|
+
tests: tests === void 0 ? "none" : tests.kind === "ci" && tests.read.status === "unsettled" ? "not_declared" : "declared"
|
|
44538
|
+
};
|
|
44539
|
+
}
|
|
44540
|
+
|
|
43338
44541
|
// src/mcp/server.ts
|
|
43339
44542
|
var TOOL_NAME = "dunstan_check_handback";
|
|
43340
44543
|
var SUGGEST_TOOL_NAME = "dunstan_suggest_declarations";
|
|
44544
|
+
var TEMPLATE_TOOL_NAME = "dunstan_template_handback";
|
|
43341
44545
|
var InputError = class extends Error {
|
|
43342
44546
|
};
|
|
43343
|
-
var INSTRUCTIONS = `Before you say a task is done: generate your dunstan-handback block
|
|
44547
|
+
var INSTRUCTIONS = `Before you say a task is done: call ${TEMPLATE_TOOL_NAME} with the absolute path of your checkout (and tests.from "ci" once CI has finished on your last push) to generate your dunstan-handback block, or run \`dunstan template\`, the same thing from a shell; add your reference claims; call ${SUGGEST_TOOL_NAME} with your report and declare or reword each sentence it lists; then call ${TOOL_NAME} with your pull request and the block. If the verdict is not pass, report the rows as they are; never edit the block to make it pass without changing the work. The check reads only what the block declares; the prose is yours to bring into the block.`;
|
|
43344
44548
|
var SUGGEST_DESCRIPTION = `List the sentences in your report's prose that look like claims your dunstan-handback block does not declare, before you say done. Give report, the report text. For each, declare it in the block, or reword it if it is not a claim about this PR. Offline: needs no pull request, reads nothing and writes nothing. Never a verdict: nothing passes or fails, and nothing is written to a record. The same text as \`dunstan suggest\`.`;
|
|
43345
44549
|
var DESCRIPTION = `Check a completion handback against the repository's own record, before you say done. Give the subject (repository owner/name and pull request number) and exactly one of: block, the dunstan-handback block as a JSON object; or report, the report text that carries exactly one \`\`\`dunstan-handback fence. Of the checks claims, checks.allSucceeded is true only if every counted check run concluded success, so a skipped or neutral run makes it false; checks.noneFailed is true if none concluded other than success, skipped or neutral and at least one succeeded. Returns the verdict (pass, fail or unverifiable), the claims table, and the full record as JSON. Same checker and same verdict as \`dunstan check\` on the same evidence; no model decides. Reads GitHub metadata with GITHUB_TOKEN and writes nothing to GitHub or the repository; the record is written to a file only when you pass outPath. If the verdict is not pass, report the rows as they are; change the work, not the block.`;
|
|
44550
|
+
var TEMPLATE_DESCRIPTION = `Generate your dunstan-handback block from the local git checkout of your pull request's branch, after your last push: headCommit is HEAD, filesChanged is HEAD's diff against its merge base with base. Give directory, the checkout's absolute path. To declare tests, give tests.from "ci" with command, workflow and job once CI has finished on your last push: the counts are read from the JUnit record CI published, as the gate reads it, with GITHUB_TOKEN. tests.from "local" declares a local JUnit file as a local run, which the gate reports unverifiable. Returns the first line (tests: declared, tests: not declared, or tests: none asked) and the notes, then the fence, byte for byte what \`dunstan template\` prints. When the first line is tests: not declared, the block has no tests: do not add counts yourself; do what the note says. Add your references and checks yourself. Reads git and, with tests.from "ci", GitHub metadata and the JUnit artifact for its counts; writes nothing.`;
|
|
43346
44551
|
function reportOfBlock(block) {
|
|
43347
44552
|
const text2 = canonicalize(block);
|
|
43348
44553
|
return {
|
|
@@ -43416,13 +44621,52 @@ function handbackResult(record2, outPath, notes, context) {
|
|
|
43416
44621
|
content: [
|
|
43417
44622
|
{ type: "text", text: `${head.join("\n")}
|
|
43418
44623
|
` },
|
|
43419
|
-
{ type: "text", text: renderRecord(record2, context) },
|
|
44624
|
+
{ type: "text", text: renderRecord(record2, { ...context, caller: "mcp" }) },
|
|
43420
44625
|
{ type: "text", text: serializeRecord(record2) }
|
|
43421
44626
|
]
|
|
43422
44627
|
};
|
|
43423
44628
|
}
|
|
43424
|
-
|
|
43425
|
-
|
|
44629
|
+
var TESTS_LINE = {
|
|
44630
|
+
declared: "tests: declared",
|
|
44631
|
+
not_declared: "tests: not declared",
|
|
44632
|
+
none: "tests: none asked"
|
|
44633
|
+
};
|
|
44634
|
+
async function templateHandback(input2, options) {
|
|
44635
|
+
if (!isAbsolute(input2.directory)) {
|
|
44636
|
+
throw new InputError(`directory ${input2.directory} is not an absolute path`);
|
|
44637
|
+
}
|
|
44638
|
+
const warnings = [];
|
|
44639
|
+
const run = await runTemplate(
|
|
44640
|
+
{ cwd: input2.directory, base: input2.base, tests: input2.tests, caller: "mcp" },
|
|
44641
|
+
{
|
|
44642
|
+
env: options.env,
|
|
44643
|
+
fetch: options.fetch,
|
|
44644
|
+
warn: (message) => {
|
|
44645
|
+
warnings.push(message);
|
|
44646
|
+
options.warn(`dunstan: ${message}
|
|
44647
|
+
`);
|
|
44648
|
+
}
|
|
44649
|
+
}
|
|
44650
|
+
);
|
|
44651
|
+
const head = [TESTS_LINE[run.tests]];
|
|
44652
|
+
if (run.tests === "not_declared") {
|
|
44653
|
+
head.push("The block below has no tests. Do not add counts yourself; do what the note says.");
|
|
44654
|
+
}
|
|
44655
|
+
head.push(...[...warnings, ...run.notes].map((n) => `note: ${n}`));
|
|
44656
|
+
return {
|
|
44657
|
+
content: [
|
|
44658
|
+
{ type: "text", text: `${head.join("\n")}
|
|
44659
|
+
` },
|
|
44660
|
+
{ type: "text", text: run.fence }
|
|
44661
|
+
]
|
|
44662
|
+
};
|
|
44663
|
+
}
|
|
44664
|
+
var ERROR_TAIL = {
|
|
44665
|
+
check: "This is an error, not a verdict: nothing was checked.",
|
|
44666
|
+
template: "This is an error: no block was produced."
|
|
44667
|
+
};
|
|
44668
|
+
function toolError(error62, tail = "check") {
|
|
44669
|
+
const known = error62 instanceof InputError || error62 instanceof PullRequestUnreadable || error62 instanceof PullRequestMoved || error62 instanceof CanonicalizationError || error62 instanceof TemplateError || typeof error62.code === "string";
|
|
43426
44670
|
const message = error62.message ?? String(error62);
|
|
43427
44671
|
return {
|
|
43428
44672
|
isError: true,
|
|
@@ -43430,12 +44674,37 @@ function toolError(error62) {
|
|
|
43430
44674
|
{
|
|
43431
44675
|
type: "text",
|
|
43432
44676
|
text: `dunstan: ${known ? message : `internal error: ${message}`}
|
|
43433
|
-
|
|
44677
|
+
${ERROR_TAIL[tail]}
|
|
43434
44678
|
`
|
|
43435
44679
|
}
|
|
43436
44680
|
]
|
|
43437
44681
|
};
|
|
43438
44682
|
}
|
|
44683
|
+
var nonEmpty = external_exports.string().min(1);
|
|
44684
|
+
var TEMPLATE_TESTS = external_exports.discriminatedUnion("from", [
|
|
44685
|
+
external_exports.object({
|
|
44686
|
+
from: external_exports.literal("ci"),
|
|
44687
|
+
command: nonEmpty.describe("The command that ran the tests, as the block declares it."),
|
|
44688
|
+
workflow: nonEmpty.describe(
|
|
44689
|
+
"The workflow file that ran them, as a path: .github/workflows/ci.yml."
|
|
44690
|
+
),
|
|
44691
|
+
job: nonEmpty.describe("The name of the job that uploaded the JUnit artifact."),
|
|
44692
|
+
artifact: nonEmpty.optional().describe("The artifact holding the JUnit file. Absent: dunstan-junit."),
|
|
44693
|
+
artifactPath: nonEmpty.optional().describe("The JUnit file in the artifact. Absent: junit.xml."),
|
|
44694
|
+
repository: external_exports.string().regex(REPOSITORY, "repository must be owner/name").optional().describe("The GitHub repository, owner/name. Absent: the one the origin remote names.")
|
|
44695
|
+
}).strict().describe(
|
|
44696
|
+
"Read CI's counts at HEAD from the JUnit record CI published, as the gate reads it (dunstan template --tests-ci)."
|
|
44697
|
+
),
|
|
44698
|
+
external_exports.object({
|
|
44699
|
+
from: external_exports.literal("local"),
|
|
44700
|
+
junit: nonEmpty.describe(
|
|
44701
|
+
"The local JUnit XML file; a relative path resolves against directory."
|
|
44702
|
+
),
|
|
44703
|
+
command: nonEmpty.describe("The command that ran the tests, as the block declares it.")
|
|
44704
|
+
}).strict().describe(
|
|
44705
|
+
"Declare a local JUnit file as a local run, which the gate reports unverifiable (dunstan template --tests-local)."
|
|
44706
|
+
)
|
|
44707
|
+
]);
|
|
43439
44708
|
function createDunstanMcpServer(options) {
|
|
43440
44709
|
const server = new McpServer(
|
|
43441
44710
|
{ name: CHECKER_NAME, version: CHECKER_VERSION },
|
|
@@ -43489,6 +44758,37 @@ function createDunstanMcpServer(options) {
|
|
|
43489
44758
|
},
|
|
43490
44759
|
async (input2) => ({ content: [{ type: "text", text: suggestText(input2.report) }] })
|
|
43491
44760
|
);
|
|
44761
|
+
server.registerTool(
|
|
44762
|
+
TEMPLATE_TOOL_NAME,
|
|
44763
|
+
{
|
|
44764
|
+
title: "Generate a handback block from git",
|
|
44765
|
+
description: TEMPLATE_DESCRIPTION,
|
|
44766
|
+
inputSchema: external_exports.object({
|
|
44767
|
+
directory: nonEmpty.describe(
|
|
44768
|
+
"The absolute path of the local git checkout of the pull request's branch. Git runs there."
|
|
44769
|
+
),
|
|
44770
|
+
base: nonEmpty.optional().describe(
|
|
44771
|
+
"The ref the pull request merges into, as dunstan template --base. Absent: the remote's default branch."
|
|
44772
|
+
),
|
|
44773
|
+
tests: TEMPLATE_TESTS.optional().describe(
|
|
44774
|
+
"Which tests to declare. Absent: none, and the note says how."
|
|
44775
|
+
)
|
|
44776
|
+
}).strict(),
|
|
44777
|
+
annotations: {
|
|
44778
|
+
readOnlyHint: true,
|
|
44779
|
+
destructiveHint: false,
|
|
44780
|
+
idempotentHint: true,
|
|
44781
|
+
openWorldHint: true
|
|
44782
|
+
}
|
|
44783
|
+
},
|
|
44784
|
+
async (input2) => {
|
|
44785
|
+
try {
|
|
44786
|
+
return await templateHandback(input2, options);
|
|
44787
|
+
} catch (error62) {
|
|
44788
|
+
return toolError(error62, "template");
|
|
44789
|
+
}
|
|
44790
|
+
}
|
|
44791
|
+
);
|
|
43492
44792
|
return server;
|
|
43493
44793
|
}
|
|
43494
44794
|
|
|
@@ -43515,6 +44815,132 @@ async function mcp(args, io) {
|
|
|
43515
44815
|
import { existsSync as existsSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
43516
44816
|
import { basename as basename3, resolve as resolve4 } from "node:path";
|
|
43517
44817
|
import { parseArgs as parseArgs3 } from "node:util";
|
|
44818
|
+
|
|
44819
|
+
// src/record/verify.ts
|
|
44820
|
+
function same(a, b) {
|
|
44821
|
+
return canonicalize(a) === canonicalize(b);
|
|
44822
|
+
}
|
|
44823
|
+
function predicateTypeOf(value) {
|
|
44824
|
+
return value !== null && typeof value === "object" ? value.predicateType : void 0;
|
|
44825
|
+
}
|
|
44826
|
+
function isDraft(value) {
|
|
44827
|
+
return predicateTypeOf(value) === DRAFT_PREDICATE_TYPE;
|
|
44828
|
+
}
|
|
44829
|
+
function isGitlabRecord(value) {
|
|
44830
|
+
return predicateTypeOf(value) === GITLAB_DRAFT_PREDICATE_TYPE;
|
|
44831
|
+
}
|
|
44832
|
+
function validateAnyRecord(value) {
|
|
44833
|
+
if (isDraft(value)) return validateDraftRecord(value);
|
|
44834
|
+
if (isGitlabRecord(value)) return validateGitlabDraftRecord(value);
|
|
44835
|
+
return validateRecord(value);
|
|
44836
|
+
}
|
|
44837
|
+
function verifyRecord(value) {
|
|
44838
|
+
const draft = isDraft(value);
|
|
44839
|
+
const schemaErrors = validateAnyRecord(value);
|
|
44840
|
+
if (schemaErrors.length > 0) {
|
|
44841
|
+
return {
|
|
44842
|
+
ok: false,
|
|
44843
|
+
problems: schemaErrors.map((e) => ({
|
|
44844
|
+
member: e.pointer,
|
|
44845
|
+
message: `schema_violation (${e.keyword}): ${e.message}`
|
|
44846
|
+
}))
|
|
44847
|
+
};
|
|
44848
|
+
}
|
|
44849
|
+
const record2 = value;
|
|
44850
|
+
const p = record2.predicate;
|
|
44851
|
+
const problems = [];
|
|
44852
|
+
const differ = (member, recorded, recomputed) => {
|
|
44853
|
+
if (!same(recorded, recomputed)) {
|
|
44854
|
+
problems.push({
|
|
44855
|
+
member,
|
|
44856
|
+
message: `recorded ${canonicalize(recorded)}, recomputed ${canonicalize(recomputed)}`
|
|
44857
|
+
});
|
|
44858
|
+
}
|
|
44859
|
+
};
|
|
44860
|
+
if (p.checker.name !== CHECKER_NAME || p.checker.version !== CHECKER_VERSION) {
|
|
44861
|
+
problems.push({
|
|
44862
|
+
member: "/predicate/checker",
|
|
44863
|
+
message: `record was written by ${p.checker.name} ${p.checker.version}; this is ${CHECKER_NAME} ${CHECKER_VERSION}`
|
|
44864
|
+
});
|
|
44865
|
+
return { ok: false, problems, record: record2 };
|
|
44866
|
+
}
|
|
44867
|
+
if (p.block.status === "found") {
|
|
44868
|
+
differ(
|
|
44869
|
+
"/predicate/block/sha256",
|
|
44870
|
+
p.block.sha256,
|
|
44871
|
+
sha256Canonical(p.block.value)
|
|
44872
|
+
);
|
|
44873
|
+
}
|
|
44874
|
+
const evidence = p.evidence;
|
|
44875
|
+
const pr = evidence.pullRequest;
|
|
44876
|
+
differ("/predicate/subject/pullRequest", p.subject.pullRequest, pr.number);
|
|
44877
|
+
differ("/predicate/subject/headSha", p.subject.headSha, pr.headSha);
|
|
44878
|
+
differ("/predicate/subject/mergeSha", p.subject.mergeSha, pr.mergeSha);
|
|
44879
|
+
differ("/subject", record2.subject, statementSubjects(p.block, p.subject));
|
|
44880
|
+
const claims = claimsFor(p.block, evidence, p.subject.repository);
|
|
44881
|
+
if (!same(p.claims, claims)) {
|
|
44882
|
+
const index = claims.findIndex((c, i) => !same(c, p.claims[i]));
|
|
44883
|
+
const at2 = index === -1 ? claims.length : index;
|
|
44884
|
+
problems.push({
|
|
44885
|
+
member: `/predicate/claims/${at2}`,
|
|
44886
|
+
message: `recorded ${canonicalize(p.claims[at2] ?? null)}, recomputed ${canonicalize(claims[at2] ?? null)}`
|
|
44887
|
+
});
|
|
44888
|
+
}
|
|
44889
|
+
differ("/predicate/verdict", p.verdict, overallVerdict(p.block.status, claims));
|
|
44890
|
+
differ(
|
|
44891
|
+
"/predicate/digests/claims",
|
|
44892
|
+
p.digests.claims,
|
|
44893
|
+
claimsDigest(claims)
|
|
44894
|
+
);
|
|
44895
|
+
differ(
|
|
44896
|
+
"/predicate/digests/evidence",
|
|
44897
|
+
p.digests.evidence,
|
|
44898
|
+
evidenceDigest(evidence)
|
|
44899
|
+
);
|
|
44900
|
+
if (draft && p.readerClaims !== void 0) {
|
|
44901
|
+
p.readerClaims.forEach((claim2, i) => {
|
|
44902
|
+
const { observed, verdict, reason } = claim2;
|
|
44903
|
+
differ(
|
|
44904
|
+
`/predicate/readerClaims/${i}`,
|
|
44905
|
+
reason === void 0 ? { observed, verdict } : { observed, verdict, reason },
|
|
44906
|
+
checkReaderClaim(claim2, evidence, p.subject.repository)
|
|
44907
|
+
);
|
|
44908
|
+
});
|
|
44909
|
+
differ(
|
|
44910
|
+
"/predicate/digests/readerClaims",
|
|
44911
|
+
p.digests.readerClaims,
|
|
44912
|
+
readerClaimsDigest(p.readerClaims)
|
|
44913
|
+
);
|
|
44914
|
+
}
|
|
44915
|
+
if (draft && p.advisory !== void 0) {
|
|
44916
|
+
const a = p.advisory;
|
|
44917
|
+
const pathsAtHead = a.pathsAtHead;
|
|
44918
|
+
differ("/predicate/advisory/extractor", a.extractor, EXTRACTOR);
|
|
44919
|
+
differ("/predicate/advisory/comparison", a.comparison, COMPARISON);
|
|
44920
|
+
differ("/predicate/advisory/precision", a.precision, precisionFor(EXTRACTOR.digest.sha256));
|
|
44921
|
+
differ(
|
|
44922
|
+
"/predicate/advisory/differsAccuracy",
|
|
44923
|
+
a.differsAccuracy,
|
|
44924
|
+
differsAccuracyFor(EXTRACTOR.digest.sha256, COMPARISON.version)
|
|
44925
|
+
);
|
|
44926
|
+
differ(
|
|
44927
|
+
"/predicate/advisory/pathsAtHead",
|
|
44928
|
+
pathsAtHead.map((answer) => answer.path),
|
|
44929
|
+
pathsToRead(a.advisories, evidence, p.subject.repository)
|
|
44930
|
+
);
|
|
44931
|
+
a.advisories.forEach((advisory, i) => {
|
|
44932
|
+
differ(
|
|
44933
|
+
`/predicate/advisory/advisories/${i}`,
|
|
44934
|
+
{ observed: advisory.observed, note: advisory.note },
|
|
44935
|
+
compareAdvisory(advisory, evidence, p.subject.repository, pathsAtHead)
|
|
44936
|
+
);
|
|
44937
|
+
});
|
|
44938
|
+
differ("/predicate/digests/advisory", p.digests.advisory, advisoryDigest(a));
|
|
44939
|
+
}
|
|
44940
|
+
return { ok: problems.length === 0, problems, record: record2 };
|
|
44941
|
+
}
|
|
44942
|
+
|
|
44943
|
+
// src/cli/rerun.ts
|
|
43518
44944
|
function diffPointers(a, b, pointer = "") {
|
|
43519
44945
|
const isObject2 = (v) => v !== null && typeof v === "object";
|
|
43520
44946
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -43548,27 +44974,45 @@ async function rerun(args, io) {
|
|
|
43548
44974
|
if (resolve4(out) === resolve4(path))
|
|
43549
44975
|
throw new UsageError("rerun never overwrites the record it re-runs");
|
|
43550
44976
|
const value = parseStrictJson(readFileSync5(path, "utf8"));
|
|
43551
|
-
const
|
|
44977
|
+
const gitlab = isGitlabRecord(value);
|
|
44978
|
+
const errors = gitlab ? validateGitlabDraftRecord(value) : validateRecord(value);
|
|
43552
44979
|
if (errors.length > 0) {
|
|
43553
44980
|
throw new UsageError(
|
|
43554
44981
|
`${path} is not a valid record: ${errors[0]?.pointer} ${errors[0]?.message}`
|
|
43555
44982
|
);
|
|
43556
44983
|
}
|
|
43557
44984
|
const old = value.predicate;
|
|
43558
|
-
const
|
|
43559
|
-
|
|
43560
|
-
|
|
43561
|
-
|
|
43562
|
-
`
|
|
43563
|
-
|
|
44985
|
+
const forge = old.subject.forge;
|
|
44986
|
+
if (gitlab && values["exclude-check-run"] !== void 0) {
|
|
44987
|
+
throw new UsageError("a GitLab record is re-run without --exclude-check-run");
|
|
44988
|
+
}
|
|
44989
|
+
const warn = (message) => io.err(`dunstan: ${message}
|
|
44990
|
+
`);
|
|
44991
|
+
const fetchOption = io.fetch === void 0 ? {} : { fetch: io.fetch };
|
|
44992
|
+
let client;
|
|
44993
|
+
let readers;
|
|
44994
|
+
if (forge === void 0) {
|
|
44995
|
+
const github = new GitHubClient({ token: io.env.GITHUB_TOKEN, ...fetchOption, warn });
|
|
44996
|
+
client = github;
|
|
44997
|
+
readers = githubReaders(github);
|
|
44998
|
+
} else {
|
|
44999
|
+
const gitlabClient = new GitLabClient({
|
|
45000
|
+
host: forge.host,
|
|
45001
|
+
token: io.env.GITLAB_TOKEN,
|
|
45002
|
+
...fetchOption,
|
|
45003
|
+
warn
|
|
45004
|
+
});
|
|
45005
|
+
client = gitlabClient;
|
|
45006
|
+
readers = gitlabReaders(gitlabClient);
|
|
45007
|
+
}
|
|
43564
45008
|
const oldExcluded = old.evidence.checkRuns?.status === "ok" ? old.evidence.checkRuns.excludedIds : [];
|
|
43565
45009
|
const excluded = [...oldExcluded, ...parseCheckRunIds(values["exclude-check-run"])];
|
|
43566
45010
|
const evidence = await atOnePullRequest(
|
|
43567
|
-
|
|
45011
|
+
readers,
|
|
43568
45012
|
old.subject.repository,
|
|
43569
45013
|
old.subject.pullRequest,
|
|
43570
45014
|
void 0,
|
|
43571
|
-
(pr) =>
|
|
45015
|
+
(pr) => readers.evidence({
|
|
43572
45016
|
repository: old.subject.repository,
|
|
43573
45017
|
pullRequest: pr,
|
|
43574
45018
|
block: old.block.status === "found" ? old.block.value : null,
|
|
@@ -43581,7 +45025,8 @@ async function rerun(args, io) {
|
|
|
43581
45025
|
block: old.block,
|
|
43582
45026
|
repository: old.subject.repository,
|
|
43583
45027
|
evidence,
|
|
43584
|
-
rerun: rerunCommands(basename3(out))
|
|
45028
|
+
rerun: rerunCommands(basename3(out)),
|
|
45029
|
+
...forge === void 0 ? {} : { forge }
|
|
43585
45030
|
});
|
|
43586
45031
|
if (existsSync2(out)) io.err(`dunstan: replacing ${out}
|
|
43587
45032
|
`);
|
|
@@ -43604,7 +45049,7 @@ async function rerun(args, io) {
|
|
|
43604
45049
|
|
|
43605
45050
|
`);
|
|
43606
45051
|
}
|
|
43607
|
-
io.out(renderRecord(record2, { hasToken: client.hasToken }));
|
|
45052
|
+
io.out(renderRecord(record2, { hasToken: client.hasToken, caller: "cli" }));
|
|
43608
45053
|
io.out(`record: ${out} (the original ${path} is unchanged)
|
|
43609
45054
|
`);
|
|
43610
45055
|
return exitFor(p.verdict);
|
|
@@ -43639,7 +45084,7 @@ import { basename as basename4 } from "node:path";
|
|
|
43639
45084
|
import { parseArgs as parseArgs5 } from "node:util";
|
|
43640
45085
|
|
|
43641
45086
|
// src/survey/mapping.ts
|
|
43642
|
-
var SURVEY_MAPPING_VERSION = "0.
|
|
45087
|
+
var SURVEY_MAPPING_VERSION = "0.3";
|
|
43643
45088
|
var CLAIM_TYPES = [
|
|
43644
45089
|
{ type: "T1", label: "head commit" },
|
|
43645
45090
|
{ type: "T2", label: "files changed" },
|
|
@@ -43710,7 +45155,7 @@ var zero = () => ({
|
|
|
43710
45155
|
});
|
|
43711
45156
|
var byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
43712
45157
|
function aggregate(entries, listed2, advisory, reader) {
|
|
43713
|
-
const context = { hasToken: reader.authenticated };
|
|
45158
|
+
const context = { hasToken: reader.authenticated, caller: "cli" };
|
|
43714
45159
|
const totals = zero();
|
|
43715
45160
|
const types = new Map(CLAIM_TYPES.map((t) => [t.type, zero()]));
|
|
43716
45161
|
const unsettled2 = /* @__PURE__ */ new Map();
|
|
@@ -43805,8 +45250,8 @@ function selectPullRequests(parameters, listed2) {
|
|
|
43805
45250
|
(pr) => authors.size === 0 || pr.author !== null && authors.has(pr.author.toLowerCase())
|
|
43806
45251
|
).slice(0, parameters.last).map((pr) => pr.number);
|
|
43807
45252
|
}
|
|
43808
|
-
function listingStop(parameters,
|
|
43809
|
-
const last =
|
|
45253
|
+
function listingStop(parameters, pages2, listed2) {
|
|
45254
|
+
const last = pages2.at(-1);
|
|
43810
45255
|
if (last === void 0) return null;
|
|
43811
45256
|
if (last.error !== void 0) return "listing_unreadable";
|
|
43812
45257
|
if (selectPullRequests(parameters, listed2).length >= parameters.last) return "selection_full";
|
|
@@ -43877,18 +45322,18 @@ function rateLimited(response) {
|
|
|
43877
45322
|
const s = response.status;
|
|
43878
45323
|
return s === 429 || s === 403 && (response.headers.get("x-ratelimit-remaining") === "0" || response.headers.get("retry-after") !== null);
|
|
43879
45324
|
}
|
|
43880
|
-
function
|
|
45325
|
+
function str3(value) {
|
|
43881
45326
|
return typeof value === "string" ? value : null;
|
|
43882
45327
|
}
|
|
43883
45328
|
function listedOf(item) {
|
|
43884
45329
|
if (item === null || typeof item !== "object") return null;
|
|
43885
45330
|
const pr = item;
|
|
43886
|
-
const createdAt =
|
|
45331
|
+
const createdAt = str3(pr.created_at);
|
|
43887
45332
|
if (typeof pr.number !== "number" || !Number.isSafeInteger(pr.number) || pr.number < 1 || createdAt === null || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?Z$/.test(createdAt)) {
|
|
43888
45333
|
return null;
|
|
43889
45334
|
}
|
|
43890
45335
|
const user = pr.user;
|
|
43891
|
-
const login =
|
|
45336
|
+
const login = str3(user?.login);
|
|
43892
45337
|
return {
|
|
43893
45338
|
number: pr.number,
|
|
43894
45339
|
createdAt,
|
|
@@ -43906,14 +45351,14 @@ async function readListing(client, parameters, now, pageBound) {
|
|
|
43906
45351
|
}
|
|
43907
45352
|
}
|
|
43908
45353
|
async function readOnce(client, parameters, now, pageBound, abandonOnRepeat) {
|
|
43909
|
-
const
|
|
45354
|
+
const pages2 = [];
|
|
43910
45355
|
const pullRequests = [];
|
|
43911
45356
|
const seen = /* @__PURE__ */ new Set();
|
|
43912
45357
|
const done = (end) => ({
|
|
43913
45358
|
pageBound,
|
|
43914
|
-
pages,
|
|
45359
|
+
pages: pages2,
|
|
43915
45360
|
pullRequests,
|
|
43916
|
-
duplicates:
|
|
45361
|
+
duplicates: pages2.reduce((sum, p) => sum + p.duplicates, 0),
|
|
43917
45362
|
end
|
|
43918
45363
|
});
|
|
43919
45364
|
for (let page = 1; page <= pageBound; page++) {
|
|
@@ -43937,9 +45382,9 @@ async function readOnce(client, parameters, now, pageBound, abandonOnRepeat) {
|
|
|
43937
45382
|
entry.duplicates = appendListed(pullRequests, seen, items);
|
|
43938
45383
|
}
|
|
43939
45384
|
}
|
|
43940
|
-
|
|
43941
|
-
if (abandonOnRepeat && entry.duplicates > 0) return { pages };
|
|
43942
|
-
const end = listingStop(parameters,
|
|
45385
|
+
pages2.push(entry);
|
|
45386
|
+
if (abandonOnRepeat && entry.duplicates > 0) return { pages: pages2 };
|
|
45387
|
+
const end = listingStop(parameters, pages2, pullRequests);
|
|
43943
45388
|
if (end !== null) return done(end);
|
|
43944
45389
|
}
|
|
43945
45390
|
return done("page_bound");
|
|
@@ -44033,7 +45478,7 @@ function share(r) {
|
|
|
44033
45478
|
function interval(r) {
|
|
44034
45479
|
return r.wilson95 === null ? "n/a" : `${r.wilson95.low}\u2013${r.wilson95.high}`;
|
|
44035
45480
|
}
|
|
44036
|
-
function
|
|
45481
|
+
function plural3(n, one, many) {
|
|
44037
45482
|
return `${n} ${n === 1 ? one : many}`;
|
|
44038
45483
|
}
|
|
44039
45484
|
function renderSurveySummary(record2) {
|
|
@@ -44057,7 +45502,7 @@ function renderSurveySummary(record2) {
|
|
|
44057
45502
|
"",
|
|
44058
45503
|
"## Claims the record could not settle",
|
|
44059
45504
|
"",
|
|
44060
|
-
`The record could not settle ${a.cannotSettle.numerator} of ${
|
|
45505
|
+
`The record could not settle ${a.cannotSettle.numerator} of ${plural3(a.cannotSettle.denominator, "claim", "claims")}.`,
|
|
44061
45506
|
""
|
|
44062
45507
|
);
|
|
44063
45508
|
if (a.unsettled.length === 0) {
|
|
@@ -44129,7 +45574,7 @@ function renderSurveySummary(record2) {
|
|
|
44129
45574
|
"",
|
|
44130
45575
|
"## Advisory notes",
|
|
44131
45576
|
"",
|
|
44132
|
-
`The advisory reader read the prose of ${
|
|
45577
|
+
`The advisory reader read the prose of ${plural3(a.advisory.pullRequests, "pull request", "pull requests")}. Its notes never enter either rate, and a possible disagreement is unverified.`,
|
|
44133
45578
|
""
|
|
44134
45579
|
);
|
|
44135
45580
|
if (a.advisory.notes.length === 0) lines.push("No notes.");
|
|
@@ -44143,7 +45588,7 @@ function renderSurveySummary(record2) {
|
|
|
44143
45588
|
}
|
|
44144
45589
|
}
|
|
44145
45590
|
const p = a.pullRequests;
|
|
44146
|
-
const context = { hasToken: record2.selection.reader.authenticated };
|
|
45591
|
+
const context = { hasToken: record2.selection.reader.authenticated, caller: "cli" };
|
|
44147
45592
|
const remedy = (reason) => remedyForReason(reason, context) ?? "";
|
|
44148
45593
|
lines.push(
|
|
44149
45594
|
"",
|
|
@@ -44162,7 +45607,7 @@ function renderSurveySummary(record2) {
|
|
|
44162
45607
|
]
|
|
44163
45608
|
),
|
|
44164
45609
|
"",
|
|
44165
|
-
`The listing was read for ${
|
|
45610
|
+
`The listing was read for ${plural3(listing.pages.length, "page", "pages")} and no further because ${listing.end === "page_bound" ? `it reached the bound of ${listing.pageBound} pages` : END_TEXT[listing.end]}.`
|
|
44166
45611
|
);
|
|
44167
45612
|
const abandoned = listing.abandoned.length;
|
|
44168
45613
|
if (listing.duplicates > 0) {
|
|
@@ -44170,18 +45615,18 @@ function renderSurveySummary(record2) {
|
|
|
44170
45615
|
const reread = abandoned === 0 ? "" : ` It was read ${abandoned + 1} times, from the first page each time, and moved during every read.`;
|
|
44171
45616
|
lines.push(
|
|
44172
45617
|
"",
|
|
44173
|
-
`The listing named ${
|
|
45618
|
+
`The listing named ${plural3(listing.duplicates, "pull request", "pull requests")} again on a later page, so it changed while it was read: most likely ${plural3(listing.duplicates, "pull request", "pull requests")} closed meanwhile and ${one ? "was" : "were"} not listed, and the selection may be missing ${one ? "it" : "them"}. Each repeated pull request is listed, selected and counted once: the pages held ${listing.pullRequests.length + listing.duplicates} items for ${listing.pullRequests.length} pull requests.${reread}`
|
|
44174
45619
|
);
|
|
44175
45620
|
} else if (abandoned > 0) {
|
|
44176
45621
|
lines.push(
|
|
44177
45622
|
"",
|
|
44178
|
-
`The listing moved while it was read: ${
|
|
45623
|
+
`The listing moved while it was read: ${plural3(abandoned, "read was", "reads were")} abandoned at a repeated pull request and the listing was read again from the first page. The read used repeated no pull request.`
|
|
44179
45624
|
);
|
|
44180
45625
|
}
|
|
44181
45626
|
if (selected.length < parameters.last) {
|
|
44182
45627
|
lines.push(
|
|
44183
45628
|
"",
|
|
44184
|
-
`${
|
|
45629
|
+
`${plural3(selected.length, "pull request was", "pull requests were")} selected, fewer than the ${parameters.last} asked for.`
|
|
44185
45630
|
);
|
|
44186
45631
|
}
|
|
44187
45632
|
lines.push(
|
|
@@ -44268,95 +45713,12 @@ summary: ${values.summary}
|
|
|
44268
45713
|
|
|
44269
45714
|
// src/cli/template.ts
|
|
44270
45715
|
import { parseArgs as parseArgs6 } from "node:util";
|
|
44271
|
-
|
|
44272
|
-
// src/template/tests-ci.ts
|
|
44273
|
-
var plural3 = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
44274
|
-
function unsettled(reason, why, remedy) {
|
|
44275
|
-
const notes = [`tests are not declared: ${why} (${reason}).`];
|
|
44276
|
-
if (remedy !== void 0) notes.push(`remedy: ${remedy}`);
|
|
44277
|
-
return { status: "unsettled", reason, notes };
|
|
44278
|
-
}
|
|
44279
|
-
async function testsFromCi(client, input2) {
|
|
44280
|
-
const { repository, headCommit: head, record: record2 } = input2;
|
|
44281
|
-
if (!client.hasToken) {
|
|
44282
|
-
return unsettled(
|
|
44283
|
-
"no_token",
|
|
44284
|
-
"GITHUB_TOKEN is not set, and GitHub answers an artifact download only with a token, even on a public repository",
|
|
44285
|
-
"set GITHUB_TOKEN to a token that can read the repository, with actions: read, and run dunstan template --tests-ci again."
|
|
44286
|
-
);
|
|
44287
|
-
}
|
|
44288
|
-
if (!input2.pushed) {
|
|
44289
|
-
return unsettled(
|
|
44290
|
-
"not_pushed",
|
|
44291
|
-
`HEAD ${head} is not on any remote-tracking branch, so no CI run can have tested it`,
|
|
44292
|
-
"push it, wait for CI to finish, and run dunstan template --tests-ci again (run git fetch first if it was pushed from elsewhere)."
|
|
44293
|
-
);
|
|
44294
|
-
}
|
|
44295
|
-
const observed = { runsNotCompleted: 0 };
|
|
44296
|
-
const evidence = await readTestRecord(client, repository, record2, head, observed);
|
|
44297
|
-
const named = `the artifact ${record2.artifact} holding ${record2.path} from job ${record2.job}`;
|
|
44298
|
-
const pending = observed.runsNotCompleted > 0 ? `CI has not finished: ${plural3(observed.runsNotCompleted, "run", "runs")} of ${record2.workflow} at ${head} ${observed.runsNotCompleted === 1 ? "is" : "are"} not completed` : void 0;
|
|
44299
|
-
const again = "run dunstan template --tests-ci again once CI has finished on this commit.";
|
|
44300
|
-
if (evidence.status !== "ok") {
|
|
44301
|
-
const reason = evidence.status === "unreadable" ? `source_unreadable:${evidence.source}` : `evidence_field_unpopulated:${evidence.field}`;
|
|
44302
|
-
if (pending !== void 0) return unsettled(reason, pending, again);
|
|
44303
|
-
const why = reason === "source_unreadable:artifact" ? `a run of ${record2.workflow} at ${head} uploaded ${record2.artifact}, and it could not be read` : `the workflow runs of ${record2.workflow} at ${head} could not be read`;
|
|
44304
|
-
return unsettled(
|
|
44305
|
-
reason,
|
|
44306
|
-
why,
|
|
44307
|
-
remedyForReason(reason, { hasToken: true }) ?? "run dunstan template --tests-ci again."
|
|
44308
|
-
);
|
|
44309
|
-
}
|
|
44310
|
-
if (evidence.runs.length === 0) {
|
|
44311
|
-
if (pending !== void 0) {
|
|
44312
|
-
return unsettled(
|
|
44313
|
-
"record_not_found",
|
|
44314
|
-
`${pending}, and no completed run uploaded ${named}`,
|
|
44315
|
-
again
|
|
44316
|
-
);
|
|
44317
|
-
}
|
|
44318
|
-
return unsettled(
|
|
44319
|
-
"record_not_found",
|
|
44320
|
-
`no run of ${record2.workflow} at ${head} uploaded ${named}`,
|
|
44321
|
-
remedyForReason("record_not_found", { hasToken: true })
|
|
44322
|
-
);
|
|
44323
|
-
}
|
|
44324
|
-
const executed = new Set(evidence.runs.map((r) => r.executed));
|
|
44325
|
-
const failed = new Set(evidence.runs.map((r) => r.failed));
|
|
44326
|
-
if (executed.size > 1 || failed.size > 1) {
|
|
44327
|
-
const counts = evidence.runs.map((r) => `run ${r.runId}: ${r.executed} executed, ${r.failed} failed`).join("; ");
|
|
44328
|
-
return unsettled(
|
|
44329
|
-
"record_ambiguous",
|
|
44330
|
-
`the runs of ${record2.workflow} at ${head} that uploaded ${record2.artifact} disagree (${counts}), and the gate never picks one`,
|
|
44331
|
-
"make the runs agree (re-run the one that differs), or name a job only one of them runs."
|
|
44332
|
-
);
|
|
44333
|
-
}
|
|
44334
|
-
const run = evidence.runs[0];
|
|
44335
|
-
const runs = evidence.runs.map((r) => r.runId).join(", ");
|
|
44336
|
-
const notes = [
|
|
44337
|
-
`tests: ${run.executed} executed and ${run.failed} failed in ${record2.path} in the artifact ${record2.artifact} of job ${record2.job} in ${record2.workflow} at ${head}, read from CI as the gate reads it (${evidence.runs.length === 1 ? "run" : "runs"} ${runs}).`
|
|
44338
|
-
];
|
|
44339
|
-
if (pending !== void 0) {
|
|
44340
|
-
notes.push(
|
|
44341
|
-
`${pending}; a run that finishes later with other counts makes the gate's read ambiguous. Run dunstan template --tests-ci again once CI has finished.`
|
|
44342
|
-
);
|
|
44343
|
-
}
|
|
44344
|
-
return {
|
|
44345
|
-
status: "settled",
|
|
44346
|
-
run: { command: input2.command, count: run.executed, failures: run.failed, record: record2 },
|
|
44347
|
-
notes
|
|
44348
|
-
};
|
|
44349
|
-
}
|
|
44350
|
-
|
|
44351
|
-
// src/cli/template.ts
|
|
44352
45716
|
var RECORD_FLAGS = [
|
|
44353
45717
|
"tests-workflow",
|
|
44354
45718
|
"tests-job",
|
|
44355
45719
|
"tests-artifact",
|
|
44356
45720
|
"tests-artifact-path"
|
|
44357
45721
|
];
|
|
44358
|
-
var DEFAULT_ARTIFACT = "dunstan-junit";
|
|
44359
|
-
var DEFAULT_ARTIFACT_PATH = "junit.xml";
|
|
44360
45722
|
async function template(args, io) {
|
|
44361
45723
|
const { values } = parseArgs6({
|
|
44362
45724
|
args,
|
|
@@ -44381,7 +45743,6 @@ async function template(args, io) {
|
|
|
44381
45743
|
}
|
|
44382
45744
|
if (flag("repo") !== void 0 && !ci) throw new UsageError("--repo goes with --tests-ci");
|
|
44383
45745
|
const given = ["tests-command", ...RECORD_FLAGS].filter((f) => flag(f) !== void 0);
|
|
44384
|
-
const cwd = io.cwd ?? process.cwd();
|
|
44385
45746
|
let tests;
|
|
44386
45747
|
if (ci) {
|
|
44387
45748
|
const missing = ["tests-command", "tests-workflow", "tests-job"].filter(
|
|
@@ -44394,28 +45755,15 @@ async function template(args, io) {
|
|
|
44394
45755
|
if (repo !== void 0 && !REPOSITORY.test(repo)) {
|
|
44395
45756
|
throw new UsageError("--repo must be <owner/name>");
|
|
44396
45757
|
}
|
|
44397
|
-
|
|
44398
|
-
|
|
44399
|
-
const client = new GitHubClient({
|
|
44400
|
-
token: io.env.GITHUB_TOKEN,
|
|
44401
|
-
...io.fetch === void 0 ? {} : { fetch: io.fetch },
|
|
44402
|
-
warn: (message) => io.err(`dunstan: ${message}
|
|
44403
|
-
`)
|
|
44404
|
-
});
|
|
44405
|
-
const read = await testsFromCi(client, {
|
|
44406
|
-
repository,
|
|
44407
|
-
headCommit: head,
|
|
45758
|
+
tests = {
|
|
45759
|
+
from: "ci",
|
|
44408
45760
|
command: flag("tests-command"),
|
|
44409
|
-
|
|
44410
|
-
|
|
44411
|
-
|
|
44412
|
-
|
|
44413
|
-
|
|
44414
|
-
|
|
44415
|
-
path: flag("tests-artifact-path") ?? DEFAULT_ARTIFACT_PATH
|
|
44416
|
-
}
|
|
44417
|
-
});
|
|
44418
|
-
tests = { kind: "ci", read };
|
|
45761
|
+
workflow: flag("tests-workflow"),
|
|
45762
|
+
job: flag("tests-job"),
|
|
45763
|
+
artifact: flag("tests-artifact"),
|
|
45764
|
+
artifactPath: flag("tests-artifact-path"),
|
|
45765
|
+
repository: repo
|
|
45766
|
+
};
|
|
44419
45767
|
} else if (flag("tests-local") !== void 0) {
|
|
44420
45768
|
if (flag("tests-command") === void 0) {
|
|
44421
45769
|
throw new UsageError("--tests-local needs --tests-command <cmd>");
|
|
@@ -44423,7 +45771,7 @@ async function template(args, io) {
|
|
|
44423
45771
|
const extra = RECORD_FLAGS.find((f) => flag(f) !== void 0);
|
|
44424
45772
|
if (extra !== void 0) throw new UsageError(`--${extra} does not go with --tests-local`);
|
|
44425
45773
|
tests = {
|
|
44426
|
-
|
|
45774
|
+
from: "local",
|
|
44427
45775
|
junit: flag("tests-local"),
|
|
44428
45776
|
command: flag("tests-command")
|
|
44429
45777
|
};
|
|
@@ -44437,143 +45785,32 @@ async function template(args, io) {
|
|
|
44437
45785
|
);
|
|
44438
45786
|
}
|
|
44439
45787
|
tests = {
|
|
44440
|
-
|
|
45788
|
+
from: "junit",
|
|
44441
45789
|
junit: flag("tests-junit"),
|
|
44442
45790
|
command: flag("tests-command"),
|
|
44443
|
-
|
|
44444
|
-
|
|
44445
|
-
|
|
44446
|
-
|
|
44447
|
-
path: flag("tests-artifact-path")
|
|
44448
|
-
}
|
|
45791
|
+
workflow: flag("tests-workflow"),
|
|
45792
|
+
job: flag("tests-job"),
|
|
45793
|
+
artifact: flag("tests-artifact"),
|
|
45794
|
+
artifactPath: flag("tests-artifact-path")
|
|
44449
45795
|
};
|
|
44450
45796
|
} else if (given.length > 0) {
|
|
44451
45797
|
throw new UsageError(`--${given[0]} goes with --tests-ci, --tests-local or --tests-junit`);
|
|
44452
45798
|
}
|
|
44453
|
-
const
|
|
44454
|
-
|
|
44455
|
-
|
|
45799
|
+
const run = await runTemplate(
|
|
45800
|
+
{ cwd: io.cwd ?? process.cwd(), base: flag("base"), tests, caller: "cli" },
|
|
45801
|
+
{ env: io.env, fetch: io.fetch, warn: (message) => io.err(`dunstan: ${message}
|
|
45802
|
+
`) }
|
|
45803
|
+
);
|
|
45804
|
+
io.out(run.fence);
|
|
45805
|
+
for (const note of run.notes) io.err(`dunstan: ${note}
|
|
44456
45806
|
`);
|
|
44457
|
-
return tests
|
|
45807
|
+
return run.tests === "not_declared" ? EXIT.unverifiable : 0;
|
|
44458
45808
|
}
|
|
44459
45809
|
|
|
44460
45810
|
// src/cli/verify.ts
|
|
44461
45811
|
import { readFileSync as readFileSync7 } from "node:fs";
|
|
44462
45812
|
import { parseArgs as parseArgs7 } from "node:util";
|
|
44463
45813
|
|
|
44464
|
-
// src/record/verify.ts
|
|
44465
|
-
function same(a, b) {
|
|
44466
|
-
return canonicalize(a) === canonicalize(b);
|
|
44467
|
-
}
|
|
44468
|
-
function isDraft(value) {
|
|
44469
|
-
return value !== null && typeof value === "object" && value.predicateType === DRAFT_PREDICATE_TYPE;
|
|
44470
|
-
}
|
|
44471
|
-
function verifyRecord(value) {
|
|
44472
|
-
const draft = isDraft(value);
|
|
44473
|
-
const schemaErrors = draft ? validateDraftRecord(value) : validateRecord(value);
|
|
44474
|
-
if (schemaErrors.length > 0) {
|
|
44475
|
-
return {
|
|
44476
|
-
ok: false,
|
|
44477
|
-
problems: schemaErrors.map((e) => ({
|
|
44478
|
-
member: e.pointer,
|
|
44479
|
-
message: `schema_violation (${e.keyword}): ${e.message}`
|
|
44480
|
-
}))
|
|
44481
|
-
};
|
|
44482
|
-
}
|
|
44483
|
-
const record2 = value;
|
|
44484
|
-
const p = record2.predicate;
|
|
44485
|
-
const problems = [];
|
|
44486
|
-
const differ = (member, recorded, recomputed) => {
|
|
44487
|
-
if (!same(recorded, recomputed)) {
|
|
44488
|
-
problems.push({
|
|
44489
|
-
member,
|
|
44490
|
-
message: `recorded ${canonicalize(recorded)}, recomputed ${canonicalize(recomputed)}`
|
|
44491
|
-
});
|
|
44492
|
-
}
|
|
44493
|
-
};
|
|
44494
|
-
if (p.checker.name !== CHECKER_NAME || p.checker.version !== CHECKER_VERSION) {
|
|
44495
|
-
problems.push({
|
|
44496
|
-
member: "/predicate/checker",
|
|
44497
|
-
message: `record was written by ${p.checker.name} ${p.checker.version}; this is ${CHECKER_NAME} ${CHECKER_VERSION}`
|
|
44498
|
-
});
|
|
44499
|
-
return { ok: false, problems, record: record2 };
|
|
44500
|
-
}
|
|
44501
|
-
if (p.block.status === "found") {
|
|
44502
|
-
differ(
|
|
44503
|
-
"/predicate/block/sha256",
|
|
44504
|
-
p.block.sha256,
|
|
44505
|
-
sha256Canonical(p.block.value)
|
|
44506
|
-
);
|
|
44507
|
-
}
|
|
44508
|
-
const evidence = p.evidence;
|
|
44509
|
-
const pr = evidence.pullRequest;
|
|
44510
|
-
differ("/predicate/subject/pullRequest", p.subject.pullRequest, pr.number);
|
|
44511
|
-
differ("/predicate/subject/headSha", p.subject.headSha, pr.headSha);
|
|
44512
|
-
differ("/predicate/subject/mergeSha", p.subject.mergeSha, pr.mergeSha);
|
|
44513
|
-
differ("/subject", record2.subject, statementSubjects(p.block, p.subject));
|
|
44514
|
-
const claims = claimsFor(p.block, evidence, p.subject.repository);
|
|
44515
|
-
if (!same(p.claims, claims)) {
|
|
44516
|
-
const index = claims.findIndex((c, i) => !same(c, p.claims[i]));
|
|
44517
|
-
const at2 = index === -1 ? claims.length : index;
|
|
44518
|
-
problems.push({
|
|
44519
|
-
member: `/predicate/claims/${at2}`,
|
|
44520
|
-
message: `recorded ${canonicalize(p.claims[at2] ?? null)}, recomputed ${canonicalize(claims[at2] ?? null)}`
|
|
44521
|
-
});
|
|
44522
|
-
}
|
|
44523
|
-
differ("/predicate/verdict", p.verdict, overallVerdict(p.block.status, claims));
|
|
44524
|
-
differ(
|
|
44525
|
-
"/predicate/digests/claims",
|
|
44526
|
-
p.digests.claims,
|
|
44527
|
-
claimsDigest(claims)
|
|
44528
|
-
);
|
|
44529
|
-
differ(
|
|
44530
|
-
"/predicate/digests/evidence",
|
|
44531
|
-
p.digests.evidence,
|
|
44532
|
-
evidenceDigest(evidence)
|
|
44533
|
-
);
|
|
44534
|
-
if (draft && p.readerClaims !== void 0) {
|
|
44535
|
-
p.readerClaims.forEach((claim2, i) => {
|
|
44536
|
-
const { observed, verdict, reason } = claim2;
|
|
44537
|
-
differ(
|
|
44538
|
-
`/predicate/readerClaims/${i}`,
|
|
44539
|
-
reason === void 0 ? { observed, verdict } : { observed, verdict, reason },
|
|
44540
|
-
checkReaderClaim(claim2, evidence, p.subject.repository)
|
|
44541
|
-
);
|
|
44542
|
-
});
|
|
44543
|
-
differ(
|
|
44544
|
-
"/predicate/digests/readerClaims",
|
|
44545
|
-
p.digests.readerClaims,
|
|
44546
|
-
readerClaimsDigest(p.readerClaims)
|
|
44547
|
-
);
|
|
44548
|
-
}
|
|
44549
|
-
if (draft && p.advisory !== void 0) {
|
|
44550
|
-
const a = p.advisory;
|
|
44551
|
-
const pathsAtHead = a.pathsAtHead;
|
|
44552
|
-
differ("/predicate/advisory/extractor", a.extractor, EXTRACTOR);
|
|
44553
|
-
differ("/predicate/advisory/comparison", a.comparison, COMPARISON);
|
|
44554
|
-
differ("/predicate/advisory/precision", a.precision, precisionFor(EXTRACTOR.digest.sha256));
|
|
44555
|
-
differ(
|
|
44556
|
-
"/predicate/advisory/differsAccuracy",
|
|
44557
|
-
a.differsAccuracy,
|
|
44558
|
-
differsAccuracyFor(EXTRACTOR.digest.sha256, COMPARISON.version)
|
|
44559
|
-
);
|
|
44560
|
-
differ(
|
|
44561
|
-
"/predicate/advisory/pathsAtHead",
|
|
44562
|
-
pathsAtHead.map((answer) => answer.path),
|
|
44563
|
-
pathsToRead(a.advisories, evidence, p.subject.repository)
|
|
44564
|
-
);
|
|
44565
|
-
a.advisories.forEach((advisory, i) => {
|
|
44566
|
-
differ(
|
|
44567
|
-
`/predicate/advisory/advisories/${i}`,
|
|
44568
|
-
{ observed: advisory.observed, note: advisory.note },
|
|
44569
|
-
compareAdvisory(advisory, evidence, p.subject.repository, pathsAtHead)
|
|
44570
|
-
);
|
|
44571
|
-
});
|
|
44572
|
-
differ("/predicate/digests/advisory", p.digests.advisory, advisoryDigest(a));
|
|
44573
|
-
}
|
|
44574
|
-
return { ok: problems.length === 0, problems, record: record2 };
|
|
44575
|
-
}
|
|
44576
|
-
|
|
44577
45814
|
// src/survey/verify.ts
|
|
44578
45815
|
var text = (value) => canonicalize(value ?? null);
|
|
44579
45816
|
function differences(a, b, pointer) {
|
|
@@ -44884,6 +46121,10 @@ usage:
|
|
|
44884
46121
|
(--report-file <path|-> | --report-pr-body | --report-comment-author <login>)
|
|
44885
46122
|
[--out <record.json>] [--sign-key <path> --signer <principal>]
|
|
44886
46123
|
[--exclude-check-run <id>]... [--advisory]
|
|
46124
|
+
dunstan check --gitlab-project <namespace/project> --mr <iid> [--gitlab-host <host>]
|
|
46125
|
+
(--report-file <path|-> | --report-mr-description)
|
|
46126
|
+
[--out <record.json>] [--sign-key <path> --signer <principal>]
|
|
46127
|
+
[--exclude-gitlab-job <id>]...
|
|
44887
46128
|
dunstan verify <record.json> [--sig <file> --allowed-signers <file>]
|
|
44888
46129
|
dunstan rerun <record.json> [--out <record.json>] [--exclude-check-run <id>]...
|
|
44889
46130
|
dunstan template [--base <ref>]
|
|
@@ -44903,8 +46144,13 @@ check reads the pull request, the named report and the evidence its block ne
|
|
|
44903
46144
|
record and prints the claims table. GITHUB_TOKEN is read from the environment.
|
|
44904
46145
|
--advisory also reads the report's prose for asserted claims and records them as
|
|
44905
46146
|
advisories (DRAFT 0.2.0, docs/advisory.md). They never change the verdict.
|
|
44906
|
-
|
|
46147
|
+
GitLab (DRAFT GitLab profile 0.1): --gitlab-project and --mr check a merge request on
|
|
46148
|
+
gitlab.com, or on --gitlab-host; --report-mr-description reads its description.
|
|
46149
|
+
GITLAB_TOKEN is optional. Checks are the head pipeline's jobs and bridges, less each
|
|
46150
|
+
--exclude-gitlab-job (the checker's own job, CI_JOB_ID).
|
|
46151
|
+
verify offline: recomputes claims, verdict and digests from the record and compares them.
|
|
44907
46152
|
rerun online: re-reads the sources for the recorded subject and block; writes a new record.
|
|
46153
|
+
A GitLab record is re-read from the host it names.
|
|
44908
46154
|
template local git: prints a dunstan-handback block for HEAD, its files against the merge base
|
|
44909
46155
|
with --base (default: the remote's default branch). Tests: --tests-ci reads the counts
|
|
44910
46156
|
at HEAD from the CI record check reads (artifact dunstan-junit, file junit.xml unless
|