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
89 changes: 89 additions & 0 deletions .github/actions/flybot-tests/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: FlyBot tests
description: >-
Node, npm ci, the DSH harness, lint, the client bundle check, ds-bot's suite and the social
and tree plugin suites (optionally as one coverage run). Used by Test (the PR gate); the
Compatibility legs (nightly.yml) can use it too, to run the same checks.
inputs:
node:
description: Node.js version
required: true
dsh:
description: DSH (@deepseek-ai/dsh) version for the harness
required: true
coverage:
description: "'true': run all suites in one coverage run and upload lcov"
required: false
default: 'false'
runs:
using: composite
steps:
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ inputs.node }}
cache: npm
cache-dependency-path: ds-bot/package-lock.json
# The job-level timeout-minutes bounds a stuck registry (composite steps take none).
- name: npm ci
shell: bash
working-directory: ds-bot
run: npm ci --no-fund --no-audit
- name: Install DSH
shell: bash
working-directory: ds-bot
env:
DSH: ${{ inputs.dsh }}
run: npm run harness -- --no-save "@deepseek-ai/dsh@$DSH"
# Lint and the bundle check first: a slip fails in seconds instead of after the suite.
- name: Lint
shell: bash
working-directory: ds-bot
run: npm run lint
- name: Build (client bundle is up to date)
shell: bash
working-directory: ds-bot
run: node scripts/build-client.mjs --check
# ds-bot's suite (incl. tree-host / tree-plugin / bot-tree: the tree plugin on a real
# ds-bot), then each plugin's own suite (social; tree's test/tree.test.js).
- name: Unit tests
if: inputs.coverage != 'true'
shell: bash
working-directory: ds-bot
run: npm test
- name: Plugin tests (social)
if: inputs.coverage != 'true'
shell: bash
working-directory: ds-bot/plugins/social
run: node --import ../../test/harness-resolve.mjs --test "test/*.test.js"
- name: Plugin tests (tree)
if: inputs.coverage != 'true'
shell: bash
working-directory: ds-bot/plugins/tree
run: node --test "test/*.test.js"
# The same three suites in one run, so coverage spans ds-bot and both plugins.
- name: Unit and plugin tests with coverage
if: inputs.coverage == 'true'
shell: bash
working-directory: ds-bot
env:
NODE_V: ${{ inputs.node }}
DSH: ${{ inputs.dsh }}
run: |
set -o pipefail
node --import ./test/harness-resolve.mjs --test --experimental-test-coverage \
--test-reporter=spec --test-reporter-destination=stdout \
--test-reporter=lcov --test-reporter-destination=lcov.info \
"test/*.test.js" "plugins/social/test/*.test.js" "plugins/tree/test/*.test.js" | tee test-output.txt
{
echo "### Coverage: ds-bot + social + tree (Node $NODE_V, DSH $DSH)"
echo '```'
grep -E 'file +\| line %|all files' test-output.txt | sed 's/^ℹ //' | head -2
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage
if: always() && inputs.coverage == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-lcov
path: ds-bot/lcov.info
if-no-files-found: ignore
retention-days: 14
34 changes: 34 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Weekly, grouped updates with a 7-day cooldown so a fresh release has a week to settle
# (retags, withdrawals) before it is proposed.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
github-actions:
patterns: ["*"]

- package-ecosystem: npm
# Plugins only declare peer dependencies (no lockfile), so ds-bot is the one npm project.
directory: /ds-bot

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the harness npm project in Dependabot

When this weekly job runs, Dependabot reads the npm project in the configured directory; ds-bot/package.json has no workspace entry for dev/harness, so /ds-bot does not discover dev/harness/package.json. Consequently its exact jsdom and oxlint dependencies, which supply test/lint tooling, never receive these updates. Add a separate npm update entry for the harness while retaining the DSH ignore there.

AGENTS.md reference: AGENTS.md:L30-L30

Useful? React with 👍 / 👎.

schedule:
interval: weekly
cooldown:
default-days: 7
semver-major-days: 7
semver-minor-days: 7
semver-patch-days: 7
# The DSH harness is pinned per matrix leg in workflows/test.yml; bump it there by hand.
ignore:
- dependency-name: "@deepseek-ai/*"
groups:
npm-dev:
dependency-type: development
patterns: ["*"]
npm-prod:
dependency-type: production
patterns: ["*"]
27 changes: 27 additions & 0 deletions .github/scripts/check-pack.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Packs each FlyBot package as a DSH profile would install it and checks the tarball:
// every `main`/`exports` target and every `files` entry is inside, nothing from test/ or dev/
// leaks in. Usage: node .github/scripts/check-pack.mjs [outDir] (run from the repo root;
// with outDir the .tgz files are kept there, for a release).
import { execFileSync } from 'node:child_process'
import { mkdirSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'

const packages = ['ds-bot', 'ds-bot/plugins/social', 'ds-bot/plugins/tree']
const out = process.argv[2] ? resolve(process.argv[2]) : undefined
if (out) mkdirSync(out, { recursive: true })
let failed = false
const targets = (exp) => typeof exp === 'string' ? [exp] : exp && typeof exp === 'object' ? Object.values(exp).flatMap(targets) : []
for (const dir of packages) {
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
const args = ['pack', '--json', '--ignore-scripts', ...(out ? ['--pack-destination', out] : ['--dry-run'])]
const [info] = JSON.parse(execFileSync('npm', args, { cwd: dir, encoding: 'utf8', shell: process.platform === 'win32' }))
const inside = new Set(info.files.map((f) => f.path))
const has = (p) => inside.has(p.replace(/^\.\//, '')) || [...inside].some((f) => f.startsWith(p.replace(/^\.\//, '').replace(/\/?$/, '/')))
const want = [pkg.main ?? 'index.js', ...targets(pkg.exports), ...(pkg.files ?? [])]
const missing = [...new Set(want)].filter((p) => !has(p))
const leaked = [...inside].filter((f) => /^(test|dev)\//.test(f))
console.log(`${pkg.name}@${pkg.version}: ${info.files.length} files, ${(info.size / 1024).toFixed(0)} KiB packed${out ? ` -> ${info.filename}` : ''}`)
for (const p of missing) { console.error(` missing from the package: ${p}`); failed = true }
for (const p of leaked) { console.error(` should not be packed: ${p}`); failed = true }
}
if (failed) process.exit(1)
10 changes: 10 additions & 0 deletions .github/scripts/release-notes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Prints the CHANGELOG.md section for one version (e.g. 0.1.5), for the release notes.
// Exits 1 when the changelog has no section for it.
import { readFileSync } from 'node:fs'

const version = process.argv[2]
const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n')
const start = lines.findIndex((l) => l.startsWith(`## [${version}]`))
if (!version || start < 0) { console.error(`CHANGELOG.md has no "## [${version}]" section`); process.exit(1) }
const end = lines.findIndex((l, i) => i > start && l.startsWith('## ['))
console.log(lines.slice(start + 1, end < 0 ? undefined : end).join('\n').trim())
50 changes: 50 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CodeQL

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Weekly, Sunday 07:13 UTC (off-peak).
- cron: "13 7 * * 0"

permissions: {}

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

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
actions: read
contents: read
# Uploads results to the Security tab; works with GITHUB_TOKEN, no secrets.
security-events: write
strategy:
fail-fast: false
matrix:
# javascript-typescript: ds-bot and its plugins (no build step needed).
# actions: data-flow analysis of these workflows, complementing zizmor.
language: [javascript-typescript, actions]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: ${{ matrix.language }}
build-mode: none
queries: security-and-quality
config: |
paths-ignore:
- ds-bot/client.js
- ds-bot/plugins/*/client.js
- "**/test/**"
- uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
category: "/language:${{ matrix.language }}"
84 changes: 84 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Release

# Pushing a v* tag (matching ds-bot/package.json's version) publishes a GitHub Release with
# the packed ds-bot, social and tree tarballs and that version's CHANGELOG.md section. It
# waits for the Test run on the tagged commit to pass first. Only GITHUB_TOKEN is used.

on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g. 0.1.6, no v prefix; the v tag must exist)'
required: true
type: string

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 75
permissions:
contents: write
actions: read
env:
GH_TOKEN: ${{ github.token }}
INPUT_VERSION: ${{ inputs.version }}
steps:
- name: Version
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/* ]]; then VERSION="${GITHUB_REF_NAME#v}"; else VERSION="$INPUT_VERSION"; fi
# a newline or quote would add outputs
[[ "$VERSION" =~ ^[0-9][0-9A-Za-z.+-]*$ ]] || { echo "bad version: $VERSION" >&2; exit 1; }
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "ref=v$VERSION" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.version.outputs.ref }}
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24
# No cache in a job that publishes: nothing restored from a cache goes into a release.
package-manager-cache: false
- name: Tag matches ds-bot/package.json
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
have=$(node -p "require('./ds-bot/package.json').version")
[ "$have" = "$VERSION" ] || { echo "ds-bot/package.json is $have, tag is v$VERSION" >&2; exit 1; }
- name: Wait for a green Test run on this commit
run: |
sha=$(git rev-parse HEAD)
for i in $(seq 1 120); do
c=$(gh run list -R "$GITHUB_REPOSITORY" --workflow Test --commit "$sha" --json status,conclusion \
-q '[.[] | select(.status == "completed")] | map(.conclusion) | if any(. == "success") then "success" elif length > 0 and all(. != "success") then "failure" else "" end')
echo "attempt $i: ${c:-pending}"
[ "$c" = success ] && exit 0
[ "$c" = failure ] && { echo "Test failed on $sha" >&2; exit 1; }
sleep 30
done
echo "no completed Test run on $sha" >&2; exit 1
- name: Pack
run: node .github/scripts/check-pack.mjs dist
- name: Publish the release
env:
VERSION: ${{ steps.version.outputs.version }}
REF: ${{ steps.version.outputs.ref }}
run: |
set -euo pipefail
node .github/scripts/release-notes.mjs "$VERSION" > notes.md
if gh release view "$REF" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$REF" -R "$GITHUB_REPOSITORY" --notes-file notes.md
gh release upload "$REF" dist/*.tgz -R "$GITHUB_REPOSITORY" --clobber
else
gh release create "$REF" dist/*.tgz -R "$GITHUB_REPOSITORY" --verify-tag --title "FlyBot $REF" --notes-file notes.md
fi
Loading
Loading