Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/_qodana-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: qodana-contract-core

on:
workflow_call:
inputs:
pre_command:
description: "Optional shell command executed before Qodana."
required: false
type: string
default: ""
artifact_name:
description: "Artifact name for uploaded Qodana evidence."
required: false
type: string
default: "ci-qodana"
upload_sarif:
description: "Upload SARIF to code scanning on non-PR events."
required: false
type: boolean
default: true

permissions:
contents: read

jobs:
qodana:
if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]')
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
PRE_COMMAND: ${{ inputs.pre_command }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "10.0.102"

- name: Run pre command
if: env.PRE_COMMAND != ''
shell: bash
run: bash -euo pipefail -c "${PRE_COMMAND}"

- name: Assert QODANA_TOKEN present
shell: bash
run: |
test -n "${QODANA_TOKEN:-}" || (echo "FAIL: QODANA_TOKEN missing" >&2; exit 1)
echo "OK: QODANA_TOKEN present"

- name: Run Qodana
uses: JetBrains/qodana-action@42dad391966aca8ca344ca2340a7f43a5507e9b2 # v2025.3.1
with:
args: --linter=jetbrains/qodana-dotnet:2025.3
results-dir: artifacts/ci/qodana
upload-result: false
use-caches: false

- name: Run Entry Check
if: always()
shell: bash
env:
CI_DEFER_ARTIFACT_LINK_RESOLUTION: "1"
run: bash tools/ci/bin/run.sh qodana

- name: Upload SARIF To Code Scanning
if: inputs.upload_sarif && github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
with:
sarif_file: artifacts/ci/qodana/qodana.upload.sarif.json

- name: Upload Artifact
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ inputs.artifact_name }}
path: artifacts/ci/qodana/
if-no-files-found: error
46 changes: 46 additions & 0 deletions .github/workflows/dependency-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: dependency-canary

on:
schedule:
- cron: "0 5 * * 1"
workflow_dispatch:
inputs:
dependency:
description: "Dependency to probe (all or exact package id)"
required: false
default: "all"
version:
description: "Target version or 'latest'"
required: false
default: "latest"

permissions:
contents: read

jobs:
canary-sharpcompress:
if: github.event_name != 'workflow_dispatch' || inputs.dependency == 'all' || inputs.dependency == 'SharpCompress'
Comment thread
tomtastisch marked this conversation as resolved.
Outdated
uses: ./.github/workflows/_qodana-contract.yml
with:
pre_command: bash -euo pipefail tools/ci/bin/dependency-canary.sh "SharpCompress" "${{ inputs.version || 'latest' }}"
Comment thread
tomtastisch marked this conversation as resolved.
Outdated
artifact_name: canary-SharpCompress
upload_sarif: false
secrets: inherit

canary-recyclable:
if: github.event_name != 'workflow_dispatch' || inputs.dependency == 'all' || inputs.dependency == 'Microsoft.IO.RecyclableMemoryStream'
uses: ./.github/workflows/_qodana-contract.yml
with:
pre_command: bash -euo pipefail tools/ci/bin/dependency-canary.sh "Microsoft.IO.RecyclableMemoryStream" "${{ inputs.version || 'latest' }}"
Comment thread
tomtastisch marked this conversation as resolved.
Outdated
artifact_name: canary-Microsoft.IO.RecyclableMemoryStream
upload_sarif: false
secrets: inherit

canary-system-text-json:
if: github.event_name != 'workflow_dispatch' || inputs.dependency == 'all' || inputs.dependency == 'System.Text.Json'
uses: ./.github/workflows/_qodana-contract.yml
with:
pre_command: bash -euo pipefail tools/ci/bin/dependency-canary.sh "System.Text.Json" "${{ inputs.version || 'latest' }}"
Comment thread
tomtastisch marked this conversation as resolved.
Outdated
artifact_name: canary-System.Text.Json
upload_sarif: false
secrets: inherit
62 changes: 5 additions & 57 deletions .github/workflows/qodana.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,8 @@ permissions:

jobs:
qodana:
# In untrusted PR contexts (forks, dependabot), repository secrets are unavailable.
if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]')
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0

- name: Assert QODANA_TOKEN present
# Fail-closed in trusted CI contexts where Qodana is expected to run.
shell: bash
run: |
test -n "${QODANA_TOKEN:-}" || (echo "FAIL: QODANA_TOKEN missing" >&2; exit 1)
echo "OK: QODANA_TOKEN present"

- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: "10.0.102"

