Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .agents/skills/cutting-releases/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Cuts Iris GitHub releases through the version-bump PR, merge commi

# Cutting Iris Releases

Use this workflow for `purescript-iris` releases. A pushed `v*` tag triggers `.github/workflows/release.yml`, which creates the GitHub release, builds and attests four archives, and tests the installers on Linux, macOS, and Windows.
Use this workflow for stable `purescript-iris` releases. A pushed stable `v*` tag triggers `.github/workflows/release.yml`, which creates the GitHub release, builds and attests four archives, and tests the installers on Linux, macOS, and Windows. Automated `v<version>-dev.<revision>` canaries are owned by `.github/workflows/canary.yml`; do not use this manual workflow to prepare or repair them.

Merging and pushing the release tag are shared, high-impact actions. Obtain explicit approval before each unless the user has already authorized that stage. Never move or delete a published release tag to repair a failed workflow.

Expand All @@ -19,12 +19,13 @@ git fetch origin main --tags
gh auth status
```

Set the requested version without the `v` prefix. Determine the previous release from the repository rather than assuming it:
Set the requested version without the `v` prefix. Determine the previous stable release from GitHub rather than assuming it or selecting an automated canary tag:

```bash
version=0.0.16
tag="v$version"
previous_tag=$(git tag --list 'v[0-9]*' --sort=-version:refname | head -1)
previous_tag=$(gh release list --exclude-drafts --exclude-pre-releases \
--limit 1 --json tagName --jq '.[0].tagName')
printf 'Release range: %s...%s\n' "$previous_tag" "$tag"
```

Expand Down
186 changes: 186 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
name: Canary Release

on:
workflow_run:
workflows:
- Cargo Build & Test
branches:
- main
types:
- completed

permissions: {}

concurrency:
group: canary-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false

jobs:
prepare:
name: Prepare canary release
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
revision: ${{ steps.canary.outputs.revision }}
tag: ${{ steps.canary.outputs.tag }}
version: ${{ steps.canary.outputs.version }}

steps:
- name: Checkout successful revision
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

- name: Derive canary version
id: canary
env:
COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
package_version=$(cargo metadata --format-version 1 --no-deps |
jq -r '.packages[] | select(.name == "purescript-iris") | .version')
if [[ ! "$package_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Package version must be a stable semantic version, found $package_version." >&2
exit 1
fi

revision=${COMMIT_SHA:0:12}
version="$package_version-dev.$revision"
echo "revision=$revision" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"

create-release:
name: Create draft canary release
needs: prepare
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Create draft release
env:
COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.prepare.outputs.tag }}
run: |
if release=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
--json isDraft,targetCommitish); then
if [[ $(jq -r '.isDraft' <<< "$release") != true ]]; then
echo "Canary release $TAG is already published and cannot be rebuilt." >&2
exit 1
fi
if [[ $(jq -r '.targetCommitish' <<< "$release") != "$COMMIT_SHA" ]]; then
echo "Draft canary release $TAG targets a different commit." >&2
exit 1
fi
echo "Reusing draft canary release $TAG."
exit 0
fi

gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--target "$COMMIT_SHA" \
--title "$TAG" \
--notes "Automated canary build of commit \`$COMMIT_SHA\`." \
--draft \
--prerelease \
--latest=false

build-and-upload:
name: Build canary
needs:
- prepare
- create-release
runs-on: ${{ matrix.os }}
permissions:
attestations: write
contents: write
id-token: write
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu

- os: ubuntu-latest
target: x86_64-unknown-linux-musl

- os: macos-latest
target: universal-apple-darwin

- os: windows-latest
target: x86_64-pc-windows-msvc

steps:
- name: Checkout successful revision
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

- name: Install cross-compilation tools
uses: taiki-e/setup-cross-toolchain-action@v1
if: startsWith(matrix.os, 'ubuntu')
with:
target: ${{ matrix.target }}

- name: Build and upload archive
id: release
uses: taiki-e/upload-rust-binary-action@v1
env:
IRIS_BUILD_REVISION: ${{ needs.prepare.outputs.revision }}
with:
bin: iris
package: purescript-iris
archive: iris-$target
include: README.md,LICENSE,ACKNOWLEDGEMENTS.md,THIRDPARTY.toml
leading-dir: true
locked: true
ref: refs/tags/${{ needs.prepare.outputs.tag }}
target: ${{ matrix.target }}
token: ${{ secrets.GITHUB_TOKEN }}

- name: Attest canary archive
uses: actions/attest-build-provenance@v4
with:
subject-path: ${{ steps.release.outputs.tar || steps.release.outputs.zip }}

publish-release:
name: Publish canary release
needs:
- prepare
- build-and-upload
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.prepare.outputs.tag }}
run: >-
gh release edit "$TAG"
--repo "$GITHUB_REPOSITORY"
--draft=false
--prerelease

test-installers:
name: Test canary installers
needs:
- prepare
- publish-release
permissions:
attestations: read
contents: read
uses: ./.github/workflows/installers.yml
with:
revision: ${{ github.event.workflow_run.head_sha }}
version: ${{ needs.prepare.outputs.tag }}
22 changes: 20 additions & 2 deletions .github/workflows/installers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
description: Release tag to install
required: true
type: string
revision:
description: Repository revision containing the installer
required: false
type: string
workflow_dispatch:
inputs:
version:
Expand Down Expand Up @@ -35,6 +39,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
ref: ${{ inputs.revision }}

- name: Require GitHub attestation verification
shell: bash
Expand All @@ -47,7 +53,12 @@ jobs:
IRIS_VERSION: ${{ inputs.version }}
run: |
sh ./install.sh
"$IRIS_INSTALL_DIR/iris" --version
installed_version=$("$IRIS_INSTALL_DIR/iris" --version)
printf 'Installed %s\n' "$installed_version"
if [ "$IRIS_VERSION" != latest ] && [ "$installed_version" != "iris ${IRIS_VERSION#v}" ]; then
printf 'Expected iris %s, found %s\n' "${IRIS_VERSION#v}" "$installed_version" >&2
exit 1
fi
test ! -e "$IRIS_INSTALL_DIR/purescript-analyzer"
test ! -e "$IRIS_INSTALL_DIR/purescript-iris"

Expand All @@ -59,8 +70,15 @@ jobs:
IRIS_VERSION: ${{ inputs.version }}
run: |
& ./install.ps1
& "$env:IRIS_INSTALL_DIR\iris.exe" --version
$InstalledVersion = & "$env:IRIS_INSTALL_DIR\iris.exe" --version
if ($LASTEXITCODE -ne 0) { throw "iris --version failed" }
Write-Host "Installed $InstalledVersion"
if ($env:IRIS_VERSION -ne "latest") {
$ExpectedVersion = "iris " + $env:IRIS_VERSION.Substring(1)
if ($InstalledVersion -ne $ExpectedVersion) {
throw "Expected $ExpectedVersion, found $InstalledVersion"
}
}
foreach ($LegacyBinary in @("purescript-analyzer.exe", "purescript-iris.exe")) {
if (Test-Path "$env:IRIS_INSTALL_DIR\$LegacyBinary") {
throw "Installer created legacy executable $LegacyBinary"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ name: Cargo Build & Release
on:
push:
tags:
- v[0-9]+.*
- 'v[0-9]+.*'
- '!v[0-9]+.*-dev.*'

jobs:
create-release:
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,13 @@ The installers verify the release's GitHub build-provenance attestation when
installed. These installers require v0.1.0 or later; to install v0.0.x, use the installer from that
release's Git tag. Set `IRIS_VERSION` to a release tag or
`IRIS_INSTALL_DIR` to an installation directory to override the defaults.

Successful builds of the `main` branch are published as GitHub prereleases tagged
`v<version>-dev.<revision>`. Consumers testing against the canary channel should resolve the newest
published, non-draft prerelease and pass its exact tag through `IRIS_VERSION`. Stable installations
continue to use GitHub's latest release.

Iris keeps its package version separate from source provenance. Packagers can set
`IRIS_BUILD_REVISION` to a Git revision when invoking Cargo to include that revision in the reported
CLI and language-server versions. The value is read at compile time; builds that omit it report the
version from `compiler-bin/Cargo.toml` unchanged.
22 changes: 22 additions & 0 deletions compiler-bin/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::env;

fn main() {
println!("cargo::rerun-if-env-changed=IRIS_BUILD_REVISION");

let package_version =
env::var("CARGO_PKG_VERSION").expect("Cargo must provide package version");
let version = match env::var("IRIS_BUILD_REVISION") {
Ok(revision) => {
assert!(
(7..=64).contains(&revision.len())
&& revision.bytes().all(|byte| byte.is_ascii_hexdigit()),
"IRIS_BUILD_REVISION must contain 7 to 64 hexadecimal characters"
);
format!("{package_version}-dev.{}", revision.to_ascii_lowercase())
}
Err(env::VarError::NotPresent) => package_version,
Err(env::VarError::NotUnicode(_)) => panic!("IRIS_BUILD_REVISION must be Unicode"),
};

println!("cargo::rustc-env=IRIS_VERSION={version}");
}
2 changes: 1 addition & 1 deletion compiler-bin/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ fn absolute_path(value: PathBuf) -> io::Result<PathBuf> {
#[usage(
bin = "iris",
about = env!("CARGO_PKG_DESCRIPTION"),
version,
version = crate::VERSION,
unknown_flags = "error",
args_override_self = false
)]
Expand Down
2 changes: 1 addition & 1 deletion compiler-bin/src/cli/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl Program {
std::process::exit(0);
}
usage::Error::Version { .. } => {
println!("iris {}", env!("CARGO_PKG_VERSION"));
println!("iris {}", crate::VERSION);
std::process::exit(0);
}
usage::Error::MissingArgsHelp { cmd } => {
Expand Down
3 changes: 3 additions & 0 deletions compiler-bin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub mod walk;
mod watch;
mod workspace;

pub(crate) const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
pub(crate) const VERSION: &str = env!("IRIS_VERSION");

pub fn run() {
let program = cli::Program::parse_with_diagnostics();

Expand Down
7 changes: 2 additions & 5 deletions compiler-bin/src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,6 @@ impl AnalyzerHost for LspAnalyzerHost<'_> {
}
}

const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
const PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");

fn initialize(
state: &mut State,
p: extension::CustomInitializeParams,
Expand All @@ -254,8 +251,8 @@ fn initialize(
async move {
Ok(InitializeResult {
server_info: Some(ServerInfo {
name: PACKAGE_NAME.to_string(),
version: Some(PACKAGE_VERSION.to_string()),
name: crate::PACKAGE_NAME.to_owned(),
version: Some(crate::VERSION.to_owned()),
}),
capabilities: ServerCapabilities {
completion_provider: Some(CompletionOptions {
Expand Down