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
38 changes: 38 additions & 0 deletions .github/actions/docker-build-service/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Docker build service
description: Set up Docker Buildx and build a service image with the repo-standard CI settings (tags, gha cache scopes, provenance/sbom off).
inputs:
service:
description: Service name, used for the image tag and gha cache scope.
required: true
file:
description: Dockerfile path relative to the repo root.
required: true
target:
description: Docker build target stage (empty for the default stage).
required: false
default: ''
load:
description: Load the built image into the local Docker daemon.
required: false
default: 'false'
runs:
using: composite
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Build ${{ inputs.service }}
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
file: ${{ inputs.file }}
target: ${{ inputs.target }}
push: false
load: ${{ inputs.load }}
tags: lucky-${{ inputs.service }}:ci
provenance: false
sbom: false
cache-from: type=gha,scope=${{ inputs.service }}
cache-to: type=gha,mode=max,scope=${{ inputs.service }}
build-args: |
COMMIT_SHA=${{ github.sha }}
NPM_CACHE_KEY=${{ hashFiles('package-lock.json') }}
5 changes: 3 additions & 2 deletions .github/workflows/auto-update-pr-branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ on:
# hasn't recomputed the other PRs' mergeStateStatus yet (it reads UNKNOWN, not
# BEHIND), so a merge-triggered run can update zero PRs and strand the queue
# with no further push to re-fire it. This cron catches any stranded PR within
# ~15 min regardless of the race.
# ~1 hour regardless of the race. (Was every 15 min; the push trigger and
# workflow_dispatch carry the real load, so hourly is enough for a backstop.)
schedule:
- cron: "*/15 * * * *"
- cron: "23 * * * *"
workflow_dispatch:

permissions:
Expand Down
42 changes: 1 addition & 41 deletions .github/workflows/bundle-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,6 @@ permissions:
pull-requests: write

jobs:
compressed-size:
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
YOUTUBE_DL_SKIP_DOWNLOAD: 'true'
steps:
- name: Checkout PR
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '24'
cache: 'npm'

# preactjs/compressed-size-action@v3 picks its package manager from
# lockfile presence — this repo has both package-lock.json (canonical,
# used by npm ci/Docker) and a root pnpm-lock.yaml (CONTRIBUTING.md's
# documented alt dev workflow), so it always invokes `pnpm run
# <build-script>` regardless of the `install-script` input above.
# pnpm-workspace.yaml previously had no `packages:` field, which pnpm
# requires even for a non-recursive `pnpm run` at the workspace root —
# see the packages: field added there.
- name: Setup pnpm
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
version: 9
run_install: false

- name: Compressed Size Diff
uses: preactjs/compressed-size-action@66325aad6443cb7cf89c4bfcd414aea2367cda94 # v3
with:
install-script: 'npm ci'
build-script: 'build'
pattern: 'packages/frontend/dist/**/*.{js,css}'
strip-hash: '\\b\\w{8}\\.'
minimum-change-threshold: 100

