@plainconceptsplatform/workflows 1.20.2 → 1.21.0

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.
@@ -34,6 +34,12 @@ if [ "$current" != "$HEAD_SHA" ]; then
34
34
  out ""
35
35
  exit 0
36
36
  fi
37
+ reason=$(bash "$(dirname "${BASH_SOURCE[0]}")/side-checks.sh" "$REPO" "$HEAD_SHA")
38
+ if [ -n "$reason" ]; then
39
+ echo "::warning::Not landing #$PR: $reason."
40
+ out ""
41
+ exit 0
42
+ fi
37
43
  base=$(jq -r .baseRefName <<<"$pr_json")
38
44
  head_ref=$(jq -r .headRefName <<<"$pr_json")
39
45
 
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # Managed by @plainconceptsplatform/workflows. Source: loops/actions/land-pull-request/side-checks.sh. Update with workflows update --force; consumer edits may be overwritten.
3
+ #
4
+ # Prints why a commit may not land yet, or nothing when it may. A side workflow
5
+ # (SIDE_CHECK_WORKFLOWS, comma-separated, default "Agentics checks") runs only when its paths change,
6
+ # so no ruleset can require it, and the CI verdict the train and the gate read leaves it out: a
7
+ # release once merged into three apps with its workflow lint red. A side workflow that did not run
8
+ # on the commit is no objection; one that is running, failed or cannot be read is.
9
+ # Usage: side-checks.sh <owner/repo> <sha>
10
+ set -euo pipefail
11
+ repo="$1"
12
+ sha="$2"
13
+ IFS=',' read -ra names <<<"${SIDE_CHECK_WORKFLOWS-Agentics checks}"
14
+ [ "${#names[@]}" -gt 0 ] || exit 0
15
+
16
+ if ! runs=$(gh api "repos/$repo/actions/runs?head_sha=$sha&per_page=100" --jq '.workflow_runs' 2>/dev/null); then
17
+ echo "the workflow runs on ${sha:0:7} could not be read"
18
+ exit 0
19
+ fi
20
+
21
+ for name in "${names[@]}"; do
22
+ name=$(sed -E 's/^ +| +$//g' <<<"$name")
23
+ [ -n "$name" ] || continue
24
+ latest=$(jq -r --arg n "$name" '[.[] | select(.name == $n)] | sort_by(.created_at) | last // empty | "\(.status) \(.conclusion // "")"' <<<"$runs")
25
+ [ -n "$latest" ] || continue
26
+ status=${latest%% *}
27
+ conclusion=${latest#* }
28
+ if [ "$status" != completed ]; then
29
+ echo "$name is still running on ${sha:0:7}"
30
+ exit 0
31
+ fi
32
+ case "$conclusion" in
33
+ success | skipped | neutral) ;;
34
+ *) echo "$name ended ${conclusion:-without a conclusion} on ${sha:0:7}"; exit 0 ;;
35
+ esac
36
+ done
@@ -34,6 +34,12 @@ runs:
34
34
 
35
35
  HEAD_REF=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefName --jq '.headRefName')
36
36
 
37
+ reason=$(bash ./.github/actions/land-pull-request/side-checks.sh "$REPO" "$HEAD_SHA")
38
+ if [ -n "$reason" ]; then
39
+ echo "::warning::Not merging #$PR_NUM: $reason."
40
+ continue
41
+ fi
42
+
37
43
  gh pr merge "$PR_NUM" --repo "$REPO" --squash --match-head-commit "$HEAD_SHA"
38
44
 
39
45
  # Delete the branch, because nothing else ever has: four repositories had accumulated
@@ -520,6 +520,17 @@ for line in "${candidates[@]}"; do
520
520
  [ $((now_epoch - $(iso_epoch "$ci_created"))) -lt $((STUCK_HOURS * 3600)) ] || ci_stuck=true
521
521
  fi
522
522
 
