Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/issue-labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ jobs:
addIf(text.includes('chore') || text.includes('cleanup'), 'chore');

// SemVer labels by explicit hints
addIf(text.includes('version:major') || text.includes('major'), 'version:major');
addIf(text.includes('version:minor') || text.includes('minor'), 'version:minor');
addIf(text.includes('version:patch') || text.includes('patch'), 'version:patch');
addIf(text.includes('version:none') || text.includes('no version'), 'version:none');
addIf(text.includes('versioning:major') || text.includes('major'), 'versioning:major');
addIf(text.includes('versioning:minor') || text.includes('minor'), 'versioning:minor');
addIf(text.includes('versioning:patch') || text.includes('patch'), 'versioning:patch');
addIf(text.includes('versioning:none') || text.includes('no version'), 'versioning:none');

if (labels.size === 0) {
labels.add('version:none');
labels.add('versioning:none');
}

await github.rest.issues.addLabels({
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/release-retention.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Release Retention

on:
workflow_run:
workflows: ["Release Publish"]
types: [completed]
schedule:
- cron: '0 4 * * 0'
workflow_dispatch:

permissions:
contents: write
packages: write

jobs:
retention:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
fetch-tags: true
- name: Setup .NET
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: |
8.0.x
10.0.102
- name: Apply retention (GH Releases + NuGet unlist + GH Packages delete)
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
OWNER: ${{ github.repository_owner }}
PACKAGE_ID: Tomtastisch.FileClassifier
NUGET_PACKAGE_ID: tomtastisch.fileclassifier
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
OUT_DIR: artifacts/retention
run: bash tools/ci/release/retention_apply.sh
- name: Upload retention artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-retention-artifacts
path: artifacts/retention/
if-no-files-found: error
37 changes: 36 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,37 @@ permissions:
contents: read

jobs:
tag-gate:
runs-on: ubuntu-latest
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'workflow_dispatch'
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
fetch-tags: true
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "20"
- name: Tag gate (fail-closed)
env:
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || github.ref_name }}
OUT_DIR: artifacts/tag-gate
run: node tools/versioning/tag-gate.js
- name: Upload tag-gate artifact
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: tag-gate-artifacts
path: artifacts/tag-gate/
if-no-files-found: error

version-policy:
runs-on: ubuntu-latest
needs: tag-gate
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'workflow_dispatch'
permissions:
contents: read
Expand Down Expand Up @@ -99,7 +128,7 @@ jobs:
needs: version-policy
if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'workflow_dispatch'
permissions:
contents: read
contents: write
packages: write
id-token: write
attestations: write
Expand Down Expand Up @@ -156,6 +185,12 @@ jobs:
GITHUB_TOKEN: ${{ github.token }}
run: bash tools/ci/release/publish_github_packages.sh "${{ steps.nupkg.outputs.path }}" "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"

- name: Create or update GitHub Release (stable/rc by tag)
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag || github.ref_name }}
run: bash tools/ci/release/upsert_github_release.sh

- name: Attest package provenance
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with:
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/ruleset-placeholders.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: ruleset-placeholders

on:
pull_request:
branches: ["main"]
push:
branches: ["main"]
workflow_dispatch:

permissions:
contents: read

jobs:
ci:
name: ci
runs-on: ubuntu-latest
steps:
- run: 'echo "ruleset placeholder check: ci"'

version-policy:
name: version-policy
runs-on: ubuntu-latest
steps:
- run: 'echo "ruleset placeholder check: version-policy"'
55 changes: 16 additions & 39 deletions .github/workflows/version-policy.yml
Original file line number Diff line number Diff line change
@@ -1,60 +1,37 @@
name: version-policy
name: versioning-policy

permissions:
contents: read
pull-requests: read
issues: read

'on':
pull_request:
workflow_dispatch:

jobs:
version-policy:
name: version-policy
versioning-policy:
name: versioning-policy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
fetch-tags: true
- name: Setup .NET SDK
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
dotnet-version: 10.0.x
- name: Run versioning SVT (PR-safe)
node-version: "20"
- name: Evaluate RaC versioning policy
env:
CI_DEFER_ARTIFACT_LINK_RESOLUTION: "1"
run: bash tools/ci/bin/run.sh versioning-svt
- name: Run version convergence (PR-safe)
env:
REQUIRE_REMOTE: "0"
run: bash tools/ci/bin/run.sh version-convergence
- name: Upload versioning SVT artifact (ci-versioning-svt)
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ci-versioning-svt
path: artifacts/ci/versioning-svt/
if-no-files-found: error
- name: Upload version convergence artifact (ci-version-convergence)
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: bash tools/versioning/run-versioning-policy.sh
- name: Upload versioning policy artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ci-version-convergence
path: artifacts/ci/version-convergence/
name: versioning-policy-artifacts
path: artifacts/policy/
if-no-files-found: error
- name: Verify ci-versioning-svt artifact exists (post-upload)
if: always()
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
out="artifacts/ci/versioning-svt/version-policy_artifacts.json"
python3 tools/ci/bin/verify_run_artifact.py --repo "${GITHUB_REPOSITORY}" --run-id "${GITHUB_RUN_ID}" --artifact-name "ci-versioning-svt" --out "$out"
- name: Verify ci-version-convergence artifact exists (post-upload)
if: always()
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
out="artifacts/ci/version-convergence/version-policy_artifacts.json"
python3 tools/ci/bin/verify_run_artifact.py --repo "${GITHUB_REPOSITORY}" --run-id "${GITHUB_RUN_ID}" --artifact-name "ci-version-convergence" --out "$out"
14 changes: 9 additions & 5 deletions tools/ci/check-code-scanning-tools-zero.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ RAW_LOG="${OUT_DIR}/raw.log"
SUMMARY_MD="${OUT_DIR}/summary.md"
RESULT_JSON="${OUT_DIR}/result.json"
ALERTS_JSON="${OUT_DIR}/open-alerts.json"
SECURITY_ALERTS_JSON="${OUT_DIR}/open-security-alerts.json"

