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
44 changes: 44 additions & 0 deletions .github/workflows/audit-cron.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Security Audit (daily)

# Runs `cargo audit` and `cargo deny` on a fixed cron so newly-published CVEs
# and dependency policy regressions are caught even if the repository has not
# been modified.

on:
schedule:
# 04:17 UTC daily - off-peak for GitHub Actions scheduler.
- cron: "17 4 * * *"
workflow_dispatch:

permissions:
contents: read
issues: write

concurrency:
group: audit-cron-${{ github.ref }}
cancel-in-progress: true

jobs:
audit:
name: cargo audit (scheduled)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
- uses: actions/checkout@v6
- uses: rustsec/audit-check@v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}

deny:
name: cargo deny (scheduled)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
- uses: actions/checkout@v6
- uses: EmbarkStudios/cargo-deny-action@v2
30 changes: 30 additions & 0 deletions .github/workflows/auto-merge-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Auto-merge release PRs

on:
pull_request:
types: [opened, reopened, labeled, synchronize, ready_for_review]

permissions:
contents: write
pull-requests: write

jobs:
auto-merge:
if: >-
contains(github.event.pull_request.labels.*.name, 'release')
&& startsWith(github.event.pull_request.head.ref, 'release-plz-')
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
env:
HAS_RELEASE_PLZ_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN != '' }}
steps:
- name: Skip when RELEASE_PLZ_TOKEN is missing
if: env.HAS_RELEASE_PLZ_TOKEN != 'true'
run: echo "::notice::RELEASE_PLZ_TOKEN is not configured; release PR auto-merge skipped."

- name: Enable auto-merge
if: env.HAS_RELEASE_PLZ_TOKEN == 'true'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
24 changes: 24 additions & 0 deletions .github/workflows/auto-update-branch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Auto-update PR branches on main push

on:
push:
branches: [main]

permissions:
contents: write
pull-requests: write

jobs:
update-prs:
runs-on: ubuntu-latest
steps:
- name: Update BEHIND PRs with auto-merge
run: |
gh pr list --base main --state open --json number,autoMergeRequest,mergeStateStatus \
--jq '.[] | select(.autoMergeRequest != null) | select(.mergeStateStatus=="BEHIND") | .number' \
| while read -r pr; do
echo "Updating PR #${pr}"
gh pr update-branch "${pr}" || echo "Skip PR #${pr}"
done
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
name: CI

on:
merge_group:
push:
branches: [main]
tags:
- "v*"
paths-ignore:
- "docs/**"
- "**.md"
- "LICENSE*"
- ".editorconfig"
- ".gitignore"
- ".gitattributes"
- "**.txt"
- "CODEOWNERS"
- ".vscode/**"
pull_request:
workflow_dispatch:
inputs:
Expand Down
98 changes: 98 additions & 0 deletions .github/workflows/cleanup-branches.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Cleanup stale branches

on:
# Run weekly on Monday at 06:00 UTC.
schedule:
- cron: "0 6 * * 1"
# Allow manual trigger.
workflow_dispatch:
# Delete head branch right after PR merge.
pull_request:
types: [closed]

permissions:
contents: write