size-limit:
name: Size Limit Check
runs-on: ubuntu-latest
Expand All @@ -64,7 +24,7 @@ jobs:
# ci.yml/madge it can't use --ignore-scripts. Without this, youtube-dl-exec's
# postinstall fetches the yt-dlp binary from GitHub unauthenticated (~60/hr/IP)
# and flakes red on shared-runner rate limits (#1827). The build doesn't need
# the binary — skip the download. Mirrors the compressed-size job above.
# the binary — skip the download.
YOUTUBE_DL_SKIP_DOWNLOAD: 'true'
steps:
- name: Checkout PR
Expand Down
132 changes: 94 additions & 38 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ jobs:
npm run lint --workspace=packages/backend
npm run lint --workspace=packages/bot
npm run lint --workspace=packages/shared
# Exits 1 on drift in either mode, so a single human-readable run
# is the CI assertion. Absorbs the deleted path-portability.yml
# workflow. ripgrep is NOT preinstalled on ubuntu-latest.
- name: Path portability
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep jq
bash scripts/check-path-portability.sh
- name: Verify shared package exports
run: npm run verify:shared-exports
env:
Expand Down Expand Up @@ -137,6 +144,27 @@ jobs:
with:
name: shared-build
path: packages/shared
# Absorbed from the deleted queueresolver-canary.yml: guard against
# discord-player major bumps beyond the version pinned in
# package-lock.json before running the bot suites.
- name: Check discord-player version
id: discord_player
run: |
VERSION=$(npm list discord-player 2>/dev/null | grep discord-player | head -1 | grep -oP '\d+\.\d+\.\d+')
PINNED=$(grep '"discord-player"' package-lock.json | head -1 | grep -oP '\d+\.\d+\.\d+')
echo "installed=$VERSION" >> "$GITHUB_OUTPUT"
echo "pinned=$PINNED" >> "$GITHUB_OUTPUT"
- name: Detect discord-player major bump beyond pinned
run: |
PINNED_MAJOR=$(echo "${{ steps.discord_player.outputs.pinned }}" | cut -d. -f1)
INSTALLED_MAJOR=$(echo "${{ steps.discord_player.outputs.installed }}" | cut -d. -f1)
if [ "$INSTALLED_MAJOR" -gt "$PINNED_MAJOR" ]; then
echo "ERROR: discord-player MAJOR bump detected"
echo "Pinned: ${{ steps.discord_player.outputs.pinned }} (major: $PINNED_MAJOR)"
echo "Installed: ${{ steps.discord_player.outputs.installed }} (major: $INSTALLED_MAJOR)"
exit 1
fi
echo "discord-player version compatible (pinned: ${{ steps.discord_player.outputs.pinned }}, installed: ${{ steps.discord_player.outputs.installed }})"
- name: Run bot tests
run: npm run test --workspace=packages/bot -- --ci --coverage
- name: Run music incident regression
Expand Down Expand Up @@ -207,6 +235,65 @@ jobs:
path: packages/frontend/coverage/lcov.info
retention-days: 1