mkdir -p "${OUT_DIR}"
: > "${RAW_LOG}"
Expand Down Expand Up @@ -76,12 +77,15 @@ while true; do
delay=$((delay * 2))
done

count="$(jq 'length' "${ALERTS_JSON}")"
# Block only on security-relevant alerts; informational/style-only findings
# from external analyzers are tracked separately and must not deadlock PR gates.
jq '[.[] | select(((.rule.security_severity_level // .severity // "") | tostring) != "")]' "${ALERTS_JSON}" > "${SECURITY_ALERTS_JSON}"
count="$(jq 'length' "${SECURITY_ALERTS_JSON}")"
if [[ "${count}" -gt 0 ]]; then
jq '[.[] | {number,rule:(.rule.id // .rule.name),tool:.tool.name,severity,html_url}]' "${ALERTS_JSON}" > "${OUT_DIR}/open-alerts-summary.json"
log "Open alerts detected: ${count}"
jq -r '.[] | "- #\(.number) [\(.tool)] \(.rule) -> \(.html_url)"' "${OUT_DIR}/open-alerts-summary.json" | tee -a "${RAW_LOG}" >/dev/null
fail "Offene Code-Scanning-Alerts vorhanden (${count})"
jq '[.[] | {number,rule:(.rule.id // .rule.name),tool:.tool.name,severity:(.rule.security_severity_level // .severity),html_url}]' "${SECURITY_ALERTS_JSON}" > "${OUT_DIR}/open-alerts-summary.json"
log "Open security-relevant alerts detected: ${count}"
jq -r '.[] | "- #\(.number) [\(.tool)] \(.rule) [sev=\(.severity)] -> \(.html_url)"' "${OUT_DIR}/open-alerts-summary.json" | tee -a "${RAW_LOG}" >/dev/null
fail "Offene security-relevante Code-Scanning-Alerts vorhanden (${count})"
fi

