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
81 changes: 81 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Deploy BobaTalks Flowers Bot on pm2 (EC2 Instance)

on:
push: # runs tests on every push (no deploy)
branches: [main]
schedule: # checks at 09:00 UTC daily - only deploys if new commits
- cron: '0 9 * * *'
workflow_dispatch: # manual deploys always run

jobs:
check-changes:
runs-on: ubuntu-latest
outputs:
should_deploy: ${{ steps.check.outputs.should_deploy }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history to check commits
- id: check
name: Check for new commits
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "Manual trigger - will deploy"
echo "should_deploy=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "push" ]; then
echo "Push event - will test only"
echo "should_deploy=false" >> $GITHUB_OUTPUT
else
# For scheduled runs, check if there are commits in the last 24 hours
COMMITS=$(git log --since="24 hours ago" --oneline | wc -l)
if [ "$COMMITS" -gt 0 ]; then
echo "Found $COMMITS new commit(s) in the last 24 hours - will deploy"
echo "should_deploy=true" >> $GITHUB_OUTPUT
else
echo "No new commits in the last 24 hours - skipping deployment"
echo "should_deploy=false" >> $GITHUB_OUTPUT
fi
fi

test:
needs: check-changes
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
if: needs.check-changes.outputs.should_deploy == 'true'
with: { name: build-artifact, path: artifact.tgz }

deploy:
if: needs.check-changes.outputs.should_deploy == 'true'
needs: [check-changes, 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,
},
],
};