diff --git a/.github/workflows/tag-gate.yml b/.github/workflows/tag-gate.yml new file mode 100644 index 0000000..7fdb7ec --- /dev/null +++ b/.github/workflows/tag-gate.yml @@ -0,0 +1,63 @@ +name: Tag Gate + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + gate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 + with: + dotnet-version: | + 8.0.x + 10.0.102 + + - name: Gate - validate tag format (stable/rc) + shell: bash + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME}" + + STABLE_RE='^v[0-9]+\.[0-9]+\.[0-9]+$' + RC_RE='^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' + + if [[ "$TAG" =~ $STABLE_RE ]]; then + echo "OK: stable tag $TAG" + elif [[ "$TAG" =~ $RC_RE ]]; then + echo "OK: rc tag $TAG" + else + echo "FAIL: invalid tag format: $TAG" >&2 + exit 1 + fi + + - name: Gate - derive tag outputs (SSOT) + shell: bash + env: + RELEASE_TAG: ${{ github.ref_name }} + run: bash tools/ci/release/derive_tag_outputs.sh + + - name: Evidence + shell: bash + run: | + set -euo pipefail + mkdir -p artifacts/tag-gate + printf '%s\n' "${GITHUB_REF_NAME}" > artifacts/tag-gate/tag.txt + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: always() + with: + name: tag-gate-evidence + path: artifacts/tag-gate/ + if-no-files-found: error diff --git a/docs/governance/002_POLICY_TAGGING_RULESETS.md b/docs/governance/002_POLICY_TAGGING_RULESETS.md new file mode 100644 index 0000000..8532483 --- /dev/null +++ b/docs/governance/002_POLICY_TAGGING_RULESETS.md @@ -0,0 +1,45 @@ +# POLICY: GitHub Tag Rulesets `tags-stable` + `tags-rc` + +## Scope / Target +Tags are SSOT for publish. Patterns: +- stable: `v*` (exclude `v*-rc.*` if UI supports excludes) +- rc: `v*-rc.*` + +## Rulesets (GitHub UI) +Create rulesets at https://github.com/tomtastisch/FileClassifier/settings/rules/new?target=tag + +### Ruleset `tags-stable` +- Target: tag refs +- Pattern: + - include: `v*` + - exclude: `v*-rc.*` (only if UI supports excludes; otherwise `tags-rc` is stricter and will match rc tags too) +- Enforcement: Active (fail-closed) +- Bypass: minimal (Admins or small Maintainer team). No broad bot/app bypass unless explicitly required. +- Rules to enable (depending on UI availability): + - Restrict/Prevent deletions + - Restrict/Prevent updates / force pushes / moving tags + - (optional) Restrict creations (only if you want to limit who can create release tags) + - (optional) Require signed commits / signed tags (only if available) + - (later) Require status checks (enable only after tag workflows exist and are stable) + +### Ruleset `tags-rc` +- Target: tag refs +- Pattern: `v*-rc.*` +- Enforcement and bypass: same as `tags-stable` +- Rules: + - Restrict/Prevent deletions + - Restrict/Prevent updates / force pushes / moving tags + - (optional) Restrict creations + - (optional) Require signed commits / signed tags + - Require status checks once the rc tag workflow is stable (RC is fail-closed) + +## Expected behaviour +- Stable and RC tags can’t be moved or deleted (without bypass). +- Stable and RC tags become the auditable SSOT for releases/publish. +- Publish workflows must depend on the tag gate. + +## Ordering / rollout +1. Create rulesets (in evaluate mode only if needed, otherwise active). +2. Add `.github/workflows/tag-gate.yml`. +3. Wire publish pipelines to `needs: tag-gate` or via `workflow_run` on success. +4. Enable status checks for tags only once the checks exist and are stable.