# Hard gate. Blocks merges if cycle count exceeds baseline.
# Baseline: COUNT > 1 (permits 1 known residual type-only cycle).
#
# 1 known residual: types/CustomClient.ts > models/Command.ts > types/CommandData.ts
# All imports in this cycle are `import type`; breaking it requires structural
# refactoring (moving types to shared file) that cascades into command-loader
# consumers (help.ts). Deferred pending command-loader contract review.
#
# See decisions/2026-05-16-next-refactor-target-bot-circular-deps.md.
# The job name must stay exactly "madge / packages/bot": the branch ruleset
# requires that check context (absorbed from the deleted madge.yml workflow).
madge:
name: madge / packages/bot
runs-on: ubuntu-latest
needs: build-shared
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps --ignore-scripts
- name: Download shared artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: shared-build
path: packages/shared
- name: Install madge
# --ignore-scripts: this static-analysis job needs only the source
# tree, not any postinstall binaries. Without it, the full install
# runs youtube-dl-exec's postinstall, which fetches the yt-dlp
# binary from GitHub releases unauthenticated and flakes on the
# ~60/hr anonymous rate limit (blocked #1105). Matches the
# --ignore-scripts convention already used throughout ci.yml.
run: npm install --no-save madge@^7 --ignore-scripts
- name: Run madge --circular on packages/bot/src
run: |
npx madge --circular --extensions ts packages/bot/src \
| tee madge-bot.txt
COUNT=$(grep -cE '^[0-9]+\)' madge-bot.txt || echo 0)
Comment thread
LucasSantana-Dev marked this conversation as resolved.
echo "bot_cycles=$COUNT" >> "$GITHUB_OUTPUT"
{
echo "## madge circular deps - packages/bot/src"
echo ""
echo "**Cycle count:** $COUNT (max: 1)"
echo ""
echo '```'
cat madge-bot.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
if [ "$COUNT" -gt 1 ]; then
echo "❌ Cycle count exceeds baseline (> 1)" >&2
exit 1
fi
id: madge

detect-docker-changes:
name: Detect Docker-relevant changes
runs-on: ubuntu-latest
Expand Down Expand Up @@ -260,48 +347,25 @@ jobs:
- {service: nginx, file: Dockerfile.nginx, target: ''}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
continue-on-error: true
- name: Set up Docker Buildx — retry once on transient failure
if: steps.buildx.outcome == 'failure'
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Build ${{ matrix.service }}
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
uses: ./.github/actions/docker-build-service
continue-on-error: true
with:
context: .
service: ${{ matrix.service }}
file: ${{ matrix.file }}
target: ${{ matrix.target }}
push: false
load: ${{ matrix.service == 'bot' }}
tags: lucky-${{ matrix.service }}:ci
provenance: false
sbom: false
cache-from: type=gha,scope=${{ matrix.service }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
build-args: |
COMMIT_SHA=${{ github.sha }}
NPM_CACHE_KEY=${{ hashFiles('package-lock.json') }}
- name: Build ${{ matrix.service }} — retry once on transient failure
# Retry once on transient failure (buildx setup and build re-run
# together, matching the previous separate retry steps).
- name: Build ${{ matrix.service }} - retry once on transient failure
if: steps.build.outcome == 'failure'
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
uses: ./.github/actions/docker-build-service
with:
context: .
service: ${{ matrix.service }}
file: ${{ matrix.file }}
target: ${{ matrix.target }}
push: false
load: ${{ matrix.service == 'bot' }}
tags: lucky-${{ matrix.service }}:ci
provenance: false
sbom: false
cache-from: type=gha,scope=${{ matrix.service }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
build-args: |
COMMIT_SHA=${{ github.sha }}
NPM_CACHE_KEY=${{ hashFiles('package-lock.json') }}
# A built image is not a working image: the bot's native addons
# (@discordjs/opus) and the baked Prisma engines only fail at
# require()-time, which `docker build` never reaches. #1735 shipped a
Expand Down Expand Up @@ -447,11 +511,3 @@ jobs:
run: npm run audit:high
- name: Secrets scan
run: npx secretlint "**/*"
- name: TruffleHog secret scan
uses: trufflesecurity/trufflehog@0ec3634f6cf66a61912a923fee9d20cc45633a67 # post-v3.95.2 (2026-05-07)
with:
extra_args: --only-verified
continue-on-error: true
- name: Socket.dev supply chain scan
run: echo "Socket.dev scan -- GitHub App (apps/socket-security) covers PR-level blocking"
# SocketDev/socket-security-action@v1 is unavailable; removed to unblock CI
13 changes: 6 additions & 7 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'staging'))
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Resolve ref to deploy
id: ref
env:
Expand All @@ -54,16 +59,10 @@ jobs:
WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
DEPLOY_REF: ${{ steps.ref.outputs.ref }}
run: |
# shellcheck disable=SC2016 # ${...} below is JS, evaluated by node, not bash
set -euo pipefail
# Derive the staging hook URL (origin + /webhook/deploy-staging)
# from the prod webhook secret, keeping the curl injection-safe.
STAGING_URL=$(node -e '
const raw = (process.env.WEBHOOK_URL ?? "").trim();
if (!raw) { console.error("DEPLOY_WEBHOOK_URL not set"); process.exit(1); }
const u = new URL(raw);
console.log(`${u.protocol}//${u.host}/webhook/deploy-staging`);
')
STAGING_URL="$(bash scripts/deploy/derive-webhook-origin.sh "$WEBHOOK_URL")/webhook/deploy-staging"
echo "POST $STAGING_URL (ref=$DEPLOY_REF)"
code=$(curl -s -o /tmp/resp.txt -w "%{http_code}" \
-X POST \
Expand Down
Loading
Loading