523
+ # A green CI run is not the whole verdict: a side workflow (Agentics checks) on the same head may be
524
+ # red or still running, and the gate could then never land it. Held until that changes.
525
+ if [ "$ci_status" = completed ] && [ "$ci_conclusion" = success ] && [ "$park" = none ]; then
526
+ side=$(bash "$(dirname "${BASH_SOURCE[0]}")/../land-pull-request/side-checks.sh" "$REPO" "$head_sha")
527
+ if [ -n "$side" ]; then
528
+ echo "#$pr ($author): hold, $side."
529
+ summary "| #$pr | $author | hold | $side |"
530
+ continue
531
+ fi
532
+ fi
533
+
523
534
  mergeable=UNKNOWN
524
535
  behind_by=0
525
536
  if [ "$park" = none ] && [ "$hold" != true ] && [ "$review_decision" != CHANGES_REQUESTED ]; then
@@ -3708,6 +3708,31 @@ if [ -f "$RTP_YML" ] && worker_installed release; then
3708
3708
  if [ "$RTP_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
3709
3709
  fi
3710
3710
 
3711
+ echo "── Required checks and side workflows ────────────────────────────────────"
3712
+
3713
+ # The main ruleset requires one check, Full CI, so Full CI must need every blocking job, and the
3714
+ # side workflows no ruleset can require (Agentics checks runs only on its paths) must be checked
3715
+ # by both merge points and held by the train. Dropping any one of these lets a red job merge.
3716
+ RC_OK=1
3717
+ rc() { grep -qE "$1" "$2" || { RC_OK=0; echo "FAIL: required checks: $3" >&2; }; }
3718
+ CI_DOTNET="${HERE}/../../templates/ci/app-ci-dotnet-next.yml"
3719
+ CI_NODE="${HERE}/../../templates/ci/app-ci-node-monorepo.yml"
3720
+ RULESET="${HERE}/../../templates/rulesets/main-required-checks.json"
3721
+ for job in sast-scan secret-scan deps-and-iac-scan platform-conformance engineering-excellence api web; do
3722
+ awk '/^ full-ci:$/{f=1} f&&/needs:/{print; exit}' "$CI_DOTNET" | grep -qE "[[ ]${job}[],]" || { RC_OK=0; echo "FAIL: required checks: the .NET template's Full CI does not need ${job}" >&2; }
3723
+ done
3724
+ for job in lint sast-scan secret-scan deps-and-iac-scan web; do
3725
+ awk '/^ full-ci:$/{f=1} f&&/needs:/{print; exit}' "$CI_NODE" | grep -qE "[[ ]${job}[],]" || { RC_OK=0; echo "FAIL: required checks: the node template's Full CI does not need ${job}" >&2; }
3726
+ done
3727
+ rc '"context": "Full CI"' "$RULESET" 'the ruleset must require Full CI'
3728
+ rc '"include": \["~DEFAULT_BRANCH"\]' "$RULESET" 'the ruleset must target the default branch'
3729
+ rc 'side-checks\.sh' "${HERE}/../land-pull-request/land-pull-request.sh" 'land-pull-request must check the side workflows'
3730
+ rc 'side-checks\.sh' "${HERE}/../merge-agent-pr/action.yml" 'merge-agent-pr must check the side workflows'
3731
+ rc 'land-pull-request/side-checks\.sh' "${HERE}/../merge-train/merge-train.sh" 'the merge train must hold a pull request whose side workflow is red'
3732
+ rc 'test-timings\.mjs' "$CI_DOTNET" 'the .NET template must summarise the test timings'
3733
+ [ "$(grep -c -- '--logger trx' "$CI_DOTNET")" -ge 5 ] || { RC_OK=0; echo "FAIL: required checks: every .NET test step must write a trx report" >&2; }
3734
+ if [ "$RC_OK" -eq 1 ]; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1)); fi
3735
+
3711
3736
  echo "── App error privacy ─────────────────────────────────────────────────────"
3712
3737
 
3713
3738
  # The application error report files into the private repository rather than out of it, so
@@ -214,19 +214,19 @@ jobs:
214
214
  - name: Unit tests + coverage
215
215
  if: env.UNIT_TEST_PROJECT != ''
216
216
  run: dotnet test "$UNIT_TEST_PROJECT" --no-build -c Release
