dsec-discord-bot/.github/workflows/ci.yml
Clupai8o0 b960d84bfc COL-BOT-05: parse all config once in AppState, remove per-event env reads
Finishes the AppState config hoisting (COR-12's groundwork was not present in
the repo, so this ticket establishes it): AppState now carries guild_id,
honeypot_channel_id, leetcode_channel_id, verified_role_id, logs_channel_id and
weather_token, all parsed once in AppState::new(). A missing or unparseable id
now produces one boot error naming every offending variable plus a pointer to
.env.example, instead of a per-event .expect() panic (a data race under the
multi-threaded runtime, since dotenv() calls the now-unsafe set_var).

- main.rs: new fields, required_u64() collector, fail-fast with combined message.
- events/message.rs: honeypot/create_leetcode_thread/on_message take &Data and
  read channel/guild ids from AppState; dropped dotenv() and env::var.
- events/interaction_create.rs: deleted verified_role_id(); use
  data.state.verified_role_id.
- commands/mods_only.rs: log_embed takes logs_channel_id: ChannelId; caller in
  message.rs passes data.state.logs_channel_id.
- commands/weather.rs: WEATHER_TOKEN read once into AppState (non-fatal), passed
  to get_weather — required so the new CI grep guard can be clean.
- .env.example: add LEETCODE_CHANNEL_ID.
- .github/workflows/ci.yml: add "No environment reads outside main.rs" guard.

grep -rn 'dotenv()\|env::var' src/ | grep -v '^src/main.rs:' now returns nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
2026-08-30 15:35:16 +10:00

93 lines
3.7 KiB
YAML

# Format, lint, build and test on every pull request.
#
# This must run on a GitHub-hosted runner and never on the club VPS. The runner
# deploy.yml uses lives on the club's own server and executes whatever a workflow
# tells it to, as root, so nothing triggered by a pull request may be pointed at
# it. Read that as a rule for this file, not a property of the repository: this
# repo is public, forking is on, and fork pull requests only need approval from a
# first-time contributor, so a fork that brings its own workflow can still reach
# that runner. See SEC-01.
#
# SEC-01: point the protect-main ruleset's required status check at this job's
# name ("fmt / clippy / build / test") so a red build blocks the merge that
# deploys.
name: CI
on:
pull_request:
# A newer push to the same pull request makes an in-flight run irrelevant.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check:
name: fmt / clippy / build / test
runs-on: ubuntu-latest
# A cold build of this dependency tree (serenity, reqwest, image, exr) in
# release, then clippy, then test, is slow on a 4-vCPU hosted runner. This is
# a guard against a hung job, not a target.
timeout-minutes: 45
steps:
# Third-party and first-party actions alike are pinned to a full commit
# SHA: a tag is mutable and can be repointed at new code by whoever owns
# the action.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Caches written by a pull_request run are scoped to that pull request, so
# this pays off across pushes to the same branch rather than across branches.
- name: Cache cargo registry and build artifacts
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
# actions/runner-images documents rustfmt for ubuntu-24.04 but not clippy (it
# lists clippy only under ubuntu-22.04), while the image actually in use does
# ship it. Adding both components is a no-op when they are present and keeps
# this job working if that undocumented extra ever goes away. Print the
# versions too: clippy's lint set moves between releases, so knowing which one
# ran is what explains a lint that appeared from nowhere.
- name: Toolchain
run: |
rustup component add clippy rustfmt
cargo --version
cargo fmt --version
cargo clippy --version
- name: Format
run: cargo fmt --all -- --check
# Kept ahead of clippy and test: those resolve dependencies and would
# refresh Cargo.lock in place, so a stale lockfile would slip past
# --locked if they ran first.
- name: Build
run: cargo build --locked --release
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
- name: Test
run: cargo test
# COL-BOT-05: configuration is parsed once in main.rs and stored on
# AppState. A per-event dotenv()/env::var read is a data race under the
# multi-threaded runtime (set_var is unsafe in edition 2024) and turns a
# config typo into a random future handler failure instead of a boot error.
- name: No environment reads outside main.rs
run: |
if grep -rn 'dotenv()\|env::var' src/ | grep -v '^src/main.rs:'; then
echo "::error::Environment variables must be read once in main.rs and stored on AppState."
exit 1
fi