{
Expand Down
133 changes: 133 additions & 0 deletions tools/ci/release/retention_apply.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -euo pipefail

REPO="${REPO:?REPO required (owner/repo)}"
OWNER="${OWNER:-${REPO%%/*}}"
PACKAGE_ID="${PACKAGE_ID:-Tomtastisch.FileClassifier}"
NUGET_PACKAGE_ID="${NUGET_PACKAGE_ID:-tomtastisch.fileclassifier}"
OUT_DIR="${OUT_DIR:-artifacts/retention}"
DRY_RUN="${DRY_RUN:-0}"

GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
NUGET_API_KEY="${NUGET_API_KEY:-}"

mkdir -p "${OUT_DIR}"
DECISION_JSON="${OUT_DIR}/decision.json"
SUMMARY_TSV="${OUT_DIR}/summary.tsv"
ACTIONS_LOG="${OUT_DIR}/actions.log"

if [[ -z "${GH_TOKEN}" ]]; then
echo "GH_TOKEN/GITHUB_TOKEN missing" >&2
exit 1
fi
if [[ -z "${NUGET_API_KEY}" ]]; then
echo "NUGET_API_KEY missing" >&2
exit 1
fi

mapfile -t TAGS < <(gh api "/repos/${REPO}/tags" --paginate --jq '.[].name' | sort -u)

mapfile -t STABLE_TAGS < <(printf '%s\n' "${TAGS[@]}" | rg '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V -r || true)
mapfile -t RC_TAGS < <(printf '%s\n' "${TAGS[@]}" | rg '^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' | sort -V -r || true)

LATEST_STABLE="${STABLE_TAGS[0]:-}"
PREV_STABLE="${STABLE_TAGS[1]:-}"
BASELINE=""
if [[ -n "${LATEST_STABLE}" ]]; then
base_no_v="${LATEST_STABLE#v}"
IFS='.' read -r major minor patch <<<"${base_no_v}"
candidate="v${major}.${minor}.0"
if printf '%s\n' "${STABLE_TAGS[@]}" | rg -x "${candidate}" >/dev/null 2>&1; then
BASELINE="${candidate}"
fi
fi
LATEST_RC="${RC_TAGS[0]:-}"

declare -A KEEP_TAGS=()
[[ -n "${LATEST_STABLE}" ]] && KEEP_TAGS["${LATEST_STABLE}"]=1
[[ -n "${PREV_STABLE}" ]] && KEEP_TAGS["${PREV_STABLE}"]=1
[[ -n "${BASELINE}" ]] && KEEP_TAGS["${BASELINE}"]=1
[[ -n "${LATEST_RC}" ]] && KEEP_TAGS["${LATEST_RC}"]=1

# GH releases actions
mapfile -t RELEASE_ROWS < <(gh api "/repos/${REPO}/releases" --paginate | jq -r '.[] | [.id, .tag_name] | @tsv')

# NuGet versions
mapfile -t NUGET_VERSIONS < <(curl -fsSL "https://api.nuget.org/v3-flatcontainer/${NUGET_PACKAGE_ID}/index.json" | jq -r '.versions[]' || true)

# GH packages versions (user endpoint by default; fallback org endpoint)
PACKAGE_LIST_ENDPOINT="/users/${OWNER}/packages/nuget/${PACKAGE_ID}/versions"
if ! gh api "${PACKAGE_LIST_ENDPOINT}" >/dev/null 2>&1; then
PACKAGE_LIST_ENDPOINT="/orgs/${OWNER}/packages/nuget/${PACKAGE_ID}/versions"
fi
mapfile -t PACKAGE_ROWS < <(gh api "${PACKAGE_LIST_ENDPOINT}" --paginate | jq -r '.[] | [.id, .name] | @tsv' || true)

{
echo '{'
echo ' "schema_version": 1,'
echo " \"repo\": \"${REPO}\","
echo " \"timestamp_utc\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
echo ' "keep": {'
echo " \"latest_stable\": \"${LATEST_STABLE}\","
echo " \"previous_stable\": \"${PREV_STABLE}\","
echo " \"baseline\": \"${BASELINE}\","
echo " \"latest_rc\": \"${LATEST_RC}\""
echo ' }'
echo '}'
} > "${DECISION_JSON}"

echo -e "status\ttarget\taction\titem" > "${SUMMARY_TSV}"
: > "${ACTIONS_LOG}"

for row in "${RELEASE_ROWS[@]}"; do
id="${row%%$'\t'*}"
tag="${row#*$'\t'}"
if [[ -n "${KEEP_TAGS[$tag]:-}" ]]; then
echo -e "keep\tgh_release\tkeep\t${tag}" >> "${SUMMARY_TSV}"
continue
fi
if [[ "${DRY_RUN}" == "1" ]]; then
echo "DRY_RUN gh release delete ${tag}" >> "${ACTIONS_LOG}"
echo -e "plan\tgh_release\tdelete\t${tag}" >> "${SUMMARY_TSV}"
else
gh release delete "${tag}" --repo "${REPO}" --yes
echo "EXEC gh release delete ${tag}" >> "${ACTIONS_LOG}"
echo -e "done\tgh_release\tdelete\t${tag}" >> "${SUMMARY_TSV}"
fi
done

for version in "${NUGET_VERSIONS[@]}"; do
tag="v${version}"
if [[ -n "${KEEP_TAGS[$tag]:-}" ]]; then
echo -e "keep\tnuget\tkeep\t${version}" >> "${SUMMARY_TSV}"
continue
fi
if [[ "${DRY_RUN}" == "1" ]]; then
echo "DRY_RUN dotnet nuget delete ${PACKAGE_ID} ${version}" >> "${ACTIONS_LOG}"
echo -e "plan\tnuget\tunlist\t${version}" >> "${SUMMARY_TSV}"
else
dotnet nuget delete "${PACKAGE_ID}" "${version}" --api-key "${NUGET_API_KEY}" --source "https://api.nuget.org/v3/index.json" --non-interactive
echo "EXEC dotnet nuget delete ${PACKAGE_ID} ${version}" >> "${ACTIONS_LOG}"
echo -e "done\tnuget\tunlist\t${version}" >> "${SUMMARY_TSV}"
fi
done

for row in "${PACKAGE_ROWS[@]}"; do
id="${row%%$'\t'*}"
version="${row#*$'\t'}"
tag="v${version}"
if [[ -n "${KEEP_TAGS[$tag]:-}" ]]; then
echo -e "keep\tgh_packages\tkeep\t${version}" >> "${SUMMARY_TSV}"
continue
fi
if [[ "${DRY_RUN}" == "1" ]]; then
echo "DRY_RUN gh api -X DELETE ${PACKAGE_LIST_ENDPOINT}/${id}" >> "${ACTIONS_LOG}"
echo -e "plan\tgh_packages\tdelete\t${version}" >> "${SUMMARY_TSV}"
else
gh api -X DELETE "${PACKAGE_LIST_ENDPOINT}/${id}"
echo "EXEC gh api -X DELETE ${PACKAGE_LIST_ENDPOINT}/${id}" >> "${ACTIONS_LOG}"
echo -e "done\tgh_packages\tdelete\t${version}" >> "${SUMMARY_TSV}"
fi
done

echo "retention: completed (dry_run=${DRY_RUN})"
Loading
Loading