redlinegate 0.0.1
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/LICENSE +21 -0
- package/README.md +188 -0
- package/commands/redline-init.md +54 -0
- package/commands/redline-verify.md +69 -0
- package/dist/bin/redline.js +605 -0
- package/dist/bin/redline.js.map +1 -0
- package/dist/commands/exempt.js +45 -0
- package/dist/commands/exempt.js.map +1 -0
- package/dist/commands/init.js +615 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/policy.js +25 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/remove.js +427 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/review.js +72 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/sync.js +42 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/verify.js +478 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/config/redline-json.js +187 -0
- package/dist/config/redline-json.js.map +1 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +152 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/log.js +22 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/severity.js +16 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/core/version.js +15 -0
- package/dist/core/version.js.map +1 -0
- package/dist/detect/scan.js +79 -0
- package/dist/detect/scan.js.map +1 -0
- package/dist/detect/stack.js +71 -0
- package/dist/detect/stack.js.map +1 -0
- package/dist/enforce/ladder.js +148 -0
- package/dist/enforce/ladder.js.map +1 -0
- package/dist/exempt/parse.js +86 -0
- package/dist/exempt/parse.js.map +1 -0
- package/dist/metrics/options.js +257 -0
- package/dist/metrics/options.js.map +1 -0
- package/dist/metrics/run.js +72 -0
- package/dist/metrics/run.js.map +1 -0
- package/dist/platforms/azure/client.js +53 -0
- package/dist/platforms/azure/client.js.map +1 -0
- package/dist/platforms/azure/index.js +69 -0
- package/dist/platforms/azure/index.js.map +1 -0
- package/dist/platforms/azure/install.js +917 -0
- package/dist/platforms/azure/install.js.map +1 -0
- package/dist/platforms/azure/policy-types.js +70 -0
- package/dist/platforms/azure/policy-types.js.map +1 -0
- package/dist/platforms/azure/verify.js +309 -0
- package/dist/platforms/azure/verify.js.map +1 -0
- package/dist/platforms/detect.js +40 -0
- package/dist/platforms/detect.js.map +1 -0
- package/dist/platforms/github/client.js +49 -0
- package/dist/platforms/github/client.js.map +1 -0
- package/dist/platforms/github/index.js +54 -0
- package/dist/platforms/github/index.js.map +1 -0
- package/dist/platforms/github/install.js +558 -0
- package/dist/platforms/github/install.js.map +1 -0
- package/dist/platforms/github/push.js +102 -0
- package/dist/platforms/github/push.js.map +1 -0
- package/dist/platforms/github/remote.js +36 -0
- package/dist/platforms/github/remote.js.map +1 -0
- package/dist/platforms/github/verify.js +366 -0
- package/dist/platforms/github/verify.js.map +1 -0
- package/dist/platforms/http.js +58 -0
- package/dist/platforms/http.js.map +1 -0
- package/dist/platforms/pull-request-templates.js +157 -0
- package/dist/platforms/pull-request-templates.js.map +1 -0
- package/dist/platforms/remote.js +21 -0
- package/dist/platforms/remote.js.map +1 -0
- package/dist/platforms/resolve.js +69 -0
- package/dist/platforms/resolve.js.map +1 -0
- package/dist/platforms/shape.js +19 -0
- package/dist/platforms/shape.js.map +1 -0
- package/dist/platforms/types.js +24 -0
- package/dist/platforms/types.js.map +1 -0
- package/dist/policy/checks.js +91 -0
- package/dist/policy/checks.js.map +1 -0
- package/dist/policy/diff.js +50 -0
- package/dist/policy/diff.js.map +1 -0
- package/dist/registry/discover.js +74 -0
- package/dist/registry/discover.js.map +1 -0
- package/dist/registry/serialize.js +41 -0
- package/dist/registry/serialize.js.map +1 -0
- package/dist/registry/types.js +2 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/remove/host.js +294 -0
- package/dist/remove/host.js.map +1 -0
- package/dist/render/commands.js +139 -0
- package/dist/render/commands.js.map +1 -0
- package/dist/render/manifest.js +82 -0
- package/dist/render/manifest.js.map +1 -0
- package/dist/render/markers.js +166 -0
- package/dist/render/markers.js.map +1 -0
- package/dist/render/profile.js +23 -0
- package/dist/render/profile.js.map +1 -0
- package/dist/render/standards.js +169 -0
- package/dist/render/standards.js.map +1 -0
- package/dist/render/vendors.js +199 -0
- package/dist/render/vendors.js.map +1 -0
- package/dist/review/engines/api.js +95 -0
- package/dist/review/engines/api.js.map +1 -0
- package/dist/review/engines/embedded.js +20 -0
- package/dist/review/engines/embedded.js.map +1 -0
- package/dist/review/engines/types.js +2 -0
- package/dist/review/engines/types.js.map +1 -0
- package/dist/review/glob.js +29 -0
- package/dist/review/glob.js.map +1 -0
- package/dist/review/prompt.js +45 -0
- package/dist/review/prompt.js.map +1 -0
- package/dist/review/rules.js +25 -0
- package/dist/review/rules.js.map +1 -0
- package/dist/review/schema.js +84 -0
- package/dist/review/schema.js.map +1 -0
- package/dist/review/scope.js +27 -0
- package/dist/review/scope.js.map +1 -0
- package/dist/sarif/map.js +58 -0
- package/dist/sarif/map.js.map +1 -0
- package/dist/sarif/parse.js +88 -0
- package/dist/sarif/parse.js.map +1 -0
- package/dist/sarif/types.js +2 -0
- package/dist/sarif/types.js.map +1 -0
- package/dist/sync/host.js +21 -0
- package/dist/sync/host.js.map +1 -0
- package/dist/sync/plan.js +47 -0
- package/dist/sync/plan.js.map +1 -0
- package/dist/sync/render.js +57 -0
- package/dist/sync/render.js.map +1 -0
- package/dist/sync/run.js +118 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/verify/host.js +52 -0
- package/dist/verify/host.js.map +1 -0
- package/dist/verify/remote.js +146 -0
- package/dist/verify/remote.js.map +1 -0
- package/package.json +52 -0
- package/platforms/azure/gate-template.yml +105 -0
- package/rulesets/redline-org-ruleset.json +46 -0
- package/rulesets/redline-ruleset.json +49 -0
- package/scripts/assign-rule-ids.mjs +129 -0
- package/scripts/build-baseline.mjs +124 -0
- package/scripts/build-correlation.mjs +74 -0
- package/scripts/build-dashboard.mjs +584 -0
- package/scripts/build-digest.mjs +164 -0
- package/scripts/build-inbox.mjs +133 -0
- package/scripts/build-registry.mjs +42 -0
- package/scripts/build-roi.mjs +181 -0
- package/scripts/check-pins.mjs +88 -0
- package/scripts/collect-telemetry.mjs +300 -0
- package/scripts/lib/__tests__/baseline.test.mjs +132 -0
- package/scripts/lib/__tests__/correlate.test.mjs +131 -0
- package/scripts/lib/__tests__/dora.test.mjs +110 -0
- package/scripts/lib/__tests__/exemptions.test.mjs +85 -0
- package/scripts/lib/__tests__/metrics-scanner.test.mjs +65 -0
- package/scripts/lib/__tests__/sarif.test.mjs +125 -0
- package/scripts/lib/__tests__/spend.test.mjs +64 -0
- package/scripts/lib/baseline.mjs +156 -0
- package/scripts/lib/correlate.mjs +125 -0
- package/scripts/lib/dora.mjs +133 -0
- package/scripts/lib/exemptions.mjs +79 -0
- package/scripts/lib/metrics.mjs +172 -0
- package/scripts/lib/rules.mjs +107 -0
- package/scripts/lib/sarif.mjs +108 -0
- package/scripts/lib/spend.mjs +69 -0
- package/scripts/measure-context.mjs +101 -0
- package/scripts/render-self.mjs +18 -0
- package/scripts/score-seeds.mjs +294 -0
- package/scripts/validate.mjs +394 -0
- package/standards/core.md +124 -0
- package/standards/manifest.json +214 -0
- package/standards/stacks/csharp.md +29 -0
- package/standards/stacks/go.md +30 -0
- package/standards/stacks/java.md +30 -0
- package/standards/stacks/javascript.md +42 -0
- package/standards/stacks/kotlin.md +27 -0
- package/standards/stacks/microservices.md +37 -0
- package/standards/stacks/nodejs.md +34 -0
- package/standards/stacks/python.md +29 -0
- package/standards/stacks/react-native.md +29 -0
- package/standards/stacks/react.md +46 -0
- package/standards/stacks/swift.md +26 -0
- package/standards/stacks/terraform.md +27 -0
- package/templates/CODEOWNERS +29 -0
- package/templates/azure/pull_request_template.md +72 -0
- package/templates/github/pull_request_template.md +72 -0
- package/templates/redline.yml +35 -0
- package/templates/repo-context.md +64 -0
- package/workflows/dashboard.yml +119 -0
- package/workflows/inbox.yml +97 -0
- package/workflows/redline-collect.yml +61 -0
- package/workflows/redline-gate.yml +301 -0
- package/workflows/redline-sync.yml +79 -0
- package/workflows/seed-canary.yml +231 -0
- package/workflows/verify-onboarding.yml +115 -0
- package/workflows/weekly-digest.yml +78 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { render } from "../render/standards.js";
|
|
5
|
+
// Which paths a profile's render manages, without needing the target's content.
|
|
6
|
+
// Sync asks this first so it knows what to fetch: fetching the whole repository
|
|
7
|
+
// to find out would be absurd, and rendering into an empty directory to find out
|
|
8
|
+
// is exactly what this does, once, cheaply.
|
|
9
|
+
export function managedPaths(root, profile, vendors) {
|
|
10
|
+
const scratch = mkdtempSync(join(tmpdir(), 'redline-sync-probe-'));
|
|
11
|
+
try {
|
|
12
|
+
return render({ root, profile, out: scratch, vendors, check: true }).managed;
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Render a target's artifacts and hand back their content.
|
|
19
|
+
//
|
|
20
|
+
// The renderer writes to a directory and merges into whatever is already there —
|
|
21
|
+
// AGENTS.md keeps everything above the REDLINE:BEGIN marker, which is the
|
|
22
|
+
// repository's own context and the single most valuable thing on the page. So
|
|
23
|
+
// the target's current files are staged into a scratch directory first and the
|
|
24
|
+
// real renderer runs against them. Rendering into an empty directory instead
|
|
25
|
+
// would produce a correct-looking AGENTS.md that silently deletes the team's own
|
|
26
|
+
// section on every sync.
|
|
27
|
+
export function renderForTarget(opts) {
|
|
28
|
+
const scratch = mkdtempSync(join(tmpdir(), 'redline-sync-'));
|
|
29
|
+
try {
|
|
30
|
+
for (const [path, content] of opts.existing) {
|
|
31
|
+
const full = join(scratch, path);
|
|
32
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
33
|
+
writeFileSync(full, content);
|
|
34
|
+
}
|
|
35
|
+
const result = render({
|
|
36
|
+
root: opts.root,
|
|
37
|
+
profile: opts.profile,
|
|
38
|
+
out: scratch,
|
|
39
|
+
vendors: opts.vendors,
|
|
40
|
+
});
|
|
41
|
+
const files = [];
|
|
42
|
+
for (const path of result.written) {
|
|
43
|
+
const content = readFileSync(join(scratch, path), 'utf8');
|
|
44
|
+
// Unchanged files are dropped here rather than pushed: the tree API would
|
|
45
|
+
// accept them, but a diff listing every managed file on every sync makes
|
|
46
|
+
// the one that actually changed impossible to see.
|
|
47
|
+
if (opts.existing.get(path) === content)
|
|
48
|
+
continue;
|
|
49
|
+
files.push({ path, content });
|
|
50
|
+
}
|
|
51
|
+
return { files, removed: result.removed };
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../cli/sync/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAqBhD,gFAAgF;AAChF,gFAAgF;AAChF,iFAAiF;AACjF,4CAA4C;AAC5C,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAe,EAAE,OAAiB;IAC3E,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC;IACnE,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC;IAC/E,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC;AAED,2DAA2D;AAC3D,EAAE;AACF,iFAAiF;AACjF,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,6EAA6E;AAC7E,iFAAiF;AACjF,yBAAyB;AACzB,MAAM,UAAU,eAAe,CAAC,IAA4B;IAC1D,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC;IAC7D,IAAI,CAAC;QACH,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YACjC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/B,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC;YACpB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,EAAE,OAAO;YACZ,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB,CAAC,CAAC;QAEH,MAAM,KAAK,GAAiB,EAAE,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;YAC1D,0EAA0E;YAC1E,yEAAyE;YACzE,mDAAmD;YACnD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,OAAO;gBAAE,SAAS;YAClD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAChC,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;IAC5C,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACH,CAAC"}
|
package/dist/sync/run.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { isRedlineError } from "../core/errors.js";
|
|
2
|
+
import { planSync } from "./plan.js";
|
|
3
|
+
import { managedPaths, renderForTarget } from "./render.js";
|
|
4
|
+
export const SYNC_BRANCH = 'redline/sync';
|
|
5
|
+
export const SYNC_LABEL = 'redline-sync';
|
|
6
|
+
// How long a sync pull request may sit before its own gate starts failing it.
|
|
7
|
+
// A sync pull request nobody merges is drift, and drift that fails nothing is
|
|
8
|
+
// drift nobody sees. Thirty days is long enough for a team to schedule the merge
|
|
9
|
+
// and short enough that "we'll get to it" has to be said out loud again.
|
|
10
|
+
export const SYNC_EXEMPTION_DAYS = 30;
|
|
11
|
+
const body = (target, version, files, now) => {
|
|
12
|
+
const until = new Date(now.getTime() + SYNC_EXEMPTION_DAYS * 86400000)
|
|
13
|
+
.toISOString()
|
|
14
|
+
.slice(0, 10);
|
|
15
|
+
return [
|
|
16
|
+
`Redline standards **v${version}** — this repository was on v${target.from}.`,
|
|
17
|
+
'',
|
|
18
|
+
'Rendered from the organisation standard. Everything in this diff is generated:',
|
|
19
|
+
'',
|
|
20
|
+
...files.map((f) => `- \`${f}\``),
|
|
21
|
+
'',
|
|
22
|
+
'Content above each `<!-- REDLINE:BEGIN -->` marker is yours and is untouched.',
|
|
23
|
+
'',
|
|
24
|
+
'Merging keeps this repository current. Not merging is a decision the estate',
|
|
25
|
+
'dashboard records as drift — it is not a silent one, which is the point.',
|
|
26
|
+
'',
|
|
27
|
+
// A generated pull request carries its own exemption rather than being a
|
|
28
|
+
// special case in the gate. One rule for everyone is worth more than a
|
|
29
|
+
// convenience for the tool that wrote the rule — and it means this pull
|
|
30
|
+
// request starts failing its own gate if it is left unmerged, which is
|
|
31
|
+
// exactly what should happen to standards a repository is quietly refusing.
|
|
32
|
+
'## Redline exemption',
|
|
33
|
+
'',
|
|
34
|
+
`- reason: generated standards sync to v${version}; the launch-readiness checklist does not apply to a diff no human wrote`,
|
|
35
|
+
`- until: ${until}`,
|
|
36
|
+
'- scope: checklist, adr',
|
|
37
|
+
].join('\n');
|
|
38
|
+
};
|
|
39
|
+
async function syncTarget(host, target, opts) {
|
|
40
|
+
const entry = target.entry;
|
|
41
|
+
const ref = {
|
|
42
|
+
host: entry.host,
|
|
43
|
+
org: entry.org,
|
|
44
|
+
repo: entry.repo,
|
|
45
|
+
defaultBranch: entry.defaultBranch,
|
|
46
|
+
...(entry.project ? { project: entry.project } : {}),
|
|
47
|
+
};
|
|
48
|
+
// The register records a profile, but vendors and the current selections live
|
|
49
|
+
// in the repository's own file. Reading it live means sync cannot render the
|
|
50
|
+
// wrong thing for a repository that changed since the register was derived.
|
|
51
|
+
const { config } = await host.readRemoteConfig(ref);
|
|
52
|
+
if (!config)
|
|
53
|
+
return { kind: 'not-onboarded' };
|
|
54
|
+
const paths = managedPaths(opts.root, config.profile, config.vendors);
|
|
55
|
+
const existing = new Map();
|
|
56
|
+
for (const path of paths) {
|
|
57
|
+
const file = await host.readRemoteFile(ref, path);
|
|
58
|
+
if (file)
|
|
59
|
+
existing.set(path, file.content);
|
|
60
|
+
}
|
|
61
|
+
const rendered = renderForTarget({
|
|
62
|
+
root: opts.root,
|
|
63
|
+
profile: config.profile,
|
|
64
|
+
vendors: config.vendors,
|
|
65
|
+
existing,
|
|
66
|
+
});
|
|
67
|
+
// A render that would delete a file is reported and not pushed. Removal is a
|
|
68
|
+
// vendor being turned off or a stack leaving the profile — a decision the
|
|
69
|
+
// repository makes by re-running init, not something a scheduled sync should
|
|
70
|
+
// do to it from the outside. But it must be SAID: a target carrying artifacts
|
|
71
|
+
// for a stack it no longer has was reported as "current" by both sync and
|
|
72
|
+
// verify, which is the definition of silent drift.
|
|
73
|
+
if (rendered.files.length === 0) {
|
|
74
|
+
return rendered.removed.length > 0
|
|
75
|
+
? { kind: 'stale-artifacts', paths: rendered.removed }
|
|
76
|
+
: { kind: 'current' };
|
|
77
|
+
}
|
|
78
|
+
if (opts.dryRun) {
|
|
79
|
+
return { kind: 'opened', number: 0, url: `(dry run) ${rendered.files.length} file(s)` };
|
|
80
|
+
}
|
|
81
|
+
const pushed = await host.pushFiles(ref, {
|
|
82
|
+
branch: SYNC_BRANCH,
|
|
83
|
+
message: `chore(redline): standards v${opts.standardsVersion}`,
|
|
84
|
+
files: rendered.files,
|
|
85
|
+
});
|
|
86
|
+
if (pushed.commit === null)
|
|
87
|
+
return { kind: 'current' };
|
|
88
|
+
const pr = await host.openPullRequest(ref, {
|
|
89
|
+
branch: SYNC_BRANCH,
|
|
90
|
+
title: `Redline: standards v${opts.standardsVersion}`,
|
|
91
|
+
body: body(target, opts.standardsVersion, rendered.files.map((f) => f.path), opts.now ?? new Date()),
|
|
92
|
+
labels: [SYNC_LABEL],
|
|
93
|
+
});
|
|
94
|
+
return pr.created
|
|
95
|
+
? { kind: 'opened', number: pr.number, url: pr.url }
|
|
96
|
+
: { kind: 'updated', number: pr.number, url: pr.url };
|
|
97
|
+
}
|
|
98
|
+
// Sync the estate. One repository failing never stops the others: an estate-wide
|
|
99
|
+
// distribution that aborts on the first unreachable repository is a distribution
|
|
100
|
+
// that never completes, and the whole point of Phase 0 is that it does.
|
|
101
|
+
export async function runSync(host, registry, opts) {
|
|
102
|
+
const plan = planSync(registry, opts.standardsVersion, opts);
|
|
103
|
+
const results = [];
|
|
104
|
+
let failures = 0;
|
|
105
|
+
for (const target of plan.targets) {
|
|
106
|
+
const repo = `${target.entry.org}/${target.entry.repo}`;
|
|
107
|
+
try {
|
|
108
|
+
results.push({ repo, outcome: await syncTarget(host, target, opts) });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
failures += 1;
|
|
112
|
+
const detail = isRedlineError(error) ? error.message : String(error);
|
|
113
|
+
results.push({ repo, outcome: { kind: 'failed', detail } });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { plan, results, failures };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../cli/sync/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { RedlineError } from "../core/errors.js";
|
|
2
|
+
import { createGitHubClient } from "../platforms/github/client.js";
|
|
3
|
+
import { createGitHubVerify, machineryFromBody } from "../platforms/github/verify.js";
|
|
4
|
+
import { readRemoteConfig, readRemoteFile } from "../platforms/github/remote.js";
|
|
5
|
+
// One client per host, for the same reason sync needs one: a remote verify runs
|
|
6
|
+
// across an estate that spans both, where every other command acts on a single
|
|
7
|
+
// repository and therefore a single host.
|
|
8
|
+
export function createRemoteVerifyHost(clients = {}) {
|
|
9
|
+
const github = clients.github ?? createGitHubClient();
|
|
10
|
+
const githubVerify = createGitHubVerify(github);
|
|
11
|
+
const unsupported = (host) => {
|
|
12
|
+
throw new RedlineError('host', `remote verify does not support ${host} yet`, 'Azure DevOps remote verification is outstanding — see the roadmap Phase 0');
|
|
13
|
+
};
|
|
14
|
+
const only = (ref) => {
|
|
15
|
+
if (ref.host !== 'github')
|
|
16
|
+
unsupported(ref.host);
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
resolveRef: (repo) => resolveRemoteRef(github, repo),
|
|
20
|
+
readRemoteConfig: (ref) => (only(ref), readRemoteConfig(github, ref)),
|
|
21
|
+
readRemoteFile: (ref, path) => (only(ref), readRemoteFile(github, ref, path)),
|
|
22
|
+
machineryFromBody,
|
|
23
|
+
readPolicy: (ref) => (only(ref), githubVerify.readPolicy(ref)),
|
|
24
|
+
readSecurityState: (ref) => (only(ref), githubVerify.readSecurityState(ref)),
|
|
25
|
+
latestPullRequestNumber: (ref) => (only(ref), githubVerify.latestPullRequestNumber(ref)),
|
|
26
|
+
readReportedCheckNames: (ref, pr) => (only(ref), githubVerify.readReportedCheckNames(ref, pr)),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// owner/name into a RepoRef. The default branch is not known until the host is
|
|
30
|
+
// asked, and every call below needs it, so it is resolved once here rather than
|
|
31
|
+
// guessed as "main" — a repository on `master` or `develop` would otherwise be
|
|
32
|
+
// reported as entirely broken.
|
|
33
|
+
export async function resolveRemoteRef(client, repo) {
|
|
34
|
+
const [org, name] = repo.split('/');
|
|
35
|
+
if (!org || !name || repo.split('/').length !== 2) {
|
|
36
|
+
throw new RedlineError('usage', `--repo must be owner/name, not "${repo}"`);
|
|
37
|
+
}
|
|
38
|
+
const response = await client.rest('GET', `/repos/${org}/${name}`);
|
|
39
|
+
if (response.status === 404) {
|
|
40
|
+
throw new RedlineError('host', `${repo} not found, or this token cannot see it`);
|
|
41
|
+
}
|
|
42
|
+
if (response.status >= 400) {
|
|
43
|
+
throw new RedlineError('host', `reading ${repo} returned ${response.status}`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
host: 'github',
|
|
47
|
+
org,
|
|
48
|
+
repo: name,
|
|
49
|
+
defaultBranch: response.body?.default_branch ?? 'main',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host.js","sourceRoot":"","sources":["../../cli/verify/host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { isRedlineError } from "../core/errors.js";
|
|
2
|
+
import { renderForTarget, managedPaths } from "../sync/render.js";
|
|
3
|
+
export const CALLER_WORKFLOW = '.github/workflows/redline.yml';
|
|
4
|
+
// Verify a repository without a checkout.
|
|
5
|
+
//
|
|
6
|
+
// Every check that can be sourced from the host is, and every check that cannot
|
|
7
|
+
// is reported `unknown` with the reason rather than passing. That distinction is
|
|
8
|
+
// the reason this is safe to run across an estate on a schedule: a check
|
|
9
|
+
// reported as passing when it never executed produces a false all-clear on every
|
|
10
|
+
// repository at once, which is materially worse than not checking.
|
|
11
|
+
export async function verifyRemote(host, ref, opts) {
|
|
12
|
+
const findings = [];
|
|
13
|
+
const add = (check, ok, detail, unknown = false) => {
|
|
14
|
+
findings.push({ check, ok, detail, ...(unknown ? { unknown: true } : {}) });
|
|
15
|
+
};
|
|
16
|
+
const repo = `${ref.org}/${ref.repo}`;
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
({ config } = await host.readRemoteConfig(ref));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// A malformed .redline.json in the estate is that repository's finding, not
|
|
23
|
+
// a failure of the run that found it — otherwise one hand-edited file stops
|
|
24
|
+
// the weekly sweep for everybody.
|
|
25
|
+
add('onboarded', false, isRedlineError(error) ? error.message : String(error));
|
|
26
|
+
return { findings, ok: false };
|
|
27
|
+
}
|
|
28
|
+
if (!config) {
|
|
29
|
+
add('onboarded', false, `${repo} carries no .redline.json — it is not onboarded`);
|
|
30
|
+
return { findings, ok: false };
|
|
31
|
+
}
|
|
32
|
+
add('onboarded', true, `profile ${config.profile}, standards v${config.standardsVersion}`);
|
|
33
|
+
// --- rendered artifacts ----------------------------------------------------
|
|
34
|
+
// Staleness is decidable remotely and is worth deciding: it is the check that
|
|
35
|
+
// tells an ignored sync pull request from a merged one.
|
|
36
|
+
try {
|
|
37
|
+
const existing = new Map();
|
|
38
|
+
for (const path of managedPaths(opts.root, config.profile, config.vendors)) {
|
|
39
|
+
const file = await host.readRemoteFile(ref, path);
|
|
40
|
+
if (file)
|
|
41
|
+
existing.set(path, file.content);
|
|
42
|
+
}
|
|
43
|
+
const rendered = renderForTarget({
|
|
44
|
+
root: opts.root,
|
|
45
|
+
profile: config.profile,
|
|
46
|
+
vendors: config.vendors,
|
|
47
|
+
existing,
|
|
48
|
+
});
|
|
49
|
+
add('artifacts', rendered.files.length === 0, rendered.files.length === 0
|
|
50
|
+
? `current against standards v${opts.standardsVersion}`
|
|
51
|
+
: `stale against standards v${opts.standardsVersion}: ${rendered.files
|
|
52
|
+
.map((f) => f.path)
|
|
53
|
+
.join(', ')} — a sync pull request is open or was closed unmerged`);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
add('artifacts', true, `could not render for comparison: ${isRedlineError(error) ? error.message : String(error)}`, true);
|
|
57
|
+
}
|
|
58
|
+
// --- gate machinery --------------------------------------------------------
|
|
59
|
+
const callerFile = await host.readRemoteFile(ref, CALLER_WORKFLOW);
|
|
60
|
+
const machinery = host.machineryFromBody(callerFile?.content ?? null);
|
|
61
|
+
const gateOwned = config.capabilities.gate;
|
|
62
|
+
add('gate-machinery', !gateOwned || (machinery.present && machinery.publishes === machinery.expected), !gateOwned
|
|
63
|
+
? machinery.present
|
|
64
|
+
? `off by choice, and ${machinery.path} is still present and still publishing ${machinery.publishes ?? 'nothing'}`
|
|
65
|
+
: 'off by choice — this repository publishes the gate its own way'
|
|
66
|
+
: !machinery.present
|
|
67
|
+
? `${machinery.path} is missing — nothing here can publish ${machinery.expected}`
|
|
68
|
+
: machinery.publishes === null
|
|
69
|
+
? `${machinery.path} is present but no longer publishes a check name — its job id or pull_request trigger was edited`
|
|
70
|
+
: machinery.publishes !== machinery.expected
|
|
71
|
+
? `${machinery.path} publishes ${machinery.publishes}, but the policy requires ${machinery.expected}`
|
|
72
|
+
: `publishes ${machinery.publishes}`);
|
|
73
|
+
// --- merge policy ----------------------------------------------------------
|
|
74
|
+
const policy = await host.readPolicy(ref);
|
|
75
|
+
if (!config.capabilities.mergePolicy) {
|
|
76
|
+
add('merge-policy', true, 'off by choice — this repository maintains its own');
|
|
77
|
+
}
|
|
78
|
+
else if (policy === null) {
|
|
79
|
+
add('merge-policy', false, 'no merge policy found on the host — one was applied at onboarding');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const weakened = [];
|
|
83
|
+
if (policy.requiredApprovals < 1) {
|
|
84
|
+
weakened.push('human approval is no longer required — automated review never approves');
|
|
85
|
+
}
|
|
86
|
+
if (policy.notEnforcedReason) {
|
|
87
|
+
// The cheapest loosening on GitHub: a ruleset switched out of active
|
|
88
|
+
// enforcement keeps every field readable while none of its rules apply.
|
|
89
|
+
weakened.push(`the policy exists but is not in force (${policy.notEnforcedReason})`);
|
|
90
|
+
}
|
|
91
|
+
if (policy.blocking && !machinery.present) {
|
|
92
|
+
weakened.push(`blocking on ${machinery.expected}, which nothing in this repository publishes — ` +
|
|
93
|
+
'every pull request here is blocked until the gate is restored or the policy relaxed');
|
|
94
|
+
}
|
|
95
|
+
add('merge-policy', weakened.length === 0, weakened.length === 0
|
|
96
|
+
? `${policy.blocking ? 'blocking' : 'advisory'}, ${policy.requiredApprovals} approval(s) required`
|
|
97
|
+
: weakened.join('; '));
|
|
98
|
+
}
|
|
99
|
+
// --- security floor --------------------------------------------------------
|
|
100
|
+
const security = await host.readSecurityState(ref);
|
|
101
|
+
// An indeterminate read is not an answer. A 403 the host cannot tell from a
|
|
102
|
+
// genuine refusal must not be reported as a security floor that is off — that
|
|
103
|
+
// files false work against an administrator — nor as one that is on.
|
|
104
|
+
const unreadable = security.outcomes.filter((o) => o.status === 'unknown');
|
|
105
|
+
const off = security.outcomes.filter((o) => o.status === 'denied');
|
|
106
|
+
if (off.length > 0) {
|
|
107
|
+
add('security-floor', false, `not enabled: ${off.map((o) => o.capability).join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
else if (unreadable.length > 0 && unreadable.length === security.outcomes.length) {
|
|
110
|
+
add('security-floor', true, `this token cannot read: ${unreadable.map((o) => o.capability).join(', ')}`, true);
|
|
111
|
+
}
|
|
112
|
+
else if (unreadable.length > 0) {
|
|
113
|
+
// Partly unreadable is still an indeterminate answer about the part that
|
|
114
|
+
// could not be read. Reporting it plainly `ok` meant a repository with
|
|
115
|
+
// secret scanning genuinely off, plus a token unable to see it, verified as
|
|
116
|
+
// healthy — the one combination this check exists to catch.
|
|
117
|
+
add('security-floor', true, `enabled, except ${unreadable.map((o) => o.capability).join(', ')} which this token cannot read`, true);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
add('security-floor', true, 'the security floor is enabled');
|
|
121
|
+
}
|
|
122
|
+
// --- the check actually reporting ------------------------------------------
|
|
123
|
+
const pr = await host.latestPullRequestNumber(ref);
|
|
124
|
+
if (pr === null) {
|
|
125
|
+
add('check-name-reported', true, 'no pull request yet — open one to confirm the check reports');
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const reported = await host.readReportedCheckNames(ref, pr);
|
|
129
|
+
const found = reported.includes(machinery.expected);
|
|
130
|
+
add('check-name-reported', !gateOwned || found, !gateOwned
|
|
131
|
+
? 'off by choice — Redline does not require a check name here'
|
|
132
|
+
: found
|
|
133
|
+
? `${machinery.expected} reported on PR #${pr}`
|
|
134
|
+
: `${machinery.expected} was not reported on PR #${pr} — reported: ${reported.join(', ') || 'nothing'}`);
|
|
135
|
+
}
|
|
136
|
+
// --- pending admin ---------------------------------------------------------
|
|
137
|
+
add('pending-admin', config.pendingAdmin.length === 0, config.pendingAdmin.length === 0
|
|
138
|
+
? 'nothing waiting on an administrator'
|
|
139
|
+
: `an administrator must still enable: ${config.pendingAdmin.join(', ')}`);
|
|
140
|
+
// An unknown never fails the report. It is not evidence of drift — it is the
|
|
141
|
+
// absence of evidence, and failing on it would train an operator to ignore
|
|
142
|
+
// the weekly issue, which costs more than the check is worth.
|
|
143
|
+
const ok = findings.every((f) => f.ok || f.unknown === true);
|
|
144
|
+
return { findings, ok };
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote.js","sourceRoot":"","sources":["../../cli/verify/remote.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "redlinegate",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Engineering control plane for AI-assisted development: standards, merge gates and evidence across GitHub and Azure DevOps",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://redline-gate.vercel.app",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"code-review",
|
|
10
|
+
"ai-code-review",
|
|
11
|
+
"engineering-standards",
|
|
12
|
+
"code-quality",
|
|
13
|
+
"merge-gate",
|
|
14
|
+
"governance",
|
|
15
|
+
"sarif",
|
|
16
|
+
"static-analysis",
|
|
17
|
+
"github-actions",
|
|
18
|
+
"azure-devops",
|
|
19
|
+
"copilot",
|
|
20
|
+
"claude"
|
|
21
|
+
],
|
|
22
|
+
"private": false,
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"redline": "dist/bin/redline.js",
|
|
28
|
+
"redlinegate": "dist/bin/redline.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"dist/",
|
|
33
|
+
"scripts/",
|
|
34
|
+
"standards/",
|
|
35
|
+
"commands/",
|
|
36
|
+
"templates/",
|
|
37
|
+
"platforms/",
|
|
38
|
+
"workflows/",
|
|
39
|
+
"rulesets/"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
43
|
+
"prepack": "npm run build",
|
|
44
|
+
"test": "node --test 'cli/**/__tests__/*.test.ts' 'scripts/**/__tests__/*.test.mjs'",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.0",
|
|
49
|
+
"semantic-release": "25.0.9",
|
|
50
|
+
"typescript": "^5.7.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Managed by Redline; regenerate with `redline init` rather than editing here.
|
|
2
|
+
# Redline merge gate for Azure DevOps.
|
|
3
|
+
#
|
|
4
|
+
# CONTRACT: the final step publishes a pull-request status with genre `redline`
|
|
5
|
+
# and name `gate`. The Status branch policy installed by `redline init` requires exactly
|
|
6
|
+
# that status. Renaming either value makes the policy unsatisfiable and every pull
|
|
7
|
+
# request in this repository will sit blocked. `redline verify` checks the live
|
|
8
|
+
# status name against this contract.
|
|
9
|
+
#
|
|
10
|
+
# There is deliberately no `pr:` block here: Azure Repos ignores YAML `pr:`
|
|
11
|
+
# triggers (they only fire for GitHub-hosted repositories). This pipeline is
|
|
12
|
+
# queued on pull requests by the "Redline: gate build" Build Validation branch
|
|
13
|
+
# policy that `redline init` creates against the registered `redline-gate`
|
|
14
|
+
# build definition.
|
|
15
|
+
|
|
16
|
+
trigger: none
|
|
17
|
+
|
|
18
|
+
pool:
|
|
19
|
+
vmImage: ubuntu-latest
|
|
20
|
+
|
|
21
|
+
variables:
|
|
22
|
+
ADR_DIFF_THRESHOLD: 300
|
|
23
|
+
FAIL_ON_DEPENDENCY_SEVERITY: high
|
|
24
|
+
SOFT_FAIL_LABELS: redline-exempt,redline-sync
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- checkout: self
|
|
28
|
+
fetchDepth: 0
|
|
29
|
+
|
|
30
|
+
- task: NodeTool@0
|
|
31
|
+
displayName: Node 22
|
|
32
|
+
inputs:
|
|
33
|
+
versionSpec: '22.x'
|
|
34
|
+
|
|
35
|
+
# --package pins npx to the redlinegate package explicitly. `redline` alone
|
|
36
|
+
# names a different, unrelated package on the public registry — `redline`
|
|
37
|
+
# is only this package's bin name, never resolve npx against it directly.
|
|
38
|
+
- script: npx --yes --package=redlinegate@latest redline verify --gate
|
|
39
|
+
displayName: Redline gate
|
|
40
|
+
name: gate
|
|
41
|
+
env:
|
|
42
|
+
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
|
43
|
+
ADR_DIFF_THRESHOLD: $(ADR_DIFF_THRESHOLD)
|
|
44
|
+
FAIL_ON_DEPENDENCY_SEVERITY: $(FAIL_ON_DEPENDENCY_SEVERITY)
|
|
45
|
+
SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
|
|
46
|
+
|
|
47
|
+
- script: |
|
|
48
|
+
set -euo pipefail
|
|
49
|
+
state="succeeded"
|
|
50
|
+
description="Redline merge gate"
|
|
51
|
+
if [ "$AGENT_JOBSTATUS" != "Succeeded" ]; then state="failed"; fi
|
|
52
|
+
|
|
53
|
+
# $SYSTEM_TEAMPROJECT is admin-set, not attacker-controlled, but a
|
|
54
|
+
# display name containing a space or a quote must not be allowed to
|
|
55
|
+
# break the JSON body or the URL path — encode it, never interpolate
|
|
56
|
+
# it raw into either.
|
|
57
|
+
project_enc=$(jq -rn --arg v "$SYSTEM_TEAMPROJECT" '$v | @uri')
|
|
58
|
+
target_url="${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${project_enc}/_build/results?buildId=$BUILD_BUILDID"
|
|
59
|
+
pr_url="${SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${project_enc}/_apis/git/repositories/$BUILD_REPOSITORY_ID/pullRequests/$SYSTEM_PULLREQUEST_PULLREQUESTID"
|
|
60
|
+
|
|
61
|
+
# The access token must never appear in argv — visible via `ps` or
|
|
62
|
+
# /proc/<pid>/cmdline to any process on the agent for the call's
|
|
63
|
+
# duration. It is already an env var; hand it to curl through a
|
|
64
|
+
# stdin config instead of putting it on the command line with -H.
|
|
65
|
+
curl_authed() {
|
|
66
|
+
printf 'header = "Authorization: Bearer %s"\n' "$SYSTEM_ACCESSTOKEN" | curl -K - "$@"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Soft-fail escape hatch, the same one workflows/redline-gate.yml gives
|
|
70
|
+
# GitHub: a pull request carrying one of SOFT_FAIL_LABELS reports the
|
|
71
|
+
# gate succeeded-with-warning instead of blocking, so a reviewer can
|
|
72
|
+
# accept a process failure deliberately. It can only ever turn a
|
|
73
|
+
# failure into a success, never the reverse.
|
|
74
|
+
#
|
|
75
|
+
# NOTE for whoever splits this job up: on GitHub the security jobs
|
|
76
|
+
# (dependency-review, secrets) are excluded from the exemption. Here the
|
|
77
|
+
# gate is one `redline verify --gate` run that checks configuration
|
|
78
|
+
# drift only — no scanning step exists to downgrade. Adding one means
|
|
79
|
+
# excluding it from this branch, exactly as the GitHub workflow does.
|
|
80
|
+
if [ "$state" = "failed" ] && [ -n "${SOFT_FAIL_LABELS:-}" ]; then
|
|
81
|
+
labels=$(curl_authed -sS "$pr_url/labels?api-version=7.1") || labels=''
|
|
82
|
+
for label in ${SOFT_FAIL_LABELS//,/ }; do
|
|
83
|
+
if printf '%s' "$labels" \
|
|
84
|
+
| jq -e --arg l "$label" '[.value[]? | select(.active != false) | .name] | index($l)' >/dev/null 2>&1
|
|
85
|
+
then
|
|
86
|
+
state="succeeded"
|
|
87
|
+
description="Redline gate failed but the \"$label\" label is applied — a reviewer accepted this deliberately"
|
|
88
|
+
echo "##vso[task.logissue type=warning]$description"
|
|
89
|
+
break
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
body=$(jq -n --arg state "$state" --arg url "$target_url" --arg description "$description" \
|
|
95
|
+
'{state: $state, description: $description, context: {name: "gate", genre: "redline"}, targetUrl: $url}')
|
|
96
|
+
|
|
97
|
+
curl_authed -sS --fail-with-body -X POST \
|
|
98
|
+
-H "Content-Type: application/json" \
|
|
99
|
+
-d "$body" \
|
|
100
|
+
"$pr_url/statuses?api-version=7.1"
|
|
101
|
+
displayName: Publish redline/gate status
|
|
102
|
+
condition: always()
|
|
103
|
+
env:
|
|
104
|
+
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
|
105
|
+
SOFT_FAIL_LABELS: $(SOFT_FAIL_LABELS)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Redline (org)",
|
|
3
|
+
"target": "branch",
|
|
4
|
+
"enforcement": "active",
|
|
5
|
+
"conditions": {
|
|
6
|
+
"ref_name": {
|
|
7
|
+
"include": ["~DEFAULT_BRANCH"],
|
|
8
|
+
"exclude": []
|
|
9
|
+
},
|
|
10
|
+
"repository_property": {
|
|
11
|
+
"include": [
|
|
12
|
+
{
|
|
13
|
+
"name": "redline",
|
|
14
|
+
"property_values": ["onboarded"],
|
|
15
|
+
"source": "custom"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"exclude": []
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"rules": [
|
|
22
|
+
{ "type": "deletion" },
|
|
23
|
+
{ "type": "non_fast_forward" },
|
|
24
|
+
{
|
|
25
|
+
"type": "pull_request",
|
|
26
|
+
"parameters": {
|
|
27
|
+
"required_approving_review_count": 1,
|
|
28
|
+
"dismiss_stale_reviews_on_push": true,
|
|
29
|
+
"require_code_owner_review": true,
|
|
30
|
+
"require_last_push_approval": true,
|
|
31
|
+
"required_review_thread_resolution": true,
|
|
32
|
+
"automatic_copilot_code_review_enabled": true,
|
|
33
|
+
"allowed_merge_methods": ["squash", "merge"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "required_status_checks",
|
|
38
|
+
"parameters": {
|
|
39
|
+
"strict_required_status_checks_policy": false,
|
|
40
|
+
"do_not_enforce_on_create": false,
|
|
41
|
+
"required_status_checks": [{ "context": "redline-gate / gate" }]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"bypass_actors": []
|
|
46
|
+
}
|