* ci: give the feedz.io push room and make it retry The "Publish to feedz.io" step timed out twice recently (on the #7985-era and #7991-era PRs) and passed on re-run both times. The 300s in those logs is not the job timeout -- it is `dotnet nuget push`'s own default --timeout, applied per push. With ~107 packages pushed sequentially from a single glob, the feed only has to get slow under the burst for one of them to cross that line, so this reads as the package set growing into the limit rather than pure network flakiness. Push four packages at a time with a 900s per-push timeout, and retry each package up to three times with a backoff. --skip-duplicate was already there and makes the retries idempotent -- a package that landed before the failure is skipped on the next attempt. A push that fails all three attempts still fails the step, and the job keeps a bounded ceiling: 25 minutes on the step, 30 on the job. Same package set, same feed, same credentials -- only how the pushes are issued changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: share the hardened package push with the nuget.org job The nuget.org publish had the same shape the feedz.io one just outgrew: a single sequential `dotnet nuget push *.nupkg` over ~107 packages against a 300s per-push default. It has not timed out yet because it only runs on a published release, but a timeout there is the worse one -- a half-finished publish to nuget.org is not something a re-run cleanly repairs. Rather than copy the retry loop into a second job, move it into a composite action both jobs call with their own feed and key. Same behaviour on both: four pushes in flight, 900s per push, three attempts with a backoff, --skip-duplicate making the retries idempotent. Two details worth calling out: Composite actions have to be on disk, so both jobs now check out .github/actions. It is a sparse, depth-1 checkout -- a couple of seconds, not a clone of the repo. The action fails when it finds no .nupkg at all. `dotnet nuget push *.nupkg` used to fail on its own when the glob matched nothing, and moving to `find | xargs` would have quietly turned a broken artifact upload into a green publish of zero packages. Step-level timeout-minutes is deliberately absent: the runner does not honour it on a step that calls a composite action. The 30-minute job timeout is the real ceiling on both jobs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: fit the package push retry budget inside the job timeout Three 900-second attempts plus backoff could run 2790s per package, so a package that kept failing got the 30-minute job cancelled mid-retry -- taking its three concurrent siblings with it and leaving a release only partially published. Budget three 420s attempts plus 45s of backoff instead: 1305s worst case, roughly eight minutes short of the timeout. Pin the checkout that supplies the composite action to a full commit SHA as well; that checkout hands the action the feed API key, so a retargeted tag would be a path to the publishing credentials. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: bound the package push by a step deadline, not a per-package budget xargs -P 4 puts the packages through in waves, and the job timeout covers every wave, so budgeting one package's retries still let enough slow waves run the job out of time -- and a cancelled runner leaves a release half published with no record of what made it. Give the step a deadline instead: no attempt starts that cannot finish before it, and running out of time fails the step naming the packages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: note the job timeout's relationship to the push deadline Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: pin the artifact download in the publishing jobs too Both third-party actions in these jobs run before the local composite is handed a feed API key, in the same writable workspace, so a retargeted tag on either could substitute the composite ahead of the credential. The checkout was pinned; the artifact download was not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
403 lines
14 KiB
YAML
403 lines
14 KiB
YAML
name: Packages
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
push:
|
|
branches:
|
|
- 'main'
|
|
- 'bug/*'
|
|
- 'perf/*'
|
|
- 'patch/*'
|
|
- 'feat/*'
|
|
- 'enh/*'
|
|
- 'rc/*'
|
|
- 'develop/*'
|
|
- 'release/*'
|
|
- 'codex/*'
|
|
release:
|
|
types: [prereleased, published]
|
|
|
|
env:
|
|
base_version: '3.8.0'
|
|
feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json'
|
|
nuget_feed_source: 'https://api.nuget.org/v3/index.json'
|
|
|
|
jobs:
|
|
test_unit_integration:
|
|
name: Test unit/integration with coverage
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up .NET SDK
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: 10.0.1xx
|
|
|
|
- name: Show .NET info
|
|
run: |
|
|
dotnet --info
|
|
dotnet --list-sdks
|
|
|
|
- name: Cache NuGet packages
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.nuget/packages
|
|
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-nuget-
|
|
|
|
- name: Restore solution
|
|
run: dotnet restore Elsa.sln --ignore-failed-sources
|
|
|
|
- name: Prepare coverage directory
|
|
run: |
|
|
rm -rf artifacts/coverage
|
|
mkdir -p artifacts/coverage
|
|
|
|
- name: Run tests with coverage
|
|
run: |
|
|
set -euo pipefail
|
|
dotnet --info
|
|
if ! dotnet --version | grep -q '^10\.'; then
|
|
echo "Expected .NET SDK 10.x but got $(dotnet --version)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Discover unit and integration test projects. Component tests run in their own job.
|
|
mapfile -t projects < <(
|
|
find test/unit test/integration \
|
|
-type f -name '*.csproj' -print0 2>/dev/null \
|
|
| xargs -0 -n1 \
|
|
| sort
|
|
)
|
|
|
|
if [ ${#projects[@]} -eq 0 ]; then
|
|
echo "No test projects were found in test/unit or test/integration." >&2
|
|
exit 1
|
|
fi
|
|
|
|
for project in "${projects[@]}"; do
|
|
echo "Building test project ${project}"
|
|
dotnet build "$project" --configuration Release --framework net10.0
|
|
|
|
echo "Running tests with coverage for ${project}"
|
|
dotnet test "$project" \
|
|
--configuration Release \
|
|
--framework net10.0 \
|
|
--no-build \
|
|
--logger "GitHubActions;report-warnings=false" \
|
|
/p:CollectCoverage=true
|
|
done
|
|
|
|
- name: Dump docker logs on failure
|
|
if: failure()
|
|
run: |
|
|
echo "=== Docker containers ==="
|
|
docker ps -a || true
|
|
echo "=== Docker logs ==="
|
|
for container in $(docker ps -aq); do
|
|
echo "--- Logs for container $container ---"
|
|
docker logs "$container" 2>&1 | tail -100 || true
|
|
done
|
|
|
|
- name: Upload coverage reports
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: unit-integration-coverage-reports
|
|
path: artifacts/coverage
|
|
|
|
test_component:
|
|
name: Test component with coverage
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up .NET SDK
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: 10.0.1xx
|
|
|
|
- name: Show .NET info
|
|
run: |
|
|
dotnet --info
|
|
dotnet --list-sdks
|
|
|
|
- name: Cache NuGet packages
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.nuget/packages
|
|
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-nuget-
|
|
|
|
- name: Restore solution
|
|
run: dotnet restore Elsa.sln --ignore-failed-sources
|
|
|
|
- name: Prepare test artifact directories
|
|
run: |
|
|
rm -rf artifacts/coverage artifacts/test-results
|
|
mkdir -p artifacts/coverage artifacts/test-results/component
|
|
|
|
- name: Run component tests with coverage and hang diagnostics
|
|
run: |
|
|
set -euo pipefail
|
|
dotnet --info
|
|
if ! dotnet --version | grep -q '^10\.'; then
|
|
echo "Expected .NET SDK 10.x but got $(dotnet --version)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mapfile -t projects < <(
|
|
find test/component \
|
|
-type f -name '*.csproj' -print0 2>/dev/null \
|
|
| xargs -0 -n1 \
|
|
| sort
|
|
)
|
|
|
|
if [ ${#projects[@]} -eq 0 ]; then
|
|
echo "No test projects were found in test/component." >&2
|
|
exit 1
|
|
fi
|
|
|
|
for project in "${projects[@]}"; do
|
|
echo "Building component test project ${project}"
|
|
dotnet build "$project" --configuration Release --framework net10.0
|
|
|
|
echo "Running component tests with coverage and hang diagnostics for ${project}"
|
|
dotnet test "$project" \
|
|
--configuration Release \
|
|
--framework net10.0 \
|
|
--no-build \
|
|
--logger "GitHubActions;report-warnings=false" \
|
|
--logger "trx" \
|
|
--results-directory artifacts/test-results/component \
|
|
--verbosity detailed \
|
|
--blame-hang \
|
|
--blame-hang-timeout 2m \
|
|
--blame-hang-dump-type mini \
|
|
/p:CollectCoverage=true
|
|
done
|
|
|
|
- name: Dump docker logs on failure
|
|
if: failure()
|
|
run: |
|
|
echo "=== Docker containers ==="
|
|
docker ps -a || true
|
|
echo "=== Docker logs ==="
|
|
for container in $(docker ps -aq); do
|
|
echo "--- Logs for container $container ---"
|
|
docker logs "$container" 2>&1 | tail -100 || true
|
|
done
|
|
|
|
- name: Upload coverage reports
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: component-coverage-reports
|
|
path: artifacts/coverage
|
|
|
|
- name: Upload component test diagnostics
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: component-test-diagnostics
|
|
path: artifacts/test-results
|
|
|
|
coverage_report:
|
|
name: Generate coverage report
|
|
needs:
|
|
- test_unit_integration
|
|
- test_component
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
if: always() && !cancelled() && needs.test_unit_integration.result == 'success' && needs.test_component.result == 'success'
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up .NET SDK
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: 10.0.1xx
|
|
|
|
- name: Download coverage reports
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: '*-coverage-reports'
|
|
path: artifacts/coverage
|
|
merge-multiple: true
|
|
|
|
- name: Install ReportGenerator
|
|
run: dotnet tool install -g dotnet-reportgenerator-globaltool
|
|
|
|
- name: Generate HTML coverage report
|
|
run: |
|
|
reportgenerator \
|
|
"-reports:./artifacts/coverage/**/coverage.cobertura.xml" \
|
|
"-targetdir:./artifacts/coverage-report" \
|
|
"-reporttypes:Html;Cobertura;TextSummary" \
|
|
"-verbosity:Info"
|
|
|
|
- name: Upload coverage reports
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: coverage-reports
|
|
path: artifacts/coverage
|
|
|
|
- name: Upload Pages artifact
|
|
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')
|
|
uses: actions/upload-pages-artifact@v3
|
|
with:
|
|
path: './artifacts/coverage-report'
|
|
|
|
build:
|
|
name: Build packages
|
|
needs:
|
|
- test_unit_integration
|
|
- test_component
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
if: ${{ github.event_name != 'pull_request' }}
|
|
steps:
|
|
- name: Extract branch name
|
|
run: |
|
|
BRANCH_NAME=${{ github.ref }} # e.g., refs/heads/main
|
|
BRANCH_NAME=${BRANCH_NAME#refs/heads/} # remove the refs/heads/ prefix
|
|
# Extract the last part after the last slash of the branch name, if any, e.g., feature/issue-123 -> issue-123 and use it as the version prefix.
|
|
PACKAGE_PREFIX=$(echo $BRANCH_NAME | rev | cut -d/ -f1 | rev | tr '_' '-')
|
|
|
|
# If the branch name is main, use the preview version. Otherwise, use the branch name as the version prefix.
|
|
if [[ "${BRANCH_NAME}" == "main" || "${BRANCH_NAME}" =~ ^develop/ || "${BRANCH_NAME}" =~ ^release/ ]]; then
|
|
PACKAGE_PREFIX="preview"
|
|
fi
|
|
|
|
echo "Ref: ${{ github.ref }}"
|
|
echo "Branch name: ${BRANCH_NAME}"
|
|
echo "Package prefix: ${PACKAGE_PREFIX}"
|
|
echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_ENV
|
|
echo "PACKAGE_PREFIX=${PACKAGE_PREFIX}" >> $GITHUB_ENV
|
|
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Verify commit exists in branch
|
|
run: |
|
|
if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased") ]]; then
|
|
git fetch --no-tags --prune origin +refs/heads/*:refs/remotes/origin/*
|
|
git branch --remote --contains | grep -E 'origin/(main|release/)'
|
|
else
|
|
git fetch --no-tags --prune origin +refs/heads/*:refs/remotes/origin/*
|
|
git branch --remote --contains | grep origin/${BRANCH_NAME}
|
|
fi
|
|
|
|
- name: Set VERSION variable
|
|
run: |
|
|
if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased") ]]; then
|
|
TAG_NAME=${{ github.ref }} # e.g., refs/tags/3.0.0
|
|
TAG_NAME=${TAG_NAME#refs/tags/} # remove the refs/tags/ prefix
|
|
echo "VERSION=${TAG_NAME}" >> $GITHUB_ENV
|
|
else
|
|
echo "VERSION=${{env.base_version}}-preview.${{github.run_number}}" >> $GITHUB_ENV
|
|
fi
|
|
|
|
- uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: 10.x
|
|
|
|
- name: Compile+Pack
|
|
run: ./build.sh Compile+Pack --version ${VERSION} --analyseCode true
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: elsa-nuget-packages
|
|
path: packages/*nupkg
|
|
if: ${{ github.event_name == 'release' || github.event_name == 'push'}}
|
|
|
|
publish_preview_feedz:
|
|
name: Publish to feedz.io
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
# 30 minutes against the push action's 20-minute deadline: the action stops
|
|
# starting pushes once its own deadline passes, so the runner never has to
|
|
# cancel a publish half-done. Move the two together.
|
|
timeout-minutes: 30
|
|
if: ${{ github.event_name == 'release' || github.event_name == 'push'}}
|
|
steps:
|
|
- name: Check out the push action
|
|
# Every third-party action in this job runs before a feed API key is handed
|
|
# to the local composite, in the same writable workspace, so each one is
|
|
# pinned to a full commit SHA. A mutable tag here could be retargeted to
|
|
# substitute the composite before it receives the credential.
|
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
sparse-checkout: .github/actions
|
|
fetch-depth: 1
|
|
|
|
- name: Download Packages
|
|
# Pinned for the reason given on the checkout above.
|
|
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
|
with:
|
|
name: elsa-nuget-packages
|
|
|
|
- name: Publish to feedz.io
|
|
uses: ./.github/actions/push-nuget-packages
|
|
with:
|
|
feed-source: ${{ env.feedz_feed_source }}
|
|
api-key: ${{ secrets.FEEDZ_API_KEY }}
|
|
|
|
publish_nuget:
|
|
name: Publish release to nuget.org
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
# 30 minutes against the push action's 20-minute deadline: the action stops
|
|
# starting pushes once its own deadline passes, so the runner never has to
|
|
# cancel a publish half-done. Move the two together.
|
|
timeout-minutes: 30
|
|
if: ${{ github.event.action == 'published' }}
|
|
steps:
|
|
- name: Check out the push action
|
|
# Every third-party action in this job runs before a feed API key is handed
|
|
# to the local composite, in the same writable workspace, so each one is
|
|
# pinned to a full commit SHA. A mutable tag here could be retargeted to
|
|
# substitute the composite before it receives the credential.
|
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
sparse-checkout: .github/actions
|
|
fetch-depth: 1
|
|
|
|
- name: Download Packages
|
|
# Pinned for the reason given on the checkout above.
|
|
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
|
with:
|
|
name: elsa-nuget-packages
|
|
|
|
- name: Publish to nuget.org
|
|
uses: ./.github/actions/push-nuget-packages
|
|
with:
|
|
feed-source: ${{ env.nuget_feed_source }}
|
|
api-key: ${{ secrets.NUGET_API_KEY }}
|
|
|
|
deploy_coverage:
|
|
name: Deploy coverage to GitHub Pages
|
|
needs: coverage_report
|
|
runs-on: ubuntu-latest
|
|
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop/3.6.1' || github.ref == 'refs/heads/release/3.6.1'
|
|
permissions:
|
|
pages: write
|
|
id-token: write
|
|
environment:
|
|
name: github-pages
|
|
url: ${{ steps.deployment.outputs.page_url }}
|
|
steps:
|
|
- name: Deploy to GitHub Pages
|
|
id: deployment
|
|
uses: actions/deploy-pages@v4
|