Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
50 changes: 50 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Deploy BobaTalks Flowers Bot on pm2 (EC2 Instance)

on:
push: # runs tests on every push
branches: [main]
schedule: # deploys at 09:00 UTC daily (4am Toronto, UTC-5)
- cron: '0 9 * * *'
workflow_dispatch: # allow manual deploys too

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npm run build
- run: npm test --if-present
- run: tar czf artifact.tgz dist package.json package-lock.json pm2.config.cjs
- uses: actions/upload-artifact@v4
with: { name: build-artifact, path: artifact.tgz }

deploy:
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { name: build-artifact, path: . }
- uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
source: artifact.tgz
target: /home/ubuntu/bobatalks/
- uses: appleboy/ssh-action@v0.1.10
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
set -e
cd ~/bobatalks
tar xzf artifact.tgz
npm ci --omit=dev
pm2 start pm2.config.cjs --only bobatalks || true
pm2 reload bobatalks
pm2 save
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,66 @@ The Discord bot code is located in `src/bot/`. See [src/bot/README.md](src/bot/R

---

## 🚀 Deployment to EC2

The bot auto-deploys daily at **4 AM Toronto time** (9 AM UTC) via GitHub Actions.

### Prerequisites

**1. Add GitHub Secrets** (`Settings → Secrets and variables → Actions`):

- `EC2_HOST` → Your EC2 IP address
- `EC2_SSH_KEY` → Your entire `.pem` file contents

**2. Setup EC2** (one-time):

```bash
ssh -i your-key.pem ubuntu@your-ec2-ip

# Install Node.js 20 & PM2
sudo apt-get update
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo npm install -g pm2

# Setup project
mkdir -p ~/bobatalks/logs
cd ~/bobatalks

# Create .env file
nano .env
# Add: DISCORD_TOKEN=... and DISCORD_CLIENT_ID=...

# Enable PM2 on boot
pm2 startup systemd -u ubuntu --hp /home/ubuntu
# Run the sudo command that PM2 prints
```

**3. Deploy**:

- **Auto**: Pushes to `main` deploy at 4 AM daily
- **Manual**: GitHub → Actions → "Deploy BobaTalks" → Run workflow

**4. Register commands** (after first deploy):

```bash
ssh -i your-key.pem ubuntu@your-ec2-ip
cd ~/bobatalks
node dist/bot/registerCommands.js
pm2 logs bobatalks
```

### Useful PM2 Commands

```bash
pm2 status # Check status
pm2 logs bobatalks # View logs
pm2 restart bobatalks # Restart
pm2 reload bobatalks # Zero-downtime reload
```

---

> 💡 _Run this one-liner to confirm everything works:_
>
> ```bash
Expand Down
121 changes: 75 additions & 46 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,85 @@ import sonarjs from 'eslint-plugin-sonarjs';
import unusedImports from 'eslint-plugin-unused-imports';
import tseslint from 'typescript-eslint';

export default tseslint.config(js.configs.recommended, ...tseslint.configs.recommended, {
ignores: ['node_modules', 'dist', 'build'],
files: ['**/*.{js,ts,tsx}'],
languageOptions: {
parser: tseslint.parser,
ecmaVersion: 2022,
sourceType: 'module',
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ['node_modules', 'dist', 'build'],
},
plugins: {
'@typescript-eslint': tseslint.plugin,
prettier,
import: importPlugin,
'unused-imports': unusedImports,
sonarjs,
// CommonJS files (.cjs)
{
files: ['**/*.cjs'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'commonjs',
globals: {
module: 'readonly',
require: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
exports: 'readonly',
process: 'readonly',
},
},
plugins: {
prettier,
},
rules: {
'prettier/prettier': ['error', { endOfLine: 'auto' }],
},
},
rules: {
// 🧹 Prettier integration
'prettier/prettier': ['error', { endOfLine: 'auto' }],
// ES Modules and TypeScript files
{
files: ['**/*.{js,ts,tsx}'],
languageOptions: {
parser: tseslint.parser,
ecmaVersion: 2022,
sourceType: 'module',
},
plugins: {
'@typescript-eslint': tseslint.plugin,
prettier,
import: importPlugin,
'unused-imports': unusedImports,
sonarjs,
},
rules: {
// 🧹 Prettier integration
'prettier/prettier': ['error', { endOfLine: 'auto' }],

// 🚫 Unused imports / variables
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'error',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
// 🚫 Unused imports / variables
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'error',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],

// 📦 Import hygiene
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/no-duplicates': 'error',
'import/no-cycle': 'warn',
// 📦 Import hygiene
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
],
'import/no-duplicates': 'error',
'import/no-cycle': 'warn',

// 🧠 Logic & readability (SonarJS)
'sonarjs/cognitive-complexity': ['warn', 15],
'sonarjs/no-duplicate-string': 'warn',
'sonarjs/no-identical-functions': 'warn',
// 🧠 Logic & readability (SonarJS)
'sonarjs/cognitive-complexity': ['warn', 15],
'sonarjs/no-duplicate-string': 'warn',
'sonarjs/no-identical-functions': 'warn',

// ⚙️ TS-specific tuning
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'off', // handled by unused-imports
// ⚙️ TS-specific tuning
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'off', // handled by unused-imports
},
},
});
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
]
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "echo \"No tests yet\" && exit 0",
"lint": "eslint . --ext .js,.ts,.tsx",
"format": "prettier --write .",
"prepare": "husky",
Expand Down
19 changes: 19 additions & 0 deletions pm2.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module.exports = {
apps: [
{
name: 'bobatalks',
script: 'dist/bot/index.js',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
},
error_file: 'logs/err.log',
out_file: 'logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
},
],
};