Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
65 changes: 6 additions & 59 deletions .github/workflows/e2e-harness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ permissions:
contents: read

env:
TRUFFLEHOG_IMAGE: ghcr.io/trufflesecurity/trufflehog@sha256:aa821cf4ace8861c7d096d83818cdf7bb9719028a52d37a52eaad44086a52577 # v3.96.0
TRUFFLEHOG_IMAGE: ghcr.io/trufflesecurity/trufflehog@sha256:562bc231afa9de3d04de44cfe624252b08207de1fc3cebc5e7ed92bed7f279e4 # v3.97.4
UV_VERSION: "0.10.12"

concurrency:
Expand Down Expand Up @@ -77,60 +77,13 @@ jobs:
DIAGNOSTICS_PATH: ${{ runner.temp }}/powercontext-e2e-diagnostics-${{ matrix.database }}
EVIDENCE_PATH: ${{ github.workspace }}/.powercontext-e2e/${{ matrix.database }}/acceptance
SCENARIO_OUTCOME: ${{ steps.acceptance_scenarios.outcome }}
run: |
set -eu
mkdir -p "${DIAGNOSTICS_PATH}"
summary="${DIAGNOSTICS_PATH}/summary.txt"
{
echo "database=${DATABASE}"
echo "diagnostic_format=powercontext-e2e-v1"
echo "scenario_outcome=${SCENARIO_OUTCOME}"
echo "scanner_image=${TRUFFLEHOG_IMAGE}"
} > "$summary"

if ! test -d "${EVIDENCE_PATH}"; then
echo "evidence_status=missing" >> "$summary"
echo "Replay evidence directory was not produced." >&2
exit 1
fi

file_count="$(find "${EVIDENCE_PATH}" -type f | wc -l | tr -d '[:space:]')"
if test "$file_count" = 0; then
echo "evidence_status=empty" >> "$summary"
echo "Replay evidence directory was empty." >&2
exit 1
fi
{
echo "evidence_status=present"
echo "evidence_file_count=${file_count}"
} >> "$summary"

set +e
docker run --rm \
--network none \
--volume "${EVIDENCE_PATH}:/evidence:ro" \
"${TRUFFLEHOG_IMAGE}" \
filesystem /evidence \
--no-verification \
--results=verified,unknown,unverified \
--fail \
--fail-on-scan-errors \
--no-update \
--json > /dev/null 2>&1
scanner_status=$?
set -e
if test "$scanner_status" -ne 0; then
{
echo "scan_status=failed"
echo "scanner_exit_code=${scanner_status}"
} >> "$summary"
echo "Replay evidence failed secret scanning; raw evidence will not be uploaded." >&2
exit 1
fi
echo "scan_status=clean" >> "$summary"
run: bash scripts/scan_e2e_evidence.sh

- name: Upload sanitized replay diagnostics
if: always()
if: >-
always() &&
(steps.evidence_scan.outcome != 'success' ||
steps.acceptance_scenarios.outcome != 'success')
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: e2e-acceptance-diagnostics-${{ matrix.database }}-${{ github.sha }}
Expand All @@ -153,9 +106,3 @@ jobs:
env:
POWERCONTEXT_E2E_DATABASE: ${{ matrix.database }}
run: make harness-compose-down

- name: Enforce replay evidence policy
if: always() && steps.evidence_scan.outcome != 'success'
run: |
echo "Replay evidence is missing or did not pass secret scanning." >&2
exit 1
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ notebooks-test: ## Execute provider-free tutorials in fresh kernels; use ARGS fo
@uv run --locked --group notebooks python examples/jupyter/run.py $(ARGS)

.PHONY: check
check: integration-manifest-check ## Run code quality tools.
check: workflow-actions-check integration-manifest-check ## Run code quality tools.

.PHONY: workflow-actions-check
workflow-actions-check: ## Verify third-party GitHub Actions use immutable commit pins.
@uv run python scripts/check_workflow_actions.py .github/workflows .github/actions
@echo "🚀 Checking lock file consistency with 'pyproject.toml'"
@uv lock --locked
@echo "🚀 Linting code: Running prek"
Expand Down
143 changes: 143 additions & 0 deletions scripts/check_workflow_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# Copyright (c) 2026 OceanBase.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Require immutable commit pins for third-party GitHub Actions."""

from __future__ import annotations

import re
import sys
from collections import deque
from pathlib import Path

import yaml
from yaml.resolver import BaseResolver

COMMIT_SHA = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE)
SUPPORTED_SUFFIXES = {".yaml", ".yml"}


class _UniqueKeyLoader(yaml.SafeLoader):
"""Reject duplicate mapping keys so validation matches one YAML meaning."""


class _DuplicateKeyError(yaml.YAMLError):
def __init__(self) -> None:
super().__init__("duplicate YAML key")


class _InvalidMappingKeyError(yaml.YAMLError):
def __init__(self) -> None:
super().__init__("unhashable YAML mapping key")


def _construct_unique_mapping(
loader: _UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False
) -> dict[object, object]:
mapping: dict[object, object] = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node, deep=deep)
try:
if key in mapping:
raise _DuplicateKeyError()
mapping[key] = loader.construct_object(value_node, deep=deep)
except TypeError as error:
raise _InvalidMappingKeyError() from error
return mapping


_UniqueKeyLoader.add_constructor(BaseResolver.DEFAULT_MAPPING_TAG, _construct_unique_mapping)


def _files_to_scan(arguments: list[str]) -> list[Path]:
files: list[Path] = []
for argument in arguments:
path = Path(argument)
if path.is_file():
if path.suffix.lower() in SUPPORTED_SUFFIXES:
files.append(path)
continue
if path.is_dir():
files.extend(
candidate
for candidate in path.rglob("*")
if candidate.is_file()
and candidate.suffix.lower() in SUPPORTED_SUFFIXES
and ".git" not in candidate.parts
)
continue
print(f"path does not exist: {path}", file=sys.stderr)
raise SystemExit(2)
return sorted(set(files), key=lambda path: str(path).lower())


def _action_references(value: object) -> list[str]:
references: list[str] = []
pending = deque([value])
visited: set[int] = set()
while pending:
current = pending.popleft()
if not isinstance(current, (dict, list)):
continue
current_id = id(current)
if current_id in visited:
continue
visited.add(current_id)
if isinstance(current, dict):
for key, child in current.items():
if key == "uses":
references.append(child if isinstance(child, str) else "")
else:
pending.append(child)
else:
pending.extend(current)
return references


def _display_reference(reference: str) -> str:
if re.fullmatch(r"[A-Za-z0-9_.@/-]{1,240}", reference):
return reference
return "[REDACTED]"


def main(arguments: list[str]) -> int:
references_checked = 0
violations: list[str] = []
for path in _files_to_scan(arguments or [".github/workflows", ".github/actions"]):
try:
document = yaml.load(
path.read_text(encoding="utf-8"),
Loader=_UniqueKeyLoader, # noqa: S506
)
except (OSError, UnicodeError, RecursionError, yaml.YAMLError):
print(f"{path}: could not parse YAML", file=sys.stderr)
return 1
for reference in _action_references(document):
references_checked += 1
if reference.startswith("./") or reference.startswith("../"):
continue
if "@" not in reference or not COMMIT_SHA.fullmatch(reference.rsplit("@", 1)[1]):
display_reference = _display_reference(reference)
violations.append(f"{path}: uses reference '{display_reference}' must use a 40-character commit SHA")

if violations:
print("\n".join(violations), file=sys.stderr)
return 1
print(f"{references_checked} action references checked")
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Loading
Loading