Compare commits
61
Commits
01e511cdbb
..
v1.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||
|
|
c018f20d4a
|
||
|
|
42829ce0a0
|
||
|
|
d5dd11c6ce
|
||
|
|
bc3f4c4ec6
|
||
|
|
c3d5ed8b26
|
||
|
|
a7c0c25b26
|
||
|
|
08cca0ca51
|
||
|
|
2f61e15646
|
||
|
|
5e5e3f5b17
|
||
|
|
f04c9611b6
|
||
|
|
14a1c4944a
|
||
|
|
c743f51526
|
||
|
|
f4160df1d7
|
||
|
|
3f5f18d98f
|
||
|
|
85e9046768
|
||
|
|
5728649c79
|
||
|
|
8eb0048606
|
||
|
|
d92cb57d87
|
||
|
|
884f2dfec1
|
||
|
|
e7407da5b4
|
||
|
|
0f28e1bdbc
|
||
|
|
bab42a5e88
|
||
|
|
65785eac76
|
||
|
|
47c6567ec7
|
||
|
|
52964e28f0
|
||
|
|
9441d647a8
|
||
|
|
966bcc3778
|
||
|
|
7abf93f28c
|
@@ -9,6 +9,10 @@ jobs:
|
||||
release-and-publish:
|
||||
name: Build, Publish Packages (Stable) & Create Release
|
||||
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
|
||||
@@ -19,18 +23,58 @@ jobs:
|
||||
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
|
||||
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,25 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
name: Dependency-Updates prüfen & Pull Requests erstellen
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:44.79.2
|
||||
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_GIT_AUTHOR: "Renovate Bot <renovate-bot@creative-dragonslayer.de>"
|
||||
RENOVATE_HOST_RULES: >-
|
||||
[{"hostType":"cargo","matchHost":"gitea.creative-dragonslayer.de","token":"${{ secrets.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
|
||||
@@ -9,6 +9,10 @@ jobs:
|
||||
build-and-publish:
|
||||
name: Build, Publish Packages (Testing) & Create Preview Release
|
||||
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
|
||||
@@ -19,18 +23,58 @@ jobs:
|
||||
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
|
||||
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@v1
|
||||
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
|
||||
@@ -109,3 +109,4 @@ fabric.properties
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
.junie/plans
|
||||
|
||||
Generated
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||
<state>
|
||||
<map>
|
||||
<entry key="GitHub Workflow">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="name" value="GitHub Workflow" />
|
||||
<option name="relativePathToSchema" value="https://www.schemastore.org/github-workflow.json" />
|
||||
<option name="applicationDefined" value="true" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/main.yaml" />
|
||||
</Item>
|
||||
<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>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</state>
|
||||
</component>
|
||||
</project>
|
||||
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" />
|
||||
|
||||
Generated
+1
-2
@@ -866,7 +866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mirror-package"
|
||||
version = "0.1.1"
|
||||
version = "1.0.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -874,7 +874,6 @@ dependencies = [
|
||||
"logger-ctdra",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
||||
+16
-9
@@ -1,12 +1,12 @@
|
||||
[package]
|
||||
name = "mirror-package"
|
||||
version = "0.1.1"
|
||||
version = "1.0.3"
|
||||
edition = "2024"
|
||||
authors = ['DragonSlayer_14']
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0-or-later"
|
||||
repository = "https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage"
|
||||
description = "Mirror prebuilt Linux packages from GitHub Releases to Gitea Package Registry"
|
||||
description = "Spiegelt vorkompilierte Linux-Pakete aus GitHub-Releases in Gitea-Paket-Registries"
|
||||
|
||||
[dependencies]
|
||||
config-ctdra = { version = "1.0.4", registry = "gitea" }
|
||||
@@ -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]
|
||||
@@ -29,18 +28,26 @@ maintainer = "DragonSlayer_14"
|
||||
copyright = "2026 DragonSlayer_14"
|
||||
section = "utils"
|
||||
priority = "optional"
|
||||
depends = "$auto"
|
||||
depends = "$auto, ca-certificates"
|
||||
extended-description = """\
|
||||
Mirror prebuilt Linux packages (.deb, .rpm, .pkg.tar.zst) from GitHub Releases into Gitea Package Registries.\
|
||||
Spiegelt vorkompilierte Linux-Pakete (.deb, .rpm, .pkg.tar.zst) aus GitHub-Releases in Gitea-Paket-Registries.\
|
||||
"""
|
||||
assets = []
|
||||
assets = [
|
||||
["target/release/mirror-package", "usr/bin/mirror-package", "755"],
|
||||
["README.md", "usr/share/doc/mirror-package/README.md", "644"],
|
||||
["LICENSE", "usr/share/doc/mirror-package/copyright", "644"],
|
||||
]
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
assets = []
|
||||
requires = { }
|
||||
assets = [
|
||||
{ source = "target/release/mirror-package", dest = "/usr/bin/mirror-package", mode = "755" },
|
||||
{ source = "README.md", dest = "/usr/share/doc/mirror-package/README.md", mode = "644", doc = true },
|
||||
{ source = "LICENSE", dest = "/usr/share/licenses/mirror-package/LICENSE", mode = "644", license = true },
|
||||
]
|
||||
requires = { "ca-certificates" = "*", "glibc" = "*" }
|
||||
|
||||
[package.metadata.arch]
|
||||
pkgrel = "1"
|
||||
arch = "x86_64"
|
||||
depends = ["gcc-libs", "glibc"]
|
||||
depends = ["gcc-libs", "glibc", "ca-certificates"]
|
||||
optdepends = []
|
||||
|
||||
+7
-7
@@ -1,9 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Minimal and secure runtime image
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
# Minimales und gehärtetes Runtime-Image
|
||||
FROM debian:trixie-slim AS runtime
|
||||
|
||||
# Install CA certificates and minimal runtime dynamic libraries
|
||||
# CA-Zertifikate und minimale dynamische Laufzeitbibliotheken installieren
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
@@ -19,18 +19,18 @@ RUN apt-get update && \
|
||||
/etc/mirror-package && \
|
||||
chown -R appuser:appuser /home/appuser /etc/mirror-package
|
||||
|
||||
# Build argument pointing to the prebuilt binary
|
||||
# Build-Argument mit Verweis auf die vorkompilierte Binary
|
||||
ARG TARGET_BIN=target/release/mirror-package
|
||||
COPY ${TARGET_BIN} /usr/local/bin/mirror-package
|
||||
|
||||
# Set strict executable permissions
|
||||
# Strikte Ausführungsrechte setzen
|
||||
RUN chmod 0755 /usr/local/bin/mirror-package
|
||||
|
||||
# Run as unprivileged user
|
||||
# Als unprivilegierter Benutzer ausführen
|
||||
USER 10001:10001
|
||||
WORKDIR /home/appuser
|
||||
|
||||
# Expose volume mount points for configuration and persistent state/logs
|
||||
# Volume-Mount-Punkte für Konfiguration und persistenten Status / Logs exponieren
|
||||
VOLUME ["/home/appuser/.config/mirror-package", "/home/appuser/.local/state/mirror-package"]
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/mirror-package"]
|
||||
|
||||
@@ -1,131 +1,134 @@
|
||||
[](https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage/actions?workflow=main.yaml)
|
||||
[](https://gitea.creative-dragonslayer.de/Linuxapps/MirrorPackage/actions?workflow=testing.yaml)
|
||||
|
||||
# mirror-package
|
||||
|
||||
Automated tool to extract, download, and mirror prebuilt Linux packages from GitHub Releases into a self-hosted Gitea / Forgejo Package Registry.
|
||||
Automatisiertes Werkzeug zum Extrahieren, Herunterladen und Spiegeln vorkompilierter Linux-Pakete aus GitHub-Releases in eine selbstgehostete Gitea- / Forgejo-Paket-Registry.
|
||||
|
||||
`mirror-package` tracks configured GitHub repositories (such as `raspberrypi/rpi-imager` or `Heroic-Games-Launcher/HeroicGamesLauncher`), identifies precompiled Linux packages (`.deb`, `.rpm`, `.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`), and automatically publishes them into the appropriate Gitea Package Registry distributions according to release stability rules.
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
## Funktionen
|
||||
|
||||
- **Multi-Distribution Package Support**:
|
||||
- **Unterstützung mehrerer Distributionen**:
|
||||
- **Debian / Ubuntu** (`.deb`)
|
||||
- **Fedora / RHEL / openSUSE** (`.rpm`)
|
||||
- **Arch Linux** (`.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz`, `.pacman`)
|
||||
- **Distribution Routing Rules**:
|
||||
- **Stable Releases**: Debian packages are published to **both** `stable` and `testing` Debian pools (`pool/stable/main` and `pool/testing/main`); RPM packages to `rpm/stable`; Arch packages to `arch/stable`.
|
||||
- **Pre-Releases**: Published **only** to testing channels: Debian `pool/testing/main`, RPM `rpm/testing`, and Arch `arch/testing`.
|
||||
- **Persistent Configuration with `config-ctdra`**:
|
||||
- Automatically stores settings in user (`~/.config/mirror-package/config.toml`) or system (`/etc/mirror-package/config.toml`) paths.
|
||||
- Adding or removing repositories via CLI automatically updates the configuration file.
|
||||
- **Flexible Authentication**:
|
||||
- GitHub releases can be queried anonymously (no token required).
|
||||
- Optional GitHub Personal Access Token support for higher API rate limits.
|
||||
- Gitea instance URL, API token, and registry owner configurable via CLI flags, environment variables, or config file.
|
||||
- **Diagnostics & Dry-Run Mode**:
|
||||
- Unified file and console logging powered by `logger-ctdra`.
|
||||
- `--dry-run` flag to inspect download and upload steps without making remote changes.
|
||||
- **Distributions-Routing-Regeln**:
|
||||
- **Stabile Releases**: Debian-Pakete werden **sowohl** im `stable`- als auch im `testing`-Debian-Pool veröffentlicht (`pool/stable/main` und `pool/testing/main`); RPM-Pakete unter `rpm/stable`; Arch-Pakete unter `arch/stable`.
|
||||
- **Pre-Releases**: Werden **ausschließlich** in Testing-Kanälen veröffentlicht: Debian `pool/testing/main`, RPM `rpm/testing` und Arch `arch/testing`.
|
||||
- **Persistente Konfiguration über `config-ctdra`**:
|
||||
- Speichert Einstellungen automatisch im Benutzerpfad (`~/.config/mirror-package/config.toml`) oder Systempfad (`/etc/mirror-package/config.toml`).
|
||||
- Das Hinzufügen oder Entfernen von Repositories über die CLI aktualisiert automatisch die Konfigurationsdatei.
|
||||
- **Flexible Authentifizierung**:
|
||||
- GitHub-Releases können anonym abgefragt werden (kein Token erforderlich).
|
||||
- Optionale Unterstützung für GitHub Personal Access Tokens zur Vermeidung von API-Rate-Limits.
|
||||
- Gitea-Instanz-URL, API-Token und Registry-Owner können per CLI-Flags, Umgebungsvariablen oder Konfigurationsdatei festgelegt werden.
|
||||
- **Diagnose & Dry-Run-Modus**:
|
||||
- Einheitliches Datei- und Konsolen-Logging über `logger-ctdra`.
|
||||
- `--dry-run`-Flag zur Simulation von Download- und Upload-Schritten ohne Änderungen an Remote-Systemen vorzunehmen.
|
||||
|
||||
---
|
||||
|
||||
## CLI Usage & Commands
|
||||
## CLI-Verwendung & Befehle
|
||||
|
||||
### 1. Configuration (`config`)
|
||||
Set up your Gitea credentials and optional GitHub token:
|
||||
### 1. Konfiguration (`config`)
|
||||
Richte deine Gitea-Zugangsdaten und das optionale GitHub-Token ein:
|
||||
|
||||
```bash
|
||||
# Configure Gitea instance and registry owner
|
||||
# Gitea-Instanz und Registry-Owner konfigurieren
|
||||
mirror-package config set \
|
||||
--gitea-url "https://gitea.creative-dragonslayer.de" \
|
||||
--gitea-token "your_gitea_api_token" \
|
||||
--gitea-token "dein_gitea_api_token" \
|
||||
--registry-owner "Linuxapps"
|
||||
|
||||
# Optional: Configure GitHub Token for rate limits
|
||||
# Optional: GitHub-Token für höhere API-Rate-Limits konfigurieren
|
||||
mirror-package config set --github-token "ghp_xxxxxxxxxxxx"
|
||||
|
||||
# Display current configuration
|
||||
# Aktuelle Konfiguration anzeigen
|
||||
mirror-package config show
|
||||
```
|
||||
|
||||
Credentials can also be passed via environment variables (`GITEA_URL`, `GITEA_TOKEN`, `REGISTRY_OWNER`, `GITHUB_TOKEN`) or global CLI arguments.
|
||||
Zugangsdaten können auch über Umgebungsvariablen (`GITEA_URL`, `GITEA_TOKEN`, `REGISTRY_OWNER`, `GITHUB_TOKEN`) oder globale CLI-Argumente übergeben werden.
|
||||
|
||||
### 2. Managing Monitored Repositories (`add`, `remove`, `list`)
|
||||
### 2. Überwachte Repositories verwalten (`add`, `remove`, `list`)
|
||||
|
||||
```bash
|
||||
# Add repositories to persistent configuration
|
||||
# Repositories zur persistenten Konfiguration hinzufügen
|
||||
mirror-package add raspberrypi/rpi-imager
|
||||
mirror-package add Heroic-Games-Launcher/HeroicGamesLauncher
|
||||
|
||||
# Add repository without pre-release syncing
|
||||
# Repository ohne Synchronisierung von Pre-Releases hinzufügen
|
||||
mirror-package add some-owner/some-repo --no-prereleases
|
||||
|
||||
# List configured repositories and their sync status
|
||||
# Konfigurierte Repositories und deren Sync-Status auflisten
|
||||
mirror-package list
|
||||
|
||||
# Remove a repository
|
||||
# Ein Repository entfernen
|
||||
mirror-package remove raspberrypi/rpi-imager
|
||||
```
|
||||
|
||||
### 3. Synchronizing Packages (`sync`)
|
||||
### 3. Pakete synchronisieren (`sync`)
|
||||
|
||||
```bash
|
||||
# Sync all configured repositories (latest stable and latest pre-release)
|
||||
# Alle konfigurierten Repositories synchronisieren (neueste stabile Version und neuestes Pre-Release)
|
||||
mirror-package sync
|
||||
|
||||
# Sync a specific repository
|
||||
# Ein bestimmtes Repository synchronisieren
|
||||
mirror-package sync raspberrypi/rpi-imager
|
||||
|
||||
# Sync all historical releases for a repository
|
||||
# Alle historischen Releases eines Repositories synchronisieren
|
||||
mirror-package sync raspberrypi/rpi-imager --history
|
||||
|
||||
# Dry-run test (simulates without uploading)
|
||||
# Dry-Run-Test (Simulation ohne Upload)
|
||||
mirror-package sync --dry-run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker & Container Usage
|
||||
## Docker & Container-Nutzung
|
||||
|
||||
`mirror-package` is packaged and published as an ultra-secure, lightweight container image to the Gitea Container Registry.
|
||||
`mirror-package` wird als minimales, gehärtetes Container-Image in der Gitea Container Registry bereitgestellt.
|
||||
|
||||
### 1. Image Registry & Tagging Strategy
|
||||
### 1. Image-Registry & Tagging-Strategie
|
||||
|
||||
- **Stable Releases (`main` branch)**:
|
||||
- **Stabile Releases (`main`-Branch)**:
|
||||
- `<registry>/<owner>/mirror-package:latest`
|
||||
- `<registry>/<owner>/mirror-package:<version>`
|
||||
- `<registry>/<owner>/mirror-package:v<version>`
|
||||
- `<registry>/<owner>/mirror-package:<version>.<build_number>`
|
||||
- **Testing & Preview Releases (`testing` branch)**:
|
||||
- **Testing- & Vorschau-Releases (`testing`-Branch)**:
|
||||
- `<registry>/<owner>/mirror-package:testing`
|
||||
- `<registry>/<owner>/mirror-package:<version>-preview`
|
||||
- `<registry>/<owner>/mirror-package:<version>-testing`
|
||||
- `<registry>/<owner>/mirror-package:<version>-preview.<build_number>`
|
||||
- `<registry>/<owner>/mirror-package:testing-<build_number>`
|
||||
- *(Note: The `:latest` tag is strictly reserved for the stable `main` workflow).*
|
||||
- *(Hinweis: Der Tag `:latest` ist strikt dem stabilen `main`-Workflow vorbehalten).*
|
||||
|
||||
### 2. Persistent Storage Paths
|
||||
### 2. Persistente Speicherpfade
|
||||
|
||||
To persist configurations, tracked repository states, and logs across container restarts, mount the following container paths to host directories or Docker volumes:
|
||||
Um Konfigurationen, Status und Protokolle über Container-Neustarts hinweg beizubehalten, binde folgende Pfade an Host-Verzeichnisse oder Docker-Volumes:
|
||||
|
||||
| Container Path | Purpose | Recommended Mount Type |
|
||||
|---|---|---|
|
||||
| `/home/appuser/.config/mirror-package` | Stores `config.toml` (credentials & repository list) | Host Directory / Volume (rw) |
|
||||
| `/home/appuser/.local/state/mirror-package` | Persistent application logs & state files | Host Directory / Volume (rw) |
|
||||
| `/tmp` | Download & stream buffer for package binaries | `tmpfs` (RAM / tempfs, rw, noexec) |
|
||||
| Container-Pfad | Zweck | Empfohlener Mount-Typ |
|
||||
|---------------------------------------------|-------------------------------------------------------|------------------------------------|
|
||||
| `/home/appuser/.config/mirror-package` | Speichert `config.toml` (Zugangsdaten & Repo-Liste) | Host-Verzeichnis / Volume (rw) |
|
||||
| `/home/appuser/.local/state/mirror-package` | Persistente Anwendungsprotokolle & Statusdateien | Host-Verzeichnis / Volume (rw) |
|
||||
| `/tmp` | Download- & Streaming-Puffer für Paketdateien | `tmpfs` (RAM / tempfs, rw, noexec) |
|
||||
|
||||
### 3. Security Hardening
|
||||
### 3. Sicherheitshärtung
|
||||
|
||||
The container is built with security as the highest priority:
|
||||
- **Unprivileged User**: Runs as `appuser` (UID `10001`, GID `10001`), never as `root`.
|
||||
- **Read-Only Root Filesystem**: Fully operational with `--read-only` / `read_only: true`.
|
||||
- **No Capabilities**: All Linux capabilities can be safely dropped (`--cap-drop=ALL`).
|
||||
- **No Privilege Escalation**: Enforces `no-new-privileges:true`.
|
||||
- **Minimal Image Size**: Based on Debian Bookworm slim, containing only CA certificates and necessary shared libraries (~40 MB).
|
||||
Der Container wurde nach höchsten Sicherheitsstandards aufgebaut:
|
||||
- **Unprivilegierter Benutzer**: Läuft als `appuser` (UID `10001`, GID `10001`), niemals als `root`.
|
||||
- **Read-Only Root-Dateisystem**: Voll funktionsfähig mit `--read-only` / `read_only: true`.
|
||||
- **Keine Capabilities**: Sämtliche Linux-Capabilities können sicher entzogen werden (`--cap-drop=ALL`).
|
||||
- **Keine Rechteausweitung**: Erzwingt `no-new-privileges:true`.
|
||||
- **Minimale Image-Größe**: Basiert auf Debian Trixie Slim und enthält nur CA-Zertifikate und notwendige dynamische Bibliotheken (~40 MB).
|
||||
|
||||
### 4. Running via Docker CLI
|
||||
### 4. Ausführung über Docker CLI
|
||||
|
||||
```bash
|
||||
# Initialize / configure credentials
|
||||
# Zugangsdaten initialisieren / konfigurieren
|
||||
docker run --rm \
|
||||
--name mirror-package \
|
||||
--read-only \
|
||||
@@ -137,10 +140,10 @@ docker run --rm \
|
||||
gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest \
|
||||
config set \
|
||||
--gitea-url "https://gitea.creative-dragonslayer.de" \
|
||||
--gitea-token "your_token" \
|
||||
--gitea-token "dein_token" \
|
||||
--registry-owner "Linuxapps"
|
||||
|
||||
# Add repositories
|
||||
# Repositories hinzufügen
|
||||
docker run --rm \
|
||||
--read-only \
|
||||
--cap-drop=ALL \
|
||||
@@ -151,7 +154,7 @@ docker run --rm \
|
||||
gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest \
|
||||
add raspberrypi/rpi-imager
|
||||
|
||||
# Run synchronization (can be triggered by a host cronjob or scheduler)
|
||||
# Synchronisation ausführen (kann über Cronjob oder Scheduler auf dem Host getriggert werden)
|
||||
docker run --rm \
|
||||
--read-only \
|
||||
--cap-drop=ALL \
|
||||
@@ -163,7 +166,7 @@ docker run --rm \
|
||||
sync
|
||||
```
|
||||
|
||||
### 5. Docker Compose Example (`docker-compose.yml`)
|
||||
### 5. Docker Compose Beispiel (`docker-compose.yml`)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
@@ -183,72 +186,71 @@ services:
|
||||
- /tmp:rw,noexec,nosuid,size=2G
|
||||
environment:
|
||||
- GITEA_URL=https://gitea.creative-dragonslayer.de
|
||||
- GITEA_TOKEN=your_gitea_api_token
|
||||
- GITEA_TOKEN=dein_gitea_api_token
|
||||
- REGISTRY_OWNER=Linuxapps
|
||||
- GITHUB_TOKEN=your_optional_github_pat
|
||||
- GITHUB_TOKEN=dein_optionaler_github_pat
|
||||
- LOG_LEVEL=info
|
||||
command: ["sync"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Repository Structure
|
||||
## Repository-Struktur
|
||||
|
||||
```text
|
||||
├── .cargo/
|
||||
│ └── config.toml # Linker & Cargo configuration
|
||||
│ └── config.toml # Linker- & Cargo-Konfiguration
|
||||
├── .gitea/
|
||||
│ └── workflows/
|
||||
│ ├── main.yaml # CI/CD: Release, Packages & Container (Stable)
|
||||
│ └── testing.yaml # CI/CD: Preview, Packages & Container (Testing)
|
||||
│ ├── main.yaml # CI/CD: Release, Pakete & Container (Stable)
|
||||
│ └── testing.yaml # CI/CD: Preview, Pakete & Container (Testing)
|
||||
├── scripts/
|
||||
│ ├── get-build-number.py # Dynamic build/revision number resolution
|
||||
│ └── package-arch.py # Native Arch Linux package builder
|
||||
│ ├── get-build-number.py # Dynamische Ermittlung der Build-/Revisionsnummer
|
||||
│ └── package-arch.py # Erstellung nativer Arch Linux-Pakete
|
||||
├── src/
|
||||
│ ├── main.rs # CLI entrypoint & subcommand dispatch
|
||||
│ ├── lib.rs # Library root & module exports
|
||||
│ ├── cli.rs # Clap CLI arguments & options
|
||||
│ ├── config.rs # config-ctdra integration & models
|
||||
│ ├── gitea.rs # Gitea Package Registry upload client
|
||||
│ ├── github.rs # GitHub API client & package classifier
|
||||
│ └── pipeline.rs # End-to-end sync workflow & temp storage
|
||||
│ ├── main.rs # CLI-Einstiegspunkt & Befehlsausführung
|
||||
│ ├── lib.rs # Bibliotheks-Wurzel & Modulexporte
|
||||
│ ├── cli.rs # Clap-CLI-Argumente & Optionen
|
||||
│ ├── config.rs # config-ctdra Anbindung & Konfigurationsmodelle
|
||||
│ ├── gitea.rs # Gitea-Paket-Registry Upload-Client
|
||||
│ ├── github.rs # GitHub-API-Client & Paket-Klassifizierung
|
||||
│ └── pipeline.rs # End-to-End-Synchronisationspipeline & temporärer Speicher
|
||||
├── tests/
|
||||
│ ├── config_tests.rs # Tests for configuration management
|
||||
│ ├── gitea_tests.rs # Tests for Gitea upload routing
|
||||
│ └── github_tests.rs # Tests for package classification & parsing
|
||||
├── .dockerignore # Container build ignore rules
|
||||
├── Dockerfile # Secure minimal runtime container
|
||||
├── docker-compose.example.yml # Example Docker Compose configuration
|
||||
├── Cargo.toml # Project manifest and packaging metadata
|
||||
├── LICENSE # GPL-3.0-or-later License
|
||||
├── AGENTS.md # Agent & developer guidelines
|
||||
└── README.md # Documentation
|
||||
│ ├── config_tests.rs # Tests für die Konfigurationsverwaltung
|
||||
│ ├── gitea_tests.rs # Tests für das Gitea-Upload-Routing
|
||||
│ └── github_tests.rs # Tests für Paket-Klassifizierung & Parsing
|
||||
├── .dockerignore # Ausschlussregeln für Container-Builds
|
||||
├── Dockerfile # Gehärtetes, minimales Runtime-Container-Image
|
||||
├── docker-compose.example.yml # Beispielkonfiguration für Docker Compose
|
||||
├── Cargo.toml # Projekt-Manifest und Paketierungs-Metadaten
|
||||
├── LICENSE # GPL-3.0-or-later Lizenztext
|
||||
├── AGENTS.md # Agenten- & Entwickler-Richtlinien
|
||||
└── README.md # Projektdokumentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local Development & Building
|
||||
## Lokale Entwicklung & Bauen
|
||||
|
||||
### Prerequisites
|
||||
- **Rust & Cargo** (Stable toolchain, Edition 2024 supported)
|
||||
- **Python 3** (for packaging scripts)
|
||||
### Voraussetzungen
|
||||
- **Rust & Cargo** (Stable Toolchain, Edition 2024 unterstützt)
|
||||
- **Python 3** (für Paketierungsskripte)
|
||||
|
||||
### Build & Test Commands
|
||||
### Befehle zum Bauen & Testen
|
||||
|
||||
```bash
|
||||
# Run syntax and type checks
|
||||
# Syntax- und Typprüfung ausführen
|
||||
cargo check
|
||||
|
||||
# Run unit tests
|
||||
# Unit-Tests ausführen
|
||||
cargo test
|
||||
|
||||
# Build release binary
|
||||
# Release-Binary kompilieren
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [GPL-3.0-or-later](LICENSE) license.
|
||||
## Lizenz
|
||||
|
||||
Dieses Projekt ist unter der [GPL-3.0-or-later](LICENSE)-Lizenz lizenziert.
|
||||
|
||||
@@ -2,14 +2,14 @@ services:
|
||||
mirror-package:
|
||||
image: gitea.creative-dragonslayer.de/linuxapps/mirror-package:latest
|
||||
container_name: mirror-package
|
||||
# Security: run as unprivileged user, read-only rootfs, drop capabilities
|
||||
# Sicherheit: Als unprivilegierter Benutzer ausführen, Read-Only-Dateisystem, Capabilities entziehen
|
||||
user: "10001:10001"
|
||||
read_only: true
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
# Persistent storage and temporary stream buffer
|
||||
# Persistenter Speicher und temporärer Streaming-Puffer
|
||||
volumes:
|
||||
- ./config:/home/appuser/.config/mirror-package
|
||||
- ./state:/home/appuser/.local/state/mirror-package
|
||||
@@ -17,8 +17,8 @@ services:
|
||||
- /tmp:rw,noexec,nosuid,size=2G
|
||||
environment:
|
||||
- GITEA_URL=https://gitea.creative-dragonslayer.de
|
||||
- GITEA_TOKEN=your_gitea_api_token
|
||||
- GITEA_TOKEN=dein_gitea_api_token
|
||||
- REGISTRY_OWNER=Linuxapps
|
||||
- GITHUB_TOKEN=your_optional_github_pat
|
||||
- GITHUB_TOKEN=dein_optionaler_github_pat
|
||||
- LOG_LEVEL=info
|
||||
command: ["sync"]
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
|
||||
#################################################################################
|
||||
# WARNING: Do not store sensitive information in this file, #
|
||||
# as its contents will be included in the Qodana report. #
|
||||
#################################################################################
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
# Quality gate. Will fail the CI/CD pipeline if any condition is not met
|
||||
# severityThresholds - configures maximum thresholds for different problem severities
|
||||
# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
|
||||
# dependencyLicenses - fails the run on prohibited or unknown dependency licenses
|
||||
# Code Coverage is available in Ultimate and Ultimate Plus plans
|
||||
#failureConditions:
|
||||
# severityThresholds:
|
||||
# any: 15
|
||||
# critical: 5
|
||||
# testCoverageThresholds:
|
||||
# fresh: 70
|
||||
# total: 50
|
||||
# dependencyLicenses:
|
||||
# failOnProhibited: true
|
||||
# failOnUnknown: false
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-<linter>:2026.2
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"timezone": "Europe/Berlin",
|
||||
"schedule": ["before 6am on monday"],
|
||||
"baseBranches": ["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",
|
||||
"fileMatch": ["^\\.gitea/workflows/.+\\.ya?ml$"],
|
||||
"matchStrings": [
|
||||
"TRIVY_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "aquasecurity/trivy",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": ["^\\.gitea/workflows/.+\\.ya?ml$"],
|
||||
"matchStrings": [
|
||||
"OSV_SCANNER_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "google/osv-scanner",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": ["^\\.gitea/workflows/.+\\.ya?ml$"],
|
||||
"matchStrings": [
|
||||
"TRUFFLEHOG_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "trufflesecurity/trufflehog",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": ["^\\.gitea/workflows/.+\\.ya?ml$"],
|
||||
"matchStrings": [
|
||||
"CARGO_BINSTALL_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-bins/cargo-binstall",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": ["^\\.gitea/workflows/.+\\.ya?ml$"],
|
||||
"matchStrings": [
|
||||
"CARGO_DEB_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-deb",
|
||||
"datasourceTemplate": "crate"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"fileMatch": ["^\\.gitea/workflows/.+\\.ya?ml$"],
|
||||
"matchStrings": [
|
||||
"CARGO_GENERATE_RPM_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-generate-rpm",
|
||||
"datasourceTemplate": "crate"
|
||||
}
|
||||
]
|
||||
}
|
||||
+24
-10
@@ -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:
|
||||
try:
|
||||
url = f"{gitea_url.rstrip('/')}/api/v1/packages/{owner}?limit=50"
|
||||
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:
|
||||
# 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}/"
|
||||
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:
|
||||
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 Exception as e:
|
||||
sys.stderr.write(f"[Hinweis] Konnte Gitea Packages nicht abfragen: {e}\n")
|
||||
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 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,308 @@
|
||||
#!/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["GITEA_URL"].rstrip("/")
|
||||
repo = os.environ["REPO"]
|
||||
token = os.environ["TOKEN"]
|
||||
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)
|
||||
|
||||
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()
|
||||
+27
-27
@@ -1,41 +1,41 @@
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Command line parser for mirror-package.
|
||||
/// Befehlszeilen-Parser für mirror-package.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "mirror-package",
|
||||
author,
|
||||
version,
|
||||
about = "Mirror Linux packages (.deb, .rpm, .pkg.tar.zst) from GitHub Releases to Gitea Package Registry",
|
||||
about = "Spiegelt Linux-Pakete (.deb, .rpm, .pkg.tar.zst) aus GitHub-Releases in Gitea-Paket-Registries",
|
||||
long_about = None
|
||||
)]
|
||||
pub struct Cli {
|
||||
/// Gitea instance base URL (e.g. https://gitea.creative-dragonslayer.de)
|
||||
/// Basis-URL der Gitea-Instanz (z. B. https://gitea.creative-dragonslayer.de)
|
||||
#[arg(long, env = "GITEA_URL", global = true)]
|
||||
pub gitea_url: Option<String>,
|
||||
|
||||
/// Gitea API Token with package write permissions
|
||||
/// Gitea-API-Token mit Schreibberechtigung für Pakete
|
||||
#[arg(long, env = "GITEA_TOKEN", global = true)]
|
||||
pub gitea_token: Option<String>,
|
||||
|
||||
/// Gitea Package Registry owner / organization
|
||||
/// Gitea-Paket-Registry-Owner / Organisation
|
||||
#[arg(long, env = "REGISTRY_OWNER", global = true)]
|
||||
pub registry_owner: Option<String>,
|
||||
|
||||
/// Optional GitHub Token for API requests (avoids rate limits)
|
||||
/// Optionales GitHub-Token für API-Anfragen (vermeidet Rate-Limits)
|
||||
#[arg(long, env = "GITHUB_TOKEN", global = true)]
|
||||
pub github_token: Option<String>,
|
||||
|
||||
/// Custom path to configuration file
|
||||
/// Benutzerdefinierter Pfad zur Konfigurationsdatei
|
||||
#[arg(long, global = true)]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
/// Perform a dry run without downloading or uploading packages
|
||||
/// Führt einen Probelauf ohne Herunterladen oder Hochladen von Paketen durch
|
||||
#[arg(long, global = true)]
|
||||
pub dry_run: bool,
|
||||
|
||||
/// Logging level (error, warn, info, debug)
|
||||
/// Logging-Level (error, warn, info, debug)
|
||||
#[arg(long, value_enum, default_value_t = LogLevelArg::Info, global = true)]
|
||||
pub log_level: LogLevelArg,
|
||||
|
||||
@@ -64,59 +64,59 @@ impl From<LogLevelArg> for logger_ctdra::LogLevel {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum Commands {
|
||||
/// Synchronize packages from GitHub Releases to Gitea Package Registry
|
||||
/// Synchronisiert Pakete aus GitHub-Releases in die Gitea-Paket-Registry
|
||||
Sync(SyncArgs),
|
||||
|
||||
/// Add a GitHub repository to the persistent configuration
|
||||
/// Fügt ein GitHub-Repository zur persistenten Konfiguration hinzu
|
||||
Add(AddArgs),
|
||||
|
||||
/// Remove a GitHub repository from the persistent configuration
|
||||
/// Entfernt ein GitHub-Repository aus der persistenten Konfiguration
|
||||
#[command(alias = "rm")]
|
||||
Remove(RemoveArgs),
|
||||
|
||||
/// List configured repositories and their sync status
|
||||
/// Listet konfigurierte Repositories und deren Sync-Status auf
|
||||
#[command(alias = "ls")]
|
||||
List,
|
||||
|
||||
/// View or update application configuration
|
||||
/// Zeigt die Anwendungskonfiguration an oder aktualisiert sie
|
||||
Config(ConfigArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct SyncArgs {
|
||||
/// Specific repository name or URL to sync (e.g. "raspberrypi/rpi-imager"). If omitted, all configured repositories are synced.
|
||||
/// Bestimmter Repository-Name oder URL zur Synchronisation (z. B. "raspberrypi/rpi-imager"). Wenn weggelassen, werden alle konfigurierten Repositories synchronisiert.
|
||||
pub repo: Option<String>,
|
||||
|
||||
/// Sync all configured repositories
|
||||
/// Alle konfigurierten Repositories synchronisieren
|
||||
#[arg(short = 'a', long)]
|
||||
pub all: bool,
|
||||
|
||||
/// Scan historical releases instead of only the latest release(s)
|
||||
/// Historische Releases scannen anstatt nur die neuesten Release(s)
|
||||
#[arg(long)]
|
||||
pub history: bool,
|
||||
|
||||
/// Include pre-releases during this sync run (defaults to repository configuration if not specified)
|
||||
/// Pre-Releases während dieses Synchronisationslaufs einbeziehen (Standard: Repository-Konfiguration)
|
||||
#[arg(long)]
|
||||
pub prereleases: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct AddArgs {
|
||||
/// GitHub repository to mirror (e.g. "raspberrypi/rpi-imager" or full GitHub URL)
|
||||
/// Zu spiegelndes GitHub-Repository (z. B. "raspberrypi/rpi-imager" oder vollständige GitHub-URL)
|
||||
pub repo: String,
|
||||
|
||||
/// Do not mirror pre-releases for this repository
|
||||
/// Keine Pre-Releases für dieses Repository spiegeln
|
||||
#[arg(long)]
|
||||
pub no_prereleases: bool,
|
||||
|
||||
/// Immediately synchronize this repository after adding
|
||||
/// Dieses Repository nach dem Hinzufügen sofort synchronisieren
|
||||
#[arg(long)]
|
||||
pub sync: bool,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct RemoveArgs {
|
||||
/// Repository to remove (e.g. "raspberrypi/rpi-imager")
|
||||
/// Zu entfernendes Repository (z. B. "raspberrypi/rpi-imager")
|
||||
pub repo: String,
|
||||
}
|
||||
|
||||
@@ -128,20 +128,20 @@ pub struct ConfigArgs {
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum ConfigAction {
|
||||
/// Show current configuration and configuration file location
|
||||
/// Aktuelle Konfiguration und Speicherort der Konfigurationsdatei anzeigen
|
||||
Show,
|
||||
|
||||
/// Set configuration parameters
|
||||
/// Konfigurationsparameter setzen
|
||||
Set {
|
||||
/// Gitea instance base URL
|
||||
/// Basis-URL der Gitea-Instanz
|
||||
#[arg(long)]
|
||||
gitea_url: Option<String>,
|
||||
|
||||
/// Gitea API Token
|
||||
/// Gitea-API-Token
|
||||
#[arg(long)]
|
||||
gitea_token: Option<String>,
|
||||
|
||||
/// Gitea Package Registry owner
|
||||
/// Gitea-Paket-Registry-Owner
|
||||
#[arg(long)]
|
||||
registry_owner: Option<String>,
|
||||
|
||||
|
||||
+63
-35
@@ -1,3 +1,4 @@
|
||||
use crate::utils::{clean_repo_input, sanitize_string};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -5,15 +6,15 @@ fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Configuration for an individual repository to mirror.
|
||||
/// Konfiguration für ein einzelnes zu spiegelndes Repository.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
pub struct RepoConfig {
|
||||
/// Full name of the repository in "owner/repo" format.
|
||||
/// Vollständiger Name des Repositories im Format "owner/repo".
|
||||
pub name: String,
|
||||
/// Whether to mirror pre-releases.
|
||||
/// Gibt an, ob Pre-Releases gespiegelt werden sollen.
|
||||
#[serde(default = "default_true")]
|
||||
pub include_prereleases: bool,
|
||||
/// Last successfully synced tag name.
|
||||
/// Zuletzt erfolgreich synchronisierter Tag-Name.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub last_synced_tag: Option<String>,
|
||||
}
|
||||
@@ -21,79 +22,75 @@ pub struct RepoConfig {
|
||||
impl RepoConfig {
|
||||
pub fn new(name: impl Into<String>, include_prereleases: bool) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
name: AppConfig::normalize_repo_name(&name.into()),
|
||||
include_prereleases,
|
||||
last_synced_tag: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistent application configuration.
|
||||
/// Persistente Anwendungskonfiguration.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
|
||||
pub struct AppConfig {
|
||||
/// Gitea instance base URL (e.g. https://gitea.example.com).
|
||||
/// Basis-URL der Gitea-Instanz (z. B. https://gitea.example.com).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gitea_url: Option<String>,
|
||||
/// Gitea API authentication token.
|
||||
/// Gitea-API-Authentifizierungstoken.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gitea_token: Option<String>,
|
||||
/// Gitea Package Registry owner/organization name.
|
||||
/// Name des Gitea-Paket-Registry-Owners bzw. der Organisation.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub registry_owner: Option<String>,
|
||||
/// Optional GitHub Personal Access Token for increased rate limits.
|
||||
/// Optionales GitHub Personal Access Token für höhere Rate-Limits.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub github_token: Option<String>,
|
||||
/// Configured repositories to mirror.
|
||||
/// Konfigurierte Repositories zur Spiegelung.
|
||||
#[serde(default)]
|
||||
pub repositories: Vec<RepoConfig>,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Normalizes repository input, stripping leading/trailing slashes and GitHub URL prefixes.
|
||||
/// Normalisiert die Repository-Eingabe, indem führende/nachgestellte Schrägstriche,
|
||||
/// Query-Parameter, Fragmente und GitHub-URL-Präfixe entfernt werden.
|
||||
pub fn normalize_repo_name(input: &str) -> String {
|
||||
let trimmed = input.trim();
|
||||
let cleaned = trimmed
|
||||
.trim_start_matches("https://github.com/")
|
||||
.trim_start_matches("http://github.com/")
|
||||
.trim_start_matches("github.com/")
|
||||
.trim_end_matches(".git")
|
||||
.trim_matches('/');
|
||||
cleaned.to_string()
|
||||
clean_repo_input(input).to_string()
|
||||
}
|
||||
|
||||
/// Adds or updates a repository in the configuration.
|
||||
pub fn add_or_update_repo(&mut self, repo: RepoConfig) {
|
||||
if let Some(existing) = self.repositories.iter_mut().find(|r| r.name.eq_ignore_ascii_case(&repo.name)) {
|
||||
/// 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)) {
|
||||
existing.name = repo.name;
|
||||
existing.include_prereleases = repo.include_prereleases;
|
||||
} else {
|
||||
self.repositories.push(repo);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a repository by name. Returns true if it was found and removed.
|
||||
/// Entfernt ein Repository anhand des Namens. Gibt true zurück, wenn es gefunden und entfernt wurde.
|
||||
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| !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
|
||||
}
|
||||
|
||||
/// Finds a repository by name.
|
||||
/// 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| r.name.eq_ignore_ascii_case(&normalized))
|
||||
self.repositories.iter().find(|r| Self::normalize_repo_name(&r.name).eq_ignore_ascii_case(&normalized))
|
||||
}
|
||||
|
||||
/// Updates the last synced tag for a given repository.
|
||||
/// 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| 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a custom path for the configuration file if provided.
|
||||
/// Setzt einen benutzerdefinierten Pfad für die Konfigurationsdatei, falls angegeben.
|
||||
pub fn init_config_path(custom_path: Option<&std::path::Path>) {
|
||||
config_ctdra::set_config_name("config");
|
||||
if let Some(path) = custom_path {
|
||||
@@ -101,22 +98,53 @@ pub fn init_config_path(custom_path: Option<&std::path::Path>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the resolved path of the configuration file.
|
||||
/// Gibt den aufgelösten Pfad der Konfigurationsdatei zurück.
|
||||
pub fn get_config_file_path() -> PathBuf {
|
||||
config_ctdra::get_config_path()
|
||||
}
|
||||
|
||||
/// Loads the application configuration. Falls back to defaults if not found.
|
||||
/// Lädt die Anwendungskonfiguration. Verwendet Standardwerte, falls keine Datei vorhanden ist.
|
||||
pub fn load_config() -> AppConfig {
|
||||
config_ctdra::load_config::<AppConfig>()
|
||||
let mut config = config_ctdra::load_config::<AppConfig>();
|
||||
|
||||
for repo in &mut config.repositories {
|
||||
repo.name = AppConfig::normalize_repo_name(&repo.name);
|
||||
}
|
||||
|
||||
if let Ok(val) = std::env::var("GITEA_URL") {
|
||||
let val = sanitize_string(&val);
|
||||
if !val.is_empty() {
|
||||
config.gitea_url = Some(val);
|
||||
}
|
||||
}
|
||||
if let Ok(val) = std::env::var("GITEA_TOKEN") {
|
||||
let val = sanitize_string(&val);
|
||||
if !val.is_empty() {
|
||||
config.gitea_token = Some(val);
|
||||
}
|
||||
}
|
||||
if let Ok(val) = std::env::var("REGISTRY_OWNER") {
|
||||
let val = sanitize_string(&val);
|
||||
if !val.is_empty() {
|
||||
config.registry_owner = Some(val);
|
||||
}
|
||||
}
|
||||
if let Ok(val) = std::env::var("GITHUB_TOKEN") {
|
||||
let val = sanitize_string(&val);
|
||||
if !val.is_empty() {
|
||||
config.github_token = Some(val);
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Saves the application configuration to disk.
|
||||
/// Speichert die Anwendungskonfiguration auf der Festplatte.
|
||||
pub fn save_config(config: &AppConfig) -> Result<(), config_ctdra::ConfyError> {
|
||||
config_ctdra::store(config)
|
||||
}
|
||||
|
||||
/// Modifies the application configuration in-place atomically and saves it.
|
||||
/// Modifiziert die Anwendungskonfiguration atomar vor Ort und speichert sie.
|
||||
pub fn modify_config<F>(f: F) -> Result<AppConfig, config_ctdra::ConfyError>
|
||||
where
|
||||
F: FnOnce(&mut AppConfig),
|
||||
|
||||
+6
-6
@@ -3,7 +3,7 @@ use anyhow::{bail, Context, Result};
|
||||
use std::path::Path;
|
||||
use tokio::fs::File;
|
||||
|
||||
/// Credentials and target registry information for Gitea.
|
||||
/// Zugangsdaten und Ziel-Registry-Informationen für Gitea.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GiteaConfig {
|
||||
pub base_url: String,
|
||||
@@ -25,7 +25,7 @@ impl GiteaConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a package upload attempt.
|
||||
/// Ergebnis eines Paket-Upload-Versuchs.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum UploadStatus {
|
||||
Uploaded,
|
||||
@@ -33,7 +33,7 @@ pub enum UploadStatus {
|
||||
SimulatedDryRun,
|
||||
}
|
||||
|
||||
/// Computes the target Gitea package registry upload URLs based on package type and release channel.
|
||||
/// Berechnet die Ziel-Upload-URLs der Gitea-Paket-Registry basierend auf Pakettyp und Release-Kanal.
|
||||
pub fn get_target_upload_urls(
|
||||
base_url: &str,
|
||||
owner: &str,
|
||||
@@ -69,7 +69,7 @@ pub fn get_target_upload_urls(
|
||||
}
|
||||
}
|
||||
|
||||
/// Client for uploading packages to a Gitea Package Registry.
|
||||
/// Client für den Upload von Paketen in eine Gitea-Paket-Registry.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GiteaClient {
|
||||
client: reqwest::Client,
|
||||
@@ -77,7 +77,7 @@ pub struct GiteaClient {
|
||||
}
|
||||
|
||||
impl GiteaClient {
|
||||
/// Creates a new Gitea client.
|
||||
/// Erstellt einen neuen Gitea-Client.
|
||||
pub fn new(config: GiteaConfig) -> Result<Self> {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
@@ -93,7 +93,7 @@ impl GiteaClient {
|
||||
Ok(Self { client, config })
|
||||
}
|
||||
|
||||
/// Uploads a package file from disk to the specified Gitea endpoint.
|
||||
/// Lädt eine Paketdatei von der Festplatte zum angegebenen Gitea-Endpunkt hoch.
|
||||
pub async fn upload_file(
|
||||
&self,
|
||||
file_path: &Path,
|
||||
|
||||
+25
-24
@@ -1,7 +1,7 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Supported Linux package distribution types.
|
||||
/// Unterstützte Linux-Paketverteilungstypen.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum PackageType {
|
||||
Debian,
|
||||
@@ -10,10 +10,10 @@ pub enum PackageType {
|
||||
}
|
||||
|
||||
impl PackageType {
|
||||
/// Classifies an asset by its filename into a supported PackageType.
|
||||
/// Klassifiziert ein Asset anhand seines Dateinamens in einen unterstützten PackageType.
|
||||
pub fn from_filename(filename: &str) -> Option<Self> {
|
||||
let name = filename.to_ascii_lowercase();
|
||||
// Check Arch packages first due to multi-part extensions
|
||||
// Arch-Pakete aufgrund mehrteiliger Dateiendungen zuerst prüfen
|
||||
if name.ends_with(".pkg.tar.zst")
|
||||
|| name.ends_with(".pkg.tar.xz")
|
||||
|| name.ends_with(".pkg.tar.gz")
|
||||
@@ -29,7 +29,7 @@ impl PackageType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns human-readable name of the package type.
|
||||
/// Gibt den lesbaren Namen des Pakettyps zurück.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
PackageType::Debian => "Debian (.deb)",
|
||||
@@ -39,7 +39,7 @@ impl PackageType {
|
||||
}
|
||||
}
|
||||
|
||||
/// A classified Linux package asset from a release.
|
||||
/// Ein klassifiziertes Linux-Paket-Asset aus einem Release.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ReleaseAsset {
|
||||
pub name: String,
|
||||
@@ -48,7 +48,7 @@ pub struct ReleaseAsset {
|
||||
pub package_type: PackageType,
|
||||
}
|
||||
|
||||
/// A GitHub release with extracted Linux package assets.
|
||||
/// Ein GitHub-Release mit extrahierten Linux-Paket-Assets.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GitHubRelease {
|
||||
pub id: u64,
|
||||
@@ -77,7 +77,7 @@ struct GhApiAsset {
|
||||
size: u64,
|
||||
}
|
||||
|
||||
/// Client for interacting with the GitHub API.
|
||||
/// Client für die Interaktion mit der GitHub-API.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitHubClient {
|
||||
client: reqwest::Client,
|
||||
@@ -85,8 +85,9 @@ pub struct GitHubClient {
|
||||
}
|
||||
|
||||
impl GitHubClient {
|
||||
/// Creates a new GitHub API client with optional authentication token.
|
||||
/// Erstellt einen neuen GitHub-API-Client mit optionalem Authentifizierungstoken.
|
||||
pub fn new(token: Option<String>) -> Result<Self> {
|
||||
let token = token.filter(|t| !t.trim().is_empty());
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
@@ -105,10 +106,16 @@ impl GitHubClient {
|
||||
Ok(Self { client, token })
|
||||
}
|
||||
|
||||
/// Fetches releases for a given repository (format "owner/repo").
|
||||
/// Gibt das optionale Authentifizierungstoken zurück (falls gesetzt).
|
||||
pub fn token(&self) -> Option<&str> {
|
||||
self.token.as_deref()
|
||||
}
|
||||
|
||||
|
||||
/// Ruft Releases für ein angegebenes Repository ab (Format "owner/repo").
|
||||
///
|
||||
/// If `history` is false, retrieves only the latest release(s).
|
||||
/// If `include_prereleases` is false, skips pre-releases.
|
||||
/// Wenn `history` false ist, werden nur die neuesten Releases abgerufen.
|
||||
/// Wenn `include_prereleases` false ist, werden Pre-Releases übersprungen.
|
||||
pub async fn fetch_releases(
|
||||
&self,
|
||||
repo: &str,
|
||||
@@ -186,9 +193,9 @@ impl GitHubClient {
|
||||
});
|
||||
}
|
||||
|
||||
// If not scanning full history, stop after the first page (or once we found stable/prerelease)
|
||||
// Wenn kein vollständiger Verlauf gescannt werden soll, nach der ersten Seite stoppen
|
||||
if !history {
|
||||
// Keep only the most recent stable release and most recent pre-release (if allowed)
|
||||
// Nur das neueste stabile Release und neueste Pre-Release behalten (falls zulässig)
|
||||
let mut selected = Vec::new();
|
||||
let latest_stable = releases.iter().find(|r| !r.prerelease);
|
||||
if let Some(stable) = latest_stable {
|
||||
@@ -198,7 +205,7 @@ impl GitHubClient {
|
||||
if include_prereleases {
|
||||
let latest_prerelease = releases.iter().find(|r| r.prerelease);
|
||||
if let Some(prerelease) = latest_prerelease {
|
||||
// Avoid duplicates if stable and pre-release are identical (rare)
|
||||
// Duplikate vermeiden, falls Stable und Pre-Release identisch sind
|
||||
if !selected.iter().any(|r| r.id == prerelease.id) {
|
||||
selected.push(prerelease.clone());
|
||||
}
|
||||
@@ -213,7 +220,7 @@ impl GitHubClient {
|
||||
Ok(releases)
|
||||
}
|
||||
|
||||
/// Downloads an asset from the given download URL as a streaming response.
|
||||
/// Lädt ein Asset von der angegebenen Download-URL als Streaming-Response herunter.
|
||||
pub async fn download_asset_stream(&self, download_url: &str) -> Result<reqwest::Response> {
|
||||
let mut req = self.client.get(download_url);
|
||||
if let Some(token) = &self.token {
|
||||
@@ -237,15 +244,9 @@ impl GitHubClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to parse "owner/repo" from repository string.
|
||||
/// Hilfsfunktion zum Parsen von "owner/repo" aus einer Repository-Zeichenkette.
|
||||
pub fn parse_repo_owner_name(repo: &str) -> Result<(&str, &str)> {
|
||||
let cleaned = repo
|
||||
.trim()
|
||||
.trim_start_matches("https://github.com/")
|
||||
.trim_start_matches("http://github.com/")
|
||||
.trim_start_matches("github.com/")
|
||||
.trim_end_matches(".git")
|
||||
.trim_matches('/');
|
||||
let cleaned = crate::utils::clean_repo_input(repo);
|
||||
|
||||
let parts: Vec<&str> = cleaned.split('/').collect();
|
||||
if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
//! `mirror-package` ist eine Anwendung zum Spiegeln vorkompilierter Linux-Pakete
|
||||
//! (.deb, .rpm, .pkg.tar.zst) aus GitHub-Releases in Gitea- / Forgejo-Paket-Registries.
|
||||
|
||||
pub mod cli;
|
||||
pub mod config;
|
||||
pub mod gitea;
|
||||
pub mod github;
|
||||
pub mod pipeline;
|
||||
pub mod utils;
|
||||
|
||||
+4
-4
@@ -11,16 +11,16 @@ use mirror_package::pipeline::{sync_single_repository, SyncOptions, SyncReport};
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize global logger level
|
||||
// Globalen Logging-Level initialisieren
|
||||
set_log_level(cli.log_level.into());
|
||||
|
||||
// Initialize custom config path if specified
|
||||
// Benutzerdefinierten Konfigurationspfad initialisieren, falls angegeben
|
||||
init_config_path(cli.config.as_deref());
|
||||
|
||||
// Load persistent configuration
|
||||
// Persistente Konfiguration laden
|
||||
let mut app_config = load_config();
|
||||
|
||||
// CLI flags override configuration values
|
||||
// CLI-Flags überschreiben Konfigurationswerte
|
||||
if let Some(url) = cli.gitea_url {
|
||||
app_config.gitea_url = Some(url);
|
||||
}
|
||||
|
||||
+10
-10
@@ -8,7 +8,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::fs::{remove_file, File};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
/// Summary statistics of a synchronization operation.
|
||||
/// Zusammenfassende Statistiken eines Synchronisationsvorgangs.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SyncReport {
|
||||
pub repositories_processed: usize,
|
||||
@@ -18,7 +18,7 @@ pub struct SyncReport {
|
||||
pub packages_simulated: usize,
|
||||
}
|
||||
|
||||
/// Options controlling a repository sync run.
|
||||
/// Optionen zur Steuerung eines Repository-Synchronisationslaufs.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SyncOptions {
|
||||
pub history: bool,
|
||||
@@ -26,7 +26,7 @@ pub struct SyncOptions {
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
/// Executes the synchronization pipeline for a given repository.
|
||||
/// Führt die Synchronisations-Pipeline für ein bestimmtes Repository aus.
|
||||
pub async fn sync_single_repository(
|
||||
repo_config: &RepoConfig,
|
||||
app_config: &AppConfig,
|
||||
@@ -47,7 +47,7 @@ pub async fn sync_single_repository(
|
||||
LogLevel::Info,
|
||||
);
|
||||
|
||||
// Resolve Gitea credentials
|
||||
// Gitea-Zugangsdaten auflösen
|
||||
let gitea_url = app_config.gitea_url.as_deref().unwrap_or_default();
|
||||
let gitea_token = app_config.gitea_token.as_deref().unwrap_or_default();
|
||||
let registry_owner = app_config.registry_owner.as_deref().unwrap_or_default();
|
||||
@@ -155,12 +155,12 @@ pub async fn sync_single_repository(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download asset to temporary file
|
||||
// Asset in temporäre Datei herunterladen
|
||||
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))?;
|
||||
|
||||
// Upload asset to all target distribution URLs
|
||||
// Asset auf alle Ziel-Distributions-URLs hochladen
|
||||
for url in &target_urls {
|
||||
log(
|
||||
"upload",
|
||||
@@ -197,14 +197,14 @@ pub async fn sync_single_repository(
|
||||
&format!("Failed to upload '{}' to {}: {:#}", asset.name, url, e),
|
||||
LogLevel::Error,
|
||||
);
|
||||
// Cleanup temp file before returning error
|
||||
// Temporäre Datei vor Fehlerrückgabe bereinigen
|
||||
let _ = remove_file(&temp_file_path).await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup temp file
|
||||
// Temporäre Datei bereinigen
|
||||
if let Err(e) = remove_file(&temp_file_path).await {
|
||||
log(
|
||||
"sync",
|
||||
@@ -219,7 +219,7 @@ pub async fn sync_single_repository(
|
||||
}
|
||||
}
|
||||
|
||||
// Persist last synced tag if not dry-run
|
||||
// Zuletzt synchronisierten Tag persistieren, wenn kein Dry-Run
|
||||
if !options.dry_run {
|
||||
if let Some(tag) = latest_synced_tag {
|
||||
let name_copy = repo_name.to_string();
|
||||
@@ -233,7 +233,7 @@ pub async fn sync_single_repository(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Downloads a GitHub release asset into a unique temporary file.
|
||||
/// Lädt ein GitHub-Release-Asset in eine eindeutige temporäre Datei herunter.
|
||||
async fn download_to_temp_file(
|
||||
github_client: &GitHubClient,
|
||||
download_url: &str,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
|
||||
/// Sanitizes a string by removing surrounding quotes (single or double).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - The string to sanitize.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The sanitized string with surrounding quotes removed.
|
||||
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('\'')) {
|
||||
stripped.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitizes environment variables by removing surrounding quotes.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `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() {
|
||||
*value = sanitize_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bereinigt eine Repository-Eingabe (URL oder Kurzform), indem Query-Parameter, Fragmente,
|
||||
/// URL-Schemata/Hosts (`github.com`, `www.github.com`, `git@github.com:`), `.git`-Endungen
|
||||
/// 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.len() >= 2 {
|
||||
trimmed = trimmed[1..trimmed.len() - 1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
let without_query_or_fragment = match trimmed.find(|c| c == '?' || c == '#') {
|
||||
Some(idx) => &trimmed[..idx],
|
||||
None => trimmed,
|
||||
};
|
||||
|
||||
let mut cleaned = without_query_or_fragment
|
||||
.trim_start_matches("git@github.com:")
|
||||
.trim_start_matches("https://")
|
||||
.trim_start_matches("http://")
|
||||
.trim_start_matches("www.github.com/")
|
||||
.trim_start_matches("github.com/")
|
||||
.trim_matches('/');
|
||||
|
||||
if let Some(stripped) = cleaned.strip_suffix(".git") {
|
||||
cleaned = stripped.trim_matches('/');
|
||||
}
|
||||
|
||||
cleaned
|
||||
}
|
||||
+146
-36
@@ -1,53 +1,163 @@
|
||||
use mirror_package::config::{AppConfig, RepoConfig};
|
||||
use mirror_package::config::{AppConfig, RepoConfig, load_config};
|
||||
use mirror_package::utils::{clean_repo_input, sanitize_env_vars, sanitize_string};
|
||||
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_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());
|
||||
|
||||
sanitize_env_vars(&mut env_vars);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_config_with_env_vars() {
|
||||
// This test relies on environment variables being set externally.
|
||||
// To avoid `unsafe` blocks, we skip setting them programmatically.
|
||||
// In a real test environment, set these variables before running the test:
|
||||
// GITEA_URL="https://gitea.example.com"
|
||||
// GITEA_TOKEN="token123"
|
||||
// REGISTRY_OWNER="owner"
|
||||
// GITHUB_TOKEN="github_token123"
|
||||
let config = load_config();
|
||||
|
||||
// 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_token, Some("token123".to_string()));
|
||||
assert_eq!(config.registry_owner, Some("owner".to_string()));
|
||||
assert_eq!(config.github_token, Some("github_token123".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[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("\"token123\""), "token123");
|
||||
assert_eq!(sanitize_string("'token123'"), "token123");
|
||||
assert_eq!(sanitize_string("\"owner\""), "owner");
|
||||
assert_eq!(sanitize_string("'owner'"), "owner");
|
||||
assert_eq!(sanitize_string("\"github_token123\""), "github_token123");
|
||||
assert_eq!(sanitize_string("'github_token123'"), "github_token123");
|
||||
assert_eq!(sanitize_string("no_quotes"), "no_quotes");
|
||||
assert_eq!(sanitize_string("\"single_quote\""), "single_quote");
|
||||
assert_eq!(sanitize_string("'single_quote'"), "single_quote");
|
||||
assert_eq!(sanitize_string("\"escaped_quote\""), "escaped_quote");
|
||||
// Edge cases: single character inputs
|
||||
assert_eq!(sanitize_string("\""), "\"");
|
||||
assert_eq!(sanitize_string("'"), "'");
|
||||
assert_eq!(sanitize_string("a"), "a");
|
||||
// Edge cases: empty quotes and empty strings
|
||||
assert_eq!(sanitize_string("\"\""), "");
|
||||
assert_eq!(sanitize_string("''"), "");
|
||||
assert_eq!(sanitize_string(""), "");
|
||||
assert_eq!(sanitize_string(" "), "");
|
||||
assert_eq!(sanitize_string(" 'hello' "), "hello");
|
||||
assert_eq!(sanitize_string(" \"world\" "), "world");
|
||||
}
|
||||
|
||||
#[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("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"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/raspberrypi/rpi-imager/?tab=readme-ov-file"),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/raspberrypi/rpi-imager.git?tab=readme-ov-file"),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/raspberrypi/rpi-imager#readme"),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file#install"),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("raspberrypi/rpi-imager?tab=readme-ov-file"),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
clean_repo_input("\"https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file\""),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_repo_name() {
|
||||
let input = "https://github.com/owner/repo.git";
|
||||
let normalized = AppConfig::normalize_repo_name(input);
|
||||
assert_eq!(normalized, "owner/repo");
|
||||
|
||||
let input_with_query = "https://github.com/raspberrypi/rpi-imager?tab=readme-ov-file";
|
||||
assert_eq!(
|
||||
AppConfig::normalize_repo_name("raspberrypi/rpi-imager"),
|
||||
AppConfig::normalize_repo_name(input_with_query),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
AppConfig::normalize_repo_name("https://github.com/raspberrypi/rpi-imager"),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
AppConfig::normalize_repo_name("https://github.com/raspberrypi/rpi-imager.git"),
|
||||
"raspberrypi/rpi-imager"
|
||||
);
|
||||
assert_eq!(
|
||||
AppConfig::normalize_repo_name("Heroic-Games-Launcher/HeroicGamesLauncher/"),
|
||||
"Heroic-Games-Launcher/HeroicGamesLauncher"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_management() {
|
||||
fn test_add_or_update_repo() {
|
||||
let mut config = AppConfig::default();
|
||||
config.add_or_update_repo(RepoConfig::new("owner/repo1", true));
|
||||
config.add_or_update_repo(RepoConfig::new("owner/repo2", false));
|
||||
|
||||
assert_eq!(config.repositories.len(), 2);
|
||||
assert!(config.find_repo("owner/repo1").is_some());
|
||||
assert!(config.find_repo("https://github.com/owner/repo1").is_some());
|
||||
|
||||
assert!(config.remove_repo("owner/repo1"));
|
||||
let repo = RepoConfig::new("https://github.com/owner/repo?tab=readme-ov-file", true);
|
||||
config.add_or_update_repo(repo.clone());
|
||||
assert_eq!(config.repositories.len(), 1);
|
||||
assert!(config.find_repo("owner/repo1").is_none());
|
||||
assert_eq!(config.repositories[0].name, "owner/repo");
|
||||
assert_eq!(config.repositories[0].include_prereleases, true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialization_roundtrip() {
|
||||
let mut config = AppConfig {
|
||||
gitea_url: Some("https://gitea.example.com".to_string()),
|
||||
gitea_token: Some("secret_token".to_string()),
|
||||
registry_owner: Some("my-org".to_string()),
|
||||
github_token: Some("gh_pat".to_string()),
|
||||
repositories: Vec::new(),
|
||||
};
|
||||
config.add_or_update_repo(RepoConfig::new("raspberrypi/rpi-imager", true));
|
||||
fn test_remove_repo() {
|
||||
let mut config = AppConfig::default();
|
||||
let repo = RepoConfig::new("owner/repo", true);
|
||||
config.add_or_update_repo(repo.clone());
|
||||
assert!(config.remove_repo("https://github.com/owner/repo?tab=readme-ov-file"));
|
||||
assert!(config.repositories.is_empty());
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string(&config).unwrap();
|
||||
let deserialized: AppConfig = serde_json::from_str(&json_str).unwrap();
|
||||
#[test]
|
||||
fn test_find_repo() {
|
||||
let mut config = AppConfig::default();
|
||||
let repo = RepoConfig::new("owner/repo", true);
|
||||
config.add_or_update_repo(repo.clone());
|
||||
assert_eq!(
|
||||
config.find_repo("https://github.com/owner/repo?tab=readme-ov-file"),
|
||||
Some(&RepoConfig::new("owner/repo", true))
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(config, deserialized);
|
||||
#[test]
|
||||
fn test_update_last_synced_tag() {
|
||||
let mut config = AppConfig::default();
|
||||
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()));
|
||||
}
|
||||
@@ -6,7 +6,7 @@ fn test_debian_routing() {
|
||||
let base_url = "https://gitea.example.com";
|
||||
let owner = "test-owner";
|
||||
|
||||
// Stable Debian goes to both stable and testing pools
|
||||
// Stabile Debian-Pakete werden in stable und testing Pools hochgeladen
|
||||
let stable_urls = get_target_upload_urls(base_url, owner, PackageType::Debian, false);
|
||||
assert_eq!(
|
||||
stable_urls,
|
||||
@@ -16,7 +16,7 @@ fn test_debian_routing() {
|
||||
]
|
||||
);
|
||||
|
||||
// Pre-release Debian goes only to testing pool
|
||||
// Pre-Release Debian-Pakete gehen nur in den testing Pool
|
||||
let prerelease_urls = get_target_upload_urls(base_url, owner, PackageType::Debian, true);
|
||||
assert_eq!(
|
||||
prerelease_urls,
|
||||
@@ -29,14 +29,14 @@ fn test_rpm_routing() {
|
||||
let base_url = "https://gitea.example.com/";
|
||||
let owner = "test-owner";
|
||||
|
||||
// Stable RPM
|
||||
// Stabiles RPM
|
||||
let stable_urls = get_target_upload_urls(base_url, owner, PackageType::Rpm, false);
|
||||
assert_eq!(
|
||||
stable_urls,
|
||||
vec!["https://gitea.example.com/api/packages/test-owner/rpm/stable/upload"]
|
||||
);
|
||||
|
||||
// Pre-release RPM
|
||||
// Pre-Release RPM
|
||||
let prerelease_urls = get_target_upload_urls(base_url, owner, PackageType::Rpm, true);
|
||||
assert_eq!(
|
||||
prerelease_urls,
|
||||
@@ -49,14 +49,14 @@ fn test_arch_routing() {
|
||||
let base_url = "https://gitea.example.com";
|
||||
let owner = "test-owner";
|
||||
|
||||
// Stable Arch
|
||||
// Stabiles Arch
|
||||
let stable_urls = get_target_upload_urls(base_url, owner, PackageType::Arch, false);
|
||||
assert_eq!(
|
||||
stable_urls,
|
||||
vec!["https://gitea.example.com/api/packages/test-owner/arch/stable"]
|
||||
);
|
||||
|
||||
// Pre-release Arch
|
||||
// Pre-Release Arch
|
||||
let prerelease_urls = get_target_upload_urls(base_url, owner, PackageType::Arch, true);
|
||||
assert_eq!(
|
||||
prerelease_urls,
|
||||
|
||||
+34
-2
@@ -1,4 +1,4 @@
|
||||
use mirror_package::github::{parse_repo_owner_name, PackageType};
|
||||
use mirror_package::github::{PackageType, parse_repo_owner_name};
|
||||
|
||||
#[test]
|
||||
fn test_package_classification() {
|
||||
@@ -40,7 +40,7 @@ fn test_package_classification() {
|
||||
Some(PackageType::Arch)
|
||||
);
|
||||
|
||||
// Non-package formats should be ignored
|
||||
// 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.dmg"), None);
|
||||
assert_eq!(PackageType::from_filename("rpi-imager-1.8.5.exe"), None);
|
||||
@@ -64,7 +64,39 @@ fn test_parse_repo_owner_name() {
|
||||
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(),
|
||||
("raspberrypi", "rpi-imager")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repo_owner_name("https://github.com/raspberrypi/rpi-imager/?tab=readme-ov-file#install").unwrap(),
|
||||
("raspberrypi", "rpi-imager")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_repo_owner_name("raspberrypi/rpi-imager?tab=readme-ov-file").unwrap(),
|
||||
("raspberrypi", "rpi-imager")
|
||||
);
|
||||
|
||||
assert!(parse_repo_owner_name("invalid_repo").is_err());
|
||||
assert!(parse_repo_owner_name("invalid/repo/extra").is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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");
|
||||
assert_eq!(client.token(), None);
|
||||
|
||||
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");
|
||||
assert_eq!(client_with_token.token(), Some("valid_token"));
|
||||
|
||||
// Test with no token
|
||||
let client_no_token = GitHubClient::new(None).expect("Failed to create client with no token");
|
||||
assert_eq!(client_no_token.token(), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user