Release #28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Bump version, tag, build & push the Docker image to GHCR, and cut a | |
| # GitHub Release with auto-generated notes — all from a single | |
| # workflow_dispatch click. The choice of bump (major/minor/patch) is the | |
| # only required input. | |
| # | |
| # Trigger: GitHub UI → Actions → Release → "Run workflow" → pick bump. | |
| # | |
| # Security notes: | |
| # - The only user input is `bump`, a `choice` enum constrained to | |
| # {patch,minor,major} — no free-text reaches a shell. Even so, it is | |
| # piped through an `env:` block (BUMP) and matched in a `case`. | |
| # - All other interpolations are GitHub-controlled context (repo, actor, | |
| # sha, secrets) or values we computed ourselves from the VERSION file. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| bump: | |
| description: 'Version bump' | |
| required: true | |
| type: choice | |
| default: patch | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| permissions: | |
| contents: write | |
| jobs: | |
| release: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.bump.outputs.tag }} | |
| version: ${{ steps.bump.outputs.next }} | |
| steps: | |
| # Org GitHub App token (no PAT — never expires). Authorizes the version | |
| # bump commit/tag push to protected main (the App is a bypass actor on the | |
| # org ruleset) AND the downstream hub dispatch. Scoped to this repo + the | |
| # hub. Minted first so checkout can push with it. | |
| - name: Mint org App token | |
| id: app | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.PINEFORGE_APP_ID }} | |
| private-key: ${{ secrets.PINEFORGE_APP_PRIVATE_KEY }} | |
| owner: pineforge-4pass | |
| repositories: pineforge-engine | |
| - name: Checkout (full history) | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| # App token bypasses the org ruleset (PR-required) for the bump push. | |
| token: ${{ steps.app.outputs.token }} | |
| - name: Configure git identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| - name: Compute next version | |
| id: bump | |
| env: | |
| BUMP: ${{ inputs.bump }} | |
| run: | | |
| set -euo pipefail | |
| current="$(tr -d '[:space:]' < VERSION)" | |
| if ! [[ "$current" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then | |
| echo "VERSION file malformed: '$current'" >&2 | |
| exit 1 | |
| fi | |
| maj="${BASH_REMATCH[1]}" | |
| min="${BASH_REMATCH[2]}" | |
| pat="${BASH_REMATCH[3]}" | |
| case "$BUMP" in | |
| major) maj=$((maj + 1)); min=0; pat=0 ;; | |
| minor) min=$((min + 1)); pat=0 ;; | |
| patch) pat=$((pat + 1)) ;; | |
| *) echo "unknown bump: $BUMP" >&2; exit 1 ;; | |
| esac | |
| next="${maj}.${min}.${pat}" | |
| tag="v${next}" | |
| echo "current=$current" >> "$GITHUB_OUTPUT" | |
| echo "next=$next" >> "$GITHUB_OUTPUT" | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| echo "Bumping ${current} -> ${next} (${BUMP})" | |
| - name: Refuse if tag already exists | |
| env: | |
| TAG: ${{ steps.bump.outputs.tag }} | |
| run: | | |
| if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then | |
| echo "tag ${TAG} already exists -- pick a different bump" >&2 | |
| exit 1 | |
| fi | |
| - name: Write bumped VERSION, commit, tag, push | |
| env: | |
| NEXT: ${{ steps.bump.outputs.next }} | |
| TAG: ${{ steps.bump.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| printf '%s\n' "${NEXT}" > VERSION | |
| git add VERSION | |
| git commit -m "chore(release): ${TAG}" | |
| git tag -a "${TAG}" -m "Release ${TAG}" | |
| git push origin HEAD | |
| git push origin "${TAG}" | |
| - name: Resolve previous tag for changelog range | |
| id: prev | |
| env: | |
| TAG: ${{ steps.bump.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| prev="$(git tag --list 'v*' --sort=-v:refname | grep -v "^${TAG}$" | head -n1 || true)" | |
| echo "prev=${prev}" >> "$GITHUB_OUTPUT" | |
| if [[ -n "$prev" ]]; then | |
| echo "Changelog range: ${prev}..${TAG}" | |
| else | |
| echo "No prior tag -- full history will be used" | |
| fi | |
| - name: Generate release notes | |
| id: notes | |
| env: | |
| TAG: ${{ steps.bump.outputs.tag }} | |
| PREV: ${{ steps.prev.outputs.prev }} | |
| run: | | |
| set -euo pipefail | |
| { | |
| echo "## ${TAG}" | |
| echo | |
| if [[ -n "$PREV" ]]; then | |
| echo "### Changes since ${PREV}" | |
| echo | |
| git log --pretty=format:'- %s (%h)' "${PREV}..${TAG}" | |
| else | |
| echo "### Initial release" | |
| echo | |
| git log --pretty=format:'- %s (%h)' "${TAG}" | |
| fi | |
| echo | |
| echo | |
| echo "### Prebuilt libraries" | |
| echo | |
| echo "Per-arch static-lib + headers tarballs are attached below (C ABI)." | |
| echo "For the full PineScript -> backtest image, use \`ghcr.io/pineforge-4pass/pineforge-release\`." | |
| } > RELEASE_NOTES.md | |
| echo "path=RELEASE_NOTES.md" >> "$GITHUB_OUTPUT" | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.bump.outputs.tag }} | |
| NOTES: ${{ steps.notes.outputs.path }} | |
| run: | | |
| gh release create "${TAG}" \ | |
| --title "${TAG}" \ | |
| --notes-file "${NOTES}" \ | |
| --verify-tag | |
| # Prebuilt static-lib + headers tarballs, one per (os, arch). Attached | |
| # to the GitHub Release created above. Tarball layout: | |
| # pineforge-vX.Y.Z-{triple}/ | |
| # include/pineforge/*.{h,hpp} (curated + generated version.h) | |
| # lib/libpineforge.a | |
| # lib/cmake/PineForge/*.cmake | |
| # LICENSE NOTICE VERSION | |
| # | |
| # Consumers still need Eigen3 (>= 3.3) installed system-wide; see | |
| # release notes / README. | |
| prebuilt: | |
| needs: release | |
| permissions: | |
| contents: write # gh release upload | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - triple: linux-x86_64 | |
| runner: ubuntu-22.04 | |
| cmake_extra: "" | |
| - triple: linux-aarch64 | |
| runner: ubuntu-22.04-arm | |
| cmake_extra: "" | |
| # Single universal slice covers x86_64 + arm64 macs. macos-13 | |
| # (Intel) runner pool is being phased out and queues for >30min; | |
| # cross-build a fat archive on macos-latest (arm64) instead. | |
| - triple: macos-universal | |
| runner: macos-latest | |
| cmake_extra: "-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64" | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - name: Checkout tag | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ needs.release.outputs.tag }} | |
| fetch-depth: 0 # version helper wants tags for `git describe` | |
| - name: Install Eigen3 (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libeigen3-dev | |
| - name: Install Eigen3 (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| brew install eigen | |
| echo "CMAKE_PREFIX_PATH=$(brew --prefix eigen)" >> "$GITHUB_ENV" | |
| - name: Configure | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| TRIPLE: ${{ matrix.triple }} | |
| CMAKE_EXTRA: ${{ matrix.cmake_extra }} | |
| run: | | |
| set -euo pipefail | |
| stage="stage/pineforge-${TAG}-${TRIPLE}" | |
| echo "STAGE_DIR=${stage}" >> "$GITHUB_ENV" | |
| # CMAKE_EXTRA is unquoted so multi-flag strings (e.g. mac fat | |
| # archive flag) split into separate argv entries. | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DPINEFORGE_BUILD_TESTS=OFF \ | |
| -DPINEFORGE_BUILD_TUTORIAL=OFF \ | |
| -DCMAKE_INSTALL_PREFIX="${PWD}/${stage}" \ | |
| ${CMAKE_EXTRA} | |
| - name: Build | |
| run: cmake --build build -j 4 | |
| - name: Install to staging | |
| run: cmake --install build | |
| - name: Bundle docs into tarball root | |
| run: | | |
| set -euo pipefail | |
| cp LICENSE NOTICE VERSION "${STAGE_DIR}/" | |
| - name: Pack tarball + sha256 | |
| id: pack | |
| env: | |
| TAG: ${{ needs.release.outputs.tag }} | |
| TRIPLE: ${{ matrix.triple }} | |
| run: | | |
| set -euo pipefail | |
| base="pineforge-${TAG}-${TRIPLE}" | |
| tarball="${base}.tar.gz" | |
| tar -czf "${tarball}" -C stage "${base}" | |
| if command -v sha256sum >/dev/null; then | |
| sha256sum "${tarball}" > "${tarball}.sha256" | |
| else | |
| shasum -a 256 "${tarball}" > "${tarball}.sha256" | |
| fi | |
| echo "tarball=${tarball}" >> "$GITHUB_OUTPUT" | |
| echo "sha256=${tarball}.sha256" >> "$GITHUB_OUTPUT" | |
| - name: Upload to release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.release.outputs.tag }} | |
| TARBALL: ${{ steps.pack.outputs.tarball }} | |
| SHASUM: ${{ steps.pack.outputs.sha256 }} | |
| run: | | |
| gh release upload "${TAG}" "${TARBALL}" "${SHASUM}" --clobber | |
| # Notify the pineforge-release hub AFTER the tarballs are uploaded — the hub | |
| # builds FROM the engine static-lib tarball, so it must exist first. The hub | |
| # bumps its ENGINE_VERSION pin, rebuilds the combined image, and fans out to | |
| # the MCPs. FAIL LOUD (no continue-on-error): a dropped notify means downstream | |
| # never rebuilds. Credential is the org GitHub App (no PAT). | |
| notify-hub: | |
| needs: [release, prebuilt] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Mint org App token (hub dispatch) | |
| id: app | |
| uses: actions/create-github-app-token@v1 | |
| with: | |
| app-id: ${{ secrets.PINEFORGE_APP_ID }} | |
| private-key: ${{ secrets.PINEFORGE_APP_PRIVATE_KEY }} | |
| owner: pineforge-4pass | |
| repositories: pineforge-release | |
| - name: Dispatch engine-release -> pineforge-release | |
| env: | |
| GH_TOKEN: ${{ steps.app.outputs.token }} | |
| ENGINE_TAG: ${{ needs.release.outputs.tag }} | |
| RUN_ID: ${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| gh api repos/pineforge-4pass/pineforge-release/dispatches \ | |
| -f event_type=engine-release \ | |
| -F "client_payload[version]=${ENGINE_TAG}" \ | |
| -F "client_payload[run_id]=${RUN_ID}" | |
| echo "dispatched engine-release -> pineforge-release (${ENGINE_TAG})" |