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 Success Gate runs-on: ubuntu-latest steps: - name: Fail Fast on Failed Validator Chain run: | if [ "${{ github.event_name }}" = "workflow_run" ] && [ "${{ github.event.workflow_run.conclusion }}" != "success" ]; then echo "ERROR: Validators workflow did not succeed; release preparation is blocked." exit 1 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 - <