Skip to content
Closed
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
140 changes: 140 additions & 0 deletions .github/workflows/CICD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# CI/CD Flows

## PR Quality Gates (ci.yml)

Trigger: pull_request to develop or master

```
┌──────────────────┐
│ PR opened │
└────────┬─────────┘
┌────────▼─────────┐
│ fmt --all │
└────────┬─────────┘
┌───────────▼──────────┐
│ clippy --all-targets │
└───┬───┬───┬───┬───┬──┘
│ │ │ │ │
┌───────────────┘ │ │ │ └────────────────┐
│ ┌───────────┘ │ └───────────┐ │
▼ ▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌─────────┐ ┌──────────┐
│ test │ │ security │ │ semgrep │ │benchmark│ │ doc │
│ ubuntu │ │ cargo │ │ AST-aware │ │ >=80% │ │ review │
│ windows │ │ audit │ │ diff-only │ │ savings │ │ ai agent │
│ macos │ │ patterns │ │ │ │ │ │ │
└────┬─────┘ └────┬─────┘ └─────┬─────┘ └────┬────┘ └────┬─────┘
│ │ │ │ │
└────────────┴─────────┬───┴─────────────┴────────────┘
┌──────────▼─────────┐
│ All must pass │
│ to merge │
└────────────────────┘

+ DCO check (independent, develop PRs only)
+ Dependabot (weekly: Cargo deps + GitHub Actions)
```

## Merge to develop — pre-release (cd.yml)

Trigger: push to develop | workflow_dispatch (not master) | Concurrency: cancel-in-progress

```
┌──────────────────┐
│ push to develop │
│ OR dispatch │
└────────┬─────────┘
┌────────▼──────────────────┐
│ pre-release │
│ compute next version │
│ from conventional commits │
│ tag = v{next}-rc.{run} │
└────────┬──────────────────┘
┌────────▼──────────────────┐
│ release.yml │
│ prerelease = true │
└────────┬──────────────────┘
┌────────▼──────────────────┐
│ Build │
│ 5 platforms + DEB + RPM │
└────────┬──────────────────┘
┌────────▼──────────────────┐
│ GitHub Release │
│ (pre-release badge) │
│ │
│ Discord: SKIPPED │
│ Homebrew: SKIPPED │
└──────────────────────────┘
```

## Merge to master — stable release (cd.yml)

Trigger: push to master (only) | Concurrency: never cancelled

```
┌──────────────────┐
│ push to master │
└────────┬─────────┘
┌────────▼──────────────────┐
│ release-please │
│ analyze conventional │
│ commits │
└────────┬──────────────────┘
┌────┴────────────────┐
│ │
no release release created
│ │
▼ ▼
┌──────────────┐ ┌───────────────────────┐
│ create/update│ │ release.yml │
│ release PR │ │ prerelease = false │
└──────────────┘ └───────────┬───────────┘
┌────────────▼────────────┐
│ Build │
│ 5 platforms + DEB + RPM │
└────────────┬────────────┘
┌────────────▼────────────┐
│ GitHub Release │
│ (stable, "Latest" badge) │
└──┬─────────┬─────────┬──┘
│ │ │
▼ ▼ ▼
Discord Homebrew latest
notify tap update tag
```

## Manual release (release.yml)

Trigger: workflow_dispatch

```
┌────────────────────────┐
│ workflow_dispatch │
│ inputs: tag, prerelease │
└───────────┬────────────┘
┌───────────▼────────────┐
│ Full build pipeline │
│ 5 platforms + DEB + RPM │
└───────────┬────────────┘
┌──────┴──────┐
│ │
prerelease=false prerelease=true
│ │
▼ ▼
Discord pre-release
Homebrew badge only
latest tag
```
155 changes: 155 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: CD

on:
workflow_dispatch:
push:
branches: [develop, master]

concurrency:
group: cd-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

permissions:
contents: write
pull-requests: write

jobs:
# ═══════════════════════════════════════════════
# DEVELOP PATH: Pre-release
# ═══════════════════════════════════════════════

pre-release:
if: >-
github.ref == 'refs/heads/develop'
|| (github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/master')
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true

- name: Compute version from commits like release please
id: tag
run: |
LATEST_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-version:refname | grep -v '-' | head -1)
if [ -z "$LATEST_TAG" ]; then
echo "::error::No stable release tag found"
exit 1
fi
LATEST_VERSION="${LATEST_TAG#v}"
echo "Latest release: $LATEST_TAG"

# ── Analyse conventional commits since that tag ──
COMMITS=$(git log "${LATEST_TAG}..HEAD" --format="%s")
HAS_BREAKING=$(echo "$COMMITS" | grep -cE '^[a-z]+(\(.+\))?!:' || true)
HAS_FEAT=$(echo "$COMMITS" | grep -cE '^feat(\(.+\))?:' || true)
HAS_FIX=$(echo "$COMMITS" | grep -cE '^fix(\(.+\))?:' || true)
echo "Commits since ${LATEST_TAG} — breaking=$HAS_BREAKING feat=$HAS_FEAT fix=$HAS_FIX"

