4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
679 lines
25 KiB
YAML
679 lines
25 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths-ignore:
|
|
- "docs/**"
|
|
- "tests/benchmarks/cloudopsbench/infra/**"
|
|
- "platform/deployment/install-proxy/**"
|
|
- "tests/e2e/kubernetes/helm/scripts/**"
|
|
- "tests/**"
|
|
- "**/*.md"
|
|
- "**/*.mdx"
|
|
- ".claude/**"
|
|
schedule:
|
|
- cron: "30 0 * * *"
|
|
workflow_dispatch:
|
|
inputs:
|
|
channel:
|
|
description: "Release channel to publish."
|
|
required: false
|
|
default: release
|
|
type: choice
|
|
options:
|
|
- release
|
|
- main
|
|
tag:
|
|
description: "Optional tag to release (e.g. v0.1.2026.6.26 or v0.1). Ignored for the main channel."
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: write
|
|
actions: read
|
|
models: read
|
|
|
|
concurrency:
|
|
group: release-${{ github.event_name == 'push' && 'main' || (github.event_name == 'workflow_dispatch' && inputs.channel == 'main' && 'main') || 'stable' }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
prepare:
|
|
if: github.repository == 'Tracer-Cloud/opensre'
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
channel: ${{ steps.meta.outputs.channel }}
|
|
tag_name: ${{ steps.meta.outputs.tag_name }}
|
|
version_name: ${{ steps.meta.outputs.version_name }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve release metadata
|
|
id: meta
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
DISPATCH_CHANNEL: ${{ inputs.channel }}
|
|
DISPATCH_TAG: ${{ inputs.tag }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
channel="release"
|
|
|
|
if [ "$EVENT_NAME" = "push" ]; then
|
|
channel="main"
|
|
fi
|
|
|
|
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$DISPATCH_CHANNEL" = "main" ]; then
|
|
channel="main"
|
|
fi
|
|
|
|
if [ "$channel" = "main" ]; then
|
|
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$DISPATCH_TAG" ]; then
|
|
echo "The main channel does not accept a custom tag." >&2
|
|
exit 1
|
|
fi
|
|
|
|
year="$(date -u +%Y)"
|
|
month="$(date -u +%-m)"
|
|
day="$(date -u +%-d)"
|
|
short_sha="${GITHUB_SHA:0:7}"
|
|
main_version="0.1.${year}.${month}.${day}+main.${short_sha}"
|
|
|
|
echo "channel=main" >> "$GITHUB_OUTPUT"
|
|
echo "tag_name=main-build" >> "$GITHUB_OUTPUT"
|
|
echo "version_name=${main_version}" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$DISPATCH_TAG" ]; then
|
|
tag_name="$DISPATCH_TAG"
|
|
else
|
|
year="$(date -u +%Y)"
|
|
month="$(date -u +%-m)"
|
|
day="$(date -u +%-d)"
|
|
# Keep the v0.1 prefix so the tag makes clear the product is still
|
|
# v0.1, while the date stays useful (e.g. v0.1.2026.6.26).
|
|
tag_name="v0.1.${year}.${month}.${day}"
|
|
fi
|
|
|
|
echo "channel=release" >> "$GITHUB_OUTPUT"
|
|
echo "tag_name=${tag_name}" >> "$GITHUB_OUTPUT"
|
|
echo "version_name=${tag_name#v}" >> "$GITHUB_OUTPUT"
|
|
|
|
verify:
|
|
runs-on: ubuntu-latest
|
|
needs: prepare
|
|
if: github.event_name != 'push'
|
|
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
|
|
- name: Install uv
|
|
uses: astral-sh/setup-uv@v8.1.0
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: |
|
|
pyproject.toml
|
|
uv.lock
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.13"
|
|
|
|
- name: Install dependencies
|
|
run: uv sync --frozen --extra dev
|
|
|
|
- name: Lint
|
|
run: make lint
|
|
|
|
- name: Type check
|
|
run: make typecheck
|
|
|
|
- name: CLI smoke tests
|
|
run: make test-cli-smoke
|
|
|
|
build-python-dist:
|
|
if: needs.prepare.outputs.channel == 'release' && needs.verify.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
needs: [verify, prepare]
|
|
env:
|
|
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
|
|
- name: Install uv
|
|
uses: astral-sh/setup-uv@v8.1.0
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: |
|
|
pyproject.toml
|
|
uv.lock
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.13"
|
|
|
|
- name: Sync release version
|
|
shell: bash
|
|
run: python platform/packaging/sync_release_version.py --tag "$TAG_NAME"
|
|
|
|
- name: Build Python distributions
|
|
run: |
|
|
uv sync --frozen --extra release-dist
|
|
uv run python -m build
|
|
uv run twine check dist/*
|
|
|
|
- name: Verify Python distribution version
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
VERSION="${TAG_NAME#v}"
|
|
test -f "dist/opensre-${VERSION}.tar.gz"
|
|
test -f "dist/opensre-${VERSION}-py3-none-any.whl"
|
|
|
|
- name: Upload Python distributions
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: release-python-dist
|
|
path: dist/*
|
|
if-no-files-found: error
|
|
|
|
build-binaries:
|
|
if: always() && (needs.prepare.outputs.channel == 'main' || needs.verify.result == 'success')
|
|
needs: [verify, prepare]
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runner: ubuntu-22.04
|
|
target: linux-x64
|
|
binary_name: opensre
|
|
archive_ext: tar.gz
|
|
pyinstaller_mode: onedir
|
|
- runner: ubuntu-22.04-arm
|
|
target: linux-arm64
|
|
binary_name: opensre
|
|
archive_ext: tar.gz
|
|
pyinstaller_mode: onedir
|
|
- runner: macos-15-intel
|
|
target: darwin-x64
|
|
binary_name: opensre
|
|
archive_ext: tar.gz
|
|
pyinstaller_mode: onedir
|
|
- runner: macos-latest
|
|
target: darwin-arm64
|
|
binary_name: opensre
|
|
archive_ext: tar.gz
|
|
pyinstaller_mode: onedir
|
|
- runner: windows-latest
|
|
target: windows-x64
|
|
binary_name: opensre.exe
|
|
archive_ext: zip
|
|
pyinstaller_mode: onefile
|
|
# windows-arm64 is currently excluded from the default release matrix:
|
|
# cryptography does not publish win_arm64 wheels, so dependency install
|
|
# falls back to a source build that requires an OpenSSL toolchain on the
|
|
# GitHub-hosted runner.
|
|
runs-on: ${{ matrix.runner }}
|
|
env:
|
|
RELEASE_CHANNEL: ${{ needs.prepare.outputs.channel }}
|
|
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
|
|
VERSION_NAME: ${{ needs.prepare.outputs.version_name }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
|
|
- name: Install uv
|
|
uses: astral-sh/setup-uv@v8.1.0
|
|
with:
|
|
enable-cache: true
|
|
cache-dependency-glob: |
|
|
pyproject.toml
|
|
uv.lock
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.13"
|
|
|
|
- name: Sync binary version
|
|
shell: bash
|
|
run: python platform/packaging/sync_release_version.py --version "$VERSION_NAME"
|
|
|
|
- name: Install binary build dependencies
|
|
shell: bash
|
|
run: uv sync --frozen --extra release-binary
|
|
|
|
- name: Stage stdlib platform module for bundling
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
# The first-party ``platform`` package shadows the stdlib ``platform``
|
|
# module. PyInstaller does not lay out the stdlib as loose ``.py`` files,
|
|
# so bundle a copy of the genuine module that platform/__init__.py can
|
|
# load from ``sys._MEIPASS`` at runtime (see _FROZEN_STDLIB_DIR).
|
|
rm -rf .stdlib_vendor
|
|
mkdir -p .stdlib_vendor
|
|
uv run python -c "import os, shutil, sysconfig; src = os.path.join(sysconfig.get_path('stdlib'), 'platform.py'); shutil.copy(src, os.path.join('.stdlib_vendor', 'platform.py')); print('Staged stdlib platform from ' + src)"
|
|
|
|
- name: Build binary
|
|
run: >-
|
|
uv run pyinstaller surfaces/cli/__main__.py
|
|
--name opensre
|
|
--${{ matrix.pyinstaller_mode }}
|
|
--clean
|
|
--noconfirm
|
|
--collect-data surfaces.cli
|
|
--collect-data config
|
|
--copy-metadata opensre
|
|
--collect-data litellm
|
|
--hidden-import tiktoken_ext
|
|
--hidden-import tiktoken_ext.openai_public
|
|
--collect-submodules integrations
|
|
--collect-submodules surfaces.interactive_shell
|
|
--collect-submodules tools
|
|
--add-data "platform:platform"
|
|
--add-data ".stdlib_vendor:_opensre_stdlib_platform"
|
|
|
|
- name: Smoke test binary (Unix)
|
|
if: runner.os != 'Windows'
|
|
shell: bash
|
|
run: |
|
|
set -uo pipefail
|
|
# Capture the exit status explicitly so a crashing binary still prints
|
|
# its stdout/stderr (under `set -e` the failed substitution aborted the
|
|
# step before the output was ever shown, hiding the real error).
|
|
# onefile builds place the executable at ./dist/<name>; onedir builds
|
|
# produce a ./dist/opensre/ directory containing the executable. A bare
|
|
# `-x` test matches the onedir directory too (dirs carry the search
|
|
# bit), so require a regular file before treating it as the binary.
|
|
if [ -f "./dist/${{ matrix.binary_name }}" ] && [ -x "./dist/${{ matrix.binary_name }}" ]; then
|
|
BINARY_PATH="./dist/${{ matrix.binary_name }}"
|
|
else
|
|
BINARY_PATH="./dist/opensre/${{ matrix.binary_name }}"
|
|
fi
|
|
set +e
|
|
VERSION_OUTPUT="$("$BINARY_PATH" --version 2>&1)"
|
|
VERSION_STATUS=$?
|
|
set -e
|
|
printf '%s\n' "$VERSION_OUTPUT"
|
|
if [ "$VERSION_STATUS" -ne 0 ]; then
|
|
printf '::error::%s --version exited with status %s\n' "${{ matrix.binary_name }}" "$VERSION_STATUS" >&2
|
|
exit "$VERSION_STATUS"
|
|
fi
|
|
case "$VERSION_OUTPUT" in
|
|
*"$VERSION_NAME"*) ;;
|
|
*)
|
|
printf 'Binary version mismatch: expected %s but saw %s\n' "$VERSION_NAME" "$VERSION_OUTPUT" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
"$BINARY_PATH" -h >/dev/null
|
|
LITELLM_DATA="./dist/opensre/_internal/litellm/model_prices_and_context_window_backup.json"
|
|
if [ ! -f "$LITELLM_DATA" ]; then
|
|
echo "LiteLLM package data missing from Unix onedir bundle: ${LITELLM_DATA}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Check Linux binary glibc compatibility
|
|
if: runner.os == 'Linux'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
max_glibc="$(
|
|
find dist/opensre -type f \( -name opensre -o -name '*.so' -o -name '*.so.*' \) -print0 \
|
|
| xargs -0 strings 2>/dev/null \
|
|
| grep -Eo 'GLIBC_[0-9]+\.[0-9]+(\.[0-9]+)?' \
|
|
| sort -Vu \
|
|
| tail -n 1 \
|
|
|| true
|
|
)"
|
|
echo "Max required glibc symbol: ${max_glibc:-none}"
|
|
if [ -n "$max_glibc" ] && [ "$(printf '%s\n' "$max_glibc" GLIBC_2.35 | sort -V | tail -n 1)" != "GLIBC_2.35" ]; then
|
|
echo "Linux binary requires ${max_glibc}; pin the Linux runner back to Ubuntu 22.04 or lower dependency wheel requirements." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Smoke test binary (Windows)
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
$versionOutput = & ".\dist\${{ matrix.binary_name }}" --version 2>&1 | Out-String
|
|
$versionText = $versionOutput.Trim()
|
|
Write-Host $versionText
|
|
$expectedVersion = $env:VERSION_NAME
|
|
if ($versionText -notmatch [regex]::Escape($expectedVersion)) {
|
|
throw "Binary version mismatch. Expected '$expectedVersion' but saw '$versionText'."
|
|
}
|
|
& ".\dist\${{ matrix.binary_name }}" -h | Out-Null
|
|
|
|
- name: Package binary archive (Unix)
|
|
if: runner.os != 'Windows'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [ "$RELEASE_CHANNEL" = "release" ]; then
|
|
ASSET_BASENAME="opensre_${TAG_NAME#v}_${{ matrix.target }}"
|
|
else
|
|
ASSET_BASENAME="opensre_main_${{ matrix.target }}"
|
|
fi
|
|
tar -C dist -czf "${ASSET_BASENAME}.tar.gz" "${{ matrix.binary_name }}"
|
|
shasum -a 256 "${ASSET_BASENAME}.tar.gz" > "${ASSET_BASENAME}.tar.gz.sha256"
|
|
|
|
- name: Package binary archive (Windows)
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
if ($env:RELEASE_CHANNEL -eq "release") {
|
|
$assetBaseName = "opensre_$($env:TAG_NAME.TrimStart('v'))_${{ matrix.target }}"
|
|
} else {
|
|
$assetBaseName = "opensre_main_${{ matrix.target }}"
|
|
}
|
|
Compress-Archive -Path "dist\${{ matrix.binary_name }}" -DestinationPath "${assetBaseName}.zip"
|
|
$hash = (Get-FileHash -Algorithm SHA256 "${assetBaseName}.zip").Hash.ToLowerInvariant()
|
|
Set-Content -Path "${assetBaseName}.zip.sha256" -Value "$hash ${assetBaseName}.zip"
|
|
|
|
- name: Upload binary archive
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: ${{ env.RELEASE_CHANNEL == 'main' && 'main-' || '' }}release-${{ matrix.target }}
|
|
path: |
|
|
opensre_*_${{ matrix.target }}.${{ matrix.archive_ext }}
|
|
opensre_*_${{ matrix.target }}.${{ matrix.archive_ext }}.sha256
|
|
if-no-files-found: error
|
|
|
|
publish-release:
|
|
if: needs.prepare.outputs.channel == 'release'
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- prepare
|
|
- build-python-dist
|
|
- build-binaries
|
|
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
fetch-depth: 0
|
|
|
|
- name: Download release artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: release-*
|
|
path: release-assets
|
|
merge-multiple: true
|
|
|
|
- name: Resolve release context
|
|
id: release_ctx
|
|
env:
|
|
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
default_branch="${{ github.event.repository.default_branch }}"
|
|
git fetch origin "$default_branch" --tags --force
|
|
target_sha="$(git rev-parse "origin/$default_branch")"
|
|
|
|
previous_tag="$(
|
|
git tag --list 'v0.1.[0-9][0-9][0-9][0-9].[0-9]*.[0-9]*' --sort=-v:refname \
|
|
| grep -v -x "$TAG_NAME" \
|
|
| head -n 1 \
|
|
|| true
|
|
)"
|
|
|
|
range_spec="$target_sha"
|
|
if [ -n "$previous_tag" ]; then
|
|
range_spec="${previous_tag}..${target_sha}"
|
|
fi
|
|
|
|
{
|
|
printf 'target_sha=%s\n' "$target_sha"
|
|
printf 'previous_tag=%s\n' "$previous_tag"
|
|
printf 'range_spec=%s\n' "$range_spec"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Create release notes
|
|
env:
|
|
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
|
|
RANGE_SPEC: ${{ steps.release_ctx.outputs.range_spec }}
|
|
PREVIOUS_TAG: ${{ steps.release_ctx.outputs.previous_tag }}
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
{
|
|
echo "## Changelog"
|
|
echo
|
|
if [ -n "$PREVIOUS_TAG" ]; then
|
|
echo "_Changes since ${PREVIOUS_TAG}_"
|
|
else
|
|
echo "_Changes up to ${TAG_NAME}_"
|
|
fi
|
|
echo
|
|
git log "$RANGE_SPEC" --no-merges -n 100 --pretty='- %s (%h) — %an'
|
|
echo
|
|
} > GENERATED_CHANGELOG.md
|
|
|
|
# Summarize commits into prose via GitHub Models (gpt-4o-mini).
|
|
# Falls back to raw changelog if the API is unavailable.
|
|
raw_commits="$(git log "$RANGE_SPEC" --no-merges --pretty='%s' | head -40 || true)"
|
|
if [ -n "$raw_commits" ]; then
|
|
api_body="$(jq -n --arg commits "$raw_commits" '{
|
|
model: "gpt-4o-mini",
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: "You are writing a Discord release announcement for an open-source SRE CLI tool called opensre. Summarize the git commits into 2-4 short, punchy prose sentences — no bullet points, no headers, no markdown. Write in present tense, active voice. Focus on what users will notice: new features, fixes, performance, integrations. Be specific but concise."
|
|
},
|
|
{role: "user", content: ("Commits:\n" + $commits)}
|
|
],
|
|
max_tokens: 300,
|
|
temperature: 0.4
|
|
}')"
|
|
curl -sS --max-time 20 \
|
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
"https://models.inference.ai.azure.com/chat/completions" \
|
|
-d "$api_body" 2>/dev/null \
|
|
| jq -r '.choices[0].message.content // empty' 2>/dev/null \
|
|
| tr -d '\r' > DISCORD_NARRATIVE.md || true
|
|
|
|
if [ -s DISCORD_NARRATIVE.md ]; then
|
|
echo "LLM narrative generated ($(wc -c < DISCORD_NARRATIVE.md) bytes)."
|
|
else
|
|
echo "LLM summary unavailable; Discord will use raw changelog."
|
|
rm -f DISCORD_NARRATIVE.md
|
|
fi
|
|
fi
|
|
|
|
CB='```'
|
|
{
|
|
printf '## Install\n\n'
|
|
printf '### cURL (macOS / Linux)\n\n%sbash\ncurl -fsSL https://install.opensre.com | bash -s -- --release --version %s\n%s\n\n' "$CB" "${TAG_NAME#v}" "$CB"
|
|
printf '### Homebrew (macOS / Linux)\n\n%sbash\nbrew tap tracer-cloud/tap\nbrew install tracer-cloud/tap/opensre\n%s\n\n' "$CB" "$CB"
|
|
printf '### PowerShell (Windows)\n\n%spowershell\n$env:OPENSRE_INSTALL_CHANNEL="release"; $env:OPENSRE_VERSION="%s"; irm https://install.opensre.com | iex\n%s\n\n' "$CB" "${TAG_NAME#v}" "$CB"
|
|
printf '### Python\n\n%sbash\npipx install opensre\n%s\n\n' "$CB" "$CB"
|
|
} > RELEASE_NOTES.md
|
|
cat GENERATED_CHANGELOG.md >> RELEASE_NOTES.md
|
|
|
|
- name: Create GitHub release
|
|
id: github_release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG_NAME: ${{ needs.prepare.outputs.tag_name }}
|
|
TARGET_SHA: ${{ steps.release_ctx.outputs.target_sha }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if gh release view "$TAG_NAME" --repo "${{ github.repository }}" >/dev/null 2>&1; then
|
|
echo "created=false" >> "$GITHUB_OUTPUT"
|
|
echo "Release $TAG_NAME already exists; nothing to do."
|
|
exit 0
|
|
fi
|
|
|
|
VERSION="${TAG_NAME#v}"
|
|
|
|
# Every release-channel publish is the newest stable build at this
|
|
# point, so mark it as GitHub's Latest. The title keeps the full
|
|
# version (e.g. "OpenSRE 0.1.2026.6.26") so the v0.1 line is clear.
|
|
gh release create "$TAG_NAME" \
|
|
release-assets/* \
|
|
--repo "${{ github.repository }}" \
|
|
--target "$TARGET_SHA" \
|
|
--title "OpenSRE ${VERSION}" \
|
|
--notes-file RELEASE_NOTES.md \
|
|
--latest
|
|
|
|
echo "created=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Announce on Discord
|
|
if: steps.github_release.outputs.created == 'true' && !github.event.repository.fork && !github.event.repository.private
|
|
continue-on-error: true
|
|
env:
|
|
DISCORD_WEBHOOK_URL_UPDATE: ${{ secrets.DISCORD_WEBHOOK_URL_UPDATE }}
|
|
DISCORD_RELEASES_ROLE_ID: ${{ secrets.DISCORD_RELEASES_ROLE_ID }}
|
|
DISCORD_RELEASE_LOGO_EMOJI: ${{ secrets.DISCORD_RELEASE_LOGO_EMOJI }}
|
|
DISCORD_RELEASE_LOGO_URL: ${{ secrets.DISCORD_RELEASE_LOGO_URL }}
|
|
RELEASE_TAG: ${{ needs.prepare.outputs.tag_name }}
|
|
RELEASE_URL: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.prepare.outputs.tag_name }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ -z "${DISCORD_WEBHOOK_URL_UPDATE:-}" ]; then
|
|
echo "DISCORD_WEBHOOK_URL_UPDATE is not set; skipping Discord announcement."
|
|
exit 0
|
|
fi
|
|
|
|
# Prefer LLM-generated narrative; fall back to raw changelog.
|
|
if [ -f DISCORD_NARRATIVE.md ] && [ -s DISCORD_NARRATIVE.md ]; then
|
|
changelog_file="DISCORD_NARRATIVE.md"
|
|
elif [ -f GENERATED_CHANGELOG.md ]; then
|
|
changelog_file="GENERATED_CHANGELOG.md"
|
|
else
|
|
echo "No changelog available." > /tmp/discord_fallback.md
|
|
changelog_file="/tmp/discord_fallback.md"
|
|
fi
|
|
|
|
# Use an external Python script to build the JSON payload.
|
|
# This avoids jq issues with multiline LLM output containing control characters.
|
|
payload="$(CHANGELOG_FILE="$changelog_file" python3 .github/scripts/build-discord-payload.py)"
|
|
|
|
curl --fail -sS --max-time 30 -X POST -H "Content-Type: application/json" \
|
|
-d "$payload" "$DISCORD_WEBHOOK_URL_UPDATE"
|
|
|
|
- name: Sync Homebrew tap formula
|
|
if: steps.github_release.outputs.created == 'true'
|
|
continue-on-error: true
|
|
env:
|
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
|
VERSION: ${{ needs.prepare.outputs.tag_name }}
|
|
ASSET_DIR: release-assets
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${HOMEBREW_TAP_GITHUB_TOKEN:-}" ]; then
|
|
echo "HOMEBREW_TAP_GITHUB_TOKEN is not set; skipping Homebrew tap sync."
|
|
exit 0
|
|
fi
|
|
export VERSION="${VERSION#v}"
|
|
bash .github/scripts/sync-homebrew-tap-formula.sh
|
|
|
|
publish-main-release:
|
|
if: always() && needs.prepare.outputs.channel == 'main' && needs.build-binaries.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- prepare
|
|
- build-binaries
|
|
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
# The rolling `main-build` tag is force-moved to each new main commit
|
|
# below. Whenever main has advanced past a commit that touches
|
|
# .github/workflows/**, the default GITHUB_TOKEN (the
|
|
# github-actions[bot] GitHub App) is refused with "refusing to allow a
|
|
# GitHub App to create or update workflow ... without `workflows`
|
|
# permission" — and that scope cannot be granted via the permissions
|
|
# block. MAIN_BUILD_TAG_TOKEN must be a token that carries workflow
|
|
# write access (classic PAT with `repo` + `workflow`, a fine-grained
|
|
# PAT with Contents + Workflows: write, or a GitHub App token). It is
|
|
# persisted so the tag push below authenticates with it. Falls back to
|
|
# GITHUB_TOKEN so the rest of the job still runs if the secret is unset
|
|
# (the tag push will then fail as before until the secret is added).
|
|
token: ${{ secrets.MAIN_BUILD_TAG_TOKEN || secrets.GITHUB_TOKEN }}
|
|
persist-credentials: true
|
|
|
|
- name: Download main release artifacts
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: main-release-*
|
|
path: main-release-assets
|
|
merge-multiple: true
|
|
|
|
- name: Create release notes
|
|
env:
|
|
VERSION_NAME: ${{ needs.prepare.outputs.version_name }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
short_sha="$(printf '%s' "$GITHUB_SHA" | cut -c1-7)"
|
|
built_at="$(date -u +"%Y-%m-%d %H:%M UTC")"
|
|
|
|
CB='```'
|
|
{
|
|
printf '## Main build\n\nRolling binary build from `main`.\n\n'
|
|
printf -- '- Version: `%s`\n- Commit: `%s`\n- Built: %s\n\n' "$VERSION_NAME" "$short_sha" "$built_at"
|
|
printf '### Install\n\nmacOS / Linux:\n\n%sbash\ncurl -fsSL https://install.opensre.com | bash\n%s\n\n' "$CB" "$CB"
|
|
printf 'Equivalent explicit main channel:\n\n%sbash\ncurl -fsSL https://install.opensre.com | bash -s -- --main\n%s\n\n' "$CB" "$CB"
|
|
printf 'Windows:\n\n%spowershell\nirm https://install.opensre.com | iex\n%s\n' "$CB" "$CB"
|
|
} > MAIN_RELEASE_NOTES.md
|
|
|
|
- name: Move main build tag to the latest commit
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
git tag -f "${{ needs.prepare.outputs.tag_name }}" "$GITHUB_SHA"
|
|
git push origin "refs/tags/${{ needs.prepare.outputs.tag_name }}" --force
|
|
|
|
- name: Publish rolling main release
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
tag_name="${{ needs.prepare.outputs.tag_name }}"
|
|
|
|
if gh release view "$tag_name" --repo "${{ github.repository }}" >/dev/null 2>&1; then
|
|
gh release upload "$tag_name" main-release-assets/* --repo "${{ github.repository }}" --clobber
|
|
gh release edit "$tag_name" \
|
|
--repo "${{ github.repository }}" \
|
|
--title "Main" \
|
|
--notes-file MAIN_RELEASE_NOTES.md
|
|
exit 0
|
|
fi
|
|
|
|
gh release create "$tag_name" \
|
|
main-release-assets/* \
|
|
--repo "${{ github.repository }}" \
|
|
--target "$GITHUB_SHA" \
|
|
--title "Main" \
|
|
--notes-file MAIN_RELEASE_NOTES.md \
|
|
--prerelease
|