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
65 changes: 5 additions & 60 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

permissions:
contents: read
actions: read
security-events: read
pull-requests: read

Expand Down Expand Up @@ -358,66 +359,10 @@ jobs:
dotnet-version: |
8.0.x
10.0.102
- name: Download CI Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-preflight
path: artifacts/ci/preflight
- name: Download Build Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-build
path: artifacts/ci/build
- name: Download API Contract Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-api-contract
path: artifacts/ci/api-contract
- name: Download Pack Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-pack
path: artifacts/ci/pack
- name: Download Consumer Smoke Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-consumer-smoke
path: artifacts/ci/consumer-smoke
- name: Download Package-Backed Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-package-backed-tests
path: artifacts/ci/package-backed-tests
- name: Download Security Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-security-nuget
path: artifacts/ci/security-nuget
- name: Download Docs Link Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-docs-links-full
path: artifacts/ci/docs-links-full
- name: Download Naming Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-naming-snt
path: artifacts/ci/naming-snt
- name: Download Versioning Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-versioning-svt
path: artifacts/ci/versioning-svt
- name: Download Version Convergence Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-version-convergence
path: artifacts/ci/version-convergence
- name: Download Test Artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ci-tests-bdd-coverage
path: artifacts/ci/tests-bdd-coverage
- name: Download Required Artifacts (retry/backoff)
env:
GH_TOKEN: ${{ github.token }}
run: bash tools/ci/bin/download_summary_artifacts.sh "${GITHUB_RUN_ID}"
- name: Run Entry Check
run: bash -euo pipefail tools/ci/bin/run.sh summary
- name: Upload Artifact
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ jobs:
with:
name: nuget-attestation-verify
path: artifacts/nuget/attestation-verify.txt
if-no-files-found: error
if-no-files-found: warn

- name: Gate 4 - SVT post-publish (git version == package version == nuget version)
shell: bash
Expand Down
92 changes: 92 additions & 0 deletions tools/ci/bin/download_artifacts_with_retry.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<'EOF' >&2
Usage: download_artifacts_with_retry.sh <run_id> <artifact=dest> [<artifact=dest>...]

Environment:
GITHUB_REPOSITORY Required (owner/repo).
ARTIFACT_DOWNLOAD_MAX_ATTEMPTS Optional, default: 6
ARTIFACT_DOWNLOAD_INITIAL_DELAY Optional, default: 2 (seconds)
EOF
}

