Release #6
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 | |
| # This workflow runs ON the public repo (OpenAnalystInc/cli) so it | |
| # uses unlimited free GitHub Actions minutes. The CLI source lives | |
| # in the private repo (OpenAnalystInc/openanalyst-cli); the workflow | |
| # clones it into the runner's ephemeral filesystem with a PAT | |
| # (PRIVATE_REPO_TOKEN secret), builds, attaches binaries to a | |
| # release here, and publishes to npm. Source never persists in the | |
| # public repo — the runner is destroyed after each job. | |
| # | |
| # Trigger options: | |
| # 1. `workflow_dispatch` from the Actions tab — supply the version | |
| # manually. Use this for the first publish, or any time you | |
| # want to rebuild a specific tag. | |
| # 2. Push a `v*` tag to this public repo — re-runs the same flow | |
| # for that tag. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to release (e.g. 2.0.31)' | |
| required: true | |
| publish_npm: | |
| description: 'Publish to npm as well?' | |
| type: boolean | |
| default: true | |
| required: false | |
| push: | |
| tags: | |
| - 'v*' | |
| permissions: | |
| contents: write | |
| env: | |
| CARGO_TERM_COLOR: always | |
| NODE_VERSION: "20" | |
| PRIVATE_REPO: OpenAnalystInc/openanalyst-cli | |
| jobs: | |
| resolve: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.pick.outputs.version }} | |
| publish_npm: ${{ steps.pick.outputs.publish_npm }} | |
| steps: | |
| - id: pick | |
| shell: bash | |
| run: | | |
| if [ -n "${{ inputs.version }}" ]; then | |
| echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" | |
| else | |
| TAG="${GITHUB_REF_NAME}" | |
| echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" | |
| fi | |
| if [ "${{ inputs.publish_npm }}" = "false" ]; then | |
| echo "publish_npm=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "publish_npm=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| build: | |
| needs: resolve | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| binary: openanalyst | |
| - os: ubuntu-latest | |
| target: aarch64-unknown-linux-gnu | |
| binary: openanalyst | |
| cross: true | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| binary: openanalyst | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| binary: openanalyst | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| binary: openanalyst.exe | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| # Clone the private repo (the source of truth). The PAT lives | |
| # in PRIVATE_REPO_TOKEN — set it once at repo-secret level on | |
| # this public repo. | |
| - uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ env.PRIVATE_REPO }} | |
| token: ${{ secrets.PRIVATE_REPO_TOKEN }} | |
| ref: v${{ needs.resolve.outputs.version }} | |
| path: source | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Install cross | |
| if: matrix.cross | |
| run: cargo install cross --locked | |
| - name: Build native target | |
| if: ${{ !matrix.cross }} | |
| working-directory: source/rust | |
| run: cargo build --release --target ${{ matrix.target }} -p openanalyst-cli | |
| - name: Build cross target | |
| if: matrix.cross | |
| working-directory: source/rust | |
| run: cross build --release --target ${{ matrix.target }} -p openanalyst-cli | |
| - name: Package binary | |
| shell: bash | |
| run: | | |
| mkdir -p dist | |
| SRC="source/rust/target/${{ matrix.target }}/release/${{ matrix.binary }}" | |
| EXT="" | |
| if [ "${{ endsWith(matrix.binary, '.exe') }}" = "true" ]; then EXT=".exe"; fi | |
| cp "$SRC" "dist/openanalyst-${{ matrix.target }}${EXT}" | |
| - uses: actions/upload-artifact@v4 | |
| with: | |
| name: binary-${{ matrix.target }} | |
| path: dist/* | |
| retention-days: 1 | |
| release: | |
| needs: | |
| - resolve | |
| - build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| # Hard guard: refuse to upload anything that isn't a compiled | |
| # binary. Source files (.rs, .ts, .tsx, .py, .toml) MUST NOT | |
| # appear in the artifact tree. If any do, abort the workflow | |
| # before the release upload step can run. | |
| - name: Guard against source leak in release artifacts | |
| shell: bash | |
| run: | | |
| set -e | |
| echo "Files staged for release upload:" | |
| find artifacts -type f | |
| echo "---" | |
| LEAKS=$(find artifacts -type f \ | |
| -not -path '*/managed/*' \ | |
| \( -name '*.rs' -o -name '*.tsx' -o -name '*.py' \ | |
| -o -name '*.toml' -o -name '*.lock' \ | |
| -o -name '*.map' \ | |
| -o \( -name '*.ts' ! -name '*.d.ts' \) \) || true) | |
| if [ -n "$LEAKS" ]; then | |
| echo "FATAL: source-shaped files detected in release artifacts:" | |
| echo "$LEAKS" | |
| exit 1 | |
| fi | |
| echo "OK — only binary artifacts present." | |
| - name: Create or update the release on this (public) repo | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| TAG="v${{ needs.resolve.outputs.version }}" | |
| gh release view "$TAG" >/dev/null 2>&1 \ | |
| || gh release create "$TAG" \ | |
| --title "OpenAnalyst CLI $TAG" \ | |
| --generate-notes | |
| # Upload (or replace) every binary | |
| find artifacts -type f -print0 \ | |
| | xargs -0 -I{} gh release upload "$TAG" "{}" --clobber | |
| publish_npm: | |
| needs: | |
| - resolve | |
| - release | |
| if: needs.resolve.outputs.publish_npm == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ env.PRIVATE_REPO }} | |
| token: ${{ secrets.PRIVATE_REPO_TOKEN }} | |
| ref: v${{ needs.resolve.outputs.version }} | |
| path: source | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| registry-url: https://registry.npmjs.org/ | |
| - name: Install dependencies | |
| working-directory: source | |
| run: npm ci | |
| - name: Build Ink TUI | |
| working-directory: source | |
| run: npm run build --prefix ink-tui | |
| # Hard guard: parse `npm pack --dry-run --json` and fail the | |
| # workflow before `npm publish` if any file in the tarball | |
| # looks like source. The `.npmignore` already filters source, | |
| # but this re-checks at publish time so even an accidental | |
| # `.npmignore` regression can't quietly ship source. | |
| - name: Guard against source leak in npm tarball | |
| working-directory: source | |
| shell: bash | |
| run: | | |
| set -e | |
| npm pack --dry-run --json > /tmp/npm-pack.json | |
| echo "Tarball file count:" | |
| node -e 'const p = require("/tmp/npm-pack.json"); console.log(p[0].files.length, "files,", p[0].size, "bytes")' | |
| node <<'EOF' | |
| const fs = require('fs'); | |
| const pack = JSON.parse(fs.readFileSync('/tmp/npm-pack.json', 'utf8')); | |
| const files = (pack[0]?.files ?? []).map(f => f.path); | |
| const forbidden = files.filter(f => { | |
| // Allow-list: managed/ is the user-facing skill | |
| // catalog (e.g. managed/skills/.../*.py helpers | |
| // that ship as runnable skill artifacts, NOT CLI | |
| // source). Never flag anything under managed/. | |
| if (f.startsWith('managed/')) return false; | |
| return f.endsWith('.rs') | |
| || f.endsWith('.tsx') | |
| || f.endsWith('.py') | |
| || f.endsWith('.toml') | |
| || f.endsWith('.lock') | |
| || f.endsWith('.map') | |
| || f.startsWith('rust/') | |
| || f.startsWith('backend/') | |
| || f.startsWith('AgenticRAG/') | |
| || f.startsWith('knowledgeBase/') | |
| || f.startsWith('Dev/') | |
| || f.startsWith('src/') | |
| || f.startsWith('ink-tui/src/') | |
| || (f.endsWith('.ts') && !f.endsWith('.d.ts')); | |
| }); | |
| if (forbidden.length > 0) { | |
| console.error('FATAL: source-shaped files detected in npm tarball:'); | |
| forbidden.forEach(f => console.error(' ' + f)); | |
| process.exit(1); | |
| } | |
| console.log('OK — no source files in tarball.'); | |
| EOF | |
| - name: Publish to npm | |
| working-directory: source | |
| run: npm publish --access public | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| cleanup_old_releases: | |
| needs: | |
| - release | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Delete public releases older than the latest 3 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| # Keep latest 3 releases; delete the rest. Cleans up | |
| # release storage so it doesn't grow unbounded across | |
| # many version bumps. | |
| gh release list --limit 100 --repo ${{ github.repository }} \ | |
| | tail -n +4 \ | |
| | awk '{print $1}' \ | |
| | while read -r old_tag; do | |
| if [ -n "$old_tag" ]; then | |
| echo "Deleting old release $old_tag" | |
| gh release delete "$old_tag" --repo ${{ github.repository }} \ | |
| --yes --cleanup-tag 2>/dev/null || true | |
| fi | |
| done |