feat: complete codebase reliability upgrade

This commit is contained in:
gadgethd committed 2026-08-02 03:40:11 +01:00
1 parent d93e817313
commit 5db382e181
368 files changed
+44682 -5905

No files matched your search

+290 -16
View File
@@ -18,12 +18,37 @@ jobs:
backend:
name: Backend
runs-on: ubuntu-latest
services:
postgres:
image: timescale/timescaledb@sha256:22e8a5ae7aef121d1537afe946dd7cc5deeeb63ab36ce19849d671bd3b663509
env:
POSTGRES_PASSWORD: analysis-test-password
ports:
- 55433:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 3s
--health-retries 10
redis:
image: redis@sha256:8b81dd37ff027bec4e516d41acfbe9fe2460070dc6d4a4570a2ac5b9d59df065
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 3s
--health-retries 10
env:
TEST_REDIS_URL: redis://127.0.0.1:6379
TEST_ANALYSIS_DATABASE_URL: postgresql://postgres:analysis-test-password@127.0.0.1:55433/postgres
TEST_INGEST_DATABASE_URL: postgresql://postgres:analysis-test-password@127.0.0.1:55433/postgres
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 20
cache: npm
@@ -32,7 +57,11 @@ jobs:
- name: Audit backend dependencies
run: npm audit --audit-level=high
- run: npm run typecheck
- run: npm run contract:check
- run: npm test
- run: npm run test:analysis-integration
- run: npm run test:ingest-integration
- run: npm run test:queue-integration
- run: npm run build
frontend:
@@ -42,15 +71,16 @@ jobs:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 20
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- name: Audit frontend dependencies
run: npm audit --audit-level=high
- run: npm test
- run: npm run build
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
@@ -58,13 +88,77 @@ jobs:
workers-and-compose:
name: Workers and Compose
runs-on: ubuntu-latest
services:
postgres:
image: postgres@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
env:
POSTGRES_PASSWORD: ml-test-password
ports:
- 55432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 3s
--health-retries 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Validate release security waivers
run: node scripts/check-trivy-waivers.mjs
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: '3.12'
- name: Test operator scripts
working-directory: scripts
run: |
npm ci
npm audit --audit-level=high
npm run typecheck
npm test
- name: Compile Python workers
run: python -m py_compile viewshed-worker/worker.py viewshed-worker/rf/*.py ml-path-learner/worker.py
run: python -m py_compile viewshed-worker/worker.py viewshed-worker/link_queue_v3.py viewshed-worker/viewshed_queue_v2.py viewshed-worker/backfill_profiles.py viewshed-worker/rf/*.py ml-path-learner/worker.py
- name: Build worker images
run: |
docker build -f viewshed-worker/Dockerfile -t meshcore-viewshed-worker:test viewshed-worker
docker build -f ml-path-learner/Dockerfile -t meshcore-ml-path-learner:test ml-path-learner
- name: Test viewshed worker in its GDAL image
run: |
trap 'docker rm -f link-queue-test-redis >/dev/null 2>&1 || true' EXIT
docker run -d --name link-queue-test-redis \
-p 127.0.0.1:6380:6379 \
redis@sha256:8b81dd37ff027bec4e516d41acfbe9fe2460070dc6d4a4570a2ac5b9d59df065 redis-server --requirepass test-queue-password --maxmemory-policy noeviction
for _ in $(seq 1 30); do
if docker exec link-queue-test-redis redis-cli -a test-queue-password --no-auth-warning ping >/dev/null; then
break
fi
sleep 1
done
docker run --rm --network host --entrypoint python \
-e TEST_REDIS_URL=redis://127.0.0.1:6380 \
-e TEST_REDIS_PASSWORD=test-queue-password \
meshcore-viewshed-worker:test -m unittest discover -s /app/tests -v
docker run --rm --entrypoint python \
meshcore-viewshed-worker:test /app/tests/benchmark_rf.py --rays 12 --steps 1000
docker run --rm --network none --entrypoint python \
meshcore-viewshed-worker:test /app/tests/benchmark_rf_radial.py --rays 72 --steps 500 --repeats 1
- name: Test ML worker in its image
run: |
docker run --rm --network host --entrypoint python \
-e TEST_DATABASE_URL=postgresql://postgres:ml-test-password@127.0.0.1:55432/postgres \
meshcore-ml-path-learner:test -m unittest discover -s /app/tests -v
- name: Bootstrap clean Mosquitto credentials
env:
MQTT_USERNAME: backend
MQTT_PASSWORD: ci-mqtt-password
MOSQUITTO_CONFIG_DIR: /tmp/meshcore-ci-mosquitto
run: |
install -d -m 700 "$MOSQUITTO_CONFIG_DIR"
cp mosquitto/mosquitto.conf "$MOSQUITTO_CONFIG_DIR/mosquitto.conf"
scripts/bootstrap-mosquitto.sh
test "$(stat -c '%a' "$MOSQUITTO_CONFIG_DIR/passwd")" = 640
test "$(stat -c '%a' "$MOSQUITTO_CONFIG_DIR/acl")" = 640
- name: Validate Compose configuration
env:
POSTGRES_PASSWORD: ci-postgres-password
@@ -73,18 +167,193 @@ jobs:
OPERATOR_SITE_TOKEN: ci-operator-site-token-0123456789abcdef0123456789abcdef
MQTT_PASSWORD: ci-mqtt-password
ANUBIS_ED25519_PRIVATE_KEY_HEX: '0000000000000000000000000000000000000000000000000000000000000001'
HEALTHCHECK_SOURCE_REF: 952eec8e0c55d0749f9abef211c266f992c0bfc7
GRAFANA_ADMIN_PASSWORD: ci-grafana-password
run: docker compose config --quiet
run: |
docker compose config --quiet
docker compose config --format json \
| jq -e '.services.timescaledb.command
| index("max_worker_processes=24") != null' >/dev/null
- name: Build every application image and smoke an empty-volume stack
env:
POSTGRES_PASSWORD: ci-postgres-password
REDIS_PASSWORD: ci-redis-password
JWT_SECRET: ci-jwt-secret
OPERATOR_SITE_TOKEN: ci-operator-site-token-0123456789abcdef0123456789abcdef
MQTT_USERNAME: backend
MQTT_PASSWORD: ci-mqtt-password
ANUBIS_ED25519_PRIVATE_KEY_HEX: '0000000000000000000000000000000000000000000000000000000000000001'
HEALTHCHECK_SOURCE_REF: 952eec8e0c55d0749f9abef211c266f992c0bfc7
GRAFANA_ADMIN_PASSWORD: ci-grafana-password
SOURCE_REVISION: ${{ github.sha }}
MOSQUITTO_IMAGE: eclipse-mosquitto@sha256:9cfdd46ad59f3e3e5f592f6baf57ab23e1ad00605509d0f5c1e9b179c5314d87
run: |
project="meshcore-ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
publisher_user="ci-e2e-publisher"
publisher_password="ci-e2e-publisher-password"
observer="CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
cleanup() {
docker compose \
--project-name "$project" \
--profile dev \
--profile viewshed \
down --volumes --remove-orphans
}
trap cleanup EXIT
scripts/bootstrap-mosquitto.sh
docker run --rm \
--user "$(id -u):$(id -g)" \
-v "$PWD/mosquitto:/mosquitto/config" \
"$MOSQUITTO_IMAGE" \
mosquitto_passwd -b /mosquitto/config/passwd \
"$publisher_user" "$publisher_password"
printf '\nuser %s\ntopic write meshcore-test/CI/%s/packets\ntopic write meshcore-test/CI/%s/status\n' \
"$publisher_user" "$observer" "$observer" >>mosquitto/acl
chmod 640 mosquitto/passwd mosquitto/acl
docker build \
--build-arg SOURCE_REVISION="$SOURCE_REVISION" \
--build-arg VITE_APP_HOSTNAME=app.ukmesh.com \
-f Dockerfile -t meshcore-combined:test .
docker compose \
--project-name "$project" \
--profile dev \
--profile viewshed \
build \
backend app-ukmesh website-ukmesh mesh-health-check \
mosquitto-reloader link-worker
docker compose \
--project-name "$project" \
--profile dev \
--profile viewshed \
up --detach --no-build --wait --wait-timeout 360
health_session_id="$(
curl --fail --silent --request POST \
--header 'Content-Type: application/json' \
--data '{}' \
http://127.0.0.1:3090/api/sessions \
| jq -er '.id'
)"
sleep 1
docker compose --project-name "$project" exec -T \
-e HEALTH_SESSION_ID="$health_session_id" \
mesh-health-check node --input-type=module <<'NODE'
import { readFileSync } from 'node:fs';
const payload = JSON.parse(
readFileSync('/app/data/session-results.json', 'utf8'),
);
if (!payload.sessions?.some(({ id }) => id === process.env.HEALTH_SESSION_ID)) {
throw new Error('health-check session was not persisted by the non-root runtime');
}
NODE
curl --fail --silent http://127.0.0.1:3000/readyz \
| jq -e '.status == "ready"' >/dev/null
curl --fail --silent http://127.0.0.1:3003/ >/dev/null
curl --fail --silent http://127.0.0.1:3004/ >/dev/null
curl --fail --silent http://127.0.0.1:3003/readyz \
| jq -e '.status == "ready"' >/dev/null
curl --fail --silent http://127.0.0.1:3004/readyz \
| jq -e '.status == "ready"' >/dev/null
for origin in http://127.0.0.1:3003 http://127.0.0.1:3004; do
for path in /metrics /operations /observer-registrations /local-api/operations; do
test "$(curl --silent --output /dev/null --write-out '%{http_code}' "${origin}${path}")" = 404
done
done
docker compose --project-name "$project" exec -T backend \
wget -qO- http://127.0.0.1:9091/metrics \
| grep -q '^meshcore_process_'
ws_log="$(mktemp)"
docker compose --project-name "$project" exec -T \
-e MQTT_E2E_OBSERVER="$observer" \
backend node --input-type=module >"$ws_log" <<'NODE' &
import WebSocket from 'ws';
const observer = process.env.MQTT_E2E_OBSERVER.toLowerCase();
const socket = new WebSocket('ws://website-ukmesh:8080/ws?network=test');
const timeout = setTimeout(() => {
console.error('timed out waiting for MQTT packet WebSocket fanout');
process.exit(1);
}, 15_000);
socket.on('message', (raw) => {
for (const line of String(raw).split('\n').filter(Boolean)) {
const message = JSON.parse(line);
if (
message.type === 'packet'
&& String(message.data?.rxNodeId ?? '').toLowerCase() === observer
) {
clearTimeout(timeout);
console.log('mqtt-packet-fanout-ok');
socket.close();
process.exit(0);
}
}
});
socket.on('error', (error) => {
console.error(error);
process.exit(1);
});
NODE
ws_pid=$!
sleep 2
docker compose --project-name "$project" exec -T mosquitto \
mosquitto_pub -h 127.0.0.1 -p 1883 \
-u "$publisher_user" -P "$publisher_password" \
-t "meshcore-test/CI/${observer}/status" \
-m '{"origin":"CI E2E observer","origin_id":"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC","stats":{"battery_mv":4100,"uptime_secs":60}}'
docker compose --project-name "$project" exec -T mosquitto \
mosquitto_pub -h 127.0.0.1 -p 1883 \
-u "$publisher_user" -P "$publisher_password" \
-t "meshcore-test/CI/${observer}/packets" \
-m '{"packet_type":1,"RSSI":-75,"SNR":8,"direction":"rx","raw":"0D02AABBDEADBEEF"}'
readback=""
for _ in $(seq 1 30); do
readback="$(
docker compose --project-name "$project" exec -T timescaledb \
psql -U meshcore -d meshcore -Atc \
"SELECT EXISTS(SELECT 1 FROM nodes WHERE node_id = '${observer}' AND network = 'test'), EXISTS(SELECT 1 FROM packets WHERE rx_node_id = '${observer}' AND network = 'test'), EXISTS(SELECT 1 FROM packet_hourly_stats WHERE network = 'test')"
)"
if [ "$readback" = "t|t|t" ]; then
break
fi
sleep 1
done
test "$readback" = "t|t|t"
wait "$ws_pid"
grep -q '^mqtt-packet-fanout-ok$' "$ws_log"
- name: Validate monitoring configuration
run: |
docker run --rm \
-v "$PWD/logging/prometheus.yml:/etc/prometheus/prometheus.yml:ro" \
-v "$PWD/logging/rules:/etc/prometheus/rules:ro" \
--entrypoint /bin/promtool \
prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996 \
check config /etc/prometheus/prometheus.yml
docker run --rm \
-v "$PWD/logging/rules:/rules:ro" \
--entrypoint /bin/promtool \
prom/prometheus@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996 \
test rules /rules/meshcore.test.yml
docker run --rm \
-v "$PWD/logging:/etc/meshcore:ro" \
--entrypoint /bin/amtool \
prom/alertmanager@sha256:27c475db5fb156cab31d5c18a4251ac7ed567746a2483ff264516437a39b15ba \
check-config /etc/meshcore/alertmanager.yml
docker run --rm \
-v "$PWD/logging/alloy/config.alloy:/etc/alloy/config.alloy:ro" \
grafana/alloy@sha256:491b0578c04983fd54fe99b587b6fab4404dc46d0dc16677bd6b00cc1140b308 \
validate /etc/alloy/config.alloy
- name: Test compressed-chunk maintenance safety gates
run: scripts/test-vacuum-maintenance.sh
- name: Test immutable release rollback and schema compatibility gates
run: scripts/test-replace-container.sh
secrets:
name: Secret scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Scan repository with gitleaks
uses: gitleaks/gitleaks-action@v2
uses: gitleaks/gitleaks-action@dcedce43c6f43de0b836d1fe38946645c9c638dc # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -93,8 +362,8 @@ jobs:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 20
cache: npm
@@ -107,10 +376,15 @@ jobs:
OPERATOR_SITE_TOKEN: nightly-operator-token-0123456789abcdef0123456789abcdef
MQTT_PASSWORD: nightly-mqtt-password
GRAFANA_ADMIN_PASSWORD: nightly-grafana-password
run: docker compose up -d --build timescaledb redis mosquitto db-migrate backend
API_RATE_LIMIT_MAX: 1000000
ANUBIS_ED25519_PRIVATE_KEY_HEX: '0000000000000000000000000000000000000000000000000000000000000001'
HEALTHCHECK_SOURCE_REF: 952eec8e0c55d0749f9abef211c266f992c0bfc7
run: |
MQTT_USERNAME=backend MQTT_PASSWORD=nightly-mqtt-password scripts/bootstrap-mosquitto.sh
docker compose up -d --build timescaledb redis mosquitto db-migrate backend
- name: Wait for readiness
run: |
for attempt in $(seq 1 60); do
for _ in $(seq 1 60); do
if curl --fail --silent http://127.0.0.1:3000/readyz >/dev/null; then
exit 0
fi
@@ -126,7 +400,7 @@ jobs:
- name: Capture logs
if: always()
run: docker compose logs --no-color backend > nightly-backend.log
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: always()
with:
name: nightly-load-logs
+2 -2
View File
@@ -16,8 +16,8 @@ jobs:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 20
cache: npm
+192
View File
@@ -0,0 +1,192 @@
name: Signed container release
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
policy:
name: Release policy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
- name: Validate vulnerability waivers
run: node scripts/check-trivy-waivers.mjs
- name: Require a clean commit release
run: test -z "$(git status --porcelain --untracked-files=all)"
image:
name: ${{ matrix.component }}
needs: policy
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
include:
- component: backend
image: meshcore-backend
context: .
file: Dockerfile.backend
build_args: ''
- component: combined
image: meshcore-combined
context: .
file: Dockerfile
build_args: VITE_APP_HOSTNAME=app.ukmesh.com
- component: app
image: meshcore-app
context: .
file: Dockerfile.app
build_args: |
VITE_APP_HOSTNAME=app.ukmesh.com
VITE_NETWORK=ukmesh
VITE_SITE=ukmesh
- component: website
image: meshcore-website
context: .
file: Dockerfile.website
build_args: |
VITE_APP_HOSTNAME=app.ukmesh.com
VITE_SITE=ukmesh
- component: rf-worker
image: meshcore-rf-worker
context: viewshed-worker
file: viewshed-worker/Dockerfile
build_args: ''
- component: ml-path-learner
image: meshcore-ml-path-learner
context: ml-path-learner
file: ml-path-learner/Dockerfile
build_args: ''
- component: mosquitto-reloader
image: meshcore-mosquitto-reloader
context: .
file: Dockerfile.mosquitto-reloader
build_args: ''
- component: health-check
image: meshcore-health-check
context: .
file: Dockerfile.mesh-health-check
build_args: MESH_HEALTH_CHECK_REF=952eec8e0c55d0749f9abef211c266f992c0bfc7
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/${{ matrix.image }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,format=long
type=ref,event=tag
- id: build
name: Build and push immutable image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SOURCE_REVISION=${{ github.sha }}
${{ matrix.build_args }}
provenance: false
sbom: false
cache-from: type=gha,scope=${{ matrix.component }}
cache-to: type=gha,mode=max,scope=${{ matrix.component }}
- name: Gate exact digest on critical vulnerabilities
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: image
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: json
output: trivy-${{ matrix.component }}.json
exit-code: '1'
severity: CRITICAL
ignore-unfixed: false
trivyignores: .trivyignore.yaml
- name: Generate SPDX SBOM for exact digest
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom-${{ matrix.component }}.spdx.json
upload-artifact: false
- uses: sigstore/cosign-installer@f713795cb21599bc4e5c4b58cbad1da852d7eeb9 # v3
- name: Sign and attach SBOM attestation
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
image="${REGISTRY}/${IMAGE_NAME}@${DIGEST}"
cosign sign --yes "$image"
cosign attest --yes \
--predicate "sbom-${{ matrix.component }}.spdx.json" \
--type spdxjson "$image"
cosign verify "$image" \
--certificate-identity-regexp "^https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
> "signature-${{ matrix.component }}.json"
- id: provenance
name: Publish registry provenance
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Write release inventory
env:
DIGEST: ${{ steps.build.outputs.digest }}
ATTESTATION_URL: ${{ steps.provenance.outputs.attestation-url }}
run: |
jq -n \
--arg component "${{ matrix.component }}" \
--arg source_revision "$GITHUB_SHA" \
--arg image "${REGISTRY}/${IMAGE_NAME}@${DIGEST}" \
--arg sbom "sbom-${{ matrix.component }}.spdx.json" \
--arg scan "trivy-${{ matrix.component }}.json" \
--arg signature "signature-${{ matrix.component }}.json" \
--arg provenance "$ATTESTATION_URL" \
'{
format: "meshcore-release-inventory-v1",
component: $component,
source_revision: $source_revision,
image: $image,
sbom: $sbom,
scan: $scan,
signature: $signature,
provenance: $provenance
}' > "inventory-${{ matrix.component }}.json"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-${{ github.sha }}-${{ matrix.component }}
if-no-files-found: error
retention-days: 90
path: |
inventory-${{ matrix.component }}.json
sbom-${{ matrix.component }}.spdx.json
trivy-${{ matrix.component }}.json
signature-${{ matrix.component }}.json