217
- --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults
217
+ --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults --logger trx
218
218
  - name: Parity tests
219
219
  if: env.PARITY_TEST_PROJECT != ''
220
- run: dotnet test "$PARITY_TEST_PROJECT" --no-build -c Release
220
+ run: dotnet test "$PARITY_TEST_PROJECT" --no-build -c Release --results-directory TestResults --logger trx
221
221
  - name: Architecture tests
222
222
  if: env.ARCHITECTURE_TEST_PROJECT != ''
223
- run: dotnet test "$ARCHITECTURE_TEST_PROJECT" --no-build -c Release
223
+ run: dotnet test "$ARCHITECTURE_TEST_PROJECT" --no-build -c Release --results-directory TestResults --logger trx
224
224
  # Executable OpenSpec scenarios (Reqnroll). Each scenario is bound to a spec scenario by tag
225
225
  # and fails if that scenario disappears from openspec/.
226
226
  - name: Acceptance tests (Reqnroll) + coverage
227
227
  if: env.ACCEPTANCE_TEST_PROJECT != ''
228
228
  run: dotnet test "$ACCEPTANCE_TEST_PROJECT" --no-build -c Release
229
- --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults
229
+ --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults --logger trx
230
230
  # APP_SQL_SERVER is the connection string without a database; each later step names its own.
231
231
  - name: Create test databases
232
232
  env:
@@ -259,11 +259,16 @@ jobs:
259
259
  export "$INTEGRATION_CONNECTION_ENV=$APP_SQL_SERVER;Database=$PRIMARY_DATABASE"
260
260
  for pair in $EXTRA_DATABASES; do export "${pair%%=*}=$APP_SQL_SERVER;Database=${pair#*=}"; done
261
261
  dotnet test "$INTEGRATION_TEST_PROJECT" --no-build -c Release \
262
- --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults
262
+ --collect:"XPlat Code Coverage" --settings "$COVERAGE_SETTINGS" --results-directory TestResults --logger trx
263
263
  # The coverage step runs a small node script; fresh runner VMs ship no node.
264
264
  - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
265
265
  with:
266
266
  node-version: ${{ env.NODE_VERSION }}
267
+ # Where the run's time went, by test class: a class is what xUnit runs in parallel and where a
268
+ # fixture's setup is paid, so a slow class shows up the day it is added. Never fails the job.
269
+ - name: Test timings
270
+ if: ${{ !cancelled() }}
271
+ run: node "$GITHUB_WORKSPACE/scripts/test-timings.mjs" TestResults >> "$GITHUB_STEP_SUMMARY" || true
267
272
  # pipefail: without it the step reports tee's status and passes however low coverage is.
268
273
  - name: Coverage floor (merged)
269
274
  run: |
@@ -854,20 +859,22 @@ jobs:
854
859
  retention-days: 90
855
860
 
856
861
  # The merge train reads this job by its name to tell a full run from a light one: do not rename it.
862
+ # It is also the check the main ruleset requires (loops/templates/rulesets), so it needs every
863
+ # blocking job, the scans as well as the heavy ones: a red scan must stop a merge too.
857
864
  full-ci:
858
865
  name: Full CI
859
- needs: [depth, api, web, e2e, engineering-excellence]
866
+ needs: [depth, api, web, e2e, engineering-excellence, markdown, repo-checks, platform-conformance, secret-scan, deps-and-iac-scan, sast-scan]
860
867
  if: ${{ always() && needs.depth.outputs.full == 'true' }}
861
868
  runs-on: ${{ vars.CI_RUNNER || 'agents-arc' }}
862
869
  timeout-minutes: 5
863
870
  steps:
864
- - name: Every heavy job passed
871
+ - name: Every blocking job passed
865
872
  env:
866
873
  RESULTS: ${{ join(needs.*.result, ' ') }}
867
874
  run: |
868
875
  for result in $RESULTS; do
869
876
  case "$result" in
870
- failure | cancelled | timed_out) echo "A heavy job ended $result."; exit 1 ;;
877
+ failure | cancelled | timed_out) echo "A blocking job ended $result."; exit 1 ;;
871
878
  esac