- name: Run Qodana
uses: JetBrains/qodana-action@42dad391966aca8ca344ca2340a7f43a5507e9b2 # v2025.3.1
with:
# gh-actions input is a single string; use --flag=value form to avoid passing a single argv containing a space.
args: --linter=jetbrains/qodana-dotnet:2025.3
results-dir: artifacts/ci/qodana
upload-result: false
use-caches: false

- name: Run Entry Check
if: always()
shell: bash
env:
# The job uploads the qodana artifact after this step; defer artifact-link rendering to avoid false-negative
# "artifact_not_found" failures when earlier steps fail.
CI_DEFER_ARTIFACT_LINK_RESOLUTION: "1"
run: bash tools/ci/bin/run.sh qodana

- name: Upload SARIF To Code Scanning
if: github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
with:
sarif_file: artifacts/ci/qodana/qodana.upload.sarif.json

- name: Upload Artifact
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ci-qodana
path: artifacts/ci/qodana/
if-no-files-found: error
uses: ./.github/workflows/_qodana-contract.yml
with:
artifact_name: ci-qodana
upload_sarif: true
secrets: inherit
6 changes: 6 additions & 0 deletions docs/audit/009_SUPPLY_CHAIN_BASELINE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Minimum an reproduzierbaren Kontrollen fuer Source-to-Package-Integritaet in die
- E4 Release/Provenance-Nachweise:
- `.github/workflows/release.yml`
- `artifacts/nuget/attestation-verify.txt` (wenn der Release-Workflow laeuft)
- E5 Dependency-Canary-Nachweise:
- `.github/workflows/dependency-canary.yml`
- Workflow-Artefakte `canary-*` mit Qodana-Contract-Output (`artifacts/ci/qodana/`)
- Laufhistorie: https://github.com/tomtastisch/FileClassifier/actions/workflows/dependency-canary.yml

## 4. Verifikationskommandos
Alle Kommandos sind fuer Ausfuehrung im Repository-Root gedacht.
Expand All @@ -45,6 +49,8 @@ NUPKG="$(find artifacts/nuget -maxdepth 1 -type f -name '*.nupkg' | head -n 1)"
test -n "$NUPKG"
dotnet nuget verify "$NUPKG"
gh attestation verify "$NUPKG" --repo tomtastisch/FileClassifier
# Optionaler Canary-Nachweis:
gh run list --workflow dependency-canary.yml --limit 5
```

## 5. Operative Kadenz
Expand Down
6 changes: 6 additions & 0 deletions docs/audit/109_SUPPLY_CHAIN_BASELINE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ Define minimum reproducible controls for source-to-package integrity in this rep
- E4 Release/provenance evidence:
- `.github/workflows/release.yml`
- `artifacts/nuget/attestation-verify.txt` (when release workflow runs)
- E5 Dependency canary evidence:
- `.github/workflows/dependency-canary.yml`
- workflow artifacts `canary-*` with Qodana contract output (`artifacts/ci/qodana/`)
- run history: https://github.com/tomtastisch/FileClassifier/actions/workflows/dependency-canary.yml

## 4. Verification Commands
All commands are intended to run from the repository root.
Expand All @@ -45,6 +49,8 @@ NUPKG="$(find artifacts/nuget -maxdepth 1 -type f -name '*.nupkg' | head -n 1)"
test -n "$NUPKG"
dotnet nuget verify "$NUPKG"
gh attestation verify "$NUPKG" --repo tomtastisch/FileClassifier
# Optional canary evidence:
gh run list --workflow dependency-canary.yml --limit 5
```

## 5. Operational Cadence
Expand Down
11 changes: 11 additions & 0 deletions docs/versioning/001_POLICY_VERSIONING.MD
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ Primary-Prioritaet ist fix:
## 11. Qodana
Qodana ist zusaetzliche Static-Analysis und ersetzt keine CI-Quality-Gates fuer Build/Test/Coverage/Versioning.

## 12. Dependency-Upgrade-Policy (Canary)
- Patch-/Minor-Upgrades fuer risikoreiche Dependencies werden ueber den Canary-Workflow vorab verifiziert:
- `.github/workflows/dependency-canary.yml`
- `tools/ci/bin/dependency-canary.sh`
- `tools/ci/policies/data/dependency_canary.json`
- Verbindliche Canary-Nachweise:
- fokussierte dependency-spezifische Fail-closed-/Guard-Tests erfolgreich
- Qodana-Lauf plus `run.sh qodana` Contract-Validierung erfolgreich
- Major-Upgrades (`X+1.0.0`) sind fuer alle Canary-Dependencies nicht automatisch:
- nur via dediziertem PR mit API-Impact-Review und expliziter SemVer-Entscheidung

