From 6b257c376a8ff388a3a8f25f1d2af96c17939a23 Mon Sep 17 00:00:00 2001 From: Justin Garcia Date: Sat, 12 Sep 2026 06:09:15 +0000 Subject: [PATCH] Publish canary builds from main Amp-Thread-ID: https://ampcode.com/threads/T-01a093ea-0f35-701b-a273-3488e1eea068 Co-authored-by: Amp --- .agents/skills/cutting-releases/SKILL.md | 7 +- .github/workflows/canary.yml | 186 +++++++++++++++++++++++ .github/workflows/installers.yml | 22 ++- .github/workflows/release.yml | 3 +- README.md | 10 ++ compiler-bin/build.rs | 22 +++ compiler-bin/src/cli.rs | 2 +- compiler-bin/src/cli/diagnostic.rs | 2 +- compiler-bin/src/lib.rs | 3 + compiler-bin/src/lsp.rs | 7 +- 10 files changed, 251 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/canary.yml create mode 100644 compiler-bin/build.rs diff --git a/.agents/skills/cutting-releases/SKILL.md b/.agents/skills/cutting-releases/SKILL.md index 80391e21a..62eb0d3b1 100644 --- a/.agents/skills/cutting-releases/SKILL.md +++ b/.agents/skills/cutting-releases/SKILL.md @@ -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-dev.` 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. @@ -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" ``` diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 000000000..8984254f0 --- /dev/null +++ b/.github/workflows/canary.yml @@ -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 }} diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml index e92527289..4f3110e3c 100644 --- a/.github/workflows/installers.yml +++ b/.github/workflows/installers.yml @@ -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: @@ -35,6 +39,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7 + with: + ref: ${{ inputs.revision }} - name: Require GitHub attestation verification shell: bash @@ -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" @@ -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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19aac0f1a..158f5eb25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,8 @@ name: Cargo Build & Release on: push: tags: - - v[0-9]+.* + - 'v[0-9]+.*' + - '!v[0-9]+.*-dev.*' jobs: create-release: diff --git a/README.md b/README.md index bf0d7923f..c0d0e57c8 100644 --- a/README.md +++ b/README.md @@ -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-dev.`. 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. diff --git a/compiler-bin/build.rs b/compiler-bin/build.rs new file mode 100644 index 000000000..8fe5a5ae8 --- /dev/null +++ b/compiler-bin/build.rs @@ -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}"); +} diff --git a/compiler-bin/src/cli.rs b/compiler-bin/src/cli.rs index ae8e9a0c7..e88b5604f 100644 --- a/compiler-bin/src/cli.rs +++ b/compiler-bin/src/cli.rs @@ -32,7 +32,7 @@ fn absolute_path(value: PathBuf) -> io::Result { #[usage( bin = "iris", about = env!("CARGO_PKG_DESCRIPTION"), - version, + version = crate::VERSION, unknown_flags = "error", args_override_self = false )] diff --git a/compiler-bin/src/cli/diagnostic.rs b/compiler-bin/src/cli/diagnostic.rs index 5b874529d..c983239e9 100644 --- a/compiler-bin/src/cli/diagnostic.rs +++ b/compiler-bin/src/cli/diagnostic.rs @@ -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 } => { diff --git a/compiler-bin/src/lib.rs b/compiler-bin/src/lib.rs index 451dbdaa7..3b94d01e7 100644 --- a/compiler-bin/src/lib.rs +++ b/compiler-bin/src/lib.rs @@ -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(); diff --git a/compiler-bin/src/lsp.rs b/compiler-bin/src/lsp.rs index b7ac76f10..1ab67ec0f 100644 --- a/compiler-bin/src/lsp.rs +++ b/compiler-bin/src/lsp.rs @@ -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, @@ -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 {