Skip to content

feat: adopt flow engine for default feedback #785

feat: adopt flow engine for default feedback

feat: adopt flow engine for default feedback #785

Workflow file for this run

name: CI
on:
pull_request:
types: [opened, synchronize, reopened]
merge_group:
permissions: {}
jobs:
# Fast checks run first
check:
name: Lint, Typecheck, Knip, Audit
runs-on: ubuntu-latest
permissions:
checks: read
contents: read
outputs:
full_ci: ${{ steps.scope.outputs.full_ci }}
diff_base: ${{ steps.scope.outputs.diff_base }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Verify previous full CI succeeded
id: previous-ci
if: github.event_name == 'pull_request' && github.event.action == 'synchronize'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
result-encoding: string
script: |
const expected = [
'Lint, Typecheck, Knip, Audit',
'Unit Tests & Build',
'E2E Tests (Shard 1/2)',
'E2E Tests (Shard 2/2)',
'Radix Cross-Browser E2E (chromium)',
'Radix Cross-Browser E2E (firefox)',
'Radix Cross-Browser E2E (webkit)',
];
const { data } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.payload.before,
filter: 'latest',
per_page: 100,
});
const expectedNames = new Set(expected);
const suites = new Map();
for (const run of data.check_runs) {
if (
run.app?.slug !== 'github-actions' ||
!expectedNames.has(run.name) ||
!run.check_suite?.id
) {
continue;
}
const suite = suites.get(run.check_suite.id) ?? {
runs: new Map(),
};
const current = suite.runs.get(run.name);
if (!current || run.id > current.id) {
suite.runs.set(run.name, run);
}
suites.set(run.check_suite.id, suite);
}
const gateName = 'Lint, Typecheck, Knip, Audit';
const latestSuite = [...suites.values()]
.filter((suite) => suite.runs.has(gateName))
.sort(
(left, right) =>
right.runs.get(gateName).id - left.runs.get(gateName).id,
)[0];
const passed = expected.every(
(name) => latestSuite?.runs.get(name)?.conclusion === 'success',
);
core.info(
passed
? 'All reusable checks passed on the previous SHA.'
: 'Previous checks are missing, pending, or unsuccessful; full CI is required.',
);
return passed;
- name: Determine CI scope
id: scope
shell: bash
env:
PR_ACTION: ${{ github.event.action }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_BEFORE_SHA: ${{ github.event.before }}
run: |
scripts/ci-scope.sh \
"$GITHUB_EVENT_NAME" \
"$PR_ACTION" \
"$PR_BASE_SHA" \
"$PR_HEAD_SHA" \
"$PR_BEFORE_SHA" \
"${{ steps.previous-ci.outputs.result }}"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
if: steps.scope.outputs.full_ci != 'false'
run: make install
- name: Run checks (lint, typecheck, knip, audit)
if: steps.scope.outputs.full_ci != 'false'
run: make check
- name: Check changed documentation formatting
if: steps.scope.outputs.full_ci == 'false'
shell: bash
env:
DIFF_BASE_SHA: ${{ steps.scope.outputs.diff_base }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
format_files=()
while IFS= read -r -d '' path; do
format_files+=("$path")
done < <(
scripts/changed-docs.sh "$DIFF_BASE_SHA" "$PR_HEAD_SHA"
)
if (( ${#format_files[@]} > 0 )); then
printf '%s\0' "${format_files[@]}" |
xargs -0 npx --yes prettier@3.8.1 --check --ignore-unknown --
else
echo 'No remaining documentation files require formatting.'
fi
# Unit tests and build
test:
name: Unit Tests & Build
runs-on: ubuntu-latest
needs: check
permissions:
contents: read
# Keep the required check visible as skipped for docs-only pull requests.
if: needs.check.outputs.full_ci != 'false'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: make install
- name: Verify legacy compatibility provenance
run: npm run verify:legacy-compat
- name: Run unit tests
run: npm test -- --coverage
- name: Build all
run: make build-all
- name: Preserve unit coverage report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unit-coverage-${{ github.run_id }}-${{ github.run_attempt }}
path: coverage/lcov.info
if-no-files-found: error
retention-days: 1
# Reporting-only coverage upload. Keep OIDC isolated from pull-request code execution.
coverage:
name: Coverage Upload
runs-on: ubuntu-latest
needs: [check, test]
if: ${{ needs.check.outputs.full_ci != 'false' && needs.test.result == 'success' }}
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Download unit coverage report
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unit-coverage-${{ github.run_id }}-${{ github.run_attempt }}
path: coverage
- name: Upload coverage to Codecov (reporting only)
continue-on-error: true
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: coverage/lcov.info
disable_search: true
plugins: noop
flags: unit
use_oidc: true
fail_ci_if_error: false
# E2E tests with sharding for speed
e2e:
name: E2E Tests (Shard ${{ matrix.shard }}/2)
runs-on: ubuntu-latest
needs: check
permissions:
contents: read
strategy:
fail-fast: false
matrix:
shard: [1, 2]
steps:
# These no-op matrix jobs preserve the two exact required check names. A job-level
# condition is evaluated before matrix expansion and would collapse them into one.
- name: Skip expensive E2E for documentation-only changes
if: needs.check.outputs.full_ci == 'false'
run: echo 'Documentation-only change; required E2E context satisfied without browser tests.'
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: needs.check.outputs.full_ci != 'false'
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
if: needs.check.outputs.full_ci != 'false'
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
if: needs.check.outputs.full_ci != 'false'
run: make install
- name: Build widget
if: needs.check.outputs.full_ci != 'false'
run: make build-widget
- name: Get Playwright version
if: needs.check.outputs.full_ci != 'false'
id: pw-version
run: echo "version=$(npx playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
if: needs.check.outputs.full_ci != 'false'
id: pw-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
- name: Install Playwright browsers
timeout-minutes: 5
if: needs.check.outputs.full_ci != 'false' && steps.pw-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium
- name: Install Playwright system deps
timeout-minutes: 5
if: needs.check.outputs.full_ci != 'false' && steps.pw-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium
- name: Run E2E Tests (Shard ${{ matrix.shard }}/2)
if: needs.check.outputs.full_ci != 'false'
run: make test-e2e-shard SHARD=${{ matrix.shard }}/2
env:
CI: true
- name: Upload E2E Report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: needs.check.outputs.full_ci != 'false' && failure()
with:
name: playwright-report-shard-${{ matrix.shard }}
path: playwright-report/
retention-days: 7
# Focused browser matrix for Radix-style host dialog compatibility
radix-e2e:
name: Radix Cross-Browser E2E (${{ matrix.browser }})
runs-on: ubuntu-latest
needs: check
permissions:
contents: read
if: needs.check.outputs.full_ci != 'false'
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: make install
- name: Build widget
run: make build-widget
- name: Get Playwright version
id: pw-version
run: echo "version=$(npx playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browser
id: pw-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-${{ matrix.browser }}-${{ steps.pw-version.outputs.version }}
- name: Install Playwright browser
timeout-minutes: 5
if: steps.pw-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Install Playwright system deps
timeout-minutes: 5
if: steps.pw-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps ${{ matrix.browser }}
- name: Run Radix E2E Tests (${{ matrix.browser }})
run: make test-radix-e2e BROWSER=${{ matrix.browser }}
env:
CI: true
- name: Upload Radix E2E Report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: playwright-radix-report-${{ matrix.browser }}
path: playwright-report/
retention-days: 7
# Deploy to preview CF Worker (merge queue only)
deploy-preview:
name: Deploy Preview
runs-on: ubuntu-latest
timeout-minutes: 45
needs: [check, test, e2e, radix-e2e]
if: github.event_name == 'merge_group'
permissions:
contents: read
concurrency:
group: bugdrop-shared-preview
cancel-in-progress: false
queue: max
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: make install
- name: Get Playwright version
id: pw-version
run: echo "version=$(npx playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: pw-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: pw-${{ runner.os }}-all-${{ steps.pw-version.outputs.version }}
- name: Install Playwright browsers
timeout-minutes: 10
if: steps.pw-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium firefox webkit
- name: Install Playwright system deps
timeout-minutes: 10
if: steps.pw-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium firefox webkit
- name: Record canary identity
shell: bash
run: |
echo "BUGDROP_CANARY_MARKER=bugdrop-ci-canary:${GITHUB_RUN_ID}:${GITHUB_RUN_ATTEMPT}:${GITHUB_SHA}" >> "$GITHUB_ENV"
echo "BUGDROP_CANARY_RESULT_FILE=test-results/issue-canary-result.json" >> "$GITHUB_ENV"
echo "BUGDROP_CANARY_ATTEMPT_FILE=$RUNNER_TEMP/bugdrop-canary-attempt-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_ENV"
echo "EXACT_WIDGET_FIXTURE_PATH=$RUNNER_TEMP/bugdrop-exact-preview-widget-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.js" >> "$GITHUB_ENV"
echo "EXPECTED_WORKER_SHA=${GITHUB_SHA}" >> "$GITHUB_ENV"
echo "EXPECTED_WIDGET_ORIGIN=https://bugdrop-preview.neonwatty.workers.dev" >> "$GITHUB_ENV"
echo "LIVE_TARGET=preview" >> "$GITHUB_ENV"
echo "PLAYWRIGHT_BASE_URL=https://bugdrop-widget-test-git-preview-jermwatts-projects.vercel.app" >> "$GITHUB_ENV"
- name: Preflight stale canary cleanup
run: >-
node scripts/github-issue-canary.mjs preflight
--profile preview
--repo mean-weasel/bugdrop-widget-test
--prefix "[BugDrop CI canary]"
env:
BUGDROP_CANARY_GITHUB_TOKEN: ${{ secrets.BUGDROP_CANARY_GITHUB_TOKEN }}
- name: Build all
run: BUGDROP_BUILD_MODE=development BUGDROP_DEVELOPMENT_ID="merge-group-${GITHUB_SHA}" make build-all
- name: Record expected preview widget hash
run: echo "EXPECTED_WIDGET_SHA256=$(shasum -a 256 public/widget.js | awk '{print $1}')" >> "$GITHUB_ENV"
- name: Deploy to preview
run: npx wrangler deploy --env preview --var "BUILD_SHA:$GITHUB_SHA"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Wait for exact preview Worker
run: |
WORKER_URL="https://bugdrop-preview.neonwatty.workers.dev/api/health"
echo "Polling preview health for the expected environment and build SHA..."
for i in $(seq 1 30); do
HEALTH=$(curl -sSf "$WORKER_URL" || true)
ENVIRONMENT=$(jq -r '.environment // empty' <<< "$HEALTH")
BUILD_SHA=$(jq -r '.buildSha // empty' <<< "$HEALTH")
if [ "$ENVIRONMENT" = "preview" ] && [ "$BUILD_SHA" = "$GITHUB_SHA" ]; then
echo "Preview worker matched the expected build after $((i * 10))s"
exit 0
fi
echo "Attempt $i/30 did not match the expected preview identity; waiting 10s..."
sleep 10
done
echo "Preview worker did not report the expected environment and build SHA"
exit 1
- name: Wait for exact preview widget asset
run: |
WIDGET_URL="$EXPECTED_WIDGET_ORIGIN/widget.js"
CANDIDATE_PATH="$EXACT_WIDGET_FIXTURE_PATH.candidate"
mkdir -p "$(dirname "$EXACT_WIDGET_FIXTURE_PATH")"
echo "Waiting for $WIDGET_URL to serve $EXPECTED_WIDGET_SHA256..."
for i in $(seq 1 30); do
if ! curl -sSf "$WIDGET_URL" -o "$CANDIDATE_PATH"; then
echo "Attempt $i/30 could not download the preview widget; waiting 5s..."
sleep 5
continue
fi
ACTUAL_SHA="$(shasum -a 256 "$CANDIDATE_PATH" | awk '{print $1}')"
if [ "$ACTUAL_SHA" = "$EXPECTED_WIDGET_SHA256" ]; then
mv "$CANDIDATE_PATH" "$EXACT_WIDGET_FIXTURE_PATH"
echo "Preview widget asset matched after $((i * 5))s"
exit 0
fi
echo "Attempt $i/30 served $ACTUAL_SHA; waiting 5s..."
sleep 5
done
echo "Preview widget did not serve expected asset $EXPECTED_WIDGET_SHA256"
exit 1
- name: Verify fixed preview venue
run: |
BYPASS_ARGS=()
if [ -n "$VERCEL_AUTOMATION_BYPASS_SECRET" ]; then
BYPASS_ARGS=(-H "x-vercel-protection-bypass:${VERCEL_AUTOMATION_BYPASS_SECRET}")
fi
HTML=$(curl -sSf "${BYPASS_ARGS[@]}" "$PLAYWRIGHT_BASE_URL")
grep -Fq "$EXPECTED_WIDGET_ORIGIN/widget.js" <<< "$HTML" || {
echo 'The fixed venue does not reference the expected preview widget.'
exit 1
}
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- name: Run live E2E tests
run: npx playwright test --project=chromium-live --workers=1 --retries=0
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- name: Run live Radix E2E tests
run: make test-live-radix
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- name: Run Chromium live cross-browser smoke
run: make test-live-cross-browser BROWSER=chromium
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- name: Run Firefox live cross-browser smoke
run: make test-live-cross-browser BROWSER=firefox
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- name: Run WebKit live cross-browser smoke
run: make test-live-cross-browser BROWSER=webkit
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- name: Run one structured real-Issue canary
run: |
mkdir -p "$(dirname "$BUGDROP_CANARY_ATTEMPT_FILE")"
: > "$BUGDROP_CANARY_ATTEMPT_FILE"
npx playwright test e2e/widget.issue-canary.spec.ts --project=chromium-issue-canary --workers=1 --retries=0
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
- name: Verify canary Issue independently
run: >-
node scripts/github-issue-canary.mjs verify
--profile preview
--repo mean-weasel/bugdrop-widget-test
--marker "$BUGDROP_CANARY_MARKER"
--expected-sha "$GITHUB_SHA"
--result-file "$BUGDROP_CANARY_RESULT_FILE"
env:
BUGDROP_CANARY_GITHUB_TOKEN: ${{ secrets.BUGDROP_CANARY_GITHUB_TOKEN }}
- name: Cleanup current canary marker
if: always()
run: |
if [ ! -f "$BUGDROP_CANARY_ATTEMPT_FILE" ]; then
echo 'The Issue canary never started; no current marker can exist.'
exit 0
fi
node scripts/github-issue-canary.mjs cleanup \
--profile preview \
--repo mean-weasel/bugdrop-widget-test \
--marker "$BUGDROP_CANARY_MARKER"
env:
BUGDROP_CANARY_GITHUB_TOKEN: ${{ secrets.BUGDROP_CANARY_GITHUB_TOKEN }}
- name: Final reserved-prefix sweep
if: always()
run: >-
node scripts/github-issue-canary.mjs sweep
--profile preview
--repo mean-weasel/bugdrop-widget-test
--prefix "[BugDrop CI canary]"
env:
BUGDROP_CANARY_GITHUB_TOKEN: ${{ secrets.BUGDROP_CANARY_GITHUB_TOKEN }}
- name: Upload preview failure report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: playwright-preview-critical-report
path: playwright-report/
retention-days: 7
# Required-check bridge: fail closed if the merge-group critical section did not succeed.
live-preview-tests:
name: Live Preview Tests
runs-on: ubuntu-latest
needs: [deploy-preview]
if: ${{ always() && github.event_name == 'merge_group' }}
steps:
- name: Require successful preview critical section
env:
CRITICAL_RESULT: ${{ needs.deploy-preview.result }}
run: |
if [ "$CRITICAL_RESULT" != "success" ]; then
echo "Deploy Preview concluded $CRITICAL_RESULT; failing required status."
exit 1
fi