## RoC-Bezug
- [Artifact-Contract-Regel](https://github.com/tomtastisch/FileClassifier/blob/main/tools/ci/policies/rules/artifact_contract.yaml)
- [Docs-Drift-Regel](https://github.com/tomtastisch/FileClassifier/blob/main/tools/ci/policies/rules/docs_drift.yaml)
Expand Down
11 changes: 11 additions & 0 deletions docs/versioning/101_POLICY_VERSIONING.MD
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ Primary priority is fixed:
## 11. Qodana
Qodana is additional static analysis and does not replace CI quality gates for build/test/coverage/versioning.

## 12. Dependency Upgrade Policy (Canary)
- Patch/minor upgrades for risky dependencies are pre-verified via the canary workflow:
- `.github/workflows/dependency-canary.yml`
- `tools/ci/bin/dependency-canary.sh`
- `tools/ci/policies/data/dependency_canary.json`
- Mandatory canary evidence:
- focused dependency-specific fail-closed/guard tests pass
- Qodana run plus `run.sh qodana` contract validation pass
- Major upgrades (`X+1.0.0`) are not automatic for all canary dependencies:
- only via dedicated PR with API impact review and explicit SemVer decision

## RoC References
- [Artifact-Contract-Regel](https://github.com/tomtastisch/FileClassifier/blob/main/tools/ci/policies/rules/artifact_contract.yaml)
- [Docs-Drift-Regel](https://github.com/tomtastisch/FileClassifier/blob/main/tools/ci/policies/rules/docs_drift.yaml)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using FileTypeDetectionLib.Tests.Support;
using Tomtastisch.FileClassifier;

namespace FileTypeDetectionLib.Tests.Unit;

public sealed class ArchiveSharpCompressCompatUnitTests
{
[Fact]
public void OpenArchive_ReturnsNull_ForNonArchivePayload()
{
using var stream = new MemoryStream(new byte[] { 0x01, 0x02, 0x03 }, false);
var archive = ArchiveSharpCompressCompat.OpenArchive(stream);
Assert.Null(archive);
}

[Fact]
public void OpenArchive_ReturnsArchive_ForTarPayload()
{
var tar = ArchivePayloadFactory.CreateTarWithSingleEntry("note.txt", "ok");
using var stream = new MemoryStream(tar, false);
using var archive = ArchiveSharpCompressCompat.OpenArchive(stream);
Assert.NotNull(archive);
}

[Fact]
public void OpenArchiveForContainer_ReturnsArchive_ForGZipPayload()
{
var gzip = ArchivePayloadFactory.CreateGZipWithSingleEntry("payload.bin", new byte[] { 0x11, 0x22, 0x33 });
using var stream = new MemoryStream(gzip, false);
using var archive = ArchiveSharpCompressCompat.OpenArchiveForContainer(stream, ArchiveContainerType.GZip);
Assert.NotNull(archive);
}

[Fact]
public void HasGZipMagic_ReturnsTrue_ForGZipHeader()
{
using var stream = new MemoryStream(new byte[] { 0x1F, 0x8B, 0x08 }, false);
Assert.True(ArchiveSharpCompressCompat.HasGZipMagic(stream));
}

[Fact]
public void HasGZipMagic_ReturnsFalse_ForNonSeekableStream()
{
using var nonSeekable = new NonSeekableStream(new byte[] { 0x1F, 0x8B, 0x08 });
Assert.False(ArchiveSharpCompressCompat.HasGZipMagic(nonSeekable));
}

private sealed class NonSeekableStream : MemoryStream
{
internal NonSeekableStream(byte[] buffer) : base(buffer, false)
{
}

public override bool CanSeek => false;
}
}
1 change: 1 addition & 0 deletions tests/FileTypeDetectionLib.Tests/Unit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Direkter Nachweis von API-Korrektheit, Security-Regeln und Determinismus.
| `ArchiveTypeResolverAdditionalUnitTests.cs` | Stream/Bytes Branches in ArchiveTypeResolver |
| `ArchiveTypeResolverExceptionUnitTests.cs` | Exception-Pfade in ArchiveTypeResolver |
| `SharpCompressArchiveBackendUnitTests.cs` | Branches fuer SharpCompress-Backend |
| `ArchiveSharpCompressCompatUnitTests.cs` | Contract-Guards fuer SharpCompress-Kompat-Schicht |
| `SharpCompressEntryModelUnitTests.cs` | Null-Entry Defaults im SharpCompressEntryModel |
| `SharpCompressEntryModelNonNullUnitTests.cs` | Real-Entry Pfade im SharpCompressEntryModel |
| `FileTypeDetectorAdditionalUnitTests.cs` | LoadOptions/ReadFileSafe/Detect Branches |
Expand Down
Loading
Loading