a8e6479193
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 13s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Workflow Lint & Validation / Notify Lint Results (push) Failing after 1s
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 12s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 12s
Workflow Lint & Validation / Validate Secrets Contract (push) Successful in 7s
Workflow Lint & Validation / Lint All Workflow Files (push) Failing after 14s
308 lines
11 KiB
YAML
308 lines
11 KiB
YAML
name: Prepare Release
|
||
|
||
on:
|
||
workflow_run:
|
||
workflows: ["Validators (Pushes and Pull Requests)"]
|
||
types: [completed]
|
||
workflow_dispatch:
|
||
inputs:
|
||
version:
|
||
description: 'Release version (auto-generated if empty, e.g. quant_20260711.0.abc1234 for the first deploy that day)'
|
||
required: false
|
||
type: string
|
||
|
||
env:
|
||
DOTNET_VERSION: '10.0.x'
|
||
|
||
concurrency:
|
||
group: prepare-release-${{ github.event.workflow_run.head_sha || github.sha }}
|
||
cancel-in-progress: false
|
||
|
||
jobs:
|
||
upstream-gate:
|
||
name: "Upstream CI Success Gate"
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Check CI Pipeline Status
|
||
run: |
|
||
if [ "${{ github.event_name }}" = "workflow_run" ]; then
|
||
if [ "${{ github.event.workflow_run.conclusion }}" != "success" ]; then
|
||
echo "❌ ERROR: CI pipeline failed — release preparation blocked"
|
||
exit 1
|
||
fi
|
||
echo "✓ CI pipeline succeeded — proceeding to release"
|
||
else
|
||
echo "ℹ Release triggered manually — skipping upstream CI check"
|
||
fi
|
||
|
||
build-and-release:
|
||
name: Build & Create Release
|
||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 30
|
||
needs: upstream-gate
|
||
outputs:
|
||
version: ${{ steps.metadata.outputs.version }}
|
||
commit: ${{ steps.metadata.outputs.commit }}
|
||
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Setup .NET
|
||
uses: actions/setup-dotnet@v4
|
||
with:
|
||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||
|
||
- name: Generate Metadata
|
||
id: metadata
|
||
run: |
|
||
VERSION_INPUT="${{ github.event.inputs.version }}"
|
||
COMMIT=$(git rev-parse --short HEAD)
|
||
|
||
# Auto-generate version if not provided
|
||
if [ -z "$VERSION_INPUT" ]; then
|
||
# Simple, reliable version scheme: timestamp + commit hash
|
||
# Avoids unreliable Gitea API calls (network failures, timeouts)
|
||
# Format: vYYYY.MM.DD.HHMMSS.COMMIT
|
||
TIMESTAMP=$(TZ=Asia/Seoul date +%Y.%m.%d.%H%M%S)
|
||
VERSION="v${TIMESTAMP}.${COMMIT}"
|
||
else
|
||
VERSION="$VERSION_INPUT"
|
||
fi
|
||
|
||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||
echo "commit=${COMMIT}" >> $GITHUB_OUTPUT
|
||
echo "Version: $VERSION"
|
||
echo "Commit: $COMMIT"
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
|
||
- name: Install Frontend Dependencies & Build
|
||
run: |
|
||
cd src/frontend
|
||
npm install
|
||
npm run build
|
||
cd ../..
|
||
|
||
- name: Copy Built Frontend to wwwroot
|
||
run: |
|
||
mkdir -p src/dotnet/QuantEngine.Web/wwwroot
|
||
cp -r src/frontend/dist/* src/dotnet/QuantEngine.Web/wwwroot/
|
||
echo "✓ Frontend assets copied to BFF wwwroot"
|
||
|
||
- name: Restore
|
||
run: |
|
||
dotnet restore src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj
|
||
|
||
|
||
- name: Build (Release)
|
||
run: |
|
||
dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \
|
||
-c Release \
|
||
--no-restore \
|
||
-p:ContinuousIntegrationBuild=true
|
||
|
||
- name: Publish
|
||
run: |
|
||
dotnet publish src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj \
|
||
-c Release \
|
||
-o ./publish \
|
||
--no-restore \
|
||
--no-build
|
||
|
||
- name: Write Version Text
|
||
run: |
|
||
echo "${{ steps.metadata.outputs.version }}" > ./publish/version.txt
|
||
|
||
- name: Write Production Config
|
||
run: |
|
||
mkdir -p ./publish
|
||
VERSION="${{ steps.metadata.outputs.version }}"
|
||
python3 -c '
|
||
import json
|
||
import pathlib
|
||
|
||
# NOTE: No ConnectionStrings here on purpose. The real DB
|
||
# password lives only in /home/kjh2064/.config/quantengine.env
|
||
# on the production server and is injected via systemd
|
||
# EnvironmentFile (ConnectionStrings__DefaultConnection),
|
||
# which overrides this file at runtime. Never bake secrets
|
||
# into a build artifact that ends up in a Gitea Release.
|
||
config = {
|
||
"Logging": {
|
||
"LogLevel": {
|
||
"Default": "Information"
|
||
}
|
||
},
|
||
"AppVersion": "'$VERSION'"
|
||
}
|
||
|
||
pathlib.Path("./publish/appsettings.Production.json").write_text(
|
||
json.dumps(config, ensure_ascii=False, indent=2),
|
||
encoding="utf-8"
|
||
)'
|
||
|
||
test -s ./publish/appsettings.Production.json || { echo "ERROR: appsettings.Production.json is empty"; exit 1; }
|
||
echo "✓ Production config created (version: $VERSION)"
|
||
|
||
- name: Package Artifact
|
||
run: |
|
||
VERSION="${{ steps.metadata.outputs.version }}"
|
||
ARTIFACT="quantengine_${VERSION}.tar.gz"
|
||
tar -czf "$ARTIFACT" -C ./publish .
|
||
echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT
|
||
echo "✓ Package: $(du -sh $ARTIFACT | cut -f1)"
|
||
file "$ARTIFACT"
|
||
|
||
- name: Generate Artifact Checksum
|
||
run: |
|
||
VERSION="${{ steps.metadata.outputs.version }}"
|
||
ARTIFACT="quantengine_${VERSION}.tar.gz"
|
||
sha256sum "$ARTIFACT" | awk '{print $1}' > "${ARTIFACT}.sha256"
|
||
echo "✓ Checksum created: ${ARTIFACT}.sha256"
|
||
cat "${ARTIFACT}.sha256"
|
||
|
||
- name: Generate Release Manifest
|
||
run: |
|
||
VERSION="${{ steps.metadata.outputs.version }}"
|
||
COMMIT="${{ steps.metadata.outputs.commit }}"
|
||
ARTIFACT="quantengine_${VERSION}.tar.gz"
|
||
CHECKSUM=$(cat "${ARTIFACT}.sha256")
|
||
python3 - <<PY
|
||
import json
|
||
import pathlib
|
||
|
||
payload = {
|
||
"version": "${VERSION}",
|
||
"commit": "${COMMIT}",
|
||
"artifact": "${ARTIFACT}",
|
||
"sha256": "${CHECKSUM}",
|
||
}
|
||
pathlib.Path("${ARTIFACT}.manifest.json").write_text(
|
||
json.dumps(payload, ensure_ascii=False, indent=2),
|
||
encoding="utf-8",
|
||
)
|
||
PY
|
||
echo "✓ Manifest created"
|
||
|
||
- name: Validate Release Manifest
|
||
run: |
|
||
ARTIFACT="quantengine_${{ steps.metadata.outputs.version }}.tar.gz"
|
||
MANIFEST="${ARTIFACT}.manifest.json"
|
||
|
||
python3 - <<PY
|
||
import json
|
||
import sys
|
||
import pathlib
|
||
|
||
try:
|
||
data = json.loads(pathlib.Path("${MANIFEST}").read_text(encoding="utf-8"))
|
||
|
||
required_fields = ["version", "commit", "artifact", "sha256"]
|
||
for field in required_fields:
|
||
if field not in data or not data[field]:
|
||
print(f"ERROR: Manifest missing or empty '{field}'")
|
||
sys.exit(1)
|
||
|
||
print(f"✓ Manifest validated: {data['version']}")
|
||
except Exception as e:
|
||
print(f"ERROR: {e}")
|
||
sys.exit(1)
|
||
PY
|
||
|
||
- name: Create Git Tag
|
||
run: |
|
||
VERSION="${{ steps.metadata.outputs.version }}"
|
||
COMMIT="${{ steps.metadata.outputs.commit }}"
|
||
|
||
git config user.name "Gitea Actions"
|
||
git config user.email "actions@gitea.local"
|
||
|
||
git tag -a "$VERSION" -m "Release $VERSION (commit: $COMMIT)" HEAD
|
||
echo "✓ Local tag created: $VERSION"
|
||
|
||
git push origin "$VERSION"
|
||
echo "✓ Tag pushed: $VERSION"
|
||
|
||
- name: Create Gitea Release
|
||
env:
|
||
VERSION: ${{ steps.metadata.outputs.version }}
|
||
COMMIT: ${{ steps.metadata.outputs.commit }}
|
||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||
run: |
|
||
ARTIFACT="quantengine_${VERSION}.tar.gz"
|
||
API="https://gitea.taxbaik.com/api/v1"
|
||
REPO="kjh2064/QuantEngineByItz"
|
||
|
||
test -s "$ARTIFACT" || { echo "ERROR: artifact missing: $ARTIFACT"; exit 1; }
|
||
|
||
echo "Creating release $VERSION via Gitea API..."
|
||
RELEASE_JSON=$(curl -sf -X POST \
|
||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||
-H "Content-Type: application/json" \
|
||
-d "{\"tag_name\":\"${VERSION}\",\"name\":\"Release ${VERSION}\",\"body\":\"Release Version: ${VERSION} | Commit: ${COMMIT}\",\"target_commitish\":\"main\"}" \
|
||
"${API}/repos/${REPO}/releases")
|
||
|
||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||
|
||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||
echo "ERROR: Failed to create release"
|
||
echo "$RELEASE_JSON"
|
||
exit 1
|
||
fi
|
||
|
||
echo "✓ Release created: $VERSION (id: $RELEASE_ID)"
|
||
|
||
echo "Uploading artifact..."
|
||
curl -sf -X POST \
|
||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||
-H "Content-Type: multipart/form-data" \
|
||
-F "attachment=@${ARTIFACT}" \
|
||
"${API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${ARTIFACT}" \
|
||
-o /dev/null
|
||
|
||
echo "✓ Artifact attached: $ARTIFACT"
|
||
|
||
echo "Uploading checksum..."
|
||
curl -sf -X POST \
|
||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||
-H "Content-Type: multipart/form-data" \
|
||
-F "attachment=@${ARTIFACT}.sha256" \
|
||
"${API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${ARTIFACT}.sha256" \
|
||
-o /dev/null
|
||
|
||
echo "✓ Checksum attached: ${ARTIFACT}.sha256"
|
||
|
||
echo "Uploading manifest..."
|
||
curl -sf -X POST \
|
||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||
-H "Content-Type: multipart/form-data" \
|
||
-F "attachment=@${ARTIFACT}.manifest.json" \
|
||
"${API}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${ARTIFACT}.manifest.json" \
|
||
-o /dev/null
|
||
|
||
echo "✓ Manifest attached: ${ARTIFACT}.manifest.json"
|
||
|
||
notification:
|
||
name: Release Notification
|
||
runs-on: ubuntu-latest
|
||
if: always()
|
||
needs: [upstream-gate, build-and-release]
|
||
|
||
steps:
|
||
- name: Notify Release Ready
|
||
if: needs.build-and-release.result == 'success'
|
||
run: |
|
||
echo "════════════════════════════════════════"
|
||
echo "✅ Release Ready for Deployment"
|
||
echo "════════════════════════════════════════"
|
||
echo "Version: ${{ needs.build-and-release.outputs.version }}"
|
||
echo "Commit: ${{ needs.build-and-release.outputs.commit }}"
|
||
echo ""
|
||
echo "Next: Use deploy-prod.yml to deploy this release"
|
||
echo "════════════════════════════════════════"
|