feat(teams): Graph-based ingestion (no @mention required) #492
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Default to least-privilege at the workflow level. Individual jobs may | |
| # elevate via their own `permissions:` block if a future change needs | |
| # write access (e.g. to comment on PRs or upload SBOMs). CodeQL alerts | |
| # #2, #4, #5, #7 — actions/missing-workflow-permissions. | |
| permissions: | |
| contents: read | |
| jobs: | |
| backend: | |
| name: Backend (Python 3.12) | |
| permissions: | |
| contents: read | |
| runs-on: ubuntu-latest | |
| services: | |
| mongodb: | |
| image: mongo:7@sha256:43fddee7e532a920f3dfdee9e8f4834398c155c26bcb92d790cc1cd3c630fc40 | |
| ports: | |
| - 27017:27017 | |
| options: >- | |
| --health-cmd "mongosh --eval 'db.runCommand({ ping: 1 })'" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| redis: | |
| image: redis:7@sha256:24ea35ee039d7345bef2377611e3b1a615231746de4cfea74c1414575ee56167 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.12" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: uv sync --extra dev --extra test --extra nebula | |
| - name: Ruff format | |
| run: uv run ruff format --check src/ tests/ | |
| - name: Ruff | |
| run: uv run ruff check src/ tests/ | |
| - name: Pyright | |
| run: uv run pyright | |
| - name: Pytest + coverage | |
| env: | |
| MONGODB_URI: mongodb://localhost:27017/beever_atlas | |
| REDIS_URL: redis://localhost:6379 | |
| # 64 hex chars = 32 bytes = AES-256-GCM key. Tests that exercise | |
| # the Endpoint+Assignment credential-encryption path | |
| # (``tests/api/test_assignments_api.py``, ``tests/llm/test_endpoint_store.py``) | |
| # ``monkeypatch.setenv`` this same value, but ``get_settings()`` is | |
| # ``@lru_cache``-decorated and the FIRST call freezes the env state. | |
| # Without this CI-level default, the first import that touches | |
| # settings caches an empty key, and every subsequent | |
| # ``encrypt_endpoint_credential`` 503s with | |
| # ``credential_encryptor_unavailable``. | |
| # Same value the ``smoke.yml`` workflow uses. | |
| CREDENTIAL_MASTER_KEY: ${{ secrets.CI_CREDENTIAL_MASTER_KEY || 'abababababababababababababababababababababababababababababababab' }} | |
| run: uv run pytest --cov=src/beever_atlas --cov-report=term-missing -q | |
| web: | |
| name: Web (Node 20) | |
| permissions: | |
| contents: read | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: web | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: web/package-lock.json | |
| - name: Install | |
| run: npm ci | |
| - name: Lint | |
| run: npm run lint | |
| - name: Typecheck | |
| run: npm run typecheck | |
| - name: Test | |
| run: npm test -- --run | |
| - name: Build | |
| run: npm run build | |
| bot: | |
| name: Bot (Node 20) | |
| permissions: | |
| contents: read | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: bot | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| cache-dependency-path: bot/package-lock.json | |
| - name: Install | |
| run: npm ci | |
| - name: Lint | |
| run: npm run lint | |
| - name: Build | |
| run: npm run build | |
| - name: Test | |
| run: npm test | |
| supply-chain-lint: | |
| name: Supply Chain (digest pinning) | |
| permissions: | |
| contents: read | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: "Reject unpinned FROM / image: references" | |
| run: | | |
| set -e | |
| violations=0 | |
| # Dockerfiles: reject FROM <ref> when ref is a real image (has : or /) | |
| # without an @sha256: digest suffix. Stage aliases (bare words) pass. | |
| for f in Dockerfile web/Dockerfile bot/Dockerfile; do | |
| [ -f "$f" ] || continue | |
| while IFS= read -r line; do | |
| ref="$(echo "$line" | awk '/^FROM /{print $2}')" | |
| [ -z "$ref" ] && continue | |
| [ "$ref" = "scratch" ] && continue | |
| case "$ref" in | |
| *@sha256:*) ;; | |
| *) | |
| case "$ref" in | |
| *[:/]*) | |
| echo "::error file=$f::unpinned FROM: $line" | |
| violations=$((violations+1)) | |
| ;; | |
| esac | |
| ;; | |
| esac | |
| done < "$f" | |
| # Also catch COPY --from=<external> without a digest. | |
| while IFS= read -r line; do | |
| from_ref="$(echo "$line" | sed -nE 's/.*COPY[[:space:]]+--from=([^[:space:]]+).*/\1/p')" | |
| [ -z "$from_ref" ] && continue | |
| # Bare stage alias (no : or /): skip. | |
| case "$from_ref" in | |
| *[:/]*) ;; | |
| *) continue ;; | |
| esac | |
| case "$from_ref" in | |
| *@sha256:*) ;; | |
| *) | |
| echo "::error file=$f::unpinned COPY --from: $line" | |
| violations=$((violations+1)) | |
| ;; | |
| esac | |
| done < "$f" | |
| done | |
| # Compose files + GitHub Actions workflow service images: reject | |
| # `image:` lines without @sha256:. Issue #42 added the workflows | |
| # scan after the nightly job's `services:` images were caught | |
| # running unpinned. The existing | |
| # `^[[:space:]]*image:[[:space:]]+` regex matches both | |
| # compose-level and `services:` block image references. | |
| targets="docker-compose.yml docker-compose.nebula.yml" | |
| for wf in .github/workflows/*.yml; do | |
| [ -f "$wf" ] && targets="$targets $wf" | |
| done | |
| for f in $targets; do | |
| [ -f "$f" ] || continue | |
| while IFS= read -r line; do | |
| ref="$(echo "$line" | awk '/^[[:space:]]*image:[[:space:]]+/{print $2}')" | |
| [ -z "$ref" ] && continue | |
| case "$ref" in | |
| *@sha256:*) ;; | |
| *) | |
| echo "::error file=$f::unpinned image: $line" | |
| violations=$((violations+1)) | |
| ;; | |
| esac | |
| done < "$f" | |
| done | |
| if [ "$violations" -gt 0 ]; then | |
| echo "Found $violations unpinned image reference(s). Pin by @sha256:<digest>." | |
| exit 1 | |
| fi | |
| echo "OK — every FROM / image: is digest-pinned." | |
| - name: Reject caret/tilde ranges on chat SDK family in bot/package.json | |
| run: | | |
| set -e | |
| # RES-195 (H6): the Vercel `chat` + @chat-adapter/* family must stay | |
| # pinned to exact versions. Caret/tilde ranges reopen the supply-chain | |
| # vector a compromised-maintainer release would exploit. | |
| violations=0 | |
| while IFS= read -r line; do | |
| # Extract the specifier string from `"name": "spec",` lines. | |
| spec="$(echo "$line" | sed -nE 's/.*"(chat|@chat-adapter\/[a-z-]+|chat-adapter-mattermost)": "([^"]+)".*/\2/p')" | |
| [ -z "$spec" ] && continue | |
| case "$spec" in | |
| [\^~]*) | |
| echo "::error file=bot/package.json::non-exact chat-family pin: $line" | |
| violations=$((violations+1)) | |
| ;; | |
| esac | |
| done < bot/package.json | |
| if [ "$violations" -gt 0 ]; then | |
| echo "Found $violations non-exact chat-SDK pin(s). Strip ^ and ~." | |
| exit 1 | |
| fi | |
| echo "OK — chat SDK family pinned to exact versions." |