if [[ $# -lt 2 ]]; then
usage
exit 2
fi

if ! command -v gh >/dev/null 2>&1; then
echo "ERROR: gh CLI is required." >&2
exit 1
fi

run_id="$1"
shift

repo="${GITHUB_REPOSITORY:-}"
if [[ -z "${repo}" ]]; then
echo "ERROR: GITHUB_REPOSITORY is required." >&2
exit 1
fi

max_attempts="${ARTIFACT_DOWNLOAD_MAX_ATTEMPTS:-6}"
initial_delay="${ARTIFACT_DOWNLOAD_INITIAL_DELAY:-2}"

if [[ ! "${max_attempts}" =~ ^[0-9]+$ || "${max_attempts}" -lt 1 ]]; then
echo "ERROR: ARTIFACT_DOWNLOAD_MAX_ATTEMPTS must be >= 1." >&2
exit 1
fi
if [[ ! "${initial_delay}" =~ ^[0-9]+$ || "${initial_delay}" -lt 1 ]]; then
echo "ERROR: ARTIFACT_DOWNLOAD_INITIAL_DELAY must be >= 1." >&2
exit 1
fi

download_one() {
local name="$1"
local dest="$2"
local attempt=1
local delay="${initial_delay}"

while (( attempt <= max_attempts )); do
echo "INFO: Download '${name}' attempt ${attempt}/${max_attempts} -> ${dest}"
rm -rf "${dest}"
mkdir -p "${dest}"

if gh run download "${run_id}" --repo "${repo}" -n "${name}" -D "${dest}" >/tmp/artifact-download.log 2>&1; then
return 0
fi

if (( attempt == max_attempts )); then
echo "ERROR: Download '${name}' failed after ${max_attempts} attempts." >&2
cat /tmp/artifact-download.log >&2 || true
return 1
fi

echo "WARN: Download '${name}' failed. Retrying in ${delay}s." >&2
cat /tmp/artifact-download.log >&2 || true
sleep "${delay}"
delay=$((delay * 2))
attempt=$((attempt + 1))
done

return 1
}

for spec in "$@"; do
if [[ "${spec}" != *=* ]]; then
echo "ERROR: Invalid artifact mapping '${spec}'. Expected <artifact=dest>." >&2
exit 1
fi
name="${spec%%=*}"
dest="${spec#*=}"
if [[ -z "${name}" || -z "${dest}" ]]; then
echo "ERROR: Invalid artifact mapping '${spec}'. Expected non-empty name and destination." >&2
exit 1
fi

download_one "${name}" "${dest}"
done

echo "OK: all artifacts downloaded with retry policy."
25 changes: 25 additions & 0 deletions tools/ci/bin/download_summary_artifacts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"

if [[ "$#" -ne 1 ]]; then
echo "Usage: $0 <run_id>" >&2
exit 2
fi

run_id="$1"

bash "${SCRIPT_DIR}/download_artifacts_with_retry.sh" "${run_id}" \
"ci-preflight=artifacts/ci/preflight" \
"ci-build=artifacts/ci/build" \
"ci-api-contract=artifacts/ci/api-contract" \
"ci-pack=artifacts/ci/pack" \
"ci-consumer-smoke=artifacts/ci/consumer-smoke" \
"ci-package-backed-tests=artifacts/ci/package-backed-tests" \
"ci-security-nuget=artifacts/ci/security-nuget" \
"ci-docs-links-full=artifacts/ci/docs-links-full" \
"ci-naming-snt=artifacts/ci/naming-snt" \
"ci-versioning-svt=artifacts/ci/versioning-svt" \
"ci-version-convergence=artifacts/ci/version-convergence" \
"ci-tests-bdd-coverage=artifacts/ci/tests-bdd-coverage"
31 changes: 25 additions & 6 deletions tools/ci/release/gate4_verify_postpublish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,21 @@ retry_sleep_seconds="${SVT_POSTPUBLISH_RETRY_SLEEP_SECONDS:-10}"
if [[ -n "${SVT_POSTPUBLISH_REQUIRE_REGISTRATION:-}" ]]; then
require_registration="${SVT_POSTPUBLISH_REQUIRE_REGISTRATION}"
else
if [[ "${is_prerelease}" == "1" ]]; then
require_registration="0"
else
require_registration="1"
fi
# Registration endpoint is typically the slowest to converge and caused repeated
# false negatives. Gate 4 now validates publish availability via V2 download.
require_registration="0"
fi

if [[ -n "${SVT_POSTPUBLISH_REQUIRE_FLATCONTAINER:-}" ]]; then
require_flatcontainer="${SVT_POSTPUBLISH_REQUIRE_FLATCONTAINER}"
else
require_flatcontainer="0"
fi

if [[ -n "${SVT_POSTPUBLISH_REQUIRE_V2_DOWNLOAD:-}" ]]; then
require_v2_download="${SVT_POSTPUBLISH_REQUIRE_V2_DOWNLOAD}"
else
require_v2_download="1"
fi

if [[ ! "${retry_count}" =~ ^[0-9]+$ ]]; then
Expand All @@ -56,6 +66,14 @@ if [[ "${require_registration}" != "0" && "${require_registration}" != "1" ]]; t
echo "SVT_POSTPUBLISH_REQUIRE_REGISTRATION must be 0 or 1 (actual='${require_registration}')" >&2
exit 1
fi
if [[ "${require_flatcontainer}" != "0" && "${require_flatcontainer}" != "1" ]]; then
echo "SVT_POSTPUBLISH_REQUIRE_FLATCONTAINER must be 0 or 1 (actual='${require_flatcontainer}')" >&2
exit 1
fi
if [[ "${require_v2_download}" != "0" && "${require_v2_download}" != "1" ]]; then
echo "SVT_POSTPUBLISH_REQUIRE_V2_DOWNLOAD must be 0 or 1 (actual='${require_v2_download}')" >&2
exit 1
fi

EXPECTED_VERSION="${expected_version}" \
NUPKG_PATH="${nupkg_path}" \
Expand All @@ -64,5 +82,6 @@ RETRY_SLEEP_SECONDS="${retry_sleep_seconds}" \
RETRY_SCHEDULE_SECONDS="${retry_schedule_seconds}" \
REQUIRE_SEARCH=0 \
REQUIRE_REGISTRATION="${require_registration}" \
REQUIRE_FLATCONTAINER=1 \
REQUIRE_FLATCONTAINER="${require_flatcontainer}" \
REQUIRE_V2_DOWNLOAD="${require_v2_download}" \
bash tools/ci/verify_nuget_release.sh
69 changes: 65 additions & 4 deletions tools/ci/release/upsert_github_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ set -euo pipefail

RELEASE_TAG="${RELEASE_TAG:?RELEASE_TAG is required}"
REPO="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
MAX_ATTEMPTS="${RELEASE_GH_MAX_ATTEMPTS:-5}"
INITIAL_DELAY_SECONDS="${RELEASE_GH_INITIAL_DELAY_SECONDS:-2}"

version="${RELEASE_TAG#v}"
prerelease_flag=()
Expand All @@ -21,15 +23,74 @@ else
latest_edit_flag+=(--latest)
fi

if [[ ! "${MAX_ATTEMPTS}" =~ ^[0-9]+$ || "${MAX_ATTEMPTS}" -lt 1 ]]; then
echo "MAX_ATTEMPTS must be a positive integer (actual='${MAX_ATTEMPTS}')" >&2
exit 1
fi
if [[ ! "${INITIAL_DELAY_SECONDS}" =~ ^[0-9]+$ || "${INITIAL_DELAY_SECONDS}" -lt 1 ]]; then
echo "INITIAL_DELAY_SECONDS must be a positive integer (actual='${INITIAL_DELAY_SECONDS}')" >&2
exit 1
fi

classify_reason() {
local msg="$1"
if grep -qiE "401|bad credentials|requires authentication|unauthorized" <<<"${msg}"; then
echo "auth"
return
fi
if grep -qiE "429|rate limit|secondary rate limit" <<<"${msg}"; then
echo "rate-limit"
return
fi
if grep -qiE "5[0-9][0-9]|internal server error|gateway timeout|bad gateway|service unavailable" <<<"${msg}"; then
echo "5xx"
return
fi
if grep -qiE "timed out|timeout|connection reset|connection refused|temporary failure|network" <<<"${msg}"; then
echo "network"
return
fi
echo "unknown"
}

run_with_retry() {
local attempt=1
local delay="${INITIAL_DELAY_SECONDS}"
local cmd=("$@")
local output=""
while (( attempt <= MAX_ATTEMPTS )); do
output="$("${cmd[@]}" 2>&1)" && {
if [[ -n "${output}" ]]; then
printf '%s\n' "${output}"
fi
return 0
}

local reason
reason="$(classify_reason "${output}")"
echo "WARN: release API call failed (attempt ${attempt}/${MAX_ATTEMPTS}, reason=${reason})." >&2
printf '%s\n' "${output}" >&2

if [[ "${reason}" == "auth" || "${attempt}" -ge "${MAX_ATTEMPTS}" ]]; then
echo "FAIL: release API call failed (reason=${reason})." >&2
return 1
fi

sleep "${delay}"
delay=$((delay * 2))
attempt=$((attempt + 1))
done
}

if gh release view "${RELEASE_TAG}" --repo "${REPO}" >/dev/null 2>&1; then
gh release edit "${RELEASE_TAG}" --repo "${REPO}" --title "${RELEASE_TAG}" "${prerelease_flag[@]}" "${latest_edit_flag[@]}"
run_with_retry gh release edit "${RELEASE_TAG}" --repo "${REPO}" --title "${RELEASE_TAG}" "${prerelease_flag[@]}" "${latest_edit_flag[@]}"

# gh does not support --latest=false for `release edit`. If an existing RC was
# accidentally marked as Latest, correct it via API.
if [[ "${is_rc}" == "true" ]]; then
release_id="$(gh api -H "Accept: application/vnd.github+json" "repos/${REPO}/releases/tags/${RELEASE_TAG}" --jq .id)"
gh api -X PATCH -H "Accept: application/vnd.github+json" "repos/${REPO}/releases/${release_id}" -f make_latest=false >/dev/null
release_id="$(run_with_retry gh api -H "Accept: application/vnd.github+json" "repos/${REPO}/releases/tags/${RELEASE_TAG}" --jq .id)"
run_with_retry gh api -X PATCH -H "Accept: application/vnd.github+json" "repos/${REPO}/releases/${release_id}" -f make_latest=false >/dev/null
fi
else
gh release create "${RELEASE_TAG}" --repo "${REPO}" --title "${RELEASE_TAG}" --notes "Automated tag-only release for ${version}" "${prerelease_flag[@]}" "${latest_create_flag[@]}"
run_with_retry gh release create "${RELEASE_TAG}" --repo "${REPO}" --title "${RELEASE_TAG}" --notes "Automated tag-only release for ${version}" "${prerelease_flag[@]}" "${latest_create_flag[@]}"
fi
Loading
Loading