Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a734edd76c
|
||
|
|
64420cc224 | ||
|
|
bdd26a9189
|
||
|
|
84a64f1bde
|
||
|
|
4d2e92f6bb | ||
|
|
eb5519be87 | ||
|
|
f7f5290404
|
||
|
|
dffa74f47c
|
||
|
|
c455aa894e
|
||
|
|
513eae640f
|
||
|
|
475f0f9903
|
||
|
|
db42d73adb | ||
|
|
074d5a7ff2 | ||
|
|
5ac35bb370
|
||
|
|
4b5de248cd
|
||
|
|
0f01f53391 | ||
|
|
64a0aaa772
|
||
|
|
d247777667
|
||
|
|
9507bb1cca
|
||
|
|
df58f0f08f
|
||
|
|
2a059466b3
|
||
|
|
12828d45e1
|
||
|
|
32eb649c20
|
||
|
|
5f1d677485
|
||
|
|
80be163e55
|
||
|
|
5b385b4277
|
||
|
|
dcc7812501 | ||
|
|
113c5d2120 | ||
|
|
65c115932f
|
||
|
|
10a3ae0bf7
|
||
|
|
9c14d965db
|
||
|
|
e85c613e7d
|
||
|
|
b8a4d2d665
|
||
|
|
7ee0c2d272
|
||
|
|
8359d6e4bf
|
||
|
|
3d33972e98
|
||
|
|
5768604ac9
|
||
|
|
07ef691bc1
|
||
|
|
4d110b7c52
|
||
|
|
5f4be34a73
|
||
|
|
2a16a866ed
|
||
|
|
e96e9dc46a
|
||
|
|
b4d2e387da
|
||
|
|
e8820fde59
|
||
|
|
906aa81c6c
|
||
|
|
9855f91adf
|
||
|
|
7821dc40be
|
||
|
|
b540663579
|
||
|
|
b14e880b4b
|
||
|
|
432cad90dc
|
||
|
|
1b9a290260
|
||
|
|
ea1530680c
|
||
|
|
072bde05e1
|
||
|
|
b3ada88288
|
||
|
|
a91e76b3e1
|
||
|
|
f1c261ebdd
|
||
|
|
186251e21a
|
||
|
|
eac398a73e
|
||
|
|
50a093afcf
|
||
|
|
9800ddcfc4
|
||
|
|
90c9a20871
|
||
|
|
134d3048cf
|
||
|
|
9445a82b53
|
||
|
|
f56a5c71ab
|
||
|
|
9a59fff032
|
||
|
|
d9089afdc9
|
||
|
|
59a81773cc
|
||
|
|
f7957ab9f5
|
||
|
|
0840f93d0d
|
@@ -0,0 +1,54 @@
|
||||
name: Code Quality (Auto-Format & Clippy-Fix)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
fix:
|
||||
name: Formatierung & Clippy automatisch beheben
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ gitea.ref_name || github.ref_name }}
|
||||
token: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy, rustfmt
|
||||
cache: false
|
||||
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
|
||||
- name: Formatierung automatisch beheben
|
||||
run: cargo fmt
|
||||
|
||||
- name: Clippy-Fixes automatisch anwenden
|
||||
run: cargo clippy --fix --allow-dirty --allow-staged --all-targets
|
||||
|
||||
- name: Änderungen committen & pushen
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git config user.name "Gitea-Bot"
|
||||
git config user.email "no-reply@creativedragonslayer.de"
|
||||
git add -A
|
||||
git commit -m "Style: Automatische Formatierung & Clippy-Fixes"
|
||||
git push origin HEAD:${{ gitea.ref_name || github.ref_name }}
|
||||
else
|
||||
echo "Keine Formatierungs- oder Clippy-Änderungen."
|
||||
fi
|
||||
@@ -6,31 +6,103 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Erkenne relevante Code-Änderungen
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code_changed: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Prüfe auf Änderungen am Programmcode
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'src/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'scripts/**'
|
||||
- '.cargo/**'
|
||||
- 'Dockerfile'
|
||||
- '.gitea/workflows/main.yaml'
|
||||
|
||||
release-and-publish:
|
||||
name: Build, Publish Packages (Stable) & Create Release
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_BINSTALL_VERSION: "1.23.0"
|
||||
CARGO_DEB_VERSION: "3.8.0"
|
||||
CARGO_GENERATE_RPM_VERSION: "0.21.0"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||
with:
|
||||
toolchain: stable
|
||||
cache: false
|
||||
|
||||
- name: Install Cross-Compilation Toolchains & Dependencies
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
|
||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||
run: rm -rf target/debian target/generate-rpm target/arch
|
||||
|
||||
- name: Install Cross-Compilation Toolchains (apt)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
|
||||
rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
||||
|
||||
- name: Install Packaging Tools (Prebuilt Binaries)
|
||||
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
||||
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache Rustup Cross-Compilation-Targets
|
||||
id: cache-rustup-targets
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
|
||||
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Add Rust Cross-Compilation Targets
|
||||
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
||||
run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
||||
|
||||
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
||||
run: |
|
||||
mkdir -p ~/.cargo/bin
|
||||
curl -fsSL https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xz -C ~/.cargo/bin
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
~/.cargo/bin/cargo-binstall -y --no-symlinks cargo-deb cargo-generate-rpm
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache Packaging-Tools (cargo-binstall, cargo-deb, cargo-generate-rpm)
|
||||
id: cache-packaging-tools
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-binstall
|
||||
~/.cargo/bin/cargo-deb
|
||||
~/.cargo/bin/cargo-generate-rpm
|
||||
key: packaging-tools-${{ runner.os }}-${{ env.CARGO_BINSTALL_VERSION }}-${{ env.CARGO_DEB_VERSION }}-${{ env.CARGO_GENERATE_RPM_VERSION }}
|
||||
|
||||
- name: Install Packaging Tools (Prebuilt Binaries)
|
||||
if: steps.cache-packaging-tools.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -fsSL "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-x86_64-unknown-linux-musl.tgz" | tar -xz -C ~/.cargo/bin
|
||||
~/.cargo/bin/cargo-binstall -y --no-symlinks "cargo-deb@${CARGO_DEB_VERSION}" "cargo-generate-rpm@${CARGO_GENERATE_RPM_VERSION}"
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
name: Dependency-Updates prüfen & Pull Requests erstellen
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:44.83.0
|
||||
steps:
|
||||
- name: Renovate ausführen
|
||||
run: renovate
|
||||
env:
|
||||
RENOVATE_PLATFORM: gitea
|
||||
RENOVATE_ENDPOINT: ${{ gitea.server_url || github.server_url }}/api/v1/
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
RENOVATE_REPOSITORIES: ${{ gitea.repository || github.repository }}
|
||||
RENOVATE_AUTODISCOVER: "false"
|
||||
RENOVATE_ALLOW_CUSTOM_CRATE_REGISTRIES: "true"
|
||||
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate-bot@creativedragonslayer.de>"
|
||||
RENOVATE_HOST_RULES: >-
|
||||
[{"hostType":"cargo","matchHost":"${{ gitea.server_url || github.server_url }}","token":"${{ secrets.RENOVATE_TOKEN }}"}]
|
||||
GITHUB_COM_TOKEN: ${{ secrets.GH_RENOVATE_TOKEN }}
|
||||
LOG_LEVEL: info
|
||||
@@ -0,0 +1,95 @@
|
||||
name: Security Scans
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- testing
|
||||
- dev
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 5 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
name: Trivy & OSV-Scanner
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TRIVY_VERSION: "0.74.0"
|
||||
OSV_SCANNER_VERSION: "2.5.1"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Lokales bin-Verzeichnis zum PATH hinzufügen
|
||||
run: |
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache Trivy-Binary
|
||||
id: cache-trivy
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.local/bin/trivy
|
||||
key: trivy-bin-${{ runner.os }}-${{ env.TRIVY_VERSION }}
|
||||
|
||||
- name: Install Trivy
|
||||
if: steps.cache-trivy.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -fsSL -o trivy.tar.gz \
|
||||
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
|
||||
tar -xzf trivy.tar.gz trivy
|
||||
chmod +x trivy
|
||||
mv trivy "$HOME/.local/bin/trivy"
|
||||
rm -f trivy.tar.gz
|
||||
|
||||
- name: Ermittle Cache-Datum für Trivy-DB
|
||||
run: echo "CACHE_DATE=$(date -u +%Y-%m-%d)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache Trivy-Schwachstellen-Datenbank
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.cache/trivy
|
||||
key: trivy-db-${{ runner.os }}-${{ env.CACHE_DATE }}
|
||||
restore-keys: |
|
||||
trivy-db-${{ runner.os }}-
|
||||
|
||||
- name: Run Trivy Scanner
|
||||
run: |
|
||||
trivy fs \
|
||||
--scanners vuln,secret,misconfig \
|
||||
--severity CRITICAL,HIGH \
|
||||
--format json \
|
||||
--output trivy-results.json \
|
||||
--exit-code 0 \
|
||||
.
|
||||
|
||||
- name: Cache OSV-Scanner-Binary
|
||||
id: cache-osv-scanner
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.local/bin/osv-scanner
|
||||
key: osv-scanner-bin-${{ runner.os }}-${{ env.OSV_SCANNER_VERSION }}
|
||||
|
||||
- name: Install OSV-Scanner
|
||||
if: steps.cache-osv-scanner.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -fsSL -o "$HOME/.local/bin/osv-scanner" \
|
||||
"https://github.com/google/osv-scanner/releases/download/v${OSV_SCANNER_VERSION}/osv-scanner_linux_amd64"
|
||||
chmod +x "$HOME/.local/bin/osv-scanner"
|
||||
|
||||
- name: Run OSV-Scanner
|
||||
run: |
|
||||
set +e
|
||||
osv-scanner scan source --recursive --format json --output-file osv-results.json .
|
||||
echo "OSV_EXIT=$?" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Ergebnisse & Gitea-Issue erstellen/aktualisieren
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
TOKEN: ${{ secrets.SECURITY_TOKEN }}
|
||||
RUN_URL: ${{ gitea.server_url || github.server_url }}/${{ gitea.repository || github.repository }}/actions/runs/${{ gitea.run_id || github.run_id }}
|
||||
run: |
|
||||
python3 scripts/report-security-issue.py trivy-results.json osv-results.json
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Auto-PR (Testing → Main)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- testing
|
||||
|
||||
jobs:
|
||||
create-pr:
|
||||
name: Erstelle automatisch PR von testing nach main
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prüfe auf bereits offenen PR nach main
|
||||
id: check_pr
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||
run: |
|
||||
OPEN_PRS=$(curl -s -H "Authorization: token ${TOKEN}" "${GITEA_URL}/api/v1/repos/${REPO}/pulls?state=open&limit=50")
|
||||
EXISTS=$(echo "$OPEN_PRS" | jq -r '[.[] | select(.base.ref == "main" and .head.ref == "testing")] | length')
|
||||
echo "Bereits offene testing→main PRs: ${EXISTS}"
|
||||
echo "exists=${EXISTS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Ermittle Versionen auf main & testing
|
||||
id: versions
|
||||
if: steps.check_pr.outputs.exists == '0'
|
||||
run: |
|
||||
git fetch origin main
|
||||
MAIN_VERSION="$(git show origin/main:Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -n1)"
|
||||
TESTING_VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
echo "Version auf main: ${MAIN_VERSION} / Version auf testing: ${TESTING_VERSION}"
|
||||
echo "main_version=${MAIN_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "testing_version=${TESTING_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Ermittle geänderte Kategorien (main...testing)
|
||||
id: categories
|
||||
if: steps.check_pr.outputs.exists == '0' && steps.versions.outputs.main_version == steps.versions.outputs.testing_version
|
||||
run: |
|
||||
CHANGED_FILES="$(git diff --name-only origin/main...HEAD)"
|
||||
echo "Geänderte Dateien main...testing:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
WORKFLOWS="false"
|
||||
CONFIG="false"
|
||||
DOCS="false"
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q '^\.gitea/workflows/'; then
|
||||
WORKFLOWS="true"
|
||||
fi
|
||||
if echo "$CHANGED_FILES" | grep -qE '^(renovate\.json|qodana\.yaml|Cargo\.toml|Cargo\.lock|\.cargo/)'; then
|
||||
CONFIG="true"
|
||||
fi
|
||||
if echo "$CHANGED_FILES" | grep -qE '(^|/)[^/]+\.md$|^LICENSE$'; then
|
||||
DOCS="true"
|
||||
fi
|
||||
|
||||
echo "workflows=${WORKFLOWS}" >> "$GITHUB_OUTPUT"
|
||||
echo "config=${CONFIG}" >> "$GITHUB_OUTPUT"
|
||||
echo "docs=${DOCS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Bestimme PR-Titel
|
||||
id: title
|
||||
if: steps.check_pr.outputs.exists == '0'
|
||||
run: |
|
||||
if [ "${{ steps.versions.outputs.main_version }}" != "${{ steps.versions.outputs.testing_version }}" ]; then
|
||||
TITLE="Merge testing in main: Release ${{ steps.versions.outputs.testing_version }}"
|
||||
else
|
||||
PARTS=()
|
||||
[ "${{ steps.categories.outputs.workflows }}" = "true" ] && PARTS+=("Workflows")
|
||||
[ "${{ steps.categories.outputs.config }}" = "true" ] && PARTS+=("Konfigurationen")
|
||||
[ "${{ steps.categories.outputs.docs }}" = "true" ] && PARTS+=("Dokumentation")
|
||||
|
||||
if [ ${#PARTS[@]} -eq 0 ]; then
|
||||
TITLE="Merge testing in main"
|
||||
else
|
||||
JOINED=""
|
||||
for PART in "${PARTS[@]}"; do
|
||||
if [ -z "$JOINED" ]; then
|
||||
JOINED="$PART"
|
||||
else
|
||||
JOINED="${JOINED} & ${PART}"
|
||||
fi
|
||||
done
|
||||
TITLE="Merge testing in main: ${JOINED} aktualisiert"
|
||||
fi
|
||||
fi
|
||||
echo "Ermittelter PR-Titel: ${TITLE}"
|
||||
echo "title=${TITLE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Erstelle PR (testing -> main)
|
||||
if: steps.check_pr.outputs.exists == '0'
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||
TITLE: ${{ steps.title.outputs.title }}
|
||||
run: |
|
||||
PAYLOAD=$(jq -n --arg title "$TITLE" --arg head "testing" --arg base "main" \
|
||||
'{title: $title, head: $head, base: $base}')
|
||||
curl -f -s -S -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/pulls"
|
||||
echo "PR erstellt: ${TITLE}"
|
||||
@@ -6,31 +6,103 @@ on:
|
||||
- testing
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Erkenne relevante Code-Änderungen
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code_changed: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Prüfe auf Änderungen am Programmcode
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'src/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'scripts/**'
|
||||
- '.cargo/**'
|
||||
- 'Dockerfile'
|
||||
- '.gitea/workflows/testing.yaml'
|
||||
|
||||
build-and-publish:
|
||||
name: Build, Publish Packages (Testing) & Create Preview Release
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_BINSTALL_VERSION: "1.23.0"
|
||||
CARGO_DEB_VERSION: "3.8.0"
|
||||
CARGO_GENERATE_RPM_VERSION: "0.21.0"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||
with:
|
||||
toolchain: stable
|
||||
cache: false
|
||||
|
||||
- name: Install Cross-Compilation Toolchains & Dependencies
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
|
||||
- name: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||
run: rm -rf target/debian target/generate-rpm target/arch
|
||||
|
||||
- name: Install Cross-Compilation Toolchains (apt)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
|
||||
rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
||||
|
||||
- name: Install Packaging Tools (Prebuilt Binaries)
|
||||
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
||||
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache Rustup Cross-Compilation-Targets
|
||||
id: cache-rustup-targets
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
|
||||
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Add Rust Cross-Compilation Targets
|
||||
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
||||
run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
||||
|
||||
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
||||
run: |
|
||||
mkdir -p ~/.cargo/bin
|
||||
curl -fsSL https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xz -C ~/.cargo/bin
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
~/.cargo/bin/cargo-binstall -y --no-symlinks cargo-deb cargo-generate-rpm
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache Packaging-Tools (cargo-binstall, cargo-deb, cargo-generate-rpm)
|
||||
id: cache-packaging-tools
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-binstall
|
||||
~/.cargo/bin/cargo-deb
|
||||
~/.cargo/bin/cargo-generate-rpm
|
||||
key: packaging-tools-${{ runner.os }}-${{ env.CARGO_BINSTALL_VERSION }}-${{ env.CARGO_DEB_VERSION }}-${{ env.CARGO_GENERATE_RPM_VERSION }}
|
||||
|
||||
- name: Install Packaging Tools (Prebuilt Binaries)
|
||||
if: steps.cache-packaging-tools.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -fsSL "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-x86_64-unknown-linux-musl.tgz" | tar -xz -C ~/.cargo/bin
|
||||
~/.cargo/bin/cargo-binstall -y --no-symlinks "cargo-deb@${CARGO_DEB_VERSION}" "cargo-generate-rpm@${CARGO_GENERATE_RPM_VERSION}"
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
name: TruffleHog Secret Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
trufflehog-scan:
|
||||
name: TruffleHog
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TRUFFLEHOG_VERSION: "3.97.4"
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Lokales bin-Verzeichnis zum PATH hinzufügen
|
||||
run: |
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache TruffleHog-Binary
|
||||
id: cache-trufflehog
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.local/bin/trufflehog
|
||||
key: trufflehog-bin-${{ runner.os }}-${{ env.TRUFFLEHOG_VERSION }}
|
||||
|
||||
- name: Install TruffleHog
|
||||
if: steps.cache-trufflehog.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -fsSL -o trufflehog.tar.gz \
|
||||
"https://github.com/trufflesecurity/trufflehog/releases/download/v${TRUFFLEHOG_VERSION}/trufflehog_${TRUFFLEHOG_VERSION}_linux_amd64.tar.gz"
|
||||
tar -xzf trufflehog.tar.gz trufflehog
|
||||
chmod +x trufflehog
|
||||
mv trufflehog "$HOME/.local/bin/trufflehog"
|
||||
rm trufflehog.tar.gz
|
||||
|
||||
- name: Run TruffleHog Scanner
|
||||
run: |
|
||||
set +e
|
||||
trufflehog git file://. --results=verified,unknown --fail --json > trufflehog-results.json
|
||||
echo "TRUFFLEHOG_EXIT=$?" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Ergebnisse & Gitea-Issue erstellen/aktualisieren
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
TOKEN: ${{ secrets.SECURITY_TOKEN }}
|
||||
RUN_URL: ${{ gitea.server_url || github.server_url }}/${{ gitea.repository || github.repository }}/actions/runs/${{ gitea.run_id || github.run_id }}
|
||||
ISSUE_TITLE: "Security-Scan: TruffleHog Secrets"
|
||||
ISSUE_LABEL: "security-scan-trufflehog"
|
||||
run: |
|
||||
python3 scripts/report-security-issue.py "" "" trufflehog-results.json
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Unit-Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
branches:
|
||||
- testing
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Unit-Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||
with:
|
||||
toolchain: stable
|
||||
cache: false
|
||||
|
||||
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||
restore-keys: |
|
||||
cargo-${{ runner.os }}-
|
||||
|
||||
- name: Run Tests
|
||||
run: cargo test
|
||||
@@ -0,0 +1,85 @@
|
||||
name: Auto Patch-Version-Bump (Dev → Testing PR)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
branches:
|
||||
- testing
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Erkenne relevante Änderungen im PR
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ (gitea.head_ref || github.head_ref) == 'dev' }}
|
||||
outputs:
|
||||
code_changed: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- name: Checkout Dev-Branch (PR-Head)
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ gitea.head_ref || github.head_ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prüfe auf Änderungen an Cargo.toml, Cargo.lock, src/ oder Dockerfile
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ gitea.base_ref || github.base_ref }}
|
||||
filters: |
|
||||
code:
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'src/**'
|
||||
- 'Dockerfile'
|
||||
|
||||
bump-version:
|
||||
name: Patch-Version erhöhen & auf Dev pushen
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Dev-Branch (PR-Head)
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ gitea.head_ref || github.head_ref }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }}
|
||||
|
||||
- name: Ermittle Cargo-Version auf testing & dev
|
||||
id: versions
|
||||
run: |
|
||||
git fetch origin testing --depth=1
|
||||
TESTING_VERSION="$(git show origin/testing:Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -n1)"
|
||||
DEV_VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
echo "Version auf testing: ${TESTING_VERSION} / Version auf dev: ${DEV_VERSION}"
|
||||
echo "testing_version=${TESTING_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "dev_version=${DEV_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Patch-Version um 1 erhöhen (Cargo.toml & Cargo.lock)
|
||||
if: steps.versions.outputs.testing_version == steps.versions.outputs.dev_version
|
||||
run: |
|
||||
VERSION="${{ steps.versions.outputs.dev_version }}"
|
||||
MAJOR="$(echo "$VERSION" | cut -d. -f1)"
|
||||
MINOR="$(echo "$VERSION" | cut -d. -f2)"
|
||||
PATCH="$(echo "$VERSION" | cut -d. -f3)"
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||
echo "Erhöhe Version: ${VERSION} -> ${NEW_VERSION}"
|
||||
|
||||
sed -i "0,/^version = \"${VERSION}\"/s//version = \"${NEW_VERSION}\"/" Cargo.toml
|
||||
|
||||
PACKAGE_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
awk -v new="$NEW_VERSION" -v pkg="$PACKAGE_NAME" '
|
||||
found_name && /^version = "/ {
|
||||
print "version = \"" new "\""
|
||||
found_name = 0
|
||||
next
|
||||
}
|
||||
$0 == "name = \"" pkg "\"" { found_name = 1 }
|
||||
{ print }
|
||||
' Cargo.lock > Cargo.lock.tmp && mv Cargo.lock.tmp Cargo.lock
|
||||
|
||||
git config user.name "Gitea-Bot"
|
||||
git config user.email "no-reply@creativedragonslayer.de"
|
||||
git add Cargo.toml Cargo.lock
|
||||
git commit -m "Chore: Erhöht Patch-Version auf ${NEW_VERSION} für Promotion nach testing"
|
||||
git push origin HEAD:${{ gitea.head_ref || github.head_ref }}
|
||||
@@ -109,3 +109,4 @@ fabric.properties
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
.junie/plans
|
||||
|
||||
Generated
+12
@@ -17,6 +17,18 @@
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/testing.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/renovate.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/security-scan.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/trufflehog-scan.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/code-quality.yaml" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run" type="CargoCommandRunConfiguration" factoryName="Cargo Command" nameIsGenerated="true">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
|
||||
<envs />
|
||||
<option name="emulateTerminal" value="true" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
<option name="allFeatures" value="false" />
|
||||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2">
|
||||
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
Generated
+1
@@ -4,6 +4,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.junie/plans" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
@@ -32,7 +32,11 @@ Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Codi
|
||||
├── .gitea/
|
||||
│ └── workflows/
|
||||
│ ├── main.yaml # CI/CD: Stabile Builds, Multi-Arch-Paketierung, Release & Upload
|
||||
│ └── testing.yaml # CI/CD: Preview-Builds & Testing-Pakete
|
||||
│ ├── testing.yaml # CI/CD: Preview-Builds & Testing-Pakete
|
||||
│ ├── unit-tests.yaml # CI: cargo test bei PRs mit Ziel-Branch testing
|
||||
│ ├── security-scan.yaml # CI: Trivy (vuln/secret/misconfig) & OSV-Scanner
|
||||
│ ├── trufflehog-scan.yaml # CI: TruffleHog Secret-Scanning
|
||||
│ └── renovate.yaml # CI: Renovate Dependency-Updates (self-hosted, wöchentlich)
|
||||
├── scripts/
|
||||
│ ├── get-build-number.py # Ermittelt automatisch die nächste Revisions-/Build-Nummer
|
||||
│ └── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete
|
||||
@@ -52,6 +56,7 @@ Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Codi
|
||||
├── Dockerfile # Minimales & gehärtetes Runtime-Container-Image
|
||||
├── docker-compose.example.yml # Beispielkonfiguration für Docker Compose
|
||||
├── Cargo.toml # Projekt-Manifest & Metadaten für deb, rpm und arch
|
||||
├── renovate.json # Renovate-Konfiguration (baseBranches, Gruppierung, Custom-Manager)
|
||||
├── LICENSE # Lizenztext
|
||||
├── README.md # Benutzerdokumentation
|
||||
└── AGENTS.md # Dieses Agenten-Handbuch
|
||||
@@ -77,7 +82,7 @@ Bei Änderungen an Binärnamen, Abhängigkeiten oder Beschreibungen müssen die
|
||||
- `pkgrel`, `arch`, `depends`, `optdepends`.
|
||||
|
||||
### 3.3 Skripte in `scripts/`
|
||||
- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `TOKEN`) ermittelt werden.
|
||||
- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `REPO_OWNER`, `TOKEN`) ermittelt werden.
|
||||
- **Python-Kompatibilität**: Verwende Standard-Python 3 ohne externe PyPI-Abhängigkeiten (nur Standardbibliothek).
|
||||
|
||||
### 3.4 Container-Sicherheit & Persistenz
|
||||
@@ -119,8 +124,33 @@ python3 scripts/package-arch.py --arch x86_64 --pkgrel 1
|
||||
|
||||
## 5. CI/CD-Pipeline Details
|
||||
|
||||
- **Trigger**:
|
||||
- `push` auf `main`: Baut Binaries für alle 3 Architekturen, baut `.deb`, `.rpm` und `.pkg.tar.zst`, lädt sie in die Gitea Package Registry hoch, erstellt ein Gitea Release `v<VERSION>` und baut/veröffentlicht das Docker-Container-Image mit den Tags `:latest`, `:<VERSION>`, `:v<VERSION>` und `:<VERSION>.<BUILD_NUMBER>`.
|
||||
- `push` auf `testing`: Baut Binaries & Pakete für den `testing`-Kanal, erstellt ein Pre-Release `v<VERSION>-preview` und baut/veröffentlicht das Docker-Container-Image ausschließlich mit eindeutigen Testing-Tags (`:testing`, `:<VERSION>-preview`, `:<VERSION>-testing`, `:<VERSION>-preview.<BUILD_NUMBER>`, `:testing-<BUILD_NUMBER>`). Der Tag `:latest` ist strikt dem `main`-Workflow vorbehalten.
|
||||
- **Secrets**:
|
||||
- `PACKAGE_TOKEN` (bzw. Fallback-Token-Namen wie `RELEASE_TOKEN`, `GITEA_TOKEN`) wird für API-Zugriffe auf Gitea Packages, Container Registry und Releases verwendet.
|
||||
### 5.1 Branch-Flow & Trigger
|
||||
Promotion-Flow: `dev` → `testing` → `main`, ausschließlich per Merge (nie direkt gepusht).
|
||||
|
||||
- `pull_request` mit Ziel-Branch `testing` (`unit-tests.yaml`): führt `cargo test` aus (`types: opened, synchronize, reopened`).
|
||||
- `push` auf `main` (`main.yaml`): Baut Binaries für alle 3 Architekturen, baut `.deb`, `.rpm` und `.pkg.tar.zst`, lädt sie in die Gitea Package Registry hoch, erstellt ein Gitea Release `v<VERSION>` und baut/veröffentlicht das Docker-Container-Image mit den Tags `:latest`, `:<VERSION>`, `:v<VERSION>` und `:<VERSION>.<BUILD_NUMBER>`.
|
||||
- `push` auf `testing` (`testing.yaml`): Baut Binaries & Pakete für den `testing`-Kanal, erstellt ein Pre-Release `v<VERSION>-preview` und baut/veröffentlicht das Docker-Container-Image ausschließlich mit eindeutigen Testing-Tags (`:testing`, `:<VERSION>-preview`, `:<VERSION>-testing`, `:<VERSION>-preview.<BUILD_NUMBER>`, `:testing-<BUILD_NUMBER>`). Der Tag `:latest` ist strikt dem `main`-Workflow vorbehalten.
|
||||
- `push`/`pull_request`/wöchentlich (`security-scan.yaml`, `trufflehog-scan.yaml`): Trivy (vuln/secret/misconfig) & OSV-Scanner laufen bei Push auf `main`/`testing`/`dev` sowie bei jedem PR; TruffleHog hat **keinen** `branches`-Filter und läuft bei Push auf **jeden** Branch (zusätzlich bei jedem PR). Funde werden per `scripts/report-security-issue.py` als Gitea-Issue gemeldet.
|
||||
- stündlich, `workflow_dispatch` (`renovate.yaml`): Renovate (containerisiert via `ghcr.io/renovatebot/renovate`) prüft Dependency-Updates, siehe 5.5. Der stündliche CI-Lauf steuert nur, wie schnell Renovate reagiert (z.B. auf Dependency-Dashboard-Checkboxen); tatsächliche neue Update-PRs entstehen weiterhin nur innerhalb des in `renovate.json` gesetzten `schedule` (Montag vor 6 Uhr).
|
||||
|
||||
### 5.2 Caching (`actions/cache@v6`)
|
||||
`main.yaml`/`testing.yaml` cachen mehrere Verzeichnisse, um wiederholte Cross-Compile-Builds zu beschleunigen:
|
||||
- `~/.cargo/registry`, `~/.cargo/git`, `target` – Cache-Key basiert auf `hashFiles('Cargo.lock')`.
|
||||
- `~/.rustup/.../lib/rustlib/<target>` für die zwei zusätzlichen Cross-Targets – Cache-Key basiert auf der **aufgelösten** `rustc --version`, nicht auf dem gleitenden `stable`-Label. Sonst könnte nach einem Rust-Update eine veraltete gecachte Std-Lib mit einem neueren Compiler kombiniert werden.
|
||||
- `~/.cargo/bin/cargo-binstall`, `~/.cargo/bin/cargo-deb`, `~/.cargo/bin/cargo-generate-rpm` (gezielt die Binaries statt des gesamten Verzeichnisses, um alte Rust-Compiler-Proxys bei Toolchain-Updates nicht wiederherzustellen) – alle drei sind auf feste Versionen gepinnt (Job-`env`), nicht auf `latest`.
|
||||
|
||||
**Wichtige Falle:** `target/debian`, `target/generate-rpm` und `target/arch` hängen ebenfalls unter `target` und werden dadurch mitgecacht, aber von keinem Tool automatisch geleert. Vor jedem Paketbau werden sie daher explizit per `rm -rf` bereinigt – sonst werden alte, bereits hochgeladene Paket-Dateien aus früheren Builds erneut mit hochgeladen, und die Gitea Package Registry lehnt sie mit `409 Conflict` ab (Paket-Dateien sind dort unveränderlich). Bei neuen Paketierungs-Outputs außerhalb dieser drei Ordner muss diese Bereinigung entsprechend erweitert werden.
|
||||
|
||||
### 5.3 Build-Nummer (`scripts/get-build-number.py`)
|
||||
Pro CI-Lauf wird genau **eine** Build-Nummer ermittelt und identisch an `cargo deb`, `cargo generate-rpm` und `package-arch.py --pkgrel` weitergereicht – `.deb`, `.rpm` und Arch-Paket tragen also immer dieselbe Nummer. Zur Ermittlung wird pro Paket-Typ (`debian`, `rpm`, `arch`) gezielt `GET /api/v1/packages/{owner}/{type}/{name}/-/latest` abgefragt (ein Request pro Typ, kein Paging, unbeeinflusst von Docker-Tags/anderen Paketen desselben Owners); das Maximum aller drei Typen + 1 ergibt die neue Nummer. Eine pauschale, ungefilterte Abfrage über alle Pakete des Owners (`GET /packages/{owner}`) darf hier nicht mehr verwendet werden, da sie durch Docker-Image-Tags & Co. verdrängt werden kann.
|
||||
|
||||
### 5.4 Secrets
|
||||
- `PACKAGE_TOKEN` (bzw. Fallback-Token-Namen wie `RELEASE_TOKEN`, `GITEA_TOKEN`) wird für API-Zugriffe auf Gitea Packages, Container Registry und Releases verwendet.
|
||||
- `SECURITY_TOKEN` für die Security-Scan-Workflows (Gitea-Issue-Erstellung).
|
||||
- `RENOVATE_TOKEN` für den Renovate-Workflow.
|
||||
- `GH_RENOVATE_TOKEN` (optional, Renovate-Workflow, als `GITHUB_COM_TOKEN` an Renovate durchgereicht) – GitHub-PAT (Public-Repo-Read genügt), damit Renovate Release-Infos für GitHub-gehostete Dependencies (u.a. `github-actions`-Manager) authentifiziert statt rate-limitiert abfragt.
|
||||
|
||||
### 5.5 Renovate (`renovate.json`)
|
||||
- `baseBranches: ["dev"]` – Renovate liest Dependency-Dateien ausschließlich von `dev` und öffnet PRs nur dort, passend zum `dev → testing → main`-Promotion-Flow. Die Konfigurationsdatei selbst muss trotzdem über den Gitea-Default-Branch auffindbar sein.
|
||||
- Drei Gruppen (`packageRules`), jeweils mit `separateMajorMinor: false`/`separateMinorPatch: false` (sonst reißt Renovate Major-Updates trotz `groupName` standardmäßig in einen eigenen PR): "Gitea Actions" (dateibasiert über `matchFileNames: [".gitea/workflows/**"]`, deckt auch die Custom-Manager unten ab), "Cargo Dependencies", "Docker-Images".
|
||||
- `customManagers` (Regex) tracken Versionen, die als reine Strings in `run:`-Blöcken stecken und vom `github-actions`-Manager nicht erkannt werden: `TRIVY_VERSION`, `OSV_SCANNER_VERSION`, `TRUFFLEHOG_VERSION`, `CARGO_BINSTALL_VERSION`, `CARGO_DEB_VERSION`, `CARGO_GENERATE_RPM_VERSION`. Wird in einer Workflow-Datei eine weitere Tool-Version nach demselben Muster (`NAME_VERSION: "x.y.z"`) gepinnt, muss hier ein passender Eintrag ergänzt werden, sonst bleibt sie von Renovate unbemerkt veraltet.
|
||||
|
||||
Generated
+19
-14
@@ -93,6 +93,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
@@ -213,9 +219,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "config-ctdra"
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
|
||||
checksum = "f7b3e25b95d4cdb6fc856f2af0106d4558b882c2f10798db46181e51827749a4"
|
||||
checksum = "83eb23d473fce79e8234ad66baf210289b5c9e4c4a587273b4df7deceb73f416"
|
||||
dependencies = [
|
||||
"confy",
|
||||
"program-ctdra",
|
||||
@@ -316,7 +322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc 0.2.189",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -565,7 +571,7 @@ version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@@ -827,9 +833,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "logger-ctdra"
|
||||
version = "1.0.3"
|
||||
version = "1.0.5"
|
||||
source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
|
||||
checksum = "af9ea239d80163c80b12cae5f74c0c2824199921dedafba74678e96a11a8b15e"
|
||||
checksum = "ce612b7d943b77060fa36eab0c85782e650c4cf023e9d560bef791951ed92cad"
|
||||
dependencies = [
|
||||
"program-ctdra",
|
||||
"time",
|
||||
@@ -866,7 +872,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mirror-package"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -874,7 +880,6 @@ dependencies = [
|
||||
"logger-ctdra",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -1036,7 +1041,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1091,11 +1096,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
|
||||
checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.23.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
@@ -1215,7 +1220,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1893,7 +1898,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mirror-package"
|
||||
version = "1.0.2"
|
||||
version = "1.0.5"
|
||||
edition = "2024"
|
||||
authors = ['DragonSlayer_14']
|
||||
readme = "README.md"
|
||||
@@ -15,7 +15,6 @@ clap = { version = "4.6.6", features = ["derive", "env"] }
|
||||
reqwest = { version = "0.13.4", features = ["json", "stream"] }
|
||||
tokio = { version = "1.53.1", features = ["full"] }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.151"
|
||||
anyhow = "1.0.104"
|
||||
|
||||
[profile.release]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# mirror-package
|
||||
|
||||
[](https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage/actions?workflow=main.yaml)
|
||||
[](https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage/actions?workflow=testing.yaml)
|
||||
|
||||
Automatisiertes Werkzeug zum Extrahieren, Herunterladen und Spiegeln vorkompilierter Linux-Pakete aus GitHub-Releases in eine selbstgehostete Gitea- / Forgejo-Paket-Registry.
|
||||
|
||||
`mirror-package` überwacht konfigurierte GitHub-Repositories (wie z. B. `raspberrypi/rpi-imager` oder `Heroic-Games-Launcher/HeroicGamesLauncher`), identifiziert vorkompilierte Linux-Pakete (`.deb`, `.rpm`, `.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`) und veröffentlicht diese anhand von Release-Stabilitätsregeln automatisch in den passenden Distributionen der Gitea-Paket-Registry.
|
||||
@@ -200,7 +203,11 @@ services:
|
||||
├── .gitea/
|
||||
│ └── workflows/
|
||||
│ ├── main.yaml # CI/CD: Release, Pakete & Container (Stable)
|
||||
│ └── testing.yaml # CI/CD: Preview, Pakete & Container (Testing)
|
||||
│ ├── testing.yaml # CI/CD: Preview, Pakete & Container (Testing)
|
||||
│ ├── unit-tests.yaml # CI: cargo test bei PRs gegen testing
|
||||
│ ├── security-scan.yaml # CI: Trivy & OSV-Scanner
|
||||
│ ├── trufflehog-scan.yaml # CI: TruffleHog Secret-Scanning
|
||||
│ └── renovate.yaml # CI: automatisierte Abhängigkeits-Updates (Renovate)
|
||||
├── scripts/
|
||||
│ ├── get-build-number.py # Dynamische Ermittlung der Build-/Revisionsnummer
|
||||
│ └── package-arch.py # Erstellung nativer Arch Linux-Pakete
|
||||
@@ -220,6 +227,7 @@ services:
|
||||
├── Dockerfile # Gehärtetes, minimales Runtime-Container-Image
|
||||
├── docker-compose.example.yml # Beispielkonfiguration für Docker Compose
|
||||
├── Cargo.toml # Projekt-Manifest und Paketierungs-Metadaten
|
||||
├── renovate.json # Renovate-Konfiguration für automatisierte Abhängigkeits-Updates
|
||||
├── LICENSE # GPL-3.0-or-later Lizenztext
|
||||
├── AGENTS.md # Agenten- & Entwickler-Richtlinien
|
||||
└── README.md # Projektdokumentation
|
||||
@@ -248,6 +256,15 @@ cargo build --release
|
||||
|
||||
---
|
||||
|
||||
## CI/CD & Contributing
|
||||
|
||||
- **Branch-Flow**: Änderungen durchlaufen `dev` → `testing` → `main`, jeweils per Merge (nie direkt gepusht).
|
||||
- **Pull Requests gegen `testing`** lösen automatisch `cargo test` aus.
|
||||
- **Sicherheits-Scans**: Trivy & OSV-Scanner laufen bei Push auf `main`/`testing`/`dev` sowie bei jedem Pull Request; TruffleHog läuft bei Push auf **jeden** Branch (kein `branches`-Filter) sowie ebenfalls bei jedem Pull Request. Funde werden als Gitea-Issue gemeldet.
|
||||
- **Abhängigkeits-Updates** werden automatisiert über [Renovate](https://docs.renovatebot.com/) als PRs gegen `dev` vorgeschlagen.
|
||||
|
||||
---
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt ist unter der [GPL-3.0-or-later](LICENSE)-Lizenz lizenziert.
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"timezone": "Europe/Berlin",
|
||||
"schedule": ["before 6am on monday"],
|
||||
"baseBranchPatterns": [
|
||||
"dev"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchFileNames": [".gitea/workflows/**"],
|
||||
"groupName": "Gitea Actions",
|
||||
"separateMajorMinor": false,
|
||||
"separateMinorPatch": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["cargo"],
|
||||
"groupName": "Cargo Dependencies",
|
||||
"separateMajorMinor": false,
|
||||
"separateMinorPatch": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["dockerfile", "docker-compose"],
|
||||
"groupName": "Docker-Images",
|
||||
"separateMajorMinor": false,
|
||||
"separateMinorPatch": false
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"TRIVY_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "aquasecurity/trivy",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"OSV_SCANNER_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "google/osv-scanner",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"TRUFFLEHOG_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "trufflesecurity/trufflehog",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"CARGO_BINSTALL_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-bins/cargo-binstall",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"CARGO_DEB_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-deb",
|
||||
"datasourceTemplate": "crate"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"CARGO_GENERATE_RPM_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-generate-rpm",
|
||||
"datasourceTemplate": "crate"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,9 +9,14 @@ import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
# Paket-Typen, unter denen dasselbe Release veroeffentlicht wird und die sich
|
||||
# daher eine gemeinsame Build-Nummer teilen muessen.
|
||||
PACKAGE_TYPES = ("debian", "rpm", "arch")
|
||||
|
||||
|
||||
def get_cargo_pkg_info():
|
||||
try:
|
||||
@@ -83,20 +88,29 @@ def query_existing_build_numbers(name, version, gitea_url, repo, owner, token=No
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[Hinweis] Konnte Gitea Releases nicht abfragen: {e}\n")
|
||||
|
||||
# 2. Gitea Packages API (falls Token vorhanden)
|
||||
if gitea_url and owner and token:
|
||||
# 2. Gitea Packages API (falls Token vorhanden): pro Paket-Typ gezielt nur die
|
||||
# neueste Version abfragen (ein Request, keine Pagination noetig). Das haelt
|
||||
# die Abfrage schnell unabhaengig von der Historie und ist unbeeinflusst von
|
||||
# Docker-Tags/anderen Paket-Typen, die unter demselben Owner haengen.
|
||||
if gitea_url and owner and token and name:
|
||||
for pkg_type in PACKAGE_TYPES:
|
||||
try:
|
||||
url = f"{gitea_url.rstrip('/')}/api/v1/packages/{owner}?limit=50"
|
||||
url = (
|
||||
f"{gitea_url.rstrip('/')}/api/v1/packages/{owner}/"
|
||||
f"{pkg_type}/{urllib.parse.quote(name, safe='')}/-/latest"
|
||||
)
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
packages = json.loads(resp.read().decode())
|
||||
for pkg in packages:
|
||||
pkg = json.loads(resp.read().decode())
|
||||
pkg_ver = pkg.get("version", "")
|
||||
m = pattern.search(pkg_ver)
|
||||
if m:
|
||||
build_nums.add(int(m.group(1)))
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404:
|
||||
sys.stderr.write(f"[Hinweis] Konnte neueste {pkg_type}-Paketversion nicht abfragen: {e}\n")
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[Hinweis] Konnte Gitea Packages nicht abfragen: {e}\n")
|
||||
sys.stderr.write(f"[Hinweis] Konnte neueste {pkg_type}-Paketversion nicht abfragen: {e}\n")
|
||||
|
||||
# 3. Lokales Target-Verzeichnis prüfen
|
||||
target_dirs = ["target/debian", "target/generate-rpm", "target/arch"]
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Erstellt oder kommentiert ein Gitea-Issue mit den Ergebnissen der Security-Scans.
|
||||
|
||||
Sucht ein offenes Issue mit dem Label ISSUE_LABEL (Standard: "security-scan").
|
||||
Existiert eines, wird der aktuelle Scan-Stand als neuer Kommentar angehängt
|
||||
(die Historie bleibt erhalten). Existiert keines (z.B. weil das letzte
|
||||
geschlossen wurde), wird ein neues Issue erstellt. Gibt es keine Funde mehr,
|
||||
wird ein offenes Issue nur kommentiert, nicht geschlossen.
|
||||
|
||||
Titel und Label lassen sich per Umgebungsvariable ISSUE_TITLE / ISSUE_LABEL
|
||||
überschreiben, damit z.B. TruffleHog-Funde in ein eigenes Issue laufen statt
|
||||
in das gemeinsame Trivy/OSV-Issue.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
LABEL_NAME = os.environ.get("ISSUE_LABEL", "security-scan")
|
||||
LABEL_COLOR = "#b60205"
|
||||
ISSUE_TITLE = os.environ.get("ISSUE_TITLE", "Security-Scan: Offene Schwachstellen")
|
||||
|
||||
SEVERITY_ORDER = {
|
||||
"VERIFIED": -1,
|
||||
"CRITICAL": 0,
|
||||
"HIGH": 1,
|
||||
"MEDIUM": 2,
|
||||
"LOW": 3,
|
||||
"UNKNOWN": 4,
|
||||
"UNVERIFIED": 6,
|
||||
}
|
||||
|
||||
|
||||
def api(method, path, token, gitea_url, data=None):
|
||||
url = f"{gitea_url}/api/v1{path}"
|
||||
body = json.dumps(data).encode() if data is not None else None
|
||||
req = urllib.request.Request(url, data=body, method=method)
|
||||
req.add_header("Authorization", f"token {token}")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
raw = resp.read()
|
||||
return json.loads(raw) if raw else None
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"Gitea API Fehler ({method} {path}): {e.code} {e.read().decode()}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
|
||||
def make_finding(source, id, severity, package, installed="-", fixed="-", target="-"):
|
||||
return {
|
||||
"source": source,
|
||||
"id": id,
|
||||
"severity": severity,
|
||||
"package": package,
|
||||
"installed": installed,
|
||||
"fixed": fixed,
|
||||
"target": target,
|
||||
}
|
||||
|
||||
|
||||
def cvss_score_to_severity(score):
|
||||
try:
|
||||
score = float(score)
|
||||
except (TypeError, ValueError):
|
||||
return "UNKNOWN"
|
||||
if score >= 9.0:
|
||||
return "CRITICAL"
|
||||
if score >= 7.0:
|
||||
return "HIGH"
|
||||
if score >= 4.0:
|
||||
return "MEDIUM"
|
||||
if score > 0.0:
|
||||
return "LOW"
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
def load_trivy(path):
|
||||
findings = []
|
||||
if not path or not os.path.isfile(path):
|
||||
return findings
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
for result in data.get("Results", []) or []:
|
||||
target = result.get("Target", "?")
|
||||
for vuln in result.get("Vulnerabilities", []) or []:
|
||||
findings.append(make_finding(
|
||||
"Trivy",
|
||||
vuln.get("VulnerabilityID", "?"),
|
||||
vuln.get("Severity", "UNKNOWN"),
|
||||
vuln.get("PkgName", "?"),
|
||||
installed=vuln.get("InstalledVersion", "?"),
|
||||
fixed=vuln.get("FixedVersion") or "-",
|
||||
target=target,
|
||||
))
|
||||
for misc in result.get("Misconfigurations", []) or []:
|
||||
findings.append(make_finding(
|
||||
"Trivy (Misconfig)",
|
||||
misc.get("ID", "?"),
|
||||
misc.get("Severity", "UNKNOWN"),
|
||||
misc.get("Title", "?"),
|
||||
target=target,
|
||||
))
|
||||
for secret in result.get("Secrets", []) or []:
|
||||
findings.append(make_finding(
|
||||
"Trivy (Secret)",
|
||||
secret.get("RuleID", "?"),
|
||||
secret.get("Severity", "UNKNOWN"),
|
||||
secret.get("Title", "?"),
|
||||
target=target,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
def load_osv(path):
|
||||
findings = []
|
||||
if not path or not os.path.isfile(path):
|
||||
return findings
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
for result in data.get("results", []) or []:
|
||||
source = (result.get("source") or {}).get("path", "?")
|
||||
for pkg in result.get("packages", []) or []:
|
||||
info = pkg.get("package", {})
|
||||
pkg_name = f"{info.get('name', '?')} ({info.get('ecosystem', '?')})"
|
||||
severity_by_id = {}
|
||||
for group in pkg.get("groups", []) or []:
|
||||
label = cvss_score_to_severity(group.get("max_severity"))
|
||||
for vuln_id in group.get("ids", []) or []:
|
||||
severity_by_id[vuln_id] = label
|
||||
for vuln in pkg.get("vulnerabilities", []) or []:
|
||||
vuln_id = vuln.get("id", "?")
|
||||
findings.append(make_finding(
|
||||
"OSV-Scanner",
|
||||
vuln_id,
|
||||
severity_by_id.get(vuln_id, "UNKNOWN"),
|
||||
pkg_name,
|
||||
installed=info.get("version", "?"),
|
||||
target=source,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
def load_trufflehog(path):
|
||||
findings = []
|
||||
if not path or not os.path.isfile(path):
|
||||
return findings
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
git_meta = ((entry.get("SourceMetadata") or {}).get("Data") or {}).get("Git") or {}
|
||||
findings.append(make_finding(
|
||||
"TruffleHog",
|
||||
entry.get("DetectorName", "?"),
|
||||
"VERIFIED" if entry.get("Verified") else "UNVERIFIED",
|
||||
git_meta.get("file", "?"),
|
||||
target=git_meta.get("commit", "-"),
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
def sort_findings(findings):
|
||||
return sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
|
||||
|
||||
|
||||
def print_summary(findings):
|
||||
if not findings:
|
||||
print("Keine Funde.")
|
||||
return
|
||||
widths = {
|
||||
key: max(len(key), *(len(str(f[key])) for f in findings))
|
||||
for key in ("source", "id", "severity", "package", "installed", "fixed", "target")
|
||||
}
|
||||
header = ("source", "id", "severity", "package", "installed", "fixed", "target")
|
||||
row_fmt = " ".join(f"{{:{widths[k]}}}" for k in header)
|
||||
print(row_fmt.format(*header))
|
||||
print(row_fmt.format(*("-" * widths[k] for k in header)))
|
||||
for f in findings:
|
||||
print(row_fmt.format(*(str(f[k]) for k in header)))
|
||||
|
||||
|
||||
TRUFFLEHOG_GUIDANCE = """### Vorgehen bei gefundenen Secrets
|
||||
|
||||
1. **Sofort rotieren/widerrufen**: Das betroffene Secret (Token, Passwort, Schlüssel) beim jeweiligen Dienst ungültig machen und durch ein neues ersetzen. Ein einmal committetes Secret gilt als kompromittiert, auch wenn es später aus der Historie entfernt wird.
|
||||
2. **Ursache beheben**: Neues Secret nur noch über Umgebungsvariablen/Secrets-Store einbinden, nicht erneut hart codieren.
|
||||
3. **Historie bereinigen (optional, manuell, erst nach Schritt 1)**: Mit `git filter-repo` oder BFG Repo-Cleaner den Commit-Inhalt entfernen, danach `git push --force` in Absprache mit allen Mitwirkenden – bestehende Clones/Forks werden dadurch ungültig.
|
||||
4. **Issue schließen**, sobald rotiert wurde. TruffleHog findet das alte Secret ggf. weiterhin in der Historie – nach der Rotation ist das unkritisch.
|
||||
|
||||
> Der Wert des Secrets selbst wird hier bewusst nicht ausgegeben, auch nicht gekürzt – nur Detector, Datei und Commit. Fund lässt sich über "Ziel" (Commit-Hash) und "Paket" (Dateipfad) lokalisieren."""
|
||||
|
||||
|
||||
def escape_md_cell(value):
|
||||
return str(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ")
|
||||
|
||||
|
||||
def build_report(findings, run_url):
|
||||
lines = [
|
||||
"Automatisch erstellt vom Security-Scan-Workflow.",
|
||||
f"Lauf: {run_url}" if run_url else "",
|
||||
"",
|
||||
"| Quelle | ID | Schweregrad | Paket | Installiert | Fix | Ziel |",
|
||||
"|---|---|---|---|---|---|---|",
|
||||
]
|
||||
for f in findings:
|
||||
cells = (f["source"], f["id"], f["severity"], f["package"], f["installed"], f["fixed"], f["target"])
|
||||
lines.append("| " + " | ".join(escape_md_cell(c) for c in cells) + " |")
|
||||
if any(f["source"] == "TruffleHog" for f in findings):
|
||||
lines.append("")
|
||||
lines.append(TRUFFLEHOG_GUIDANCE)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def ensure_label(token, gitea_url, repo):
|
||||
page = 1
|
||||
while True:
|
||||
labels = api("GET", f"/repos/{repo}/labels?limit=50&page={page}", token, gitea_url) or []
|
||||
for label in labels:
|
||||
if label.get("name") == LABEL_NAME:
|
||||
return label["id"]
|
||||
if len(labels) < 50:
|
||||
break
|
||||
page += 1
|
||||
created = api("POST", f"/repos/{repo}/labels", token, gitea_url, {
|
||||
"name": LABEL_NAME,
|
||||
"color": LABEL_COLOR,
|
||||
"description": "Automatisch verwaltet vom Security-Scan-Workflow",
|
||||
})
|
||||
return created["id"]
|
||||
|
||||
|
||||
def find_open_issue(token, gitea_url, repo):
|
||||
issues = api(
|
||||
"GET",
|
||||
f"/repos/{repo}/issues?state=open&type=issues&labels={LABEL_NAME}",
|
||||
token,
|
||||
gitea_url,
|
||||
) or []
|
||||
for issue in issues:
|
||||
if issue.get("title") == ISSUE_TITLE:
|
||||
return issue
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
trivy_path = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
osv_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
trufflehog_path = sys.argv[3] if len(sys.argv) > 3 else None
|
||||
|
||||
gitea_url = os.environ.get("GITEA_URL", "").strip().rstrip("/")
|
||||
repo = os.environ.get("REPO", "").strip()
|
||||
token = os.environ.get("TOKEN", "").strip()
|
||||
run_url = os.environ.get("RUN_URL", "")
|
||||
osv_exit = int(os.environ.get("OSV_EXIT", "0"))
|
||||
trufflehog_exit = int(os.environ.get("TRUFFLEHOG_EXIT", "0"))
|
||||
|
||||
findings = sort_findings(load_trivy(trivy_path) + load_osv(osv_path) + load_trufflehog(trufflehog_path))
|
||||
print_summary(findings)
|
||||
|
||||
if not token or not gitea_url or not repo:
|
||||
missing = [name for name, val in [("TOKEN", token), ("GITEA_URL", gitea_url), ("REPO", repo)] if not val]
|
||||
print(f"{', '.join(missing)} nicht gesetzt oder leer – überspringe Gitea-Issue-Synchronisation.")
|
||||
else:
|
||||
open_issue = find_open_issue(token, gitea_url, repo)
|
||||
|
||||
if findings:
|
||||
report = build_report(findings, run_url)
|
||||
if open_issue:
|
||||
print(f"Kommentiere bestehendes Issue #{open_issue['number']} mit {len(findings)} Fund(en).")
|
||||
api("POST", f"/repos/{repo}/issues/{open_issue['number']}/comments", token, gitea_url, {"body": report})
|
||||
else:
|
||||
label_id = ensure_label(token, gitea_url, repo)
|
||||
print(f"Erstelle neues Issue mit {len(findings)} Fund(en).")
|
||||
api("POST", f"/repos/{repo}/issues", token, gitea_url, {
|
||||
"title": ISSUE_TITLE,
|
||||
"body": report,
|
||||
"labels": [label_id],
|
||||
})
|
||||
elif open_issue:
|
||||
print(f"Keine aktuellen Funde mehr. Kommentiere Issue #{open_issue['number']}.")
|
||||
api("POST", f"/repos/{repo}/issues/{open_issue['number']}/comments", token, gitea_url, {
|
||||
"body": f"Aktueller Scan hat keine offenen Schwachstellen mehr gefunden.\n\n{run_url}".strip(),
|
||||
})
|
||||
else:
|
||||
print("Keine Funde und kein offenes Issue vorhanden.")
|
||||
|
||||
osv_ok_exits = {0, 1}
|
||||
trufflehog_ok_exits = {0, 183}
|
||||
if osv_exit not in osv_ok_exits:
|
||||
print(f"WARNUNG: osv-scanner beendete sich mit unerwartetem Exit-Code {osv_exit} - Scan evtl. unvollständig.", file=sys.stderr)
|
||||
if trufflehog_exit not in trufflehog_ok_exits:
|
||||
print(f"WARNUNG: trufflehog beendete sich mit unerwartetem Exit-Code {trufflehog_exit} - Scan evtl. unvollständig.", file=sys.stderr)
|
||||
|
||||
has_trivy_findings = any(f["source"].startswith("Trivy") for f in findings)
|
||||
if (
|
||||
has_trivy_findings
|
||||
or osv_exit == 1
|
||||
or trufflehog_exit == 183
|
||||
or osv_exit not in osv_ok_exits
|
||||
or trufflehog_exit not in trufflehog_ok_exits
|
||||
):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+15
-4
@@ -59,7 +59,11 @@ impl AppConfig {
|
||||
/// Fügt ein Repository zur Konfiguration hinzu oder aktualisiert ein bestehendes.
|
||||
pub fn add_or_update_repo(&mut self, mut repo: RepoConfig) {
|
||||
repo.name = Self::normalize_repo_name(&repo.name);
|
||||
if let Some(existing) = self.repositories.iter_mut().find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&repo.name)) {
|
||||
if let Some(existing) = self
|
||||
.repositories
|
||||
.iter_mut()
|
||||
.find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&repo.name))
|
||||
{
|
||||
existing.name = repo.name;
|
||||
existing.include_prereleases = repo.include_prereleases;
|
||||
} else {
|
||||
@@ -71,20 +75,27 @@ impl AppConfig {
|
||||
pub fn remove_repo(&mut self, repo_name: &str) -> bool {
|
||||
let normalized = Self::normalize_repo_name(repo_name);
|
||||
let before_len = self.repositories.len();
|
||||
self.repositories.retain(|r| !Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized));
|
||||
self.repositories
|
||||
.retain(|r| !Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized));
|
||||
self.repositories.len() < before_len
|
||||
}
|
||||
|
||||
/// Sucht ein Repository anhand des Namens.
|
||||
pub fn find_repo(&self, repo_name: &str) -> Option<&RepoConfig> {
|
||||
let normalized = Self::normalize_repo_name(repo_name);
|
||||
self.repositories.iter().find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized))
|
||||
self.repositories
|
||||
.iter()
|
||||
.find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized))
|
||||
}
|
||||
|
||||
/// Aktualisiert den zuletzt synchronisierten Tag für ein bestimmtes Repository.
|
||||
pub fn update_last_synced_tag(&mut self, repo_name: &str, tag: String) {
|
||||
let normalized = Self::normalize_repo_name(repo_name);
|
||||
if let Some(repo) = self.repositories.iter_mut().find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized)) {
|
||||
if let Some(repo) = self
|
||||
.repositories
|
||||
.iter_mut()
|
||||
.find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized))
|
||||
{
|
||||
repo.last_synced_tag = Some(tag);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-7
@@ -1,5 +1,5 @@
|
||||
use crate::github::PackageType;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::path::Path;
|
||||
use tokio::fs::File;
|
||||
|
||||
@@ -12,7 +12,11 @@ pub struct GiteaConfig {
|
||||
}
|
||||
|
||||
impl GiteaConfig {
|
||||
pub fn new(base_url: impl Into<String>, token: impl Into<String>, owner: impl Into<String>) -> Self {
|
||||
pub fn new(
|
||||
base_url: impl Into<String>,
|
||||
token: impl Into<String>,
|
||||
owner: impl Into<String>,
|
||||
) -> Self {
|
||||
let mut base_url = base_url.into();
|
||||
while base_url.ends_with('/') {
|
||||
base_url.pop();
|
||||
@@ -44,17 +48,29 @@ pub fn get_target_upload_urls(
|
||||
match pkg_type {
|
||||
PackageType::Debian => {
|
||||
if prerelease {
|
||||
vec![format!("{}/api/packages/{}/debian/pool/testing/main/upload", base, owner)]
|
||||
vec![format!(
|
||||
"{}/api/packages/{}/debian/pool/testing/main/upload",
|
||||
base, owner
|
||||
)]
|
||||
} else {
|
||||
vec![
|
||||
format!("{}/api/packages/{}/debian/pool/stable/main/upload", base, owner),
|
||||
format!("{}/api/packages/{}/debian/pool/testing/main/upload", base, owner),
|
||||
format!(
|
||||
"{}/api/packages/{}/debian/pool/stable/main/upload",
|
||||
base, owner
|
||||
),
|
||||
format!(
|
||||
"{}/api/packages/{}/debian/pool/testing/main/upload",
|
||||
base, owner
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
PackageType::Rpm => {
|
||||
if prerelease {
|
||||
vec![format!("{}/api/packages/{}/rpm/testing/upload", base, owner)]
|
||||
vec![format!(
|
||||
"{}/api/packages/{}/rpm/testing/upload",
|
||||
base, owner
|
||||
)]
|
||||
} else {
|
||||
vec![format!("{}/api/packages/{}/rpm/stable/upload", base, owner)]
|
||||
}
|
||||
@@ -82,7 +98,10 @@ impl GiteaClient {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
reqwest::header::HeaderValue::from_static(concat!("mirror-package/", env!("CARGO_PKG_VERSION"))),
|
||||
reqwest::header::HeaderValue::from_static(concat!(
|
||||
"mirror-package/",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)),
|
||||
);
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
|
||||
+14
-7
@@ -91,7 +91,10 @@ impl GitHubClient {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
reqwest::header::HeaderValue::from_static(concat!("mirror-package/", env!("CARGO_PKG_VERSION"))),
|
||||
reqwest::header::HeaderValue::from_static(concat!(
|
||||
"mirror-package/",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)),
|
||||
);
|
||||
headers.insert(
|
||||
reqwest::header::ACCEPT,
|
||||
@@ -111,7 +114,6 @@ impl GitHubClient {
|
||||
self.token.as_deref()
|
||||
}
|
||||
|
||||
|
||||
/// Ruft Releases für ein angegebenes Repository ab (Format "owner/repo").
|
||||
///
|
||||
/// Wenn `history` false ist, werden nur die neuesten Releases abgerufen.
|
||||
@@ -139,7 +141,10 @@ impl GitHubClient {
|
||||
req = req.bearer_auth(token);
|
||||
}
|
||||
|
||||
let resp = req.send().await.with_context(|| format!("Failed to send request to {}", url))?;
|
||||
let resp = req
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to send request to {}", url))?;
|
||||
let status = resp.status();
|
||||
|
||||
if !status.is_success() {
|
||||
@@ -153,10 +158,12 @@ impl GitHubClient {
|
||||
);
|
||||
}
|
||||
|
||||
let raw_releases: Vec<GhApiRelease> = resp
|
||||
.json()
|
||||
.await
|
||||
.with_context(|| format!("Failed to parse GitHub releases JSON for {}/{}", owner, name))?;
|
||||
let raw_releases: Vec<GhApiRelease> = resp.json().await.with_context(|| {
|
||||
format!(
|
||||
"Failed to parse GitHub releases JSON for {}/{}",
|
||||
owner, name
|
||||
)
|
||||
})?;
|
||||
|
||||
if raw_releases.is_empty() {
|
||||
break;
|
||||
|
||||
+59
-15
@@ -1,11 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use logger_ctdra::{log, set_log_level, LogLevel};
|
||||
use logger_ctdra::{LogLevel, log, set_log_level};
|
||||
use mirror_package::cli::{Cli, Commands, ConfigAction};
|
||||
use mirror_package::config::{
|
||||
get_config_file_path, init_config_path, load_config, modify_config, AppConfig, RepoConfig,
|
||||
AppConfig, RepoConfig, get_config_file_path, init_config_path, load_config, modify_config,
|
||||
};
|
||||
use mirror_package::pipeline::{sync_single_repository, SyncOptions, SyncReport};
|
||||
use mirror_package::pipeline::{SyncOptions, SyncReport, sync_single_repository};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -65,7 +65,9 @@ async fn main() -> Result<()> {
|
||||
let mut report = SyncReport::default();
|
||||
|
||||
for repo in repos_to_sync {
|
||||
if let Err(e) = sync_single_repository(&repo, &app_config, &options, &mut report).await {
|
||||
if let Err(e) =
|
||||
sync_single_repository(&repo, &app_config, &options, &mut report).await
|
||||
{
|
||||
log(
|
||||
"sync",
|
||||
&format!("Failed to sync repository '{}': {:#}", repo.name, e),
|
||||
@@ -128,23 +130,38 @@ async fn main() -> Result<()> {
|
||||
|
||||
log(
|
||||
"config",
|
||||
&format!("Repository '{}' removed from configuration.", normalized_name),
|
||||
&format!(
|
||||
"Repository '{}' removed from configuration.",
|
||||
normalized_name
|
||||
),
|
||||
LogLevel::Info,
|
||||
);
|
||||
}
|
||||
|
||||
Commands::List => {
|
||||
println!("Configured repositories (Config file: {:?}):", get_config_file_path());
|
||||
println!(
|
||||
"Configured repositories (Config file: {:?}):",
|
||||
get_config_file_path()
|
||||
);
|
||||
if app_config.repositories.is_empty() {
|
||||
println!(" (None configured yet. Use 'mirror-package add <owner/repo>' to add one.)");
|
||||
println!(
|
||||
" (None configured yet. Use 'mirror-package add <owner/repo>' to add one.)"
|
||||
);
|
||||
} else {
|
||||
println!("{:<40} {:<15} {:<20}", "REPOSITORY", "PRE-RELEASES", "LAST SYNCED TAG");
|
||||
println!(
|
||||
"{:<40} {:<15} {:<20}",
|
||||
"REPOSITORY", "PRE-RELEASES", "LAST SYNCED TAG"
|
||||
);
|
||||
println!("{:-<40} {:-<15} {:-<20}", "", "", "");
|
||||
for repo in &app_config.repositories {
|
||||
println!(
|
||||
"{:<40} {:<15} {:<20}",
|
||||
repo.name,
|
||||
if repo.include_prereleases { "yes" } else { "no" },
|
||||
if repo.include_prereleases {
|
||||
"yes"
|
||||
} else {
|
||||
"no"
|
||||
},
|
||||
repo.last_synced_tag.as_deref().unwrap_or("-")
|
||||
);
|
||||
}
|
||||
@@ -154,17 +171,40 @@ async fn main() -> Result<()> {
|
||||
Commands::Config(config_args) => match config_args.action {
|
||||
None | Some(ConfigAction::Show) => {
|
||||
println!("Configuration location: {:?}", get_config_file_path());
|
||||
println!("Gitea URL: {}", app_config.gitea_url.as_deref().unwrap_or("(not configured)"));
|
||||
println!("Registry Owner: {}", app_config.registry_owner.as_deref().unwrap_or("(not configured)"));
|
||||
println!(
|
||||
"Gitea URL: {}",
|
||||
app_config
|
||||
.gitea_url
|
||||
.as_deref()
|
||||
.unwrap_or("(not configured)")
|
||||
);
|
||||
println!(
|
||||
"Registry Owner: {}",
|
||||
app_config
|
||||
.registry_owner
|
||||
.as_deref()
|
||||
.unwrap_or("(not configured)")
|
||||
);
|
||||
println!(
|
||||
"Gitea Token: {}",
|
||||
if app_config.gitea_token.is_some() { "******** (set)" } else { "(not configured)" }
|
||||
if app_config.gitea_token.is_some() {
|
||||
"******** (set)"
|
||||
} else {
|
||||
"(not configured)"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"GitHub Token: {}",
|
||||
if app_config.github_token.is_some() { "******** (set)" } else { "(not configured, public access only)" }
|
||||
if app_config.github_token.is_some() {
|
||||
"******** (set)"
|
||||
} else {
|
||||
"(not configured, public access only)"
|
||||
}
|
||||
);
|
||||
println!(
|
||||
"Repositories: {} configured",
|
||||
app_config.repositories.len()
|
||||
);
|
||||
println!("Repositories: {} configured", app_config.repositories.len());
|
||||
}
|
||||
|
||||
Some(ConfigAction::Set {
|
||||
@@ -188,7 +228,11 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
})?;
|
||||
|
||||
log("config", "Configuration successfully updated.", LogLevel::Info);
|
||||
log(
|
||||
"config",
|
||||
"Configuration successfully updated.",
|
||||
LogLevel::Info,
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+32
-19
@@ -1,11 +1,11 @@
|
||||
use crate::config::{modify_config, AppConfig, RepoConfig};
|
||||
use crate::gitea::{get_target_upload_urls, GiteaClient, GiteaConfig, UploadStatus};
|
||||
use crate::config::{AppConfig, RepoConfig, modify_config};
|
||||
use crate::gitea::{GiteaClient, GiteaConfig, UploadStatus, get_target_upload_urls};
|
||||
use crate::github::GitHubClient;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use logger_ctdra::{log, LogLevel};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use logger_ctdra::{LogLevel, log};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::fs::{remove_file, File};
|
||||
use tokio::fs::{File, remove_file};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// Zusammenfassende Statistiken eines Synchronisationsvorgangs.
|
||||
@@ -52,7 +52,9 @@ pub async fn sync_single_repository(
|
||||
let gitea_token = app_config.gitea_token.as_deref().unwrap_or_default();
|
||||
let registry_owner = app_config.registry_owner.as_deref().unwrap_or_default();
|
||||
|
||||
if !options.dry_run && (gitea_url.is_empty() || gitea_token.is_empty() || registry_owner.is_empty()) {
|
||||
if !options.dry_run
|
||||
&& (gitea_url.is_empty() || gitea_token.is_empty() || registry_owner.is_empty())
|
||||
{
|
||||
bail!(
|
||||
"Missing Gitea configuration. Ensure --gitea-url, --gitea-token, and --registry-owner are provided or configured."
|
||||
);
|
||||
@@ -61,11 +63,7 @@ pub async fn sync_single_repository(
|
||||
let github_client = GitHubClient::new(app_config.github_token.clone())
|
||||
.context("Failed to initialize GitHub client")?;
|
||||
|
||||
let gitea_client = GiteaClient::new(GiteaConfig::new(
|
||||
gitea_url,
|
||||
gitea_token,
|
||||
registry_owner,
|
||||
))
|
||||
let gitea_client = GiteaClient::new(GiteaConfig::new(gitea_url, gitea_token, registry_owner))
|
||||
.context("Failed to initialize Gitea client")?;
|
||||
|
||||
let releases = github_client
|
||||
@@ -97,7 +95,11 @@ pub async fn sync_single_repository(
|
||||
|
||||
for release in &releases {
|
||||
report.releases_processed += 1;
|
||||
let release_type_str = if release.prerelease { "pre-release" } else { "stable release" };
|
||||
let release_type_str = if release.prerelease {
|
||||
"pre-release"
|
||||
} else {
|
||||
"stable release"
|
||||
};
|
||||
|
||||
log(
|
||||
"sync",
|
||||
@@ -156,7 +158,8 @@ pub async fn sync_single_repository(
|
||||
}
|
||||
|
||||
// Asset in temporäre Datei herunterladen
|
||||
let temp_file_path = download_to_temp_file(&github_client, &asset.download_url, &asset.name)
|
||||
let temp_file_path =
|
||||
download_to_temp_file(&github_client, &asset.download_url, &asset.name)
|
||||
.await
|
||||
.with_context(|| format!("Failed to download asset '{}'", asset.name))?;
|
||||
|
||||
@@ -168,7 +171,10 @@ pub async fn sync_single_repository(
|
||||
LogLevel::Debug,
|
||||
);
|
||||
|
||||
match gitea_client.upload_file(&temp_file_path, url, options.dry_run).await {
|
||||
match gitea_client
|
||||
.upload_file(&temp_file_path, url, options.dry_run)
|
||||
.await
|
||||
{
|
||||
Ok(UploadStatus::Uploaded) => {
|
||||
log(
|
||||
"upload",
|
||||
@@ -220,14 +226,14 @@ pub async fn sync_single_repository(
|
||||
}
|
||||
|
||||
// Zuletzt synchronisierten Tag persistieren, wenn kein Dry-Run
|
||||
if !options.dry_run {
|
||||
if let Some(tag) = latest_synced_tag {
|
||||
if !options.dry_run
|
||||
&& let Some(tag) = latest_synced_tag
|
||||
{
|
||||
let name_copy = repo_name.to_string();
|
||||
let _ = modify_config(move |cfg| {
|
||||
cfg.update_last_synced_tag(&name_copy, tag);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
report.repositories_processed += 1;
|
||||
Ok(())
|
||||
@@ -254,7 +260,10 @@ async fn download_to_temp_file(
|
||||
|
||||
log(
|
||||
"download",
|
||||
&format!("Downloading '{}' to temporary path {:?}...", filename, temp_path),
|
||||
&format!(
|
||||
"Downloading '{}' to temporary path {:?}...",
|
||||
filename, temp_path
|
||||
),
|
||||
LogLevel::Debug,
|
||||
);
|
||||
|
||||
@@ -263,7 +272,11 @@ async fn download_to_temp_file(
|
||||
.await
|
||||
.with_context(|| format!("Failed to create temp file {:?}", temp_path))?;
|
||||
|
||||
while let Some(chunk) = response.chunk().await.context("Error reading download stream")? {
|
||||
while let Some(chunk) = response
|
||||
.chunk()
|
||||
.await
|
||||
.context("Error reading download stream")?
|
||||
{
|
||||
file.write_all(&chunk)
|
||||
.await
|
||||
.context("Error writing chunk to temp file")?;
|
||||
|
||||
+9
-8
@@ -1,4 +1,3 @@
|
||||
|
||||
/// Sanitizes a string by removing surrounding quotes (single or double).
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -12,7 +11,10 @@ pub fn sanitize_string(input: &str) -> String {
|
||||
let trimmed = input.trim();
|
||||
if let Some(stripped) = trimmed.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
|
||||
stripped.to_string()
|
||||
} else if let Some(stripped) = trimmed.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
|
||||
} else if let Some(stripped) = trimmed
|
||||
.strip_prefix('\'')
|
||||
.and_then(|s| s.strip_suffix('\''))
|
||||
{
|
||||
stripped.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
@@ -25,7 +27,7 @@ pub fn sanitize_string(input: &str) -> String {
|
||||
///
|
||||
/// * `env_vars` - A reference to a mutable map of environment variables.
|
||||
pub fn sanitize_env_vars(env_vars: &mut std::collections::HashMap<String, String>) {
|
||||
for (_key, value) in env_vars.iter_mut() {
|
||||
for value in env_vars.values_mut() {
|
||||
*value = sanitize_string(value);
|
||||
}
|
||||
}
|
||||
@@ -35,15 +37,14 @@ pub fn sanitize_env_vars(env_vars: &mut std::collections::HashMap<String, String
|
||||
/// und umgebende Schrägstriche entfernt werden.
|
||||
pub fn clean_repo_input(input: &str) -> &str {
|
||||
let mut trimmed = input.trim();
|
||||
if (trimmed.starts_with('"') && trimmed.ends_with('"'))
|
||||
|| (trimmed.starts_with('\'') && trimmed.ends_with('\''))
|
||||
if ((trimmed.starts_with('"') && trimmed.ends_with('"'))
|
||||
|| (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
|
||||
&& trimmed.len() >= 2
|
||||
{
|
||||
if trimmed.len() >= 2 {
|
||||
trimmed = trimmed[1..trimmed.len() - 1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
let without_query_or_fragment = match trimmed.find(|c| c == '?' || c == '#') {
|
||||
let without_query_or_fragment = match trimmed.find(['?', '#']) {
|
||||
Some(idx) => &trimmed[..idx],
|
||||
None => trimmed,
|
||||
};
|
||||
|
||||
+46
-13
@@ -5,14 +5,23 @@ use std::collections::HashMap;
|
||||
#[test]
|
||||
fn test_sanitize_env_vars() {
|
||||
let mut env_vars = HashMap::new();
|
||||
env_vars.insert("GITEA_URL".to_string(), "\"https://gitea.example.com\"".to_string());
|
||||
env_vars.insert(
|
||||
"GITEA_URL".to_string(),
|
||||
"\"https://gitea.example.com\"".to_string(),
|
||||
);
|
||||
env_vars.insert("GITEA_TOKEN".to_string(), "\"token123\"".to_string());
|
||||
env_vars.insert("REGISTRY_OWNER".to_string(), "\"owner\"".to_string());
|
||||
env_vars.insert("GITHUB_TOKEN".to_string(), "\"github_token123\"".to_string());
|
||||
env_vars.insert(
|
||||
"GITHUB_TOKEN".to_string(),
|
||||
"\"github_token123\"".to_string(),
|
||||
);
|
||||
|
||||
sanitize_env_vars(&mut env_vars);
|
||||
|
||||
assert_eq!(env_vars.get("GITEA_URL").unwrap(), "https://gitea.example.com");
|
||||
assert_eq!(
|
||||
env_vars.get("GITEA_URL").unwrap(),
|
||||
"https://gitea.example.com"
|
||||
);
|
||||
assert_eq!(env_vars.get("GITEA_TOKEN").unwrap(), "token123");
|
||||
assert_eq!(env_vars.get("REGISTRY_OWNER").unwrap(), "owner");
|
||||
assert_eq!(env_vars.get("GITHUB_TOKEN").unwrap(), "github_token123");
|
||||
@@ -31,7 +40,10 @@ fn test_load_config_with_env_vars() {
|
||||
|
||||
// Only assert if the environment variables are set
|
||||
if std::env::var("GITEA_URL").is_ok() {
|
||||
assert_eq!(config.gitea_url, Some("https://gitea.example.com".to_string()));
|
||||
assert_eq!(
|
||||
config.gitea_url,
|
||||
Some("https://gitea.example.com".to_string())
|
||||
);
|
||||
assert_eq!(config.gitea_token, Some("token123".to_string()));
|
||||
assert_eq!(config.registry_owner, Some("owner".to_string()));
|
||||
assert_eq!(config.github_token, Some("github_token123".to_string()));
|
||||
@@ -40,8 +52,14 @@ fn test_load_config_with_env_vars() {
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_string() {
|
||||
assert_eq!(sanitize_string("\"https://gitea.example.com\""), "https://gitea.example.com");
|
||||
assert_eq!(sanitize_string("'https://gitea.example.com'"), "https://gitea.example.com");
|
||||
assert_eq!(
|
||||
sanitize_string("\"https://gitea.example.com\""),
|
||||
"https://gitea.example.com"
|
||||
);
|
||||
assert_eq!(
|
||||
sanitize_string("'https://gitea.example.com'"),
|
||||
"https://gitea.example.com"
|
||||
);
|
||||
assert_eq!(sanitize_string("\"token123\""), "token123");
|
||||
assert_eq!(sanitize_string("'token123'"), "token123");
|
||||
assert_eq!(sanitize_string("\"owner\""), "owner");
|
||||
@@ -68,11 +86,23 @@ fn test_sanitize_string() {
|
||||
#[test]
|
||||
fn test_clean_repo_input() {
|
||||
assert_eq!(clean_repo_input("owner/repo"), "owner/repo");
|
||||
assert_eq!(clean_repo_input("https://github.com/owner/repo"), "owner/repo");
|
||||
assert_eq!(clean_repo_input("http://github.com/owner/repo"), "owner/repo");
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/owner/repo"),
|
||||
"owner/repo"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("http://github.com/owner/repo"),
|
||||
"owner/repo"
|
||||
);
|
||||
assert_eq!(clean_repo_input("github.com/owner/repo"), "owner/repo");
|
||||
assert_eq!(clean_repo_input("https://github.com/owner/repo.git"), "owner/repo");
|
||||
assert_eq!(clean_repo_input("https://github.com/owner/repo/"), "owner/repo");
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/owner/repo.git"),
|
||||
"owner/repo"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/owner/repo/"),
|
||||
"owner/repo"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file"),
|
||||
"raspberrypi/rpi-imager"
|
||||
@@ -123,14 +153,14 @@ fn test_add_or_update_repo() {
|
||||
config.add_or_update_repo(repo.clone());
|
||||
assert_eq!(config.repositories.len(), 1);
|
||||
assert_eq!(config.repositories[0].name, "owner/repo");
|
||||
assert_eq!(config.repositories[0].include_prereleases, true);
|
||||
assert!(config.repositories[0].include_prereleases);
|
||||
|
||||
// Updating existing repo with a URL with query param
|
||||
let repo_updated = RepoConfig::new("https://github.com/owner/repo#readme", false);
|
||||
config.add_or_update_repo(repo_updated);
|
||||
assert_eq!(config.repositories.len(), 1);
|
||||
assert_eq!(config.repositories[0].name, "owner/repo");
|
||||
assert_eq!(config.repositories[0].include_prereleases, false);
|
||||
assert!(!config.repositories[0].include_prereleases);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -159,5 +189,8 @@ fn test_update_last_synced_tag() {
|
||||
let repo = RepoConfig::new("owner/repo", true);
|
||||
config.add_or_update_repo(repo.clone());
|
||||
config.update_last_synced_tag("owner/repo", "v1.0.0".to_string());
|
||||
assert_eq!(config.find_repo("owner/repo").unwrap().last_synced_tag, Some("v1.0.0".to_string()));
|
||||
assert_eq!(
|
||||
config.find_repo("owner/repo").unwrap().last_synced_tag,
|
||||
Some("v1.0.0".to_string())
|
||||
);
|
||||
}
|
||||
+20
-8
@@ -41,7 +41,10 @@ fn test_package_classification() {
|
||||
);
|
||||
|
||||
// Nicht unterstützte Paketformate sollten ignoriert werden
|
||||
assert_eq!(PackageType::from_filename("rpi-imager-1.8.5.AppImage"), None);
|
||||
assert_eq!(
|
||||
PackageType::from_filename("rpi-imager-1.8.5.AppImage"),
|
||||
None
|
||||
);
|
||||
assert_eq!(PackageType::from_filename("rpi-imager-1.8.5.dmg"), None);
|
||||
assert_eq!(PackageType::from_filename("rpi-imager-1.8.5.exe"), None);
|
||||
assert_eq!(PackageType::from_filename("source-code.tar.gz"), None);
|
||||
@@ -57,19 +60,25 @@ fn test_parse_repo_owner_name() {
|
||||
("raspberrypi", "rpi-imager")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repo_owner_name("https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher").unwrap(),
|
||||
parse_repo_owner_name("https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher")
|
||||
.unwrap(),
|
||||
("Heroic-Games-Launcher", "HeroicGamesLauncher")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repo_owner_name("https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher.git").unwrap(),
|
||||
parse_repo_owner_name("https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher.git")
|
||||
.unwrap(),
|
||||
("Heroic-Games-Launcher", "HeroicGamesLauncher")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repo_owner_name("https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file").unwrap(),
|
||||
parse_repo_owner_name("https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file")
|
||||
.unwrap(),
|
||||
("raspberrypi", "rpi-imager")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repo_owner_name("https://github.com/raspberrypi/rpi-imager/?tab=readme-ov-file#install").unwrap(),
|
||||
parse_repo_owner_name(
|
||||
"https://github.com/raspberrypi/rpi-imager/?tab=readme-ov-file#install"
|
||||
)
|
||||
.unwrap(),
|
||||
("raspberrypi", "rpi-imager")
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -86,14 +95,17 @@ async fn test_github_client_empty_token() {
|
||||
use mirror_package::github::GitHubClient;
|
||||
|
||||
// Test that a client with an empty or whitespace token sanitizes it to None
|
||||
let client = GitHubClient::new(Some("".to_string())).expect("Failed to create client with empty token");
|
||||
let client =
|
||||
GitHubClient::new(Some("".to_string())).expect("Failed to create client with empty token");
|
||||
assert_eq!(client.token(), None);
|
||||
|
||||
let client_whitespace = GitHubClient::new(Some(" ".to_string())).expect("Failed to create client with whitespace token");
|
||||
let client_whitespace = GitHubClient::new(Some(" ".to_string()))
|
||||
.expect("Failed to create client with whitespace token");
|
||||
assert_eq!(client_whitespace.token(), None);
|
||||
|
||||
// Test with a valid token
|
||||
let client_with_token = GitHubClient::new(Some("valid_token".to_string())).expect("Failed to create client with valid token");
|
||||
let client_with_token = GitHubClient::new(Some("valid_token".to_string()))
|
||||
.expect("Failed to create client with valid token");
|
||||
assert_eq!(client_with_token.token(), Some("valid_token"));
|
||||
|
||||
// Test with no token
|
||||
|
||||
Reference in New Issue
Block a user