872
879
  done
873
880
  echo "Full CI passed."
@@ -209,21 +209,22 @@ jobs:
209
209
  # Same guard as above: a tag release needs contents: write, which this job lacks.
210
210
  upload-release-assets: false
211
211
 
212
- # The merge queue requires this check. On a pull request with MERGE_QUEUE on, the heavy jobs are
212
+ # The merge queue and the main ruleset (loops/templates/rulesets) require this check, so it needs
213
+ # every blocking job, the scans too. On a pull request with MERGE_QUEUE on, the heavy jobs are
213
214
  # skipped and the full CI runs on the merge group instead; skipped counts as passed.
214
215
  full-ci:
215
216
  name: Full CI
216
- needs: [web, e2e, desktop, mobile]
217
+ needs: [lint, web, e2e, desktop, mobile, secret-scan, deps-and-iac-scan, sast-scan]
217
218
  if: always()
218
219
  runs-on: agents-arc
219
220
  timeout-minutes: 5
220
221
  steps:
221
- - name: Every heavy job passed
222
+ - name: Every blocking job passed
222
223
  env:
223
224
  RESULTS: ${{ join(needs.*.result, ' ') }}
224
225
  run: |
225
226
  for result in $RESULTS; do
226
- case "$result" in failure | cancelled | timed_out) echo "A heavy job ended $result."; exit 1 ;; esac
227
+ case "$result" in failure | cancelled | timed_out) echo "A blocking job ended $result."; exit 1 ;; esac
227
228
  done
228
229
 
229
230
  # Hands this run's verdict to the router's merge train, which decides whether this pull request
