Skip to content

Generics & variance, algebraic-effect handlers, and documentation com… #16

Generics & variance, algebraic-effect handlers, and documentation com…

Generics & variance, algebraic-effect handlers, and documentation com… #16

Workflow file for this run

name: Release
# Tag-triggered release. THIS is the only thing that publishes Osprey.
# Pushing a tag like v1.2.3 builds per-platform binaries, cuts a GitHub Release,
# updates the Homebrew tap + Scoop bucket, publishes the per-platform VSIX to the
# VS Code Marketplace, and (only on success) deploys the website + web compiler.
#
# Trigger rules (see docs/RELEASING.md):
# - release -> ONLY on tag push v*
# - CI -> ONLY on PR to main (ci.yml / ci-windows.yml)
# - merge to main -> nothing
#
# Version stamping ([SWR-VERSION-BUILD-STAMPING]): the version is derived from
# the tag and stamped into every artifact. Source stays at 0.0.0-dev.
#
# Cost gate ([SWR-REL-CHANGES-*]): the `scope` job diffs this tag against the
# previous one and gates the rest, so a website/benchmark-only tag deploys the
# site without rebuilding the macOS/Windows binary matrix or republishing
# brew/scoop/VSIX/Marketplace. Ruleset: .github/release-scope.json.
#
# Job shapes follow Shipwright's templates/gh-actions/*.yml (binary-multiplatform,
# publish-brew-tap, publish-scoop-bucket, publish-vsix-per-platform,
# release-change-detection).
on:
push:
tags: ['v*']
permissions:
contents: write
env:
# Stable identity used across formula/manifest/marketplace.
PRODUCT_REPO: Nimblesite/osprey
HOMEPAGE: https://ospreylang.dev
DESCRIPTION: "Osprey — a functional language with algebraic effects, fibers, and compile-time safety"
jobs:
# --------------------------------------------------------------------------
# Release scope (COST GATE) — diff this tag against the previous release tag and
# decide which surfaces must publish, so the expensive macOS/Windows build matrix
# only runs when the artifact it produces actually changed. Ruleset lives in
# .github/release-scope.json. Implements [SWR-REL-CHANGES-*]:
# binary changed -> full release (build matrix + every channel)
# vsix changed -> still build the matrix (the VSIX bundles a binary at the
# new tag version), publish the VSIX, skip the other channels
# website only -> deploy the site, skip the whole matrix
# unclassified path -> full release, fail-safe
scope:
name: Decide release scope
uses: ./.github/workflows/release-change-detection.yml
permissions:
contents: read
with:
# Pinned Shipwright commit the classifier is installed from [SWR-SEC-ACTION-PINNING].
shipwright_rev: 78f1455f27e8bfefc96e757a3eab23289be0e3f7
# --------------------------------------------------------------------------
version:
name: Resolve version from tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.v.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: vscode-extension/package-lock.json
- name: Install Shipwright validator dependencies
working-directory: vscode-extension
run: npm ci
- name: Derive version + validate manifest
id: v
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Releasing osprey $version"
# [SWR-VERSION-MANIFEST] validate shipwright.json before building anything.
npm run test:shipwright --prefix vscode-extension
# --------------------------------------------------------------------------
build:
name: Build ${{ matrix.platform }}
needs: [version, scope]
# The expensive native matrix runs only when an artifact that bundles a binary
# changed — a full release, or a vsix/jetbrains change (which must rebuild the
# binary at the new tag version). A website-only tag skips it. [SWR-REL-CHANGES-MATRIX]
if: ${{ needs.scope.outputs.build_matrix == 'true' }}
runs-on: ${{ matrix.os }}
# Windows is the newest, least-proven target — keep a win32 hiccup from
# blocking the mac/linux binaries + VSIX publish. A win32 failure shows as a
# failed leg (warning) but doesn't fail the build job.
continue-on-error: ${{ matrix.platform == 'win32-x64' }}
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
os: macos-14
# darwin-x64 (Intel mac) is dropped: GitHub is deprecating the free
# `macos-13` Intel runner and it no longer allocates (jobs queue
# indefinitely). Restore this leg once an Intel runner is available
# (paid large runner) or via an Apple-Silicon → x86_64 cross-compile.
- platform: linux-x64
os: ubuntu-latest
- platform: win32-x64
os: windows-latest
steps:
- uses: actions/checkout@v4
# ---- Linux + macOS: toolchain + C archives + stamped binary -------------
- name: Build (Linux/macOS)
if: runner.os != 'Windows'
uses: ./.github/actions/setup-osprey-compiler
with:
version: ${{ needs.version.outputs.version }}
# ---- Windows: MSYS2 (MinGW C runtime) + cargo -------------------------
- name: Install Rust (Windows)
if: runner.os == 'Windows'
uses: dtolnay/rust-toolchain@stable
- name: Setup MSYS2 (Windows)
if: runner.os == 'Windows'
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
path-type: inherit
install: >-
make
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-llvm
mingw-w64-ucrt-x86_64-pkgconf
mingw-w64-ucrt-x86_64-openssl
- name: Build (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
env:
OSPREY_VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
make _runtime CC=gcc AR=ar SHELL=/usr/bin/bash MKDIR='mkdir -p'
cargo build --release -p osprey-cli
# ---- Verify version contract [SWR-VERSION-CLI-OUTPUT] ------------------
- name: Verify version
shell: bash
run: |
set -euo pipefail
exe=""; [ "${{ runner.os }}" = "Windows" ] && exe=".exe"
"target/release/osprey${exe}" --version | grep -Fx "osprey ${{ needs.version.outputs.version }}"
# ---- Package: binary + runtime libs -> tar.gz + sha256 -----------------
- name: Package
shell: bash
run: |
set -euo pipefail
v="${{ needs.version.outputs.version }}"
plat="${{ matrix.platform }}"
exe=""; [ "${{ runner.os }}" = "Windows" ] && exe=".exe"
staging="osprey-$v-$plat"
mkdir -p "dist/$staging"
cp "target/release/osprey${exe}" "dist/$staging/"
cp compiler/lib/lib*.a "dist/$staging/" 2>/dev/null || cp compiler/bin/lib*.a "dist/$staging/" 2>/dev/null || true
tar -czf "dist/$staging.tar.gz" -C dist "$staging"
# macOS ships shasum (Perl) but not sha256sum; Windows Git Bash ships
# sha256sum but not shasum; Linux has both. Identical output format
# (`<hash> <file>`), so the downstream `cut -d' ' -f1` parsing in the
# brew/scoop jobs is unaffected.
sha256() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$@"; else sha256sum "$@"; fi; }
( cd dist && sha256 "$staging.tar.gz" | tee "$staging.tar.gz.sha256" )
- uses: actions/upload-artifact@v4
with:
name: osprey-${{ matrix.platform }}
path: dist/*.tar.gz*
# --------------------------------------------------------------------------
github-release:
name: Publish GitHub Release
needs: [version, build, scope]
# The standalone binary GitHub Release (and the brew/scoop jobs that depend on
# it) only ship on a full release. [SWR-REL-CHANGES-CASCADE]
if: ${{ needs.scope.outputs.full == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.version.outputs.version }}
files: dist/*.tar.gz
fail_on_unmatched_files: true
# --------------------------------------------------------------------------
# Token preflight — brew/scoop (BREW_SCOOP_PAT) and Open VSX (OPEN_VSX_PAT) use
# standing tokens that can't be minted from OIDC. Probe them once so those jobs
# SKIP cleanly when the token isn't configured (the Marketplace OIDC publish +
# binaries + GitHub Release + site still ship). Secrets aren't readable in a
# job-level `if:`, so we surface their presence as booleans here. Add the
# secrets later and re-run the skipped jobs — no re-tag needed.
preflight:
name: Probe optional publish tokens
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
has_brew: ${{ steps.probe.outputs.has_brew }}
has_ovsx: ${{ steps.probe.outputs.has_ovsx }}
steps:
- id: probe
env:
BREW_SCOOP_PAT: ${{ secrets.BREW_SCOOP_PAT }}
OPEN_VSX_PAT: ${{ secrets.OPEN_VSX_PAT }}
run: |
set -euo pipefail
if [ -n "${BREW_SCOOP_PAT:-}" ]; then echo "has_brew=true" >> "$GITHUB_OUTPUT"; else echo "has_brew=false" >> "$GITHUB_OUTPUT"; echo "::warning::BREW_SCOOP_PAT not set — Homebrew + Scoop publish will be skipped"; fi
if [ -n "${OPEN_VSX_PAT:-}" ]; then echo "has_ovsx=true" >> "$GITHUB_OUTPUT"; else echo "has_ovsx=false" >> "$GITHUB_OUTPUT"; echo "::warning::OPEN_VSX_PAT not set — Open VSX publish will be skipped"; fi
# --------------------------------------------------------------------------
# Homebrew tap — shape from templates/gh-actions/publish-brew-tap.yml, adapted
# to install the runtime libs and depend on llvm (osprey shells out to it).
brew:
name: Publish Homebrew formula
needs: [version, github-release, preflight]
if: ${{ needs.preflight.outputs.has_brew == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { path: dist, merge-multiple: true }
- uses: actions/checkout@v4
with:
repository: Nimblesite/homebrew-tap
token: ${{ secrets.BREW_SCOOP_PAT }}
path: tap
- name: Write formula
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
base="https://github.com/${PRODUCT_REPO}/releases/download/v${VERSION}"
sha() { cut -d' ' -f1 "dist/osprey-${VERSION}-$1.tar.gz.sha256"; }
arm=$(sha darwin-arm64); linux=$(sha linux-x64)
mkdir -p tap/Formula
cat > tap/Formula/osprey.rb <<RUBY
# typed: false
# frozen_string_literal: true
class Osprey < Formula
desc "${DESCRIPTION}"
homepage "${HOMEPAGE}"
version "${VERSION}"
depends_on "llvm"
on_macos do
# Apple Silicon only — no Intel mac build (see release.yml matrix).
on_arm do
url "${base}/osprey-${VERSION}-darwin-arm64.tar.gz"
sha256 "${arm}"
end
end
on_linux do
url "${base}/osprey-${VERSION}-linux-x64.tar.gz"
sha256 "${linux}"
end
def install
bin.install "osprey"
lib.install Dir["lib*.a"]
end
test do
assert_match "osprey ${VERSION}", shell_output("#{bin}/osprey --version")
end
end
RUBY
- name: Commit + push
working-directory: tap
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/osprey.rb
git commit -m "osprey ${{ needs.version.outputs.version }}" || exit 0
git push
# --------------------------------------------------------------------------
# Scoop bucket — shape from templates/gh-actions/publish-scoop-bucket.yml.
# Windows install depends on llvm + gcc (MinGW) since osprey shells out to
# llc/clang/gcc at compile time. [WINDOWS-PORT]
scoop:
name: Publish Scoop manifest
needs: [version, github-release, preflight]
if: ${{ needs.preflight.outputs.has_brew == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { path: dist, merge-multiple: true }
- uses: actions/checkout@v4
with:
repository: Nimblesite/scoop-bucket
token: ${{ secrets.BREW_SCOOP_PAT }}
path: bucket
- uses: actions/setup-node@v4
with: { node-version: '20' }
- name: Write manifest
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
HASH=$(cut -d' ' -f1 "dist/osprey-${VERSION}-win32-x64.tar.gz.sha256")
URL="https://github.com/${PRODUCT_REPO}/releases/download/v${VERSION}/osprey-${VERSION}-win32-x64.tar.gz"
node - "$URL" "$HASH" <<'NODE'
const fs = require("node:fs");
const [, , url, hash] = process.argv;
const v = process.env.VERSION;
const manifest = {
version: v,
description: process.env.DESCRIPTION,
homepage: process.env.HOMEPAGE,
license: "MIT",
depends: ["llvm", "gcc"],
architecture: { "64bit": { url, hash, extract_dir: `osprey-${v}-win32-x64` } },
bin: "osprey.exe",
checkver: { github: `https://github.com/${process.env.PRODUCT_REPO}` },
autoupdate: {
architecture: {
"64bit": {
url: `https://github.com/${process.env.PRODUCT_REPO}/releases/download/v$version/osprey-$version-win32-x64.tar.gz`,
extract_dir: "osprey-$version-win32-x64",
},
},
},
};
fs.mkdirSync("bucket/bucket", { recursive: true });
fs.writeFileSync("bucket/bucket/osprey.json", JSON.stringify(manifest, null, 2) + "\n");
NODE
- name: Commit + push
working-directory: bucket
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add bucket/osprey.json
git commit -m "osprey ${{ needs.version.outputs.version }}" || exit 0
git push
# --------------------------------------------------------------------------
# Per-platform VSIX — shape from templates/gh-actions/publish-vsix-per-platform.yml
# (which follows the official microsoft/vscode-platform-specific-sample). The
# matching osprey binary + runtime libs + shipwright.json are bundled so the
# extension launches a version-matched compiler verified by @nimblesite/shipwright-vscode.
vsix:
name: Publish VSIX ${{ matrix.target }}
needs: [version, build, scope]
# Package the per-platform VSIX only when the extension changed (or a full
# release). build_matrix is guaranteed true here, so the bundled binary is
# rebuilt at the new tag version. [SWR-REL-CHANGES-MATRIX]
if: ${{ needs.scope.outputs.vsix == 'true' }}
runs-on: ${{ matrix.os }}
# Match the build job: a win32 VSIX hiccup must not block the mac/linux
# VSIXes from reaching the Marketplace.
continue-on-error: ${{ matrix.target == 'win32-x64' }}
strategy:
fail-fast: false
matrix:
include:
- { os: macos-14, target: darwin-arm64, artifact: darwin-arm64 }
# darwin-x64 dropped — see the build matrix note (no Intel runner).
- { os: ubuntu-latest, target: linux-x64, artifact: linux-x64 }
- { os: windows-latest, target: win32-x64, artifact: win32-x64 }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- uses: actions/download-artifact@v4
with:
name: osprey-${{ matrix.artifact }}
path: dist
- name: Stage bundled compiler + manifest
shell: bash
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
tar -xzf "dist/osprey-${VERSION}-${{ matrix.target }}.tar.gz" -C dist
dest="vscode-extension/bin/${{ matrix.target }}"
mkdir -p "$dest"
cp -r "dist/osprey-${VERSION}-${{ matrix.target }}/." "$dest/"
# Stamp + bundle shipwright.json so the extension can version-check the
# bundled binary at activation. [SWR-VERSION-MANIFEST]
node -e '
const fs=require("fs"); const v=process.env.VERSION;
const m=JSON.parse(fs.readFileSync("shipwright.json","utf8"));
m.product.version=v;
for (const c of m.components) if (c.expectedVersion) c.expectedVersion=v;
fs.writeFileSync("vscode-extension/shipwright.json", JSON.stringify(m,null,2)+"\n");
'
- name: Install deps + stamp package.json
working-directory: vscode-extension
run: |
npm install
npm version "${{ needs.version.outputs.version }}" --no-git-tag-version --allow-same-version
npm run compile
- name: Package VSIX
working-directory: vscode-extension
run: npx vsce package --target ${{ matrix.target }}
- name: Verify bundled binary is inside the VSIX
shell: bash
working-directory: vscode-extension
run: |
set -euo pipefail
exe=""; [ "${{ runner.os }}" = "Windows" ] && exe=".exe"
unzip -l *.vsix | grep -F "extension/bin/${{ matrix.target }}/osprey${exe}"
# Packaging is per-platform (each VSIX bundles the matching native binary);
# publishing is centralised in the publish-marketplace / publish-openvsx
# jobs so the registry tokens live in one least-privilege place.
- name: Upload VSIX artifact
uses: actions/upload-artifact@v4
with:
name: osprey-vsix-${{ matrix.target }}
path: vscode-extension/*.vsix
if-no-files-found: error
retention-days: 7
# --------------------------------------------------------------------------
# Marketplace publish — passwordless via Microsoft Entra ID OIDC. The `release`
# environment gives the GitHub OIDC subject the stable
# repo:Nimblesite/osprey:environment:release shape that the shared
# `Nimblesite-VSCode-Marketplace` app's federated credential trusts; the
# short-lived Marketplace token is minted from that session and handed to vsce
# via VSCE_PAT — no PAT is stored. AZURE_CLIENT_ID + AZURE_TENANT_ID are the
# non-sensitive identifiers of the shared app/tenant. See
# Nimblesite/NimblesiteDeployment docs/vscode-marketplace-oidc.md.
publish-marketplace:
name: Publish VSIX to VS Code Marketplace
needs: [version, vsix]
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release
permissions:
contents: read
id-token: write
steps:
- uses: actions/setup-node@v4
with: { node-version: '20' }
- uses: actions/download-artifact@v4
with:
pattern: osprey-vsix-*
path: artifacts
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
allow-no-subscriptions: true
# One `vsce publish` per platform-specific VSIX — vsce silently uses only
# the first when several are globbed into a single call. A hyphenated tag
# (v1.2.3-rc.1) is a prerelease and gets --pre-release. The Marketplace
# token is minted from the Entra OIDC session via az; the resource GUID is
# the immutable first-party app id vsce authenticates against.
- name: Publish each platform VSIX (Entra OIDC, no PAT)
run: |
set -euo pipefail
shopt -s globstar nullglob
flag=""
if [[ "${GITHUB_REF_NAME}" == *-* ]]; then
flag="--pre-release"
echo "Prerelease tag ${GITHUB_REF_NAME}; publishing with --pre-release"
fi
VSCE_PAT="$(az account get-access-token \
--resource 499b84ac-1321-427f-aa17-267ca6975798 \
--query accessToken -o tsv)"
echo "::add-mask::${VSCE_PAT}"
export VSCE_PAT
published=0
for vsix in artifacts/**/*.vsix; do
echo "Publishing ${vsix}"
npx --yes @vscode/vsce@3.9.2 publish --skip-duplicate ${flag} --packagePath "${vsix}"
published=$((published + 1))
done
if [ "${published}" -eq 0 ]; then
echo "::error::no VSIX artifacts found to publish"
exit 1
fi
echo "Published ${published} VSIX(es) to the Marketplace"
# --------------------------------------------------------------------------
# Open VSX publish — serves the VS Code forks (Cursor, Windsurf, …). Open VSX
# has no OIDC trusted-publishing path today, so this uses an Open VSX token
# scoped to the protected `release` environment. Independent of the Marketplace
# publish so one registry hiccup never blocks the other.
publish-openvsx:
name: Publish VSIX to Open VSX
needs: [version, vsix, preflight]
if: ${{ needs.preflight.outputs.has_ovsx == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
environment: release
permissions:
contents: read
steps:
- uses: actions/setup-node@v4
with: { node-version: '20' }
- uses: actions/download-artifact@v4
with:
pattern: osprey-vsix-*
path: artifacts
- name: Publish each platform VSIX to Open VSX
env:
OVSX_PAT: ${{ secrets.OPEN_VSX_PAT }}
run: |
set -euo pipefail
if [ -z "${OVSX_PAT:-}" ]; then
echo "::error::OPEN_VSX_PAT secret is not set; cannot publish to Open VSX"
exit 1
fi
shopt -s globstar nullglob
flag=""
if [[ "${GITHUB_REF_NAME}" == *-* ]]; then
flag="--pre-release"
echo "Prerelease tag ${GITHUB_REF_NAME}; publishing with --pre-release"
fi
published=0
for vsix in artifacts/**/*.vsix; do
echo "Publishing ${vsix}"
npx --yes ovsx@1.0.0 publish --skip-duplicate ${flag} --packagePath "${vsix}"
published=$((published + 1))
done
if [ "${published}" -eq 0 ]; then
echo "::error::no VSIX artifacts found to publish"
exit 1
fi
echo "Published ${published} VSIX(es) to Open VSX"
# --------------------------------------------------------------------------
# Website deploy — the live site updates ONLY as part of a release (never on a
# push to main). Decoupled from the binary release: it deploys whenever the
# website surface changed (full release, or a website-only tag), so benchmark /
# docs updates ship without rebuilding the whole binary matrix. [SWR-REL-CHANGES]
# Stable tags only: a SemVer prerelease tag (v1.2.3-rc.1) contains a hyphen and
# is skipped, so a prerelease never overwrites the live site.
deploy-site:
name: Deploy website to GitHub Pages
needs: [scope]
if: ${{ needs.scope.outputs.website == 'true' && !contains(github.ref_name, '-') }}
uses: ./.github/workflows/deploy-pages.yml
permissions:
contents: read
pages: write
id-token: write
deploy-webcompiler:
name: Deploy web compiler
needs: [github-release]
# Non-blocking: the browser compiler on Fly.io is an optional add-on, not a
# release artifact. The deploy step itself is `continue-on-error` inside
# deploy-webcompiler.yml (continue-on-error is not valid on a `uses:` job),
# so a Fly outage or invalid/banned FLY_API_TOKEN can't fail the release.
uses: ./.github/workflows/deploy-webcompiler.yml
secrets: inherit