# ── Compute next version (matches release-please observed behaviour) ──
# Pre-1.0 with bump-minor-pre-major: breaking → minor, feat → minor, fix → patch
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_VERSION"
if [ "$MAJOR" -eq 0 ]; then
if [ "$HAS_BREAKING" -gt 0 ] || [ "$HAS_FEAT" -gt 0 ]; then
MINOR=$((MINOR + 1)); PATCH=0 # breaking or feat → minor
else
PATCH=$((PATCH + 1)) # fix only → patch
fi
else
if [ "$HAS_BREAKING" -gt 0 ]; then
MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 # breaking → major
elif [ "$HAS_FEAT" -gt 0 ]; then
MINOR=$((MINOR + 1)); PATCH=0 # feat → minor
else
PATCH=$((PATCH + 1)) # fix → patch
fi
fi
VERSION="${MAJOR}.${MINOR}.${PATCH}"
TAG="dev-${VERSION}-rc.${{ github.run_number }}"

echo "Next version: $VERSION (from $LATEST_VERSION)"
echo "Pre-release tag: $TAG"

# Safety: fail if this exact tag already exists
if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
echo "::error::Tag ${TAG} already exists"
exit 1
fi

echo "tag=$TAG" >> $GITHUB_OUTPUT

build-prerelease:
name: Build pre-release
needs: pre-release
if: needs.pre-release.outputs.tag != ''
uses: ./.github/workflows/release.yml
with:
tag: ${{ needs.pre-release.outputs.tag }}
prerelease: true
permissions:
contents: write
secrets: inherit

# ═══════════════════════════════════════════════
# MASTER PATH: Full release
# ═══════════════════════════════════════════════

release-please:
if: github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ secrets.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: rust
package-name: rtk
token: ${{ steps.app-token.outputs.token }}

build-release:
name: Build and upload release assets
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
uses: ./.github/workflows/release.yml
with:
tag: ${{ needs.release-please.outputs.tag_name }}
permissions:
contents: write
secrets: inherit

update-latest-tag:
name: Update 'latest' tag
needs: [release-please, build-release]
if: ${{ needs.release-please.outputs.release_created == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ secrets.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write

- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}

- name: Update latest tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa latest -m "Latest stable release (${{ needs.release-please.outputs.tag_name }})"
git push origin latest --force
99 changes: 99 additions & 0 deletions .github/workflows/ci-self-hosted.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: CI (self-hosted)

# Routes the compile-heavy cargo test pass to a self-hosted
# Linux/X64 runner. Triggered only on pushes to in-repo branches and on
# manual dispatch — NEVER on `pull_request`, because fork PRs from
# outside contributors must not be able to execute code on the
# self-hosted box (they hit the existing `ubuntu-latest` ci.yml jobs
# instead, where GitHub sandboxes the work).
#
# Belt-and-braces: GitHub repo setting "Require approval for all
# outside collaborators" must also be enabled in Settings -> Actions
# -> General so even the cloud-hosted workflows don't fire on a fork
# PR without manual approval.

on:
push:
branches:
- develop
- main
- 'feat/**'
- 'fix/**'
- 'harden/**'
- 'polish/**'
- 'perf/**'
- 'docs/**'
- 'ci/**'
workflow_dispatch:

# Tighten the default GITHUB_TOKEN to read-only. Per-job permissions can
# override if needed (none of these jobs write to the repo).
permissions:
contents: read

concurrency:
group: self-hosted-${{ github.ref }}
cancel-in-progress: true

# All third-party actions pinned to commit SHAs (not tags) so a
# compromised tag re-point cannot poison the self-hosted runner.
# Version comments reflect the tag/branch the SHA resolved from at
# pinning time — update via Dependabot or manual re-resolve.

jobs:
test:
name: cargo test (self-hosted)
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable channel ref

- name: Cargo cache
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: self-hosted-stable

- name: cargo test --bin contextcrawler
run: cargo test --bin contextcrawler --no-fail-fast

- name: Upload tee logs on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: rtk-tee-logs-${{ github.run_id }}
path: ~/.local/share/rtk/tee/
if-no-files-found: ignore
retention-days: 7

clippy:
name: cargo clippy (self-hosted)
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable channel ref
with:
components: clippy

- name: Cargo cache
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: self-hosted-stable

# Clippy runs in visible-but-non-fatal mode while the codebase
# works through new doc/MSRV lints introduced by post-1.80
# toolchains. Re-tighten to `-- -D warnings` after lint cleanup
# lands (tracked separately).
- name: cargo clippy
run: cargo clippy --bin contextcrawler --all-features
Loading
Loading