@@ -0,0 +1,74 @@
1
+ // Managed by @plainconceptsplatform/workflows. Source: loops/templates/quality-gates/scripts/lib/test-timings.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
2
+
3
+ // Reads VSTest .trx reports and says where a test run's time goes: by test class, since a class is
4
+ // what xUnit runs in parallel and where a fixture's setup is paid once.
5
+
6
+ const attr = (tag, name) => tag.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1];
7
+
8
+ function seconds(duration) {
9
+ const [h, m, s] = (duration ?? '0:0:0').split(':');
10
+ return Number(h) * 3600 + Number(m) * 60 + Number(s);
11
+ }
12
+
13
+ export function parseTrx(xml) {
14
+ const classes = new Map();
15
+ for (const [, id, body] of xml.matchAll(/<UnitTest\b[^>]*\bid="([^"]+)"[^>]*>([\s\S]*?)<\/UnitTest>/g)) {
16
+ const method = body.match(/<TestMethod\b[^>]*>/)?.[0];
17
+ if (method) classes.set(id, attr(method, 'className'));
18
+ }
19
+ const results = [];
20
+ for (const [tag] of xml.matchAll(/<UnitTestResult\b[^>]*>/g)) {
21
+ const className = classes.get(attr(tag, 'testId')) ?? 'unknown';
22
+ results.push({
23
+ className: className.split('.').pop(),
24
+ name: (attr(tag, 'testName') ?? '').replace(`${className}.`, ''),
25
+ seconds: seconds(attr(tag, 'duration')),
26
+ start: Date.parse(attr(tag, 'startTime') ?? ''),
27
+ end: Date.parse(attr(tag, 'endTime') ?? ''),
28
+ outcome: attr(tag, 'outcome') ?? 'Unknown',
29
+ });
30
+ }
31
+ const times = xml.match(/<Times\b[^>]*>/)?.[0] ?? '';
32
+ const wall = (Date.parse(attr(times, 'finish') ?? '') - Date.parse(attr(times, 'start') ?? '')) / 1000;
33
+ const runName = xml.match(/<TestRun\b[^>]*>/)?.[0];
34
+ const storage = xml.match(/<UnitTest\b[^>]*\bstorage="([^"]+)"/)?.[1] ?? attr(runName ?? '', 'name') ?? 'tests';
35
+ return { name: storage.split(/[\\/]/).pop().replace(/\.dll$/i, ''), wall: Number.isFinite(wall) ? wall : 0, results };
36
+ }
37
+
38
+ export function summarize(run, top = 15) {
39
+ const byClass = new Map();
40
+ for (const r of run.results) {
41
+ const c = byClass.get(r.className) ?? { name: r.className, tests: 0, seconds: 0, first: null };
42
+ c.tests += 1;
43
+ c.seconds += r.seconds;
44
+ if (!c.first || r.start < c.first.start) c.first = r;
45
+ byClass.set(r.className, c);
46
+ }
47
+ const total = run.results.reduce((sum, r) => sum + r.seconds, 0);
48
+ const classes = [...byClass.values()].sort((a, b) => b.seconds - a.seconds);
49
+ return {
50
+ name: run.name,
51
+ tests: run.results.length,
52
+ wall: run.wall,
53
+ total,
54
+ concurrency: run.wall > 0 ? total / run.wall : 0,
55
+ classes: classes.slice(0, top).map(({ name, tests, seconds, first }) => ({ name, tests, seconds, firstTest: first?.seconds ?? 0 })),
56
+ slowest: [...run.results].sort((a, b) => b.seconds - a.seconds).slice(0, top).map(({ className, name, seconds }) => ({ className, name, seconds })),
57
+ };
58
+ }
59
+
60
+ const s = (n) => `${n.toFixed(1)} s`;
61
+
62
+ export function toMarkdown(summaries) {
63
+ const lines = ['### Test timings', ''];
64
+ for (const t of summaries) {
65
+ lines.push(`**${t.name}**: ${t.tests} tests, ${s(t.wall)} wall, ${s(t.total)} of test time, ${t.concurrency.toFixed(1)} running at once on average.`, '');
66
+ if (t.tests < 20) continue;
67
+ lines.push('| Class | Tests | Time | First test |', '|---|--:|--:|--:|');
68
+ for (const c of t.classes) lines.push(`| ${c.name} | ${c.tests} | ${s(c.seconds)} | ${s(c.firstTest)} |`);
69
+ lines.push('', '<details><summary>Slowest tests</summary>', '');
70
+ for (const r of t.slowest) lines.push(`- ${s(r.seconds)} ${r.className}: ${r.name.replace(/\|/g, '\\|')}`);
71
+ lines.push('', '</details>', '');
72
+ }
73
+ return lines.join('\n');
74
+ }
@@ -0,0 +1,46 @@
1
+ // Managed by @plainconceptsplatform/workflows. Source: loops/templates/quality-gates/scripts/lib/test-timings.test.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
2
+ // Run: node --test scripts/lib/test-timings.test.mjs
3
+ import assert from 'node:assert/strict';
4
+ import { test } from 'node:test';
5
+ import { parseTrx, summarize, toMarkdown } from './test-timings.mjs';
6
+
7
+ const unit = (id, cls, name) =>
8
+ `<UnitTest name="${name}" storage="/w/App.IntegrationTests.dll" id="${id}"><Execution id="e${id}" /><TestMethod codeBase="x" className="App.Tests.${cls}" name="${name}" /></UnitTest>`;
9
+ const result = (id, duration, start, end) =>
10
+ `<UnitTestResult executionId="e${id}" testId="${id}" testName="${id} name" duration="${duration}" startTime="${start}" endTime="${end}" outcome="Passed" />`;
11
+
12
+ const trx = `<?xml version="1.0"?><TestRun id="r" name="run">
13
+ <Times creation="2026-01-01T00:00:00Z" start="2026-01-01T00:00:00.000Z" finish="2026-01-01T00:00:10.000Z" />
14
+ <Results>
15
+ ${result('a', '00:00:06.0000000', '2026-01-01T00:00:00Z', '2026-01-01T00:00:06Z')}
16
+ ${result('b', '00:00:01.0000000', '2026-01-01T00:00:06Z', '2026-01-01T00:00:07Z')}
17
+ ${result('c', '00:00:08.0000000', '2026-01-01T00:00:01Z', '2026-01-01T00:00:09Z')}
18
+ </Results>
19
+ <TestDefinitions>${unit('a', 'OrdersTests', 'First')}${unit('b', 'OrdersTests', 'Second')}${unit('c', 'StockTests', 'Only')}</TestDefinitions>
20
+ </TestRun>`;
21
+
22
+ test('parseTrx_resultsAndDefinitions_joinsClassAndDurationAndWallTime', () => {
23
+ const run = parseTrx(trx);
24
+ assert.equal(run.name, 'App.IntegrationTests');
25
+ assert.equal(run.wall, 10);
26
+ assert.deepEqual(run.results.map((r) => [r.className, r.seconds]), [['OrdersTests', 6], ['OrdersTests', 1], ['StockTests', 8]]);
27
+ });
28
+
29
+ test('summarize_classesBySummedTime_reportsConcurrencyAndTheFirstTestCost', () => {
30
+ const t = summarize(parseTrx(trx));
31
+ assert.equal(t.total, 15);
32
+ assert.equal(t.concurrency, 1.5);
33
+ assert.deepEqual(t.classes.map((c) => [c.name, c.tests, c.seconds, c.firstTest]), [['StockTests', 1, 8, 8], ['OrdersTests', 2, 7, 6]]);
34
+ assert.equal(t.slowest[0].className, 'StockTests');
35
+ });
36
+
37
+ test('toMarkdown_smallRun_givesTheTotalsWithoutTables', () => {
38
+ const md = toMarkdown([summarize(parseTrx(trx))]);
39
+ assert.match(md, /\*\*App\.IntegrationTests\*\*: 3 tests, 10\.0 s wall, 15\.0 s of test time, 1\.5 running at once on average\./);
40
+ assert.doesNotMatch(md, /\| Class \|/);
41
+ });
42
+
43
+ test('parseTrx_fullyQualifiedTestName_dropsTheClassPrefix', () => {
44
+ const xml = `<TestRun><UnitTestResult testId="x" testName="App.Tests.OrdersTests.Saves(total: 1.5)" duration="00:00:01" outcome="Passed" />${unit('x', 'OrdersTests', 'Saves')}</TestRun>`;
45
+ assert.equal(parseTrx(xml).results[0].name, 'Saves(total: 1.5)');
46
+ });
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // Managed by @plainconceptsplatform/workflows. Source: loops/templates/quality-gates/scripts/test-timings.mjs. Update with `workflows update --force`; consumer edits may be overwritten.
3
+
4
+ // Usage: node scripts/test-timings.mjs <dir-with-trx> [--top 15]
5
+ // Prints where the test run's time went as Markdown, for the job summary. Never fails the job.
6
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { parseTrx, summarize, toMarkdown } from './lib/test-timings.mjs';
9
+
10
+ const [dir = 'TestResults', ...rest] = process.argv.slice(2);
11
+ const top = Number(rest[rest.indexOf('--top') + 1]) || 15;
12
+
13
+ function trxFiles(root) {
14
+ try {
15
+ return readdirSync(root).flatMap((entry) => {
16
+ const full = join(root, entry);
17
+ return statSync(full).isDirectory() ? trxFiles(full) : entry.endsWith('.trx') ? [full] : [];
18
+ });
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ const runs = trxFiles(dir).map((file) => summarize(parseTrx(readFileSync(file, 'utf8')), top)).filter((t) => t.tests > 0);
25
+ console.log(runs.length ? toMarkdown(runs.sort((a, b) => b.wall - a.wall)) : `### Test timings\n\nNo .trx reports under ${dir}.`);
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "Default branch: Full CI required",
3
+ "target": "branch",
4
+ "enforcement": "active",
5
+ "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
6
+ "rules": [
7
+ {
8
+ "type": "required_status_checks",
9
+ "parameters": {
10
+ "strict_required_status_checks_policy": false,
11
+ "do_not_enforce_on_create": false,
12
+ "required_status_checks": [{ "context": "Full CI" }]
13
+ }
14
+ }
15
+ ],
16
+ "bypass_actors": [{ "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always" }]
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/workflows",
3
- "version": "1.20.2",
3
+ "version": "1.21.0",
4
4
  "description": "Install and update Platform GitHub agentic workflows.",
5
5
  "keywords": [
6
6
  "github-actions",