jobs:
# Delete the PR head branch immediately after merge.
delete-pr-branch:
if: github.event_name == 'pull_request' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Delete merged branch
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const branch = pr.head.ref;
const protectedNames = new Set(['main', 'master', 'develop', 'release']);
const protectedPrefixes = ['release/', 'hotfix/', 'dependabot/'];
if (pr.head.repo.full_name !== context.repo.owner + '/' + context.repo.repo) {
console.log(`Skipping branch from fork: ${pr.head.repo.full_name}:${branch}`);
return;
}
if (protectedNames.has(branch) || protectedPrefixes.some(prefix => branch.startsWith(prefix))) {
console.log(`Skipping protected branch: ${branch}`);
return;
}
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${branch}`,
});
console.log(`Deleted branch: ${branch}`);
} catch (e) {
console.log(`Could not delete ${branch}: ${e.message}`);
}

# Weekly cleanup of old release-plz and agent (claude/* , codex/*) branches.
sweep-stale:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const protectedBranches = ['main', 'master', 'develop', 'release'];
const protectedPrefixes = ['release/', 'hotfix/', 'dependabot/'];
// Patterns for branches that should be auto-cleaned.
const stalePatterns = [/^release-plz-/, /^claude\//, /^codex\//];

const { data: branches } = await github.rest.repos.listBranches({
owner, repo, per_page: 100,
});

let deleted = 0;
for (const branch of branches) {
if (protectedBranches.includes(branch.name)) continue;
if (protectedPrefixes.some(prefix => branch.name.startsWith(prefix))) continue;
if (!stalePatterns.some(p => p.test(branch.name))) continue;

// Only delete if fully merged into main.
try {
const { data: comparison } = await github.rest.repos.compareCommits({
owner, repo,
base: 'main',
head: branch.name,
});
// ahead_by == 0 means all commits are in main.
if (comparison.ahead_by !== 0) continue;
} catch {
continue;
}

try {
await github.rest.git.deleteRef({
owner, repo,
ref: `heads/${branch.name}`,
});
console.log(`Deleted: ${branch.name}`);
deleted++;
} catch (e) {
console.log(`Failed to delete ${branch.name}: ${e.message}`);
}
}
console.log(`Cleaned up ${deleted} stale branches.`);
56 changes: 56 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: "CodeQL Security Analysis"

on:
push:
branches: [main]
paths:
- "crates/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/codeql.yml"
pull_request:
branches: [main]
paths:
- "crates/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/codeql.yml"
schedule:
# Weekly scan (Mondays 06:00 UTC)
- cron: "0 6 * * 1"

permissions:
security-events: write
contents: read

jobs:
analyze:
name: CodeQL Rust Analysis
runs-on: macos-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: codeql
save-if: ${{ github.ref == 'refs/heads/main' }}

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: rust

- name: Build for CodeQL
run: cargo build --workspace --release --locked

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:rust"
116 changes: 116 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Nightly Toolchain Check

on:
schedule:
# Every day at 03:00 UTC.
- cron: "0 3 * * *"
workflow_dispatch:

permissions:
contents: read
issues: write # Used by the failure alert step below

env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUST_BACKTRACE: short

jobs:
check:
name: ${{ matrix.toolchain }} / ${{ matrix.scope }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
# Nightly may break - allow failure. Beta must pass.
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- toolchain: beta
os: macos-latest
scope: workspace
cargo_args: "--workspace --all-features"
- toolchain: nightly
os: macos-latest
scope: workspace
cargo_args: "--workspace --all-features"
- toolchain: beta
os: ubuntu-latest
scope: pure-crates
cargo_args: "-p dunst-core -p dunst-graph -p dunst-vision"
- toolchain: nightly
os: ubuntu-latest
scope: pure-crates
cargo_args: "-p dunst-core -p dunst-graph -p dunst-vision"
steps:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
- uses: actions/checkout@v6
with:
ref: main
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
components: clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: nightly-${{ matrix.toolchain }}-${{ matrix.scope }}-${{ matrix.os }}
save-if: true
- name: Clippy
run: cargo clippy ${{ matrix.cargo_args }} --all-targets --locked -- -D warnings
- name: Tests
run: cargo test ${{ matrix.cargo_args }} --all-targets --locked
- name: Doc tests
run: cargo test ${{ matrix.cargo_args }} --doc --locked

# Open (or reuse) a tracking issue when the BETA matrix fails. Nightly
# failures stay silent (they are allowed to break). Avoids spamming: the
# step searches for an open "nightly-toolchain" labeled issue and comments
# on it instead of creating duplicates.
alert:
name: Alert on beta regression
needs: [check]
if: always() && contains(needs.check.result, 'failure') && github.event_name == 'schedule'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
- name: Open or update tracking issue
uses: actions/github-script@v7
with:
script: |
const label = 'nightly-toolchain';
const title = `Beta/nightly toolchain regression (${new Date().toISOString().slice(0,10)})`;
const body = [
`Nightly run ${context.runId} observed a failure on the \`beta\` or \`nightly\` toolchain.`,
``,
`Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
``,
`If this is a nightly-only soft failure, close the issue. If beta is broken, this is a release blocker.`,
].join('\n');
const { data: issues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: label,
per_page: 1,
});
if (issues.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issues[0].number,
body,
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: [label],
});
}
Loading
Loading