chore: initialize from backend template 0a6dd0e
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Replace this template owner when forking the repository. CODEOWNERS is effective only when the
|
||||
# forge supports it and default-branch protection requires code-owner review.
|
||||
|
||||
/.trivyignore.yaml @DongHyeonka
|
||||
/.github/ @DongHyeonka
|
||||
/renovate.json @DongHyeonka
|
||||
/flaky-quarantine.yaml @DongHyeonka
|
||||
|
||||
# Public security and compatibility baselines require an explicit maintainer review.
|
||||
/docs/security/ @DongHyeonka
|
||||
/src/config/architecture/modules.json @DongHyeonka
|
||||
*.approved.json @DongHyeonka
|
||||
*.approved.txt @DongHyeonka
|
||||
@@ -0,0 +1,289 @@
|
||||
# Current repository CI controls. This file lists only mechanisms and jobs that exist in this
|
||||
# checkout. Build/release supply-chain, image, signing, provenance, SBOM, and tag-release jobs are
|
||||
# intentionally absent until their later bounded reconstruction.
|
||||
#
|
||||
# Fields:
|
||||
# release_blocking: true, false, or conditional
|
||||
# mechanism: gradle-custom-task, gradle-plugin-task, contract-test, workflow-job,
|
||||
# or delegated-pending
|
||||
# ref: task, plugin@task, repository-relative test path below src/, or workflow job id
|
||||
# workflow/job: canonical workflow and job that execute or represent the control
|
||||
# execution: check (through Gradle check), explicit (named in the job), or job
|
||||
gates:
|
||||
- id: format-lint
|
||||
release_blocking: true
|
||||
mechanism: gradle-plugin-task
|
||||
ref: com.diffplug.spotless@spotlessCheck
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: unit-and-contract-tests
|
||||
release_blocking: true
|
||||
mechanism: gradle-plugin-task
|
||||
ref: java@test
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: conditional-transport-qualification
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: conditionalTransportQualification
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: explicit
|
||||
- id: clean-architecture-dependencies
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyCleanArchitectureDependencies
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: environment-contract
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyEnvKeys
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: one-type-per-file
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyOneTypePerFile
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: readme-command-drift
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyReadmeCommands
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: trivy-suppression-governance
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyTrivyignore
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: quarantine-sunset
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyQuarantineSunset
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: public-path-snapshot
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyPublicPathSnapshot
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: explicit
|
||||
- id: dependency-locks
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: verifyDependencyLocks
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: explicit
|
||||
- id: architecture-contract-test
|
||||
release_blocking: true
|
||||
mechanism: contract-test
|
||||
ref: app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
- id: sample-off
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: sampleOffTest
|
||||
workflow: ci-quality-gates.yml
|
||||
job: sample-off
|
||||
execution: explicit
|
||||
- id: gate-matrix-lint
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: gate-matrix-lint
|
||||
workflow: ci-quality-gates.yml
|
||||
job: gate-matrix-lint
|
||||
execution: job
|
||||
- id: redis-sdk
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: redis-sdk
|
||||
workflow: ci-quality-gates.yml
|
||||
job: redis-sdk
|
||||
execution: job
|
||||
- id: jpa-candidate-evidence
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: jpa-candidate-evidence
|
||||
workflow: ci-quality-gates.yml
|
||||
job: jpa-candidate-evidence
|
||||
execution: job
|
||||
- id: jpa-r2-evidence
|
||||
release_blocking: conditional
|
||||
mechanism: workflow-job
|
||||
ref: jpa-r2-evidence
|
||||
workflow: jpa-r2-evidence.yml
|
||||
job: jpa-r2-evidence
|
||||
execution: job
|
||||
- id: quality-release-gate
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: release-gate
|
||||
workflow: ci-quality-gates.yml
|
||||
job: release-gate
|
||||
execution: job
|
||||
- id: flaky-quarantine
|
||||
release_blocking: false
|
||||
mechanism: workflow-job
|
||||
ref: quarantine
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quarantine
|
||||
execution: job
|
||||
- id: dependency-review
|
||||
release_blocking: conditional
|
||||
mechanism: workflow-job
|
||||
ref: dependency-review
|
||||
workflow: dependency-vulnerability.yml
|
||||
job: dependency-review
|
||||
execution: job
|
||||
- id: dependency-submission
|
||||
release_blocking: false
|
||||
mechanism: workflow-job
|
||||
ref: dependency-submission
|
||||
workflow: dependency-vulnerability.yml
|
||||
job: dependency-submission
|
||||
execution: job
|
||||
- id: filesystem-vulnerability-scan
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: trivy-fs
|
||||
workflow: dependency-vulnerability.yml
|
||||
job: trivy-fs
|
||||
execution: job
|
||||
- id: documentation-links
|
||||
release_blocking: conditional
|
||||
mechanism: workflow-job
|
||||
ref: lychee
|
||||
workflow: link-check.yml
|
||||
job: lychee
|
||||
execution: job
|
||||
- id: object-storage-minio-managed-contract
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: objectStorageMinioContractTest
|
||||
workflow: object-storage-qualification.yml
|
||||
job: minio-managed-contract
|
||||
execution: explicit
|
||||
- id: poster-image-migration
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: posterImageMigrationTest
|
||||
workflow: object-storage-qualification.yml
|
||||
job: poster-image-v7-migration
|
||||
execution: explicit
|
||||
- id: object-storage-minio-managed-fault
|
||||
release_blocking: conditional
|
||||
mechanism: gradle-custom-task
|
||||
ref: objectStorageMinioFaultTest
|
||||
workflow: object-storage-qualification.yml
|
||||
job: minio-managed-fault
|
||||
execution: explicit
|
||||
- id: object-storage-aws-protected-qualification
|
||||
release_blocking: conditional
|
||||
mechanism: delegated-pending
|
||||
ref: approval-gate-b
|
||||
workflow: object-storage-qualification.yml
|
||||
job: aws-managed-common-subset
|
||||
execution: job
|
||||
- id: redis-sdk-support-matrix
|
||||
release_blocking: true
|
||||
mechanism: contract-test
|
||||
ref: adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/RedisSupportMatrixTest.java
|
||||
workflow: ci-quality-gates.yml
|
||||
job: quality-gates
|
||||
execution: check
|
||||
# Promoted from delegated-pending: the workflow is no longer manual-only. A pull request that
|
||||
# touches the Redis leaf runs the standalone lane, and the full supported-version x topology
|
||||
# matrix runs nightly and on a release candidate. While it was dispatch-only, a release could
|
||||
# claim topology evidence that nobody had produced for that commit.
|
||||
- id: redis-sdk-topology-evidence
|
||||
release_blocking: conditional
|
||||
mechanism: workflow-job
|
||||
ref: topology-evidence
|
||||
workflow: redis-sdk-topology.yml
|
||||
job: topology-evidence
|
||||
execution: job
|
||||
- id: httpclient-stable-contract
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: httpClientStableContractTest
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: explicit
|
||||
- id: httpclient-security-suite
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: httpClientSecurityTest
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: explicit
|
||||
- id: httpclient-fault-injection
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: httpClientFailureInjectionTest
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: explicit
|
||||
- id: httpclient-performance-certification
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: httpClientPerformanceTest
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: explicit
|
||||
- id: httpclient-spring62-api-surface
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: spring62ApiSurfaceScan
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: explicit
|
||||
# The 6.2 API-surface scan above proves the common packages compile against the older surface. It
|
||||
# does not prove they run on it, and the two were being conflated: a lane called
|
||||
# "spring62CompatibilityTest" reads as a runtime compatibility proof. The Gradle task is renamed to
|
||||
# say what it does, and the runtime claim is registered here as its own delegated-pending control
|
||||
# so the gap is a tracked absence rather than an unstated one. Executing it needs a Spring
|
||||
# Framework 6.2 distribution resolved into a separate test runtime, which this repository's
|
||||
# Boot 4.0 baseline does not carry.
|
||||
- id: httpclient-spring62-runtime
|
||||
release_blocking: conditional
|
||||
mechanism: delegated-pending
|
||||
ref: spring62-runtime-lane
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: job
|
||||
- id: httpclient-spring70-compatibility
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: spring70CompatibilityTest
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: explicit
|
||||
- id: httpclient-documentation-drift
|
||||
release_blocking: true
|
||||
mechanism: workflow-job
|
||||
ref: httpclient-documentation
|
||||
workflow: httpclient-release.yml
|
||||
job: httpclient-documentation
|
||||
execution: job
|
||||
- id: httpclient-event-loop-blocking
|
||||
release_blocking: true
|
||||
mechanism: gradle-custom-task
|
||||
ref: httpClientBlockHoundTest
|
||||
workflow: httpclient-release.yml
|
||||
job: release-gate
|
||||
execution: explicit
|
||||
@@ -0,0 +1,18 @@
|
||||
# GitHub dependency-review configuration. The workflow guards this GitHub-only API so other forges
|
||||
# use the platform-neutral Trivy filesystem snapshot instead.
|
||||
fail-on-severity: high
|
||||
|
||||
fail-on-scopes:
|
||||
- runtime
|
||||
|
||||
# Template legal posture: deny strong and network copyleft for newly introduced dependencies.
|
||||
# Forks must have their legal/security owner review this organization-specific list.
|
||||
deny-licenses:
|
||||
- GPL-2.0-only
|
||||
- GPL-2.0-or-later
|
||||
- GPL-3.0-only
|
||||
- GPL-3.0-or-later
|
||||
- AGPL-3.0-only
|
||||
- AGPL-3.0-or-later
|
||||
|
||||
comment-summary-in-pr: never
|
||||
@@ -0,0 +1,92 @@
|
||||
# Dependency Vulnerability Policy
|
||||
|
||||
This policy is enforced by
|
||||
[`dependency-vulnerability.yml`](workflows/dependency-vulnerability.yml),
|
||||
[`dependency-review-config.yml`](dependency-review-config.yml),
|
||||
[`../.trivyignore.yaml`](../.trivyignore.yaml), `verifyTrivyignore`, CODEOWNERS, and
|
||||
[`../renovate.json`](../renovate.json).
|
||||
|
||||
## Execution and platform boundary
|
||||
|
||||
Canonical workflow files live under `.github/workflows`. The current origin is Gitea and
|
||||
server-side Actions is externally disabled; committing these controls does not enable or prove
|
||||
remote execution. An administrator must enable Actions, provide compatible runners, configure
|
||||
required checks, and require code-owner review separately.
|
||||
|
||||
External actions execute only by verified full commit SHA; inline comments retain the immutable
|
||||
release tag for review. GitHub dependency review publishes check output but never writes a PR
|
||||
summary comment, preserving the workflow-wide `contents: read` permission.
|
||||
|
||||
GitHub dependency review and dependency submission depend on GitHub.com APIs. They are guarded by
|
||||
`github.server_url == 'https://github.com'` and intentionally skip on Gitea. `trivy-fs` is the
|
||||
platform-neutral release-blocking snapshot and runs for pull requests, `main` pushes, daily
|
||||
schedule, and manual dispatch.
|
||||
|
||||
The later supply-chain slice owns image builds/scans, SBOM, signing, provenance, tag release, and
|
||||
retention. This policy does not claim those absent jobs.
|
||||
|
||||
## Severity and response
|
||||
|
||||
| Severity | CI posture | Target remediation |
|
||||
| --- | --- | --- |
|
||||
| KEV / Critical | block | 7 days or the CISA due date, whichever is sooner |
|
||||
| High | block | 30 days |
|
||||
| Medium | advisory | 90 days |
|
||||
| Low | advisory | best effort |
|
||||
|
||||
High/Critical is the template's release threshold. The exact threshold and remediation targets are
|
||||
team policy rather than an external mandate and should be reviewed when the template is adopted.
|
||||
GitHub dependency review applies the same High threshold to newly introduced runtime dependencies.
|
||||
|
||||
The Trivy JSON snapshot is also compared with the CISA Known Exploited Vulnerabilities catalog.
|
||||
Any intersection blocks regardless of Trivy severity. Before intersection, CI requires nonblank
|
||||
catalog metadata, a positive integral declared count, a non-empty vulnerability array whose length
|
||||
matches that count, and unique CVE-pattern `cveID` values. A missing, empty, malformed,
|
||||
count-inconsistent, duplicate, or unreachable KEV feed fails closed; configure the `KEV_FEED_URL`
|
||||
repository variable to an approved internal mirror when direct CISA access is unavailable.
|
||||
|
||||
Candidate extraction also validates Trivy's JSON first: the top level must be an object with a
|
||||
non-empty `Results` array; each result is an object whose `Vulnerabilities` value is null or an
|
||||
array; and every listed vulnerability has a nonblank string `VulnerabilityID`. Invalid scanner
|
||||
output fails closed instead of becoming an empty candidate set.
|
||||
|
||||
## Scanner and network requirements
|
||||
|
||||
Trivy scans the committed filesystem and Gradle lockfiles. High/Critical findings exit non-zero;
|
||||
Medium/Low findings are reported with exit zero. Scanner database or tool-download failures remain
|
||||
failures rather than silently producing an empty result.
|
||||
|
||||
Runners need HTTPS egress to:
|
||||
|
||||
- the configured Trivy binary source (GitHub Releases by default);
|
||||
- Trivy vulnerability databases (the scanner defaults, commonly OCI registries);
|
||||
- jq releases, unless `JQ_DOWNLOAD_BASE_URL` points to an internal mirror;
|
||||
- the CISA KEV feed, unless `KEV_FEED_URL` points to an internal mirror;
|
||||
- GitHub action sources when the forge does not mirror actions.
|
||||
|
||||
Closed networks must mirror the pinned Trivy/jq artifacts and checksums, set
|
||||
`TRIVY_DOWNLOAD_BASE_URL` and `JQ_DOWNLOAD_BASE_URL`, configure Trivy's documented database mirror
|
||||
environment, and set `KEV_FEED_URL`. Mirror availability and freshness are operational
|
||||
dependencies; stale mirrors can delay detection.
|
||||
|
||||
## Suppression governance
|
||||
|
||||
The only suppression source is repository-root `.trivyignore.yaml`. Every Trivy scan passes it
|
||||
explicitly with `--ignorefile .trivyignore.yaml`. Each future entry must contain an identifier, a
|
||||
non-empty rationale, and a future expiry no more than 90 days away. `verifyTrivyignore` validates
|
||||
the shape and expiry; CODEOWNERS plus branch protection controls who may approve the change.
|
||||
Neither control substitutes for the other.
|
||||
|
||||
Do not use an ad-hoc ignore file or an inline scanner bypass. An expired suppression is removed or
|
||||
renewed with fresh owner review and current evidence.
|
||||
|
||||
## Dependency update policy
|
||||
|
||||
Renovate is the checked-in security-update bot configuration. Patch/pin/digest security updates
|
||||
may auto-merge only after every configured required check succeeds; minor and major updates require
|
||||
human review. Dependency declarations and all 19 `gradle.lockfile` files must move together, and
|
||||
`verifyDependencyLocks` remains release-blocking.
|
||||
|
||||
Bot alerts are not a transitive-dependency backstop on every forge. Use a Gradle constraint or
|
||||
resolution rule for a vulnerable transitive dependency, refresh locks deliberately, and retain the
|
||||
full-snapshot Trivy scan.
|
||||
@@ -0,0 +1,18 @@
|
||||
## What changed and why
|
||||
|
||||
<!-- Summarize the behavior, architecture boundary, or policy being changed. -->
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] I ran the focused test for each changed leaf.
|
||||
- [ ] I ran `cd src && ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks`.
|
||||
- [ ] I did not add an unregistered production module dependency.
|
||||
- [ ] Dependency changes include refreshed `gradle.lockfile` files and a strict-lock verification.
|
||||
- [ ] Trivy suppressions include an owner-reviewed reason and an expiry within 90 days.
|
||||
- [ ] Any quarantined test is registered in `flaky-quarantine.yaml` with a reason, issue, and
|
||||
`quarantined_since` date.
|
||||
- [ ] I documented checks that could not run and their remaining risk.
|
||||
|
||||
## Compatibility and operations
|
||||
|
||||
<!-- Note public API/config changes, migration needs, rollout/rollback impact, and external blockers. -->
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly JQ_VERSION='1.8.1'
|
||||
readonly JQ_SHA256_AMD64='020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d'
|
||||
readonly JQ_SHA256_ARM64='6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4'
|
||||
readonly DOWNLOAD_BASE_URL="${JQ_DOWNLOAD_BASE_URL:-https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}}"
|
||||
|
||||
: "${RUNNER_TEMP:?RUNNER_TEMP must be set by the CI runner}"
|
||||
: "${GITHUB_PATH:?GITHUB_PATH must be set by the CI runner}"
|
||||
|
||||
architecture="${RUNNER_ARCH:-$(uname -m)}"
|
||||
case "${architecture}" in
|
||||
X64 | x86_64 | amd64)
|
||||
asset='jq-linux-amd64'
|
||||
expected_sha256="${JQ_SHA256_AMD64}"
|
||||
;;
|
||||
ARM64 | aarch64 | arm64)
|
||||
asset='jq-linux-arm64'
|
||||
expected_sha256="${JQ_SHA256_ARM64}"
|
||||
;;
|
||||
*)
|
||||
printf '::error::install-jq: unsupported runner architecture: %s\n' "${architecture}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
install_dir="${RUNNER_TEMP}/jq-${JQ_VERSION}/bin"
|
||||
destination="${install_dir}/jq"
|
||||
mkdir -p "${install_dir}"
|
||||
|
||||
temporary="$(mktemp "${RUNNER_TEMP}/jq-${JQ_VERSION}.XXXXXX")"
|
||||
trap 'rm -f "${temporary}"' EXIT
|
||||
|
||||
curl --fail --show-error --silent --location --retry 3 \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
"${DOWNLOAD_BASE_URL}/${asset}" \
|
||||
--output "${temporary}"
|
||||
printf '%s %s\n' "${expected_sha256}" "${temporary}" | sha256sum -c -
|
||||
chmod 0755 "${temporary}"
|
||||
mv "${temporary}" "${destination}"
|
||||
trap - EXIT
|
||||
|
||||
printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}"
|
||||
installed_version="$("${destination}" --version)"
|
||||
if [[ "${installed_version}" != "jq-${JQ_VERSION}" ]]; then
|
||||
printf '::error::install-jq: expected jq-%s, got %s\n' "${JQ_VERSION}" "${installed_version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'install-jq: %s installed under RUNNER_TEMP\n' "${installed_version}"
|
||||
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
if (( $# > 1 )); then
|
||||
printf '::error::gate-matrix-lint: expected zero arguments or one repository root\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if (( $# == 1 )); then
|
||||
if [[ ! -d "$1" ]]; then
|
||||
printf '::error::gate-matrix-lint: repository root is not a directory: %s\n' "$1" >&2
|
||||
exit 2
|
||||
fi
|
||||
REPO_ROOT="$(cd -- "$1" && pwd -P)"
|
||||
else
|
||||
REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel)"
|
||||
EXPECTED_SCRIPT_DIR="$(cd -- "${REPO_ROOT}/.github/scripts" && pwd -P)"
|
||||
if [[ "${SCRIPT_DIR}" != "${EXPECTED_SCRIPT_DIR}" ]]; then
|
||||
printf '::error::gate-matrix-lint: script location must be repository .github/scripts directory\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
readonly REPO_ROOT
|
||||
readonly MATRIX="${REPO_ROOT}/.github/ci-gate-matrix.yml"
|
||||
# Deliberately a literal: a gate silently appearing or disappearing is the drift this lint exists to
|
||||
# catch, so growing the matrix is an explicit edit here. 38 as of the HTTP Client platform hardening,
|
||||
# which registered httpclient-spring62-runtime as a delegated-pending control — the 6.2 *runtime*
|
||||
# claim, distinct from the API-surface scan that was standing in for it.
|
||||
readonly EXPECTED_GATE_COUNT=38
|
||||
|
||||
if [[ ! -f "${MATRIX}" ]]; then
|
||||
printf '::error::gate-matrix-lint: missing %s\n' "${MATRIX}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
records="$(
|
||||
awk '
|
||||
function flush() {
|
||||
if (id != "") {
|
||||
printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", id, blocking, mechanism, ref, workflow, job, execution
|
||||
}
|
||||
}
|
||||
/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/ {
|
||||
flush()
|
||||
id=$0
|
||||
sub(/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/, "", id)
|
||||
blocking=mechanism=ref=workflow=job=execution=""
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+release_blocking:[[:space:]]*/ {
|
||||
blocking=$0
|
||||
sub(/^[[:space:]]+release_blocking:[[:space:]]*/, "", blocking)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+mechanism:[[:space:]]*/ {
|
||||
mechanism=$0
|
||||
sub(/^[[:space:]]+mechanism:[[:space:]]*/, "", mechanism)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+ref:[[:space:]]*/ {
|
||||
ref=$0
|
||||
sub(/^[[:space:]]+ref:[[:space:]]*/, "", ref)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+workflow:[[:space:]]*/ {
|
||||
workflow=$0
|
||||
sub(/^[[:space:]]+workflow:[[:space:]]*/, "", workflow)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+job:[[:space:]]*/ {
|
||||
job=$0
|
||||
sub(/^[[:space:]]+job:[[:space:]]*/, "", job)
|
||||
next
|
||||
}
|
||||
/^[[:space:]]+execution:[[:space:]]*/ {
|
||||
execution=$0
|
||||
sub(/^[[:space:]]+execution:[[:space:]]*/, "", execution)
|
||||
next
|
||||
}
|
||||
END { flush() }
|
||||
' "${MATRIX}"
|
||||
)"
|
||||
|
||||
declare -A seen_ids=()
|
||||
declare -a failures=()
|
||||
total=0
|
||||
verified=0
|
||||
delegated=0
|
||||
|
||||
job_body() {
|
||||
local workflow_file="$1"
|
||||
local job_id="$2"
|
||||
awk -v target="${job_id}" '
|
||||
$0 ~ "^ " target ":[[:space:]]*$" { inside=1; print; next }
|
||||
inside && $0 ~ "^ [A-Za-z0-9_-]+:[[:space:]]*$" { exit }
|
||||
inside { print }
|
||||
' "${workflow_file}"
|
||||
}
|
||||
|
||||
gradle_command_has_safe_literal_grammar() {
|
||||
local command="$1"
|
||||
[[ "${command}" =~ ^\./gradlew([[:space:]]+[A-Za-z0-9_.:/@=,+-]+)+[[:space:]]*$ ]]
|
||||
}
|
||||
|
||||
gradle_token_suppresses_execution() {
|
||||
local token="$1"
|
||||
case "${token}" in
|
||||
'--dry-run'|'--dry-run='*|'-m'|'-x'|'-x'*|'--exclude-task'|'--exclude-task='*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
gradle_token_is_allowed_gate_argument() {
|
||||
local token="$1"
|
||||
case "${token}" in
|
||||
'--no-daemon'|'--stacktrace'|'--warning-mode=fail') return 0 ;;
|
||||
esac
|
||||
[[ "${token}" =~ ^:?[A-Za-z0-9_][A-Za-z0-9_.-]*(:[A-Za-z0-9_][A-Za-z0-9_.-]*)*$ ]]
|
||||
}
|
||||
|
||||
gradle_plugin_is_applied() {
|
||||
local plugin_id="$1"
|
||||
grep -RqsF --include='build.gradle' -- "id '${plugin_id}'" "${REPO_ROOT}/src" \
|
||||
|| grep -RqsF --include='build.gradle' -- "id \"${plugin_id}\"" "${REPO_ROOT}/src" \
|
||||
|| grep -RqsF --include='build.gradle' -- "apply plugin: '${plugin_id}'" "${REPO_ROOT}/src" \
|
||||
|| grep -RqsF --include='build.gradle' -- "apply plugin: \"${plugin_id}\"" "${REPO_ROOT}/src"
|
||||
}
|
||||
|
||||
gradle_custom_task_is_registered_in_build_file() {
|
||||
local task_name="$1"
|
||||
local build_file="$2"
|
||||
if grep -qsE -- "tasks\\.register\\(['\"]${task_name}['\"]" "${build_file}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
awk -v required_task="${task_name}" '
|
||||
index($0, "registerStrictQualificationTest(") > 0 { inside_registration=1 }
|
||||
inside_registration && /^[[:space:]]*name:[[:space:]]*/ {
|
||||
candidate=$0
|
||||
sub(/^[[:space:]]*name:[[:space:]]*/, "", candidate)
|
||||
quote=substr(candidate, 1, 1)
|
||||
if (quote != "\"" && quote != sprintf("%c", 39)) {
|
||||
next
|
||||
}
|
||||
candidate=substr(candidate, 2)
|
||||
closing_quote=index(candidate, quote)
|
||||
if (closing_quote == 0) {
|
||||
next
|
||||
}
|
||||
candidate=substr(candidate, 1, closing_quote - 1)
|
||||
if (candidate == required_task) {
|
||||
found=1
|
||||
}
|
||||
}
|
||||
inside_registration && /\)[[:space:]]*$/ { inside_registration=0 }
|
||||
END { exit found ? 0 : 1 }
|
||||
' "${build_file}"
|
||||
}
|
||||
|
||||
gradle_custom_task_is_registered() {
|
||||
local task_name="$1"
|
||||
local build_file
|
||||
while IFS= read -r -d '' build_file; do
|
||||
if gradle_custom_task_is_registered_in_build_file "${task_name}" "${build_file}"; then
|
||||
return 0
|
||||
fi
|
||||
done < <(find "${REPO_ROOT}/src" -type f -name '*.gradle' -print0)
|
||||
return 1
|
||||
}
|
||||
|
||||
gradle_token_matches_registered_task() {
|
||||
local token="$1"
|
||||
local required_task="$2"
|
||||
local project_path build_file
|
||||
if [[ "${token}" == "${required_task}" || "${token}" == ":${required_task}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "${token}" != :* || "${token}" != *:"${required_task}" ]]; then
|
||||
return 1
|
||||
fi
|
||||
project_path="${token%:"${required_task}"}"
|
||||
project_path="${project_path#:}"
|
||||
project_path="${project_path%:}"
|
||||
build_file="${REPO_ROOT}/src/${project_path//:/\/}/build.gradle"
|
||||
[[ -f "${build_file}" ]] \
|
||||
&& gradle_custom_task_is_registered_in_build_file "${required_task}" "${build_file}"
|
||||
}
|
||||
|
||||
job_runs_gradle_task() {
|
||||
local workflow_file="$1"
|
||||
local job_id="$2"
|
||||
local required_task="$3"
|
||||
local command token
|
||||
local found_task suppressed
|
||||
local -a tokens=()
|
||||
|
||||
while IFS= read -r command; do
|
||||
if ! gradle_command_has_safe_literal_grammar "${command}"; then
|
||||
continue
|
||||
fi
|
||||
read -r -a tokens <<< "${command}"
|
||||
if (( ${#tokens[@]} < 2 )) || [[ "${tokens[0]}" != './gradlew' ]]; then
|
||||
continue
|
||||
fi
|
||||
found_task=0
|
||||
suppressed=0
|
||||
for token in "${tokens[@]:1}"; do
|
||||
case "${token}" in
|
||||
'&&'|'||'|';'|'|'|'#'*) break ;;
|
||||
esac
|
||||
if gradle_token_suppresses_execution "${token}"; then
|
||||
suppressed=1
|
||||
break
|
||||
fi
|
||||
if ! gradle_token_is_allowed_gate_argument "${token}"; then
|
||||
suppressed=1
|
||||
break
|
||||
fi
|
||||
if gradle_token_matches_registered_task "${token}" "${required_task}"; then
|
||||
found_task=1
|
||||
fi
|
||||
done
|
||||
if (( found_task == 1 && suppressed == 0 )); then
|
||||
return 0
|
||||
fi
|
||||
done < <(
|
||||
job_body "${workflow_file}" "${job_id}" | awk '
|
||||
/^[[:space:]]+(-[[:space:]]+)?run:[[:space:]]+/ {
|
||||
command=$0
|
||||
sub(/^[[:space:]]+(-[[:space:]]+)?run:[[:space:]]+/, "", command)
|
||||
if (command !~ /^(\||>)/) {
|
||||
print command
|
||||
}
|
||||
}
|
||||
'
|
||||
)
|
||||
return 1
|
||||
}
|
||||
|
||||
while IFS=$'\t' read -r id blocking mechanism ref workflow job execution; do
|
||||
[[ -z "${id}" ]] && continue
|
||||
total=$((total + 1))
|
||||
|
||||
if [[ -n "${seen_ids[${id}]:-}" ]]; then
|
||||
failures+=("duplicate gate id '${id}'")
|
||||
fi
|
||||
seen_ids["${id}"]=1
|
||||
|
||||
if [[ -z "${blocking}" || -z "${mechanism}" || -z "${ref}" || -z "${workflow}" \
|
||||
|| -z "${job}" || -z "${execution}" ]]; then
|
||||
failures+=("gate '${id}' has an empty required field")
|
||||
continue
|
||||
fi
|
||||
if [[ ! "${blocking}" =~ ^(true|false|conditional)$ ]]; then
|
||||
failures+=("gate '${id}' has invalid release_blocking '${blocking}'")
|
||||
fi
|
||||
if [[ ! "${workflow}" =~ ^[A-Za-z0-9._-]+\.ya?ml$ || ! "${job}" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
||||
failures+=("gate '${id}' has an unsafe workflow or job identifier")
|
||||
continue
|
||||
fi
|
||||
|
||||
workflow_file="${REPO_ROOT}/.github/workflows/${workflow}"
|
||||
if [[ ! -f "${workflow_file}" ]]; then
|
||||
failures+=("gate '${id}' references missing workflow '.github/workflows/${workflow}'")
|
||||
continue
|
||||
fi
|
||||
if ! grep -Eqs -- "^[[:space:]]{2}${job}:[[:space:]]*$" "${workflow_file}"; then
|
||||
failures+=("gate '${id}' references missing job '${job}' in '${workflow}'")
|
||||
continue
|
||||
fi
|
||||
|
||||
case "${mechanism}" in
|
||||
gradle-custom-task)
|
||||
if [[ ! "${ref}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
|
||||
failures+=("gate '${id}' has unsafe Gradle custom task ref '${ref}'")
|
||||
continue
|
||||
fi
|
||||
if ! gradle_custom_task_is_registered "${ref}"; then
|
||||
failures+=("gate '${id}' references unregistered Gradle task '${ref}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
gradle-plugin-task)
|
||||
plugin="${ref%@*}"
|
||||
task="${ref#*@}"
|
||||
if [[ "${plugin}" == "${ref}" \
|
||||
|| ! "${plugin}" =~ ^[A-Za-z][A-Za-z0-9.-]*$ \
|
||||
|| ! "${task}" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ ]]; then
|
||||
failures+=("gate '${id}' has unsafe Gradle plugin task ref '${ref}'")
|
||||
continue
|
||||
fi
|
||||
if ! gradle_plugin_is_applied "${plugin}"; then
|
||||
failures+=("gate '${id}' references unapplied Gradle plugin '${plugin}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
contract-test)
|
||||
if [[ "${ref}" == /* || "${ref}" == *".."* || ! -f "${REPO_ROOT}/src/${ref}" ]]; then
|
||||
failures+=("gate '${id}' references missing or unsafe contract test 'src/${ref}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
workflow-job)
|
||||
if [[ "${ref}" != "${job}" ]]; then
|
||||
failures+=("gate '${id}' workflow-job ref '${ref}' must equal job '${job}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
delegated-pending)
|
||||
delegated=$((delegated + 1))
|
||||
printf "gate '%s': explicitly delegated-pending\n" "${id}"
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
failures+=("gate '${id}' has unknown mechanism '${mechanism}'")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${execution}" in
|
||||
check)
|
||||
if ! job_runs_gradle_task "${workflow_file}" "${job}" 'check'; then
|
||||
failures+=("gate '${id}' expects Gradle check in job '${job}'")
|
||||
continue
|
||||
fi
|
||||
if [[ "${mechanism}" == "gradle-custom-task" ]] \
|
||||
&& ! grep -RqsE -- "dependsOn.*named\\(['\"]${ref}['\"]\\)" "${REPO_ROOT}/src" \
|
||||
--include='build.gradle'; then
|
||||
failures+=("gate '${id}' task '${ref}' exists but is not wired into Gradle check")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
explicit)
|
||||
if ! job_runs_gradle_task "${workflow_file}" "${job}" "${ref}"; then
|
||||
failures+=("gate '${id}' task '${ref}' is not explicit in job '${job}'")
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
job)
|
||||
;;
|
||||
*)
|
||||
failures+=("gate '${id}' has unknown execution '${execution}'")
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
verified=$((verified + 1))
|
||||
done <<< "${records}"
|
||||
|
||||
if (( total != EXPECTED_GATE_COUNT )); then
|
||||
failures+=("matrix has ${total} gates; expected ${EXPECTED_GATE_COUNT}")
|
||||
fi
|
||||
|
||||
printf 'gate-matrix-lint: %d gates, %d verified, %d delegated-pending\n' \
|
||||
"${total}" "${verified}" "${delegated}"
|
||||
if (( ${#failures[@]} > 0 )); then
|
||||
printf '::error::gate-matrix-lint: %d drift(s) found\n' "${#failures[@]}" >&2
|
||||
for failure in "${failures[@]}"; do
|
||||
printf ' - %s\n' "${failure}" >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
printf 'gate-matrix-lint: OK\n'
|
||||
Executable
+740
@@ -0,0 +1,740 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly EXPECTED_DISTRIBUTION_SUFFIX='/gradle-9.0.0-bin.zip'
|
||||
readonly EXPECTED_DISTRIBUTION_SHA256='8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b'
|
||||
readonly EXPECTED_WRAPPER_JAR_SHA256='76805e32c009c0cf0dd5d206bddc9fb22ea42e84db904b764f3047de095493f3'
|
||||
readonly EXPECTED_VALIDATION_ACTION='gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6'
|
||||
readonly EXPECTED_DEPENDENCY_SUBMISSION_ACTION='gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1'
|
||||
readonly EXPECTED_GUARDED_GRADLE_IF="\${{ always() && steps.gradle-wrapper-validation.outcome == 'success' }}"
|
||||
# Workflow-lock update procedure (only after intentional review of the complete workflow diff):
|
||||
# find .github/workflows -mindepth 1 -maxdepth 1 \
|
||||
# \( -name '*.yml' -o -name '*.yaml' \) ! -type f -print # must print nothing
|
||||
# find .github/workflows -mindepth 1 -maxdepth 1 -type f \
|
||||
# \( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
||||
# | LC_ALL=C sort -z | xargs -0 sha256sum
|
||||
# Replace this entire sorted array in the same reviewed change. Never refresh a single digest
|
||||
# merely to make this verifier pass.
|
||||
readonly EXPECTED_WORKFLOW_LOCK=(
|
||||
'a5986c6d865e28d6160dc09c513c430c9d9c38d154c67423cb34448cb1e9863c .github/workflows/ci-quality-gates.yml'
|
||||
'59de260a70c2c0a0d686d97035a189dc0567395977dfa18758f1a2d89d15a00d .github/workflows/dependency-vulnerability.yml'
|
||||
'1b3220c922f954500f727c6a799b24e4962915845b9248e8e496e5050e829f28 .github/workflows/fileserver-nightly.yml'
|
||||
'26812e16b8d6e4472543ddd49c7b16ee6b7697834ddbb653fa0424befd71c544 .github/workflows/fileserver-pr.yml'
|
||||
'86a240c4ce7d0d293616e30de30ed77bcfdc700fedb8916f083eda9567099096 .github/workflows/fileserver-release.yml'
|
||||
'58e28f3358d794ca08f4aa8df4516e03f50a9ee58488b3f0d2619998e069ef14 .github/workflows/httpclient-contract.yml'
|
||||
'823bc346e58a58b2c0814cd1e3e55ec90d360c138419ec3d8f05deb59c62c7eb .github/workflows/httpclient-nightly.yml'
|
||||
'ad84000efc438ee7439517b8f85819e62b13dab0aa4f94066c2905060f3bb581 .github/workflows/httpclient-release.yml'
|
||||
'59cb3a0ffc687a15eefe96bc5e3a70d42be78e1cc85d2e7f7880dac6124ca4c7 .github/workflows/jpa-r2-evidence.yml'
|
||||
'5be7e931db749029d89787da042d6d7cf8e683d60698bd8a2993c29db26355fb .github/workflows/link-check.yml'
|
||||
'64245586cd5936f1a5647b57f2cd9acd316f96fd75f713b1890decb812e7d5fe .github/workflows/object-storage-qualification.yml'
|
||||
'cbc104ea486c746229895e804e3be7716e056a02cce0588c537bce9f442f8b38 .github/workflows/redis-sdk-topology.yml'
|
||||
)
|
||||
readonly EXPECTED_WRAPPER_PROPERTIES=(
|
||||
'distributionBase=GRADLE_USER_HOME'
|
||||
'distributionPath=wrapper/dists'
|
||||
"distributionUrl=https\://services.gradle.org/distributions${EXPECTED_DISTRIBUTION_SUFFIX}"
|
||||
"distributionSha256Sum=${EXPECTED_DISTRIBUTION_SHA256}"
|
||||
'networkTimeout=10000'
|
||||
'validateDistributionUrl=true'
|
||||
'zipStoreBase=GRADLE_USER_HOME'
|
||||
'zipStorePath=wrapper/dists'
|
||||
)
|
||||
|
||||
fail() {
|
||||
printf 'gradle-wrapper-contract: FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
fail 'expected exactly one repository-root argument'
|
||||
fi
|
||||
|
||||
readonly REPOSITORY_ROOT=$1
|
||||
[[ -d "${REPOSITORY_ROOT}" ]] || fail "repository root is not a directory: ${REPOSITORY_ROOT}"
|
||||
|
||||
readonly WRAPPER_PROPERTIES="${REPOSITORY_ROOT}/src/gradle/wrapper/gradle-wrapper.properties"
|
||||
readonly WRAPPER_JAR="${REPOSITORY_ROOT}/src/gradle/wrapper/gradle-wrapper.jar"
|
||||
readonly WORKFLOWS_DIRECTORY="${REPOSITORY_ROOT}/.github/workflows"
|
||||
|
||||
[[ -f "${WRAPPER_PROPERTIES}" ]] || fail "missing wrapper properties: ${WRAPPER_PROPERTIES}"
|
||||
[[ -f "${WRAPPER_JAR}" ]] || fail "missing wrapper JAR: ${WRAPPER_JAR}"
|
||||
[[ -d "${WORKFLOWS_DIRECTORY}" ]] || fail "missing workflows directory: ${WORKFLOWS_DIRECTORY}"
|
||||
|
||||
if ! printf '%s\n' "${EXPECTED_WRAPPER_PROPERTIES[@]}" | cmp -s - "${WRAPPER_PROPERTIES}"; then
|
||||
fail 'wrapper properties must match the exact canonical Gradle 9.0.0 eight-line contract'
|
||||
fi
|
||||
|
||||
readonly actual_wrapper_jar_sha256=$(sha256sum "${WRAPPER_JAR}" | awk '{print $1}')
|
||||
[[ "${actual_wrapper_jar_sha256}" == "${EXPECTED_WRAPPER_JAR_SHA256}" ]] \
|
||||
|| fail "wrapper JAR SHA-256 mismatch: ${actual_wrapper_jar_sha256}"
|
||||
|
||||
workflow_lock_valid=1
|
||||
actual_workflow_lock=()
|
||||
while IFS= read -r -d '' locked_workflow; do
|
||||
locked_workflow_relative=${locked_workflow#"${REPOSITORY_ROOT}"/}
|
||||
if [[ -L "${locked_workflow}" || ! -f "${locked_workflow}" ]]; then
|
||||
locked_workflow_sha256='<invalid-file-type>'
|
||||
else
|
||||
locked_workflow_sha256=$(sha256sum -- "${locked_workflow}" | awk '{print $1}')
|
||||
fi
|
||||
actual_workflow_lock+=("${locked_workflow_sha256} ${locked_workflow_relative}")
|
||||
done < <(
|
||||
find "${WORKFLOWS_DIRECTORY}" -mindepth 1 -maxdepth 1 \
|
||||
\( -name '*.yml' -o -name '*.yaml' \) -print0 \
|
||||
| LC_ALL=C sort -z
|
||||
)
|
||||
|
||||
workflow_lock_entry_count=${#EXPECTED_WORKFLOW_LOCK[@]}
|
||||
if ((${#actual_workflow_lock[@]} > workflow_lock_entry_count)); then
|
||||
workflow_lock_entry_count=${#actual_workflow_lock[@]}
|
||||
fi
|
||||
for ((workflow_lock_index = 0; workflow_lock_index < workflow_lock_entry_count; workflow_lock_index++)); do
|
||||
expected_workflow_lock_entry=${EXPECTED_WORKFLOW_LOCK[workflow_lock_index]-<missing>}
|
||||
actual_workflow_lock_entry=${actual_workflow_lock[workflow_lock_index]-<missing>}
|
||||
if [[ "${actual_workflow_lock_entry}" != "${expected_workflow_lock_entry}" ]]; then
|
||||
printf 'gradle-wrapper-contract: workflow lock mismatch: expected %q; actual %q\n' \
|
||||
"${expected_workflow_lock_entry}" "${actual_workflow_lock_entry}" >&2
|
||||
workflow_lock_valid=0
|
||||
fi
|
||||
done
|
||||
|
||||
workflow_count=0
|
||||
gradle_job_count=0
|
||||
while IFS= read -r -d '' workflow; do
|
||||
if ! awk -v workflow="${workflow#"${REPOSITORY_ROOT}"/}" '
|
||||
function reset_step(known_field) {
|
||||
step_active = 0
|
||||
run_block = 0
|
||||
for (known_field in step_fields) {
|
||||
delete step_fields[known_field]
|
||||
}
|
||||
}
|
||||
|
||||
function reset_job() {
|
||||
job = ""
|
||||
in_steps = 0
|
||||
steps_count = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
function indentation(line, first_non_space) {
|
||||
if (line ~ /^ *$/) {
|
||||
return length(line)
|
||||
}
|
||||
first_non_space = match(line, /[^ ]/)
|
||||
return first_non_space - 1
|
||||
}
|
||||
|
||||
function trim(value) {
|
||||
sub(/^[[:space:]]+/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
return value
|
||||
}
|
||||
|
||||
function grammar_error(message) {
|
||||
printf "%s: job %s %s\n", workflow, job == "" ? "<unknown>" : job, message > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
|
||||
function workflow_grammar_error(message) {
|
||||
printf "%s: %s\n", workflow, message > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
|
||||
function validate_job_shape() {
|
||||
if (job != "" && steps_count != 1) {
|
||||
grammar_error("must contain exactly one canonical steps block")
|
||||
}
|
||||
}
|
||||
|
||||
function is_allowed_step_field(field) {
|
||||
return field == "name" \
|
||||
|| field == "id" \
|
||||
|| field == "uses" \
|
||||
|| field == "run" \
|
||||
|| field == "if" \
|
||||
|| field == "shell" \
|
||||
|| field == "with" \
|
||||
|| field == "env" \
|
||||
|| field == "working-directory" \
|
||||
|| field == "continue-on-error" \
|
||||
|| field == "timeout-minutes"
|
||||
}
|
||||
|
||||
function validate_uses_scalar(value, first, quote, closing, index_value, suffix, action, single_quote) {
|
||||
value = trim(value)
|
||||
if (value == "" || index(value, "\\") != 0) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
|
||||
first = substr(value, 1, 1)
|
||||
single_quote = sprintf("%c", 39)
|
||||
if (first == "\"" || first == single_quote) {
|
||||
quote = first
|
||||
closing = 0
|
||||
for (index_value = 2; index_value <= length(value); index_value++) {
|
||||
if (substr(value, index_value, 1) == quote) {
|
||||
closing = index_value
|
||||
break
|
||||
}
|
||||
}
|
||||
if (closing == 0) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
suffix = substr(value, closing + 1)
|
||||
if (suffix !~ /^[[:space:]]*(#.*)?$/) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
action = substr(value, 2, closing - 2)
|
||||
if (index(action, quote) != 0) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
action = value
|
||||
sub(/[[:space:]]+#.*$/, "", action)
|
||||
action = trim(action)
|
||||
if (action ~ /["'"'"'\\]/ || action ~ /^[*!&|>]/) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (action !~ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*@[A-Za-z0-9_.\/-]+$/ \
|
||||
&& action !~ /^\.\/[A-Za-z0-9_.\/-]+$/ \
|
||||
&& action !~ /^docker:\/\/[^[:space:]]+$/) {
|
||||
grammar_error("has unsupported uses scalar")
|
||||
}
|
||||
}
|
||||
|
||||
function validate_run_scalar(value, first) {
|
||||
value = trim(value)
|
||||
if (value ~ /^(\||>)[+-]?([[:space:]]+#.*)?$/) {
|
||||
run_block = 1
|
||||
return
|
||||
}
|
||||
first = substr(value, 1, 1)
|
||||
if (value == "" || first == "\"" || first == sprintf("%c", 39) \
|
||||
|| first ~ /[*&!|>]/ || index(value, "\\") != 0) {
|
||||
grammar_error("has unsupported run scalar")
|
||||
}
|
||||
}
|
||||
|
||||
function validate_step_field(content, field, value, separator) {
|
||||
content = trim(content)
|
||||
if (content ~ /^[{[]/) {
|
||||
grammar_error("contains unsupported flow-style step syntax")
|
||||
return
|
||||
}
|
||||
if (content ~ /^<</) {
|
||||
grammar_error("contains a forbidden step merge key")
|
||||
return
|
||||
}
|
||||
if (content ~ /^[*&!]/) {
|
||||
grammar_error("contains unsupported step anchor, alias, or tag syntax")
|
||||
return
|
||||
}
|
||||
if (content !~ /^[A-Za-z][A-Za-z0-9-]*:/) {
|
||||
grammar_error("contains unsupported step field syntax")
|
||||
return
|
||||
}
|
||||
|
||||
separator = index(content, ":")
|
||||
field = substr(content, 1, separator - 1)
|
||||
value = substr(content, separator + 1)
|
||||
sub(/^[[:space:]]*/, "", value)
|
||||
if (!is_allowed_step_field(field)) {
|
||||
grammar_error("contains unsupported step field: " field)
|
||||
return
|
||||
}
|
||||
if (field in step_fields) {
|
||||
grammar_error("contains duplicate step field: " field)
|
||||
return
|
||||
}
|
||||
step_fields[field] = 1
|
||||
|
||||
if (field == "uses") {
|
||||
validate_uses_scalar(value)
|
||||
} else if (field == "run") {
|
||||
validate_run_scalar(value)
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
in_jobs = 0
|
||||
invalid = 0
|
||||
jobs_count = 0
|
||||
single_quote = sprintf("%c", 39)
|
||||
reset_job()
|
||||
}
|
||||
|
||||
/^jobs:/ {
|
||||
if ($0 !~ /^jobs:[[:space:]]*(#.*)?$/) {
|
||||
workflow_grammar_error("jobs container must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
jobs_count++
|
||||
if (jobs_count != 1) {
|
||||
workflow_grammar_error("workflow must contain exactly one canonical jobs block")
|
||||
}
|
||||
in_jobs = 1
|
||||
next
|
||||
}
|
||||
|
||||
/^"jobs":/ {
|
||||
workflow_grammar_error("jobs container must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
|
||||
substr($0, 1, 7) == single_quote "jobs" single_quote ":" {
|
||||
workflow_grammar_error("jobs container must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
|
||||
run_block == 0 && /^<<:/ {
|
||||
workflow_grammar_error("workflow contains a forbidden merge key")
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && /^[^[:space:]#]/ {
|
||||
validate_job_shape()
|
||||
reset_job()
|
||||
in_jobs = 0
|
||||
}
|
||||
|
||||
in_jobs && /^ [^[:space:]#]/ {
|
||||
if ($0 !~ /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/) {
|
||||
grammar_error("job declaration must use a canonical block mapping")
|
||||
next
|
||||
}
|
||||
validate_job_shape()
|
||||
reset_job()
|
||||
job = $0
|
||||
sub(/^ /, "", job)
|
||||
sub(/:.*/, "", job)
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && job != "" {
|
||||
raw = $0
|
||||
line_indent = indentation(raw)
|
||||
|
||||
if (run_block != 0) {
|
||||
if (raw ~ /^ *$/ || line_indent > 8) {
|
||||
next
|
||||
}
|
||||
run_block = 0
|
||||
}
|
||||
|
||||
if (raw ~ /^ *#/) {
|
||||
next
|
||||
}
|
||||
if (raw ~ /^ steps:/ || raw ~ /^ "steps":/ \
|
||||
|| substr(raw, 1, 11) == " " single_quote "steps" single_quote ":") {
|
||||
if (raw != " steps:") {
|
||||
grammar_error("steps container must use a canonical block sequence")
|
||||
next
|
||||
}
|
||||
steps_count++
|
||||
if (steps_count != 1) {
|
||||
grammar_error("must contain exactly one canonical steps block")
|
||||
}
|
||||
in_steps = 1
|
||||
reset_step()
|
||||
next
|
||||
}
|
||||
if (in_steps != 0 && line_indent == 4) {
|
||||
in_steps = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
if (raw ~ /^ *<<:/) {
|
||||
grammar_error("contains a forbidden merge key")
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && raw ~ /^ - /) {
|
||||
reset_step()
|
||||
step_active = 1
|
||||
content = substr(raw, 9)
|
||||
validate_step_field(content)
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && raw ~ /^ -[[:space:]]*$/) {
|
||||
grammar_error("contains unsupported empty step syntax")
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && step_active != 0 && line_indent == 8) {
|
||||
content = substr(raw, 9)
|
||||
validate_step_field(content)
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && line_indent == 6 && raw !~ /^ *$/) {
|
||||
grammar_error("contains unsupported step-list syntax")
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
validate_job_shape()
|
||||
if (jobs_count != 1) {
|
||||
workflow_grammar_error("workflow must contain exactly one canonical jobs block")
|
||||
}
|
||||
if (invalid) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "${workflow}"; then
|
||||
fail "workflow structural validation failed: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
fi
|
||||
|
||||
if ! grep -Fq -- './gradlew' "${workflow}" \
|
||||
&& ! grep -Fq -- 'gradle/actions/dependency-submission@' "${workflow}"; then
|
||||
continue
|
||||
fi
|
||||
((workflow_count += 1))
|
||||
|
||||
if ! jobs_in_workflow=$(
|
||||
awk -v workflow="${workflow#"${REPOSITORY_ROOT}"/}" \
|
||||
-v validation_action="${EXPECTED_VALIDATION_ACTION}" \
|
||||
-v dependency_action="${EXPECTED_DEPENDENCY_SUBMISSION_ACTION}" \
|
||||
-v guarded_gradle_if="${EXPECTED_GUARDED_GRADLE_IF}" '
|
||||
function reset_step(known_field) {
|
||||
step_active = 0
|
||||
run_block = 0
|
||||
step_kind = ""
|
||||
step_name = ""
|
||||
step_id = ""
|
||||
step_uses = ""
|
||||
step_uses_action = ""
|
||||
step_if = ""
|
||||
step_if_present = 0
|
||||
step_continue_on_error = 0
|
||||
step_gradle = 0
|
||||
step_gradle_line = 0
|
||||
step_unsupported_gradle = 0
|
||||
step_field_count = 0
|
||||
step_name_line = 0
|
||||
step_id_line = 0
|
||||
step_uses_line = 0
|
||||
step_extra_field = ""
|
||||
for (known_field in step_fields) {
|
||||
delete step_fields[known_field]
|
||||
delete step_field_raw[known_field]
|
||||
}
|
||||
}
|
||||
|
||||
function reset_job() {
|
||||
job = ""
|
||||
checkout_line = 0
|
||||
validation_line = 0
|
||||
gradle_line = 0
|
||||
in_steps = 0
|
||||
unsupported_gradle = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
function indentation(line, first_non_space) {
|
||||
if (line ~ /^ *$/) {
|
||||
return length(line)
|
||||
}
|
||||
first_non_space = match(line, /[^ ]/)
|
||||
return first_non_space - 1
|
||||
}
|
||||
|
||||
function has_gradle_reference(line) {
|
||||
return index(line, "./gradlew") != 0 \
|
||||
|| index(line, "gradle/actions/dependency-submission@") != 0
|
||||
}
|
||||
|
||||
function trim(value) {
|
||||
sub(/^[[:space:]]+/, "", value)
|
||||
sub(/[[:space:]]+$/, "", value)
|
||||
return value
|
||||
}
|
||||
|
||||
function normalize_action(value, scalar, first, quote, closing, index_value) {
|
||||
scalar = trim(value)
|
||||
first = substr(scalar, 1, 1)
|
||||
if (first == "\"" || first == single_quote) {
|
||||
quote = first
|
||||
closing = index(substr(scalar, 2), quote)
|
||||
if (closing == 0) {
|
||||
return ""
|
||||
}
|
||||
return substr(scalar, 2, closing - 1)
|
||||
}
|
||||
sub(/[[:space:]]+#.*$/, "", scalar)
|
||||
return trim(scalar)
|
||||
}
|
||||
|
||||
function record_gradle(line_number) {
|
||||
step_gradle = 1
|
||||
if (step_gradle_line == 0) {
|
||||
step_gradle_line = line_number
|
||||
}
|
||||
if (gradle_line == 0) {
|
||||
gradle_line = line_number
|
||||
}
|
||||
}
|
||||
|
||||
function record_uses(value, line_number, action) {
|
||||
if (step_kind == "run") {
|
||||
if (index(value, "gradle/actions/dependency-submission@") != 0) {
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
step_kind = "uses"
|
||||
action = normalize_action(value)
|
||||
step_uses = trim(value)
|
||||
step_uses_action = action
|
||||
step_uses_line = line_number
|
||||
if (checkout_line == 0 && action ~ /^actions\/checkout@/) {
|
||||
checkout_line = line_number
|
||||
}
|
||||
if (action == dependency_action) {
|
||||
record_gradle(line_number)
|
||||
} else if (index(action, "gradle/actions/dependency-submission@") != 0) {
|
||||
record_gradle(line_number)
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
}
|
||||
|
||||
function record_run(value, line_number) {
|
||||
if (step_kind == "uses") {
|
||||
if (index(value, "./gradlew") != 0) {
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
return
|
||||
}
|
||||
step_kind = "run"
|
||||
if (value ~ /^(\||>)[+-]?([[:space:]]+#.*)?$/) {
|
||||
run_block = 1
|
||||
} else if (index(value, "./gradlew") != 0) {
|
||||
record_gradle(line_number)
|
||||
}
|
||||
}
|
||||
|
||||
function record_step_field(content, line_number, separator, field, value) {
|
||||
separator = index(content, ":")
|
||||
field = substr(content, 1, separator - 1)
|
||||
value = substr(content, separator + 1)
|
||||
sub(/^[[:space:]]*/, "", value)
|
||||
step_fields[field] = 1
|
||||
step_field_raw[field] = trim(content)
|
||||
step_field_count++
|
||||
|
||||
if (field == "name") {
|
||||
step_name = trim(value)
|
||||
step_name_line = line_number
|
||||
} else if (field == "id") {
|
||||
step_id = trim(value)
|
||||
step_id_line = line_number
|
||||
} else if (field == "uses") {
|
||||
record_uses(value, line_number)
|
||||
} else if (field == "run") {
|
||||
record_run(trim(value), line_number)
|
||||
} else if (field == "if") {
|
||||
step_if_present = 1
|
||||
step_if = trim(value)
|
||||
} else if (field == "continue-on-error") {
|
||||
step_continue_on_error = 1
|
||||
}
|
||||
|
||||
if (field != "name" && field != "id" && field != "uses" && step_extra_field == "") {
|
||||
step_extra_field = step_field_raw[field]
|
||||
}
|
||||
}
|
||||
|
||||
function validate_wrapper_step() {
|
||||
if (step_uses_action != validation_reference) {
|
||||
return
|
||||
}
|
||||
if (step_extra_field != "") {
|
||||
printf "%s: job %s wrapper validation step contains unsupported field: %s\n", workflow, job, step_extra_field > "/dev/stderr"
|
||||
invalid = 1
|
||||
return
|
||||
}
|
||||
if (step_field_count != 3 \
|
||||
|| step_name != "Validate Gradle wrapper" \
|
||||
|| step_id != "gradle-wrapper-validation" \
|
||||
|| step_uses != validation_action \
|
||||
|| !(step_name_line < step_id_line && step_id_line < step_uses_line)) {
|
||||
printf "%s: job %s wrapper validation step must contain exact name, id, and uses fields only\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
return
|
||||
}
|
||||
if (validation_line == 0) {
|
||||
validation_line = step_uses_line
|
||||
}
|
||||
}
|
||||
|
||||
function validate_gradle_step() {
|
||||
if (step_gradle == 0 && step_unsupported_gradle == 0) {
|
||||
return
|
||||
}
|
||||
if (step_unsupported_gradle != 0 || ("uses" in step_fields && "run" in step_fields)) {
|
||||
unsupported_gradle = 1
|
||||
}
|
||||
if (step_if_present != 0 && step_if != guarded_gradle_if) {
|
||||
printf "%s: job %s has Gradle step with unsupported if condition: %s\n", workflow, job, step_if > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
if (step_continue_on_error != 0) {
|
||||
printf "%s: job %s has Gradle step with unsupported field: %s\n", workflow, job, step_field_raw["continue-on-error"] > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
}
|
||||
|
||||
function finalize_step() {
|
||||
if (step_active == 0) {
|
||||
return
|
||||
}
|
||||
validate_wrapper_step()
|
||||
validate_gradle_step()
|
||||
}
|
||||
|
||||
function start_step() {
|
||||
finalize_step()
|
||||
reset_step()
|
||||
step_active = 1
|
||||
}
|
||||
|
||||
function validate_job() {
|
||||
finalize_step()
|
||||
if (job == "" || (gradle_line == 0 && unsupported_gradle == 0)) {
|
||||
return
|
||||
}
|
||||
gradle_jobs++
|
||||
if (unsupported_gradle != 0) {
|
||||
printf "%s: job %s uses a Gradle invocation outside the canonical workflow structure\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
if (gradle_line == 0) {
|
||||
return
|
||||
} else if (checkout_line == 0) {
|
||||
printf "%s: job %s invokes Gradle without checkout\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
} else if (validation_line == 0) {
|
||||
printf "%s: job %s invokes Gradle without the exact pinned wrapper validation action\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
} else if (!(checkout_line < validation_line && validation_line < gradle_line)) {
|
||||
printf "%s: job %s must order checkout, exact wrapper validation, then Gradle\n", workflow, job > "/dev/stderr"
|
||||
invalid = 1
|
||||
}
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
in_jobs = 0
|
||||
invalid = 0
|
||||
gradle_jobs = 0
|
||||
single_quote = sprintf("%c", 39)
|
||||
validation_reference = validation_action
|
||||
sub(/[[:space:]]+#.*$/, "", validation_reference)
|
||||
reset_job()
|
||||
}
|
||||
|
||||
/^jobs:[[:space:]]*(#.*)?$/ {
|
||||
in_jobs = 1
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && /^[^[:space:]#]/ {
|
||||
validate_job()
|
||||
reset_job()
|
||||
in_jobs = 0
|
||||
}
|
||||
|
||||
in_jobs && /^ [A-Za-z0-9_.-]+:[[:space:]]*(#.*)?$/ {
|
||||
validate_job()
|
||||
reset_job()
|
||||
job = $0
|
||||
sub(/^ /, "", job)
|
||||
sub(/:.*/, "", job)
|
||||
next
|
||||
}
|
||||
|
||||
in_jobs && job != "" {
|
||||
raw = $0
|
||||
line_indent = indentation(raw)
|
||||
|
||||
if (run_block != 0) {
|
||||
if (raw ~ /^ *$/) {
|
||||
next
|
||||
}
|
||||
if (line_indent > 8) {
|
||||
if (index(raw, "./gradlew") != 0) {
|
||||
record_gradle(NR)
|
||||
}
|
||||
if (index(raw, "gradle/actions/dependency-submission@") != 0) {
|
||||
step_unsupported_gradle = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
run_block = 0
|
||||
}
|
||||
|
||||
if (raw ~ /^ *#/) {
|
||||
next
|
||||
}
|
||||
|
||||
if (raw == " steps:") {
|
||||
in_steps = 1
|
||||
reset_step()
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && line_indent == 4) {
|
||||
finalize_step()
|
||||
in_steps = 0
|
||||
reset_step()
|
||||
}
|
||||
|
||||
if (in_steps != 0 && raw ~ /^ - /) {
|
||||
start_step()
|
||||
content = substr(raw, 9)
|
||||
record_step_field(content, NR)
|
||||
next
|
||||
}
|
||||
|
||||
if (in_steps != 0 && step_active != 0 && line_indent == 8) {
|
||||
content = substr(raw, 9)
|
||||
record_step_field(content, NR)
|
||||
next
|
||||
}
|
||||
|
||||
if (has_gradle_reference(raw)) {
|
||||
unsupported_gradle = 1
|
||||
}
|
||||
}
|
||||
|
||||
END {
|
||||
validate_job()
|
||||
print gradle_jobs
|
||||
if (invalid) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "${workflow}"
|
||||
); then
|
||||
fail "workflow validation failed: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
fi
|
||||
[[ "${jobs_in_workflow}" =~ ^[0-9]+$ ]] \
|
||||
|| fail "workflow parser returned an invalid Gradle job count: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
((jobs_in_workflow > 0)) \
|
||||
|| fail "Gradle-running workflow contains no detected Gradle job: ${workflow#"${REPOSITORY_ROOT}"/}"
|
||||
((gradle_job_count += jobs_in_workflow))
|
||||
done < <(find "${WORKFLOWS_DIRECTORY}" -type f \( -name '*.yml' -o -name '*.yaml' \) -print0)
|
||||
|
||||
((workflow_count > 0)) || fail 'no Gradle-running workflow was found'
|
||||
((gradle_job_count > 0)) || fail 'no individual Gradle-running job was found'
|
||||
((workflow_lock_valid != 0)) \
|
||||
|| fail 'workflow lock mismatch: workflow set or bytes differ from the reviewed embedded manifest'
|
||||
|
||||
printf 'gradle-wrapper-contract: PASS\n'
|
||||
@@ -0,0 +1,197 @@
|
||||
name: ci-quality-gates
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: ["main"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TESTCONTAINERS_REUSE_ENABLE: "false"
|
||||
|
||||
jobs:
|
||||
quality-gates:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- name: Require the committed public-path security baseline
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readonly snapshot='docs/security/public-paths-snapshot.txt'
|
||||
if [[ ! -s "${snapshot}" ]]; then
|
||||
echo "::error::${snapshot} is missing or empty. CI must not let verifyPublicPathSnapshot create its own first-run baseline."
|
||||
exit 1
|
||||
fi
|
||||
if ! git ls-files --error-unmatch "${snapshot}" >/dev/null 2>&1; then
|
||||
echo "::error::${snapshot} exists locally but is not committed."
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Check quality, public paths, and dependency locks
|
||||
working-directory: src
|
||||
run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace
|
||||
- name: Qualify opt-in inbound transports without skips
|
||||
working-directory: src
|
||||
run: ./gradlew conditionalTransportQualification --no-daemon --stacktrace
|
||||
|
||||
sample-off:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Verify the application without the sample fixture
|
||||
working-directory: src
|
||||
run: ./gradlew :app-bootstrap:sampleOffTest verifyCleanArchitectureDependencies --no-daemon --stacktrace
|
||||
|
||||
gate-matrix-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Verify the gate matrix against the repository
|
||||
run: bash .github/scripts/verify-gate-matrix.sh
|
||||
|
||||
redis-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
# Milestone A of the Redis wrapper/typed API plan: policy catalog, typed API parity,
|
||||
# permit provenance, connection isolation, and the executor guard. There is no real-server
|
||||
# lane yet — Tasks 10-17 add the contract suites that need one.
|
||||
- name: Verify the Redis SDK policy, API parity, and guardrail contracts
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:shared-contract:edgeRateLimitContractTest
|
||||
:adapter:outbound:cache-redis:check
|
||||
verifyCleanArchitectureDependencies
|
||||
verifyEnvKeys
|
||||
verifyPublicPathSnapshot
|
||||
verifyConfigurationPropertiesProcessor
|
||||
--no-daemon --stacktrace
|
||||
|
||||
jpa-candidate-evidence:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Produce zero-skip JPA candidate manifests
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
- name: Retain content-addressed JPA candidate manifests
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7.0.1
|
||||
with:
|
||||
name: jpa-candidate-evidence-${{ github.sha }}
|
||||
path: src/adapter/outbound/persistence-jpa/build/jpa-evidence/manifests
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
# Advisory only. Quarantine expiry/drift remains blocking through verifyQuarantineSunset in check.
|
||||
quarantine:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run quarantined tests as an advisory signal
|
||||
working-directory: src
|
||||
run: ./gradlew quarantineTest --no-daemon
|
||||
|
||||
release-gate:
|
||||
needs:
|
||||
- quality-gates
|
||||
- sample-off
|
||||
- gate-matrix-lint
|
||||
- redis-sdk
|
||||
- jpa-candidate-evidence
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require every current blocking job to succeed
|
||||
env:
|
||||
QUALITY_RESULT: ${{ needs.quality-gates.result }}
|
||||
SAMPLE_OFF_RESULT: ${{ needs.sample-off.result }}
|
||||
MATRIX_RESULT: ${{ needs.gate-matrix-lint.result }}
|
||||
REDIS_RESULT: ${{ needs.redis-sdk.result }}
|
||||
JPA_CANDIDATE_RESULT: ${{ needs.jpa-candidate-evidence.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for result in \
|
||||
"${QUALITY_RESULT}" \
|
||||
"${SAMPLE_OFF_RESULT}" \
|
||||
"${MATRIX_RESULT}" \
|
||||
"${REDIS_RESULT}" \
|
||||
"${JPA_CANDIDATE_RESULT}"; do
|
||||
if [[ "${result}" != "success" ]]; then
|
||||
echo "::error::release-gate: required job result was ${result}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "release-gate: all current blocking quality jobs succeeded."
|
||||
@@ -0,0 +1,188 @@
|
||||
name: dependency-vulnerability
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# The compare API exists only on GitHub.com. Trivy remains the full-snapshot backstop elsewhere.
|
||||
dependency-review:
|
||||
if: github.event_name == 'pull_request' && github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Review newly introduced dependencies
|
||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # actions/dependency-review-action@v4.8.0
|
||||
with:
|
||||
config-file: ./.github/dependency-review-config.yml
|
||||
|
||||
# The submission API is also GitHub.com-only and is not required for the platform-neutral scan.
|
||||
dependency-submission:
|
||||
if: github.event_name == 'push' && github.server_url == 'https://github.com'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Submit the resolved Gradle dependency graph
|
||||
uses: gradle/actions/dependency-submission@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # gradle/actions@v4.4.4
|
||||
with:
|
||||
build-root-directory: src
|
||||
|
||||
trivy-fs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Install pinned Trivy under RUNNER_TEMP
|
||||
env:
|
||||
TRIVY_DOWNLOAD_BASE_URL: ${{ vars.TRIVY_DOWNLOAD_BASE_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readonly TRIVY_VERSION='0.71.2'
|
||||
readonly TRIVY_SHA256_AMD64='0510e71e2fd39bf863856d499c8dc19feb4e7336546394c502a8f5cc7ab27460'
|
||||
readonly TRIVY_SHA256_ARM64='fe1c7106e15a5365d485b098a8c338f91e3b7ba71cb0e4963b98a3a098763cfc'
|
||||
readonly DOWNLOAD_BASE_URL="${TRIVY_DOWNLOAD_BASE_URL:-https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}}"
|
||||
case "${RUNNER_ARCH:-X64}" in
|
||||
X64)
|
||||
asset_arch='64bit'
|
||||
expected_sha256="${TRIVY_SHA256_AMD64}"
|
||||
;;
|
||||
ARM64)
|
||||
asset_arch='ARM64'
|
||||
expected_sha256="${TRIVY_SHA256_ARM64}"
|
||||
;;
|
||||
*)
|
||||
echo "::error::unsupported runner architecture: ${RUNNER_ARCH:-unknown}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
install_dir="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}"
|
||||
archive="${RUNNER_TEMP}/trivy-${TRIVY_VERSION}.tar.gz"
|
||||
mkdir -p "${install_dir}"
|
||||
curl --fail --show-error --silent --location --retry 3 \
|
||||
--proto '=https' --tlsv1.2 \
|
||||
"${DOWNLOAD_BASE_URL}/trivy_${TRIVY_VERSION}_Linux-${asset_arch}.tar.gz" \
|
||||
--output "${archive}"
|
||||
printf '%s %s\n' "${expected_sha256}" "${archive}" | sha256sum -c -
|
||||
tar -xzf "${archive}" -C "${install_dir}" trivy
|
||||
chmod 0755 "${install_dir}/trivy"
|
||||
printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}"
|
||||
- name: Install checksum-pinned jq
|
||||
env:
|
||||
JQ_DOWNLOAD_BASE_URL: ${{ vars.JQ_DOWNLOAD_BASE_URL }}
|
||||
run: bash .github/scripts/install-jq.sh
|
||||
|
||||
- name: Block High and Critical vulnerabilities
|
||||
run: |
|
||||
trivy fs \
|
||||
--scanners vuln,license \
|
||||
--severity CRITICAL,HIGH \
|
||||
--exit-code 1 \
|
||||
--ignorefile .trivyignore.yaml \
|
||||
.
|
||||
|
||||
- name: Report Medium and Low vulnerabilities
|
||||
run: |
|
||||
trivy fs \
|
||||
--scanners vuln,license \
|
||||
--severity MEDIUM,LOW \
|
||||
--exit-code 0 \
|
||||
--ignorefile .trivyignore.yaml \
|
||||
.
|
||||
|
||||
- name: Produce the governed all-severity KEV input
|
||||
run: |
|
||||
trivy fs \
|
||||
--scanners vuln \
|
||||
--severity CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN \
|
||||
--exit-code 0 \
|
||||
--ignorefile .trivyignore.yaml \
|
||||
--format json \
|
||||
--output trivy-kev.json \
|
||||
.
|
||||
|
||||
- name: Fail closed on any CISA KEV match
|
||||
env:
|
||||
CONFIGURED_KEV_FEED_URL: ${{ vars.KEV_FEED_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
readonly DEFAULT_KEV_FEED_URL='https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json'
|
||||
readonly KEV_FEED_URL="${CONFIGURED_KEV_FEED_URL:-${DEFAULT_KEV_FEED_URL}}"
|
||||
if ! curl --fail --show-error --silent --location --retry 3 \
|
||||
--proto '=https' --tlsv1.2 "${KEV_FEED_URL}" --output kev.json; then
|
||||
echo "::error::KEV feed unavailable; configure KEV_FEED_URL to an approved internal mirror"
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '
|
||||
(.catalogVersion | type == "string" and test("\\S"))
|
||||
and (.dateReleased | type == "string" and test("\\S"))
|
||||
and (.count | type == "number")
|
||||
and (.count > 0)
|
||||
and (.count == (.count | floor))
|
||||
and (.vulnerabilities | type == "array")
|
||||
and ((.vulnerabilities | length) > 0)
|
||||
and (.count == (.vulnerabilities | length))
|
||||
and (all(
|
||||
.vulnerabilities[];
|
||||
(.cveID | type == "string" and test("^CVE-[0-9]{4}-[0-9]{4,}$"))
|
||||
))
|
||||
and (([.vulnerabilities[].cveID] | unique | length) == .count)
|
||||
' kev.json >/dev/null; then
|
||||
echo "::error::KEV feed is malformed, empty, count-inconsistent, or contains invalid/duplicate cveID values"
|
||||
exit 1
|
||||
fi
|
||||
if ! jq -e '
|
||||
(type == "object")
|
||||
and (.Results | type == "array")
|
||||
and ((.Results | length) > 0)
|
||||
and (all(.Results[]; type == "object"))
|
||||
and (all(
|
||||
.Results[];
|
||||
(.Vulnerabilities == null) or (.Vulnerabilities | type == "array")
|
||||
))
|
||||
and (all(
|
||||
.Results[];
|
||||
all(
|
||||
.Vulnerabilities[]?;
|
||||
(type == "object")
|
||||
and (.VulnerabilityID | type == "string" and test("\\S"))
|
||||
)
|
||||
))
|
||||
' trivy-kev.json >/dev/null; then
|
||||
echo "::error::Trivy KEV input is malformed, empty, or contains an invalid VulnerabilityID"
|
||||
exit 1
|
||||
fi
|
||||
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID | select(type == "string")] | unique[]?' \
|
||||
trivy-kev.json | sort -u > found-cves.txt
|
||||
jq -r '.vulnerabilities[]?.cveID | select(type == "string")' \
|
||||
kev.json | sort -u > kev-cves.txt
|
||||
hits="$(comm -12 found-cves.txt kev-cves.txt || true)"
|
||||
if [[ -n "${hits}" ]]; then
|
||||
echo "::error::CISA KEV-listed vulnerability found regardless of CVSS:"
|
||||
printf '%s\n' "${hits}"
|
||||
exit 1
|
||||
fi
|
||||
echo "KEV cross-check: no catalog match."
|
||||
@@ -0,0 +1,132 @@
|
||||
name: fileserver-nightly
|
||||
|
||||
# The environments that cannot run on every pull request: a real network filesystem, a foreign
|
||||
# filesystem, and the long-running fault matrices. They are nightly rather than skipped because a
|
||||
# green pull-request run is not certification of any of them.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 18 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
fileserver-nfs-ambiguity:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
FILESERVER_NFS_TESTS: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Start the NFSv4 certification environment
|
||||
run: docker compose -f infra/fileserver/nfs/compose.yml up -d --wait
|
||||
- name: Run the network-filesystem ambiguity suite
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:fileserver:test --tests '*NfsAmbiguityIntegrationTest'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
- name: Tear down the NFS environment
|
||||
if: always()
|
||||
run: docker compose -f infra/fileserver/nfs/compose.yml down -v
|
||||
|
||||
fileserver-process-kill-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the crash matrix and reconciliation suites
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:fileserver:test --tests '*CrashRecoveryMatrixTest'
|
||||
:application-core:test --tests '*FileReconciliationServiceTest'
|
||||
--rerun-tasks
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-large-file-performance:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the large-file and slow-client suites under a constrained heap
|
||||
working-directory: src
|
||||
env:
|
||||
GRADLE_OPTS: -Xmx512m
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:fileserver:test --tests '*LargeFileBoundedMemoryTest'
|
||||
:adapter:outbound:fileserver:test --tests '*LocalAppendMemoryTest'
|
||||
--rerun-tasks
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-multi-instance-lease:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Prove no run commits bytes from a stale lease
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:application-core:test --tests '*MultiInstanceWriterLeaseTest'
|
||||
--rerun-tasks
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
@@ -0,0 +1,164 @@
|
||||
name: fileserver-pr
|
||||
|
||||
# Every claim in docs/fileserver/support-matrix.md that says "Stable" is backed by a job here.
|
||||
# A support level with no job behind it is a marketing claim, not an engineering one, and
|
||||
# DocumentationCoverageTest fails the build when the two drift apart.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/application-core/src/**/fileserver/**'
|
||||
- 'src/adapter/inbound/web/src/**/fileserver/**'
|
||||
- 'src/adapter/outbound/fileserver/**'
|
||||
- 'src/adapter/outbound/persistence-jpa/src/**/fileserver/**'
|
||||
- 'src/app-bootstrap/src/**/fileserver/**'
|
||||
- 'docs/fileserver/**'
|
||||
# The capability is not only its Java files. A change to the bound settings, the shipped
|
||||
# environment, the registry that documents it, or the container that has to give it a
|
||||
# writable volume changes how it behaves at runtime just as surely — and those were the
|
||||
# exact files that could previously ship unverified.
|
||||
- 'src/app-bootstrap/src/main/resources/application.yml'
|
||||
- 'src/.env'
|
||||
- 'docs/registries/env-keys.yaml'
|
||||
- 'src/Dockerfile'
|
||||
- 'docker-compose.yml'
|
||||
- 'infra/nginx/**'
|
||||
- 'infra/k8s/**'
|
||||
- '.github/workflows/fileserver-pr.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fileserver-unit-and-architecture:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the fileserver application and architecture suites
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:application-core:test
|
||||
:app-bootstrap:test --tests '*CleanArchitectureTest' --tests '*Fileserver*'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-local-ext4-contract:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Certify the local content store against the shared contract
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:fileserver:test
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-http-contract:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the servlet and reactive transport contracts
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:inbound:web:test
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-security-suite:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the path, filename, range, and problem-detail hardening suite
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:inbound:web:test --tests '*FileserverHardeningContractTest'
|
||||
:adapter:outbound:fileserver:test --tests '*PhysicalPathResolverTest'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-bounded-memory:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Prove transfer cost does not scale with file size
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:fileserver:test --tests '*LargeFileBoundedMemoryTest'
|
||||
:adapter:inbound:web:test --tests '*DataBufferReleaseTest'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
@@ -0,0 +1,143 @@
|
||||
name: fileserver-release
|
||||
|
||||
# The gate a release must clear. Its job list is deliberately the same shape as the support matrix:
|
||||
# nothing may be advertised at a support level whose evidence job is absent here.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
fileserver-full-verification:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the architecture-wide dependency and module verification
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
verifyCleanArchitectureDependencies
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
- name: Run the complete fileserver suite across every leaf
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:application-core:check
|
||||
:adapter:inbound:web:check
|
||||
:adapter:outbound:fileserver:check
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-documentation-gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Prove every support claim maps to a job and every endpoint is documented
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:app-bootstrap:test --tests '*FileserverDocumentationCoverageTest'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
fileserver-pvc-certification:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
# Two different things, kept apart on purpose. The manifest checks below run everywhere and
|
||||
# fail on real drift; the cluster run needs a cluster and is skipped without one. The job
|
||||
# used to `test -f` the manifest and report success, which read as "ReadWriteOnce certified"
|
||||
# when nothing had been applied anywhere.
|
||||
- name: Check the certification manifest still says what the claim depends on
|
||||
run: |
|
||||
set -euo pipefail
|
||||
manifest=infra/fileserver/kubernetes/pvc-certification-job.yaml
|
||||
test -f "$manifest"
|
||||
grep -q 'kind: PersistentVolumeClaim' "$manifest"
|
||||
grep -q 'kind: Job' "$manifest"
|
||||
# ReadWriteMany is explicitly not claimed; a manifest that quietly widened the access
|
||||
# mode would certify a topology the support matrix says is uncertified.
|
||||
grep -q 'ReadWriteOnce' "$manifest"
|
||||
! grep -q 'ReadWriteMany' "$manifest"
|
||||
- name: Certify the ReadWriteOnce claim on the release cluster
|
||||
id: pvc-cluster-run
|
||||
env:
|
||||
KUBECONFIG_CONTENT: ${{ secrets.FILESERVER_PVC_KUBECONFIG }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${KUBECONFIG_CONTENT:-}" ]; then
|
||||
echo "::warning::no release cluster configured; PVC certification was NOT run."
|
||||
echo "The support matrix records this profile as Limited for exactly this reason:"
|
||||
echo "the cluster result is produced by an operator against a real cluster and read"
|
||||
echo "from docs/fileserver/storage-certification.md, not by this job."
|
||||
echo "certified=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
printf '%s' "$KUBECONFIG_CONTENT" > /tmp/kubeconfig
|
||||
export KUBECONFIG=/tmp/kubeconfig
|
||||
kubectl apply -f infra/fileserver/kubernetes/pvc-certification-job.yaml
|
||||
kubectl wait --for=condition=complete --timeout=30m job/fileserver-pvc-certification
|
||||
kubectl logs job/fileserver-pvc-certification
|
||||
echo "certified=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
fileserver-sensitive-telemetry-scan:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Prove telemetry carries no filename, path, or raw identifier
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:application-core:test --tests '*FileserverObservabilityTest'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
@@ -0,0 +1,132 @@
|
||||
name: httpclient-contract
|
||||
|
||||
# Per-PR gate for the HTTP Client Platform (design §29). Each transport runs the same semantic
|
||||
# contract in its own job, so a transport that stops satisfying it fails on its own row instead of
|
||||
# disappearing into an aggregate run.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/adapter/outbound/httpclient/**'
|
||||
- 'src/app-bootstrap/src/**/httpclient/**'
|
||||
- 'docs/httpclient/**'
|
||||
- 'scripts/verify-httpclient-docs.py'
|
||||
- '.github/workflows/httpclient-contract.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
httpclient-unit-and-boundaries:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the focused module suite and the architecture gate
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:httpclient:test
|
||||
verifyCleanArchitectureDependencies
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
httpclient-stable-contract:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
transport: [apache, jdk, reactor]
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Certify one transport against the shared contract
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:httpclient:httpClientStableContractTest
|
||||
-Phttpclient.contract.transports=${{ matrix.transport }}
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
httpclient-security-and-compatibility:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run the SSRF, cardinality, and Spring compatibility lanes
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:httpclient:httpClientSecurityTest
|
||||
:adapter:outbound:httpclient:httpClientBlockHoundTest
|
||||
:adapter:outbound:httpclient:spring62ApiSurfaceScan
|
||||
:adapter:outbound:httpclient:spring70CompatibilityTest
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
httpclient-composition:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Verify composition and architecture in the bootstrap module
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:app-bootstrap:test --tests '*httpclient*' --tests '*CleanArchitectureTest'
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
@@ -0,0 +1,94 @@
|
||||
name: httpclient-nightly
|
||||
|
||||
# Lanes that need a container runtime, real time, or a QUIC-capable host (design §29). They are
|
||||
# separated from the per-PR gate rather than made optional inside it: a lane that cannot run here
|
||||
# fails, it does not skip.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
httpclient-fault-injection:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Inject TCP faults against a real upstream
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:httpclient:httpClientFailureInjectionTest
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
httpclient-performance:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Certify pool, streaming, retry, and rotation bounds
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:httpclient:httpClientPerformanceTest
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
|
||||
httpclient-http3-experimental:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
# Experimental by design (D-08): the result is reported, never used to block a merge.
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Exercise the experimental HTTP/3 opt-in
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:httpclient:test
|
||||
-Phttp3.tests.enabled=true
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
@@ -0,0 +1,70 @@
|
||||
name: httpclient-release
|
||||
|
||||
# Release gate for the HTTP Client Platform (design §38 step 4). Each declared gate runs as its own
|
||||
# single-line `./gradlew <task>` step, because .github/scripts/verify-gate-matrix.sh reads these
|
||||
# commands to prove the gate is actually executed — a folded or flag-laden command would make the
|
||||
# declaration in .github/ci-gate-matrix.yml unverifiable.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release-gate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
working-directory: src
|
||||
env:
|
||||
# A project property rather than a command-line flag, so each run command stays a plain,
|
||||
# verifiable task invocation while the machine-dependent bounds are still asserted.
|
||||
GRADLE_OPTS: -Dorg.gradle.project.performance.assertions.enabled=true
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Focused module tests
|
||||
run: ./gradlew :adapter:outbound:httpclient:test --no-daemon --stacktrace
|
||||
- name: Spring 6.2 API surface lane
|
||||
run: ./gradlew :adapter:outbound:httpclient:spring62ApiSurfaceScan --no-daemon --stacktrace
|
||||
- name: Spring 7.0 compatibility lane
|
||||
run: ./gradlew :adapter:outbound:httpclient:spring70CompatibilityTest --no-daemon --stacktrace
|
||||
- name: Stable cross-transport contract suite
|
||||
run: ./gradlew :adapter:outbound:httpclient:httpClientStableContractTest --no-daemon --stacktrace
|
||||
- name: SSRF and cardinality suite
|
||||
run: ./gradlew :adapter:outbound:httpclient:httpClientSecurityTest --no-daemon --stacktrace
|
||||
- name: Event-loop blocking suite
|
||||
run: ./gradlew :adapter:outbound:httpclient:httpClientBlockHoundTest --no-daemon --stacktrace
|
||||
- name: Toxiproxy fault-injection suite
|
||||
run: ./gradlew :adapter:outbound:httpclient:httpClientFailureInjectionTest --no-daemon --stacktrace
|
||||
- name: Resource-bound performance certification
|
||||
run: ./gradlew :adapter:outbound:httpclient:httpClientPerformanceTest --no-daemon --stacktrace
|
||||
- name: Architecture dependency gate
|
||||
run: ./gradlew verifyCleanArchitectureDependencies --no-daemon --stacktrace
|
||||
|
||||
httpclient-documentation:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Verify documentation matches the code
|
||||
run: python3 scripts/verify-httpclient-docs.py
|
||||
@@ -0,0 +1,56 @@
|
||||
name: jpa-r2-evidence
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TESTCONTAINERS_REUSE_ENABLE: "false"
|
||||
|
||||
jobs:
|
||||
jpa-r2-evidence:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
JPA_EVIDENCE_PROFILE: r2
|
||||
JPA_EVIDENCE_CI_JOB: >-
|
||||
actions:${{ github.workflow }}:${{ github.run_id }}:${{ github.job }}
|
||||
JPA_EVIDENCE_ARTIFACT_LOCATION: >-
|
||||
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
JPA_EVIDENCE_TOPOLOGY: postgresql-16-testcontainers-tls-and-fault-matrix
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Verify the production-profile JPA R2 manifest DAG
|
||||
working-directory: src
|
||||
run: >-
|
||||
./gradlew
|
||||
:adapter:outbound:persistence-jpa:verifyJpaPrimaryFoundationEvidence
|
||||
-PjpaEvidenceProfile=r2
|
||||
--no-daemon
|
||||
--stacktrace
|
||||
- name: Retain JPA R2 attempt manifests
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # actions/upload-artifact@v7.0.1
|
||||
with:
|
||||
name: jpa-r2-evidence-${{ github.sha }}-${{ github.run_id }}
|
||||
path: src/adapter/outbound/persistence-jpa/build/jpa-evidence/manifests
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
@@ -0,0 +1,49 @@
|
||||
name: link-check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "README.md"
|
||||
- "src/README.md"
|
||||
- "src/**/README.md"
|
||||
- "src/**/CLAUDE.md"
|
||||
- "docs/**/*.md"
|
||||
- ".github/**/*.md"
|
||||
- ".github/workflows/link-check.yml"
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "README.md"
|
||||
- "src/README.md"
|
||||
- "src/**/README.md"
|
||||
- "src/**/CLAUDE.md"
|
||||
- "docs/**/*.md"
|
||||
- ".github/**/*.md"
|
||||
- ".github/workflows/link-check.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lychee:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Check committed documentation links
|
||||
uses: lycheeverse/lychee-action@7cd0af4c74a61395d455af97419279d86aafaede # lycheeverse/lychee-action@v2.0.2
|
||||
with:
|
||||
args: >-
|
||||
--no-progress
|
||||
--root-dir .
|
||||
README.md
|
||||
src/README.md
|
||||
'src/**/README.md'
|
||||
'src/**/CLAUDE.md'
|
||||
'docs/**/*.md'
|
||||
'.github/**/*.md'
|
||||
fail: true
|
||||
@@ -0,0 +1,108 @@
|
||||
name: object-storage-qualification
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "23 3 * * 2"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
run_protected_aws:
|
||||
description: Run the protected AWS sandbox qualification lane
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
TESTCONTAINERS_REUSE_ENABLE: "false"
|
||||
|
||||
jobs:
|
||||
poster-image-v7-migration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run non-skipping Poster image migration qualification
|
||||
working-directory: src
|
||||
run: ./gradlew :sample-portfolio:posterImageMigrationTest --no-daemon --stacktrace
|
||||
|
||||
minio-managed-contract:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run exact-release MinIO managed contract
|
||||
working-directory: src
|
||||
run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioContractTest --no-daemon --stacktrace
|
||||
|
||||
minio-managed-fault:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run digest-pinned MinIO and Toxiproxy fault contract
|
||||
working-directory: src
|
||||
run: ./gradlew :adapter:outbound:objectstorage:objectStorageMinioFaultTest --no-daemon --stacktrace
|
||||
|
||||
aws-managed-common-subset:
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.run_protected_aws
|
||||
environment: object-storage-aws-qualification
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
OBJECT_STORAGE_AWS_QUALIFICATION_ENABLED: "true"
|
||||
OBJECT_STORAGE_AWS_BUCKET: ${{ secrets.OBJECT_STORAGE_AWS_BUCKET }}
|
||||
OBJECT_STORAGE_AWS_REGION: ${{ secrets.OBJECT_STORAGE_AWS_REGION }}
|
||||
OBJECT_STORAGE_AWS_EXPECTED_OWNER: ${{ secrets.OBJECT_STORAGE_AWS_EXPECTED_OWNER }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Run protected AWS common-subset qualification
|
||||
working-directory: src
|
||||
run: ./gradlew :adapter:outbound:objectstorage:objectStorageAwsQualificationTest --no-daemon --stacktrace
|
||||
@@ -0,0 +1,190 @@
|
||||
# Redis SDK topology evidence.
|
||||
#
|
||||
# The lanes in infra/redis-sdk answer what the deterministic in-memory gateway cannot — Sentinel
|
||||
# promotion behaviour, Cluster redirects, ACL coverage. docs/redis/support-matrix.md records which
|
||||
# lane produced which evidence, and RedisSupportMatrixTest refuses an evidence claim that does not
|
||||
# name the test class behind it.
|
||||
#
|
||||
# Three cadences, because the cost and the question differ:
|
||||
#
|
||||
# pull_request standalone only, current supported version. The cheapest lane that can still
|
||||
# catch "this change cannot talk to a real Redis at all". A PR gate that starts
|
||||
# three topologies is a PR gate people learn to ignore.
|
||||
# schedule the full supported-version x topology matrix, nightly. This is where Sentinel
|
||||
# promotion and Cluster redirect evidence comes from.
|
||||
# workflow_dispatch one lane on demand, for reproducing a specific failure.
|
||||
#
|
||||
# A release candidate uses the nightly matrix run for its tag: `release-candidate` selects the full
|
||||
# matrix on demand so an RC does not have to wait for the next scheduled run.
|
||||
#
|
||||
# Each lane has its own endpoint. A sentinel is not a data node and a cluster node is not the whole
|
||||
# cluster, so the address, port, and (for Sentinel) the monitored primary's name are per-lane rather
|
||||
# than one hardcoded 6379 that happens to be right for standalone only.
|
||||
#
|
||||
# The Gradle task is fail-closed on its own account: an unknown mode, a missing endpoint, a lane
|
||||
# with no tagged test class, and a run that executed zero tests are all errors. This workflow does
|
||||
# not need to re-check those, but it does have to keep the evidence, which is why every run uploads
|
||||
# the JUnit XML together with the commit SHA, the server version and the resolved image digest. An
|
||||
# evidence artifact that cannot say which image produced it is not evidence.
|
||||
name: redis-sdk-topology
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/adapter/outbound/cache-redis/**"
|
||||
- "infra/redis-sdk/**"
|
||||
- ".github/workflows/redis-sdk-topology.yml"
|
||||
schedule:
|
||||
# 02:30 UTC daily. Nightly, not hourly: the matrix starts real servers.
|
||||
- cron: "30 2 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
topology:
|
||||
description: standalone, sentinel, cluster, tls, or release-candidate for the full matrix
|
||||
required: true
|
||||
default: standalone
|
||||
type: choice
|
||||
options: [standalone, sentinel, cluster, tls, release-candidate]
|
||||
redis_version:
|
||||
description: server version tag
|
||||
required: true
|
||||
default: "7.4"
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# The matrix is computed rather than duplicated per trigger, so adding a supported version is one
|
||||
# edit and no trigger can silently keep testing an old set.
|
||||
lanes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.select.outputs.matrix }}
|
||||
steps:
|
||||
- id: select
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${{ github.event_name }}" in
|
||||
pull_request)
|
||||
matrix='{"include":[{"topology":"standalone","redis_version":"7.4"}]}'
|
||||
;;
|
||||
schedule)
|
||||
matrix='{"include":[
|
||||
{"topology":"standalone","redis_version":"7.2"},
|
||||
{"topology":"standalone","redis_version":"7.4"},
|
||||
{"topology":"standalone","redis_version":"8.2"},
|
||||
{"topology":"sentinel","redis_version":"7.2"},
|
||||
{"topology":"sentinel","redis_version":"7.4"},
|
||||
{"topology":"sentinel","redis_version":"8.2"},
|
||||
{"topology":"cluster","redis_version":"7.2"},
|
||||
{"topology":"cluster","redis_version":"7.4"},
|
||||
{"topology":"cluster","redis_version":"8.2"},
|
||||
{"topology":"tls","redis_version":"7.4"},
|
||||
{"topology":"tls","redis_version":"8.2"}]}'
|
||||
;;
|
||||
*)
|
||||
if [ "${{ inputs.topology }}" = "release-candidate" ]; then
|
||||
matrix='{"include":[
|
||||
{"topology":"standalone","redis_version":"7.2"},
|
||||
{"topology":"standalone","redis_version":"7.4"},
|
||||
{"topology":"standalone","redis_version":"8.2"},
|
||||
{"topology":"sentinel","redis_version":"7.2"},
|
||||
{"topology":"sentinel","redis_version":"7.4"},
|
||||
{"topology":"sentinel","redis_version":"8.2"},
|
||||
{"topology":"cluster","redis_version":"7.2"},
|
||||
{"topology":"cluster","redis_version":"7.4"},
|
||||
{"topology":"cluster","redis_version":"8.2"},
|
||||
{"topology":"tls","redis_version":"7.4"},
|
||||
{"topology":"tls","redis_version":"8.2"}]}'
|
||||
else
|
||||
matrix='{"include":[{"topology":"${{ inputs.topology }}","redis_version":"${{ inputs.redis_version }}"}]}'
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
printf 'matrix=%s\n' "$(printf '%s' "$matrix" | tr -d '\n ')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
topology-evidence:
|
||||
needs: lanes
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.lanes.outputs.matrix) }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # actions/checkout@v4.2.2
|
||||
- name: Validate Gradle wrapper
|
||||
id: gradle-wrapper-validation
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # gradle/actions@v6
|
||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # actions/setup-java@v4.7.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21.0.11+10"
|
||||
cache: gradle
|
||||
cache-dependency-path: |
|
||||
src/**/*.gradle
|
||||
src/**/gradle-wrapper.properties
|
||||
src/**/gradle.lockfile
|
||||
- name: Start the topology
|
||||
env:
|
||||
REDIS_VERSION: ${{ matrix.redis_version }}
|
||||
run: docker compose -f "infra/redis-sdk/${{ matrix.topology }}/compose.yml" up -d --wait
|
||||
- name: Record the image digest
|
||||
id: image
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The tag says 7.4; the digest says which 7.4. Evidence that names only the tag cannot be
|
||||
# reproduced once the tag moves.
|
||||
digest="$(docker image inspect --format '{{index .RepoDigests 0}}' \
|
||||
"redis:${{ matrix.redis_version }}" 2>/dev/null || echo 'unresolved')"
|
||||
printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT"
|
||||
- name: Run the topology contracts
|
||||
working-directory: src
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case '${{ matrix.topology }}' in
|
||||
standalone) port=6379; extra='' ;;
|
||||
sentinel) port=27010; extra='-Predis.topology.master=skeleton' ;;
|
||||
cluster) port=7100; extra='' ;;
|
||||
# The TLS lane's CA is generated at start-up, so the trust material is extracted from
|
||||
# the lane rather than checked in. A checked-in key is a secret in the repository
|
||||
# however loudly the file is named "test".
|
||||
tls)
|
||||
port=6390
|
||||
docker compose -f ../infra/redis-sdk/tls/compose.yml cp redis:/tls/ca.crt "$RUNNER_TEMP/redis-lane-ca.pem"
|
||||
extra="-Predis.topology.trust-material=$RUNNER_TEMP/redis-lane-ca.pem"
|
||||
;;
|
||||
*) echo "unknown topology"; exit 1 ;;
|
||||
esac
|
||||
./gradlew :adapter:outbound:cache-redis:redisTopologyTest --console=plain \
|
||||
-Predis.topology.host=localhost \
|
||||
-Predis.topology.port="$port" \
|
||||
-Predis.topology.mode='${{ matrix.topology }}' \
|
||||
$extra
|
||||
- name: Write the evidence manifest
|
||||
if: always()
|
||||
run: |
|
||||
set -euo pipefail
|
||||
out=src/adapter/outbound/cache-redis/build/test-results/redisTopologyTest
|
||||
mkdir -p "$out"
|
||||
cat > "$out/evidence-manifest.txt" <<MANIFEST
|
||||
commit=${{ github.sha }}
|
||||
workflow_run=${{ github.run_id }}
|
||||
trigger=${{ github.event_name }}
|
||||
topology=${{ matrix.topology }}
|
||||
redis_version=${{ matrix.redis_version }}
|
||||
image_digest=${{ steps.image.outputs.digest }}
|
||||
MANIFEST
|
||||
- name: Preserve the evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: redis-topology-${{ matrix.topology }}-${{ matrix.redis_version }}
|
||||
path: |
|
||||
src/adapter/outbound/cache-redis/build/test-results/redisTopologyTest/**
|
||||
src/adapter/outbound/cache-redis/build/reports/tests/redisTopologyTest/**
|
||||
if-no-files-found: error
|
||||
retention-days: 90
|
||||
- name: Stop the topology
|
||||
if: always()
|
||||
run: docker compose -f "infra/redis-sdk/${{ matrix.topology }}/compose.yml" down -v
|
||||
Reference in New Issue
Block a user