Repository navigation
Expand file tree
/
Copy pathaction.yml
More file actions
761 lines (740 loc) · 34.4 KB
/
Copy pathaction.yml
File metadata and controls
761 lines (740 loc) · 34.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
name: Agents Shipgate
description: >-
The deterministic merge gate for AI-generated agent capability changes.
Review PR changes to declared coding-agent settings, MCP bindings, hooks
and workflow permissions without a manifest or saved baseline. Separately,
verify declared application-agent tool surfaces from MCP, OpenAPI,
OpenAI Agents SDK, Anthropic, Google ADK, LangChain, CrewAI, OpenAI API,
Codex config, Codex plugin, n8n, and Conductor OSS workflow JSON artifacts.
Host comparisons are
static and advisory, name coverage limits, and imply no verdict. Verification
artifacts and the configured release gate retain their own control rules.
Local-first and static-by-default. Apache-2.0.
author: ThreeMoonsLab
branding:
icon: shield
color: blue
inputs:
application:
required: false
default: "false"
description: "Opt in to manifest-free, advisory application comparison. Uses exact PR base/head SHAs; checkout must provide full history and objects. Verifier options do not apply."
application_scope:
required: false
default: ""
description: "Optional repository-relative application scope. Empty derives scope from the PR change."
application_fail_on:
required: false
default: ""
description: "Optional comma-separated application comparison statuses that fail CI: compared, partial, not_established. Empty is advisory. Execution refusals still fail."
config:
required: false
default: shipgate.yaml
description: Path to shipgate.yaml.
ci_mode:
required: false
default: advisory
description: advisory or strict.
fail_on:
required: false
default: ""
description: Optional comma-separated severities that fail CI, for example critical,high.
fail_on_merge_verdicts:
required: false
default: ""
description: "Optional comma-separated merge verdicts that fail CI. Supported values: blocked, human_review_required, insufficient_evidence, unknown, mergeable."
baseline:
required: false
default: ""
description: Optional local baseline JSON path. Strict mode fails only on new findings.
diff_from:
required: false
default: ""
description: Optional prior report.json or v0.4 baseline JSON for tool/action surface diff.
diff_base:
required: false
default: ""
description: Set to target to use the locally available pull request base branch for diff.
verify_mode:
required: false
default: verify
description: "Verifier mode for the Action. Supported values: verify (default) or scan for legacy direct-scan execution."
base_ref:
required: false
default: ""
description: Optional local base ref/SHA; application defaults to the PR base SHA. With diff_base=target on pull_request, defaults to origin/<github.base_ref>. The action never fetches.
head_ref:
required: false
default: ""
description: Optional local head ref/SHA for PR diff and scan. Omit to scan the checked-out workspace in verifier/scan mode. Application defaults to the PR head SHA.
baseline_mode:
required: false
default: new-findings
description: "Baseline comparison mode. Supported value: new-findings."
policy_packs:
required: false
default: ""
description: Optional comma- or newline-separated declarative YAML policy pack paths.
no_plugins:
required: false
default: "false"
description: Force third-party check plugins off even when AGENTS_SHIPGATE_ENABLE_PLUGINS is set.
output_dir:
required: false
default: agents-shipgate-reports
description: Output directory for reports. verify refuses (exit 2) a directory inside the repository that holds committed files or unignored files other than Shipgate artifacts, or that lies in a trust root Git does not ignore.
upload_artifact:
required: false
default: "true"
description: Upload reports as a workflow artifact.
check_annotations:
required: false
default: "true"
description: Emit GitHub Actions job annotations for source-backed blockers and review items.
check_annotation_limit:
required: false
default: "50"
description: Maximum number of GitHub Actions annotations to emit.
pr_comment:
required: false
default: "false"
description: Post a short PR comment when running on pull_request.
pr_comment_style:
required: false
default: capability-review
description: "PR comment renderer. Supported values: capability-review (default) or findings for the legacy v1 findings comment."
check_run:
required: false
default: "false"
description: >-
Create/update a GitHub Check Run named by check_run_name with the
merge verdict as the check conclusion (mergeable=success,
blocked=failure, otherwise neutral) and up to 50 line-level
annotations from the PR projection. Requires `checks: write`
permission on the workflow.
check_run_policy:
required: false
default: advisory
description: "Check Run conclusion policy: advisory, blocked-fails, or require-mergeable."
check_run_name:
required: false
default: "Agents Shipgate"
description: Name of the check run created when check_run is true.
github_token:
required: false
default: ""
description: Token for optional PR comments.
shipgate_version:
required: false
default: ""
description: Optional Agents Shipgate PyPI version to install. Empty installs this action source.
shipgate_wheel:
required: false
default: ""
description: Optional exact candidate wheel inside GITHUB_WORKSPACE. Requires shipgate_wheel_sha256; cannot be combined with shipgate_version. Install runtime dependencies separately from the reviewed lock.
shipgate_wheel_sha256:
required: false
default: ""
description: Full lowercase SHA-256 of shipgate_wheel. The Action checks and installs the same captured bytes.
attestation:
required: false
default: "false"
description: Emit agents-shipgate-reports/attestation.json from verifier.json/report.json.
registry_repo_label:
required: false
default: ""
description: Optional repository label copied into attestation org context.
org_bundle:
required: false
default: "false"
description: Emit agents-shipgate-reports/org-evidence-bundle.json for org/fleet aggregation.
host_audit:
required: false
default: "false"
description: Emit agents-shipgate-reports/host-grants.json from audit --host.
org_status:
required: false
default: "false"
description: Emit agents-shipgate-reports/org-status.json from org status.
registry_path:
required: false
default: ""
description: Optional registry path label embedded in org bundle metadata; never mutated by this action.
outputs:
# Legacy v0.7-caller output; baseline-blind. Prefer release_decision.decision
# via the `decision` output below for gating.
status:
description: Agents Shipgate summary status (legacy v0.7 caller; baseline-blind).
value: ${{ steps.report_outputs.outputs.status }}
application_status:
description: "Application comparison status: compared, partial, not_established, or refused. Not a merge verdict."
value: ${{ steps.scan.outputs.application_status }}
application_json:
description: Path to the application comparison JSON; empty on refusal.
value: ${{ steps.scan.outputs.application_json }}
application_markdown:
description: Path to the complete application review, including limits.
value: ${{ steps.scan.outputs.application_markdown }}
critical_count:
description: Number of unsuppressed critical findings.
value: ${{ steps.report_outputs.outputs.critical_count }}
high_count:
description: Number of unsuppressed high findings.
value: ${{ steps.report_outputs.outputs.high_count }}
medium_count:
description: Number of unsuppressed medium findings.
value: ${{ steps.report_outputs.outputs.medium_count }}
baseline_new_count:
description: Number of new findings when a baseline is used.
value: ${{ steps.report_outputs.outputs.baseline_new_count }}
baseline_matched_count:
description: Number of baseline-matched findings when a baseline is used.
value: ${{ steps.report_outputs.outputs.baseline_matched_count }}
baseline_resolved_count:
description: Number of baseline findings resolved by the current scan.
value: ${{ steps.report_outputs.outputs.baseline_resolved_count }}
adk_agent_count:
description: Number of Google ADK agents statically detected.
value: ${{ steps.report_outputs.outputs.adk_agent_count }}
adk_dynamic_toolset_count:
description: Number of Google ADK toolsets that were dynamic or unresolved.
value: ${{ steps.report_outputs.outputs.adk_dynamic_toolset_count }}
report_json:
description: Path to the generated JSON report.
value: ${{ steps.report_outputs.outputs.report_json }}
report_markdown:
description: Path to the generated Markdown report.
value: ${{ steps.report_outputs.outputs.report_markdown }}
report_sarif:
description: Path to the generated SARIF report.
value: ${{ steps.report_outputs.outputs.report_sarif }}
verifier_json:
description: Path to verifier.json.
value: ${{ steps.report_outputs.outputs.verifier_json }}
verify_run_json:
description: Path to verify-run.json.
value: ${{ steps.report_outputs.outputs.verify_run_json }}
agent_handoff_json:
description: Path to agent-handoff.json.
value: ${{ steps.report_outputs.outputs.agent_handoff_json }}
verification_receipt_json:
description: Path to the validated terminal verification receipt.
value: ${{ steps.report_outputs.outputs.verification_receipt_json }}
request_id:
description: Content-addressed verification request identity.
value: ${{ steps.report_outputs.outputs.request_id }}
receipt_id:
description: Content-addressed terminal receipt identity.
value: ${{ steps.report_outputs.outputs.receipt_id }}
decision_id:
description: Content-addressed assembled decision identity.
value: ${{ steps.report_outputs.outputs.decision_id }}
artifact_set_id:
description: Content-addressed artifact-set identity.
value: ${{ steps.report_outputs.outputs.artifact_set_id }}
run_id:
description: Deterministic verify-run input identity from verify-run.json.
value: ${{ steps.report_outputs.outputs.run_id }}
pr_comment_markdown:
description: Path to the generated PR comment Markdown.
value: ${{ steps.report_outputs.outputs.pr_comment_markdown }}
check_annotations_json:
description: Path to generated GitHub Actions annotation payload.
value: ${{ steps.report_outputs.outputs.check_annotations_json }}
capability_lock_json:
description: Path to generated head capability lock JSON.
value: ${{ steps.report_outputs.outputs.capability_lock_json }}
base_capability_lock_json:
description: Path to generated base capability lock JSON when base diff is available.
value: ${{ steps.report_outputs.outputs.base_capability_lock_json }}
capability_lock_diff_json:
description: Path to generated capability lock diff JSON when base and head locks are available.
value: ${{ steps.report_outputs.outputs.capability_lock_diff_json }}
attestation_json:
description: Path to generated release attestation JSON when attestation is true.
value: ${{ steps.report_outputs.outputs.attestation_json }}
org_evidence_bundle_json:
description: Path to generated organization evidence bundle JSON when org_bundle is true.
value: ${{ steps.report_outputs.outputs.org_evidence_bundle_json }}
host_grants_json:
description: Path to generated host-grants inventory JSON when host_audit is true.
value: ${{ steps.report_outputs.outputs.host_grants_json }}
org_status_json:
description: Path to generated organization status JSON when org_status is true.
value: ${{ steps.report_outputs.outputs.org_status_json }}
exit_code:
description: Agents Shipgate CLI exit code.
value: ${{ steps.scan.outputs.exit_code }}
decision:
description: Release decision (`blocked`, `review_required`, `insufficient_evidence`, or `passed`). v0.8+; `insufficient_evidence` added v0.14.
value: ${{ steps.report_outputs.outputs.decision }}
blocker_count:
description: Number of blockers in release_decision (v0.8+).
value: ${{ steps.report_outputs.outputs.blocker_count }}
review_item_count:
description: Number of review items in release_decision (v0.8+).
value: ${{ steps.report_outputs.outputs.review_item_count }}
ci_would_fail:
description: Whether the active fail policy would fail CI (`true`/`false`). v0.8+.
value: ${{ steps.report_outputs.outputs.ci_would_fail }}
diff_enabled:
description: Whether tool_surface_diff was enabled in report.json.
value: ${{ steps.report_outputs.outputs.diff_enabled }}
action_diff_enabled:
description: Whether action_surface_diff was enabled in report.json.
value: ${{ steps.report_outputs.outputs.action_diff_enabled }}
actions_added:
description: Number of actions added by Action Surface Diff.
value: ${{ steps.report_outputs.outputs.actions_added }}
actions_modified:
description: Number of actions modified by Action Surface Diff.
value: ${{ steps.report_outputs.outputs.actions_modified }}
actions_removed:
description: Number of actions removed by Action Surface Diff.
value: ${{ steps.report_outputs.outputs.actions_removed }}
should_run:
description: Trigger evaluator should_run value from verifier.json (`true`/`false`).
value: ${{ steps.report_outputs.outputs.should_run }}
trigger_action:
description: >-
Final trigger action (`force_run`, `run_shipgate`, `skip_shipgate`,
`dry_run`, `withheld`, or `none`). `withheld` means the evaluator
published no verdict — read `trigger_evaluation_status` to tell which
of the two withheld states applies. Never treat it as a skip.
value: ${{ steps.report_outputs.outputs.trigger_action }}
trigger_evaluation_status:
description: >-
Trigger evaluation status (`evaluated`, `unclassified`, or
`not_evaluated`). `unclassified` means the diff was read in full and no
rule classified it — run the scan. `not_evaluated` means the diff could
not be read — repair the input. `should_run` is empty for both.
value: ${{ steps.report_outputs.outputs.trigger_evaluation_status }}
trigger_rule_ids:
description: Comma-separated trigger rule IDs that matched.
value: ${{ steps.report_outputs.outputs.trigger_rule_ids }}
verifier_verdict:
description: Verifier convenience verdict. Prefer `decision` for CI gating; projected from release_decision.
value: ${{ steps.report_outputs.outputs.verifier_verdict }}
merge_verdict:
description: "PR/controller verdict projected from release_decision (`mergeable`, `human_review_required`, `insufficient_evidence`, `blocked`, or `unknown`). Use `decision` for CI gating."
value: ${{ steps.report_outputs.outputs.merge_verdict }}
can_merge_without_human:
description: True only for a verified passed result or a completed deterministic not-applicable skip.
value: ${{ steps.report_outputs.outputs.can_merge_without_human }}
agent_control_state:
description: Authoritative agent control state (`complete`, `agent_action_required`, `review_publishable`, or `human_review_required`). `review_publishable` authorizes commit/push/PR updates while denying merge and completion.
value: ${{ steps.report_outputs.outputs.agent_control_state }}
agent_control_reason:
description: Deterministic reason for the authoritative agent control state.
value: ${{ steps.report_outputs.outputs.agent_control_reason }}
agent_controller_must_stop:
description: Compatibility mirror of verifier.json.control.must_stop (`true`/`false`).
value: ${{ steps.report_outputs.outputs.agent_controller_must_stop }}
agent_controller_stop_reason:
description: Compatibility mirror of verifier.json.control.stop_reason.
value: ${{ steps.report_outputs.outputs.agent_controller_stop_reason }}
agent_controller_completion_allowed:
description: Compatibility mirror of verifier.json.control.completion_allowed (`true`/`false`).
value: ${{ steps.report_outputs.outputs.agent_controller_completion_allowed }}
trust_root_touched:
description: Whether verifier_summary reports a touched release trust root (`true`/`false`). Review signal; not a second gate.
value: ${{ steps.report_outputs.outputs.trust_root_touched }}
policy_weakened:
description: Whether verifier_summary reports a policy weakening (`true`/`false`). Review signal; not a second gate.
value: ${{ steps.report_outputs.outputs.policy_weakened }}
capability_changes_added:
description: Supporting/provisional count of added members in the capability delta; not a second gate.
value: ${{ steps.report_outputs.outputs.capability_changes_added }}
capability_changes_modified:
description: Supporting/provisional count of broadened plus narrowed capability delta members; not a second gate.
value: ${{ steps.report_outputs.outputs.capability_changes_modified }}
capability_changes_removed:
description: Supporting/provisional count of removed members in the capability delta; not a second gate.
value: ${{ steps.report_outputs.outputs.capability_changes_removed }}
runs:
using: composite
steps:
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.12"
- name: Install Agents Shipgate
shell: bash
env:
SHIPGATE_VERSION: ${{ inputs.shipgate_version }}
SHIPGATE_WHEEL: ${{ inputs.shipgate_wheel }}
SHIPGATE_WHEEL_SHA256: ${{ inputs.shipgate_wheel_sha256 }}
run: |
# This step runs in GITHUB_WORKSPACE, the pull request's own files.
# `python -m` puts that directory first on sys.path, so a PR adding
# `pip/__main__.py` would run before the engine is installed; `-P`
# leaves it off. A script path puts the script's directory first, not
# the workspace, and the installer passes `-P` to its own pip.
if [ -n "${SHIPGATE_WHEEL}" ] || [ -n "${SHIPGATE_WHEEL_SHA256}" ]; then
python "${GITHUB_ACTION_PATH}/scripts/install_action_engine.py"
elif [ -n "${SHIPGATE_VERSION}" ]; then
python -P -m pip install "agents-shipgate==${SHIPGATE_VERSION}"
python "${GITHUB_ACTION_PATH}/scripts/action_engine_identity.py"
else
python -P -m pip install "${GITHUB_ACTION_PATH}"
fi
- name: Run Agents Shipgate
id: scan
shell: bash
env:
CONFIG: ${{ inputs.config }}
CI_MODE: ${{ inputs.ci_mode }}
FAIL_ON: ${{ inputs.fail_on }}
BASELINE: ${{ inputs.baseline }}
BASELINE_MODE: ${{ inputs.baseline_mode }}
DIFF_FROM: ${{ inputs.diff_from }}
DIFF_BASE: ${{ inputs.diff_base }}
VERIFY_MODE: ${{ inputs.verify_mode }}
INPUT_BASE_REF: ${{ inputs.base_ref }}
INPUT_HEAD_REF: ${{ inputs.head_ref }}
BASE_REF: ${{ github.base_ref }}
EVENT_NAME: ${{ github.event_name }}
EVALUATED_HEAD_SHA: ${{ github.sha }}
POLICY_PACKS: ${{ inputs.policy_packs }}
NO_PLUGINS: ${{ inputs.no_plugins }}
OUTPUT_DIR: ${{ inputs.output_dir }}
PR_COMMENT_STYLE: ${{ inputs.pr_comment_style }}
APPLICATION: ${{ inputs.application }}
APPLICATION_SCOPE: ${{ inputs.application_scope }}
APPLICATION_FAIL_ON: ${{ inputs.application_fail_on }}
APPLICATION_BASE_SHA: ${{ github.event.pull_request.base.sha }}
APPLICATION_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
FAIL_ON_MERGE_VERDICTS: ${{ inputs.fail_on_merge_verdicts }}
ATTESTATION: ${{ inputs.attestation }}
ORG_BUNDLE: ${{ inputs.org_bundle }}
HOST_AUDIT: ${{ inputs.host_audit }}
ORG_STATUS: ${{ inputs.org_status }}
CHECK_RUN: ${{ inputs.check_run }}
run: |
if [ "${APPLICATION}" = "true" ]; then
echo "execution_mode=application" >> "${GITHUB_OUTPUT}"
set +e
python "${GITHUB_ACTION_PATH}/scripts/github_action_application.py"
exit_code=$?
echo "exit_code=${exit_code}" >> "${GITHUB_OUTPUT}"
exit 0
elif [ "${APPLICATION}" != "false" ]; then
echo "application must be 'true' or 'false'." >&2
echo "exit_code=2" >> "${GITHUB_OUTPUT}"
exit 0
fi
if [ "${VERIFY_MODE}" = "verify" ]; then
args=(
verify
--workspace "."
--config "${CONFIG}"
--ci-mode "${CI_MODE}"
--out "${OUTPUT_DIR}"
--format json
--pr-comment-style "${PR_COMMENT_STYLE}"
)
elif [ "${VERIFY_MODE}" = "scan" ]; then
args=(
scan
--config "${CONFIG}"
--ci-mode "${CI_MODE}"
--out "${OUTPUT_DIR}"
--format markdown,json,sarif
--packet-format json
)
else
echo "verify_mode must be 'verify' or 'scan'." >&2
echo "exit_code=2" >> "${GITHUB_OUTPUT}"
exit 2
fi
if [ -n "${FAIL_ON}" ]; then
args+=(--fail-on "${FAIL_ON}")
fi
if [ -n "${BASELINE}" ]; then
args+=(--baseline "${BASELINE}" --baseline-mode "${BASELINE_MODE}")
fi
if [ -n "${DIFF_FROM}" ]; then
args+=(--diff-from "${DIFF_FROM}")
fi
if [ "${VERIFY_MODE}" = "verify" ]; then
resolved_base_ref="${INPUT_BASE_REF}"
if [ -z "${resolved_base_ref}" ] && [ "${DIFF_BASE}" = "target" ] && [ "${EVENT_NAME}" = "pull_request" ] && [ -n "${BASE_REF}" ]; then
resolved_base_ref="origin/${BASE_REF}"
fi
if [ -n "${resolved_base_ref}" ]; then
args+=(--base "${resolved_base_ref}")
fi
if [ -n "${INPUT_HEAD_REF}" ]; then
args+=(--head "${INPUT_HEAD_REF}")
elif [ -n "${EVALUATED_HEAD_SHA}" ]; then
args+=(--head "${EVALUATED_HEAD_SHA}")
fi
fi
if [ -n "${POLICY_PACKS}" ]; then
while IFS= read -r policy_pack; do
[ -z "${policy_pack}" ] && continue
args+=(--policy-pack "${policy_pack}")
done < <(printf '%s\n' "${POLICY_PACKS}" | tr ',' '\n')
fi
if [ "${NO_PLUGINS}" = "true" ]; then
args+=(--no-plugins)
fi
echo "execution_mode=manifest" >> "${GITHUB_OUTPUT}"
set +e
agents-shipgate "${args[@]}"
exit_code=$?
echo "exit_code=${exit_code}" >> "${GITHUB_OUTPUT}"
exit 0
- name: Build Agents Shipgate attestation
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.attestation == 'true' }}
shell: bash
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
CONFIG: ${{ inputs.config }}
REGISTRY_REPO_LABEL: ${{ inputs.registry_repo_label }}
run: |
if [ -f "${OUTPUT_DIR}/verifier.json" ]; then
args=(
attest
--from "${OUTPUT_DIR}/verifier.json"
--out "${OUTPUT_DIR}/attestation.json"
--config "${CONFIG}"
--ci-context github-actions
)
if [ -n "${REGISTRY_REPO_LABEL}" ]; then
args+=(--repo "${REGISTRY_REPO_LABEL}")
fi
agents-shipgate "${args[@]}"
else
echo "Agents Shipgate verifier.json not found; attestation skipped." >&2
fi
- name: Build Agents Shipgate host audit
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.host_audit == 'true' }}
shell: bash
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
run: |
mkdir -p "${OUTPUT_DIR}"
shipgate audit --host --workspace "." --json \
--out "${OUTPUT_DIR}/host-grants.json" >/dev/null || \
echo "Agents Shipgate host audit failed; artifact skipped." >&2
- name: Build Agents Shipgate org status
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.org_status == 'true' }}
shell: bash
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
CONFIG: ${{ inputs.config }}
run: |
mkdir -p "${OUTPUT_DIR}"
set +e
agents-shipgate org status --config "${CONFIG}" --workspace "." --json \
> "${OUTPUT_DIR}/org-status.json"
status_code=$?
set -e
if [ "${status_code}" != "0" ] && [ "${status_code}" != "20" ]; then
echo "Agents Shipgate org status failed with ${status_code}." >&2
fi
- name: Build Agents Shipgate org evidence bundle
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.org_bundle == 'true' }}
shell: bash
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
CONFIG: ${{ inputs.config }}
REGISTRY_PATH: ${{ inputs.registry_path }}
run: |
mkdir -p "${OUTPUT_DIR}"
if [ -f "${OUTPUT_DIR}/verifier.json" ]; then
args=(
org
bundle
--config "${CONFIG}"
--workspace "."
--from "${OUTPUT_DIR}/verifier.json"
--out "${OUTPUT_DIR}/org-evidence-bundle.json"
--json
)
if [ -n "${REGISTRY_PATH}" ]; then
args+=(--registry "${REGISTRY_PATH}")
fi
agents-shipgate "${args[@]}" >/dev/null || \
echo "Agents Shipgate org evidence bundle failed; artifact skipped." >&2
else
echo "Agents Shipgate verifier.json not found; org bundle skipped." >&2
fi
- name: Extract Agents Shipgate outputs
id: report_outputs
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' }}
shell: bash
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
run: |
python "${GITHUB_ACTION_PATH}/scripts/github_action_outputs.py"
- name: Emit Agents Shipgate annotations
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.check_annotations == 'true' }}
shell: bash
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
CHECK_ANNOTATION_LIMIT: ${{ inputs.check_annotation_limit }}
run: |
python "${GITHUB_ACTION_PATH}/scripts/github_action_annotations.py"
- name: Upload Agents Shipgate report
if: ${{ always() && inputs.upload_artifact == 'true' && (steps.scan.outputs.execution_mode == 'manifest' || (steps.scan.outputs.execution_mode == 'application' && steps.scan.outputs.application_reports != '')) }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: ${{ steps.scan.outputs.execution_mode == 'application' && 'agents-shipgate-application-review' || 'agents-shipgate-report' }}
path: ${{ steps.scan.outputs.execution_mode == 'application' && steps.scan.outputs.application_reports || inputs.output_dir }}/
if-no-files-found: warn
- name: Comment on pull request
if: ${{ always() && inputs.pr_comment == 'true' && github.event_name == 'pull_request' && (steps.scan.outputs.execution_mode == 'manifest' || (steps.scan.outputs.execution_mode == 'application' && steps.scan.outputs.application_reports != '')) }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
env:
OUTPUT_DIR: ${{ steps.scan.outputs.execution_mode == 'application' && steps.scan.outputs.application_reports || inputs.output_dir }}
APPLICATION: ${{ steps.scan.outputs.execution_mode == 'application' }}
with:
github-token: ${{ inputs.github_token || github.token }}
script: |
const fs = require("fs");
const path = require("path");
const outputDir = process.env.OUTPUT_DIR || "agents-shipgate-reports";
const commentPath = path.join(outputDir, "pr-comment.md");
if (!fs.existsSync(commentPath)) {
core.warning(`Agents Shipgate PR comment not found at ${commentPath}`);
return;
}
const application = process.env.APPLICATION === 'true';
const STICKY = application ? "<!-- agents-shipgate-application-comment -->" : "<!-- agents-shipgate-pr-comment -->";
let body = fs.readFileSync(commentPath, "utf8").slice(0, 6000);
const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
const runId = process.env.GITHUB_RUN_ID;
const runUrl = `${serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`;
const artifactLine = `\n\nWorkflow artifacts: ${runUrl}`;
if (body.length + artifactLine.length <= 6000) {
body = `${body}${artifactLine}`;
}
// Upsert via sticky marker so re-runs update the existing comment instead of spamming the PR.
// Paginate the lookup — single-page (per_page=100) misses the marker on PRs with >100
// earlier comments before Shipgate's first run, which would silently regress to append-only.
try {
const allComments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
});
const prior = allComments.find((c) => c.body && c.body.startsWith(STICKY));
if (prior) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: prior.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
} catch (error) {
const headers = error.response?.headers || {};
const rateLimited = headers['x-ratelimit-remaining'] === '0' ||
headers['retry-after'] !== undefined || /rate limit/i.test(error.message || '');
if (![403, 404].includes(error.status) || rateLimited) throw error;
const limitation = application
? 'PR comment publication is unavailable with this token or PR context. ' +
'Read the advisory application review below in the workflow summary. No merge verdict is implied.'
: 'PR comment publication is unavailable with this token or PR context. ' +
'Read the review below in the workflow summary. No authenticated decision is recorded; ' +
'the verifier gate and current control still apply.';
core.warning(limitation);
await core.summary.addRaw(
`## Agents Shipgate: PR publication unavailable\n\n${limitation}\n\n${body}\n`
).write();
}
- name: Build Agents Shipgate check-run payload
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.check_run == 'true' }}
shell: bash
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
CHECK_RUN_NAME: ${{ inputs.check_run_name }}
CHECK_RUN_POLICY: ${{ inputs.check_run_policy }}
run: |
python "${GITHUB_ACTION_PATH}/scripts/github_check_run.py"
- name: Create Agents Shipgate check run
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.check_run == 'true' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
env:
OUTPUT_DIR: ${{ inputs.output_dir }}
with:
github-token: ${{ inputs.github_token || github.token }}
script: |
const fs = require("fs");
const path = require("path");
const outputDir = process.env.OUTPUT_DIR || "agents-shipgate-reports";
const payloadPath = path.join(outputDir, "check-run-payload.json");
if (!fs.existsSync(payloadPath)) {
core.warning(`Agents Shipgate check-run payload not found at ${payloadPath}`);
return;
}
const payload = JSON.parse(fs.readFileSync(payloadPath, "utf8"));
// Attach to the PR head SHA, not the synthetic merge commit, so the
// check shows up on the PR's checks tab and branch protection.
const headSha = context.payload.pull_request
? context.payload.pull_request.head.sha
: context.sha;
try {
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: headSha,
status: "completed",
...payload,
});
} catch (error) {
// Forks and minimal tokens lack `checks: write`; degrade to a
// warning rather than failing the gate step.
core.warning(`Agents Shipgate check-run creation failed: ${error.message}`);
}
- name: Apply Agents Shipgate merge verdict policy
if: ${{ always() && steps.scan.outputs.execution_mode == 'manifest' && inputs.fail_on_merge_verdicts != '' }}
shell: bash
env:
FAIL_ON_MERGE_VERDICTS: ${{ inputs.fail_on_merge_verdicts }}
MERGE_VERDICT: ${{ steps.report_outputs.outputs.merge_verdict }}
run: |
# `-P`: a script read from stdin puts the workspace (the pull
# request's files) on sys.path ahead of site-packages, where an
# `agents_shipgate/` package there would stand in for the engine.
python -P - <<'PY'
import os
import sys
sys.path.insert(0, os.environ["GITHUB_ACTION_PATH"])
from scripts.github_action_outputs import merge_verdict_policy_exit_code
code = merge_verdict_policy_exit_code(
os.environ.get("MERGE_VERDICT", ""),
os.environ.get("FAIL_ON_MERGE_VERDICTS", ""),
)
if code:
verdict = os.environ.get("MERGE_VERDICT", "")
if verdict:
print(
"Agents Shipgate merge verdict "
f"{verdict!r} matched fail_on_merge_verdicts.",
file=sys.stderr,
)
else:
print(
"Agents Shipgate fail_on_merge_verdicts was configured, but "
"verifier.json did not expose a merge verdict.",
file=sys.stderr,
)
raise SystemExit(code)
PY
- name: Apply Agents Shipgate exit code
if: ${{ always() && steps.scan.outputs.exit_code != '' && steps.scan.outputs.exit_code != '0' }}
shell: bash
env:
EXIT_CODE: ${{ steps.scan.outputs.exit_code }}
run: exit "${EXIT_CODE}"