800921d5b3
Workflow Lint & Validation / Validate Secrets Contract (push) Failing after 7s
Snapshot Admin Validation / Validate Snapshot Admin Workflow (push) Failing after 9s
Workflow Lint & Validation / Lint All Workflow Files (push) Failing after 13s
Snapshot Admin Validation / Validate Snapshot Admin UI (push) Successful in 5s
Snapshot Admin Validation / Notify Snapshot Admin Validation Status (push) Failing after 0s
Major improvements: - ci.yml: refactored single 30-step job → 9 parallel jobs * core: CRITICAL tests + DB setup (blocks others) * wbs-audit, dotnet-contracts, ui-storage, database-schema: parallel (7 independent) * calibration-pipeline, operational-reporting: sequential chain * security-validation, workflow-lint: parallel * notify-results: final aggregation * Expected speedup: ~40min → ~15-20min (2-2.5x faster) * Benefit: fault isolation, parallel resource utilization, clearer dependencies - kis_data_collection.yml: split into 2 jobs (credentials + db), improved UX - qualitative_sell_strategy.yml: added push trigger, better test integration - ci_lint.yml → workflow_lint.yml: comprehensive workflow validation - deploy-prod.yml: refactored SSH setup (reduced duplication) - prepare-release.yml: improved upstream-gate messaging - snapshot_admin.yml: split into 2 jobs (workflow + UI) Documentation: - CLAUDE.md: added "Gitea Actions Workflow Structure" section with: * Architecture diagram & dependency graph * Job matrix & trigger schedule * Performance improvements summary * Maintenance checklist & troubleshooting guide No breaking changes: all workflows maintain 100% backward compatibility. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
285 lines
11 KiB
YAML
285 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
|
||
env:
|
||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||
run: |
|
||
VERSION_INPUT="${{ github.event.inputs.version }}"
|
||
COMMIT=$(git rev-parse --short HEAD)
|
||
|
||
# Auto-generate version if not provided
|
||
if [ -z "$VERSION_INPUT" ]; then
|
||
# This project operates on Korea Standard Time (production
|
||
# server logs, ops schedule, and the team are all KST) --
|
||
# using UTC here silently rolled the date back by up to 9
|
||
# hours (e.g. 2026-07-12 01:xx KST is still 2026-07-11 16:xx
|
||
# UTC), so a release cut right after midnight KST would tag
|
||
# itself with yesterday's date.
|
||
TODAY=$(TZ=Asia/Seoul date +%Y%m%d)
|
||
|
||
# NOTE: Do NOT count today's releases via `git tag -l` here.
|
||
# actions/checkout@v4 defaults to a shallow, single-branch
|
||
# clone that does not fetch any tags, so every job container
|
||
# sees zero local tags regardless of how many releases exist
|
||
# -- this is exactly why every release tonight came out as
|
||
# "quant_20260711.1.*" (three of them: b7591fb, 6ab270f,
|
||
# e49922e, all claiming to be deploy #1). Query the actual
|
||
# Gitea Releases API instead, which reflects real state.
|
||
# Sequence number resets to 0 on each new date -- the first
|
||
# release of a day is quant_YYYYMMDD.0.hash, the second .1, etc.
|
||
RELEASES_TODAY=$(curl -sf --connect-timeout 10 --max-time 30 \
|
||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||
"https://gitea.taxbaik.com/api/v1/repos/${{ github.repository }}/tags?limit=50" \
|
||
| jq -r --arg prefix "quant_${TODAY}." '[.[] | select(.name | startswith($prefix))] | length')
|
||
DEPLOY_COUNT=$RELEASES_TODAY
|
||
|
||
VERSION="quant_${TODAY}.${DEPLOY_COUNT}.${COMMIT}"
|
||
else
|
||
VERSION="$VERSION_INPUT"
|
||
fi
|
||
|
||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||
echo "commit=${COMMIT}" >> $GITHUB_OUTPUT
|
||
echo "Version: $VERSION"
|
||
echo "Commit: $COMMIT"
|
||
|
||
- 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
|
||
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"
|
||
}
|
||
}
|
||
}
|
||
|
||
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 (no secrets included)"
|
||
|
||
- 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: ${ARTIFACT}.manifest.json"
|
||
cat "${ARTIFACT}.manifest.json"
|
||
|
||
- 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 "════════════════════════════════════════"
|