Skip to content

ci: idempotent tag step — check remote, not local clone #210

ci: idempotent tag step — check remote, not local clone

ci: idempotent tag step — check remote, not local clone #210

Workflow file for this run

name: CI
on:
push:
branches:
- master
- main
- fdv3
- 'support/**'
pull_request:
schedule:
- cron: '0 6 * * 1' # Monday 06:00 UTC
workflow_dispatch:
inputs:
run_integration:
description: 'Run integration tests'
required: false
type: boolean
default: false
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
env:
DOTNET_NOLOGO: 'true'
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
steps:
- name: Detect environment
id: detect-env
shell: bash
run: |
if [ "$GITHUB_ACTOR" == "nektos/act" ]; then
echo "running_in_act=true" >> $GITHUB_OUTPUT
else
echo "running_in_act=false" >> $GITHUB_OUTPUT
fi
- name: Echo environment
shell: bash
run: |
echo "Running in act environment: ${{ steps.detect-env.outputs.running_in_act }}"
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Read release version
id: version
shell: bash
run: |
# Single source of truth: <Version> in Directory.Build.props.
VERSION=$(dotnet msbuild FluentDocker/FluentDocker.csproj -getProperty:Version -nologo -verbosity:quiet | tr -d '[:space:]')
echo "Release version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Restore dependencies
run: dotnet restore
- name: Build solution
run: dotnet build --configuration Release --no-restore
- name: Run tests (in act)
if: steps.detect-env.outputs.running_in_act == 'true'
env:
TERM: dumb
DOTNET_CLI_UI_LANGUAGE: en-US
run: |
echo "Running in act environment"
dotnet test \
--no-build \
--configuration Release \
--framework net10.0 \
--filter "Category=Unit" \
--logger "console;verbosity=detailed"
- name: Run tests with coverage (in GitHub)
if: steps.detect-env.outputs.running_in_act != 'true'
run: >
dotnet test
--no-build
--configuration Release
--framework net10.0
--filter "Category=Unit"
--collect:"XPlat Code Coverage"
--results-directory ./coverage
--settings coverletArgs.runsettings
- name: Upload coverage reports to Codecov
if: steps.detect-env.outputs.running_in_act != 'true' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Create NuGet packages
if: matrix.os == 'ubuntu-latest'
run: |
echo "Creating NuGet packages at version ${{ steps.version.outputs.version }}"
dotnet pack --configuration Release --no-build --output packages
- name: Validate NuGet packages
if: matrix.os == 'ubuntu-latest'
run: |
EXPECTED=("FluentDocker" "FluentDocker.Testing.Xunit" "FluentDocker.Testing.MsTest" "FluentDocker.Testing.NUnit")
for pkg in "${EXPECTED[@]}"; do
ls packages/${pkg}.*.nupkg 1>/dev/null 2>&1 || { echo "MISSING: ${pkg}"; exit 1; }
echo "OK: ${pkg}"
done
- name: List NuGet packages
if: matrix.os == 'ubuntu-latest'
run: |
echo "Created NuGet packages:"
ls -la packages/*.nupkg
- name: Upload NuGet packages as artifacts
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: nuget-packages
path: packages/*.nupkg
retention-days: 7
continue-on-error: ${{ steps.detect-env.outputs.running_in_act == 'true' }}
- name: Publish NuGet packages
if: matrix.os == 'ubuntu-latest' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/support/'))
run: |
dotnet nuget push "packages/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
continue-on-error: ${{ steps.detect-env.outputs.running_in_act == 'true' }}
- name: Create and push Git tag for release
if: matrix.os == 'ubuntu-latest' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/support/')) && steps.detect-env.outputs.running_in_act != 'true'
env:
RELEASE_VERSION: ${{ steps.version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# actions/checkout@v4 does not fetch tags, so check the remote
# directly. `--exit-code` makes ls-remote return non-zero when no
# matching ref exists, which we use to decide whether to push.
if git ls-remote --exit-code --tags origin "refs/tags/$RELEASE_VERSION" >/dev/null 2>&1; then
echo "Tag $RELEASE_VERSION already exists on remote, skipping"
else
echo "Creating Git tag for version: $RELEASE_VERSION"
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
git tag "$RELEASE_VERSION"
git push origin "$RELEASE_VERSION"
fi
# CodeQL Security Analysis
codeql:
name: CodeQL Security Analysis
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main'
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
queries: +security-and-quality
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Build for CodeQL
run: |
dotnet restore
dotnet build --configuration Release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:csharp"
# Integration tests — run on PRs (ubuntu only), schedule, or manual dispatch
integration-tests:
name: Integration Tests (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
# PRs: ubuntu only (fast feedback). Schedule/dispatch: full matrix.
os: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.run_integration == true)) && fromJson('["ubuntu-latest","macos-latest","windows-latest"]') || fromJson('["ubuntu-latest"]') }}
runs-on: ${{ matrix.os }}
if: >
github.event_name == 'pull_request' ||
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && inputs.run_integration == true)
env:
DOTNET_NOLOGO: 'true'
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Check Docker availability
id: docker-check
shell: bash
run: |
if docker info > /dev/null 2>&1; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
fi
- run: dotnet restore
- run: dotnet build --configuration Debug --no-restore
- name: Run integration tests
if: steps.docker-check.outputs.available == 'true'
run: dotnet test FluentDocker.Tests/FluentDocker.Tests.csproj --configuration Debug --no-build --verbosity normal
timeout-minutes: 30
- name: Skip integration tests (no Docker)
if: steps.docker-check.outputs.available != 'true'
shell: bash
run: |
echo "Docker not available on this runner — skipping integration tests"