Skip to content
Open
Changes from 2 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
168 changes: 168 additions & 0 deletions .github/workflows/frontend_private_plugin_checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
name: Frontend Private Plugin Checks
Comment thread
andriidudar marked this conversation as resolved.
run-name: "Frontend Private Plugin Checks ${{ github.ref_name }} by @${{ github.actor }}"

# The comet frontend image compiles comet-ml/opik-plugin-ai-spend, checked out at
# build time into src/plugins/ai-spend. Nothing else here compiles it, so a
# breaking change to a shared surface it imports passes every check on the PR
# that makes it and only fails later, in the image build. This runs the frontend
# checks with the plugin staged the same way the image stages it.
#
# For a change that intentionally breaks the plugin, name the plugin branch that
# adapts to it in the PR body, then merge the plugin PR first:
#
# ai-spend-plugin-ref: someone/my-branch
#
# Same-repo PRs get the token; that is the standard model this repo already
# uses for other secrets (e.g. typescript_sdk_e2e_tests.yml), and this repo
# additionally requires maintainer approval before any workflow runs for a
# first-time/outside contributor. Fork PRs get no token at all, from GitHub
# itself, regardless of what any workflow file says.

permissions:
contents: read

on:
pull_request:
paths:
- "apps/opik-frontend/**"
# A change to this check, or to the workflow that controls how the real
# image stages this same plugin, should re-run it.
- ".github/workflows/frontend_private_plugin_checks.yml"
Comment thread
andriidudar marked this conversation as resolved.
- ".github/workflows/build_and_push_docker.yaml"
workflow_dispatch:
inputs:
ai_spend_plugin_ref:
type: string
required: false
description: ai-spend plugin ref
default: "main"

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
checks:
name: Checks with ai-spend plugin
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false

Comment thread
andriidudar marked this conversation as resolved.
# Token availability checked first: without one, the job exits green with
# a notice, before ref parsing -- a malformed ai-spend-plugin-ref must
# never fail a fork event, since nothing downstream would use it anyway.
- name: Resolve plugin ref and token availability
id: resolve
env:
HAS_TOKEN: ${{ secrets.OPIK_PLUGIN_AI_SPEND_TOKEN != '' }}
DISPATCH_REF: ${{ inputs.ai_spend_plugin_ref }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
set -euo pipefail
echo "has_token=${HAS_TOKEN}" >> "$GITHUB_OUTPUT"

if [ "${HAS_TOKEN}" != "true" ]; then
echo "::notice title=ai-spend plugin not checked::No access to the private plugin repository from this event. The plugin was not checked."
exit 0
fi

# Fenced blocks are skipped: a PR that documents this syntax in an
# example must not be taken as using it.
ref="${DISPATCH_REF:-}"
if [ -z "${ref}" ]; then
ref="$(printf '%s' "${PR_BODY:-}" \
| awk '/^[[:space:]]*```/ { fenced = !fenced; next } !fenced' \
| grep -iEm1 '^[[:space:]]*ai-spend-plugin-ref:[[:space:]]*[^[:space:]]+' \
| sed -E 's/^[^:]*:[[:space:]]*//' \
| tr -d '[:space:]' || true)"
fi
ref="${ref:-main}"
Comment thread
andriidudar marked this conversation as resolved.

if ! printf '%s' "${ref}" | grep -qE '^[A-Za-z0-9._/-]+$'; then
echo "::error title=Invalid ai-spend-plugin-ref::'${ref}' is not a valid git ref."
exit 1
Comment thread
andriidudar marked this conversation as resolved.
fi

echo "ref=${ref}" >> "$GITHUB_OUTPUT"
echo "Plugin ref: ${ref}"

- name: Checkout ai-spend plugin (private)
if: steps.resolve.outputs.has_token == 'true'
uses: actions/checkout@v7
with:
repository: comet-ml/opik-plugin-ai-spend
ref: ${{ steps.resolve.outputs.ref }}
token: ${{ secrets.OPIK_PLUGIN_AI_SPEND_TOKEN }}
path: .ai-spend-plugin
fetch-depth: 1
persist-credentials: false
Comment thread
andriidudar marked this conversation as resolved.

# Same staging as the image build. Asserted non-empty, and specifically
# checked for the manifest PluginsStore actually loads by name -- a layout
# change that drops or misnames manifest.ts would otherwise leave
# production silently without the plugin's routes while this still passed
# on an unrelated .ts file count.
- name: Stage ai-spend plugin into frontend src
if: steps.resolve.outputs.has_token == 'true'
run: |
set -euo pipefail
mkdir -p apps/opik-frontend/src/plugins/ai-spend
cp -R .ai-spend-plugin/src/. apps/opik-frontend/src/plugins/ai-spend/
Comment thread
andriidudar marked this conversation as resolved.
rm -rf .ai-spend-plugin

count="$(find apps/opik-frontend/src/plugins/ai-spend -type f \( -name '*.ts' -o -name '*.tsx' \) | wc -l | tr -d ' ')"
if [ "${count}" -eq 0 ]; then
echo "::error title=Plugin staging produced nothing::Copied 0 TypeScript files. The plugin's src layout likely changed."
exit 1
fi

manifest=apps/opik-frontend/src/plugins/ai-spend/manifest.ts
if [ ! -f "${manifest}" ] || ! grep -qE 'name:[[:space:]]*"ai-spend"' "${manifest}"; then
echo "::error title=Plugin manifest missing or misnamed::PluginsStore loads plugins by the name declared in plugins/*/manifest.ts. ${manifest} is missing, or no longer declares name: \"ai-spend\" -- production would silently drop the plugin's routes."
exit 1
fi

echo "Staged ${count} TypeScript files from the plugin; manifest present and named correctly."

- name: Set up Node.js
if: steps.resolve.outputs.has_token == 'true'
uses: actions/setup-node@v7
with:
node-version: "20"

- name: Install dependencies
if: steps.resolve.outputs.has_token == 'true'
run: npm ci
working-directory: apps/opik-frontend

# All three run even if an earlier one fails, so a PR sees every problem in
# one go. eslint is scoped to the plugin: core files are already linted by
# the code quality workflow.
- name: Typecheck, lint and validate dependencies
if: steps.resolve.outputs.has_token == 'true'
working-directory: apps/opik-frontend
run: |
set -uo pipefail
failed=0

echo "::group::typecheck"
npm run typecheck || failed=1
echo "::endgroup::"

echo "::group::eslint (plugin sources)"
npx eslint src/plugins/ai-spend --max-warnings=0 || failed=1
Comment thread
andriidudar marked this conversation as resolved.
echo "::endgroup::"

echo "::group::dependency-cruiser"
npm run deps:validate || failed=1
echo "::endgroup::"

if [ "${failed}" -ne 0 ]; then
echo "::error title=Frontend checks fail with the ai-spend plugin staged::Reproduce locally: symlink or copy a sibling opik-plugin-ai-spend checkout's src/ into apps/opik-frontend/src/plugins/ai-spend, then from apps/opik-frontend run: npm run typecheck && npx eslint src/plugins/ai-spend --max-warnings=0 && npm run deps:validate. (bash scripts/dev-runner.sh --lint-fe is close but not equivalent -- it lints the whole src tree with --fix, plus stylelint, none of which this check runs.) Keep the shared surface backward compatible, or land the matching plugin change first and add 'ai-spend-plugin-ref: <branch>' to this PR body."
exit 1
Comment thread
andriidudar marked this conversation as resolved.
fi
Loading