diff --git a/.github/settings.yml b/.github/settings.yml index a1cc85990c8..e148dddb4fd 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -215,6 +215,7 @@ branches: - "Build and Test (ubuntu-18.04)" - "Build and Test (windows-latest)" - "Docs Check" + - "Changelog Check" - "Rust Check" - "Rust Lint" - "Rust Test Native (macOS-latest)" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d24da1319af..e100ead5af7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,3 +27,31 @@ jobs: run: npm install - name: Check Docs run: npx prettier --check . + + changelog-check: + name: Changelog Check + runs-on: ubuntu-latest + timeout-minutes: 10 + if: ${{ github.event_name == 'pull_request' }} + steps: + - name: Checkout Sources + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install Node + uses: actions/setup-node@v1 + with: + node-version: ${{ env.nodeVersion }} + - name: Install Dependencies + shell: bash + working-directory: tools/ci/nightly + run: | + npm install + - name: Check If Changelog Was Modified + shell: bash + run: | + node tools/ci/check-changelog.js RELEASES.md ${{ github.event.pull_request.base.sha }} + - name: Verify That Changelog Format Is Correct + shell: bash + run: | + node tools/ci/nightly/extract-release-notes.js RELEASES.md release_notes.md diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000000..53696cdb7b3 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,554 @@ +# TODO we may consider merging this with `release.yml` as there is quite an overlap +name: Nightly Release CI + +on: + push: + branches: + - main + schedule: + - cron: "0 4 * * 2-6" # 4am (UTC) from Tuesday to Saturday (i.e. after every workday) + +env: + # Please ensure that this is in sync with graalVersion in build.sbt + graalVersion: 21.0.0.2 + # Please ensure that this is in sync with javaVersion in build.sbt + javaVersion: 11 + # Please ensure that this is in sync with project/build.properties + sbtVersion: 1.4.9 + # Please ensure that this is in sync with rustVersion in build.sbt + rustToolchain: nightly-2019-11-04 + +concurrency: "releases" + +jobs: + preflight-check: + name: Nightly Preflight Check + runs-on: ubuntu-18.04 + timeout-minutes: 10 + if: + "${{ github.event_name == 'schedule' || + contains(github.event.head_commit.message,'[release: nightly]') }}" + outputs: + proceed: ${{ steps.preparations.outputs.proceed }} + nightly-version: ${{ steps.preparations.outputs.nightly-version }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - id: preparations + name: Check If The Build Should Proceed And Prepare Version String + shell: bash + working-directory: tools/ci/nightly + env: + GITHUB_TOKEN: ${{ github.token }} + # The script below sets an output 'proceed' to true or false depending on whether the nightly build should proceed. + # Nightly builds are skipped if no new changes are present since the last one. + run: | + npm install + node preflight-check.js ${{ github.sha }} + + # This job should be kept up-to-date with release.yml#build (but keep the relevant changes) + # The difference is the version bump which modifies the version in build.sbt to the output of the preflight check. + # It should be done before any actual SBT builds steps. + build: + name: Build + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + strategy: + matrix: + os: [macOS-latest, ubuntu-18.04, windows-latest] + fail-fast: true + needs: + - preflight-check + if: ${{ needs.preflight-check.outputs.proceed == 'true' }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + path: repo + - name: Enable Developer Command Prompt (Windows) + uses: ilammy/msvc-dev-cmd@v1.5.0 + - name: Disable TCP/UDP Offloading (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + sudo sysctl -w net.link.generic.system.hwcksum_tx=0 + sudo sysctl -w net.link.generic.system.hwcksum_rx=0 + - name: Disable TCP/UDP Offloading (Linux) + if: runner.os == 'Linux' + shell: bash + run: sudo ethtool -K eth0 tx off rx off + - name: Disable TCP/UDP Offloading (Windows) + if: runner.os == 'Windows' + shell: powershell + run: > + Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 + -UdpIPv6 + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: ${{ env.rustToolchain }} + override: true + - name: Setup conda + uses: s-weigand/setup-conda@v1.0.5 + with: + update-conda: true + conda-channels: anaconda, conda-forge + - name: Setup Conda Environment on Windows + if: runner.os == 'Windows' + run: | + conda create --name enso + conda init powershell + - name: Activate Conda Environment on Windows + if: runner.os == 'Windows' + run: conda activate enso + - name: Install FlatBuffers Compiler + run: conda install flatbuffers=1.12.0 + - name: Setup GraalVM Environment + uses: ayltai/setup-graalvm@v1 + with: + graalvm-version: ${{ env.graalVersion }} + java-version: ${{ env.javaVersion }} + native-image: true + - name: Set Up SBT + shell: bash + run: | + curl -fsSL -o sbt.tgz https://github.com/sbt/sbt/releases/download/v${{env.sbtVersion}}/sbt-${{env.sbtVersion}}.tgz + tar -xzf sbt.tgz + echo $GITHUB_WORKSPACE/sbt/bin/ >> $GITHUB_PATH + + # Caches + - name: Cache SBT + uses: actions/cache@v2 + with: + path: | + ~/.sbt + ~/.ivy2/cache + ~/.cache + key: ${{ runner.os }}-sbt-${{ hashFiles('**build.sbt') }} + restore-keys: ${{ runner.os }}-sbt- + + # Bootstrap + - name: Prepare Environment + shell: bash + run: | + echo "ENSO_RELEASE_MODE=true" >> $GITHUB_ENV + echo "ENSO_VERSION=${{ needs.preflight-check.outputs.nightly-version }}" >> $GITHUB_ENV + + - name: Update the Version Number to the Nightly + working-directory: repo + shell: bash + run: | + node tools/ci/nightly/bump-version.js $ENSO_VERSION + + - name: Bootstrap the Project + working-directory: repo + shell: bash + run: | + sleep 1 + sbt --no-colors bootstrap + + # Verify Legal Review + - name: Verify Packages + if: runner.os != 'Windows' # TODO [RW] CRLF handling in licenses task + working-directory: repo + shell: bash + run: | + sleep 1 + sbt --no-colors verifyLicensePackages + + # Prepare distributions + - name: Build the Launcher Native Image + working-directory: repo + shell: bash + run: | + sleep 1 + sbt --no-colors launcher/buildNativeImage + + - name: Prepare Distribution Version (Unix) + working-directory: repo + if: runner.os != 'Windows' + shell: bash + run: | + chmod +x enso + DIST_VERSION=$(./enso version --json --only-launcher | jq -r '.version') + echo "DIST_VERSION=$DIST_VERSION" >> $GITHUB_ENV + + - name: Prepare Distribution Version (Windows) + working-directory: repo + if: runner.os == 'Windows' + shell: bash + run: | + DIST_VERSION=$(./enso.exe version --json --only-launcher | jq -r '.version') + echo "DIST_VERSION=$DIST_VERSION" >> $GITHUB_ENV + + # Currently the only architecture supported by Github runners is amd64 + - name: Prepare Distribution Environment + working-directory: repo + shell: bash + run: > + GRAAL_VERSION=$(echo ${{ env.graalVersion }}) DIST_OS=$(echo + ${{runner.os }} | awk '{print tolower($0)}') bash + tools/ci/prepare-distribution-env.sh + + - name: Prepare Launcher Distribution + working-directory: repo + shell: bash + run: | + sleep 1 + sbt buildLauncherDistribution + - name: Prepare Engine Distribution + working-directory: repo + shell: bash + run: | + sleep 1 + sbt buildEngineDistribution + - name: Prepare Project Manager Distribution + working-directory: repo + shell: bash + run: | + sleep 1 + sbt buildProjectManagerDistribution + - name: Prepare GraalVM Distribution + working-directory: repo + shell: bash + run: | + sleep 1 + sbt buildGraalDistribution + + # Ensure that the versions encoded in the binary and in the release match + - name: Check Versions (Unix) + working-directory: repo + if: runner.os != 'Windows' + shell: bash + run: | + refversion=${{ env.ENSO_VERSION }} + binversion=${{ env.DIST_VERSION }} + engineversion=$(${{ env.ENGINE_DIST_DIR }}/bin/enso --version --json | jq -r '.version') + test $binversion = $refversion || (echo "Tag version $refversion and the launcher version $binversion do not match" && false) + test $engineversion = $refversion || (echo "Tag version $refversion and the engine version $engineversion do not match" && false) + - name: Check Versions (Windows) + working-directory: repo + if: runner.os == 'Windows' + shell: bash + run: | + refversion=${{ env.ENSO_VERSION }} + binversion=${{ env.DIST_VERSION }} + engineversion=$(${{ env.ENGINE_DIST_DIR }}/bin/enso.bat --version --json | jq -r '.version') + test $binversion = $refversion || (echo "Tag version $refversion and the launcher version $binversion do not match" && false) + test $engineversion = $refversion || (echo "Tag version $refversion and the engine version $engineversion do not match" && false) + + # Verify License Packages in Distributions + - name: Verify Distributed Licenses Package + working-directory: repo + if: runner.os != 'Windows' # TODO [RW] CRLF handling in licenses task + shell: bash + run: | + sleep 1 + sbt "enso/verifyGeneratedPackage engine ${{ env.ENGINE_DIST_DIR }}/THIRD-PARTY" + sbt "enso/verifyGeneratedPackage launcher ${{ env.LAUNCHER_DIST_DIR }}/THIRD-PARTY" + sbt "enso/verifyGeneratedPackage project-manager ${{ env.PROJECTMANAGER_DIST_DIR }}/THIRD-PARTY" + sbt "enso/verifyGeneratedPackage Standard ${{ env.ENGINE_DIST_DIR }}/std-lib/Standard/THIRD-PARTY" + + # Publish + - name: Upload the Engine Artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ env.ENGINE_DIST_NAME }} + path: repo/${{ env.ENGINE_DIST_ROOT }} + - name: Upload the Launcher Artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ env.LAUNCHER_DIST_NAME }} + path: repo/${{ env.LAUNCHER_DIST_ROOT }} + - name: Upload the Project Manager Artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ env.PROJECTMANAGER_DIST_NAME }} + path: repo/${{ env.PROJECTMANAGER_DIST_ROOT }} + - name: Upload the GraalVM Artifact + uses: actions/upload-artifact@v2 + with: + name: ${{ env.GRAAL_DIST_NAME }} + path: repo/${{ env.GRAAL_DIST_ROOT }} + - name: Upload the Manifest Artifact + uses: actions/upload-artifact@v2 + with: + name: manifest + path: repo/${{ env.ENGINE_DIST_DIR }}/manifest.yaml + - name: Upload the Launcher Manifest Artifact + uses: actions/upload-artifact@v2 + with: + name: launcher-manifest + path: repo/distribution/launcher-manifest.yaml + + create-release: + name: Prepare Release + runs-on: ubuntu-18.04 + needs: [build, preflight-check] + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + path: repo + # Without specifying options, it downloads all artifacts + - uses: actions/download-artifact@v2 + with: + path: repo/built-distribution + - name: Prepare Nodejs + shell: bash + working-directory: repo/tools/ci/nightly + run: npm install + + # This jobs can be used to debug errors, it may be removed + - name: Display Structure of Downloaded Files + run: ls + working-directory: repo/built-distribution + + - name: Setup GraalVM Environment + uses: ayltai/setup-graalvm@v1 + with: + graalvm-version: ${{ env.graalVersion }} + java-version: ${{ env.javaVersion }} + native-image: true + - name: Set Up SBT + shell: bash + run: | + curl -fsSL -o sbt.tgz https://github.com/sbt/sbt/releases/download/v${{env.sbtVersion}}/sbt-${{env.sbtVersion}}.tgz + tar -xzf sbt.tgz + echo $GITHUB_WORKSPACE/sbt/bin/ >> $GITHUB_PATH + + # Caches + - name: Cache SBT + uses: actions/cache@v2 + with: + path: | + ~/.sbt + ~/.ivy2/cache + ~/.cache + key: ${{ runner.os }}-sbt-${{ hashFiles('**build.sbt') }} + restore-keys: ${{ runner.os }}-sbt- + + - name: Save Version to Environment + shell: bash + run: | + DIST_VERSION=${{ needs.preflight-check.outputs.nightly-version }} + echo "Preparing release for $DIST_VERSION" + echo "DIST_VERSION=$DIST_VERSION" >> $GITHUB_ENV + - name: Update the Version Number to the Nightly + working-directory: repo + shell: bash + run: | + node tools/ci/nightly/bump-version.js $DIST_VERSION + + - name: Prepare Packages + shell: bash + working-directory: repo + run: | + sleep 1 + sbt makePackages + + - name: Prepare Bundles + shell: bash + working-directory: repo + run: | + sleep 1 + sbt makeBundles + + - name: Prepare Release Notes + working-directory: repo + shell: bash + run: | + node tools/ci/nightly/extract-release-notes.js RELEASES.md release_notes.md + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: enso-${{ env.DIST_VERSION }} + release_name: Enso Nightly ${{ env.DIST_VERSION }} + body_path: repo/release_notes.md + draft: true + prerelease: true + + # Upload the assets to the created release + - name: Publish the Engine (Linux) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-engine-${{ env.DIST_VERSION + }}-linux-amd64.tar.gz + asset_name: enso-engine-${{ env.DIST_VERSION }}-linux-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Engine (MacOS) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-engine-${{ env.DIST_VERSION + }}-macos-amd64.tar.gz + asset_name: enso-engine-${{ env.DIST_VERSION }}-macos-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Engine (Windows) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-engine-${{ env.DIST_VERSION + }}-windows-amd64.zip + asset_name: enso-engine-${{ env.DIST_VERSION }}-windows-amd64.zip + asset_content_type: application/zip + + - name: Publish the Launcher (Linux) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-launcher-${{ env.DIST_VERSION + }}-linux-amd64.tar.gz + asset_name: enso-launcher-${{ env.DIST_VERSION }}-linux-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Launcher (MacOS) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-launcher-${{ env.DIST_VERSION + }}-macos-amd64.tar.gz + asset_name: enso-launcher-${{ env.DIST_VERSION }}-macos-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Launcher (Windows) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-launcher-${{ env.DIST_VERSION + }}-windows-amd64.zip + asset_name: enso-launcher-${{ env.DIST_VERSION }}-windows-amd64.zip + asset_content_type: application/zip + + - name: Publish the Project Manager (Linux) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-project-manager-${{ env.DIST_VERSION + }}-linux-amd64.tar.gz + asset_name: + enso-project-manager-${{ env.DIST_VERSION }}-linux-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Project Manager (MacOS) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-project-manager-${{ env.DIST_VERSION + }}-macos-amd64.tar.gz + asset_name: + enso-project-manager-${{ env.DIST_VERSION }}-macos-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Project Manager (Windows) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-project-manager-${{ env.DIST_VERSION + }}-windows-amd64.zip + asset_name: + enso-project-manager-${{ env.DIST_VERSION }}-windows-amd64.zip + asset_content_type: application/zip + + - name: Publish the Bundle (Linux) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-bundle-${{ env.DIST_VERSION + }}-linux-amd64.tar.gz + asset_name: enso-bundle-${{ env.DIST_VERSION }}-linux-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Bundle (MacOS) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-bundle-${{ env.DIST_VERSION + }}-macos-amd64.tar.gz + asset_name: enso-bundle-${{ env.DIST_VERSION }}-macos-amd64.tar.gz + asset_content_type: application/x-tar + - name: Publish the Bundle (Windows) + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: + repo/built-distribution/enso-bundle-${{ env.DIST_VERSION + }}-windows-amd64.zip + asset_name: enso-bundle-${{ env.DIST_VERSION }}-windows-amd64.zip + asset_content_type: application/zip + + - name: Publish the Manifest + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: repo/built-distribution/manifest/manifest.yaml + asset_name: manifest.yaml + asset_content_type: application/yaml + - name: Publish the Launcher Manifest + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: repo/built-distribution/launcher-manifest/launcher-manifest.yaml + asset_name: launcher-manifest.yaml + asset_content_type: application/yaml + - name: Publish Release + shell: bash + working-directory: repo/tools/ci/nightly + run: node publish-release.js ${{ steps.create_release.outputs.id }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # TODO enable this once IDE supports triggers + # - name: Trigger IDE Build + # shell: bash + # working-directory: repo/tools/ci/nightly + # run: node trigger-workflow.js ide gui-ci develop + # env: + # GITHUB_TOKEN: ${{ github.token }} + - uses: dev-drprasad/delete-older-releases@v0.2.0 + name: Remove Old Releases + with: + keep_latest: 3 + delete_tag_pattern: SNAPSHOT + delete_tags: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbe5052cb45..286e24b54b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,8 @@ env: # Please ensure that this is in sync with rustVersion in build.sbt rustToolchain: nightly-2019-11-04 +concurrency: "releases" + jobs: # This job should be kept up-to-date with scala.yml#build (but keep the added version check) build: @@ -253,11 +255,6 @@ jobs: with: path: repo/built-distribution - # This jobs can be used to debug errors, it may be removed - - name: Display Structure of Downloaded Files - run: ls -R - working-directory: repo/built-distribution - - name: Setup GraalVM Environment uses: ayltai/setup-graalvm@v1 with: diff --git a/RELEASES.md b/RELEASES.md index 18abd6f9df8..05b16786961 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,14 @@ +# Enso Next + +## Miscellaneous + +- Adding a pipeline for automatic nightly builds - during the night after each + workday any new changes to the `main` branch are built and released as a + nightly builds. The nightly builds can be useful to preview in-development + features, but they should not be relied on, as they are not considered stable + and only the 3 latest nightly builds are kept, so the nightly versions become + obsolete very fast. ([#1689](https://github.com/enso-org/enso/pull/1689)). + # Enso 0.2.11 (2021-04-28) ## Tooling diff --git a/docs/distribution/README.md b/docs/distribution/README.md index d2d5841fa3d..cb3e4046952 100644 --- a/docs/distribution/README.md +++ b/docs/distribution/README.md @@ -31,3 +31,5 @@ dependencies, and Enso projects for use by our users. standard libraries for Enso. - [**Bundles**](bundles.md) An explanation of distributed bundles that contain all components necessary to run Enso out of the box. +- [**Nightly**](nightly.md) Description of the infrastructure related to the + nightly builds. diff --git a/docs/distribution/nightly.md b/docs/distribution/nightly.md new file mode 100644 index 00000000000..8e014efec21 --- /dev/null +++ b/docs/distribution/nightly.md @@ -0,0 +1,83 @@ +--- +layout: developer-doc +title: Nightly Builds +category: distribution +tags: [distribution, release, nightly] +order: 10 +--- + +# Nightly Builds + +This document describes the infrastructure for Enso's automated nightly builds. + + + +- [Triggering the Build](#triggering-the-build) +- [Nightly Build Versions](#nightly-build-versions) +- [Release Notes](#release-notes) + + + +## Triggering the Build + +The build can be triggered by two possible events: + +- automatically, at 4am UTC after each working day (that is, on Tuesday to + Saturday), +- manually, if a commit with message containing `[release: nightly]` is pushed + to the `main` branch. + +The nightly build is based off of the state of the `main` branch at the moment +when it was triggered. + +However, when a nightly build is triggered (by any of the two above conditions), +it will only proceed if there are any changes. That is, if the current commit is +the same as the one used for the previous nightly build, the build will not +proceed because there are no changes. + +Thanks to +[GitHub's concurrency settings](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#concurrency), +only one nightly or regular release job may be running at a given time. If a +build is triggered while another one is in progress, it will be pending until +the first one finishes. + +## Nightly Build Versions + +The nightly build will modify the version (which is set in `build.sbt`) to +indicate that this is a specific nightly build. + +If the version does not include a `SNAPSHOT` suffix, it is added. Then the +current date is appended. Moreover, if the build is not the first nightly build +to be done on a given day, an increasing numeric suffix is appended, to +differentiate between builds from a single day. + +For example, if `build.sbt` specifies the version to be `1.2.3` or +`1.2.3-SNAPSHOT`, the nightly build based off of that version, triggered on 1st +of February 2021 will result in version `1.2.3-SNAPSHOT.2021-02-01`. If a +subsequent build based off of the same version is triggered on the same day it +will be `1.2.3-SNAPSHOT.2021-02-01.1` etc. + +Only the 3 most recent nightly builds are kept in the repository, any older +builds are removed from the releases page and their corresponding tags are also +removed. + +## Release Notes + +Each PR should update the first, `Enso Next`, section in `RELEASES.md`. These +changes will be later moved to the specific section for the full release. This +section is also used to fill the release notes for the nightly builds. + +Most PRs should update the release notes, so there is a PR check that ensures +the file was modified. However in some situations there is no need to update the +notes. If any commit included as part of a PR includes `[no-changelog]` within +its message, that check is ignored. + +The changelog should keep consistent formatting: + +- each version should be delimited by a top-level section (`#` in Markdown), +- the first section should always be called `Enso Next`, +- all subsequent sections should be called `Enso ()`. + +A heuristic check is ran for PRs, checking that at least the first two sections +satisfy the above requirements - which is necessary for the nightly build +pipeline to be able to correctly infer the release notes. diff --git a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala index 6ca723c240e..ac09f4194fe 100644 --- a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala +++ b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala @@ -2,7 +2,6 @@ package org.enso.launcher.components import java.nio.file.{Files, Path} import java.util.UUID - import akka.http.scaladsl.model.Uri import nl.gn0s1s.bump.SemVer import org.enso.runtimeversionmanager.FileSystem.PathSyntax @@ -10,7 +9,7 @@ import org.enso.runtimeversionmanager.config.GlobalConfigurationManager import org.enso.runtimeversionmanager.runner._ import org.enso.runtimeversionmanager.test.RuntimeVersionManagerTest import org.enso.launcher.project.ProjectManager -import org.enso.loggingservice.LogLevel +import org.enso.loggingservice.{LogLevel, TestLogger} import scala.concurrent.Future @@ -138,6 +137,31 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest { commandLine should include(s"--new-project-author-email $authorEmail") } + "warn when creating a project using a nightly version" in { + val runner = makeFakeRunner() + val projectPath = getTestDirectory / "project2" + val nightlyVersion = SemVer(0, 0, 0, Some("SNAPSHOT.2000-01-01")) + val logs = TestLogger.gatherLogs { + runner + .newProject( + path = projectPath, + name = "ProjectName2", + engineVersion = nightlyVersion, + authorName = None, + authorEmail = None, + additionalArguments = Seq() + ) + .get + } + assert( + logs.exists(msg => + msg.logLevel == LogLevel.Warning && msg.message.contains( + "Consider using a stable version." + ) + ) + ) + } + "run repl with default version and additional arguments" in { val runner = makeFakeRunner() val runSettings = runner diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceSetupHelper.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceSetupHelper.scala index 738845bf245..564fcb1ce34 100644 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceSetupHelper.scala +++ b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/LoggingServiceSetupHelper.scala @@ -180,10 +180,20 @@ abstract class LoggingServiceSetupHelper(implicit } case Success(serverBinding) => val uri = serverBinding.toUri() - loggingServiceEndpointPromise.success(Some(uri)) - logger.trace( - s"Logging service has been set-up and is listening at `$uri`." - ) + try { + loggingServiceEndpointPromise.success(Some(uri)) + logger.trace( + s"Logging service has been set-up and is listening at `$uri`." + ) + } catch { + case _: IllegalStateException => + val earlierValue = loggingServiceEndpointPromise.future.value + logger.warn( + s"The logging service has been set-up at `$uri`, but the " + + s"logging URI has been initialized before that to " + + s"$earlierValue." + ) + } } } @@ -214,10 +224,20 @@ abstract class LoggingServiceSetupHelper(implicit loggingServiceEndpointPromise.trySuccess(None) case Success(connected) => if (connected) { - loggingServiceEndpointPromise.success(Some(uri)) - System.err.println( - s"Log messages are forwarded to `$uri`." - ) + try { + loggingServiceEndpointPromise.success(Some(uri)) + System.err.println( + s"Log messages are forwarded to `$uri`." + ) + } catch { + case _: IllegalStateException => + val earlierValue = loggingServiceEndpointPromise.future.value + logger.warn( + s"The logging service has been set-up at `$uri`, but the " + + s"logging URI has been initialized before that to " + + s"$earlierValue." + ) + } } else { loggingServiceEndpointPromise.trySuccess(None) } diff --git a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/TestLogger.scala b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/TestLogger.scala index 56f86f0d103..e05b8ad27df 100644 --- a/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/TestLogger.scala +++ b/lib/scala/logging-service/src/main/scala/org/enso/loggingservice/TestLogger.scala @@ -36,6 +36,7 @@ object TestLogger { ) Await.ready(future, 1.second) action + Thread.sleep(100) LoggingServiceManager.tearDown() printer.getLoggedMessages } diff --git a/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManagerSpec.scala b/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManagerSpec.scala index 269dbf7ddee..0ac83299d15 100644 --- a/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManagerSpec.scala +++ b/lib/scala/runtime-version-manager-test/src/test/scala/org/enso/runtimeversionmanager/components/RuntimeVersionManagerSpec.scala @@ -1,10 +1,10 @@ package org.enso.runtimeversionmanager.components import java.nio.file.{Files, Path} - import nl.gn0s1s.bump.SemVer import org.enso.runtimeversionmanager.FileSystem.PathSyntax import org.enso.runtimeversionmanager.config.GlobalConfigurationManager +import org.enso.runtimeversionmanager.releases.ReleaseNotFound import org.enso.runtimeversionmanager.test.{ RuntimeVersionManagerTest, TestRuntimeVersionManagementUserInterface @@ -112,6 +112,15 @@ class RuntimeVersionManagerSpec extends RuntimeVersionManagerTest with OsSpec { componentsManager.findEngine(brokenVersion).value } + "issue a context-specific error when a nightly release cannot be found" in { + val componentsManager = makeManagers()._2 + val nightlyVersion = SemVer(0, 0, 0, Some("SNAPSHOT.2000-01-01")) + val exception = intercept[ReleaseNotFound] { + componentsManager.findOrInstallEngine(nightlyVersion) + } + exception.getMessage should include("Nightly releases expire") + } + "uninstall the runtime iff it is not used by any engines" in { val componentsManager = makeRuntimeVersionManager() val engineVersions = diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Engine.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Engine.scala index d777487201a..61c284b356c 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Engine.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/components/Engine.scala @@ -67,4 +67,26 @@ case class Engine(version: SemVer, path: Path, manifest: Manifest) { * installed release and issue a warning when they are used. */ def isMarkedBroken: Boolean = manifest.brokenMark + + /** Specifies if the engine version comes from a nightly release. + * + * See `docs/distribution/nightly.md` for more information. + */ + def isNightly: Boolean = Engine.isNightly(version) +} + +object Engine { + + /** The infix that should be included in the pre-release part of the semantic + * versioning string that describes the engine version to indicate that this + * is a prerelease. + */ + def nightlyInfix = "SNAPSHOT" + + /** Specifies if the engine version comes from a nightly release. + * + * See `docs/distribution/nightly.md` for more information. + */ + def isNightly(version: SemVer): Boolean = + version.preRelease.exists(_.contains(nightlyInfix)) } diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/ReleaseProviderException.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/ReleaseProviderException.scala index 0e4b883931e..34eccc58dde 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/ReleaseProviderException.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/ReleaseProviderException.scala @@ -1,12 +1,35 @@ package org.enso.runtimeversionmanager.releases -/** Indicates a release provider failure. - */ -case class ReleaseProviderException(message: String, cause: Throwable = null) +/** Indicates a release provider failure. */ +sealed class ReleaseProviderException(message: String, cause: Throwable) extends RuntimeException(message, cause) { - /** @inheritdoc - */ + /** @inheritdoc */ override def toString: String = s"A problem occurred when trying to find the release: $message" } + +object ReleaseProviderException { + + /** Creates a release provider exception with a given message and optional + * cause. + */ + def apply( + message: String, + cause: Throwable = null + ): ReleaseProviderException = new ReleaseProviderException(message, cause) +} + +/** Indicates that the specific release could not be found. + * + * @param tag the tag of the release + * @param cause (optional) an exception that has caused this error + */ +case class ReleaseNotFound( + tag: String, + message: Option[String] = None, + cause: Throwable = null +) extends ReleaseProviderException( + message.getOrElse(s"Cannot find release `$tag`."), + cause + ) diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/engine/EngineReleaseProvider.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/engine/EngineReleaseProvider.scala index 35dd533500a..60f777a60c7 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/engine/EngineReleaseProvider.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/engine/EngineReleaseProvider.scala @@ -1,13 +1,13 @@ package org.enso.runtimeversionmanager.releases.engine import java.nio.file.Path - import nl.gn0s1s.bump.SemVer import org.enso.cli.task.TaskProgress -import org.enso.runtimeversionmanager.components.Manifest +import org.enso.runtimeversionmanager.components.{Engine, Manifest} import org.enso.runtimeversionmanager.releases.{ EnsoReleaseProvider, Release, + ReleaseNotFound, ReleaseProviderException, SimpleReleaseProvider } @@ -22,7 +22,7 @@ class EngineReleaseProvider(releaseProvider: SimpleReleaseProvider) def fetchRelease(version: SemVer): Try[EngineRelease] = { val tag = tagPrefix + version.toString for { - release <- releaseProvider.releaseForTag(tag) + release <- wrapFetchError(releaseProvider.releaseForTag(tag)) manifestAsset <- release.assets .find(_.fileName == Manifest.DEFAULT_MANIFEST_NAME) @@ -53,6 +53,26 @@ class EngineReleaseProvider(releaseProvider: SimpleReleaseProvider) ) } + /** Intercepts the `ReleaseNotFound` and injects a message regarding nightly + * releases if applicable. + */ + private def wrapFetchError[A](result: Try[A]): Try[A] = result.recoverWith { + case ReleaseNotFound(tag, _, cause) if tag.contains(Engine.nightlyInfix) => + // TODO [RW] explain how to upgrade using the upgrade command once its + // implemented (#1717) + Failure( + ReleaseNotFound( + tag, + Some( + s"Cannot find release `$tag`. Nightly releases expire after some " + + s"time. Consider upgrading to a stable release or a newer " + + s"nightly build." + ), + cause + ) + ) + } + private case class DefaultEngineRelease( version: SemVer, manifest: Manifest, diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/github/GithubAPI.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/github/GithubAPI.scala index 9d1b8eb74ed..94ed977a83b 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/github/GithubAPI.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/github/GithubAPI.scala @@ -1,7 +1,6 @@ package org.enso.runtimeversionmanager.releases.github import java.nio.file.Path - import io.circe._ import io.circe.parser._ import org.enso.cli.task.TaskProgress @@ -12,7 +11,10 @@ import org.enso.runtimeversionmanager.http.{ Header, URIBuilder } -import org.enso.runtimeversionmanager.releases.ReleaseProviderException +import org.enso.runtimeversionmanager.releases.{ + ReleaseNotFound, + ReleaseProviderException +} import scala.util.{Success, Try} @@ -107,7 +109,7 @@ object GithubAPI { .map(err => handleError( response, - ReleaseProviderException(s"Cannot find release `$tag`.", err) + ReleaseNotFound(tag, cause = err) ) ) .toTry diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala index f066cd70d6a..7a4b6cd8e91 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/releases/testing/FakeReleaseProvider.scala @@ -1,14 +1,13 @@ package org.enso.runtimeversionmanager.releases.testing import java.nio.file.{Files, Path, StandardCopyOption} - import org.enso.cli.task.{ProgressListener, TaskProgress} import org.enso.runtimeversionmanager.FileSystem import org.enso.runtimeversionmanager.locking.{LockManager, LockType} import org.enso.runtimeversionmanager.releases.{ Asset, Release, - ReleaseProviderException, + ReleaseNotFound, SimpleReleaseProvider } @@ -39,7 +38,7 @@ case class FakeReleaseProvider( override def releaseForTag(tag: String): Try[Release] = releases .find(_.tag == tag) - .toRight(ReleaseProviderException(s"Release $tag does not exist.")) + .toRight(ReleaseNotFound(tag)) .toTry /** @inheritdoc */ diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala index cb9c7fdc5df..215915dbe8b 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala @@ -1,7 +1,6 @@ package org.enso.runtimeversionmanager.runner import java.nio.file.Path - import akka.http.scaladsl.model.Uri import com.typesafe.scalalogging.Logger import nl.gn0s1s.bump.SemVer @@ -55,6 +54,14 @@ class Runner( "--new-project-name", name ) ++ authorNameOption ++ authorEmailOption ++ additionalArguments + // TODO [RW] reporting warnings to the IDE (#1710) + if (Engine.isNightly(engineVersion)) { + Logger[Runner].warn( + s"Creating a new project using a nightly build ($engineVersion). " + + "Nightly builds may disappear after a while, so you may need to " + + "upgrade. Consider using a stable version." + ) + } RunSettings(engineVersion, arguments, connectLoggerIfAvailable = false) } diff --git a/tools/ci/check-changelog.js b/tools/ci/check-changelog.js new file mode 100644 index 00000000000..c3f1f3a5f0a --- /dev/null +++ b/tools/ci/check-changelog.js @@ -0,0 +1,63 @@ +const fs = require("fs"); +const proc = require("child_process"); + +const skipChangelogInfix = "[no-changelog]"; +const changelogPath = process.argv[2]; +const baseRef = process.argv[3]; + +/// Runs the git command with the provided arguments. +function runGit(args) { + const result = proc.spawnSync("git", args); + if (result.error) { + console.log("Cannot access git", result.error); + process.exit(1); + } + return result; +} + +/** Checks if the changelog file was changed in any commits that are part of the + * PR. + */ +function wasChangelogModified() { + const diffArgs = [ + "--no-pager", + "diff", + "--exit-code", + baseRef, + "--", + changelogPath, + ]; + + const result = runGit(diffArgs); + const exitCode = result.status; + console.log(result.stdout.toString("utf-8")); + const noDifference = exitCode == 0; + return !noDifference; +} + +/// Checks if any commit has overridden the changelog check. +function isChangelogSkipped() { + const logArgs = ["--no-pager", "log", "HEAD~3...HEAD", "--pretty=oneline"]; + const result = runGit(logArgs); + + const output = result.stdout.toString("utf-8"); + const containsSkipCommit = output.indexOf(skipChangelogInfix) >= 0; + return containsSkipCommit; +} + +if (wasChangelogModified()) { + console.log("Changelog was changed"); + process.exit(0); +} else { + console.log("No changes to the changelog"); + if (isChangelogSkipped()) { + console.log( + "But one of the commits within the PR includes " + + skipChangelogInfix + + ", so the check is skipped." + ); + process.exit(0); + } else { + process.exit(1); + } +} diff --git a/tools/ci/nightly/bump-version.js b/tools/ci/nightly/bump-version.js new file mode 100644 index 00000000000..6636dfaf296 --- /dev/null +++ b/tools/ci/nightly/bump-version.js @@ -0,0 +1,13 @@ +const fs = require("fs"); + +const path = "build.sbt"; +const version = process.argv[2]; + +const content = fs.readFileSync(path, { encoding: "utf-8" }); +const updated = content.replace( + /val ensoVersion.*= ".*"/g, + 'val ensoVersion = "' + version + '"' +); +fs.writeFileSync(path, updated); + +console.log("Updated build version to " + version); diff --git a/tools/ci/nightly/extract-release-notes.js b/tools/ci/nightly/extract-release-notes.js new file mode 100644 index 00000000000..e0b94c7869c --- /dev/null +++ b/tools/ci/nightly/extract-release-notes.js @@ -0,0 +1,59 @@ +const fs = require("fs"); + +const inputPath = process.argv[2]; +const outputPath = process.argv[3]; + +console.log("Extracting release notes from " + inputPath + " to " + outputPath); + +/** Returns the part of the text until the second top-level heading (exclusive) + * in Markdown formatting. + */ +function cutFirstSection(content) { + const nightlySectionRegex = /^# Enso Next$/gm; + function findNightlySectionStart(text) { + return text.search(nightlySectionRegex); + } + const regularSectionRegex = /^# Enso .*? \(\d\d\d\d-\d\d-\d\d\)$/gm; + function findFirstRegularSectionStart(text) { + return text.search(regularSectionRegex); + } + function findNewline(text) { + return text.indexOf("\n"); + } + + const firstHeading = findNightlySectionStart(content); + if (firstHeading < 0) { + throw "Could not find the nightly section, matching " + nightlySectionRegex; + } + + const restOffset = firstHeading + 2; + const newLineOffset = findNewline(content.substring(restOffset)); + if (newLineOffset < 0) { + throw "No content after the section heading"; + } + const restStart = restOffset + newLineOffset + 1; + + const rest = content.substring(restStart); + const secondHeading = findFirstRegularSectionStart(rest); + if (secondHeading < 0) { + throw ( + "Could not find the first released section, matching" + + regularSectionRegex + ); + } + + const firstSectionContent = rest.substring(0, secondHeading); + return firstSectionContent; +} + +try { + const content = fs.readFileSync(inputPath, { encoding: "utf-8" }); + const nightlyPart = cutFirstSection(content); + fs.writeFileSync(outputPath, nightlyPart); + + console.log("Created " + outputPath + " with the following content:"); + console.log(nightlyPart); +} catch (exc) { + console.error(exc); + process.exit(1); +} diff --git a/tools/ci/nightly/github.js b/tools/ci/nightly/github.js new file mode 100644 index 00000000000..b40e201d635 --- /dev/null +++ b/tools/ci/nightly/github.js @@ -0,0 +1,73 @@ +const { Octokit } = require("@octokit/core"); + +const organization = "enso-org"; +function determineRepositoryName() { + const fallback = "enso"; + const fallbackMessage = + "Could not determine the repository name, falling back to the default."; + const fullName = process.env.GITHUB_REPOSITORY; + if (!fullName) { + console.log(fallbackMessage); + return fallback; + } + + const prefix = organization + "/"; + if (fullName.startsWith(prefix)) { + return fullName.substring(prefix.length); + } else { + console.log(fallbackMessage); + return fallback; + } +} + +const repo = determineRepositoryName(); +const token = process.env.GITHUB_TOKEN; +const octokit = new Octokit({ auth: token }); + +function isNightly(release) { + const nightlyInfix = "Nightly"; + return release.name.indexOf(nightlyInfix) >= 0 && !release.draft; +} + +async function fetchAllReleases() { + const res = await octokit.request("GET /repos/{owner}/{repo}/releases", { + owner: organization, + repo: repo, + }); + return res.data; +} + +async function fetchNightlies() { + const releases = await fetchAllReleases(); + const nightlies = releases.filter(isNightly); + return nightlies; +} + +async function triggerWorkflow(repo, workflow_id, ref) { + await octokit.request( + "POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", + { + owner: organization, + repo: repo, + workflow_id: workflow_id, + ref: ref, + } + ); +} + +async function publishRelease(id) { + return await octokit.request( + "PATCH /repos/{owner}/{repo}/releases/{release_id}", + { + owner: organization, + repo: repo, + release_id: id, + draft: false, + } + ); +} + +exports.fetchAllReleases = fetchAllReleases; +exports.fetchNightlies = fetchNightlies; +exports.publishRelease = publishRelease; +exports.repository = repo; diff --git a/tools/ci/nightly/package.json b/tools/ci/nightly/package.json new file mode 100644 index 00000000000..865e39d39f8 --- /dev/null +++ b/tools/ci/nightly/package.json @@ -0,0 +1,14 @@ +{ + "name": "nightly", + "version": "1.0.0", + "description": "", + "main": "\"\"", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@octokit/core": "^3.4.0" + } +} diff --git a/tools/ci/nightly/preflight-check.js b/tools/ci/nightly/preflight-check.js new file mode 100644 index 00000000000..9bb967a85ea --- /dev/null +++ b/tools/ci/nightly/preflight-check.js @@ -0,0 +1,122 @@ +const fs = require("fs"); +const github = require("./github"); + +const currentHeadSha = process.argv[2]; +const buildConfigPath = "../../../build.sbt"; + +/// Returns the current date formatted as 'YYYY-mm-dd'. +function isoDate() { + const now = new Date(); + const year = "" + now.getFullYear(); + let month = "" + (now.getMonth() + 1); + let day = "" + now.getDate(); + if (month.length < 2) { + month = "0" + month; + } + if (day.length < 2) { + day = "0" + day; + } + return year + "-" + month + "-" + day; +} + +/// Sets the step output 'proceed'. +function setProceed(proceed) { + console.log("::set-output name=proceed::" + proceed); +} + +/// Sets the step output 'nightly-version'. +function setVersionString(name) { + console.log("::set-output name=nightly-version::" + name); +} + +/** Checks if there are any new changes to see if the nightly build should + * proceed. + */ +function checkProceed(nightlies) { + if (nightlies.length == 0) { + console.log( + "No prior nightly releases found. Proceeding with the first release." + ); + return true; + } + + const first = nightlies[0]; + const firstNightlySha = first.target_commitish; + if (firstNightlySha == currentHeadSha) { + console.log( + "Current commit (" + + currentHeadSha + + ") is the same as for the most recent nightly build. A new build is not needed." + ); + return false; + } else { + console.log( + "Current commit (" + + currentHeadSha + + ") is different from the most recent nightly build (" + + firstNightlySha + + "). Proceeding with a new nightly build." + ); + return true; + } +} + +/** Prepares a version string for the nightly build. + * + * A '-SNAPSHOT' suffix is added if it is not already present, next the current + * date is appended. If this is not the first nightly build on that date, an + * increasing numeric suffix is added. + */ +function prepareVersionString(nightlies) { + function isTaken(name) { + const tagName = "enso-" + name; + return nightlies.some((entry) => entry.tag_name == tagName); + } + + const content = fs.readFileSync(buildConfigPath, { encoding: "utf-8" }); + const match = content.match(/val ensoVersion += +"(.*)"/); + if (!match) { + console.error("Could not find the version string in configuration!"); + process.exit(1); + } + + const version = match[1]; + let baseName = version; + if (!baseName.endsWith("SNAPSHOT")) { + baseName += "-SNAPSHOT"; + } + + baseName += "." + isoDate(); + + function makeName(ix) { + if (ix == 0) { + return baseName; + } else { + return baseName + "." + ix; + } + } + + let ix = 0; + while (isTaken(makeName(ix))) { + ix++; + } + + const name = makeName(ix); + console.log("The build will be using version '" + name + "'"); + return name; +} + +async function main() { + const nightlies = await github.fetchNightlies(); + const shouldProceed = checkProceed(nightlies); + setProceed(shouldProceed); + if (shouldProceed) { + const versionString = prepareVersionString(nightlies); + setVersionString(versionString); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tools/ci/nightly/publish-release.js b/tools/ci/nightly/publish-release.js new file mode 100644 index 00000000000..c8edfb1b302 --- /dev/null +++ b/tools/ci/nightly/publish-release.js @@ -0,0 +1,14 @@ +const github = require("./github"); + +const releaseId = process.argv[2]; + +async function main() { + console.log("Making release " + releaseId + " public."); + await github.publishRelease(releaseId); + console.log("Done."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tools/ci/nightly/trigger-workflow.js b/tools/ci/nightly/trigger-workflow.js new file mode 100644 index 00000000000..067004b4aad --- /dev/null +++ b/tools/ci/nightly/trigger-workflow.js @@ -0,0 +1,18 @@ +const github = require("./github"); + +const repo = process.argv[2]; +const workflow_id = process.argv[3]; +const ref = process.argv[4]; + +async function main() { + console.log( + "Triggering workflow " + workflow_id + " in " + repo + " on " + ref + ); + await github.triggerWorkflow(repo, workflow_id, ref); + console.log("Done."); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});